Run prettier over everything and fix some imports

This commit is contained in:
Ryan Gossiaux
2021-12-13 18:22:16 -08:00
parent 3bf974a654
commit 82b138f0ae
63 changed files with 3317 additions and 3319 deletions

View File

@@ -1,20 +1,24 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], extends: [
plugins: ['svelte3', '@typescript-eslint'], "eslint:recommended",
ignorePatterns: ['*.cjs'], "plugin:@typescript-eslint/recommended",
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], "prettier",
],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
settings: { settings: {
'svelte3/typescript': () => require('typescript') "svelte3/typescript": () => require("typescript"),
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: "module",
ecmaVersion: 2020 ecmaVersion: 2020,
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
} },
}; };

View File

@@ -1,2 +1,3 @@
# svelte-headlessui # svelte-headlessui
Unofficial Svelte port of Headless UI components (https://headlessui.dev/) Unofficial Svelte port of Headless UI components (https://headlessui.dev/)

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { DescriptionContext } from "./DescriptionProvider.svelte"; import type { DescriptionContext } from "./DescriptionProvider.svelte";
const id = `headlessui-description-${useId()}`; const id = `headlessui-description-${useId()}`;
let contextStore: Writable<DescriptionContext> | undefined = getContext( let contextStore: Writable<DescriptionContext> | undefined = getContext(
"headlessui-description-context" "headlessui-description-context"

View File

@@ -1,10 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import { getContext, setContext, createEventDispatcher, tick } from "svelte";
getContext,
setContext,
createEventDispatcher,
tick,
} from "svelte";
export enum DialogStates { export enum DialogStates {
Open, Open,
Closed, Closed,
@@ -38,17 +33,17 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { useInertOthers } from "./use-inert-others"; import { useInertOthers } from "$lib/hooks/use-inert-others";
import { contains } from "./dom-containers"; import { contains } from "$lib/internal/dom-containers";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import FocusTrap from "./FocusTrap.svelte"; import FocusTrap from "$lib/components/FocusTrap/FocusTrap.svelte";
import StackContextProvider, { import StackContextProvider, {
StackMessage, StackMessage,
} from "./StackContextProvider.svelte"; } from "$lib/internal/StackContextProvider.svelte";
import DescriptionProvider from "./DescriptionProvider.svelte"; import DescriptionProvider from "./DescriptionProvider.svelte";
import ForcePortalRootContext from "./ForcePortalRootContext.svelte"; import ForcePortalRootContext from "./ForcePortalRootContext.svelte";
import Portal from "./Portal.svelte"; import Portal from "./Portal.svelte";
@@ -204,7 +199,7 @@
on:mousedown={handleWindowMousedown} on:mousedown={handleWindowMousedown}
on:keydown={handleWindowKeydown} on:keydown={handleWindowKeydown}
/> />
{#if open} {#if visible}
<FocusTrap {containers} {enabled} options={{ initialFocus }} /> <FocusTrap {containers} {enabled} options={{ initialFocus }} />
<StackContextProvider <StackContextProvider
element={internalDialogRef} element={internalDialogRef}
@@ -223,10 +218,7 @@
<Portal> <Portal>
<PortalGroup target={internalDialogRef}> <PortalGroup target={internalDialogRef}>
<ForcePortalRootContext force={false}> <ForcePortalRootContext force={false}>
<DescriptionProvider <DescriptionProvider name={"Dialog.Description"} let:describedby>
name={"Dialog.Description"}
let:describedby
>
<div <div
{...{ ...$$restProps, ...propsWeControl }} {...{ ...$$restProps, ...propsWeControl }}
aria-describedby={describedby} aria-describedby={describedby}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DialogStates, useDialogContext } from "./Dialog.svelte"; import { DialogStates, useDialogContext } from "./Dialog.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
let api = useDialogContext("DialogOverlay"); let api = useDialogContext("DialogOverlay");
let id = `headlessui-dialog-overlay-${useId()}`; let id = `headlessui-dialog-overlay-${useId()}`;
function handleClick(event: MouseEvent) { function handleClick(event: MouseEvent) {

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DialogStates, useDialogContext } from "./Dialog.svelte"; import { DialogStates, useDialogContext } from "./Dialog.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { onMount } from "svelte"; import { onMount } from "svelte";
let api = useDialogContext("DialogTitle"); let api = useDialogContext("DialogTitle");
let id = `headlessui-dialog-title-${useId()}`; let id = `headlessui-dialog-title-${useId()}`;

View File

@@ -27,8 +27,9 @@
export function useDisclosureContext( export function useDisclosureContext(
component: string component: string
): Writable<StateDefinition | undefined> { ): Writable<StateDefinition | undefined> {
let context: Writable<StateDefinition | undefined> | undefined = let context: Writable<StateDefinition | undefined> | undefined = getContext(
getContext(DISCLOSURE_CONTEXT_NAME); DISCLOSURE_CONTEXT_NAME
);
if (context === undefined) { if (context === undefined) {
throw new Error( throw new Error(
@@ -41,9 +42,9 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
export let defaultOpen = false; export let defaultOpen = false;
let buttonId = `headlessui-disclosure-button-${useId()}`; let buttonId = `headlessui-disclosure-button-${useId()}`;
let panelId = `headlessui-disclosure-panel-${useId()}`; let panelId = `headlessui-disclosure-panel-${useId()}`;
@@ -78,8 +79,7 @@
let restoreElement = (() => { let restoreElement = (() => {
if (!focusableElement) return $buttonStore; if (!focusableElement) return $buttonStore;
if (focusableElement instanceof HTMLElement) if (focusableElement instanceof HTMLElement) return focusableElement;
return focusableElement;
return $buttonStore; return $buttonStore;
})(); })();
@@ -98,8 +98,5 @@
</script> </script>
<div {...$$restProps}> <div {...$$restProps}>
<slot <slot open={disclosureState === DisclosureStates.Open} close={$api?.close} />
open={disclosureState === DisclosureStates.Open}
close={$api?.close}
/>
</div> </div>

View File

@@ -1,11 +1,8 @@
<script lang="ts"> <script lang="ts">
import { import { useDisclosureContext, DisclosureStates } from "./Disclosure.svelte";
useDisclosureContext,
DisclosureStates,
} from "./Disclosure.svelte";
import { usePanelContext } from "./DisclosurePanel.svelte"; import { usePanelContext } from "./DisclosurePanel.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
export let disabled = false; export let disabled = false;
const api = useDisclosureContext("DisclosureButton"); const api = useDisclosureContext("DisclosureButton");
const panelContext = usePanelContext(); const panelContext = usePanelContext();

View File

@@ -8,12 +8,9 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { import { useDisclosureContext, DisclosureStates } from "./Disclosure.svelte";
useDisclosureContext, import type { Writable } from "svelte/store";
DisclosureStates, import { State } from "$lib/internal/open-closed";
} from "./Disclosure.svelte";
import { Writable } from "svelte/store";
import { State } from "./open-closed";
const api = useDisclosureContext("DisclosureButton"); const api = useDisclosureContext("DisclosureButton");
$: id = $api?.panelId; $: id = $api?.panelId;
let openClosedState: Writable<State> | undefined = getContext("OpenClosed"); let openClosedState: Writable<State> | undefined = getContext("OpenClosed");

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { import {
focusElement, focusElement,
focusIn, focusIn,
Focus, Focus,
FocusResult, FocusResult,
} from "./focus-management"; } from "$lib/utils/focus-management";
import { contains } from "./dom-containers"; import { contains } from "$lib/internal/dom-containers";
import { afterUpdate, onMount, onDestroy } from "svelte"; import { afterUpdate, onMount, onDestroy } from "svelte";
export let containers: Set<HTMLElement>; export let containers: Set<HTMLElement>;
@@ -85,8 +85,7 @@
for (let element of containers) { for (let element of containers) {
let result = focusIn( let result = focusIn(
element, element,
(event.shiftKey ? Focus.Previous : Focus.Next) | (event.shiftKey ? Focus.Previous : Focus.Next) | Focus.WrapAround
Focus.WrapAround
); );
if (result === FocusResult.Success) { if (result === FocusResult.Success) {

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { LabelContext } from "./LabelProvider.svelte"; import type { LabelContext } from "./LabelProvider.svelte";
const id = `headlessui-label-${useId()}`; const id = `headlessui-label-${useId()}`;
export let passive = false; export let passive = false;
let contextStore: Writable<LabelContext> | undefined = getContext( let contextStore: Writable<LabelContext> | undefined = getContext(

View File

@@ -37,11 +37,14 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { Focus, calculateActiveIndex } from "./calculate-active-index"; import {
Focus,
calculateActiveIndex,
} from "$lib/utils/calculate-active-index";
import { createEventDispatcher, setContext } from "svelte"; import { createEventDispatcher, setContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { State, useOpenClosedProvider } from "./open-closed"; import { State, useOpenClosedProvider } from "$lib/internal/open-closed";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -116,10 +119,7 @@
} }
); );
if ( if (searchQuery === "" && activeOptionIndex === nextActiveOptionIndex)
searchQuery === "" &&
activeOptionIndex === nextActiveOptionIndex
)
return; return;
activeOptionIndex = nextActiveOptionIndex; activeOptionIndex = nextActiveOptionIndex;
searchQuery = ""; searchQuery = "";
@@ -131,8 +131,7 @@
searchQuery += value.toLowerCase(); searchQuery += value.toLowerCase();
let match = options.findIndex( let match = options.findIndex(
(option) => (option) => !option.disabled && option.textValue.startsWith(searchQuery)
!option.disabled && option.textValue.startsWith(searchQuery)
); );
if (match === -1 || match === activeOptionIndex) return; if (match === -1 || match === activeOptionIndex) return;
@@ -151,9 +150,7 @@
unregisterOption(id: string) { unregisterOption(id: string) {
let nextOptions = options.slice(); let nextOptions = options.slice();
let currentActiveOption = let currentActiveOption =
activeOptionIndex !== null activeOptionIndex !== null ? nextOptions[activeOptionIndex] : null;
? nextOptions[activeOptionIndex]
: null;
let idx = nextOptions.findIndex((a) => a.id === id); let idx = nextOptions.findIndex((a) => a.id === id);
if (idx !== -1) nextOptions.splice(idx, 1); if (idx !== -1) nextOptions.splice(idx, 1);
options = nextOptions; options = nextOptions;

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getContext, tick } from "svelte"; import { getContext, tick } from "svelte";
import { ListboxStates, StateDefinition } from "./Listbox.svelte"; import { ListboxStates, StateDefinition } from "./Listbox.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
let api: SvelteStore<StateDefinition> = getContext("api"); let api: SvelteStore<StateDefinition> = getContext("api");
let id = `headlessui-listbox-button-${useId()}`; let id = `headlessui-listbox-button-${useId()}`;
let buttonStore: SvelteStore<HTMLButtonElement> = getContext("buttonStore"); let buttonStore: SvelteStore<HTMLButtonElement> = getContext("buttonStore");

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import { ListboxStates, StateDefinition } from "./Listbox.svelte"; import { ListboxStates, StateDefinition } from "./Listbox.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
let api: SvelteStore<StateDefinition> = getContext("api"); let api: SvelteStore<StateDefinition> = getContext("api");
let id = `headlessui-listbox-label-${useId()}`; let id = `headlessui-listbox-label-${useId()}`;
let labelStore: SvelteStore<HTMLLabelElement> = getContext("labelStore"); let labelStore: SvelteStore<HTMLLabelElement> = getContext("labelStore");

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { getContext, onDestroy, onMount, tick } from "svelte"; import { getContext, onDestroy, onMount, tick } from "svelte";
import { ListboxStates, StateDefinition } from "./Listbox.svelte"; import { ListboxStates, StateDefinition } from "./Listbox.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
export let value: any; export let value: any;
export let disabled = false; export let disabled = false;
let api: SvelteStore<StateDefinition> = getContext("api"); let api: SvelteStore<StateDefinition> = getContext("api");
@@ -48,9 +48,7 @@
} }
if (newState !== oldState || newActive !== oldActive) { if (newState !== oldState || newActive !== oldActive) {
if (newState === ListboxStates.Open && newActive) { if (newState === ListboxStates.Open && newActive) {
document document.getElementById(id)?.scrollIntoView?.({ block: "nearest" });
.getElementById(id)
?.scrollIntoView?.({ block: "nearest" });
} }
} }
oldState = newState; oldState = newState;

View File

@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { getContext, tick } from "svelte"; import { getContext, tick } from "svelte";
import { ListboxStates, StateDefinition } from "./Listbox.svelte"; import { ListboxStates, StateDefinition } from "./Listbox.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
import { State, useOpenClosed } from "./open-closed"; import { State, useOpenClosed } from "$lib/internal/open-closed";
let api: SvelteStore<StateDefinition> = getContext("api"); let api: SvelteStore<StateDefinition> = getContext("api");
let id = `headlessui-listbox-options-${useId()}`; let id = `headlessui-listbox-options-${useId()}`;
let optionsStore: SvelteStore<HTMLUListElement> = let optionsStore: SvelteStore<HTMLUListElement> = getContext("optionsStore");
getContext("optionsStore");
let searchDebounce: ReturnType<typeof setTimeout> | null = null; let searchDebounce: ReturnType<typeof setTimeout> | null = null;
async function handleKeyDown(event: KeyboardEvent) { async function handleKeyDown(event: KeyboardEvent) {

View File

@@ -1,9 +1,12 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { Focus, calculateActiveIndex } from "./calculate-active-index"; import {
Focus,
calculateActiveIndex,
} from "$lib/utils/calculate-active-index";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { match } from "./match"; import { match } from "$lib/utils/match";
export enum MenuStates { export enum MenuStates {
Open, Open,
Closed, Closed,
@@ -81,8 +84,7 @@
} }
); );
if (searchQuery === "" && activeItemIndex === nextActiveItemIndex) if (searchQuery === "" && activeItemIndex === nextActiveItemIndex) return;
return;
searchQuery = ""; searchQuery = "";
activeItemIndex = nextActiveItemIndex; activeItemIndex = nextActiveItemIndex;
}, },
@@ -91,8 +93,7 @@
let match = items.findIndex( let match = items.findIndex(
(item) => (item) =>
item.data.textValue.startsWith(searchQuery) && item.data.textValue.startsWith(searchQuery) && !item.data.disabled
!item.data.disabled
); );
if (match === -1 || match === activeItemIndex) return; if (match === -1 || match === activeItemIndex) return;
@@ -132,8 +133,7 @@
if (!$itemsStore?.contains(target)) $api.closeMenu(); if (!$itemsStore?.contains(target)) $api.closeMenu();
if (active !== document.body && active?.contains(target)) return; // Keep focus on newly clicked/focused element if (active !== document.body && active?.contains(target)) return; // Keep focus on newly clicked/focused element
if (!event.defaultPrevented) if (!event.defaultPrevented) $buttonStore?.focus({ preventScroll: true });
$buttonStore?.focus({ preventScroll: true });
} }
let openClosedState: Writable<State> | undefined = writable(); let openClosedState: Writable<State> | undefined = writable();

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { useMenuContext, MenuStates } from "./Menu.svelte"; import { useMenuContext, MenuStates } from "./Menu.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
import { tick } from "svelte"; import { tick } from "svelte";
export let disabled = false; export let disabled = false;
const api = useMenuContext("MenuButton"); const api = useMenuContext("MenuButton");
@@ -66,9 +66,7 @@
id, id,
"aria-haspopup": true, "aria-haspopup": true,
"aria-controls": $itemsStore?.id, "aria-controls": $itemsStore?.id,
"aria-expanded": disabled "aria-expanded": disabled ? undefined : $api.menuState === MenuStates.Open,
? undefined
: $api.menuState === MenuStates.Open,
}; };
</script> </script>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { useMenuContext, MenuStates, MenuItemData } from "./Menu.svelte"; import { useMenuContext, MenuStates, MenuItemData } from "./Menu.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
import { afterUpdate, onDestroy, onMount, tick } from "svelte"; import { afterUpdate, onDestroy, onMount, tick } from "svelte";
export let disabled = false; export let disabled = false;
const api = useMenuContext("MenuItem"); const api = useMenuContext("MenuItem");

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { useMenuContext, MenuStates } from "./Menu.svelte"; import { useMenuContext, MenuStates } from "./Menu.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { Focus } from "./calculate-active-index"; import { Focus } from "$lib/utils/calculate-active-index";
import { treeWalker } from "./tree-walker"; import { treeWalker } from "$lib/utils/tree-walker";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { getContext, tick } from "svelte"; import { getContext, tick } from "svelte";
import { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
const api = useMenuContext("MenuButton"); const api = useMenuContext("MenuButton");
const id = `headlessui-menu-items-${useId()}`; const id = `headlessui-menu-items-${useId()}`;
let searchDebounce: ReturnType<typeof setTimeout> | null = null; let searchDebounce: ReturnType<typeof setTimeout> | null = null;

View File

@@ -27,10 +27,13 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { match } from "./match"; import { match } from "$lib/utils/match";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { isFocusableElement, FocusableMode } from "./focus-management"; import {
import { State } from "./open-closed"; isFocusableElement,
FocusableMode,
} from "$lib/utils/focus-management";
import { State } from "$lib/internal/open-closed";
import type { PopoverGroupContext } from "./PopoverGroup.svelte"; import type { PopoverGroupContext } 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";
@@ -79,8 +82,7 @@
let restoreElement = (() => { let restoreElement = (() => {
if (!focusableElement) return $api.button; if (!focusableElement) return $api.button;
if (focusableElement instanceof HTMLElement) if (focusableElement instanceof HTMLElement) return focusableElement;
return focusableElement;
return $api.button; return $api.button;
})(); })();

View File

@@ -1,13 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { getFocusableElements, Focus, focusIn } from "./focus-management"; import {
getFocusableElements,
Focus,
focusIn,
} from "$lib/utils/focus-management";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { PopoverStates, StateDefinition } from "./Popover.svelte"; import { PopoverStates, StateDefinition } from "./Popover.svelte";
import type { PopoverGroupContext } from "./PopoverGroup.svelte"; import type { PopoverGroupContext } from "./PopoverGroup.svelte";
import type { PopoverPanelContext } from "./PopoverPanel.svelte"; import type { PopoverPanelContext } from "./PopoverPanel.svelte";
let buttonStore: Writable<HTMLButtonElement> = let buttonStore: Writable<HTMLButtonElement> = getContext("PopoverButtonRef");
getContext("PopoverButtonRef");
export let disabled: Boolean = false; export let disabled: Boolean = false;
let api: Writable<StateDefinition> | undefined = getContext("PopoverApi"); let api: Writable<StateDefinition> | undefined = getContext("PopoverApi");
@@ -75,10 +78,8 @@
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)) if ($api.button?.contains(previousActiveElementRef)) return;
return; if ($api.panel?.contains(previousActiveElementRef)) return;
if ($api.panel?.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();

View File

@@ -8,7 +8,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { PopoverRegisterBag } from "./Popover.svelte"; import type { PopoverRegisterBag } from "./Popover.svelte";
import { setContext } from "svelte"; import { setContext } from "svelte";
let groupRef: HTMLDivElement | undefined; let groupRef: HTMLDivElement | undefined;
let popovers: PopoverRegisterBag[] = []; let popovers: PopoverRegisterBag[] = [];

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { PopoverStates, StateDefinition } from "./Popover.svelte"; import { PopoverStates, StateDefinition } from "./Popover.svelte";

View File

@@ -3,14 +3,14 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { import {
getFocusableElements, getFocusableElements,
Focus, Focus,
FocusResult, FocusResult,
focusIn, focusIn,
} from "./focus-management"; } from "$lib/utils/focus-management";
import { getContext, setContext, onMount } from "svelte"; import { getContext, setContext, onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { PopoverStates, StateDefinition } from "./Popover.svelte"; import { PopoverStates, StateDefinition } from "./Popover.svelte";
@@ -109,9 +109,6 @@
/> />
{#if visible} {#if visible}
<div {...$$restProps} on:keydown={handleKeydown} bind:this={$panelStore}> <div {...$$restProps} on:keydown={handleKeydown} bind:this={$panelStore}>
<slot <slot open={$api.popoverState === PopoverStates.Open} close={$api.close} />
open={$api.popoverState === PopoverStates.Open}
close={$api.close}
/>
</div> </div>
{/if} {/if}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { usePortalGroupContext } from "./PortalGroup.svelte"; import { usePortalGroupContext } from "./PortalGroup.svelte";
import { usePortalRoot } from "./ForcePortalRootContext.svelte"; import { usePortalRoot } from "$lib/internal/ForcePortalRootContext.svelte";
import { portal } from "./use-portal"; import { portal } from "$lib/hooks/use-portal";
let forceInRoot = usePortalRoot(); let forceInRoot = usePortalRoot();
let groupTarget = usePortalGroupContext(); let groupTarget = usePortalGroupContext();
$: target = (() => { $: target = (() => {

View File

@@ -3,9 +3,9 @@
import LabelProvider from "./LabelProvider.svelte"; import LabelProvider from "./LabelProvider.svelte";
import { createEventDispatcher, getContext, setContext } from "svelte"; import { createEventDispatcher, getContext, setContext } from "svelte";
import { Writable, writable } from "svelte/store"; import { Writable, writable } from "svelte/store";
import { Focus, focusIn, FocusResult } from "./focus-management"; import { Focus, focusIn, FocusResult } from "$lib/utils/focus-management";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
export interface Option { export interface Option {
id: string; id: string;
element: HTMLElement | null; element: HTMLElement | null;
@@ -45,7 +45,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { treeWalker } from "./use-tree-walker"; import { treeWalker } from "$lib/hooks/use-tree-walker";
export let disabled = false; export let disabled = false;
export let value: any; export let value: any;
let radioGroupRef: HTMLElement | null = null; let radioGroupRef: HTMLElement | null = null;
@@ -112,18 +112,13 @@
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
let result = focusIn( let result = focusIn(all, Focus.Previous | Focus.WrapAround);
all,
Focus.Previous | Focus.WrapAround
);
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.find( let activeOption = options.find(
(option) => (option) => option.element === document.activeElement
option.element === document.activeElement
); );
if (activeOption) if (activeOption) $api.change(activeOption.propsRef.value);
$api.change(activeOption.propsRef.value);
} }
} }
break; break;
@@ -138,11 +133,9 @@
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.find( let activeOption = options.find(
(option) => (option) => option.element === document.activeElement
option.element === document.activeElement
); );
if (activeOption) if (activeOption) $api.change(activeOption.propsRef.value);
$api.change(activeOption.propsRef.value);
} }
} }
break; break;

View File

@@ -4,7 +4,7 @@
import LabelProvider from "./LabelProvider.svelte"; import LabelProvider from "./LabelProvider.svelte";
import { useRadioGroupContext, Option } from "./RadioGroup.svelte"; import { useRadioGroupContext, Option } from "./RadioGroup.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
enum OptionState { enum OptionState {
Empty = 1 << 0, Empty = 1 << 0,

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { StateDefinition } from "./SwitchGroup.svelte"; import type { StateDefinition } from "./SwitchGroup.svelte";
import { LabelContext } from "./LabelProvider.svelte"; import type { LabelContext } from "$lib/components/label/LabelProvider.svelte";
import { DescriptionContext } from "./DescriptionProvider.svelte"; import type { DescriptionContext } from "$lib/components/description/DescriptionProvider.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { getContext, createEventDispatcher } from "svelte"; import { getContext, createEventDispatcher } from "svelte";
import { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let checked = false; export let checked = false;
@@ -13,8 +13,9 @@
let labelContext: Writable<LabelContext> | undefined = getContext( let labelContext: Writable<LabelContext> | undefined = getContext(
"headlessui-label-context" "headlessui-label-context"
); );
let descriptionContext: Writable<DescriptionContext> | undefined = let descriptionContext: Writable<DescriptionContext> | undefined = getContext(
getContext("headlessui-description-context"); "headlessui-description-context"
);
let id = `headlessui-switch-${useId()}`; let id = `headlessui-switch-${useId()}`;
$: switchStore = $api?.switchStore; $: switchStore = $api?.switchStore;
let internalSwitchRef = null; let internalSwitchRef = null;

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Focus, focusIn } from "./focus-management"; import { Focus, focusIn } from "$lib/utils/focus-management";
import { Keys } from "./keyboard"; import { Keys } from "$lib/utils/keyboard";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { useTabsContext } from "./TabGroup.svelte"; import { useTabsContext } from "./TabGroup.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
export let disabled = false; export let disabled = false;

View File

@@ -97,9 +97,7 @@
// Overflow // Overflow
else if (defaultIndex > $api.tabs.length) { else if (defaultIndex > $api.tabs.length) {
selectedIndex = tabs.indexOf( selectedIndex = tabs.indexOf(focusableTabs[focusableTabs.length - 1]);
focusableTabs[focusableTabs.length - 1]
);
} }
// Middle // Middle

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { useTabsContext } from "./TabGroup.svelte"; import { useTabsContext } from "./TabGroup.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
let api = useTabsContext("TabPanel"); let api = useTabsContext("TabPanel");
let id = `headlessui-tabs-panel-${useId()}`; let id = `headlessui-tabs-panel-${useId()}`;

View File

@@ -1,14 +1,9 @@
<script lang="ts"> <script lang="ts">
import { import { createEventDispatcher, onMount, setContext } from "svelte";
createEventDispatcher,
getContext,
onMount,
setContext,
} from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { Reason, transition } from "./transition"; import { Reason, transition } from "$lib/utils/transition";
import { import {
hasChildren, hasChildren,
@@ -19,7 +14,7 @@
useParentNesting, useParentNesting,
useTransitionContext, useTransitionContext,
} from "./TransitionRoot.svelte"; } from "./TransitionRoot.svelte";
import { useId } from "./use-id"; import { useId } from "$lib/hooks/use-id";
export let unmount = true; export let unmount = true;
export let enter = ""; export let enter = "";

View File

@@ -54,9 +54,7 @@
| { children: NestingContextValues["children"] } | { children: NestingContextValues["children"] }
): boolean { ): boolean {
if ("children" in bag) return hasChildren(bag.children); if ("children" in bag) return hasChildren(bag.children);
return ( return bag.filter(({ state }) => state === TreeStates.Visible).length > 0;
bag.filter(({ state }) => state === TreeStates.Visible).length > 0
);
} }
export function useNesting(done?: () => void) { export function useNesting(done?: () => void) {
@@ -67,9 +65,7 @@
onDestroy(() => (mounted = false)); onDestroy(() => (mounted = false));
function unregister(childId: ID, strategy = RenderStrategy.Hidden) { function unregister(childId: ID, strategy = RenderStrategy.Hidden) {
let idx = transitionableChildren.findIndex( let idx = transitionableChildren.findIndex(({ id }) => id === childId);
({ id }) => id === childId
);
if (idx === -1) return; if (idx === -1) return;
match(strategy, { match(strategy, {
@@ -112,11 +108,11 @@
import { getContext, onDestroy, onMount, setContext } from "svelte"; import { getContext, onDestroy, onMount, setContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
import { match } from "./match"; import { match } from "$lib/utils/match";
import { State } from "./open-closed"; import { State } from "$lib/internal/open-closed";
import { RenderStrategy } from "./Render.svelte"; import { RenderStrategy } from "$lib/utils/Render.svelte";
import TransitionChild from "./TransitionChild.svelte"; import TransitionChild from "./TransitionChild.svelte";
import type { useId } from "./use-id"; import type { useId } from "$lib/hooks/use-id";
export let show: boolean; export let show: boolean;
export let unmount = true; export let unmount = true;

View File

@@ -1,8 +1,8 @@
let id = 0 let id = 0;
function generateId() { function generateId() {
return ++id return ++id;
} }
export function useId() { export function useId() {
return generateId() return generateId();
} }

View File

@@ -1,100 +1,103 @@
let interactables = new Set<HTMLElement>() let interactables = new Set<HTMLElement>();
let originals = new Map<HTMLElement, { 'aria-hidden': string | null; inert: boolean }>() let originals = new Map<
HTMLElement,
{ "aria-hidden": string | null; inert: boolean }
>();
function inert(element: HTMLElement) { function inert(element: HTMLElement) {
element.setAttribute('aria-hidden', 'true') element.setAttribute("aria-hidden", "true");
// @ts-expect-error `inert` does not exist on HTMLElement (yet!) // @ts-expect-error `inert` does not exist on HTMLElement (yet!)
element.inert = true element.inert = true;
} }
function restore(element: HTMLElement) { function restore(element: HTMLElement) {
let original = originals.get(element) let original = originals.get(element);
if (!original) return if (!original) return;
if (original['aria-hidden'] === null) element.removeAttribute('aria-hidden') if (original["aria-hidden"] === null) element.removeAttribute("aria-hidden");
else element.setAttribute('aria-hidden', original['aria-hidden']) else element.setAttribute("aria-hidden", original["aria-hidden"]);
// @ts-expect-error `inert` does not exist on HTMLElement (yet!) // @ts-expect-error `inert` does not exist on HTMLElement (yet!)
element.inert = original.inert element.inert = original.inert;
} }
export function useInertOthers<TElement extends HTMLElement>( export function useInertOthers<TElement extends HTMLElement>(
container: TElement | null, container: TElement | null,
enabled: boolean = true enabled: boolean = true
) { ) {
if (!enabled) return if (!enabled) return;
if (!container) return if (!container) return;
let element = container let element = container;
// Mark myself as an interactable element // Mark myself as an interactable element
interactables.add(element) interactables.add(element);
// Restore elements that now contain an interactable child // Restore elements that now contain an interactable child
for (let original of originals.keys()) { for (let original of originals.keys()) {
if (original.contains(element)) { if (original.contains(element)) {
restore(original) restore(original);
originals.delete(original) originals.delete(original);
} }
} }
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll('body > *').forEach(child => { document.querySelectorAll("body > *").forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return; // Skip non-HTMLElements
// Skip the interactables, and the parents of the interactables // Skip the interactables, and the parents of the interactables
for (let interactable of interactables) { for (let interactable of interactables) {
if (child.contains(interactable)) return if (child.contains(interactable)) return;
} }
// Keep track of the elements // Keep track of the elements
if (interactables.size === 1) { if (interactables.size === 1) {
originals.set(child, { originals.set(child, {
'aria-hidden': child.getAttribute('aria-hidden'), "aria-hidden": child.getAttribute("aria-hidden"),
// @ts-expect-error `inert` does not exist on HTMLElement (yet!) // @ts-expect-error `inert` does not exist on HTMLElement (yet!)
inert: child.inert, inert: child.inert,
}) });
// Mutate the element // Mutate the element
inert(child) inert(child);
} }
}) });
return () => { return () => {
// Inert is disabled on the current element // Inert is disabled on the current element
interactables.delete(element) interactables.delete(element);
// We still have interactable elements, therefore this one and its parent // We still have interactable elements, therefore this one and its parent
// will become inert as well. // will become inert as well.
if (interactables.size > 0) { if (interactables.size > 0) {
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll('body > *').forEach(child => { document.querySelectorAll("body > *").forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return; // Skip non-HTMLElements
// Skip already inert parents // Skip already inert parents
if (originals.has(child)) return if (originals.has(child)) return;
// Skip the interactables, and the parents of the interactables // Skip the interactables, and the parents of the interactables
for (let interactable of interactables) { for (let interactable of interactables) {
if (child.contains(interactable)) return if (child.contains(interactable)) return;
} }
originals.set(child, { originals.set(child, {
'aria-hidden': child.getAttribute('aria-hidden'), "aria-hidden": child.getAttribute("aria-hidden"),
// @ts-expect-error `inert` does not exist on HTMLElement (yet!) // @ts-expect-error `inert` does not exist on HTMLElement (yet!)
inert: child.inert, inert: child.inert,
}) });
// Mutate the element // Mutate the element
inert(child) inert(child);
}) });
} else { } else {
for (let element of originals.keys()) { for (let element of originals.keys()) {
// Restore // Restore
restore(element) restore(element);
// Cleanup // Cleanup
originals.delete(element) originals.delete(element);
}
} }
} }
};
} }

View File

@@ -10,5 +10,5 @@ export function portal(element: HTMLElement, target: HTMLElement) {
target.parentElement?.removeChild(target); target.parentElement?.removeChild(target);
} }
}, },
} };
} }

View File

@@ -3,7 +3,7 @@ type AcceptNode = (
) => ) =>
| typeof NodeFilter.FILTER_ACCEPT | typeof NodeFilter.FILTER_ACCEPT
| typeof NodeFilter.FILTER_SKIP | typeof NodeFilter.FILTER_SKIP
| typeof NodeFilter.FILTER_REJECT | typeof NodeFilter.FILTER_REJECT;
export function treeWalker({ export function treeWalker({
container, container,
@@ -11,18 +11,25 @@ export function treeWalker({
walk, walk,
enabled, enabled,
}: { }: {
container: HTMLElement | null container: HTMLElement | null;
accept: AcceptNode accept: AcceptNode;
walk(node: HTMLElement): void walk(node: HTMLElement): void;
enabled?: boolean enabled?: boolean;
}) { }) {
let root = container let root = container;
if (!root) return if (!root) return;
if (enabled !== undefined && !enabled) return if (enabled !== undefined && !enabled) return;
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) let acceptNode = Object.assign((node: HTMLElement) => accept(node), {
acceptNode: accept,
});
// @ts-ignore-error Typescript bug thinks this can only have 3 args // @ts-ignore-error Typescript bug thinks this can only have 3 args
let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false) let walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
acceptNode,
false
);
while (walker.nextNode()) walk(walker.currentNode as HTMLElement) while (walker.nextNode()) walk(walker.currentNode as HTMLElement);
} }

View File

@@ -1,8 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
const FORCE_PORTAL_ROOT_CONTEXT_NAME = const FORCE_PORTAL_ROOT_CONTEXT_NAME = "headlessui-force-portal-root-context";
"headlessui-force-portal-root-context";
export function usePortalRoot(): Writable<boolean> | undefined { export function usePortalRoot(): Writable<boolean> | undefined {
return getContext(FORCE_PORTAL_ROOT_CONTEXT_NAME); return getContext(FORCE_PORTAL_ROOT_CONTEXT_NAME);

View File

@@ -8,10 +8,7 @@
<script lang="ts"> <script lang="ts">
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { writable, Writable } from "svelte/store"; import { writable, Writable } from "svelte/store";
type OnUpdate = ( type OnUpdate = (message: StackMessage, element: HTMLElement | null) => void;
message: StackMessage,
element: HTMLElement | null
) => void;
export let onUpdate: OnUpdate | undefined; export let onUpdate: OnUpdate | undefined;
export let element: HTMLElement | null; export let element: HTMLElement | null;

View File

@@ -1,7 +1,7 @@
export function contains(containers: Set<HTMLElement>, element: HTMLElement) { export function contains(containers: Set<HTMLElement>, element: HTMLElement) {
for (let container of containers) { for (let container of containers) {
if (container.contains(element)) return true if (container.contains(element)) return true;
} }
return false return false;
} }

View File

@@ -8,7 +8,7 @@ export enum State {
const OPEN_CLOSED_CONTEXT_NAME = "OpenClosed"; const OPEN_CLOSED_CONTEXT_NAME = "OpenClosed";
export function hasOpenClosed() { export function hasOpenClosed() {
return useOpenClosed() !== undefined return useOpenClosed() !== undefined;
} }
export function useOpenClosed(): Writable<State> | undefined { export function useOpenClosed(): Writable<State> | undefined {
@@ -18,4 +18,3 @@ export function useOpenClosed(): Writable<State> | undefined {
export function useOpenClosedProvider(value: Writable<State>) { export function useOpenClosedProvider(value: Writable<State>) {
setContext(OPEN_CLOSED_CONTEXT_NAME, value); setContext(OPEN_CLOSED_CONTEXT_NAME, value);
} }

View File

@@ -1,5 +1,5 @@
function assertNever(x: never): never { function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x) throw new Error("Unexpected object: " + x);
} }
export enum Focus { export enum Focus {
@@ -23,62 +23,67 @@ export enum Focus {
} }
export function calculateActiveIndex<TItem>( export function calculateActiveIndex<TItem>(
action: { focus: Focus.Specific; id: string } | { focus: Exclude<Focus, Focus.Specific> }, action:
| { focus: Focus.Specific; id: string }
| { focus: Exclude<Focus, Focus.Specific> },
resolvers: { resolvers: {
resolveItems(): TItem[] resolveItems(): TItem[];
resolveActiveIndex(): number | null resolveActiveIndex(): number | null;
resolveId(item: TItem): string resolveId(item: TItem): string;
resolveDisabled(item: TItem): boolean resolveDisabled(item: TItem): boolean;
} }
) { ) {
let items = resolvers.resolveItems() let items = resolvers.resolveItems();
if (items.length <= 0) return null if (items.length <= 0) return null;
let currentActiveIndex = resolvers.resolveActiveIndex() let currentActiveIndex = resolvers.resolveActiveIndex();
let activeIndex = currentActiveIndex ?? -1 let activeIndex = currentActiveIndex ?? -1;
let nextActiveIndex = (() => { let nextActiveIndex = (() => {
switch (action.focus) { switch (action.focus) {
case Focus.First: case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item)) return items.findIndex((item) => !resolvers.resolveDisabled(item));
case Focus.Previous: { case Focus.Previous: {
let idx = items let idx = items
.slice() .slice()
.reverse() .reverse()
.findIndex((item, idx, all) => { .findIndex((item, idx, all) => {
if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex) return false if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex)
return !resolvers.resolveDisabled(item) return false;
}) return !resolvers.resolveDisabled(item);
if (idx === -1) return idx });
return items.length - 1 - idx if (idx === -1) return idx;
return items.length - 1 - idx;
} }
case Focus.Next: case Focus.Next:
return items.findIndex((item, idx) => { return items.findIndex((item, idx) => {
if (idx <= activeIndex) return false if (idx <= activeIndex) return false;
return !resolvers.resolveDisabled(item) return !resolvers.resolveDisabled(item);
}) });
case Focus.Last: { case Focus.Last: {
let idx = items let idx = items
.slice() .slice()
.reverse() .reverse()
.findIndex(item => !resolvers.resolveDisabled(item)) .findIndex((item) => !resolvers.resolveDisabled(item));
if (idx === -1) return idx if (idx === -1) return idx;
return items.length - 1 - idx return items.length - 1 - idx;
} }
case Focus.Specific: case Focus.Specific:
return items.findIndex(item => resolvers.resolveId(item) === action.id) return items.findIndex(
(item) => resolvers.resolveId(item) === action.id
);
case Focus.Nothing: case Focus.Nothing:
return null return null;
default: default:
assertNever(action) assertNever(action);
} }
})() })();
return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex;
} }

View File

@@ -1,33 +1,33 @@
export function disposables() { export function disposables() {
let disposables: Function[] = [] let disposables: Function[] = [];
let api = { let api = {
requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) { requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) {
let raf = requestAnimationFrame(...args) let raf = requestAnimationFrame(...args);
api.add(() => cancelAnimationFrame(raf)) api.add(() => cancelAnimationFrame(raf));
}, },
nextFrame(...args: Parameters<typeof requestAnimationFrame>) { nextFrame(...args: Parameters<typeof requestAnimationFrame>) {
api.requestAnimationFrame(() => { api.requestAnimationFrame(() => {
api.requestAnimationFrame(...args) api.requestAnimationFrame(...args);
}) });
}, },
setTimeout(...args: Parameters<typeof setTimeout>) { setTimeout(...args: Parameters<typeof setTimeout>) {
let timer = setTimeout(...args) let timer = setTimeout(...args);
api.add(() => clearTimeout(timer)) api.add(() => clearTimeout(timer));
}, },
add(cb: () => void) { add(cb: () => void) {
disposables.push(cb) disposables.push(cb);
}, },
dispose() { dispose() {
for (let dispose of disposables.splice(0)) { for (let dispose of disposables.splice(0)) {
dispose() dispose();
} }
}, },
} };
return api return api;
} }

View File

@@ -1,27 +1,28 @@
import { match } from './match' import { match } from "./match";
// Credit: // Credit:
// - https://stackoverflow.com/a/30753870 // - https://stackoverflow.com/a/30753870
let focusableSelector = [ let focusableSelector = [
'[contentEditable=true]', "[contentEditable=true]",
'[tabindex]', "[tabindex]",
'a[href]', "a[href]",
'area[href]', "area[href]",
'button:not([disabled])', "button:not([disabled])",
'iframe', "iframe",
'input:not([disabled])', "input:not([disabled])",
'select:not([disabled])', "select:not([disabled])",
'textarea:not([disabled])', "textarea:not([disabled])",
] ]
.map( .map(
process.env.NODE_ENV === 'test' process.env.NODE_ENV === "test"
? // TODO: Remove this once JSDOM fixes the issue where an element that is ? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible // "hidden" can be the document.activeElement, because this is not possible
// in real browsers. // in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])` (selector) =>
: selector => `${selector}:not([tabindex='-1'])` `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: (selector) => `${selector}:not([tabindex='-1'])`
) )
.join(',') .join(",");
export enum Focus { export enum Focus {
/** Focus the first non-disabled element */ /** Focus the first non-disabled element */
@@ -55,9 +56,11 @@ enum Direction {
Next = 1, Next = 1,
} }
export function getFocusableElements(container: HTMLElement | null = document.body) { export function getFocusableElements(
if (container == null) return [] container: HTMLElement | null = document.body
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector)) ) {
if (container == null) return [];
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector));
} }
export enum FocusableMode { export enum FocusableMode {
@@ -72,75 +75,82 @@ export function isFocusableElement(
element: HTMLElement, element: HTMLElement,
mode: FocusableMode = FocusableMode.Strict mode: FocusableMode = FocusableMode.Strict
) { ) {
if (element === document.body) return false if (element === document.body) return false;
return match(mode, { return match(mode, {
[FocusableMode.Strict]() { [FocusableMode.Strict]() {
return element.matches(focusableSelector) return element.matches(focusableSelector);
}, },
[FocusableMode.Loose]() { [FocusableMode.Loose]() {
let next: HTMLElement | null = element let next: HTMLElement | null = element;
while (next !== null) { while (next !== null) {
if (next.matches(focusableSelector)) return true if (next.matches(focusableSelector)) return true;
next = next.parentElement next = next.parentElement;
} }
return false return false;
}, },
}) });
} }
export function focusElement(element: HTMLElement | null) { export function focusElement(element: HTMLElement | null) {
element?.focus({ preventScroll: true }) element?.focus({ preventScroll: true });
} }
export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) {
let elements = Array.isArray(container) ? container : getFocusableElements(container) let elements = Array.isArray(container)
let active = document.activeElement as HTMLElement ? container
: getFocusableElements(container);
let active = document.activeElement as HTMLElement;
let direction = (() => { let direction = (() => {
if (focus & (Focus.First | Focus.Next)) return Direction.Next if (focus & (Focus.First | Focus.Next)) return Direction.Next;
if (focus & (Focus.Previous | Focus.Last)) return Direction.Previous if (focus & (Focus.Previous | Focus.Last)) return Direction.Previous;
throw new Error('Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last') throw new Error(
})() "Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"
);
})();
let startIndex = (() => { let startIndex = (() => {
if (focus & Focus.First) return 0 if (focus & Focus.First) return 0;
if (focus & Focus.Previous) return Math.max(0, elements.indexOf(active)) - 1 if (focus & Focus.Previous)
if (focus & Focus.Next) return Math.max(0, elements.indexOf(active)) + 1 return Math.max(0, elements.indexOf(active)) - 1;
if (focus & Focus.Last) return elements.length - 1 if (focus & Focus.Next) return Math.max(0, elements.indexOf(active)) + 1;
if (focus & Focus.Last) return elements.length - 1;
throw new Error('Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last') throw new Error(
})() "Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"
);
})();
let focusOptions = focus & Focus.NoScroll ? { preventScroll: true } : {} let focusOptions = focus & Focus.NoScroll ? { preventScroll: true } : {};
let offset = 0 let offset = 0;
let total = elements.length let total = elements.length;
let next = undefined let next = undefined;
do { do {
// Guard against infinite loops // Guard against infinite loops
if (offset >= total || offset + total <= 0) return FocusResult.Error if (offset >= total || offset + total <= 0) return FocusResult.Error;
let nextIdx = startIndex + offset let nextIdx = startIndex + offset;
if (focus & Focus.WrapAround) { if (focus & Focus.WrapAround) {
nextIdx = (nextIdx + total) % total nextIdx = (nextIdx + total) % total;
} else { } else {
if (nextIdx < 0) return FocusResult.Underflow if (nextIdx < 0) return FocusResult.Underflow;
if (nextIdx >= total) return FocusResult.Overflow if (nextIdx >= total) return FocusResult.Overflow;
} }
next = elements[nextIdx] next = elements[nextIdx];
// Try the focus the next element, might not work if it is "hidden" to the user. // Try the focus the next element, might not work if it is "hidden" to the user.
next?.focus(focusOptions) next?.focus(focusOptions);
// Try the next one in line // Try the next one in line
offset += direction offset += direction;
} while (next !== document.activeElement) } while (next !== document.activeElement);
// This is a little weird, but let me try and explain: There are a few scenario's // This is a little weird, but let me try and explain: There are a few scenario's
// in chrome for example where a focused `<a>` tag does not get the default focus // in chrome for example where a focused `<a>` tag does not get the default focus
@@ -149,7 +159,7 @@ export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) {
// then the active element (document.activeElement) is this anchor, which is expected. // then the active element (document.activeElement) is this anchor, which is expected.
// However in that case the default focus styles are not applied *unless* you // However in that case the default focus styles are not applied *unless* you
// also add this tabindex. // also add this tabindex.
if (!next.hasAttribute('tabindex')) next.setAttribute('tabindex', '0') if (!next.hasAttribute("tabindex")) next.setAttribute("tabindex", "0");
return FocusResult.Success return FocusResult.Success;
} }

View File

@@ -1,21 +1,21 @@
// TODO: This must already exist somewhere, right? 🤔 // TODO: This must already exist somewhere, right? 🤔
// Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values // Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
export enum Keys { export enum Keys {
Space = ' ', Space = " ",
Enter = 'Enter', Enter = "Enter",
Escape = 'Escape', Escape = "Escape",
Backspace = 'Backspace', Backspace = "Backspace",
ArrowLeft = 'ArrowLeft', ArrowLeft = "ArrowLeft",
ArrowUp = 'ArrowUp', ArrowUp = "ArrowUp",
ArrowRight = 'ArrowRight', ArrowRight = "ArrowRight",
ArrowDown = 'ArrowDown', ArrowDown = "ArrowDown",
Home = 'Home', Home = "Home",
End = 'End', End = "End",
PageUp = 'PageUp', PageUp = "PageUp",
PageDown = 'PageDown', PageDown = "PageDown",
Tab = 'Tab', Tab = "Tab",
} }

View File

@@ -1,20 +1,25 @@
export function match<TValue extends string | number = string, TReturnValue = unknown>( export function match<
TValue extends string | number = string,
TReturnValue = unknown
>(
value: TValue, value: TValue,
lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>, lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>,
...args: any[] ...args: any[]
): TReturnValue { ): TReturnValue {
if (value in lookup) { if (value in lookup) {
let returnValue = lookup[value] let returnValue = lookup[value];
return typeof returnValue === 'function' ? returnValue(...args) : returnValue return typeof returnValue === "function"
? returnValue(...args)
: returnValue;
} }
let error = new Error( let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup lookup
) )
.map(key => `"${key}"`) .map((key) => `"${key}"`)
.join(', ')}.` .join(", ")}.`
) );
if (Error.captureStackTrace) Error.captureStackTrace(error, match) if (Error.captureStackTrace) Error.captureStackTrace(error, match);
throw error throw error;
} }

View File

@@ -1,9 +1,9 @@
export function once<T>(cb: (...args: T[]) => void) { export function once<T>(cb: (...args: T[]) => void) {
let state = { called: false } let state = { called: false };
return (...args: T[]) => { return (...args: T[]) => {
if (state.called) return if (state.called) return;
state.called = true state.called = true;
return cb(...args) return cb(...args);
} };
} }

View File

@@ -1,38 +1,40 @@
import { once } from './once' import { once } from "./once";
import { disposables } from './disposables' import { disposables } from "./disposables";
function addClasses(node: HTMLElement, ...classes: string[]) { function addClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.add(...classes) node && classes.length > 0 && node.classList.add(...classes);
} }
function removeClasses(node: HTMLElement, ...classes: string[]) { function removeClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.remove(...classes) node && classes.length > 0 && node.classList.remove(...classes);
} }
export enum Reason { export enum Reason {
Finished = 'finished', Finished = "finished",
Cancelled = 'cancelled', Cancelled = "cancelled",
} }
function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) { function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
let d = disposables() let d = disposables();
if (!node) return d.dispose if (!node) return d.dispose;
// Safari returns a comma separated list of values, so let's sort them and take the highest value. // Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node) let { transitionDuration, transitionDelay } = getComputedStyle(node);
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => { let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(
(value) => {
let [resolvedValue = 0] = value let [resolvedValue = 0] = value
.split(',') .split(",")
// Remove falsy we can't work with // Remove falsy we can't work with
.filter(Boolean) .filter(Boolean)
// Values are returned as `0.3s` or `75ms` // Values are returned as `0.3s` or `75ms`
.map(v => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000)) .map((v) => (v.includes("ms") ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a) .sort((a, z) => z - a);
return resolvedValue return resolvedValue;
}) }
);
// Waiting for the transition to end. We could use the `transitionend` event, however when no // Waiting for the transition to end. We could use the `transitionend` event, however when no
// actual transition/duration is defined then the `transitionend` event is not fired. // actual transition/duration is defined then the `transitionend` event is not fired.
@@ -41,18 +43,18 @@ function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
// full 100% speed instead of the 25% or 10%. // full 100% speed instead of the 25% or 10%.
if (durationMs !== 0) { if (durationMs !== 0) {
d.setTimeout(() => { d.setTimeout(() => {
done(Reason.Finished) done(Reason.Finished);
}, durationMs + delaysMs) }, durationMs + delaysMs);
} else { } else {
// No transition is happening, so we should cleanup already. Otherwise we have to wait until we // No transition is happening, so we should cleanup already. Otherwise we have to wait until we
// get disposed. // get disposed.
done(Reason.Finished) done(Reason.Finished);
} }
// If we get disposed before the timeout runs we should cleanup anyway // If we get disposed before the timeout runs we should cleanup anyway
d.add(() => done(Reason.Cancelled)) d.add(() => done(Reason.Cancelled));
return d.dispose return d.dispose;
} }
export function transition( export function transition(
@@ -63,33 +65,33 @@ export function transition(
entered: string[], entered: string[],
done?: (reason: Reason) => void done?: (reason: Reason) => void
) { ) {
let d = disposables() let d = disposables();
let _done = done !== undefined ? once(done) : () => { } let _done = done !== undefined ? once(done) : () => {};
removeClasses(node, ...entered) removeClasses(node, ...entered);
addClasses(node, ...base, ...from) addClasses(node, ...base, ...from);
d.nextFrame(() => { d.nextFrame(() => {
removeClasses(node, ...from) removeClasses(node, ...from);
addClasses(node, ...to) addClasses(node, ...to);
d.add( d.add(
waitForTransition(node, reason => { waitForTransition(node, (reason) => {
removeClasses(node, ...to, ...base) removeClasses(node, ...to, ...base);
addClasses(node, ...entered) addClasses(node, ...entered);
return _done(reason) return _done(reason);
})
)
}) })
);
});
// Once we get disposed, we should ensure that we cleanup after ourselves. In case of an unmount, // Once we get disposed, we should ensure that we cleanup after ourselves. In case of an unmount,
// the node itself will be nullified and will be a no-op. In case of a full transition the classes // the node itself will be nullified and will be a no-op. In case of a full transition the classes
// are already removed which is also a no-op. However if you go from enter -> leave mid-transition // are already removed which is also a no-op. However if you go from enter -> leave mid-transition
// then we have some leftovers that should be cleaned. // then we have some leftovers that should be cleaned.
d.add(() => removeClasses(node, ...base, ...from, ...to, ...entered)) d.add(() => removeClasses(node, ...base, ...from, ...to, ...entered));
// When we get disposed early, than we should also call the done method but switch the reason. // When we get disposed early, than we should also call the done method but switch the reason.
d.add(() => _done(Reason.Cancelled)) d.add(() => _done(Reason.Cancelled));
return d.dispose return d.dispose;
} }

View File

@@ -3,7 +3,7 @@ type AcceptNode = (
) => ) =>
| typeof NodeFilter.FILTER_ACCEPT | typeof NodeFilter.FILTER_ACCEPT
| typeof NodeFilter.FILTER_SKIP | typeof NodeFilter.FILTER_SKIP
| typeof NodeFilter.FILTER_REJECT | typeof NodeFilter.FILTER_REJECT;
export function treeWalker({ export function treeWalker({
container, container,
@@ -11,18 +11,25 @@ export function treeWalker({
walk, walk,
enabled, enabled,
}: { }: {
container: HTMLElement | null container: HTMLElement | null;
accept: AcceptNode accept: AcceptNode;
walk(node: HTMLElement): void walk(node: HTMLElement): void;
enabled?: boolean enabled?: boolean;
}) { }) {
let root = container let root = container;
if (!root) return if (!root) return;
if (enabled !== undefined && !enabled) return if (enabled !== undefined && !enabled) return;
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) let acceptNode = Object.assign((node: HTMLElement) => accept(node), {
acceptNode: accept,
});
// @ts-ignore-error Typescript bug thinks this can only have 3 args // @ts-ignore-error Typescript bug thinks this can only have 3 args
let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false) let walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
acceptNode,
false
);
while (walker.nextNode()) walk(walker.currentNode as HTMLElement) while (walker.nextNode()) walk(walker.currentNode as HTMLElement);
} }

View File

@@ -1,2 +1,4 @@
<h1>Welcome to SvelteKit</h1> <h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p> <p>
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>

View File

@@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from "@sveltejs/adapter-auto";
import preprocess from 'svelte-preprocess'; import preprocess from "svelte-preprocess";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@@ -11,8 +11,8 @@ const config = {
adapter: adapter(), adapter: adapter(),
// hydrate the <div id="svelte"> element in src/app.html // hydrate the <div id="svelte"> element in src/app.html
target: '#svelte' target: "#svelte",
} },
}; };
export default config; export default config;