Refactor Popover stores/contexts
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user