Run prettier over everything and fix some imports
This commit is contained in:
@@ -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,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()}`;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 = (() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()}`;
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export function portal(element: HTMLElement, target: HTMLElement) {
|
|||||||
target.parentElement?.removeChild(target);
|
target.parentElement?.removeChild(target);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user