diff --git a/src/lib/components/popover/Popover.svelte b/src/lib/components/popover/Popover.svelte index 05ee03a..b72d22a 100644 --- a/src/lib/components/popover/Popover.svelte +++ b/src/lib/components/popover/Popover.svelte @@ -6,9 +6,9 @@ export interface StateDefinition { // State popoverState: PopoverStates; - button: HTMLElement | null; + button: Writable; buttonId: string; - panel: HTMLElement | null; + panel: Writable; 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 = writable(null); - setContext("PopoverPanelRef", panel); - - let button: Writable = writable(null); - setContext("PopoverButtonRef", button); + let panel: StateDefinition["panel"] = writable(null); + let button: StateDefinition["button"] = writable(null); let api: Writable = 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() { diff --git a/src/lib/components/popover/PopoverButton.svelte b/src/lib/components/popover/PopoverButton.svelte index 1a4ad63..d64ab5c 100644 --- a/src/lib/components/popover/PopoverButton.svelte +++ b/src/lib/components/popover/PopoverButton.svelte @@ -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 = 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} > diff --git a/src/lib/components/popover/PopoverPanel.svelte b/src/lib/components/popover/PopoverPanel.svelte index eac2809..2f1fc09 100644 --- a/src/lib/components/popover/PopoverPanel.svelte +++ b/src/lib/components/popover/PopoverPanel.svelte @@ -26,12 +26,14 @@ } from "./Popover.svelte"; import { ActionArray, useActions } from "$lib/hooks/use-actions"; export let use: ActionArray = []; - let panelStore: SvelteStore = 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; } }