@windmorph/react 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/Autocomplete.js +1 -1
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/chip/chip-tokens.d.ts +4 -4
- package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
- package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
- package/dist/components/dialog/dialog-tokens.d.ts +1 -1
- package/dist/components/fab-menu/FabMenu.js +2 -2
- package/dist/components/fab-menu/FabMenu.js.map +1 -1
- package/dist/components/menu/Menu.d.ts +13 -0
- package/dist/components/menu/Menu.js +14 -1
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-tokens.d.ts +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/search/search-styles.js +2 -2
- package/dist/components/search/search-styles.js.map +1 -1
- package/dist/components/slider/Slider.js +5 -1
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/text-field/text-field-styles.js +2 -2
- package/dist/components/text-field/text-field-styles.js.map +1 -1
- package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/package.json +1 -1
|
@@ -256,7 +256,7 @@ function Autocomplete(props) {
|
|
|
256
256
|
openOnPress();
|
|
257
257
|
},
|
|
258
258
|
ref: inputRef,
|
|
259
|
-
className: cn("min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none", "text-[color:var(--_md-comp-text-field-input-text-color)]", "caret-[color:var(--_md-comp-text-field-caret-color)]", "placeholder:text-on-surface-variant placeholder:opacity-100", label ? "placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]" : "", "group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38", classNames?.input)
|
|
259
|
+
className: cn("min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none", "text-[color:var(--_md-comp-text-field-input-text-color)]", "caret-[color:var(--_md-comp-text-field-caret-color)]", "text-ellipsis placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis", label ? "placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]" : "", "group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38", classNames?.input)
|
|
260
260
|
})]
|
|
261
261
|
})
|
|
262
262
|
}), /* @__PURE__ */ jsx(OptionPopover, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Autocomplete.js","names":[],"sources":["../../../src/components/autocomplete/Autocomplete.tsx"],"sourcesContent":["'use client';\n\nimport {\n useMemo,\n useRef,\n type AriaAttributes,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useButton,\n useComboBox,\n useFocusRing,\n useHover,\n useObjectRef,\n ListKeyboardDelegate,\n type AriaComboBoxOptions,\n} from 'react-aria';\nimport {\n useComboBoxState,\n type ComboBoxProps,\n type ComboBoxStateOptions,\n type SelectionMode,\n} from 'react-stately/useComboBoxState';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { matchesSearch } from '../../utils/search';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { InputChip } from '../chip/Chip';\nimport {\n DropdownArrow,\n FieldShell,\n fieldState,\n type FieldShellClassNames,\n} from '../select/field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from '../select/option-list';\nimport { useSelectionCap } from '../select/selection-cap';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\n\n/** Words the Autocomplete shows or announces; override them for other languages. */\nexport interface AutocompleteLabels {\n /** Shown when no option matches what was typed. @default \"No results\" */\n noResults: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** The arrow button's name. @default \"Show options\" */\n showOptions: string;\n /** A chip's remove button; the chip's text follows it (\"Remove London\"). @default \"Remove\" */\n remove: string;\n /** With `clearable`: the clear button's name. @default \"Clear\" */\n clear: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\n/** Elements that keep their own presses; a press anywhere else on the field opens it. */\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\nconst DEFAULT_LABELS: AutocompleteLabels = {\n noResults: 'No results',\n loading: 'Loading options',\n showOptions: 'Show options',\n remove: 'Remove',\n clear: 'Clear',\n error: 'Error',\n};\n\nexport interface AutocompleteClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text `<input>`. */\n input?: string;\n /** With `selectionMode=\"multiple\"`: each chosen option's input chip. */\n chip?: string;\n /** With `clearable`: the clear button. */\n clear?: string;\n}\n\n/** Material Symbols `close`. */\nconst ClearIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n);\n\n/** React Aria props this API names the library's way (`disabled`, `readOnly`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isReadOnly'\n | 'label'\n | 'description'\n | 'validationState'\n | 'allowsCustomValue'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange';\n\nexport type AutocompleteProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n ComboBoxProps<T, M>,\n RenamedProps\n> &\n Pick<AriaComboBoxOptions<T, M>, 'id' | 'name' | 'form'> &\n Omit<WrapperRootDOMProps, keyof ComboBoxProps<T, M>> & {\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style, as `TextField`'s: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Free text: whatever is typed stays as the field's text when no option matches, instead\n * of returning to the chosen option's. @default false\n */\n allowCustomValue?: boolean;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the field (56px at 0, 44px at -3) and\n * off its menu's options (44px at 0, 32px at -3). Left out, it takes the density of the\n * nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the reader's choice\n * rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The field's label. Give `aria-label` instead when there's no visible label. */\n label?: ReactNode;\n /** Accessible name when there's no visible label. */\n 'aria-label'?: string;\n /** Shown in the empty input. With a `label`, it appears once the label floats (on focus). */\n placeholder?: string;\n /**\n * Helper text below the field. Replaced visually by the error when invalid, and shown in\n * the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /** Marks the field invalid (in addition to `validate` and `required`). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /** The value can be read and copied but not changed; the menu doesn't open. @default false */\n readOnly?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute, so\n * the field shows its error state as well as announcing it.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * The icon shown before the arrow while the field is invalid (M3 strongly recommends one).\n * Defaults to Material Symbols `error`, named by `labels.error`; `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /**\n * A clear button (M3's clear icon) that appears while there's typed text and empties it,\n * and a single choice with it. It's skipped by Tab, since the text can be deleted from\n * the keyboard.\n * @default false\n */\n clearable?: boolean;\n /**\n * With `selectionMode=\"multiple\"`, the most options that can be chosen; the rest show as\n * disabled until one is removed.\n */\n maxSelections?: number;\n /**\n * Shows a loading indicator in the list, e.g. while options load for what was typed.\n * The indicator's code loads when it first shows, and ahead of that when the field is\n * hovered or focused while this prop is set at all, so pass `loading={false}` (rather than\n * leaving it out) on a field that loads options.\n */\n loading?: boolean;\n /**\n * Whether an option matches what was typed. Defaults to a match anywhere in its text,\n * ignoring case and accents (\"zurich\" finds \"Zürich\"). Not used with controlled `items`.\n */\n filter?: (textValue: string, inputValue: string) => boolean;\n /** Words the Autocomplete shows or announces, for other languages. */\n labels?: Partial<AutocompleteLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: AutocompleteClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the text `<input>`, e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement>;\n };\n\n/**\n * The arrow's press moves focus to the input (React Aria does it on a touch press) without\n * scrolling the page: a press on a field partly off-screen scrolled it into view\n * (2026-10-15). The input's `focus` asks for `preventScroll` only while the press runs.\n */\nfunction withFocusInPlace(input: HTMLInputElement | null, run: () => void) {\n if (!input) return run();\n input.focus = (options) =>\n HTMLElement.prototype.focus.call(input, { ...options, preventScroll: true });\n try {\n run();\n } finally {\n Reflect.deleteProperty(input, 'focus');\n }\n}\n\n/**\n * M3 exposed dropdown menu, editable (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryEditable` anchor): type in the field and the menu below shows the matching\n * options. Single or multiple selection (chosen options as M3 input chips in the field),\n * accent-insensitive filtering by default, options loaded from a server through `items`,\n * `onInputChange` and `loading`, and free text with `allowCustomValue` (React Aria\n * `useComboBox`).\n *\n * @example\n * <Autocomplete label=\"City\" defaultItems={cities}>\n * {(city) => <AutocompleteItem key={city.id}>{city.name}</AutocompleteItem>}\n * </Autocomplete>\n */\nexport function Autocomplete<T extends object, M extends SelectionMode = 'single'>(\n props: AutocompleteProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n disabled = false,\n readOnly: readOnlyProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n leadingIcon,\n errorIcon,\n clearable = false,\n maxSelections,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef: inputRefProp,\n errorMessage,\n filter,\n allowCustomValue,\n ...otherProps\n } = props;\n // Items given as elements (not `items` with a render function) must come from a client.\n if (typeof props.children !== 'function') {\n assertCollectionChildren(\n props.children,\n 'Autocomplete',\n 'AutocompleteItem and AutocompleteSection elements',\n );\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly) ?? false;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const stately = rest as Omit<ComboBoxProps<T, M>, RenamedProps>;\n const multiple = stately.selectionMode === 'multiple';\n const cap = useSelectionCap({\n multiple,\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n\n const inputRef = useObjectRef(inputRefProp);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n allowsCustomValue: allowCustomValue,\n // The arrow keys wrap at the ends of the list, as in menus and lists.\n shouldFocusWrap: true,\n placeholder,\n inputRef,\n buttonRef,\n listBoxRef,\n popoverRef,\n } as AriaComboBoxOptions<T, M>;\n\n const state = useComboBoxState<T, M>({\n ...(ariaProps as unknown as ComboBoxStateOptions<T, M>),\n ...FOCUSABLE_DISABLED_OPTIONS,\n defaultFilter: filter ?? matchesSearch,\n allowsEmptyCollection: true,\n });\n const { collection } = state;\n const { disabledKeys } = state.selectionManager;\n const keyboardDelegate = useMemo(\n () =>\n new ListKeyboardDelegate({\n collection,\n disabledKeys,\n disabledBehavior: 'selection',\n ref: listBoxRef,\n }),\n [collection, disabledKeys],\n );\n const {\n buttonProps,\n inputProps,\n listBoxProps,\n labelProps,\n valueProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useComboBox<T, M>(\n {\n ...ariaProps,\n // The input's arrow keys reach disabled options too (M3: they \"can receive focus\").\n keyboardDelegate,\n },\n state,\n );\n const arrowPress = buttonProps.onPress;\n const { buttonProps: arrowProps } = useButton(\n {\n ...buttonProps,\n onPress:\n arrowPress && ((event) => withFocusInPlace(inputRef.current, () => arrowPress(event))),\n },\n buttonRef,\n );\n // A touch on the arrow, opening the list on its click (see the arrow's handlers).\n const fingerOnArrowRef = useRef(false);\n const { focusProps, isFocused } = useFocusRing({ within: true, isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const chips = multiple ? state.selectedItems : [];\n const removeChip = (key: string | number) => state.selectionManager.toggleSelection(key);\n // Backspace in an empty input removes the last chip, as in other chip inputs.\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const last = chips.at(-1);\n if (event.key === 'Backspace' && state.inputValue === '' && last && !readOnly) {\n event.preventDefault();\n removeChip(last.key);\n }\n };\n\n // M3: \"A menu should open when a person … Selects an element, such as … a text field\", as\n // Compose's editable anchor opens on press. A press on the input or the field opens the\n // whole list, as the arrow does; keyboard focus alone doesn't (unless `menuTrigger=\"focus\"`),\n // and `menuTrigger=\"manual\"` leaves opening to the arrow and the arrow keys.\n const openOnPress = () => {\n if (!state.isOpen && !disabled && !readOnly && ariaProps.menuTrigger !== 'manual') {\n state.open(null, 'manual');\n }\n };\n\n const focused = isFocused || state.isOpen;\n const floated = focused || state.inputValue !== '' || chips.length > 0;\n const showClear = clearable && state.inputValue !== '' && !disabled && !readOnly;\n const clear = () => {\n if (!multiple) state.setValue(null);\n state.setInputValue('');\n // Back to the input in the same field, without scrolling the page (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n };\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n return (\n <>\n <FieldShell\n variant={variant}\n density={density}\n label={label}\n labelProps={labelProps}\n required={required}\n state={fieldState({ disabled, invalid: isInvalid, focused, hovered: isHovered })}\n floated={floated}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n trailing={\n <button\n {...arrowProps}\n // A finger opens the list on the click, with focus in the input. React Aria's\n // touch press never ended here in Chrome (the list stayed closed), and the mouse\n // events a tap emulates focused the arrow, out of the Tab order (2026-10-17). So a\n // touch keeps both away from the arrow; the mouse and the keyboard keep React\n // Aria's press, which opens on the press start.\n onPointerDown={(event) => {\n fingerOnArrowRef.current = event.pointerType !== 'mouse';\n if (!fingerOnArrowRef.current) return arrowProps.onPointerDown?.(event);\n event.preventDefault();\n }}\n onPointerCancel={() => {\n fingerOnArrowRef.current = false;\n }}\n onClick={(event) => {\n if (!fingerOnArrowRef.current) return arrowProps.onClick?.(event);\n fingerOnArrowRef.current = false;\n inputRef.current?.focus({ preventScroll: true });\n if (!disabled && !readOnly) state.toggle(null, 'manual');\n }}\n ref={buttonRef}\n aria-label={labels.showOptions}\n className=\"relative flex size-[40px] items-center justify-center rounded-corner-full outline-none state-layer focus-ring [&>svg]:size-[24px]\"\n >\n <TouchTarget />\n <DropdownArrow open={state.isOpen} />\n </button>\n }\n trailingAction={\n showClear ? (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n // Keeps focus (and the open menu) in the input.\n onPointerDown={(event) => event.preventDefault()}\n onClick={clear}\n className={cn(\n 'relative flex size-[40px] items-center justify-center rounded-corner-full text-on-surface-variant outline-none state-layer [&>svg]:size-[24px]',\n classNames?.clear,\n )}\n >\n <TouchTarget />\n <ClearIcon />\n </button>\n ) : null\n }\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n containerProps={mergeProps(hoverProps, focusProps)}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (its padding, a chip's label, the space between\n // chips) puts the caret in the input and opens the menu, as TextField and Select\n // react to the whole field. The input opens on its own click, and the buttons (the\n // arrow, clear, a chip's remove) keep their own presses. Presses in the menu reach\n // here through React's tree from its portal, and are left alone.\n const target = event.target as Element;\n if (event.currentTarget.contains(target) && !target.closest(INTERACTIVE) && !disabled) {\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n openOnPress();\n }\n }}\n rootProps={rootData}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n multiline={multiple}\n >\n <div\n className={cn(\n 'flex min-w-0 flex-1 flex-wrap items-center gap-[8px]',\n // A row of 32px input chips fits the 56px container (12px above and below), so a\n // first chip doesn't make the field taller and each further row adds 40px. A filled\n // field's floated label takes the top 24px, so there the row stays below it.\n multiple &&\n chips.length > 0 &&\n !(variant === 'filled' && label !== undefined && label !== null) &&\n '-my-[4px]',\n )}\n >\n {chips.map((item) => (\n <InputChip\n key={item.key}\n // The field's own density sets its padding around one 32px row of chips, so the\n // chips keep their size (the field's height is what density changes here).\n density={0}\n onRemove={() => removeChip(item.key)}\n labels={{ remove: labels.remove }}\n disabled={disabled || readOnly}\n className={classNames?.chip}\n >\n {item.textValue}\n </InputChip>\n ))}\n <input\n {...pickDescriptionAria(rest)}\n {...inputProps}\n aria-describedby={renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n // React Aria's element for the chosen values, which the chips show instead.\n valueProps.id,\n ])}\n onKeyDown={(event) => {\n onKeyDown(event);\n inputProps.onKeyDown?.(event);\n }}\n onClick={(event) => {\n inputProps.onClick?.(event);\n openOnPress();\n }}\n ref={inputRef}\n className={cn(\n // A zero flex basis with a 48px minimum: the input shares the chips' last row\n // while 48px are left there, and wraps to a row of its own only below that.\n 'min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)]',\n 'caret-[color:var(--_md-comp-text-field-caret-color)]',\n 'placeholder:text-on-surface-variant placeholder:opacity-100',\n label\n ? 'placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]'\n : '',\n 'group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.input,\n )}\n />\n </div>\n </FieldShell>\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n nonModal\n density={density}\n >\n <OptionList\n animateFiltering\n listBoxProps={listBoxProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n </OptionPopover>\n </>\n );\n}\n\nexport type { Key as AutocompleteKey } from 'react-stately';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Autocomplete.js","names":[],"sources":["../../../src/components/autocomplete/Autocomplete.tsx"],"sourcesContent":["'use client';\n\nimport {\n useMemo,\n useRef,\n type AriaAttributes,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useButton,\n useComboBox,\n useFocusRing,\n useHover,\n useObjectRef,\n ListKeyboardDelegate,\n type AriaComboBoxOptions,\n} from 'react-aria';\nimport {\n useComboBoxState,\n type ComboBoxProps,\n type ComboBoxStateOptions,\n type SelectionMode,\n} from 'react-stately/useComboBoxState';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { matchesSearch } from '../../utils/search';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { InputChip } from '../chip/Chip';\nimport {\n DropdownArrow,\n FieldShell,\n fieldState,\n type FieldShellClassNames,\n} from '../select/field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from '../select/option-list';\nimport { useSelectionCap } from '../select/selection-cap';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\n\n/** Words the Autocomplete shows or announces; override them for other languages. */\nexport interface AutocompleteLabels {\n /** Shown when no option matches what was typed. @default \"No results\" */\n noResults: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** The arrow button's name. @default \"Show options\" */\n showOptions: string;\n /** A chip's remove button; the chip's text follows it (\"Remove London\"). @default \"Remove\" */\n remove: string;\n /** With `clearable`: the clear button's name. @default \"Clear\" */\n clear: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\n/** Elements that keep their own presses; a press anywhere else on the field opens it. */\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\nconst DEFAULT_LABELS: AutocompleteLabels = {\n noResults: 'No results',\n loading: 'Loading options',\n showOptions: 'Show options',\n remove: 'Remove',\n clear: 'Clear',\n error: 'Error',\n};\n\nexport interface AutocompleteClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text `<input>`. */\n input?: string;\n /** With `selectionMode=\"multiple\"`: each chosen option's input chip. */\n chip?: string;\n /** With `clearable`: the clear button. */\n clear?: string;\n}\n\n/** Material Symbols `close`. */\nconst ClearIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n);\n\n/** React Aria props this API names the library's way (`disabled`, `readOnly`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isReadOnly'\n | 'label'\n | 'description'\n | 'validationState'\n | 'allowsCustomValue'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange';\n\nexport type AutocompleteProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n ComboBoxProps<T, M>,\n RenamedProps\n> &\n Pick<AriaComboBoxOptions<T, M>, 'id' | 'name' | 'form'> &\n Omit<WrapperRootDOMProps, keyof ComboBoxProps<T, M>> & {\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style, as `TextField`'s: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Free text: whatever is typed stays as the field's text when no option matches, instead\n * of returning to the chosen option's. @default false\n */\n allowCustomValue?: boolean;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the field (56px at 0, 44px at -3) and\n * off its menu's options (44px at 0, 32px at -3). Left out, it takes the density of the\n * nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the reader's choice\n * rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The field's label. Give `aria-label` instead when there's no visible label. */\n label?: ReactNode;\n /** Accessible name when there's no visible label. */\n 'aria-label'?: string;\n /** Shown in the empty input. With a `label`, it appears once the label floats (on focus). */\n placeholder?: string;\n /**\n * Helper text below the field. Replaced visually by the error when invalid, and shown in\n * the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /** Marks the field invalid (in addition to `validate` and `required`). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /** The value can be read and copied but not changed; the menu doesn't open. @default false */\n readOnly?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute, so\n * the field shows its error state as well as announcing it.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * The icon shown before the arrow while the field is invalid (M3 strongly recommends one).\n * Defaults to Material Symbols `error`, named by `labels.error`; `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /**\n * A clear button (M3's clear icon) that appears while there's typed text and empties it,\n * and a single choice with it. It's skipped by Tab, since the text can be deleted from\n * the keyboard.\n * @default false\n */\n clearable?: boolean;\n /**\n * With `selectionMode=\"multiple\"`, the most options that can be chosen; the rest show as\n * disabled until one is removed.\n */\n maxSelections?: number;\n /**\n * Shows a loading indicator in the list, e.g. while options load for what was typed.\n * The indicator's code loads when it first shows, and ahead of that when the field is\n * hovered or focused while this prop is set at all, so pass `loading={false}` (rather than\n * leaving it out) on a field that loads options.\n */\n loading?: boolean;\n /**\n * Whether an option matches what was typed. Defaults to a match anywhere in its text,\n * ignoring case and accents (\"zurich\" finds \"Zürich\"). Not used with controlled `items`.\n */\n filter?: (textValue: string, inputValue: string) => boolean;\n /** Words the Autocomplete shows or announces, for other languages. */\n labels?: Partial<AutocompleteLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: AutocompleteClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the text `<input>`, e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement>;\n };\n\n/**\n * The arrow's press moves focus to the input (React Aria does it on a touch press) without\n * scrolling the page: a press on a field partly off-screen scrolled it into view\n * (2026-10-15). The input's `focus` asks for `preventScroll` only while the press runs.\n */\nfunction withFocusInPlace(input: HTMLInputElement | null, run: () => void) {\n if (!input) return run();\n input.focus = (options) =>\n HTMLElement.prototype.focus.call(input, { ...options, preventScroll: true });\n try {\n run();\n } finally {\n Reflect.deleteProperty(input, 'focus');\n }\n}\n\n/**\n * M3 exposed dropdown menu, editable (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryEditable` anchor): type in the field and the menu below shows the matching\n * options. Single or multiple selection (chosen options as M3 input chips in the field),\n * accent-insensitive filtering by default, options loaded from a server through `items`,\n * `onInputChange` and `loading`, and free text with `allowCustomValue` (React Aria\n * `useComboBox`).\n *\n * @example\n * <Autocomplete label=\"City\" defaultItems={cities}>\n * {(city) => <AutocompleteItem key={city.id}>{city.name}</AutocompleteItem>}\n * </Autocomplete>\n */\nexport function Autocomplete<T extends object, M extends SelectionMode = 'single'>(\n props: AutocompleteProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n disabled = false,\n readOnly: readOnlyProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n leadingIcon,\n errorIcon,\n clearable = false,\n maxSelections,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef: inputRefProp,\n errorMessage,\n filter,\n allowCustomValue,\n ...otherProps\n } = props;\n // Items given as elements (not `items` with a render function) must come from a client.\n if (typeof props.children !== 'function') {\n assertCollectionChildren(\n props.children,\n 'Autocomplete',\n 'AutocompleteItem and AutocompleteSection elements',\n );\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly) ?? false;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const stately = rest as Omit<ComboBoxProps<T, M>, RenamedProps>;\n const multiple = stately.selectionMode === 'multiple';\n const cap = useSelectionCap({\n multiple,\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n\n const inputRef = useObjectRef(inputRefProp);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n allowsCustomValue: allowCustomValue,\n // The arrow keys wrap at the ends of the list, as in menus and lists.\n shouldFocusWrap: true,\n placeholder,\n inputRef,\n buttonRef,\n listBoxRef,\n popoverRef,\n } as AriaComboBoxOptions<T, M>;\n\n const state = useComboBoxState<T, M>({\n ...(ariaProps as unknown as ComboBoxStateOptions<T, M>),\n ...FOCUSABLE_DISABLED_OPTIONS,\n defaultFilter: filter ?? matchesSearch,\n allowsEmptyCollection: true,\n });\n const { collection } = state;\n const { disabledKeys } = state.selectionManager;\n const keyboardDelegate = useMemo(\n () =>\n new ListKeyboardDelegate({\n collection,\n disabledKeys,\n disabledBehavior: 'selection',\n ref: listBoxRef,\n }),\n [collection, disabledKeys],\n );\n const {\n buttonProps,\n inputProps,\n listBoxProps,\n labelProps,\n valueProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useComboBox<T, M>(\n {\n ...ariaProps,\n // The input's arrow keys reach disabled options too (M3: they \"can receive focus\").\n keyboardDelegate,\n },\n state,\n );\n const arrowPress = buttonProps.onPress;\n const { buttonProps: arrowProps } = useButton(\n {\n ...buttonProps,\n onPress:\n arrowPress && ((event) => withFocusInPlace(inputRef.current, () => arrowPress(event))),\n },\n buttonRef,\n );\n // A touch on the arrow, opening the list on its click (see the arrow's handlers).\n const fingerOnArrowRef = useRef(false);\n const { focusProps, isFocused } = useFocusRing({ within: true, isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const chips = multiple ? state.selectedItems : [];\n const removeChip = (key: string | number) => state.selectionManager.toggleSelection(key);\n // Backspace in an empty input removes the last chip, as in other chip inputs.\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const last = chips.at(-1);\n if (event.key === 'Backspace' && state.inputValue === '' && last && !readOnly) {\n event.preventDefault();\n removeChip(last.key);\n }\n };\n\n // M3: \"A menu should open when a person … Selects an element, such as … a text field\", as\n // Compose's editable anchor opens on press. A press on the input or the field opens the\n // whole list, as the arrow does; keyboard focus alone doesn't (unless `menuTrigger=\"focus\"`),\n // and `menuTrigger=\"manual\"` leaves opening to the arrow and the arrow keys.\n const openOnPress = () => {\n if (!state.isOpen && !disabled && !readOnly && ariaProps.menuTrigger !== 'manual') {\n state.open(null, 'manual');\n }\n };\n\n const focused = isFocused || state.isOpen;\n const floated = focused || state.inputValue !== '' || chips.length > 0;\n const showClear = clearable && state.inputValue !== '' && !disabled && !readOnly;\n const clear = () => {\n if (!multiple) state.setValue(null);\n state.setInputValue('');\n // Back to the input in the same field, without scrolling the page (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n };\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n return (\n <>\n <FieldShell\n variant={variant}\n density={density}\n label={label}\n labelProps={labelProps}\n required={required}\n state={fieldState({ disabled, invalid: isInvalid, focused, hovered: isHovered })}\n floated={floated}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n trailing={\n <button\n {...arrowProps}\n // A finger opens the list on the click, with focus in the input. React Aria's\n // touch press never ended here in Chrome (the list stayed closed), and the mouse\n // events a tap emulates focused the arrow, out of the Tab order (2026-10-17). So a\n // touch keeps both away from the arrow; the mouse and the keyboard keep React\n // Aria's press, which opens on the press start.\n onPointerDown={(event) => {\n fingerOnArrowRef.current = event.pointerType !== 'mouse';\n if (!fingerOnArrowRef.current) return arrowProps.onPointerDown?.(event);\n event.preventDefault();\n }}\n onPointerCancel={() => {\n fingerOnArrowRef.current = false;\n }}\n onClick={(event) => {\n if (!fingerOnArrowRef.current) return arrowProps.onClick?.(event);\n fingerOnArrowRef.current = false;\n inputRef.current?.focus({ preventScroll: true });\n if (!disabled && !readOnly) state.toggle(null, 'manual');\n }}\n ref={buttonRef}\n aria-label={labels.showOptions}\n className=\"relative flex size-[40px] items-center justify-center rounded-corner-full outline-none state-layer focus-ring [&>svg]:size-[24px]\"\n >\n <TouchTarget />\n <DropdownArrow open={state.isOpen} />\n </button>\n }\n trailingAction={\n showClear ? (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n // Keeps focus (and the open menu) in the input.\n onPointerDown={(event) => event.preventDefault()}\n onClick={clear}\n className={cn(\n 'relative flex size-[40px] items-center justify-center rounded-corner-full text-on-surface-variant outline-none state-layer [&>svg]:size-[24px]',\n classNames?.clear,\n )}\n >\n <TouchTarget />\n <ClearIcon />\n </button>\n ) : null\n }\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n containerProps={mergeProps(hoverProps, focusProps)}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (its padding, a chip's label, the space between\n // chips) puts the caret in the input and opens the menu, as TextField and Select\n // react to the whole field. The input opens on its own click, and the buttons (the\n // arrow, clear, a chip's remove) keep their own presses. Presses in the menu reach\n // here through React's tree from its portal, and are left alone.\n const target = event.target as Element;\n if (event.currentTarget.contains(target) && !target.closest(INTERACTIVE) && !disabled) {\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n openOnPress();\n }\n }}\n rootProps={rootData}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n multiline={multiple}\n >\n <div\n className={cn(\n 'flex min-w-0 flex-1 flex-wrap items-center gap-[8px]',\n // A row of 32px input chips fits the 56px container (12px above and below), so a\n // first chip doesn't make the field taller and each further row adds 40px. A filled\n // field's floated label takes the top 24px, so there the row stays below it.\n multiple &&\n chips.length > 0 &&\n !(variant === 'filled' && label !== undefined && label !== null) &&\n '-my-[4px]',\n )}\n >\n {chips.map((item) => (\n <InputChip\n key={item.key}\n // The field's own density sets its padding around one 32px row of chips, so the\n // chips keep their size (the field's height is what density changes here).\n density={0}\n onRemove={() => removeChip(item.key)}\n labels={{ remove: labels.remove }}\n disabled={disabled || readOnly}\n className={classNames?.chip}\n >\n {item.textValue}\n </InputChip>\n ))}\n <input\n {...pickDescriptionAria(rest)}\n {...inputProps}\n aria-describedby={renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n // React Aria's element for the chosen values, which the chips show instead.\n valueProps.id,\n ])}\n onKeyDown={(event) => {\n onKeyDown(event);\n inputProps.onKeyDown?.(event);\n }}\n onClick={(event) => {\n inputProps.onClick?.(event);\n openOnPress();\n }}\n ref={inputRef}\n className={cn(\n // A zero flex basis with a 48px minimum: the input shares the chips' last row\n // while 48px are left there, and wraps to a row of its own only below that.\n 'min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)]',\n 'caret-[color:var(--_md-comp-text-field-caret-color)]',\n // A long hint or value ends in an ellipsis, as a text field's (2026-11-11).\n 'text-ellipsis placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis',\n label\n ? 'placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]'\n : '',\n 'group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.input,\n )}\n />\n </div>\n </FieldShell>\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n nonModal\n density={density}\n >\n <OptionList\n animateFiltering\n listBoxProps={listBoxProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n </OptionPopover>\n </>\n );\n}\n\nexport type { Key as AutocompleteKey } from 'react-stately';\n"],"mappings":"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"}
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/checkbox/checkbox-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Checkbox}: `--md-comp-checkbox-<part>`. */
|
|
5
|
-
export declare const checkboxTokenSet: ComponentTokenSet<"selected-
|
|
5
|
+
export declare const checkboxTokenSet: ComponentTokenSet<"selected-icon-color" | "selected-container-color" | "unselected-outline-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=checkbox-tokens.d.ts.map
|
|
@@ -2,12 +2,12 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/chip/chip-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link AssistChip}: `--md-comp-assist-chip-*`. */
|
|
5
|
-
export declare const assistChipTokenSet: ComponentTokenSet<"
|
|
5
|
+
export declare const assistChipTokenSet: ComponentTokenSet<"label-text-color" | "container-shape" | "with-icon-icon-color" | "flat-outline-color" | "elevated-container-color">;
|
|
6
6
|
/** The tokens of {@link SuggestionChip}: `--md-comp-suggestion-chip-*`. */
|
|
7
|
-
export declare const suggestionChipTokenSet: ComponentTokenSet<"
|
|
7
|
+
export declare const suggestionChipTokenSet: ComponentTokenSet<"label-text-color" | "container-shape" | "flat-outline-color" | "elevated-container-color" | "with-leading-icon-leading-icon-color">;
|
|
8
8
|
/** The tokens of {@link FilterChip}: `--md-comp-filter-chip-*`. */
|
|
9
|
-
export declare const filterChipTokenSet: ComponentTokenSet<"
|
|
9
|
+
export declare const filterChipTokenSet: ComponentTokenSet<"selected-label-text-color" | "unselected-label-text-color" | "container-shape" | "with-leading-icon-unselected-leading-icon-color" | "flat-unselected-outline-color" | "flat-selected-container-color" | "elevated-unselected-container-color">;
|
|
10
10
|
/** The tokens of {@link InputChip}: `--md-comp-input-chip-*`. */
|
|
11
|
-
export declare const inputChipTokenSet: ComponentTokenSet<"
|
|
11
|
+
export declare const inputChipTokenSet: ComponentTokenSet<"selected-container-color" | "selected-label-text-color" | "unselected-label-text-color" | "container-shape" | "unselected-outline-color">;
|
|
12
12
|
//#endregion
|
|
13
13
|
//# sourceMappingURL=chip-tokens.d.ts.map
|
|
@@ -161,8 +161,8 @@ export declare const dockedDatePickerStyles: import("tailwind-variants").TVRetur
|
|
|
161
161
|
header?: import("tailwind-variants").ClassValue;
|
|
162
162
|
view?: import("tailwind-variants").ClassValue;
|
|
163
163
|
item?: import("tailwind-variants").ClassValue;
|
|
164
|
-
check?: import("tailwind-variants").ClassValue;
|
|
165
164
|
list?: import("tailwind-variants").ClassValue;
|
|
165
|
+
check?: import("tailwind-variants").ClassValue;
|
|
166
166
|
menuIcon?: import("tailwind-variants").ClassValue;
|
|
167
167
|
group?: import("tailwind-variants").ClassValue;
|
|
168
168
|
calendar?: import("tailwind-variants").ClassValue;
|
|
@@ -177,8 +177,8 @@ export declare const dockedDatePickerStyles: import("tailwind-variants").TVRetur
|
|
|
177
177
|
header?: import("tailwind-variants").ClassValue;
|
|
178
178
|
view?: import("tailwind-variants").ClassValue;
|
|
179
179
|
item?: import("tailwind-variants").ClassValue;
|
|
180
|
-
check?: import("tailwind-variants").ClassValue;
|
|
181
180
|
list?: import("tailwind-variants").ClassValue;
|
|
181
|
+
check?: import("tailwind-variants").ClassValue;
|
|
182
182
|
menuIcon?: import("tailwind-variants").ClassValue;
|
|
183
183
|
group?: import("tailwind-variants").ClassValue;
|
|
184
184
|
calendar?: import("tailwind-variants").ClassValue;
|
|
@@ -205,8 +205,8 @@ export declare const dockedDatePickerStyles: import("tailwind-variants").TVRetur
|
|
|
205
205
|
header?: import("tailwind-variants").ClassValue;
|
|
206
206
|
view?: import("tailwind-variants").ClassValue;
|
|
207
207
|
item?: import("tailwind-variants").ClassValue;
|
|
208
|
-
check?: import("tailwind-variants").ClassValue;
|
|
209
208
|
list?: import("tailwind-variants").ClassValue;
|
|
209
|
+
check?: import("tailwind-variants").ClassValue;
|
|
210
210
|
menuIcon?: import("tailwind-variants").ClassValue;
|
|
211
211
|
group?: import("tailwind-variants").ClassValue;
|
|
212
212
|
calendar?: import("tailwind-variants").ClassValue;
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/date-picker/date-picker-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link DatePicker}, {@link DateRangePicker} and the date {@link PickerDialog}. */
|
|
5
|
-
export declare const datePickerTokenSet: ComponentTokenSet<"container-color" | "container-
|
|
5
|
+
export declare const datePickerTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "container-shape" | "header-headline-color" | "date-selected-container-color" | "date-selected-label-text-color" | "date-today-container-outline-color" | "date-today-label-text-color" | "range-selection-active-indicator-container-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=date-picker-tokens.d.ts.map
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/dialog/dialog-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Dialog}: `--md-comp-dialog-<part>`. */
|
|
5
|
-
export declare const dialogTokenSet: ComponentTokenSet<"container-color" | "container-
|
|
5
|
+
export declare const dialogTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "container-shape" | "supporting-text-color" | "with-icon-icon-color" | "headline-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=dialog-tokens.d.ts.map
|
|
@@ -213,8 +213,8 @@ function FabMenu({ open, defaultOpen = false, onOpenChange, icon, openIcon, icon
|
|
|
213
213
|
}
|
|
214
214
|
/**
|
|
215
215
|
* A `FabMenuItem`'s action for a `MenuItem`. Menu items are React Stately collection items,
|
|
216
|
-
* which keep any prop
|
|
217
|
-
* `
|
|
216
|
+
* which keep any prop: React Aria's `useMenuItem` runs an item's `onAction`, and an `href`
|
|
217
|
+
* makes the `MenuItem` a real link, followed through the router.
|
|
218
218
|
*/
|
|
219
219
|
function menuItemAction({ onAction, href }) {
|
|
220
220
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FabMenu.js","names":[],"sources":["../../../src/components/fab-menu/FabMenu.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactElement,\n type ReactNode,\n type TransitionEvent,\n} from 'react';\nimport { useReducedMotion } from 'motion/react';\nimport { mergeProps, useFocusWithin, useInteractOutside, useObjectRef } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { IconFaces } from '../../primitives/IconFaces';\nimport type { IconTransition } from '../../primitives/icon-faces-styles';\nimport { TriggerContext } from '../../primitives/TriggerContext';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport { Menu, MenuItem, MenuTrigger, type MenuItemProps } from '../menu/Menu';\nimport type { FabElevation } from '../fab/fab-styles';\nimport { FabTooltip } from '../fab/FabTooltip';\nimport { staggerSteps } from './fab-menu-stagger';\nimport { fabMenuStyles, type FabMenuColor, type FabMenuSize } from './fab-menu-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\ntype Styles = ReturnType<typeof fabMenuStyles>;\n\ninterface FabMenuContextValue {\n styles: Styles;\n /** Items at or below this index (counted from the top) are shown. */\n firstVisible: number;\n count: number;\n close: () => void;\n onLastExit: (event: TransitionEvent<HTMLElement>) => void;\n}\n\nexport type { IconTransition };\n\nconst FabMenuContext = createContext<FabMenuContextValue | null>(null);\nconst ItemIndexContext = createContext(0);\n\ntype AccessibleName =\n | {\n /** Accessible name for the button, also its tooltip. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n /** The id of the element that names the button. */\n 'aria-labelledby'?: string;\n }\n | {\n /** Accessible name for the button, also its tooltip. */\n 'aria-label'?: string;\n /** The id of the element that names the button. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface FabMenuClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The box that keeps the FAB's size while the button shrinks inside it. */\n anchor?: string;\n /** The FAB that opens and closes the menu. */\n button?: string;\n /** The button's icon. */\n icon?: string;\n /** The list of items (`presentation=\"list\"`). */\n list?: string;\n /** The menu (`presentation=\"menu\"`). */\n menu?: string;\n /** The built-in plain tooltip shown on the closed button (`tooltip`). */\n tooltip?: string;\n}\n\n/**\n * `list`: the expressive stack of pills that opens upwards from the button, for compact\n * and medium windows. `menu`: the FAB opens a standard {@link Menu} 4px above it, as M3\n * asks for on the web and in large windows.\n */\nexport type FabMenuPresentation = 'list' | 'menu';\n\ninterface FabMenuOwnProps {\n /** Whether the menu is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called when the menu opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** The button's icon while closed, usually \"add\". Hidden from assistive tech. */\n icon: ReactElement;\n /** The button's icon while open, usually \"close\". Defaults to `icon`. */\n openIcon?: ReactElement;\n /**\n * How the button hands over from `icon` to `openIcon`: `swap` halfway through the morph\n * (the M3 spec and Compose), or `rotate`, where the outgoing icon turns 90° and fades\n * out while the incoming one turns in and fades in. `rotate` goes beyond the M3 spec; it\n * matches Google's Expressive showcase motion. It mirrors in RTL, and reduced motion keeps\n * only the crossfade. Only used with `openIcon`. @default \"swap\"\n */\n iconTransition?: IconTransition;\n /** The FAB size the button starts at; open, it is always the 56px close button. @default \"default\" */\n size?: FabMenuSize;\n /** The button's and items' colour scheme, from M3's FAB menu colour sets. @default \"primary\" */\n color?: FabMenuColor;\n /**\n * The button's and items' elevation, as on `Fab`: level 3 by default (the spec), `lowered`\n * level 1, or `flat` level 0. @default \"default\"\n */\n elevation?: FabElevation;\n /**\n * How the items show: `list`, the expressive stack of pills above the button, or `menu`,\n * a standard menu 4px above it (M3's choice for the web and large windows).\n * @default \"list\"\n */\n presentation?: FabMenuPresentation;\n /**\n * While closed, shows `aria-label` in a plain tooltip on hover and keyboard focus, as M3\n * asks of FABs on the web. Open, the button is the close button and has no tooltip. It\n * steps aside inside your own `TooltipTrigger`. @default true\n */\n tooltip?: boolean;\n /**\n * Shows the button. Hiding it closes the menu, shrinks the button towards its centre and\n * fades it out; showing it expands it back (the FAB's appear / disappear motion). A\n * hidden menu is `inert`. @default true\n */\n visible?: boolean;\n /**\n * Plays the appearing animation when the menu mounts. Change its `key` on a page or tab\n * change to replay it. @default false\n */\n appear?: boolean;\n /** Two to six `FabMenuItem` elements, top to bottom. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: FabMenuClassNames;\n}\n\nexport type FabMenuProps = FabMenuOwnProps &\n AccessibleName &\n Omit<\n ComponentPropsWithRef<'div'>,\n keyof FabMenuOwnProps | 'aria-label' | 'aria-labelledby' | 'color'\n >;\n\nconst ITEM_SELECTOR = '[data-fab-menu-item]:not([inert])';\n\n/**\n * M3 Expressive FAB menu: a FAB that opens two to six related actions above it. The button\n * shrinks into a 56px close button while the items (`FabMenuItem`) appear one after\n * another from the button upwards, each springing out to its full width; on short windows\n * they scroll behind the close button. Under reduced motion the button changes at once and\n * the items fade in and out together. Position the menu on the window's trailing edge\n * with `className` (`fixed end-4 bottom-4`, and `end-6 bottom-6` in large windows). Keep it\n * out of normal flow: `fixed`, or at the bottom of a box. In flow, the open list pushes the\n * button down and can scroll the page.\n *\n * The button is named by `aria-label`, which it shows in a tooltip while closed, and reports\n * `aria-expanded`. Tab or ↓ moves from the button to the top item; ↑ / ↓ move between\n * items and back to the button. Escape, an outside press, focus leaving the menu or\n * choosing an item closes it. With `presentation=\"menu\"` the\n * button opens a standard `Menu` instead, with menu semantics and keyboard support.\n *\n * @example\n * <FabMenu icon={<AddIcon />} openIcon={<CloseIcon />} aria-label=\"Create\" className=\"fixed end-4 bottom-4\">\n * <FabMenuItem icon={<ReplyIcon />} onPress={reply}>Reply</FabMenuItem>\n * <FabMenuItem icon={<ArchiveIcon />} onPress={archive}>Archive</FabMenuItem>\n * </FabMenu>\n */\nexport function FabMenu({\n open,\n defaultOpen = false,\n onOpenChange,\n icon,\n openIcon,\n iconTransition,\n size,\n color,\n elevation,\n presentation = 'list',\n tooltip = true,\n visible = true,\n appear = false,\n children,\n classNames,\n className,\n ref,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...rest\n}: FabMenuProps) {\n const [isOpenState, setOpen] = useControlledState(open, defaultOpen, onOpenChange);\n const isOpen = isOpenState && visible;\n const items = Children.toArray(children).filter(isValidElement<FabMenuItemProps>);\n const total = items.length;\n if (process.env.NODE_ENV !== 'production' && (total < 2 || total > 6)) {\n console.warn(\n `[@windmorph/react] A FabMenu holds 2 to 6 items (M3); this one has ${total}. Use a FAB for a single action.`,\n );\n }\n const isMenu = presentation === 'menu';\n const styles = fabMenuStyles({ size, color, elevation });\n const menuColor = color ?? 'primary';\n const listId = useId();\n const rootRef = useObjectRef(ref);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n\n // Hiding the button closes the menu.\n useEffect(() => {\n if (!visible && isOpenState) setOpen(false);\n }, [visible, isOpenState, setOpen]);\n\n // The visible item count steps through Compose's stagger spring (fab-menu-stagger.ts);\n // under reduced motion every item shows and hides with the menu, in the same render.\n const reducedMotion = useReducedMotion() ?? false;\n const target = isOpen ? total : 0;\n const [staggered, setStaggered] = useState(target);\n const count = reducedMotion ? target : staggered;\n const countRef = useRef(count);\n useEffect(() => {\n if (reducedMotion) {\n countRef.current = target;\n return;\n }\n const steps = staggerSteps(countRef.current, target);\n const timers = steps.map(({ at, count: next }) =>\n setTimeout(() => {\n countRef.current = next;\n setStaggered(next);\n }, at),\n );\n return () => timers.forEach(clearTimeout);\n }, [target, reducedMotion]);\n\n // The list stays laid out until the last item has faded.\n const { isPresent, exitProps } = usePresence(isOpen || count > 0);\n\n const close = () => {\n if (listRef.current?.contains(document.activeElement)) buttonRef.current?.focus();\n setOpen(false);\n };\n\n useInteractOutside({ ref: rootRef, onInteractOutside: close, isDisabled: !isOpen || isMenu });\n\n // Tabbing out of the open menu closes it, as an outside press does, so the open list never\n // covers what takes focus next (M3: don't obscure the focus indicator; 2026-10-15). Focus\n // leaving the window (no new target) keeps it open. Always tracked, so focus that came in\n // while it was closed (the button, then Enter) closes it too as it leaves (2026-10-17).\n const { focusWithinProps } = useFocusWithin({\n onBlurWithin: (event) => {\n if (!isOpen || isMenu) return;\n const next = event.relatedTarget;\n if (next instanceof Node && !rootRef.current?.contains(next)) setOpen(false);\n },\n });\n\n // A list taller than the window scrolls; it opens showing the items nearest the button.\n useLayoutEffect(() => {\n const list = listRef.current;\n if (isOpen && list) list.scrollTop = list.scrollHeight;\n }, [isOpen]);\n\n const focusItem = (from: Element, step: 1 | -1) => {\n const all = [...(listRef.current?.querySelectorAll<HTMLElement>(ITEM_SELECTOR) ?? [])];\n const index = all.indexOf(from as HTMLElement);\n const next = index === -1 ? all[0] : all[index + step];\n (next ?? buttonRef.current)?.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!isOpen || isMenu) return;\n const onButton = event.target === buttonRef.current;\n if (event.key === 'Escape') {\n event.preventDefault();\n buttonRef.current?.focus();\n setOpen(false);\n } else if (event.key === 'ArrowDown') {\n event.preventDefault();\n focusItem(event.target as Element, 1);\n } else if (event.key === 'ArrowUp' && !onButton) {\n event.preventDefault();\n focusItem(event.target as Element, -1);\n }\n };\n\n const context: FabMenuContextValue = {\n styles,\n firstVisible: total - count,\n count: total,\n close,\n onLastExit: exitProps.onTransitionEnd,\n };\n\n const toggle = (\n <ButtonBase\n ref={buttonRef}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n {...(isMenu\n ? {}\n : {\n 'aria-expanded': isOpen,\n 'aria-controls': isPresent ? listId : undefined,\n onPress: () => setOpen(!isOpen),\n })}\n data-open={(isOpen && !isMenu) || undefined}\n data-slot=\"button\"\n // Closed, the button is a FAB of the menu's container colour, painted by its tokens.\n data-tokens=\"md-comp-fab\"\n data-color={`${menuColor}-container`}\n className={styles.button({ class: classNames?.button })}\n >\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {openIcon ? (\n <IconFaces\n icon={icon}\n alternateIcon={openIcon}\n alternate={isOpen && !isMenu}\n transition={iconTransition}\n />\n ) : (\n icon\n )}\n </span>\n </ButtonBase>\n );\n\n const button = (\n <span\n data-hidden={visible ? undefined : true}\n data-appear={appear || undefined}\n className={styles.anchor({ class: classNames?.anchor })}\n >\n {tooltip && ariaLabel ? (\n <FabTooltip available={!isOpen} label={ariaLabel} className={classNames?.tooltip}>\n {toggle}\n </FabTooltip>\n ) : (\n toggle\n )}\n </span>\n );\n\n return (\n <div\n {...mergeProps(rest, focusWithinProps)}\n ref={rootRef}\n inert={!visible || undefined}\n // The component tokens' hook (fab-menu-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-fab-menu', rest)}\n data-color={menuColor}\n data-item-color={`${menuColor}-container`}\n data-open={isOpen || undefined}\n data-presentation={presentation}\n className={styles.root({ class: cn(classNames?.root, className) })}\n // The app's own handler runs first, then the menu's keys.\n onKeyDown={(event) => {\n rest.onKeyDown?.(event);\n onKeyDown(event);\n }}\n >\n {isMenu ? (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n {button}\n <Menu\n placement=\"top-end\"\n offset={4}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n classNames={{ panel: styles.menu({ class: classNames?.menu }) }}\n >\n {items.map((item) => (\n <MenuItem\n key={item.key ?? undefined}\n leadingIcon={item.props.icon}\n {...menuItemAction(item.props)}\n className={item.props.className}\n >\n {item.props.children}\n </MenuItem>\n ))}\n </Menu>\n </MenuTrigger>\n ) : (\n <>\n {button}\n <div\n ref={listRef}\n id={listId}\n hidden={!isPresent}\n className={styles.list({ class: classNames?.list })}\n >\n {/* An enclosing trigger (e.g. a TooltipTrigger) is for the button, not the items. */}\n <TriggerContext value={null}>\n <FabMenuContext value={context}>\n {items.map((item, index) => (\n <ItemIndexContext key={item.key} value={index}>\n {item}\n </ItemIndexContext>\n ))}\n </FabMenuContext>\n </TriggerContext>\n </div>\n </>\n )}\n </div>\n );\n}\n\n/**\n * A `FabMenuItem`'s action for a `MenuItem`. Menu items are React Stately collection items,\n * which keep any prop; React Aria's `useMenuItem` runs an item's `onAction` and opens its\n * `href` through the router.\n */\nfunction menuItemAction({ onAction, href }: FabMenuItemProps): Partial<MenuItemProps> {\n return { onAction, href } as Partial<MenuItemProps>;\n}\n\nexport interface FabMenuItemClassNames {\n /** The item's button, link or menu item, merged with `className`. */\n root?: string;\n /** The row holding the icon and the label. */\n content?: string;\n /** The icon. */\n icon?: string;\n /** The item's label text. */\n label?: string;\n}\n\ninterface FabMenuItemOwnProps {\n /**\n * The item's icon. Hidden from assistive tech; the label names the item. M3 asks for an\n * icon on every item, since it tells the items apart; leave it out only if necessary.\n */\n icon?: ReactElement;\n /** The label, which M3 always requires. */\n children: ReactNode;\n /**\n * Runs when the item is chosen, in either presentation. (`onPress` works only in the\n * list, since a menu item has no press event.)\n */\n onAction?: () => void;\n /** Classes for each part, by slot. */\n classNames?: FabMenuItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof FabMenuItemOwnProps | 'disabled' | 'toggle'>;\n\nexport type FabMenuItemProps = FabMenuItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * An action in a {@link FabMenu}: a 56px pill with an icon and a label (or, with\n * `presentation=\"menu\"`, a menu item). Choosing it calls `onAction` (and, in the list,\n * `onPress`), or follows `href`, and closes the menu.\n */\nexport function FabMenuItem({\n icon,\n children,\n classNames,\n className,\n style,\n onPress,\n onAction,\n ...rest\n}: FabMenuItemProps) {\n const menu = useContext(FabMenuContext);\n if (!menu) throw new Error('FabMenuItem must be rendered inside a FabMenu');\n const index = useContext(ItemIndexContext);\n const visible = index >= menu.firstVisible;\n const isLast = index === menu.count - 1;\n const { styles } = menu;\n\n // The item's width springs between 0 and its content's natural width.\n const contentRef = useRef<HTMLSpanElement>(null);\n const [width, setWidth] = useState(0);\n useLayoutEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n const measure = () => setWidth(content.offsetWidth);\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(content);\n return () => observer.disconnect();\n }, []);\n // The list is hidden while the menu is closed, where the content measures 0; measure it\n // again as the item shows, before paint, so it never shows a frame at width 0.\n useLayoutEffect(() => {\n const content = contentRef.current;\n if (visible && content) setWidth(content.offsetWidth);\n }, [visible]);\n\n const props = {\n ...rest,\n inert: !visible,\n 'data-visible': visible || undefined,\n 'data-fab-menu-item': '',\n 'data-slot': 'item',\n style: { ...style, width: visible ? width : 0 },\n className: styles.item({ class: cn(classNames?.root, className) }),\n onPress: (event: Parameters<NonNullable<ButtonBaseActionProps['onPress']>>[0]) => {\n onPress?.(event);\n onAction?.();\n menu.close();\n },\n onTransitionEnd: isLast ? menu.onLastExit : undefined,\n };\n\n return (\n <ButtonBase {...(props as ButtonBaseActionProps)}>\n <span ref={contentRef} className={styles.itemContent({ class: classNames?.content })}>\n {icon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"item-icon\"\n className={styles.itemIcon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n )}\n <span className={classNames?.label}>{children}</span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FabMenu.js","names":[],"sources":["../../../src/components/fab-menu/FabMenu.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactElement,\n type ReactNode,\n type TransitionEvent,\n} from 'react';\nimport { useReducedMotion } from 'motion/react';\nimport { mergeProps, useFocusWithin, useInteractOutside, useObjectRef } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { IconFaces } from '../../primitives/IconFaces';\nimport type { IconTransition } from '../../primitives/icon-faces-styles';\nimport { TriggerContext } from '../../primitives/TriggerContext';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport { Menu, MenuItem, MenuTrigger, type MenuItemProps } from '../menu/Menu';\nimport type { FabElevation } from '../fab/fab-styles';\nimport { FabTooltip } from '../fab/FabTooltip';\nimport { staggerSteps } from './fab-menu-stagger';\nimport { fabMenuStyles, type FabMenuColor, type FabMenuSize } from './fab-menu-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\ntype Styles = ReturnType<typeof fabMenuStyles>;\n\ninterface FabMenuContextValue {\n styles: Styles;\n /** Items at or below this index (counted from the top) are shown. */\n firstVisible: number;\n count: number;\n close: () => void;\n onLastExit: (event: TransitionEvent<HTMLElement>) => void;\n}\n\nexport type { IconTransition };\n\nconst FabMenuContext = createContext<FabMenuContextValue | null>(null);\nconst ItemIndexContext = createContext(0);\n\ntype AccessibleName =\n | {\n /** Accessible name for the button, also its tooltip. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n /** The id of the element that names the button. */\n 'aria-labelledby'?: string;\n }\n | {\n /** Accessible name for the button, also its tooltip. */\n 'aria-label'?: string;\n /** The id of the element that names the button. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface FabMenuClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The box that keeps the FAB's size while the button shrinks inside it. */\n anchor?: string;\n /** The FAB that opens and closes the menu. */\n button?: string;\n /** The button's icon. */\n icon?: string;\n /** The list of items (`presentation=\"list\"`). */\n list?: string;\n /** The menu (`presentation=\"menu\"`). */\n menu?: string;\n /** The built-in plain tooltip shown on the closed button (`tooltip`). */\n tooltip?: string;\n}\n\n/**\n * `list`: the expressive stack of pills that opens upwards from the button, for compact\n * and medium windows. `menu`: the FAB opens a standard {@link Menu} 4px above it, as M3\n * asks for on the web and in large windows.\n */\nexport type FabMenuPresentation = 'list' | 'menu';\n\ninterface FabMenuOwnProps {\n /** Whether the menu is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called when the menu opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** The button's icon while closed, usually \"add\". Hidden from assistive tech. */\n icon: ReactElement;\n /** The button's icon while open, usually \"close\". Defaults to `icon`. */\n openIcon?: ReactElement;\n /**\n * How the button hands over from `icon` to `openIcon`: `swap` halfway through the morph\n * (the M3 spec and Compose), or `rotate`, where the outgoing icon turns 90° and fades\n * out while the incoming one turns in and fades in. `rotate` goes beyond the M3 spec; it\n * matches Google's Expressive showcase motion. It mirrors in RTL, and reduced motion keeps\n * only the crossfade. Only used with `openIcon`. @default \"swap\"\n */\n iconTransition?: IconTransition;\n /** The FAB size the button starts at; open, it is always the 56px close button. @default \"default\" */\n size?: FabMenuSize;\n /** The button's and items' colour scheme, from M3's FAB menu colour sets. @default \"primary\" */\n color?: FabMenuColor;\n /**\n * The button's and items' elevation, as on `Fab`: level 3 by default (the spec), `lowered`\n * level 1, or `flat` level 0. @default \"default\"\n */\n elevation?: FabElevation;\n /**\n * How the items show: `list`, the expressive stack of pills above the button, or `menu`,\n * a standard menu 4px above it (M3's choice for the web and large windows).\n * @default \"list\"\n */\n presentation?: FabMenuPresentation;\n /**\n * While closed, shows `aria-label` in a plain tooltip on hover and keyboard focus, as M3\n * asks of FABs on the web. Open, the button is the close button and has no tooltip. It\n * steps aside inside your own `TooltipTrigger`. @default true\n */\n tooltip?: boolean;\n /**\n * Shows the button. Hiding it closes the menu, shrinks the button towards its centre and\n * fades it out; showing it expands it back (the FAB's appear / disappear motion). A\n * hidden menu is `inert`. @default true\n */\n visible?: boolean;\n /**\n * Plays the appearing animation when the menu mounts. Change its `key` on a page or tab\n * change to replay it. @default false\n */\n appear?: boolean;\n /** Two to six `FabMenuItem` elements, top to bottom. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: FabMenuClassNames;\n}\n\nexport type FabMenuProps = FabMenuOwnProps &\n AccessibleName &\n Omit<\n ComponentPropsWithRef<'div'>,\n keyof FabMenuOwnProps | 'aria-label' | 'aria-labelledby' | 'color'\n >;\n\nconst ITEM_SELECTOR = '[data-fab-menu-item]:not([inert])';\n\n/**\n * M3 Expressive FAB menu: a FAB that opens two to six related actions above it. The button\n * shrinks into a 56px close button while the items (`FabMenuItem`) appear one after\n * another from the button upwards, each springing out to its full width; on short windows\n * they scroll behind the close button. Under reduced motion the button changes at once and\n * the items fade in and out together. Position the menu on the window's trailing edge\n * with `className` (`fixed end-4 bottom-4`, and `end-6 bottom-6` in large windows). Keep it\n * out of normal flow: `fixed`, or at the bottom of a box. In flow, the open list pushes the\n * button down and can scroll the page.\n *\n * The button is named by `aria-label`, which it shows in a tooltip while closed, and reports\n * `aria-expanded`. Tab or ↓ moves from the button to the top item; ↑ / ↓ move between\n * items and back to the button. Escape, an outside press, focus leaving the menu or\n * choosing an item closes it. With `presentation=\"menu\"` the\n * button opens a standard `Menu` instead, with menu semantics and keyboard support.\n *\n * @example\n * <FabMenu icon={<AddIcon />} openIcon={<CloseIcon />} aria-label=\"Create\" className=\"fixed end-4 bottom-4\">\n * <FabMenuItem icon={<ReplyIcon />} onPress={reply}>Reply</FabMenuItem>\n * <FabMenuItem icon={<ArchiveIcon />} onPress={archive}>Archive</FabMenuItem>\n * </FabMenu>\n */\nexport function FabMenu({\n open,\n defaultOpen = false,\n onOpenChange,\n icon,\n openIcon,\n iconTransition,\n size,\n color,\n elevation,\n presentation = 'list',\n tooltip = true,\n visible = true,\n appear = false,\n children,\n classNames,\n className,\n ref,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...rest\n}: FabMenuProps) {\n const [isOpenState, setOpen] = useControlledState(open, defaultOpen, onOpenChange);\n const isOpen = isOpenState && visible;\n const items = Children.toArray(children).filter(isValidElement<FabMenuItemProps>);\n const total = items.length;\n if (process.env.NODE_ENV !== 'production' && (total < 2 || total > 6)) {\n console.warn(\n `[@windmorph/react] A FabMenu holds 2 to 6 items (M3); this one has ${total}. Use a FAB for a single action.`,\n );\n }\n const isMenu = presentation === 'menu';\n const styles = fabMenuStyles({ size, color, elevation });\n const menuColor = color ?? 'primary';\n const listId = useId();\n const rootRef = useObjectRef(ref);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n\n // Hiding the button closes the menu.\n useEffect(() => {\n if (!visible && isOpenState) setOpen(false);\n }, [visible, isOpenState, setOpen]);\n\n // The visible item count steps through Compose's stagger spring (fab-menu-stagger.ts);\n // under reduced motion every item shows and hides with the menu, in the same render.\n const reducedMotion = useReducedMotion() ?? false;\n const target = isOpen ? total : 0;\n const [staggered, setStaggered] = useState(target);\n const count = reducedMotion ? target : staggered;\n const countRef = useRef(count);\n useEffect(() => {\n if (reducedMotion) {\n countRef.current = target;\n return;\n }\n const steps = staggerSteps(countRef.current, target);\n const timers = steps.map(({ at, count: next }) =>\n setTimeout(() => {\n countRef.current = next;\n setStaggered(next);\n }, at),\n );\n return () => timers.forEach(clearTimeout);\n }, [target, reducedMotion]);\n\n // The list stays laid out until the last item has faded.\n const { isPresent, exitProps } = usePresence(isOpen || count > 0);\n\n const close = () => {\n if (listRef.current?.contains(document.activeElement)) buttonRef.current?.focus();\n setOpen(false);\n };\n\n useInteractOutside({ ref: rootRef, onInteractOutside: close, isDisabled: !isOpen || isMenu });\n\n // Tabbing out of the open menu closes it, as an outside press does, so the open list never\n // covers what takes focus next (M3: don't obscure the focus indicator; 2026-10-15). Focus\n // leaving the window (no new target) keeps it open. Always tracked, so focus that came in\n // while it was closed (the button, then Enter) closes it too as it leaves (2026-10-17).\n const { focusWithinProps } = useFocusWithin({\n onBlurWithin: (event) => {\n if (!isOpen || isMenu) return;\n const next = event.relatedTarget;\n if (next instanceof Node && !rootRef.current?.contains(next)) setOpen(false);\n },\n });\n\n // A list taller than the window scrolls; it opens showing the items nearest the button.\n useLayoutEffect(() => {\n const list = listRef.current;\n if (isOpen && list) list.scrollTop = list.scrollHeight;\n }, [isOpen]);\n\n const focusItem = (from: Element, step: 1 | -1) => {\n const all = [...(listRef.current?.querySelectorAll<HTMLElement>(ITEM_SELECTOR) ?? [])];\n const index = all.indexOf(from as HTMLElement);\n const next = index === -1 ? all[0] : all[index + step];\n (next ?? buttonRef.current)?.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!isOpen || isMenu) return;\n const onButton = event.target === buttonRef.current;\n if (event.key === 'Escape') {\n event.preventDefault();\n buttonRef.current?.focus();\n setOpen(false);\n } else if (event.key === 'ArrowDown') {\n event.preventDefault();\n focusItem(event.target as Element, 1);\n } else if (event.key === 'ArrowUp' && !onButton) {\n event.preventDefault();\n focusItem(event.target as Element, -1);\n }\n };\n\n const context: FabMenuContextValue = {\n styles,\n firstVisible: total - count,\n count: total,\n close,\n onLastExit: exitProps.onTransitionEnd,\n };\n\n const toggle = (\n <ButtonBase\n ref={buttonRef}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n {...(isMenu\n ? {}\n : {\n 'aria-expanded': isOpen,\n 'aria-controls': isPresent ? listId : undefined,\n onPress: () => setOpen(!isOpen),\n })}\n data-open={(isOpen && !isMenu) || undefined}\n data-slot=\"button\"\n // Closed, the button is a FAB of the menu's container colour, painted by its tokens.\n data-tokens=\"md-comp-fab\"\n data-color={`${menuColor}-container`}\n className={styles.button({ class: classNames?.button })}\n >\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {openIcon ? (\n <IconFaces\n icon={icon}\n alternateIcon={openIcon}\n alternate={isOpen && !isMenu}\n transition={iconTransition}\n />\n ) : (\n icon\n )}\n </span>\n </ButtonBase>\n );\n\n const button = (\n <span\n data-hidden={visible ? undefined : true}\n data-appear={appear || undefined}\n className={styles.anchor({ class: classNames?.anchor })}\n >\n {tooltip && ariaLabel ? (\n <FabTooltip available={!isOpen} label={ariaLabel} className={classNames?.tooltip}>\n {toggle}\n </FabTooltip>\n ) : (\n toggle\n )}\n </span>\n );\n\n return (\n <div\n {...mergeProps(rest, focusWithinProps)}\n ref={rootRef}\n inert={!visible || undefined}\n // The component tokens' hook (fab-menu-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-fab-menu', rest)}\n data-color={menuColor}\n data-item-color={`${menuColor}-container`}\n data-open={isOpen || undefined}\n data-presentation={presentation}\n className={styles.root({ class: cn(classNames?.root, className) })}\n // The app's own handler runs first, then the menu's keys.\n onKeyDown={(event) => {\n rest.onKeyDown?.(event);\n onKeyDown(event);\n }}\n >\n {isMenu ? (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n {button}\n <Menu\n placement=\"top-end\"\n offset={4}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n classNames={{ panel: styles.menu({ class: classNames?.menu }) }}\n >\n {items.map((item) => (\n <MenuItem\n key={item.key ?? undefined}\n leadingIcon={item.props.icon}\n {...menuItemAction(item.props)}\n className={item.props.className}\n >\n {item.props.children}\n </MenuItem>\n ))}\n </Menu>\n </MenuTrigger>\n ) : (\n <>\n {button}\n <div\n ref={listRef}\n id={listId}\n hidden={!isPresent}\n className={styles.list({ class: classNames?.list })}\n >\n {/* An enclosing trigger (e.g. a TooltipTrigger) is for the button, not the items. */}\n <TriggerContext value={null}>\n <FabMenuContext value={context}>\n {items.map((item, index) => (\n <ItemIndexContext key={item.key} value={index}>\n {item}\n </ItemIndexContext>\n ))}\n </FabMenuContext>\n </TriggerContext>\n </div>\n </>\n )}\n </div>\n );\n}\n\n/**\n * A `FabMenuItem`'s action for a `MenuItem`. Menu items are React Stately collection items,\n * which keep any prop: React Aria's `useMenuItem` runs an item's `onAction`, and an `href`\n * makes the `MenuItem` a real link, followed through the router.\n */\nfunction menuItemAction({ onAction, href }: FabMenuItemProps): Partial<MenuItemProps> {\n return { onAction, href } as Partial<MenuItemProps>;\n}\n\nexport interface FabMenuItemClassNames {\n /** The item's button, link or menu item, merged with `className`. */\n root?: string;\n /** The row holding the icon and the label. */\n content?: string;\n /** The icon. */\n icon?: string;\n /** The item's label text. */\n label?: string;\n}\n\ninterface FabMenuItemOwnProps {\n /**\n * The item's icon. Hidden from assistive tech; the label names the item. M3 asks for an\n * icon on every item, since it tells the items apart; leave it out only if necessary.\n */\n icon?: ReactElement;\n /** The label, which M3 always requires. */\n children: ReactNode;\n /**\n * Runs when the item is chosen, in either presentation. (`onPress` works only in the\n * list, since a menu item has no press event.)\n */\n onAction?: () => void;\n /** Classes for each part, by slot. */\n classNames?: FabMenuItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof FabMenuItemOwnProps | 'disabled' | 'toggle'>;\n\nexport type FabMenuItemProps = FabMenuItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * An action in a {@link FabMenu}: a 56px pill with an icon and a label (or, with\n * `presentation=\"menu\"`, a menu item). Choosing it calls `onAction` (and, in the list,\n * `onPress`), or follows `href`, and closes the menu.\n */\nexport function FabMenuItem({\n icon,\n children,\n classNames,\n className,\n style,\n onPress,\n onAction,\n ...rest\n}: FabMenuItemProps) {\n const menu = useContext(FabMenuContext);\n if (!menu) throw new Error('FabMenuItem must be rendered inside a FabMenu');\n const index = useContext(ItemIndexContext);\n const visible = index >= menu.firstVisible;\n const isLast = index === menu.count - 1;\n const { styles } = menu;\n\n // The item's width springs between 0 and its content's natural width.\n const contentRef = useRef<HTMLSpanElement>(null);\n const [width, setWidth] = useState(0);\n useLayoutEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n const measure = () => setWidth(content.offsetWidth);\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(content);\n return () => observer.disconnect();\n }, []);\n // The list is hidden while the menu is closed, where the content measures 0; measure it\n // again as the item shows, before paint, so it never shows a frame at width 0.\n useLayoutEffect(() => {\n const content = contentRef.current;\n if (visible && content) setWidth(content.offsetWidth);\n }, [visible]);\n\n const props = {\n ...rest,\n inert: !visible,\n 'data-visible': visible || undefined,\n 'data-fab-menu-item': '',\n 'data-slot': 'item',\n style: { ...style, width: visible ? width : 0 },\n className: styles.item({ class: cn(classNames?.root, className) }),\n onPress: (event: Parameters<NonNullable<ButtonBaseActionProps['onPress']>>[0]) => {\n onPress?.(event);\n onAction?.();\n menu.close();\n },\n onTransitionEnd: isLast ? menu.onLastExit : undefined,\n };\n\n return (\n <ButtonBase {...(props as ButtonBaseActionProps)}>\n <span ref={contentRef} className={styles.itemContent({ class: classNames?.content })}>\n {icon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"item-icon\"\n className={styles.itemIcon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n )}\n <span className={classNames?.label}>{children}</span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
@@ -26,6 +26,19 @@ export interface MenuItemProps {
|
|
|
26
26
|
shortcut?: string;
|
|
27
27
|
/** Icon at the end (an item with a submenu shows an arrow by default). */
|
|
28
28
|
trailingIcon?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Makes the item a link: a real `<a>` with the item's look, role and keys, so the browser's
|
|
31
|
+
* link menu (open in a new tab, copy or save the link) and modified clicks work. A press,
|
|
32
|
+
* Enter or Space follows it, through a `RouterProvider` (such as `NextRouterProvider`) on a
|
|
33
|
+
* plain click, and closes the menu. A disabled item, or one that opens a submenu, isn't a link.
|
|
34
|
+
*/
|
|
35
|
+
href?: string;
|
|
36
|
+
/** A link item's target; `_blank` adds `rel="noopener noreferrer"` unless `rel` is given. */
|
|
37
|
+
target?: string;
|
|
38
|
+
/** A link item's `rel`, e.g. `"nofollow"`. */
|
|
39
|
+
rel?: string;
|
|
40
|
+
/** Downloads the link item's file instead of opening it (optionally with a file name). */
|
|
41
|
+
download?: boolean | string;
|
|
29
42
|
/** Accessible name when the label isn't plain text. */
|
|
30
43
|
'aria-label'?: string;
|
|
31
44
|
/** Classes for the item, merged after `classNames.item`; consumer classes win. */
|
|
@@ -28,6 +28,15 @@ const CheckIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
28
28
|
fill: "currentColor",
|
|
29
29
|
children: /* @__PURE__ */ jsx("path", { d: "M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z" })
|
|
30
30
|
});
|
|
31
|
+
/** Clears the link attributes `useMenuItem` copies from an item's props onto one that isn't a link. */
|
|
32
|
+
const NOT_A_LINK = {
|
|
33
|
+
href: void 0,
|
|
34
|
+
target: void 0,
|
|
35
|
+
rel: void 0,
|
|
36
|
+
download: void 0,
|
|
37
|
+
ping: void 0,
|
|
38
|
+
referrerPolicy: void 0
|
|
39
|
+
};
|
|
31
40
|
/** The default submenu arrow (Material Symbols `arrow_right`), mirrored in RTL. */
|
|
32
41
|
const SubmenuIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
33
42
|
viewBox: "0 -960 960 960",
|
|
@@ -633,6 +642,9 @@ function MenuItemView({ node, state, variant, top, bottom, classNames, triggerPr
|
|
|
633
642
|
role: "menuitem",
|
|
634
643
|
"aria-checked": void 0
|
|
635
644
|
} : ariaItemProps;
|
|
645
|
+
const link = props.href != null && !isDisabled && !props.submenu;
|
|
646
|
+
const Root = link ? "a" : "div";
|
|
647
|
+
const linkAttributes = link ? { rel: props.rel ?? (props.target === "_blank" ? "noopener noreferrer" : void 0) } : NOT_A_LINK;
|
|
636
648
|
const { interactionProps, dataAttributes } = useM3Interaction({
|
|
637
649
|
isPressed,
|
|
638
650
|
isDisabled,
|
|
@@ -648,8 +660,9 @@ function MenuItemView({ node, state, variant, top, bottom, classNames, triggerPr
|
|
|
648
660
|
const trailingIcon = props.trailingIcon ?? (props.submenu ? /* @__PURE__ */ jsx(SubmenuIcon, {}) : null);
|
|
649
661
|
const badgeId = useId();
|
|
650
662
|
const describedBy = [props.shortcut ? keyboardShortcutProps.id : void 0, props.badge ? badgeId : void 0].filter(Boolean).join(" ") || void 0;
|
|
651
|
-
return /* @__PURE__ */ jsxs(
|
|
663
|
+
return /* @__PURE__ */ jsxs(Root, {
|
|
652
664
|
...mergeProps(menuItemProps, interactionProps),
|
|
665
|
+
...linkAttributes,
|
|
653
666
|
...dataAttributes,
|
|
654
667
|
"aria-labelledby": void 0,
|
|
655
668
|
"aria-describedby": describedBy,
|