@sandlada/mdc
Version:
@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.
244 lines • 10.3 kB
TypeScript
/**
* @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