diff --git a/.changeset/eleven-scissors-design.md b/.changeset/eleven-scissors-design.md
new file mode 100644
index 0000000..fab7a6b
--- /dev/null
+++ b/.changeset/eleven-scissors-design.md
@@ -0,0 +1,5 @@
+---
+"svelte-interactions": minor
+---
+
+feat: long press
diff --git a/README.md b/README.md
index e08068c..3afe682 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ type PressConfig = PressHandlers {
The `PressConfig` object also includes handlers for all the different `PressHandlers`. These are provided as a convenience, should you prefer to handle the events here rather than the custom `on:press*` events dispatched by the element with the `pressAction`.
-Be aware that event if you use these handlers, the custom `on:press*` events will still be dispatched, so be sure you aren't handling the same event twice.
+Be aware that event if you use these handlers, the custom `on:press*` events for whatever handlers you use will not be dispatched to the element. We only dispatch the events that aren't handled by the `PressHandlers`.
```ts
type PressHandlers = {
@@ -160,7 +160,7 @@ type PressResult = {
### Custom Events
-When you apply the `pressAction` to an element, it will dispatch custom `on:press*` events. You can use these and/or the `PressHandlers` to handle the various press events.
+When you apply the `pressAction` to an element, it will dispatch custom `on:press*` events. You can use these or the `PressHandlers` to handle the various press events.
```ts
type PressActionReturn = ActionReturn<
@@ -229,6 +229,183 @@ interface PressEvent {
}
```
+## Long Press Interaction
+
+The `hover` interaction provides an API for consistent long press behavior across all browsers and devices, with support for a custom time threshold and accessible description.
+
+#### Basic Usage
+
+```svelte
+
+
+
+```
+
+### createLongPress
+
+Creates a new `longpress` interaction instance. Each element should have its own instance, as it maintains state for a single element. For example, if you had multiple buttons on a page:
+
+```svelte
+
+
+
+
+```
+
+#### LongPressConfig
+
+`createLongPress` takes in an optional `LongPressConfig` object, which can be used to customize the interaction.
+
+```ts
+import { createLongPress } from 'svelte-interactions';
+
+const { pressLongAction } = createPress({ isDisabled: true, threshold: 1000 });
+```
+
+```ts
+type LongPressConfig = LongPressHandlers & {
+ /**
+ * Whether the long press events should be disabled
+ */
+ isDisabled?: boolean;
+
+ /**
+ * The amount of time (in milliseconds) to wait before
+ * triggering a long press event.
+ */
+ threshold?: number;
+
+ /**
+ * A description for assistive techology users indicating that a
+ * long press action is available, e.g. "Long press to open menu".
+ */
+ accessibilityDescription?: string;
+};
+```
+
+The `LongPressConfig` object also includes handlers for all the different `LongPressHandlers`. These are provided as a convenience, should you prefer to handle the events here rather than the custom `on:longpress*` events dispatched by the element with the `longPressAction`.
+
+Be aware that event if you use these handlers, the custom `on:longpress*` events for whatever handlers you use will not be dispatched to the element. We only dispatch the events that aren't handled by the `LongPressHandlers`.
+
+```ts
+export type LongPressHandlers = {
+ /**
+ * Handler that is called when a long press interaction starts.
+ */
+ onLongPressStart?: (e: LongPressEvent) => void;
+
+ /**
+ * Handler that is called when a long press interaction ends, either
+ * over the target or when the pointer leaves the target.
+ */
+ onLongPressEnd?: (e: LongPressEvent) => void;
+
+ /**
+ * Handler that is called when the threshold time is met while
+ * the press is over the target.
+ */
+ onLongPress?: (e: LongPressEvent) => void;
+};
+```
+
+### LongPressResult
+
+The `createLongPress` function returns a `LongPressResult` object, which contains the `longPressAction` action, and the `description` state. More returned properties may be added in the future if needed.
+
+```ts
+type LongPressResult = {
+ /**
+ * A Svelte action which handles applying the event listeners
+ * and dispatching events to the element
+ */
+ longPressAction: (node: HTMLElement | SVGElement) => LongPressActionReturn;
+
+ /**
+ * A writable store to manage the accessible description for the long
+ * press action. It's initially populated with the value passed to the
+ * `accessibilityDescription` config option, but can be updated at any
+ * time by calling `description.set()`, and the new description will
+ * reflect in the DOM.
+ */
+ accessibilityDescription: Writable;
+};
+```
+
+### Custom Events
+
+When you apply the `longPressAction` to an element, it will dispatch custom `on:longpress*` events for events you aren't handling via the `LongPressConfig` props. You can use these or the `LongPressHandlers` to handle the various `longpress` events.
+
+```ts
+type LongPressActionReturn = ActionReturn<
+ undefined,
+ {
+ /**
+ * Dispatched when the threshold time is met while
+ * the press is over the target.
+ */
+ 'on:longpress'?: (e: CustomEvent) => void;
+
+ /**
+ * Dispatched when a long press interaction starts.
+ */
+ 'on:longpressstart'?: (e: CustomEvent) => void;
+
+ /**
+ * Dispatched when a long press interaction ends, either
+ * over the target or when the pointer leaves the target.
+ */
+ 'on:longpressend'?: (e: CustomEvent) => void;
+ }
+>;
+```
+
+#### PressEvent
+
+This is the event object dispatched by the custom `on:press*` events, and is also passed to the `PressHandlers` should you choose to use them.
+
+```ts
+type PointerType = 'mouse' | 'pen' | 'touch' | 'keyboard' | 'virtual';
+
+interface PressEvent {
+ /** The type of longpress event being fired. */
+ type: 'longpressstart' | 'longpressend' | 'longpress';
+
+ /** The pointer type that triggered the press event. */
+ pointerType: PointerType;
+
+ /** The target element of the press event. */
+ target: Element;
+
+ /** Whether the shift keyboard modifier was held during the press event. */
+ shiftKey: boolean;
+
+ /** Whether the ctrl keyboard modifier was held during the press event. */
+ ctrlKey: boolean;
+
+ /** Whether the meta keyboard modifier was held during the press event. */
+ metaKey: boolean;
+
+ /** Whether the alt keyboard modifier was held during the press event. */
+ altKey: boolean;
+}
+```
+
## Hover Interaction
The `hover` interaction provides an API for consistent hover behavior across all browsers and devices, ignoring emulated mouse events on touch devices.
@@ -292,7 +469,7 @@ type HoverConfig = HoverHandlers & {
The `HoverConfig` object also includes handlers for all the different `HoverHandlers`. These are provided as a convenience, should you prefer to handle the events here rather than the custom `on:hover*` events dispatched by the element with the `hoverAction`.
-Be aware that even if you use these handlers, the custom `on:hover*` events will still be dispatched, so be sure you aren't handling the same event twice.
+Be aware that event if you use these handlers, the custom `on:hover*` events for whatever handlers you use will not be dispatched to the element. We only dispatch the events that aren't handled by the `HoverHandlers`.
```ts
type HoverHandlers = {
@@ -334,7 +511,7 @@ export type HoverResult = {
### Custom Events
-When you apply the `hoverAction` to an element, it will dispatch custom `on:hover*` events. You can use these and/or the `HoverHandlers` to handle the various hover events.
+When you apply the `hoverAction` to an element, it will dispatch custom `on:hover*` events. You can use these or the `HoverHandlers` to handle the various hover events.
```ts
type HoverActionReturn = ActionReturn<
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 6983190..6746771 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,2 +1 @@
-export * from './hover/index.js';
-export * from './press/index.js';
+export * from './interactions/index.js';
diff --git a/src/lib/hover/create.ts b/src/lib/interactions/hover/create.ts
similarity index 96%
rename from src/lib/hover/create.ts
rename to src/lib/interactions/hover/create.ts
index 5b6fc15..54f3a06 100644
--- a/src/lib/hover/create.ts
+++ b/src/lib/interactions/hover/create.ts
@@ -156,13 +156,15 @@ export function createHover(config?: HoverConfig): HoverResult {
const event = new HoverEvent('hoverstart', pointerType, originalEvent);
- onHoverStart?.({
- type: 'hoverstart',
- target,
- pointerType
- });
-
- dispatchHoverEvent(event);
+ if (onHoverStart) {
+ onHoverStart({
+ type: 'hoverstart',
+ target,
+ pointerType
+ });
+ } else {
+ dispatchHoverEvent(event);
+ }
onHoverChange?.(true);
@@ -178,12 +180,15 @@ export function createHover(config?: HoverConfig): HoverResult {
state.update((curr) => ({ ...curr, isHovered: false }));
const event = new HoverEvent('hoverend', pointerType, originalEvent);
- onHoverEnd?.({
- type: 'hoverend',
- target: currentTarget,
- pointerType
- });
- dispatchHoverEvent(event);
+ if (onHoverEnd) {
+ onHoverEnd({
+ type: 'hoverend',
+ target: currentTarget,
+ pointerType
+ });
+ } else {
+ dispatchHoverEvent(event);
+ }
onHoverChange?.(false);
diff --git a/src/lib/hover/events.ts b/src/lib/interactions/hover/events.ts
similarity index 100%
rename from src/lib/hover/events.ts
rename to src/lib/interactions/hover/events.ts
diff --git a/src/lib/hover/index.ts b/src/lib/interactions/hover/index.ts
similarity index 100%
rename from src/lib/hover/index.ts
rename to src/lib/interactions/hover/index.ts
diff --git a/src/lib/interactions/index.ts b/src/lib/interactions/index.ts
new file mode 100644
index 0000000..308fba3
--- /dev/null
+++ b/src/lib/interactions/index.ts
@@ -0,0 +1,3 @@
+export * from './hover/index.js';
+export * from './press/index.js';
+export * from './long-press/index.js';
diff --git a/src/lib/interactions/long-press/create.ts b/src/lib/interactions/long-press/create.ts
new file mode 100644
index 0000000..bab522d
--- /dev/null
+++ b/src/lib/interactions/long-press/create.ts
@@ -0,0 +1,200 @@
+import type { ActionReturn } from 'svelte/action';
+import type { LongPressEvent as ILongPressEvent, LongPressHandlers } from './events.js';
+import type { PointerType } from '$lib/types/index.js';
+import { createPress, type PressEvent } from '$lib/interactions/press/index.js';
+import { createGlobalListeners } from '$lib/utils/globalListeners.js';
+import { effect } from '$lib/utils/effect.js';
+import { createDescription } from '$lib/utils/description.js';
+import { writable, type Writable } from 'svelte/store';
+
+export type LongPressConfig = LongPressHandlers & {
+ /**
+ * Whether the long press events should be disabled
+ */
+ isDisabled?: boolean;
+
+ /**
+ * The amount of time (in milliseconds) to wait before
+ * triggering a long press event.
+ */
+ threshold?: number;
+
+ /**
+ * A description for assistive techology users indicating that a
+ * long press action is available, e.g. "Long press to open menu".
+ */
+ accessibilityDescription?: string;
+};
+
+class LongPressEvent implements ILongPressEvent {
+ type: ILongPressEvent['type'];
+ pointerType: PointerType;
+ target: Element;
+ shiftKey: boolean;
+ ctrlKey: boolean;
+ metaKey: boolean;
+ altKey: boolean;
+ #shouldStopPropagation = true;
+
+ constructor(
+ type: ILongPressEvent['type'],
+ pointerType: PointerType,
+ pressEvent: Omit & { type: ILongPressEvent['type'] }
+ ) {
+ this.type = type;
+ this.pointerType = pointerType;
+ this.target = pressEvent.target;
+ this.shiftKey = pressEvent.shiftKey;
+ this.metaKey = pressEvent.metaKey;
+ this.ctrlKey = pressEvent.ctrlKey;
+ this.altKey = pressEvent.altKey;
+ }
+
+ get shouldStopPropagation() {
+ return this.#shouldStopPropagation;
+ }
+}
+
+type LongPressActionReturn = ActionReturn<
+ undefined,
+ {
+ 'on:longpress'?: (e: CustomEvent) => void;
+ 'on:longpressstart'?: (e: CustomEvent) => void;
+ 'on:longpressend'?: (e: CustomEvent) => void;
+ }
+>;
+
+export type LongPressResult = {
+ /**
+ * A Svelte action which handles applying the event listeners
+ * and dispatching events to the element
+ */
+ longPressAction: (node: HTMLElement | SVGElement) => LongPressActionReturn;
+
+ /**
+ * A writable store to manage the accessible description for the long
+ * press action. It's initially populated with the value passed to the
+ * `accessibilityDescription` config option, but can be updated at any
+ * time by calling `description.set()`, and the new description will
+ * reflect in the DOM.
+ */
+ accessibilityDescription: Writable;
+};
+
+const DEFAULT_THRESHOLD = 500;
+
+/**
+ * Handles long press interactions across mouse and touch devices.
+ * Supports a customizable time threshold,accessibility description,
+ * and normalizes behavior across browsers and devices.
+ */
+export function createLongPress(config?: LongPressConfig): LongPressResult {
+ const defaults = {
+ isDisabled: false,
+ threshold: DEFAULT_THRESHOLD
+ };
+ const {
+ onLongPress,
+ onLongPressEnd,
+ onLongPressStart,
+ isDisabled,
+ threshold,
+ accessibilityDescription: accessibilityDescriptionProp
+ } = {
+ ...defaults,
+ ...config
+ };
+
+ let timeout: ReturnType | undefined = undefined;
+ let nodeEl: HTMLElement | SVGElement | null = null;
+
+ const { addGlobalListener, removeGlobalListener } = createGlobalListeners();
+ const accessibilityDescription = writable(accessibilityDescriptionProp);
+
+ function dispatchLongPressEvent(longPressEvent: LongPressEvent) {
+ nodeEl?.dispatchEvent(
+ new CustomEvent(longPressEvent.type, { detail: longPressEvent })
+ );
+ }
+
+ const { pressAction } = createPress({
+ isDisabled,
+ onPressStart(e) {
+ e.continuePropagation();
+ if (e.pointerType !== 'mouse' && e.pointerType !== 'touch') return;
+
+ const startEvent = { ...e, type: 'longpressstart' as const };
+ const event = new LongPressEvent('longpressstart', e.pointerType, startEvent);
+ onLongPressStart?.(startEvent);
+ dispatchLongPressEvent(event);
+
+ timeout = setTimeout(() => {
+ // prevent other `press` handlers from handling this event
+ e.target.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true }));
+ const pressEvent = { ...e, type: 'longpress' as const };
+ const event = new LongPressEvent('longpress', e.pointerType, pressEvent);
+ onLongPress?.(pressEvent);
+ dispatchLongPressEvent(event);
+ timeout = undefined;
+ }, threshold);
+
+ // prevent context menu, which may open on long press on touch devices
+ if (e.pointerType !== 'touch') return;
+ const onContextMenu = (e: Event) => {
+ e.preventDefault();
+ };
+
+ addGlobalListener(e.target, 'contextmenu', onContextMenu, { once: true });
+ addGlobalListener(
+ window,
+ 'pointerup',
+ () => {
+ // If no contextmenu event is fired quickly after pointerup,
+ // remove the handler so future context menu events outside
+ // a long press are not prevented.
+ setTimeout(() => {
+ removeGlobalListener(e.target, 'contextmenu', onContextMenu);
+ }, 30);
+ },
+ { once: true }
+ );
+ },
+ onPressEnd(e) {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+
+ if (e.pointerType !== 'mouse' && e.pointerType !== 'touch') return;
+
+ const endEvent = { ...e, type: 'longpressend' as const };
+ const event = new LongPressEvent('longpressend', e.pointerType, endEvent);
+ onLongPressEnd?.(endEvent);
+ dispatchLongPressEvent(event);
+ }
+ });
+
+ const ariaDescribedBy = createDescription(accessibilityDescription);
+
+ function longPressAction(node: HTMLElement | SVGElement): LongPressActionReturn {
+ nodeEl = node;
+
+ const unsub = effect([ariaDescribedBy], ([$ariaDescribedBy]) => {
+ if (!$ariaDescribedBy) return;
+ node.setAttribute('aria-describedby', $ariaDescribedBy);
+ });
+
+ const { destroy } = pressAction(node);
+
+ return {
+ destroy() {
+ unsub();
+ destroy?.();
+ }
+ };
+ }
+
+ return {
+ longPressAction,
+ accessibilityDescription
+ };
+}
diff --git a/src/lib/interactions/long-press/events.ts b/src/lib/interactions/long-press/events.ts
new file mode 100644
index 0000000..0f0db64
--- /dev/null
+++ b/src/lib/interactions/long-press/events.ts
@@ -0,0 +1,25 @@
+import type { PressEvent } from '$lib/interactions/press/events.js';
+
+export interface LongPressEvent extends Omit {
+ /** The type of long press event being fired. */
+ type: 'longpressstart' | 'longpressend' | 'longpress';
+}
+
+export type LongPressHandlers = {
+ /**
+ * Handler that is called when a long press interaction starts.
+ */
+ onLongPressStart?: (e: LongPressEvent) => void;
+
+ /**
+ * Handler that is called when a long press interaction ends, either
+ * over the target or when the pointer leaves the target.
+ */
+ onLongPressEnd?: (e: LongPressEvent) => void;
+
+ /**
+ * Handler that is called when the threshold time is met while
+ * the press is over the target.
+ */
+ onLongPress?: (e: LongPressEvent) => void;
+};
diff --git a/src/lib/press/index.ts b/src/lib/interactions/long-press/index.ts
similarity index 100%
rename from src/lib/press/index.ts
rename to src/lib/interactions/long-press/index.ts
diff --git a/src/lib/press/create.ts b/src/lib/interactions/press/create.ts
similarity index 98%
rename from src/lib/press/create.ts
rename to src/lib/interactions/press/create.ts
index 8c70a5c..f17e813 100644
--- a/src/lib/press/create.ts
+++ b/src/lib/interactions/press/create.ts
@@ -7,8 +7,8 @@ import type { ActionReturn } from 'svelte/action';
// prettier-ignore
import { disableTextSelection, restoreTextSelection, focusWithoutScrolling, getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, isVirtualPointerEvent, openLink, createGlobalListeners, toWritableStores, executeCallbacks, noop, addEventListener, isHTMLorSVGElement } from '$lib/utils/index.js';
-import type { PressEvent as IPressEvent, PointerType, PressHandlers } from './events.js';
-import type { FocusableElement } from '$lib/types/dom.js';
+import type { PressEvent as IPressEvent, PressHandlers } from './events.js';
+import type { FocusableElement, EventBase, PointerType } from '$lib/types/index.js';
export type PressConfig = PressHandlers & {
/** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
@@ -43,14 +43,6 @@ type PressState = {
metaKeyEvents?: Map;
};
-type EventBase = {
- currentTarget: EventTarget | null;
- shiftKey: boolean;
- ctrlKey: boolean;
- metaKey: boolean;
- altKey: boolean;
-};
-
type PressActionReturn = ActionReturn<
undefined,
{
@@ -158,8 +150,11 @@ export function createPress(config?: PressConfig): PressResult {
state.update((curr) => ({ ...curr, isTriggeringEvent: true }));
const event = new PressEvent('pressstart', pointerType, originalEvent);
- onPressStart?.(event);
- dispatchPressEvent(event);
+ if (onPressStart) {
+ onPressStart(event);
+ } else {
+ dispatchPressEvent(event);
+ }
shouldStopPropagation = event.shouldStopPropagation;
onPressChange?.(true);
@@ -189,8 +184,11 @@ export function createPress(config?: PressConfig): PressResult {
let shouldStopPropagation = true;
const event = new PressEvent('pressend', pointerType, originalEvent);
- onPressEnd?.(event);
- dispatchPressEvent(event);
+ if (onPressEnd) {
+ onPressEnd(event);
+ } else {
+ dispatchPressEvent(event);
+ }
shouldStopPropagation = event.shouldStopPropagation;
onPressChange?.(false);
@@ -199,8 +197,11 @@ export function createPress(config?: PressConfig): PressResult {
const $isDisabled = get(opts.isDisabled);
if (wasPressed && !$isDisabled) {
const event = new PressEvent('press', pointerType, originalEvent);
- onPress?.(event);
- dispatchPressEvent(event);
+ if (onPress) {
+ onPress(event);
+ } else {
+ dispatchPressEvent(event);
+ }
shouldStopPropagation &&= event.shouldStopPropagation;
}
@@ -219,8 +220,12 @@ export function createPress(config?: PressConfig): PressResult {
state.update((curr) => ({ ...curr, isTriggeringEvent: true }));
const event = new PressEvent('pressup', pointerType, originalEvent);
- onPressUp?.(event);
- dispatchPressEvent(event);
+
+ if (onPressUp) {
+ onPressUp(event);
+ } else {
+ dispatchPressEvent(event);
+ }
state.update((curr) => ({ ...curr, isTriggeringEvent: false }));
return event.shouldStopPropagation;
}
diff --git a/src/lib/press/events.ts b/src/lib/interactions/press/events.ts
similarity index 95%
rename from src/lib/press/events.ts
rename to src/lib/interactions/press/events.ts
index 19b45e9..89b6533 100644
--- a/src/lib/press/events.ts
+++ b/src/lib/interactions/press/events.ts
@@ -1,7 +1,8 @@
// Portions of the code in this file are based on code from Adobe.
// Original licensing for the following can be found in the NOTICE.txt
// file in the root directory of this source tree.
-export type PointerType = 'mouse' | 'pen' | 'touch' | 'keyboard' | 'virtual';
+
+import type { PointerType } from '$lib/types/events.js';
export interface PressEvent {
/** The type of press event being fired. */
diff --git a/src/lib/interactions/press/index.ts b/src/lib/interactions/press/index.ts
new file mode 100644
index 0000000..6900ec5
--- /dev/null
+++ b/src/lib/interactions/press/index.ts
@@ -0,0 +1,2 @@
+export * from './create.js';
+export * from './events.js';
diff --git a/src/lib/types/dom.ts b/src/lib/types/dom.ts
index 1a14ad7..2cc2d10 100644
--- a/src/lib/types/dom.ts
+++ b/src/lib/types/dom.ts
@@ -1,11 +1,2 @@
/** Any focusable element, including both HTML and SVG elements. */
export type FocusableElement = HTMLElement | SVGElement;
-
-/**
- * A type alias for a general event listener function.
- *
- * @template E - The type of event to listen for
- * @param evt - The event object
- * @returns The return value of the event listener function
- */
-export type GeneralEventListener = (evt: E) => unknown;
diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts
index 81b47c5..bd77e5a 100644
--- a/src/lib/types/events.ts
+++ b/src/lib/types/events.ts
@@ -1 +1,11 @@
export type GeneralEventListener = (evt: E) => unknown;
+
+export type EventBase = {
+ currentTarget: EventTarget | null;
+ shiftKey: boolean;
+ ctrlKey: boolean;
+ metaKey: boolean;
+ altKey: boolean;
+};
+
+export type PointerType = 'mouse' | 'pen' | 'touch' | 'keyboard' | 'virtual';
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
new file mode 100644
index 0000000..c34fd47
--- /dev/null
+++ b/src/lib/types/index.ts
@@ -0,0 +1,2 @@
+export * from './dom.js';
+export * from './events.js';
diff --git a/src/lib/utils/browser.ts b/src/lib/utils/browser.ts
new file mode 100644
index 0000000..9294730
--- /dev/null
+++ b/src/lib/utils/browser.ts
@@ -0,0 +1 @@
+export const isBrowser = typeof document !== 'undefined';
diff --git a/src/lib/utils/description.ts b/src/lib/utils/description.ts
new file mode 100644
index 0000000..b44cb39
--- /dev/null
+++ b/src/lib/utils/description.ts
@@ -0,0 +1,51 @@
+// Portions of the code in this file are based on code from Adobe.
+// Original licensing for the following can be found in the NOTICE.txt
+// file in the root directory of this source tree.
+
+import { derived, writable, type Writable } from 'svelte/store';
+import { effect, isBrowser } from '$lib/utils/index.js';
+
+let descriptionId = 0;
+
+type DescriptionNode = { nodeCount: number; element: Element };
+const descriptionNodes = new Map();
+
+export function createDescription(description: Writable) {
+ const id = writable(undefined);
+
+ effect([description], ([$description]) => {
+ if (!$description || !isBrowser) return;
+
+ let desc = descriptionNodes.get($description);
+ if (!desc) {
+ const newId = `si-description-${descriptionId++}`;
+ id.set(newId);
+
+ const node = document.createElement('div');
+ node.id = newId;
+ node.style.display = 'none';
+ node.textContent = $description;
+ document.body.appendChild(node);
+ desc = { nodeCount: 0, element: node };
+ descriptionNodes.set($description, desc);
+ } else {
+ id.set(desc.element.id);
+ }
+
+ desc.nodeCount++;
+ return () => {
+ if (!desc) return;
+
+ if (--desc.nodeCount === 0) {
+ desc.element.remove();
+ descriptionNodes.delete($description);
+ }
+ };
+ });
+
+ const ariaDescribedBy = derived([id, description], ([$id, $description]) => {
+ return $description ? $id : undefined;
+ });
+
+ return ariaDescribedBy;
+}
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index 74fd307..ab154df 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -13,3 +13,5 @@ export * from './platform.js';
export * from './runAfterTransition.js';
export * from './textSelection.js';
export * from './toWritableStores.js';
+export * from './description.js';
+export * from './browser.js';
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 257f471..e438e90 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,12 +1,14 @@