Refactor Popover stores/contexts

This commit is contained in:
Ryan Gossiaux
2021-12-18 22:36:57 -08:00
parent 9e45d92929
commit 1966219b30
3 changed files with 52 additions and 56 deletions

View File

@@ -6,9 +6,9 @@
export interface StateDefinition { export interface StateDefinition {
// State // State
popoverState: PopoverStates; popoverState: PopoverStates;
button: HTMLElement | null; button: Writable<HTMLElement | null>;
buttonId: string; buttonId: string;
panel: HTMLElement | null; panel: Writable<HTMLElement | null>;
panelId: string; panelId: string;
// State mutators // State mutators
@@ -49,7 +49,7 @@
FocusableMode, FocusableMode,
} from "$lib/utils/focus-management"; } from "$lib/utils/focus-management";
import { State, useOpenClosedProvider } from "$lib/internal/open-closed"; 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 { getContext, setContext, onMount } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { ActionArray, useActions } from "$lib/hooks/use-actions"; import { ActionArray, useActions } from "$lib/hooks/use-actions";
@@ -59,18 +59,15 @@
let popoverState: PopoverStates = PopoverStates.Closed; let popoverState: PopoverStates = PopoverStates.Closed;
let panel: Writable<StateDefinition["panel"]> = writable(null); let panel: StateDefinition["panel"] = writable(null);
setContext("PopoverPanelRef", panel); let button: StateDefinition["button"] = writable(null);
let button: Writable<StateDefinition["button"]> = writable(null);
setContext("PopoverButtonRef", button);
let api: Writable<StateDefinition> = writable({ let api: Writable<StateDefinition> = writable({
popoverState, popoverState,
buttonId, buttonId,
panelId, panelId,
panel: $panel, panel,
button: $button, button,
togglePopover() { togglePopover() {
popoverState = match(popoverState, { popoverState = match(popoverState, {
[PopoverStates.Open]: PopoverStates.Closed, [PopoverStates.Open]: PopoverStates.Closed,
@@ -85,10 +82,10 @@
$api.closePopover(); $api.closePopover();
let restoreElement = (() => { let restoreElement = (() => {
if (!focusableElement) return $api.button; if (!focusableElement) return $button;
if (focusableElement instanceof HTMLElement) return focusableElement; if (focusableElement instanceof HTMLElement) return focusableElement;
return $api.button; return $button;
})(); })();
restoreElement?.focus(); restoreElement?.focus();
@@ -108,10 +105,6 @@
return { return {
...obj, ...obj,
popoverState, popoverState,
buttonId,
panelId,
panel: $panel,
button: $button,
}; };
}); });
@@ -123,8 +116,7 @@
}, },
}; };
const groupContext: PopoverGroupContext | undefined = const groupContext = usePopoverGroupContext();
getContext("PopoverGroup");
const registerPopover = groupContext?.registerPopover; const registerPopover = groupContext?.registerPopover;
function isFocusWithinPopoverGroup() { function isFocusWithinPopoverGroup() {

View File

@@ -5,17 +5,18 @@
Focus, Focus,
focusIn, focusIn,
} from "$lib/utils/focus-management"; } from "$lib/utils/focus-management";
import { getContext } from "svelte"; import { writable } from "svelte/store";
import { writable, Writable } from "svelte/store";
import { PopoverStates, usePopoverContext } from "./Popover.svelte"; import { PopoverStates, usePopoverContext } from "./Popover.svelte";
import { usePopoverGroupContext } from "./PopoverGroup.svelte"; import { usePopoverGroupContext } from "./PopoverGroup.svelte";
import { usePopoverPanelContext } from "./PopoverPanel.svelte"; import { usePopoverPanelContext } from "./PopoverPanel.svelte";
import { ActionArray, useActions } from "$lib/hooks/use-actions"; import { ActionArray, useActions } from "$lib/hooks/use-actions";
export let use: ActionArray = []; export let use: ActionArray = [];
let buttonStore: Writable<HTMLButtonElement> = getContext("PopoverButtonRef");
export let disabled: Boolean = false; export let disabled: Boolean = false;
let api = usePopoverContext("PopoverButton"); let api = usePopoverContext("PopoverButton");
let apiButton = $api.button;
let ourStore = apiButton;
let groupContext = usePopoverGroupContext(); let groupContext = usePopoverGroupContext();
let closeOthers = groupContext?.closeOthers; let closeOthers = groupContext?.closeOthers;
@@ -23,8 +24,9 @@
let isWithinPanel = let isWithinPanel =
panelContext === null ? false : panelContext === $api.panelId; panelContext === null ? false : panelContext === $api.panelId;
if (isWithinPanel) { if (isWithinPanel) {
buttonStore = writable(); ourStore = writable();
} }
let apiPanel = $api.panel;
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
let activeElementRef: Element | null = null; let activeElementRef: Element | null = null;
@@ -45,7 +47,7 @@
event.preventDefault(); // Prevent triggering a *click* event event.preventDefault(); // Prevent triggering a *click* event
event.stopPropagation(); event.stopPropagation();
$api.closePopover(); $api.closePopover();
$api.button?.focus(); // Re-focus the original opening Button $apiButton?.focus(); // Re-focus the original opening Button
break; break;
} }
} else { } else {
@@ -63,7 +65,7 @@
if ($api.popoverState !== PopoverStates.Open) if ($api.popoverState !== PopoverStates.Open)
return closeOthers?.($api.buttonId); return closeOthers?.($api.buttonId);
if (!$api.button) return; if (!$api.button) return;
if (!$api.button?.contains(document.activeElement)) return; if (!$apiButton?.contains(document.activeElement)) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$api.closePopover(); $api.closePopover();
@@ -71,33 +73,33 @@
case Keys.Tab: case Keys.Tab:
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$apiPanel) return;
if (!$api.button) return; if (!$apiButton) return;
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
if (event.shiftKey) { if (event.shiftKey) {
// Check if the last focused element exists, and check that it is not inside button or panel itself // Check if the last focused element exists, and check that it is not inside button or panel itself
if (!previousActiveElementRef) return; if (!previousActiveElementRef) return;
if ($api.button?.contains(previousActiveElementRef)) return; if ($apiButton?.contains(previousActiveElementRef)) return;
if ($api.panel?.contains(previousActiveElementRef)) return; if ($apiPanel?.contains(previousActiveElementRef)) return;
// Check if the last focused element is *after* the button in the DOM // Check if the last focused element is *after* the button in the DOM
let focusableElements = getFocusableElements(); let focusableElements = getFocusableElements();
let previousIdx = focusableElements.indexOf( let previousIdx = focusableElements.indexOf(
previousActiveElementRef as HTMLElement previousActiveElementRef as HTMLElement
); );
let buttonIdx = focusableElements.indexOf($api.button!); let buttonIdx = focusableElements.indexOf($apiButton);
if (buttonIdx > previousIdx) return; if (buttonIdx > previousIdx) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
focusIn($api.panel!, Focus.Last); focusIn($apiPanel, Focus.Last);
} else { } else {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
focusIn($api.panel!, Focus.First); focusIn($apiPanel, Focus.First);
} }
break; break;
@@ -113,28 +115,28 @@
event.preventDefault(); event.preventDefault();
} }
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$apiPanel) return;
if (!$api.button) return; if (!$apiButton) return;
// TODO: Revisit when handling Tab/Shift+Tab when using Portal's // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
switch (event.key) { switch (event.key) {
case Keys.Tab: case Keys.Tab:
// Check if the last focused element exists, and check that it is not inside button or panel itself // Check if the last focused element exists, and check that it is not inside button or panel itself
if (!previousActiveElementRef) return; if (!previousActiveElementRef) return;
if ($api.button?.contains(previousActiveElementRef)) return; if ($apiButton?.contains(previousActiveElementRef)) return;
if ($api.panel?.contains(previousActiveElementRef)) return; if ($apiPanel?.contains(previousActiveElementRef)) return;
// Check if the last focused element is *after* the button in the DOM // Check if the last focused element is *after* the button in the DOM
let focusableElements = getFocusableElements(); let focusableElements = getFocusableElements();
let previousIdx = focusableElements.indexOf( let previousIdx = focusableElements.indexOf(
previousActiveElementRef as HTMLElement previousActiveElementRef as HTMLElement
); );
let buttonIdx = focusableElements.indexOf($api.button!); let buttonIdx = focusableElements.indexOf($apiButton);
if (buttonIdx > previousIdx) return; if (buttonIdx > previousIdx) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
focusIn($api.panel!, Focus.Last); focusIn($apiPanel, Focus.Last);
break; break;
} }
} }
@@ -142,11 +144,11 @@
if (disabled) return; if (disabled) return;
if (isWithinPanel) { if (isWithinPanel) {
$api.closePopover(); $api.closePopover();
$api.button?.focus(); // Re-focus the original opening Button $apiButton?.focus(); // Re-focus the original opening Button
} else { } else {
if ($api.popoverState === PopoverStates.Closed) if ($api.popoverState === PopoverStates.Closed)
closeOthers?.($api.buttonId); closeOthers?.($api.buttonId);
$api.button?.focus(); $apiButton?.focus();
$api.togglePopover(); $api.togglePopover();
} }
} }
@@ -172,7 +174,7 @@
on:click={handleClick} on:click={handleClick}
on:keydown={handleKeyDown} on:keydown={handleKeyDown}
on:keyup={handleKeyUp} on:keyup={handleKeyUp}
bind:this={$buttonStore} bind:this={$ourStore}
> >
<slot open={$api.popoverState === PopoverStates.Open} /> <slot open={$api.popoverState === PopoverStates.Open} />
</button> </button>

View File

@@ -26,12 +26,14 @@
} from "./Popover.svelte"; } from "./Popover.svelte";
import { ActionArray, useActions } from "$lib/hooks/use-actions"; import { ActionArray, useActions } from "$lib/hooks/use-actions";
export let use: ActionArray = []; export let use: ActionArray = [];
let panelStore: SvelteStore<HTMLDivElement> = getContext("PopoverPanelRef");
export let focus = false; export let focus = false;
let api = usePopoverContext("PopoverPanel"); let api = usePopoverContext("PopoverPanel");
setContext(POPOVER_PANEL_CONTEXT_NAME, $api.panelId); setContext(POPOVER_PANEL_CONTEXT_NAME, $api.panelId);
let panelStore = $api.panel;
let apiButton = $api.button;
let openClosedState = useOpenClosed(); let openClosedState = useOpenClosed();
$: visible = $: visible =
@@ -42,21 +44,21 @@
$: (() => { $: (() => {
if (!focus) return; if (!focus) return;
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$panelStore) return;
let activeElement = document.activeElement as HTMLElement; 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) { function handleWindowKeydown(event: KeyboardEvent) {
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$panelStore) return;
if (event.key !== Keys.Tab) return; if (event.key !== Keys.Tab) return;
if (!document.activeElement) 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 // 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, // control over what is focused next. It will behave exactly the same,
@@ -65,21 +67,21 @@
event.preventDefault(); event.preventDefault();
let result = focusIn( let result = focusIn(
$api.panel!, $panelStore,
event.shiftKey ? Focus.Previous : Focus.Next event.shiftKey ? Focus.Previous : Focus.Next
); );
if (result === FocusResult.Underflow) { if (result === FocusResult.Underflow) {
return $api.button?.focus(); return $apiButton?.focus();
} else if (result === FocusResult.Overflow) { } else if (result === FocusResult.Overflow) {
if (!$api.button) return; if (!$apiButton) return;
let elements = getFocusableElements(); let elements = getFocusableElements();
let buttonIdx = elements.indexOf($api.button!); let buttonIdx = elements.indexOf($apiButton!);
let nextElements = elements let nextElements = elements
.splice(buttonIdx + 1) // Elements after button .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 // 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 // Portal that happens to be the very last one in the DOM. In that
@@ -95,8 +97,8 @@
function handleFocus() { function handleFocus() {
if (!focus) return; if (!focus) return;
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$panelStore) return;
if ($api.panel?.contains(document.activeElement as HTMLElement)) return; if ($panelStore.contains(document.activeElement as HTMLElement)) return;
$api.closePopover(); $api.closePopover();
} }
@@ -104,12 +106,12 @@
switch (event.key) { switch (event.key) {
case Keys.Escape: case Keys.Escape:
if ($api.popoverState !== PopoverStates.Open) return; if ($api.popoverState !== PopoverStates.Open) return;
if (!$api.panel) return; if (!$panelStore) return;
if (!$api.panel?.contains(document.activeElement)) return; if (!$panelStore.contains(document.activeElement)) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$api.closePopover(); $api.closePopover();
$api.button?.focus(); $apiButton?.focus();
break; break;
} }
} }