132 lines
4.0 KiB
Svelte
132 lines
4.0 KiB
Svelte
<script lang="ts" context="module">
|
|
export type PopoverPanelContext = string | null;
|
|
|
|
const POPOVER_PANEL_CONTEXT_NAME = "PopoverPanelContext";
|
|
export function usePopoverPanelContext():
|
|
| StateDefinition["panelId"]
|
|
| undefined {
|
|
return getContext(POPOVER_PANEL_CONTEXT_NAME);
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { Keys } from "$lib/utils/keyboard";
|
|
import { State, useOpenClosed } from "$lib/internal/open-closed";
|
|
import {
|
|
getFocusableElements,
|
|
Focus,
|
|
FocusResult,
|
|
focusIn,
|
|
} from "$lib/utils/focus-management";
|
|
import { getContext, setContext } from "svelte";
|
|
import {
|
|
PopoverStates,
|
|
StateDefinition,
|
|
usePopoverContext,
|
|
} 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 openClosedState = useOpenClosed();
|
|
|
|
$: visible =
|
|
openClosedState !== undefined
|
|
? $openClosedState === State.Open
|
|
: $api.popoverState === PopoverStates.Open;
|
|
|
|
$: (() => {
|
|
if (!focus) return;
|
|
if ($api.popoverState !== PopoverStates.Open) return;
|
|
if (!$api.panel) return;
|
|
|
|
let activeElement = document.activeElement as HTMLElement;
|
|
if ($api.panel?.contains(activeElement)) return; // Already focused within Dialog
|
|
|
|
focusIn($api.panel!, Focus.First);
|
|
})();
|
|
|
|
function handleWindowKeydown(event: KeyboardEvent) {
|
|
if ($api.popoverState !== PopoverStates.Open) return;
|
|
if (!$api.panel) return;
|
|
|
|
if (event.key !== Keys.Tab) return;
|
|
if (!document.activeElement) return;
|
|
if (!$api.panel?.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,
|
|
// but it will also "fix" some issues based on whether you are using a
|
|
// Portal or not.
|
|
event.preventDefault();
|
|
|
|
let result = focusIn(
|
|
$api.panel!,
|
|
event.shiftKey ? Focus.Previous : Focus.Next
|
|
);
|
|
|
|
if (result === FocusResult.Underflow) {
|
|
return $api.button?.focus();
|
|
} else if (result === FocusResult.Overflow) {
|
|
if (!$api.button) return;
|
|
|
|
let elements = getFocusableElements();
|
|
let buttonIdx = elements.indexOf($api.button!);
|
|
|
|
let nextElements = elements
|
|
.splice(buttonIdx + 1) // Elements after button
|
|
.filter((element) => !$api.panel?.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
|
|
// case we would Error (because nothing after the button is
|
|
// focusable). Therefore we will try and focus the very first item in
|
|
// the document.body.
|
|
if (focusIn(nextElements, Focus.First) === FocusResult.Error) {
|
|
focusIn(document.body, Focus.First);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleFocus() {
|
|
if (!focus) return;
|
|
if ($api.popoverState !== PopoverStates.Open) return;
|
|
if (!$api.panel) return;
|
|
if ($api.panel?.contains(document.activeElement as HTMLElement)) return;
|
|
$api.closePopover();
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
switch (event.key) {
|
|
case Keys.Escape:
|
|
if ($api.popoverState !== PopoverStates.Open) return;
|
|
if (!$api.panel) return;
|
|
if (!$api.panel?.contains(document.activeElement)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
$api.closePopover();
|
|
$api.button?.focus();
|
|
break;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window
|
|
on:keydown={handleWindowKeydown}
|
|
on:focus|capture={handleFocus}
|
|
/>
|
|
{#if visible}
|
|
<div
|
|
use:useActions={use}
|
|
{...$$restProps}
|
|
on:keydown={handleKeydown}
|
|
bind:this={$panelStore}
|
|
>
|
|
<slot open={$api.popoverState === PopoverStates.Open} close={$api.close} />
|
|
</div>
|
|
{/if}
|