UNPKG

@sandlada/mdc

Version:

@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.

244 lines 10.3 kB
/** * @license * Copyright 2025 Kai-Orion & Sandlada * SPDX-License-Identifier: MIT */ import type { ReactiveController, ReactiveControllerHost } from 'lit'; /** * Minimum shape required of any element that acts as a host for * `SelectionController`. Satisfied by `MDCTogglableButton` and any other * form-associated custom element that participates in a named selection group. */ export interface ISelectionControllerHost extends HTMLElement { /** Whether this control is currently selected / checked. */ checked: boolean; /** When true the control is non-interactive and ignored by navigation. */ disabled?: boolean; /** Used by roving-tabindex management. */ tabIndex: number; /** Controls that share the same `name` form a mutual-exclusion group (radio behaviour). */ name?: string; } /** * Configuration options for `SelectionController`. * All fields are optional – unspecified fields retain their defaults. */ export interface ISelectionControllerOptions { /** * When `true` the group behaves like checkboxes: each item can be toggled * independently and no mutual-exclusion enforcement is applied. * When `false` the group behaves like radio buttons: selecting one item * automatically deselects all siblings that share the same `name`. * @default false */ multiple: boolean; /** * When `true` clicking an already-selected control deselects it (toggle). * Typically `true` for checkbox groups and `false` for radio groups. * @default false */ canCancel: boolean; /** * When `true`, receiving focus does NOT automatically select the focused * control (useful for checkbox groups where focus and selection are * independent). * @default false */ preventSelectionDuringInitialFocus: boolean; /** * When `true`, moving focus between controls with arrow keys does NOT * automatically select the newly focused control. * @default false */ preventSelectionDuringSwitching: boolean; /** * When `true`, a synthetic `click` is dispatched on the newly focused * control after each arrow-key navigation. This fires ripple press * animations and other click-based visual effects without re-triggering * `toggleSelection()` (the click is internally suppressed for that purpose). * * Enable for radio-button groups; leave `false` for focus-only groups such * as checkbox lists where arrow keys move focus without selecting. * @default false */ dispatchNavigationClick: boolean; /** * Controls whether `SelectionController` dispatches native `input` and * `change` events when selection changes. * * Set to `false` when the host component needs to dispatch custom events * with richer payloads or custom timing. * @default true */ dispatchInputChangeEvents: boolean; /** * Returns the focusable DOM element for a given host. * Defaults to returning the host itself. */ getFocusableElement: (host: ISelectionControllerHost) => HTMLElement; /** Called once after `hostConnected()` completes. */ onConnected: (host: ISelectionControllerHost) => void; /** Called once after `hostDisconnected()` completes. */ onDisconnected: (host: ISelectionControllerHost) => void; /** Called immediately before `host.checked` is mutated by `toggleSelection()`. */ onBeforeSelect: (host: ISelectionControllerHost) => void; /** Called immediately after `host.checked` is mutated by `toggleSelection()`. */ onAfterSelected: (host: ISelectionControllerHost) => void; /** * Called after `moveFocus()` completes on the **newly focused** host. * Fires after `.focus()`, state mutation, `input`/`change` events, and the * optional navigation click have all been dispatched. * * Use this to perform visual effects that depend on `:focus-visible` not * being reliable (e.g. force-opening a focus ring for a custom element that * browsers may not grant `:focus-visible` to on programmatic focus). * * @param next - the host that just received focus via keyboard navigation. */ onAfterNavigate: (next: ISelectionControllerHost) => void; } /** * A `ReactiveController` that adds radio/checkbox selection semantics, * keyboard navigation, and roving-tabindex management to a custom element. * * ### Group discovery * Controls are grouped by the value of their `name` attribute, queried from * the host's root node (shadow root or document). This mirrors native * `<input type="radio">` / `<input type="checkbox">` behaviour where any * element with the same `name` – regardless of tag name – belongs to the * same logical group. * * ### Usage * ```ts * class MyToggle extends LitElement { * private readonly selectionController = new SelectionController(this, { * multiple: false, * canCancel: false, * }) * // host.addController(this.selectionController) not required here; * // SelectionController calls host.addController internally. * } * ``` */ export declare class SelectionController implements ReactiveController { private readonly host; private root; /** @see ISelectionControllerOptions.multiple */ multiple: boolean; /** @see ISelectionControllerOptions.canCancel */ canCancel: boolean; /** @see ISelectionControllerOptions.preventSelectionDuringInitialFocus */ preventSelectionDuringInitialFocus: boolean; /** @see ISelectionControllerOptions.preventSelectionDuringSwitching */ preventSelectionDuringSwitching: boolean; /** @see ISelectionControllerOptions.dispatchNavigationClick */ dispatchNavigationClick: boolean; /** @see ISelectionControllerOptions.dispatchInputChangeEvents */ dispatchInputChangeEvents: boolean; /** @see ISelectionControllerOptions.getFocusableElement */ getFocusableElement: (host: ISelectionControllerHost) => HTMLElement; /** @see ISelectionControllerOptions.onConnected */ onConnected: (host: ISelectionControllerHost) => void; /** @see ISelectionControllerOptions.onDisconnected */ onDisconnected: (host: ISelectionControllerHost) => void; /** @see ISelectionControllerOptions.onBeforeSelect */ onBeforeSelect: (host: ISelectionControllerHost) => void; /** @see ISelectionControllerOptions.onAfterSelected */ onAfterSelected: (host: ISelectionControllerHost) => void; /** @see ISelectionControllerOptions.onAfterNavigate */ onAfterNavigate: (next: ISelectionControllerHost) => void; constructor(host: ISelectionControllerHost & ReactiveControllerHost, options?: Partial<ISelectionControllerOptions>); /** * Atomically updates any subset of controller options. * Useful when a host property (e.g. `type`) changes and several behavioural * flags must be updated together. * * @example * ```ts * this.selectionController.configure({ * multiple: false, * canCancel: false, * preventSelectionDuringInitialFocus: false, * preventSelectionDuringSwitching: false, * }) * ``` */ configure(options: Partial<ISelectionControllerOptions>): void; /** * Returns all controls that belong to the same selection group as the host. * A group is defined by matching `name` attribute values within the host's * root node (shadow root or document). * * When the host has no `name`, or is not connected, returns `[host]`. */ get controls(): ISelectionControllerHost[]; /** * Toggles or selects the host according to the current `canCancel` / * `multiple` settings, then enforces mutex consistency for radio groups * and updates the roving tabindex. * * No-op when the host is disabled. */ toggleSelection(): void; /** * Called when the host's `checked` property changes programmatically * (i.e. outside of `toggleSelection()`), for example from a property * setter. Enforces mutex consistency for radio groups and refreshes the * roving tabindex. * * @example * ```ts * set checked(value: boolean) { * this._checked = value; * this.selectionController.handleCheckedChange(); * } * ``` */ handleCheckedChange(): void; /** * Ensures that only the host is checked within its group. * No-op when `multiple` is true or the host itself is not checked. */ enforceMutexConsistency(): void; private updateRovingTabindex; /** * Moves focus in the given direction, skipping disabled controls. * Falls back to keeping focus on the current host if no enabled sibling * is found. */ private moveFocus; /** * When Space / Enter toggles the selection we also dispatch a synthetic * (non-composed) click on the host so that visual effects — ripple, * activation-click forwarding — fire exactly as they do for pointer clicks. * * This flag prevents that synthetic click from triggering a second * `toggleSelection()` inside `handleClick`. */ private _suppressNextClick; /** * Hosts registered here are expecting a navigation click dispatched by * `moveFocus()`. `handleClick` on that host will skip `toggleSelection()` * so only visual effects (ripple press animation) are triggered. */ private static readonly _pendingNavigationClick; /** * `true` while a pointer (mouse / touch) is physically held down on the host. * * Used by `handleFocus` to suppress auto-selection when a mouse click * causes focus before the `click` event fires: without this guard, * `handleFocus` would set `checked = true` for radio controls, making * `toggleSelection()` see no state change and therefore not dispatch * `input` / `change` events. */ private _pointerIsDown; private readonly handlePointerDown; private readonly handlePointerUp; private readonly handleFocus; private readonly handleClick; private readonly handleKeyDown; hostConnected(): void; hostDisconnected(): void; hostUpdated(): void; } //# sourceMappingURL=selection-controller.d.ts.map