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 {
// 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() {

View File

@@ -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>

View File

@@ -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;
}
}