Refactor Popover stores/contexts
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
export interface StateDefinition {
|
||||
// State
|
||||
popoverState: PopoverStates;
|
||||
button: HTMLElement | null;
|
||||
button: Writable<HTMLElement | null>;
|
||||
buttonId: string;
|
||||
panel: HTMLElement | null;
|
||||
panel: Writable<HTMLElement | null>;
|
||||
panelId: string;
|
||||
|
||||
// State mutators
|
||||
@@ -49,7 +49,7 @@
|
||||
FocusableMode,
|
||||
} from "$lib/utils/focus-management";
|
||||
import { State, useOpenClosedProvider } from "$lib/internal/open-closed";
|
||||
import type { PopoverGroupContext } from "./PopoverGroup.svelte";
|
||||
import { usePopoverGroupContext } from "./PopoverGroup.svelte";
|
||||
import { getContext, setContext, onMount } from "svelte";
|
||||
import { writable, Writable } from "svelte/store";
|
||||
import { ActionArray, useActions } from "$lib/hooks/use-actions";
|
||||
@@ -59,18 +59,15 @@
|
||||
|
||||
let popoverState: PopoverStates = PopoverStates.Closed;
|
||||
|
||||
let panel: Writable<StateDefinition["panel"]> = writable(null);
|
||||
setContext("PopoverPanelRef", panel);
|
||||
|
||||
let button: Writable<StateDefinition["button"]> = writable(null);
|
||||
setContext("PopoverButtonRef", button);
|
||||
let panel: StateDefinition["panel"] = writable(null);
|
||||
let button: StateDefinition["button"] = writable(null);
|
||||
|
||||
let api: Writable<StateDefinition> = writable({
|
||||
popoverState,
|
||||
buttonId,
|
||||
panelId,
|
||||
panel: $panel,
|
||||
button: $button,
|
||||
panel,
|
||||
button,
|
||||
togglePopover() {
|
||||
popoverState = match(popoverState, {
|
||||
[PopoverStates.Open]: PopoverStates.Closed,
|
||||
@@ -85,10 +82,10 @@
|
||||
$api.closePopover();
|
||||
|
||||
let restoreElement = (() => {
|
||||
if (!focusableElement) return $api.button;
|
||||
if (!focusableElement) return $button;
|
||||
if (focusableElement instanceof HTMLElement) return focusableElement;
|
||||
|
||||
return $api.button;
|
||||
return $button;
|
||||
})();
|
||||
|
||||
restoreElement?.focus();
|
||||
@@ -108,10 +105,6 @@
|
||||
return {
|
||||
...obj,
|
||||
popoverState,
|
||||
buttonId,
|
||||
panelId,
|
||||
panel: $panel,
|
||||
button: $button,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -123,8 +116,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
const groupContext: PopoverGroupContext | undefined =
|
||||
getContext("PopoverGroup");
|
||||
const groupContext = usePopoverGroupContext();
|
||||
const registerPopover = groupContext?.registerPopover;
|
||||
|
||||
function isFocusWithinPopoverGroup() {
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
Focus,
|
||||
focusIn,
|
||||
} from "$lib/utils/focus-management";
|
||||
import { getContext } from "svelte";
|
||||
import { writable, Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
import { PopoverStates, usePopoverContext } from "./Popover.svelte";
|
||||
import { usePopoverGroupContext } from "./PopoverGroup.svelte";
|
||||
import { usePopoverPanelContext } from "./PopoverPanel.svelte";
|
||||
import { ActionArray, useActions } from "$lib/hooks/use-actions";
|
||||
export let use: ActionArray = [];
|
||||
let buttonStore: Writable<HTMLButtonElement> = getContext("PopoverButtonRef");
|
||||
export let disabled: Boolean = false;
|
||||
let api = usePopoverContext("PopoverButton");
|
||||
|
||||
let apiButton = $api.button;
|
||||
let ourStore = apiButton;
|
||||
|
||||
let groupContext = usePopoverGroupContext();
|
||||
let closeOthers = groupContext?.closeOthers;
|
||||
|
||||
@@ -23,8 +24,9 @@
|
||||
let isWithinPanel =
|
||||
panelContext === null ? false : panelContext === $api.panelId;
|
||||
if (isWithinPanel) {
|
||||
buttonStore = writable();
|
||||
ourStore = writable();
|
||||
}
|
||||
let apiPanel = $api.panel;
|
||||
|
||||
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's
|
||||
let activeElementRef: Element | null = null;
|
||||
@@ -45,7 +47,7 @@
|
||||
event.preventDefault(); // Prevent triggering a *click* event
|
||||
event.stopPropagation();
|
||||
$api.closePopover();
|
||||
$api.button?.focus(); // Re-focus the original opening Button
|
||||
$apiButton?.focus(); // Re-focus the original opening Button
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -63,7 +65,7 @@
|
||||
if ($api.popoverState !== PopoverStates.Open)
|
||||
return closeOthers?.($api.buttonId);
|
||||
if (!$api.button) return;
|
||||
if (!$api.button?.contains(document.activeElement)) return;
|
||||
if (!$apiButton?.contains(document.activeElement)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$api.closePopover();
|
||||
@@ -71,33 +73,33 @@
|
||||
|
||||
case Keys.Tab:
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if (!$api.button) return;
|
||||
if (!$apiPanel) return;
|
||||
if (!$apiButton) return;
|
||||
|
||||
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's
|
||||
if (event.shiftKey) {
|
||||
// Check if the last focused element exists, and check that it is not inside button or panel itself
|
||||
if (!previousActiveElementRef) return;
|
||||
if ($api.button?.contains(previousActiveElementRef)) return;
|
||||
if ($api.panel?.contains(previousActiveElementRef)) return;
|
||||
if ($apiButton?.contains(previousActiveElementRef)) return;
|
||||
if ($apiPanel?.contains(previousActiveElementRef)) return;
|
||||
|
||||
// Check if the last focused element is *after* the button in the DOM
|
||||
let focusableElements = getFocusableElements();
|
||||
let previousIdx = focusableElements.indexOf(
|
||||
previousActiveElementRef as HTMLElement
|
||||
);
|
||||
let buttonIdx = focusableElements.indexOf($api.button!);
|
||||
let buttonIdx = focusableElements.indexOf($apiButton);
|
||||
if (buttonIdx > previousIdx) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
focusIn($api.panel!, Focus.Last);
|
||||
focusIn($apiPanel, Focus.Last);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
focusIn($api.panel!, Focus.First);
|
||||
focusIn($apiPanel, Focus.First);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -113,28 +115,28 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if (!$api.button) return;
|
||||
if (!$apiPanel) return;
|
||||
if (!$apiButton) return;
|
||||
|
||||
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's
|
||||
switch (event.key) {
|
||||
case Keys.Tab:
|
||||
// Check if the last focused element exists, and check that it is not inside button or panel itself
|
||||
if (!previousActiveElementRef) return;
|
||||
if ($api.button?.contains(previousActiveElementRef)) return;
|
||||
if ($api.panel?.contains(previousActiveElementRef)) return;
|
||||
if ($apiButton?.contains(previousActiveElementRef)) return;
|
||||
if ($apiPanel?.contains(previousActiveElementRef)) return;
|
||||
|
||||
// Check if the last focused element is *after* the button in the DOM
|
||||
let focusableElements = getFocusableElements();
|
||||
let previousIdx = focusableElements.indexOf(
|
||||
previousActiveElementRef as HTMLElement
|
||||
);
|
||||
let buttonIdx = focusableElements.indexOf($api.button!);
|
||||
let buttonIdx = focusableElements.indexOf($apiButton);
|
||||
if (buttonIdx > previousIdx) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
focusIn($api.panel!, Focus.Last);
|
||||
focusIn($apiPanel, Focus.Last);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -142,11 +144,11 @@
|
||||
if (disabled) return;
|
||||
if (isWithinPanel) {
|
||||
$api.closePopover();
|
||||
$api.button?.focus(); // Re-focus the original opening Button
|
||||
$apiButton?.focus(); // Re-focus the original opening Button
|
||||
} else {
|
||||
if ($api.popoverState === PopoverStates.Closed)
|
||||
closeOthers?.($api.buttonId);
|
||||
$api.button?.focus();
|
||||
$apiButton?.focus();
|
||||
$api.togglePopover();
|
||||
}
|
||||
}
|
||||
@@ -172,7 +174,7 @@
|
||||
on:click={handleClick}
|
||||
on:keydown={handleKeyDown}
|
||||
on:keyup={handleKeyUp}
|
||||
bind:this={$buttonStore}
|
||||
bind:this={$ourStore}
|
||||
>
|
||||
<slot open={$api.popoverState === PopoverStates.Open} />
|
||||
</button>
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
} from "./Popover.svelte";
|
||||
import { ActionArray, useActions } from "$lib/hooks/use-actions";
|
||||
export let use: ActionArray = [];
|
||||
let panelStore: SvelteStore<HTMLDivElement> = getContext("PopoverPanelRef");
|
||||
export let focus = false;
|
||||
|
||||
let api = usePopoverContext("PopoverPanel");
|
||||
setContext(POPOVER_PANEL_CONTEXT_NAME, $api.panelId);
|
||||
|
||||
let panelStore = $api.panel;
|
||||
let apiButton = $api.button;
|
||||
|
||||
let openClosedState = useOpenClosed();
|
||||
|
||||
$: visible =
|
||||
@@ -42,21 +44,21 @@
|
||||
$: (() => {
|
||||
if (!focus) return;
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if (!$panelStore) return;
|
||||
|
||||
let activeElement = document.activeElement as HTMLElement;
|
||||
if ($api.panel?.contains(activeElement)) return; // Already focused within Dialog
|
||||
if ($panelStore.contains(activeElement)) return; // Already focused within Dialog
|
||||
|
||||
focusIn($api.panel!, Focus.First);
|
||||
focusIn($panelStore, Focus.First);
|
||||
})();
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if (!$panelStore) return;
|
||||
|
||||
if (event.key !== Keys.Tab) return;
|
||||
if (!document.activeElement) return;
|
||||
if (!$api.panel?.contains(document.activeElement)) return;
|
||||
if (!$panelStore?.contains(document.activeElement)) return;
|
||||
|
||||
// We will take-over the default tab behaviour so that we have a bit
|
||||
// control over what is focused next. It will behave exactly the same,
|
||||
@@ -65,21 +67,21 @@
|
||||
event.preventDefault();
|
||||
|
||||
let result = focusIn(
|
||||
$api.panel!,
|
||||
$panelStore,
|
||||
event.shiftKey ? Focus.Previous : Focus.Next
|
||||
);
|
||||
|
||||
if (result === FocusResult.Underflow) {
|
||||
return $api.button?.focus();
|
||||
return $apiButton?.focus();
|
||||
} else if (result === FocusResult.Overflow) {
|
||||
if (!$api.button) return;
|
||||
if (!$apiButton) return;
|
||||
|
||||
let elements = getFocusableElements();
|
||||
let buttonIdx = elements.indexOf($api.button!);
|
||||
let buttonIdx = elements.indexOf($apiButton!);
|
||||
|
||||
let nextElements = elements
|
||||
.splice(buttonIdx + 1) // Elements after button
|
||||
.filter((element) => !$api.panel?.contains(element)); // Ignore items in panel
|
||||
.filter((element) => !$panelStore?.contains(element)); // Ignore items in panel
|
||||
|
||||
// Try to focus the next element, however it could fail if we are in a
|
||||
// Portal that happens to be the very last one in the DOM. In that
|
||||
@@ -95,8 +97,8 @@
|
||||
function handleFocus() {
|
||||
if (!focus) return;
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if ($api.panel?.contains(document.activeElement as HTMLElement)) return;
|
||||
if (!$panelStore) return;
|
||||
if ($panelStore.contains(document.activeElement as HTMLElement)) return;
|
||||
$api.closePopover();
|
||||
}
|
||||
|
||||
@@ -104,12 +106,12 @@
|
||||
switch (event.key) {
|
||||
case Keys.Escape:
|
||||
if ($api.popoverState !== PopoverStates.Open) return;
|
||||
if (!$api.panel) return;
|
||||
if (!$api.panel?.contains(document.activeElement)) return;
|
||||
if (!$panelStore) return;
|
||||
if (!$panelStore.contains(document.activeElement)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$api.closePopover();
|
||||
$api.button?.focus();
|
||||
$apiButton?.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user