@windmorph/react 0.2.3 → 0.2.4
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.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +2 -4
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +15 -15
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +4 -1
- package/dist/components/menu/Menu.js +7 -10
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +2 -4
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-styles.js","names":[],"sources":["../../../src/components/menu/menu-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow the M3 Expressive menu spec and Compose Material 3's Expressive menu\n * (Menu.kt, MenuDefaults.kt and the Menu / StandardMenu / VibrantMenu / SegmentedMenu\n * token files):\n * - two layouts (M3 \"Vertical menu with gap\" / \"with divider\"). `gap`: groups are separate\n * surfaces 2px apart at elevation 2, with 4px padding (the SegmentedMenu `GroupPadding`\n * token); corners: only 16px; first 16px top / 8px bottom; middle 8px; last 8px top /\n * 16px bottom. `divider`: one surface with 4px padding and 16px corners, groups split by\n * a 1px `outline-variant` divider with 8px above and below (the baseline menu's divider\n * padding). M3 recommends dividers on the web and in scrolling menus;\n * - the outer corners read `--m3-menu-corner`, so they morph while a submenu is open: the\n * menu holding focus takes 24px (SegmentedMenu `ActiveContainerShape`), the others 8px\n * (`InactiveContainerShape`), on the fast spatial spring;\n * - items are 44px minimum (4px less per web density step, `--md-sys-density`: 44 → 40 → 36 →\n * 32px; dividers' 8px margins lose 2px per step), 112–280px wide, 16px padding at\n * the start and end, body-large labels, 20px icons with a 12px gap and label-small trailing\n * text (the shortcut); 12px corners when selected (morph on the fast spatial spring). The\n * padding, gap and type follow the Menu token files (2026-10-14); Compose's code uses 12px,\n * an 8px gap and label-large on touch;\n * - item corners nest inside the container's: an edge item takes the outer corner minus\n * the 4px padding (16px → 12px, 8px → 4px), other corners are 4px. Compose picks item\n * shapes per group alone, so a last item could be rounder than its group's 8px corner;\n * - group labels start 16px inside the group's 4px padding, lined up with the items' 16px\n * padding, and end 4px from its edge (Compose starts them 12px from the edge; aligned\n * with the labels, 2026-10-15);\n * - standard: surface-container-low, on-surface text, on-surface-variant icons, selected\n * tertiary-container; vibrant: tertiary-container, on-tertiary-container content\n * (icons turn tertiary on hover, focus and press), selected tertiary;\n * - the menu is revealed from its anchored edge (a clip) as it fades in, on the fast springs\n * (M3 motion: components enter along the x or y axis; scale is avoided), as do the\n * exposed dropdown menus (Select, Autocomplete) and the docked date picker;\n * - the scrolling list keeps 8px of room around the groups so their shadows aren't clipped,\n * and shows a persistent scrollbar (M3).\n *\n * The container's colour, shadow and resting corners and the items' resting and selected\n * colours come from the menu's component tokens (`menu-tokens.ts`, `--md-comp-menus-*`):\n * the menu, each item and the option lists that share these styles carry\n * `data-tokens=\"md-comp-menus\"` and `data-variant`. `--m3-menu-corner` takes the shape\n * token, so the submenu morph still animates from it. A consumer class still wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/**\n * M3: a menu that scrolls \"show[s] a persistent scrollbar\". Overlay scrollbars hide at rest,\n * so a styled WebKit scrollbar is always drawn (a 4px `outline` thumb in a 12px track);\n * elsewhere, a thin one. Shared by the menu and the exposed dropdown menus' option list.\n */\nexport const persistentScrollbar = [\n '[&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar-track]:bg-transparent',\n '[&::-webkit-scrollbar-thumb]:rounded-corner-full [&::-webkit-scrollbar-thumb]:border-[4px] [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-outline [&::-webkit-scrollbar-thumb]:bg-clip-padding',\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--md-sys-color-outline)_transparent]',\n];\n\n/** Variant definitions for {@link Menu} and its parts. */\n/**\n * Keeps a menu's placed box at the menu's own width (`!`, so a consumer's `w-full` or `min-w-*`\n * doesn't merge it away): React Aria places the box by its size, so a full-width box was held\n * 12px from the window's edge and the menu inside it sat off its trigger (2026-10-17). Only the\n * menus take it: an `!important` minimum would also beat the inline minimum that keeps a\n * Select's or Autocomplete's option list as wide as its field.\n */\nexport const menuOwnWidth = 'w-max! min-w-0!';\n\nexport const menuStyles = tv({\n slots: {\n // The outermost element, which React Aria places (inline `position` and insets) and which\n // takes the consumer's `className` and `style`. A menu adds `menuOwnWidth` to it; the\n // option list of a Select or Autocomplete doesn't, since it sets its own minimum (the\n // field's width) inline.\n popover: 'z-(--md-sys-z-menu)',\n // The menu's enter and exit: revealed from its anchored edge with a clip (24px of room\n // for the shadows) as it fades in. M3 motion avoids scale, and a clip leaves the items'\n // layout untouched, so a submenu opened mid-animation is placed against its item's final\n // position. It also passes the popover's maximum height down, so a long list scrolls.\n // `w-max` keeps the menu's own width whatever width a consumer class gives the popover\n // (whose insets only React Aria sets: `data-overlay-placement`, styles/utilities.css).\n reveal: [\n 'flex max-h-[inherit] w-max flex-col opacity-100 [clip-path:inset(-24px)]',\n '[transition-property:opacity,clip-path]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Closed until React Aria has placed it (`data-entering`, `useRevealEntering`), with no\n // transition, then it opens from there: a `@starting-style` was resolved before the\n // placement was known, so every menu opened from its top edge.\n 'data-entering:opacity-0 data-entering:[transition-property:none] data-exiting:opacity-0',\n // Leaving, it takes no presses (nor do its groups and items, which set their own):\n // an item clicked as it faded ran its action.\n 'data-exiting:pointer-events-none data-exiting:**:pointer-events-none',\n // Below the anchor it opens downwards, above it upwards, beside it sideways. Under\n // reduced motion it only fades: the clip stays open.\n 'motion-safe:data-entering:[clip-path:inset(-24px_-24px_100%_-24px)] motion-safe:data-exiting:[clip-path:inset(-24px_-24px_100%_-24px)]',\n 'motion-safe:data-[placement=top]:data-entering:[clip-path:inset(100%_-24px_-24px_-24px)] motion-safe:data-[placement=top]:data-exiting:[clip-path:inset(100%_-24px_-24px_-24px)]',\n 'motion-safe:data-[placement=right]:data-entering:[clip-path:inset(-24px_100%_-24px_-24px)] motion-safe:data-[placement=right]:data-exiting:[clip-path:inset(-24px_100%_-24px_-24px)]',\n 'motion-safe:data-[placement=left]:data-entering:[clip-path:inset(-24px_-24px_-24px_100%)] motion-safe:data-[placement=left]:data-exiting:[clip-path:inset(-24px_-24px_-24px_100%)]',\n ],\n // The list scrolls when tall, and a scroll container clips its children's shadows to\n // its box. 8px of padding (cancelled by a negative margin, so nothing moves) leaves room\n // for the groups' elevation-2 shadows; that ring lets clicks through, so a press just\n // outside the menu still dismisses it. Widths include the padding: 112–280px groups.\n menu: [\n 'flex max-h-[inherit] flex-col overflow-y-auto bg-[var(--_md-comp-menus-container-color)] outline-none',\n ...persistentScrollbar,\n '[--m3-menu-corner:var(--_md-comp-menus-container-shape)]',\n 'data-[shape=active]:[--m3-menu-corner:24px]',\n 'data-[shape=inactive]:[--m3-menu-corner:var(--md-sys-shape-corner-small)]',\n ],\n group: [\n 'pointer-events-auto flex flex-col bg-[var(--_md-comp-menus-container-color)] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n // Forced colours drop the surface colour and the shadow: a system-colour outline keeps\n // the edge (an outline, so nothing moves).\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n // 8px above and below at density 0, 2px less per step (M3: density takes spacing off).\n divider: 'my-[calc(8px+var(--md-sys-density,0)*2px)] -mx-[4px] w-auto',\n // 16px inside the group's 4px padding, as the items' own 16px padding: in line with them.\n heading: 'flex min-h-[32px] items-center ps-[16px] text-label-large',\n item: [\n 'group/item flex min-h-[calc(44px+var(--md-sys-density,0)*4px)] w-full cursor-pointer items-center px-[16px] text-start',\n 'text-body-large text-[color:var(--_md-comp-menus-menu-item-label-text-color)] outline-none select-none',\n 'data-selected:bg-[var(--_md-comp-menus-menu-item-selected-container-color)] data-selected:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'state-layer focus-ring-inset container-motion',\n 'data-selected:rounded-corner-medium',\n 'data-disabled:cursor-default',\n ],\n selectedIcon: [\n 'grid grid-cols-[0fr] opacity-0',\n '[transition-property:grid-template-columns,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]',\n 'group-data-selected/item:grid-cols-[1fr] group-data-selected/item:opacity-100',\n ],\n selectedIconInner: 'overflow-hidden',\n icon: 'me-[12px] inline-flex size-[20px] shrink-0 items-center justify-center text-[color:var(--_md-comp-menus-menu-item-leading-icon-color)] [&>svg]:size-full',\n text: 'flex min-w-0 flex-1 flex-col',\n description: 'text-body-medium',\n trailing:\n 'ms-auto inline-flex shrink-0 items-center ps-[12px] text-label-small text-[color:var(--_md-comp-menus-menu-item-trailing-icon-color)] [&>svg]:size-[20px]',\n badge: 'inline-flex shrink-0 items-center ps-[12px]',\n // A menu's place in a bottom sheet. An open submenu's panel follows the menu that opened\n // it (portalled in after it) and hides it, and any panel before it, while it's open.\n sheetContent: [\n 'flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]',\n '[&>:not([data-overlay-scope]):has(~[data-overlay-scope])]:hidden',\n '[&>[data-overlay-scope]:has(~[data-overlay-scope])>*]:hidden',\n ],\n // A submenu in a sheet: a back button and its item's label above its list, fading in.\n sheetPanel: [\n 'flex min-h-0 flex-1 flex-col opacity-100 starting:opacity-0',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n sheetHeader: 'flex min-h-[48px] shrink-0 items-center gap-[4px] pe-[12px]',\n sheetBack: [\n 'inline-flex size-[48px] shrink-0 cursor-pointer items-center justify-center rounded-corner-full',\n 'state-layer focus-ring text-on-surface-variant [&>svg]:size-[24px]',\n ],\n sheetTitle: 'min-w-0 flex-1 truncate text-title-medium text-on-surface',\n },\n variants: {\n variant: {\n standard: {\n heading: 'text-on-surface-variant',\n item: [\n 'data-disabled:text-on-surface/38',\n 'data-disabled:data-selected:bg-tertiary-container/38 data-disabled:data-selected:text-on-tertiary-container/38',\n ],\n icon: 'group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n description:\n 'text-on-surface-variant group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n trailing:\n 'group-data-selected/item:text-on-tertiary-container group-data-disabled/item:text-on-surface/38',\n },\n vibrant: {\n heading: 'text-on-tertiary-container',\n item: [\n 'data-disabled:text-on-tertiary-container/38',\n 'data-disabled:data-selected:bg-tertiary/38 data-disabled:data-selected:text-on-tertiary/38',\n ],\n icon: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-on-tertiary',\n 'group-data-selected/item:group-data-hovered/item:text-on-tertiary',\n 'group-data-selected/item:group-data-focus-visible/item:text-on-tertiary',\n 'group-data-selected/item:group-data-pressed/item:text-on-tertiary',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n description:\n 'text-on-tertiary-container group-data-selected/item:text-on-tertiary group-data-disabled/item:text-on-tertiary-container/38',\n trailing: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-on-tertiary',\n 'group-data-selected/item:group-data-hovered/item:text-on-tertiary',\n 'group-data-selected/item:group-data-focus-visible/item:text-on-tertiary',\n 'group-data-selected/item:group-data-pressed/item:text-on-tertiary',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n },\n },\n groupPosition: {\n only: { group: 'rounded-[var(--_md-comp-menus-container-shape)]' },\n first: { group: 'rounded-t-[var(--_md-comp-menus-container-shape)] rounded-b-corner-small' },\n middle: { group: 'rounded-corner-small' },\n last: { group: 'rounded-t-corner-small rounded-b-[var(--_md-comp-menus-container-shape)]' },\n },\n /**\n * The item's top corners: `nested` inside a 16px corner (12px), `outer` inside the\n * menu's morphing outer corner (`--m3-menu-corner` less the 4px padding), else 4px.\n */\n itemTop: {\n nested: { item: 'rounded-t-corner-medium' },\n outer: {\n item: 'rounded-t-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-t-corner-extra-small' },\n },\n /** The item's bottom corners, as `itemTop`. */\n itemBottom: {\n nested: { item: 'rounded-b-corner-medium' },\n outer: {\n item: 'rounded-b-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-b-corner-extra-small' },\n },\n /** Groups' outer corners follow `--m3-menu-corner` (the submenu morph). */\n morph: {\n true: {},\n false: {},\n },\n hasDescription: {\n true: { item: 'py-[calc(8px+var(--md-sys-density,0)*2px)]' },\n false: {},\n },\n /** Open and close with no motion (`instant`). */\n instant: {\n true: { reveal: '[transition-property:none]' },\n false: {},\n },\n /** In a bottom sheet: the list spans it, on the sheet's own surface. */\n embedded: {\n true: {},\n false: {},\n },\n /**\n * `gap`: every group is its own surface, and the list only scrolls (its 8px ring keeps\n * the groups' shadows); `divider`: the list is the one surface.\n */\n layout: {\n gap: {\n menu: [\n '-m-[8px] p-[8px] pointer-events-none gap-[2px] bg-transparent',\n 'min-w-[128px] max-w-[296px]',\n ],\n },\n divider: {\n menu: [\n 'pointer-events-auto min-w-[112px] max-w-[280px] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n group: 'bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n },\n compoundVariants: [\n {\n morph: true,\n groupPosition: 'only',\n class: { group: 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]' },\n },\n {\n morph: true,\n groupPosition: 'first',\n class: {\n group:\n 'rounded-t-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))] rounded-b-corner-small',\n },\n },\n {\n morph: true,\n groupPosition: 'last',\n class: {\n group:\n 'rounded-t-corner-small rounded-b-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n },\n },\n // A divider layout's groups are plain sections of the one surface.\n { layout: 'divider', class: { group: 'rounded-none' } },\n // In a sheet the list has no surface, shadow or width limits of its own.\n {\n embedded: true,\n class: {\n menu: 'w-full max-w-none min-w-0 rounded-none bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n ],\n defaultVariants: {\n variant: 'standard',\n layout: 'gap',\n morph: false,\n groupPosition: 'only',\n itemTop: 'plain',\n itemBottom: 'plain',\n hasDescription: false,\n instant: false,\n embedded: false,\n },\n});\n\n/**\n * The variants of {@link menuStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type MenuStyleProps = VariantProps<typeof menuStyles>;\nexport type MenuVariant = NonNullable<MenuStyleProps['variant']>;\nexport type MenuLayout = NonNullable<MenuStyleProps['layout']>;\n"],"mappings":";;;;;;;AAiDA,MAAa,sBAAsB;CACjC;CACA;CACA;AACF;;;;;;;;;AAUA,MAAa,eAAe;AAE5B,MAAa,aAAa,GAAG;CAC3B,OAAO;EAKL,SAAS;EAOT,QAAQ;GACN;GACA;GACA;GACA;GAIA;GAGA;GAGA;GACA;GACA;GACA;EACF;EAKA,MAAM;GACJ;GACA,GAAG;GACH;GACA;GACA;EACF;EACA,OAAO;GACL;GAGA;GACA;GACA;GACA;EACF;EAEA,SAAS;EAET,SAAS;EACT,MAAM;GACJ;GACA;GACA;GACA;GACA;GACA;EACF;EACA,cAAc;GACZ;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;EACnB,MAAM;EACN,MAAM;EACN,aAAa;EACb,UACE;EACF,OAAO;EAGP,cAAc;GACZ;GACA;GACA;EACF;EAEA,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,aAAa;EACb,WAAW,CACT,mGACA,oEACF;EACA,YAAY;CACd;CACA,UAAU;EACR,SAAS;GACP,UAAU;IACR,SAAS;IACT,MAAM,CACJ,oCACA,gHACF;IACA,MAAM;IACN,aACE;IACF,UACE;GACJ;GACA,SAAS;IACP,SAAS;IACT,MAAM,CACJ,+CACA,4FACF;IACA,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,aACE;IACF,UAAU;KACR;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,eAAe;GACb,MAAM,EAAE,OAAO,kDAAkD;GACjE,OAAO,EAAE,OAAO,2EAA2E;GAC3F,QAAQ,EAAE,OAAO,uBAAuB;GACxC,MAAM,EAAE,OAAO,2EAA2E;EAC5F;;;;;EAKA,SAAS;GACP,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,YAAY;GACV,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EACA,gBAAgB;GACd,MAAM,EAAE,MAAM,6CAA6C;GAC3D,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,QAAQ,6BAA6B;GAC7C,OAAO,CAAC;EACV;;EAEA,UAAU;GACR,MAAM,CAAC;GACP,OAAO,CAAC;EACV;;;;;EAKA,QAAQ;GACN,KAAK,EACH,MAAM,CACJ,iEACA,6BACF,EACF;GACA,SAAS;IACP,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;GACT;EACF;CACF;CACA,kBAAkB;EAChB;GACE,OAAO;GACP,eAAe;GACf,OAAO,EAAE,OAAO,mEAAmE;EACrF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EAEA;GAAE,QAAQ;GAAW,OAAO,EAAE,OAAO,eAAe;EAAE;EAEtD;GACE,UAAU;GACV,OAAO,EACL,MAAM,mGACR;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,QAAQ;EACR,OAAO;EACP,eAAe;EACf,SAAS;EACT,YAAY;EACZ,gBAAgB;EAChB,SAAS;EACT,UAAU;CACZ;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"menu-styles.js","names":[],"sources":["../../../src/components/menu/menu-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow the M3 Expressive menu spec and Compose Material 3's Expressive menu\n * (Menu.kt, MenuDefaults.kt and the Menu / StandardMenu / VibrantMenu / SegmentedMenu\n * token files):\n * - two layouts (M3 \"Vertical menu with gap\" / \"with divider\"). `gap`: groups are separate\n * surfaces 2px apart at elevation 2, with 4px padding (the SegmentedMenu `GroupPadding`\n * token); corners: only 16px; first 16px top / 8px bottom; middle 8px; last 8px top /\n * 16px bottom. `divider`: one surface with 4px padding and 16px corners, groups split by\n * a 1px `outline-variant` divider with 8px above and below (the baseline menu's divider\n * padding). M3 recommends dividers on the web and in scrolling menus;\n * - the outer corners read `--m3-menu-corner`, so they morph while a submenu is open: the\n * menu holding focus takes 24px (SegmentedMenu `ActiveContainerShape`), the others 8px\n * (`InactiveContainerShape`), on the fast spatial spring;\n * - items are 44px minimum (4px less per web density step, `--md-sys-density`: 44 → 40 → 36 →\n * 32px; dividers' 8px margins lose 2px per step), 112–280px wide, 16px padding at\n * the start and end, body-large labels, 20px icons with a 12px gap and label-small trailing\n * text (the shortcut); 12px corners when selected (morph on the fast spatial spring). The\n * padding, gap and type follow the Menu token files (2026-10-14); Compose's code uses 12px,\n * an 8px gap and label-large on touch;\n * - item corners nest inside the container's: an edge item takes the outer corner minus\n * the 4px padding (16px → 12px, 8px → 4px), other corners are 4px. Compose picks item\n * shapes per group alone, so a last item could be rounder than its group's 8px corner;\n * - group labels start 16px inside the group's 4px padding, lined up with the items' 16px\n * padding, and end 4px from its edge (Compose starts them 12px from the edge; aligned\n * with the labels, 2026-10-15);\n * - standard: surface-container-low, on-surface text, on-surface-variant icons, selected\n * tertiary-container; vibrant: tertiary-container, on-tertiary-container content\n * (icons turn tertiary on hover, focus and press), selected tertiary;\n * - the menu is revealed from its anchored edge (a clip) as it fades in, on the fast springs\n * (M3 motion: components enter along the x or y axis; scale is avoided), as do the\n * exposed dropdown menus (Select, Autocomplete) and the docked date picker;\n * - the scrolling list keeps 8px of room around the groups so their shadows aren't clipped,\n * and shows a persistent scrollbar (M3).\n *\n * The container's colour, shadow and resting corners and the items' resting and selected\n * colours come from the menu's component tokens (`menu-tokens.ts`, `--md-comp-menus-*`):\n * the menu, each item and the option lists that share these styles carry\n * `data-tokens=\"md-comp-menus\"` and `data-variant`. `--m3-menu-corner` takes the shape\n * token, so the submenu morph still animates from it. A consumer class still wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/**\n * M3: a menu that scrolls \"show[s] a persistent scrollbar\". Overlay scrollbars hide at rest,\n * so a styled WebKit scrollbar is always drawn (a 4px `outline` thumb in a 12px track);\n * elsewhere, a thin one. Shared by the menu and the exposed dropdown menus' option list.\n */\nexport const persistentScrollbar = [\n '[&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar-track]:bg-transparent',\n '[&::-webkit-scrollbar-thumb]:rounded-corner-full [&::-webkit-scrollbar-thumb]:border-[4px] [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-outline [&::-webkit-scrollbar-thumb]:bg-clip-padding',\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--md-sys-color-outline)_transparent]',\n];\n\n/** Variant definitions for {@link Menu} and its parts. */\n/**\n * Keeps a menu's placed box at the menu's own width (`!`, so a consumer's `w-full` or `min-w-*`\n * doesn't merge it away): React Aria places the box by its size, so a full-width box was held\n * 12px from the window's edge and the menu inside it sat off its trigger (2026-10-17). Only the\n * menus take it: an `!important` minimum would also beat the inline minimum that keeps a\n * Select's or Autocomplete's option list as wide as its field.\n */\nexport const menuOwnWidth = 'w-max! min-w-0!';\n\nexport const menuStyles = tv({\n slots: {\n // The outermost element, which React Aria places (inline `position` and insets) and which\n // takes the consumer's `className` and `style`. A menu adds `menuOwnWidth` to it; the\n // option list of a Select or Autocomplete doesn't, since it sets its own minimum (the\n // field's width) inline.\n popover: 'z-(--md-sys-z-menu)',\n // The menu's enter and exit: revealed from its anchored edge with a clip (24px of room\n // for the shadows) as it fades in. M3 motion avoids scale, and a clip leaves the items'\n // layout untouched, so a submenu opened mid-animation is placed against its item's final\n // position. It also passes the popover's maximum height down, so a long list scrolls.\n // `w-max` keeps the menu's own width whatever width a consumer class gives the popover\n // (whose insets only React Aria sets: `data-overlay-placement`, styles/utilities.css).\n reveal: [\n 'flex max-h-[inherit] w-max flex-col opacity-100 [clip-path:inset(-24px)]',\n '[transition-property:opacity,clip-path]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Closed until React Aria has placed it (`data-entering`, `useRevealEntering`), with no\n // transition, then it opens from there: a `@starting-style` was resolved before the\n // placement was known, so every menu opened from its top edge.\n 'data-entering:opacity-0 data-entering:[transition-property:none] data-exiting:opacity-0',\n // Leaving, it takes no presses (nor do its groups and items, which set their own):\n // an item clicked as it faded ran its action.\n 'data-exiting:pointer-events-none data-exiting:**:pointer-events-none',\n // Below the anchor it opens downwards, above it upwards, beside it sideways. Under\n // reduced motion it only fades: the clip stays open.\n 'motion-safe:data-entering:[clip-path:inset(-24px_-24px_100%_-24px)] motion-safe:data-exiting:[clip-path:inset(-24px_-24px_100%_-24px)]',\n 'motion-safe:data-[placement=top]:data-entering:[clip-path:inset(100%_-24px_-24px_-24px)] motion-safe:data-[placement=top]:data-exiting:[clip-path:inset(100%_-24px_-24px_-24px)]',\n 'motion-safe:data-[placement=right]:data-entering:[clip-path:inset(-24px_100%_-24px_-24px)] motion-safe:data-[placement=right]:data-exiting:[clip-path:inset(-24px_100%_-24px_-24px)]',\n 'motion-safe:data-[placement=left]:data-entering:[clip-path:inset(-24px_-24px_-24px_100%)] motion-safe:data-[placement=left]:data-exiting:[clip-path:inset(-24px_-24px_-24px_100%)]',\n ],\n // The list scrolls when tall, and a scroll container clips its children's shadows to\n // its box. 8px of padding (cancelled by a negative margin, so nothing moves) leaves room\n // for the groups' elevation-2 shadows; that ring lets clicks through, so a press just\n // outside the menu still dismisses it. Widths include the padding: 112–280px groups.\n menu: [\n 'flex max-h-[inherit] flex-col overflow-y-auto bg-[var(--_md-comp-menus-container-color)] outline-none',\n ...persistentScrollbar,\n '[--m3-menu-corner:var(--_md-comp-menus-container-shape)]',\n 'data-[shape=active]:[--m3-menu-corner:24px]',\n 'data-[shape=inactive]:[--m3-menu-corner:var(--md-sys-shape-corner-small)]',\n ],\n group: [\n 'pointer-events-auto flex flex-col bg-[var(--_md-comp-menus-container-color)] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n // Forced colours drop the surface colour and the shadow: a system-colour outline keeps\n // the edge (an outline, so nothing moves).\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n // 8px above and below at density 0, 2px less per step (M3: density takes spacing off).\n divider: 'my-[calc(8px+var(--md-sys-density,0)*2px)] -mx-[4px] w-auto',\n // 16px inside the group's 4px padding, as the items' own 16px padding: in line with them.\n heading: 'flex min-h-[32px] items-center ps-[16px] text-label-large',\n item: [\n 'group/item flex min-h-[calc(44px+var(--md-sys-density,0)*4px)] w-full cursor-pointer items-center px-[16px] text-start',\n 'text-body-large text-[color:var(--_md-comp-menus-menu-item-label-text-color)] outline-none select-none',\n 'data-selected:bg-[var(--_md-comp-menus-menu-item-selected-container-color)] data-selected:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'state-layer focus-ring-inset container-motion',\n 'data-selected:rounded-corner-medium',\n 'data-disabled:cursor-default',\n ],\n selectedIcon: [\n 'grid grid-cols-[0fr] opacity-0',\n '[transition-property:grid-template-columns,opacity]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]',\n 'group-data-selected/item:grid-cols-[1fr] group-data-selected/item:opacity-100',\n ],\n selectedIconInner: 'overflow-hidden',\n icon: 'me-[12px] inline-flex size-[20px] shrink-0 items-center justify-center text-[color:var(--_md-comp-menus-menu-item-leading-icon-color)] [&>svg]:size-full',\n text: 'flex min-w-0 flex-1 flex-col',\n description: 'text-body-medium',\n trailing:\n 'ms-auto inline-flex shrink-0 items-center ps-[12px] text-label-small text-[color:var(--_md-comp-menus-menu-item-trailing-icon-color)] [&>svg]:size-[20px]',\n badge: 'inline-flex shrink-0 items-center ps-[12px]',\n // A menu's place in a bottom sheet. An open submenu's panel follows the menu that opened\n // it (portalled in after it) and hides it, and any panel before it, while it's open.\n sheetContent: [\n 'flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]',\n '[&>:not([data-overlay-scope]):has(~[data-overlay-scope])]:hidden',\n '[&>[data-overlay-scope]:has(~[data-overlay-scope])>*]:hidden',\n ],\n // A submenu in a sheet: a back button and its item's label above its list, fading in.\n sheetPanel: [\n 'flex min-h-0 flex-1 flex-col opacity-100 starting:opacity-0',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n sheetHeader: 'flex min-h-[48px] shrink-0 items-center gap-[4px] pe-[12px]',\n sheetBack: [\n 'inline-flex size-[48px] shrink-0 cursor-pointer items-center justify-center rounded-corner-full',\n 'state-layer focus-ring text-on-surface-variant [&>svg]:size-[24px]',\n ],\n sheetTitle: 'min-w-0 flex-1 truncate text-title-medium text-on-surface',\n },\n variants: {\n variant: {\n // A selected item's tick, icons and description follow its label's token, so an app\n // that sets the selected colours gets every part in them (2026-11-11; they were a fixed\n // role, 1.08:1 against another container in dark mode).\n standard: {\n heading: 'text-on-surface-variant',\n item: [\n 'data-disabled:text-on-surface/38',\n 'data-disabled:data-selected:bg-tertiary-container/38 data-disabled:data-selected:text-on-tertiary-container/38',\n ],\n icon: 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n description:\n 'text-on-surface-variant group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n trailing:\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-surface/38',\n },\n vibrant: {\n heading: 'text-on-tertiary-container',\n item: [\n 'data-disabled:text-on-tertiary-container/38',\n 'data-disabled:data-selected:bg-tertiary/38 data-disabled:data-selected:text-on-tertiary/38',\n ],\n icon: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-hovered/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-focus-visible/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-pressed/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n description:\n 'text-on-tertiary-container group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)] group-data-disabled/item:text-on-tertiary-container/38',\n trailing: [\n 'group-data-hovered/item:text-tertiary group-data-focus-visible/item:text-tertiary group-data-pressed/item:text-tertiary',\n 'group-data-selected/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-hovered/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-focus-visible/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-selected/item:group-data-pressed/item:text-[color:var(--_md-comp-menus-menu-item-selected-label-text-color)]',\n 'group-data-disabled/item:text-on-tertiary-container/38',\n ],\n },\n },\n groupPosition: {\n only: { group: 'rounded-[var(--_md-comp-menus-container-shape)]' },\n first: { group: 'rounded-t-[var(--_md-comp-menus-container-shape)] rounded-b-corner-small' },\n middle: { group: 'rounded-corner-small' },\n last: { group: 'rounded-t-corner-small rounded-b-[var(--_md-comp-menus-container-shape)]' },\n },\n /**\n * The item's top corners: `nested` inside a 16px corner (12px), `outer` inside the\n * menu's morphing outer corner (`--m3-menu-corner` less the 4px padding), else 4px.\n */\n itemTop: {\n nested: { item: 'rounded-t-corner-medium' },\n outer: {\n item: 'rounded-t-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-t-corner-extra-small' },\n },\n /** The item's bottom corners, as `itemTop`. */\n itemBottom: {\n nested: { item: 'rounded-b-corner-medium' },\n outer: {\n item: 'rounded-b-[calc(var(--m3-menu-corner,var(--md-sys-shape-corner-large))-4px)]',\n },\n plain: { item: 'rounded-b-corner-extra-small' },\n },\n /** Groups' outer corners follow `--m3-menu-corner` (the submenu morph). */\n morph: {\n true: {},\n false: {},\n },\n hasDescription: {\n true: { item: 'py-[calc(8px+var(--md-sys-density,0)*2px)]' },\n false: {},\n },\n /** Open and close with no motion (`instant`). */\n instant: {\n true: { reveal: '[transition-property:none]' },\n false: {},\n },\n /** In a bottom sheet: the list spans it, on the sheet's own surface. */\n embedded: {\n true: {},\n false: {},\n },\n /**\n * `gap`: every group is its own surface, and the list only scrolls (its 8px ring keeps\n * the groups' shadows); `divider`: the list is the one surface.\n */\n layout: {\n gap: {\n menu: [\n '-m-[8px] p-[8px] pointer-events-none gap-[2px] bg-transparent',\n 'min-w-[128px] max-w-[296px]',\n ],\n },\n divider: {\n menu: [\n 'pointer-events-auto min-w-[112px] max-w-[280px] p-[4px] shadow-[shadow:var(--_md-comp-menus-container-elevation)]',\n 'forced-colors:outline-solid forced-colors:outline-1 forced-colors:outline-[CanvasText]',\n 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n '[transition-property:border-radius]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n ],\n group: 'bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n },\n compoundVariants: [\n {\n morph: true,\n groupPosition: 'only',\n class: { group: 'rounded-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]' },\n },\n {\n morph: true,\n groupPosition: 'first',\n class: {\n group:\n 'rounded-t-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))] rounded-b-corner-small',\n },\n },\n {\n morph: true,\n groupPosition: 'last',\n class: {\n group:\n 'rounded-t-corner-small rounded-b-[var(--m3-menu-corner,var(--md-sys-shape-corner-large))]',\n },\n },\n // A divider layout's groups are plain sections of the one surface.\n { layout: 'divider', class: { group: 'rounded-none' } },\n // In a sheet the list has no surface, shadow or width limits of its own.\n {\n embedded: true,\n class: {\n menu: 'w-full max-w-none min-w-0 rounded-none bg-transparent p-0 shadow-none forced-colors:outline-none',\n },\n },\n ],\n defaultVariants: {\n variant: 'standard',\n layout: 'gap',\n morph: false,\n groupPosition: 'only',\n itemTop: 'plain',\n itemBottom: 'plain',\n hasDescription: false,\n instant: false,\n embedded: false,\n },\n});\n\n/**\n * The variants of {@link menuStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type MenuStyleProps = VariantProps<typeof menuStyles>;\nexport type MenuVariant = NonNullable<MenuStyleProps['variant']>;\nexport type MenuLayout = NonNullable<MenuStyleProps['layout']>;\n"],"mappings":";;;;;;;AAiDA,MAAa,sBAAsB;CACjC;CACA;CACA;AACF;;;;;;;;;AAUA,MAAa,eAAe;AAE5B,MAAa,aAAa,GAAG;CAC3B,OAAO;EAKL,SAAS;EAOT,QAAQ;GACN;GACA;GACA;GACA;GAIA;GAGA;GAGA;GACA;GACA;GACA;EACF;EAKA,MAAM;GACJ;GACA,GAAG;GACH;GACA;GACA;EACF;EACA,OAAO;GACL;GAGA;GACA;GACA;GACA;EACF;EAEA,SAAS;EAET,SAAS;EACT,MAAM;GACJ;GACA;GACA;GACA;GACA;GACA;EACF;EACA,cAAc;GACZ;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;EACnB,MAAM;EACN,MAAM;EACN,aAAa;EACb,UACE;EACF,OAAO;EAGP,cAAc;GACZ;GACA;GACA;EACF;EAEA,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,aAAa;EACb,WAAW,CACT,mGACA,oEACF;EACA,YAAY;CACd;CACA,UAAU;EACR,SAAS;GAIP,UAAU;IACR,SAAS;IACT,MAAM,CACJ,oCACA,gHACF;IACA,MAAM;IACN,aACE;IACF,UACE;GACJ;GACA,SAAS;IACP,SAAS;IACT,MAAM,CACJ,+CACA,4FACF;IACA,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,aACE;IACF,UAAU;KACR;KACA;KACA;KACA;KACA;KACA;IACF;GACF;EACF;EACA,eAAe;GACb,MAAM,EAAE,OAAO,kDAAkD;GACjE,OAAO,EAAE,OAAO,2EAA2E;GAC3F,QAAQ,EAAE,OAAO,uBAAuB;GACxC,MAAM,EAAE,OAAO,2EAA2E;EAC5F;;;;;EAKA,SAAS;GACP,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,YAAY;GACV,QAAQ,EAAE,MAAM,0BAA0B;GAC1C,OAAO,EACL,MAAM,+EACR;GACA,OAAO,EAAE,MAAM,+BAA+B;EAChD;;EAEA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EACA,gBAAgB;GACd,MAAM,EAAE,MAAM,6CAA6C;GAC3D,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,QAAQ,6BAA6B;GAC7C,OAAO,CAAC;EACV;;EAEA,UAAU;GACR,MAAM,CAAC;GACP,OAAO,CAAC;EACV;;;;;EAKA,QAAQ;GACN,KAAK,EACH,MAAM,CACJ,iEACA,6BACF,EACF;GACA,SAAS;IACP,MAAM;KACJ;KACA;KACA;KACA;KACA;KACA;IACF;IACA,OAAO;GACT;EACF;CACF;CACA,kBAAkB;EAChB;GACE,OAAO;GACP,eAAe;GACf,OAAO,EAAE,OAAO,mEAAmE;EACrF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EACA;GACE,OAAO;GACP,eAAe;GACf,OAAO,EACL,OACE,4FACJ;EACF;EAEA;GAAE,QAAQ;GAAW,OAAO,EAAE,OAAO,eAAe;EAAE;EAEtD;GACE,UAAU;GACV,OAAO,EACL,MAAM,mGACR;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,QAAQ;EACR,OAAO;EACP,eAAe;EACf,SAAS;EACT,YAAY;EACZ,gBAAgB;EAChB,SAAS;EACT,UAAU;CACZ;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-tokens.js","names":[],"sources":["../../../src/components/menu/menu-tokens.ts"],"sourcesContent":["import { color, corner, defineComponentTokens, elevation } from '../../tokens/component';\n\n/*\n * The menu's component tokens, M3 Expressive's `md.comp.menus.*` (common) and\n * `md.comp.menus.standard.*` names (checked against m3.material.io's token tables,\n * 2026-10-14; `md.comp.menu.*` is the baseline set), with the defaults of `menu-styles.ts`.\n *\n * The menu, its items, and Select's and Autocomplete's option lists (which share the menu's\n * styles) carry `data-tokens=\"md-comp-menus\"` and `data-variant`, so an item's own state\n * (`data-selected`) picks its colours. The vibrant menu's colours have no public names yet\n * (fixed below). The container shape is the menu's outer corner at rest: the submenu morph's\n * active and inactive corners, hover, focus, press
|
|
1
|
+
{"version":3,"file":"menu-tokens.js","names":[],"sources":["../../../src/components/menu/menu-tokens.ts"],"sourcesContent":["import { color, corner, defineComponentTokens, elevation } from '../../tokens/component';\n\n/*\n * The menu's component tokens, M3 Expressive's `md.comp.menus.*` (common) and\n * `md.comp.menus.standard.*` names (checked against m3.material.io's token tables,\n * 2026-10-14; `md.comp.menu.*` is the baseline set), with the defaults of `menu-styles.ts`.\n *\n * The menu, its items, and Select's and Autocomplete's option lists (which share the menu's\n * styles) carry `data-tokens=\"md-comp-menus\"` and `data-variant`, so an item's own state\n * (`data-selected`) picks its colours. The vibrant menu's colours have no public names yet\n * (fixed below). The container shape is the menu's outer corner at rest: the submenu morph's\n * active and inactive corners, hover, focus, press and disabled colours stay classes; a\n * selected item's tick, icons and description read its label's token.\n */\n\n/** The tokens of {@link Menu}: `--md-comp-menus-<part>` and `--md-comp-menus-standard-<part>`. */\nexport const menuTokenSet = defineComponentTokens({\n name: 'md-comp-menus',\n tokens: {\n 'container-elevation': {\n value: elevation(2),\n description: \"The menu's shadow (each group's, in the gap layout).\",\n compose: 'MenuTokens.ContainerElevation',\n paints: { property: 'box-shadow' },\n },\n 'container-shape': {\n value: corner('large'),\n description: \"The menu's outer corners at rest.\",\n compose: 'MenuTokens.ContainerShape',\n paints: { property: 'border-top-left-radius' },\n },\n },\n scopes: {\n variant: {\n standard: {\n prefix: 'md-comp-menus-standard',\n tokens: {\n 'container-color': {\n value: color('surface-container-low'),\n description: 'The menu (each group, in the gap layout).',\n compose: 'StandardMenuTokens.ContainerColor',\n paints: { property: 'background-color' },\n },\n 'menu-item-label-text-color': {\n value: color('on-surface'),\n description: 'An item’s label.',\n compose: 'StandardMenuTokens.ItemLabelTextColor',\n paints: { property: 'color', slot: 'item' },\n },\n 'menu-item-leading-icon-color': {\n value: color('on-surface-variant'),\n description: 'An item’s leading icon.',\n compose: 'StandardMenuTokens.ItemLeadingIconColor',\n paints: { property: 'color', slot: 'leading-icon' },\n },\n 'menu-item-trailing-icon-color': {\n value: color('on-surface-variant'),\n description: 'An item’s trailing icon, keyboard shortcut or trailing text.',\n compose: 'StandardMenuTokens.ItemTrailingIconColor',\n paints: { property: 'color', slot: 'trailing-icon' },\n },\n 'menu-item-selected-container-color': {\n value: color('tertiary-container'),\n description: 'A selected item.',\n compose: 'StandardMenuTokens.ItemSelectedContainerColor',\n paints: { property: 'background-color', state: 'selected' },\n },\n 'menu-item-selected-label-text-color': {\n value: color('on-tertiary-container'),\n description: 'A selected item’s label.',\n compose: 'StandardMenuTokens.ItemSelectedLabelTextColor',\n paints: { property: 'color', state: 'selected' },\n },\n },\n },\n vibrant: {\n prefix: 'md-comp-menus-vibrant',\n fixed: {\n 'container-color': color('tertiary-container'),\n 'menu-item-label-text-color': color('on-tertiary-container'),\n 'menu-item-leading-icon-color': color('on-tertiary-container'),\n 'menu-item-trailing-icon-color': color('on-tertiary-container'),\n 'menu-item-selected-container-color': color('tertiary'),\n 'menu-item-selected-label-text-color': color('on-tertiary'),\n },\n },\n },\n },\n});\n"],"mappings":";;;AAgBA,MAAa,eAAe,sBAAsB;CAChD,MAAM;CACN,QAAQ;EACN,uBAAuB;GACrB,OAAO,UAAU,CAAC;GAClB,aAAa;GACb,SAAS;GACT,QAAQ,EAAE,UAAU,aAAa;EACnC;EACA,mBAAmB;GACjB,OAAO,OAAO,OAAO;GACrB,aAAa;GACb,SAAS;GACT,QAAQ,EAAE,UAAU,yBAAyB;EAC/C;CACF;CACA,QAAQ,EACN,SAAS;EACP,UAAU;GACR,QAAQ;GACR,QAAQ;IACN,mBAAmB;KACjB,OAAO,MAAM,uBAAuB;KACpC,aAAa;KACb,SAAS;KACT,QAAQ,EAAE,UAAU,mBAAmB;IACzC;IACA,8BAA8B;KAC5B,OAAO,MAAM,YAAY;KACzB,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAO;IAC5C;IACA,gCAAgC;KAC9B,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAe;IACpD;IACA,iCAAiC;KAC/B,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,MAAM;KAAgB;IACrD;IACA,sCAAsC;KACpC,OAAO,MAAM,oBAAoB;KACjC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAoB,OAAO;KAAW;IAC5D;IACA,uCAAuC;KACrC,OAAO,MAAM,uBAAuB;KACpC,aAAa;KACb,SAAS;KACT,QAAQ;MAAE,UAAU;MAAS,OAAO;KAAW;IACjD;GACF;EACF;EACA,SAAS;GACP,QAAQ;GACR,OAAO;IACL,mBAAmB,MAAM,oBAAoB;IAC7C,8BAA8B,MAAM,uBAAuB;IAC3D,gCAAgC,MAAM,uBAAuB;IAC7D,iCAAiC,MAAM,uBAAuB;IAC9D,sCAAsC,MAAM,UAAU;IACtD,uCAAuC,MAAM,aAAa;GAC5D;EACF;CACF,EACF;AACF,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ButtonBaseActionProps, ButtonBaseLinkProps } from "../../primitives/ButtonBase.js";
|
|
2
1
|
import { BadgeProps } from "../badge/Badge.js";
|
|
2
|
+
import { ButtonBaseActionProps, ButtonBaseLinkProps } from "../../primitives/ButtonBase.js";
|
|
3
3
|
import "./nav-item-styles.js";
|
|
4
4
|
import { ReactElement, ReactNode, RefObject } from "react";
|
|
5
5
|
//#region src/components/navigation/NavItem.d.ts
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { useM3Spring } from "../../motion/use-m3-spring.js";
|
|
4
5
|
import { setRippleOrigin, useRippleHold } from "../../primitives/use-m3-interaction.js";
|
|
5
6
|
import { ButtonBase } from "../../primitives/ButtonBase.js";
|
|
6
7
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
7
|
-
import { BadgedBox } from "../badge/Badge.js";
|
|
8
|
+
import { BadgedBox, DEFAULT_BADGE_LABELS } from "../badge/Badge.js";
|
|
8
9
|
import { navItemStyles } from "./nav-item-styles.js";
|
|
9
10
|
import { isValidElement, useId, useLayoutEffect, useRef, useState } from "react";
|
|
10
11
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -57,14 +58,15 @@ function useSlidingIndicator(selected, lastIndicatorRef) {
|
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* What a badge tells assistive tech about its item: its `label`, its text, or, for a small
|
|
60
|
-
* badge (no children),
|
|
61
|
+
* badge (no children), `newNotification`: the badge's own word, a `LabelsProvider`'s, or
|
|
62
|
+
* "New notification".
|
|
61
63
|
*/
|
|
62
|
-
function badgeDescription(badge) {
|
|
64
|
+
function badgeDescription(badge, newNotification) {
|
|
63
65
|
if (!isValidElement(badge)) return null;
|
|
64
|
-
const { label,
|
|
66
|
+
const { label, children } = badge.props;
|
|
65
67
|
if (label != null) return label;
|
|
66
68
|
if (typeof children === "string" || typeof children === "number") return String(children);
|
|
67
|
-
if (children == null || children === false) return
|
|
69
|
+
if (children == null || children === false) return newNotification;
|
|
68
70
|
return null;
|
|
69
71
|
}
|
|
70
72
|
/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */
|
|
@@ -78,7 +80,8 @@ function NavItem({ layout, enterLabel, lastIndicatorRef, icon, selectedIcon, chi
|
|
|
78
80
|
const descriptionId = useId();
|
|
79
81
|
const inline = layout === "rail-start" || layout === "bar-start";
|
|
80
82
|
const badgeAfterLabel = layout === "rail-start";
|
|
81
|
-
const
|
|
83
|
+
const { newNotification } = useLabels("Badge", DEFAULT_BADGE_LABELS, isValidElement(badge) ? badge.props.labels : void 0);
|
|
84
|
+
const description = badgeLabel ?? badgeDescription(badge, newNotification);
|
|
82
85
|
const label = /* @__PURE__ */ jsx("span", {
|
|
83
86
|
"data-nav-label": "",
|
|
84
87
|
"data-slot": "label",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavItem.js","names":[],"sources":["../../../src/components/navigation/NavItem.tsx"],"sourcesContent":["'use client';\n\nimport { animate, useReducedMotion } from 'motion/react';\nimport {\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { setRippleOrigin, useRippleHold } from '../../primitives/use-m3-interaction';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { cn } from '../../utils/cn';\nimport { BadgedBox, type BadgeProps } from '../badge/Badge';\nimport { navItemStyles, type NavItemLayout } from './nav-item-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\nexport interface NavItemClassNames {\n /** The item's button or link, merged with `className`. */\n root?: string;\n /** The pill around the icon that holds the active indicator and the state layer. */\n pill?: string;\n /** The active indicator: the filled pill shown on the current destination. */\n indicator?: string;\n /** The state layer over the pill (hover, focus and press). */\n stateLayer?: string;\n /** The icon. */\n icon?: string;\n /** The label text. */\n label?: string;\n /** The badge's wrapper: on the icon, or after the label in the expanded rail. */\n badge?: string;\n}\n\nexport interface NavItemOwnProps {\n /** The icon. Hidden from assistive tech; the label names the item. */\n icon: ReactElement;\n /** Icon shown while selected, e.g. a filled variant. */\n selectedIcon?: ReactElement;\n /** The label. */\n children: ReactNode;\n /** Marks the current destination (`aria-current=\"page\"`). */\n selected?: boolean;\n /**\n * A `Badge` for dynamic information, such as a count. It overlaps the icon's top end, or\n * sits after the label in the expanded rail. It describes the item to assistive tech\n * with its text, or with `badgeLabel`.\n */\n badge?: ReactElement<BadgeProps>;\n /**\n * What the badge means, read after the item's name, e.g. \"3 new messages\". Defaults to\n * the badge's `label` or text; a small (dot) badge reads its `labels.newNotification`\n * (\"New notification\"), as M3 asks of badges without a count.\n */\n badgeLabel?: string;\n /**\n * Called when the current destination is pressed again. M3: re-selecting the active\n * destination resets the page's scroll position to the top, so scroll to the top here.\n */\n onReselect?: () => void;\n /** Classes for each part, by slot. */\n classNames?: NavItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof NavItemOwnProps | 'toggle'>;\n\nexport type NavItemProps = NavItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * Slides the current item's pill from the previous current item's, on the fast spatial\n * spring, changing width on the way when their labels differ. `lastIndicatorRef` holds the\n * last current pill for every item in one list; without it the pill doesn't slide, and under\n * reduced motion it fades in place.\n */\nfunction useSlidingIndicator(\n selected: boolean,\n lastIndicatorRef: RefObject<HTMLElement | null> | undefined,\n) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n const wasSelected = useRef(selected);\n const spring = useM3Spring('spatial', 'fast');\n const reducedMotion = useReducedMotion();\n useLayoutEffect(() => {\n const indicator = indicatorRef.current;\n const changed = wasSelected.current !== selected;\n wasSelected.current = selected;\n if (!lastIndicatorRef || !selected || !indicator) return;\n const previous = lastIndicatorRef.current;\n lastIndicatorRef.current = indicator;\n if (!changed || reducedMotion || !previous?.isConnected || previous === indicator) return;\n // Measure both pills at rest; an earlier slide may have left a transform or width behind.\n indicator.style.transform = '';\n indicator.style.width = '';\n const from = previous.getBoundingClientRect();\n const to = indicator.getBoundingClientRect();\n const offset = from.top - to.top;\n if (offset === 0) return;\n // Start from the old place and width now: Motion's first frame comes a frame later.\n indicator.style.transform = `translateY(${offset}px)`;\n if (from.width === to.width) {\n animate(indicator, { y: [offset, 0] }, spring);\n return;\n }\n indicator.style.width = `${from.width}px`;\n // Width animates in px; once settled the pill sizes itself to its content again.\n void animate(indicator, { y: [offset, 0], width: [from.width, to.width] }, spring).then(() => {\n indicator.style.width = '';\n });\n }, [selected, lastIndicatorRef, reducedMotion, spring]);\n return indicatorRef;\n}\n\n/**\n * What a badge tells assistive tech about its item: its `label`, its text, or, for a small\n * badge (no children), \"New notification\" (`labels.newNotification`).\n */\nfunction badgeDescription(badge: ReactElement<BadgeProps> | undefined): string | null {\n // A badge can arrive as a lazy server reference without props yet (React's dev server\n // rendering), so read it only from a real element.\n if (!isValidElement<BadgeProps>(badge)) return null;\n const { label, labels, children } = badge.props;\n if (label != null) return label;\n if (typeof children === 'string' || typeof children === 'number') return String(children);\n if (children == null || children === false) {\n return labels?.newNotification ?? 'New notification';\n }\n return null;\n}\n\n/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */\nexport function NavItem({\n layout,\n enterLabel,\n lastIndicatorRef,\n icon,\n selectedIcon,\n children,\n selected,\n badge,\n badgeLabel,\n onReselect,\n classNames,\n className,\n ...base\n}: NavItemProps & {\n layout: NavItemLayout;\n enterLabel?: boolean;\n /** The list's last current pill, so this item's pill slides from it when it becomes current. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n}) {\n const styles = navItemStyles({ layout, enterLabel, slide: Boolean(lastIndicatorRef) });\n const indicatorRef = useSlidingIndicator(Boolean(selected), lastIndicatorRef);\n const descriptionId = useId();\n const inline = layout === 'rail-start' || layout === 'bar-start';\n // M3: badges sit on the icon, except in the expanded rail, where they follow the label.\n const badgeAfterLabel = layout === 'rail-start';\n const description = badgeLabel ?? badgeDescription(badge);\n const label = (\n <span\n data-nav-label=\"\"\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {children}\n </span>\n );\n const iconFaces = selectedIcon ? (\n <>\n <span data-shown={!selected || undefined} className={styles.iconFace()}>\n {icon}\n </span>\n <span data-shown={selected || undefined} className={styles.iconFace()}>\n {selectedIcon}\n </span>\n </>\n ) : (\n icon\n );\n const action = base as ButtonBaseActionProps;\n // M3: a press ripples through the pill. The item is the pressable element, so the pill's\n // state layer gets its own press state and a ripple origin in its own box.\n const stateLayerRef = useRef<HTMLSpanElement>(null);\n const [pressed, setPressed] = useState(false);\n const rippling = useRippleHold(pressed && !action.disabled);\n return (\n <ButtonBase\n {...action}\n onPress={(event) => {\n action.onPress?.(event);\n if (selected) onReselect?.();\n }}\n onPressChange={(isPressed) => {\n action.onPressChange?.(isPressed);\n setPressed(isPressed);\n }}\n onPointerDown={(event: PointerEvent<HTMLButtonElement>) => {\n action.onPointerDown?.(event);\n if (stateLayerRef.current && !action.disabled && event.button === 0) {\n setRippleOrigin(stateLayerRef.current, event.clientX, event.clientY);\n }\n }}\n onKeyDown={(event: KeyboardEvent<HTMLButtonElement>) => {\n action.onKeyDown?.(event);\n if (\n stateLayerRef.current &&\n !action.disabled &&\n (event.key === 'Enter' || event.key === ' ')\n ) {\n setRippleOrigin(stateLayerRef.current);\n }\n }}\n data-nav-item=\"\"\n // An explicit value wins, for a destination that shows as current without being the page.\n aria-current={action['aria-current'] ?? (selected ? 'page' : undefined)}\n aria-describedby={\n [action['aria-describedby'], badge && description != null ? descriptionId : undefined]\n .filter(Boolean)\n .join(' ') || undefined\n }\n data-current={selected || undefined}\n // The bar's or rail's component tokens, resolved on the item for its state and layout.\n data-tokens={mergeDataTokens(\n layout.startsWith('rail') ? 'md-comp-nav-rail' : 'md-comp-nav-bar',\n action,\n )}\n data-layout={layout}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <span data-slot=\"pill\" className={styles.pill({ class: classNames?.pill })}>\n <span\n ref={indicatorRef}\n aria-hidden=\"true\"\n data-slot=\"indicator\"\n className={styles.indicator({ class: classNames?.indicator })}\n />\n <span\n ref={stateLayerRef}\n aria-hidden=\"true\"\n data-rippling={rippling || undefined}\n data-disabled={action.disabled || undefined}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n <span className={styles.content()}>\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={styles.icon({ class: classNames?.icon })}\n >\n {badge && !badgeAfterLabel ? (\n <BadgedBox badge={badge} className={styles.badgedBox({ class: classNames?.badge })}>\n <span className={styles.iconStack()}>{iconFaces}</span>\n </BadgedBox>\n ) : (\n iconFaces\n )}\n </span>\n {inline && label}\n {badge && badgeAfterLabel && (\n <span aria-hidden=\"true\" className={styles.badge({ class: classNames?.badge })}>\n {badge}\n </span>\n )}\n </span>\n </span>\n {!inline && label}\n {badge && description != null && (\n <span id={descriptionId} hidden>\n {description}\n </span>\n )}\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"NavItem.js","names":[],"sources":["../../../src/components/navigation/NavItem.tsx"],"sourcesContent":["'use client';\n\nimport { animate, useReducedMotion } from 'motion/react';\nimport {\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { setRippleOrigin, useRippleHold } from '../../primitives/use-m3-interaction';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { cn } from '../../utils/cn';\nimport { BadgedBox, DEFAULT_BADGE_LABELS, type BadgeProps } from '../badge/Badge';\nimport { useLabels } from '../labels/use-labels';\nimport { navItemStyles, type NavItemLayout } from './nav-item-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\nexport interface NavItemClassNames {\n /** The item's button or link, merged with `className`. */\n root?: string;\n /** The pill around the icon that holds the active indicator and the state layer. */\n pill?: string;\n /** The active indicator: the filled pill shown on the current destination. */\n indicator?: string;\n /** The state layer over the pill (hover, focus and press). */\n stateLayer?: string;\n /** The icon. */\n icon?: string;\n /** The label text. */\n label?: string;\n /** The badge's wrapper: on the icon, or after the label in the expanded rail. */\n badge?: string;\n}\n\nexport interface NavItemOwnProps {\n /** The icon. Hidden from assistive tech; the label names the item. */\n icon: ReactElement;\n /** Icon shown while selected, e.g. a filled variant. */\n selectedIcon?: ReactElement;\n /** The label. */\n children: ReactNode;\n /** Marks the current destination (`aria-current=\"page\"`). */\n selected?: boolean;\n /**\n * A `Badge` for dynamic information, such as a count. It overlaps the icon's top end, or\n * sits after the label in the expanded rail. It describes the item to assistive tech\n * with its text, or with `badgeLabel`.\n */\n badge?: ReactElement<BadgeProps>;\n /**\n * What the badge means, read after the item's name, e.g. \"3 new messages\". Defaults to\n * the badge's `label` or text; a small (dot) badge reads its `labels.newNotification`\n * (\"New notification\"), as M3 asks of badges without a count.\n */\n badgeLabel?: string;\n /**\n * Called when the current destination is pressed again. M3: re-selecting the active\n * destination resets the page's scroll position to the top, so scroll to the top here.\n */\n onReselect?: () => void;\n /** Classes for each part, by slot. */\n classNames?: NavItemClassNames;\n}\n\ntype OmitItem<T> = Omit<T, keyof NavItemOwnProps | 'toggle'>;\n\nexport type NavItemProps = NavItemOwnProps &\n (OmitItem<ButtonBaseActionProps> | OmitItem<ButtonBaseLinkProps>);\n\n/**\n * Slides the current item's pill from the previous current item's, on the fast spatial\n * spring, changing width on the way when their labels differ. `lastIndicatorRef` holds the\n * last current pill for every item in one list; without it the pill doesn't slide, and under\n * reduced motion it fades in place.\n */\nfunction useSlidingIndicator(\n selected: boolean,\n lastIndicatorRef: RefObject<HTMLElement | null> | undefined,\n) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n const wasSelected = useRef(selected);\n const spring = useM3Spring('spatial', 'fast');\n const reducedMotion = useReducedMotion();\n useLayoutEffect(() => {\n const indicator = indicatorRef.current;\n const changed = wasSelected.current !== selected;\n wasSelected.current = selected;\n if (!lastIndicatorRef || !selected || !indicator) return;\n const previous = lastIndicatorRef.current;\n lastIndicatorRef.current = indicator;\n if (!changed || reducedMotion || !previous?.isConnected || previous === indicator) return;\n // Measure both pills at rest; an earlier slide may have left a transform or width behind.\n indicator.style.transform = '';\n indicator.style.width = '';\n const from = previous.getBoundingClientRect();\n const to = indicator.getBoundingClientRect();\n const offset = from.top - to.top;\n if (offset === 0) return;\n // Start from the old place and width now: Motion's first frame comes a frame later.\n indicator.style.transform = `translateY(${offset}px)`;\n if (from.width === to.width) {\n animate(indicator, { y: [offset, 0] }, spring);\n return;\n }\n indicator.style.width = `${from.width}px`;\n // Width animates in px; once settled the pill sizes itself to its content again.\n void animate(indicator, { y: [offset, 0], width: [from.width, to.width] }, spring).then(() => {\n indicator.style.width = '';\n });\n }, [selected, lastIndicatorRef, reducedMotion, spring]);\n return indicatorRef;\n}\n\n/**\n * What a badge tells assistive tech about its item: its `label`, its text, or, for a small\n * badge (no children), `newNotification`: the badge's own word, a `LabelsProvider`'s, or\n * \"New notification\".\n */\nfunction badgeDescription(\n badge: ReactElement<BadgeProps> | undefined,\n newNotification: string,\n): string | null {\n // A badge can arrive as a lazy server reference without props yet (React's dev server\n // rendering), so read it only from a real element.\n if (!isValidElement<BadgeProps>(badge)) return null;\n const { label, children } = badge.props;\n if (label != null) return label;\n if (typeof children === 'string' || typeof children === 'number') return String(children);\n if (children == null || children === false) return newNotification;\n return null;\n}\n\n/** Shared navigation item: a link (`href`) or button with the M3 pill indicator. */\nexport function NavItem({\n layout,\n enterLabel,\n lastIndicatorRef,\n icon,\n selectedIcon,\n children,\n selected,\n badge,\n badgeLabel,\n onReselect,\n classNames,\n className,\n ...base\n}: NavItemProps & {\n layout: NavItemLayout;\n enterLabel?: boolean;\n /** The list's last current pill, so this item's pill slides from it when it becomes current. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n}) {\n const styles = navItemStyles({ layout, enterLabel, slide: Boolean(lastIndicatorRef) });\n const indicatorRef = useSlidingIndicator(Boolean(selected), lastIndicatorRef);\n const descriptionId = useId();\n const inline = layout === 'rail-start' || layout === 'bar-start';\n // M3: badges sit on the icon, except in the expanded rail, where they follow the label.\n const badgeAfterLabel = layout === 'rail-start';\n // The badge's words as the badge itself reads them, a `LabelsProvider`'s included.\n const { newNotification } = useLabels(\n 'Badge',\n DEFAULT_BADGE_LABELS,\n isValidElement<BadgeProps>(badge) ? badge.props.labels : undefined,\n );\n const description = badgeLabel ?? badgeDescription(badge, newNotification);\n const label = (\n <span\n data-nav-label=\"\"\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {children}\n </span>\n );\n const iconFaces = selectedIcon ? (\n <>\n <span data-shown={!selected || undefined} className={styles.iconFace()}>\n {icon}\n </span>\n <span data-shown={selected || undefined} className={styles.iconFace()}>\n {selectedIcon}\n </span>\n </>\n ) : (\n icon\n );\n const action = base as ButtonBaseActionProps;\n // M3: a press ripples through the pill. The item is the pressable element, so the pill's\n // state layer gets its own press state and a ripple origin in its own box.\n const stateLayerRef = useRef<HTMLSpanElement>(null);\n const [pressed, setPressed] = useState(false);\n const rippling = useRippleHold(pressed && !action.disabled);\n return (\n <ButtonBase\n {...action}\n onPress={(event) => {\n action.onPress?.(event);\n if (selected) onReselect?.();\n }}\n onPressChange={(isPressed) => {\n action.onPressChange?.(isPressed);\n setPressed(isPressed);\n }}\n onPointerDown={(event: PointerEvent<HTMLButtonElement>) => {\n action.onPointerDown?.(event);\n if (stateLayerRef.current && !action.disabled && event.button === 0) {\n setRippleOrigin(stateLayerRef.current, event.clientX, event.clientY);\n }\n }}\n onKeyDown={(event: KeyboardEvent<HTMLButtonElement>) => {\n action.onKeyDown?.(event);\n if (\n stateLayerRef.current &&\n !action.disabled &&\n (event.key === 'Enter' || event.key === ' ')\n ) {\n setRippleOrigin(stateLayerRef.current);\n }\n }}\n data-nav-item=\"\"\n // An explicit value wins, for a destination that shows as current without being the page.\n aria-current={action['aria-current'] ?? (selected ? 'page' : undefined)}\n aria-describedby={\n [action['aria-describedby'], badge && description != null ? descriptionId : undefined]\n .filter(Boolean)\n .join(' ') || undefined\n }\n data-current={selected || undefined}\n // The bar's or rail's component tokens, resolved on the item for its state and layout.\n data-tokens={mergeDataTokens(\n layout.startsWith('rail') ? 'md-comp-nav-rail' : 'md-comp-nav-bar',\n action,\n )}\n data-layout={layout}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <span data-slot=\"pill\" className={styles.pill({ class: classNames?.pill })}>\n <span\n ref={indicatorRef}\n aria-hidden=\"true\"\n data-slot=\"indicator\"\n className={styles.indicator({ class: classNames?.indicator })}\n />\n <span\n ref={stateLayerRef}\n aria-hidden=\"true\"\n data-rippling={rippling || undefined}\n data-disabled={action.disabled || undefined}\n className={styles.stateLayer({ class: classNames?.stateLayer })}\n />\n <span className={styles.content()}>\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={styles.icon({ class: classNames?.icon })}\n >\n {badge && !badgeAfterLabel ? (\n <BadgedBox badge={badge} className={styles.badgedBox({ class: classNames?.badge })}>\n <span className={styles.iconStack()}>{iconFaces}</span>\n </BadgedBox>\n ) : (\n iconFaces\n )}\n </span>\n {inline && label}\n {badge && badgeAfterLabel && (\n <span aria-hidden=\"true\" className={styles.badge({ class: classNames?.badge })}>\n {badge}\n </span>\n )}\n </span>\n </span>\n {!inline && label}\n {badge && description != null && (\n <span id={descriptionId} hidden>\n {description}\n </span>\n )}\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
@@ -45,7 +45,7 @@ interface PullToRefreshOwnProps {
|
|
|
45
45
|
disabled?: boolean;
|
|
46
46
|
/**
|
|
47
47
|
* Words the pull to refresh announces, with English defaults; name them for what refreshes
|
|
48
|
-
* ("Refreshing messages").
|
|
48
|
+
* ("Refreshing messages"). A {@link LabelsProvider} sets them for the whole app.
|
|
49
49
|
*/
|
|
50
50
|
labels?: Partial<PullToRefreshLabels>;
|
|
51
51
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { useM3Spring } from "../../motion/use-m3-spring.js";
|
|
4
5
|
import { usePreloadWhenIdle } from "../../utils/load-on-demand.js";
|
|
5
6
|
import { LazyLoadingIndicator, preloadLoadingIndicator } from "../loading-indicator/lazy-loading-indicator.js";
|
|
@@ -79,10 +80,7 @@ const isThenable = (value) => typeof value === "object" && value !== null && typ
|
|
|
79
80
|
* </PullToRefresh>
|
|
80
81
|
*/
|
|
81
82
|
function PullToRefresh({ refreshing: refreshingProp, onRefresh, threshold = 80, disabled = false, labels: labelsProp, children, classNames, className, ref, ...rest }) {
|
|
82
|
-
const labels =
|
|
83
|
-
...DEFAULT_LABELS,
|
|
84
|
-
...labelsProp
|
|
85
|
-
};
|
|
83
|
+
const labels = useLabels("PullToRefresh", DEFAULT_LABELS, labelsProp);
|
|
86
84
|
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
87
85
|
const refreshing = refreshingProp ?? pendingRefresh;
|
|
88
86
|
const fraction = useMotionValue(refreshing ? 1 : 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PullToRefresh.js","names":[],"sources":["../../../src/components/pull-to-refresh/PullToRefresh.tsx"],"sourcesContent":["'use client';\n\nimport {\n animate,\n useMotionValue,\n useMotionValueEvent,\n useReducedMotion,\n type MotionValue,\n} from 'motion/react';\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { cn } from '../../utils/cn';\nimport { usePreloadWhenIdle } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport {\n POSITIONAL_THRESHOLD,\n distanceFraction,\n overshootRotation,\n passesThreshold,\n} from './pull-to-refresh-math';\nimport { pullToRefreshStyles } from './pull-to-refresh-styles';\n\nexport interface PullToRefreshClassNames {\n /** The outer element, around the content. */\n root?: string;\n /**\n * The zero-height sticky layer the indicator hangs from, at the top of the area. Give it a\n * `top-*` class when a sticky app bar covers the top of a scrolling page. Its margins are\n * the library's, so a flex root's `gap-*` or a block root's `space-y-*` never moves the\n * content. In a grid root it is a grid item, so one row gap opens above the content.\n */\n layer?: string;\n /** The 48px circle holding the loading indicator. */\n indicator?: string;\n}\n\n/** Words the pull to refresh announces. */\nexport interface PullToRefreshLabels {\n /** The indicator's name while it follows a pull. @default \"Pull to refresh\" */\n pull: string;\n /**\n * The indicator's name while refreshing, also announced when a refresh starts.\n * @default \"Refreshing\"\n */\n refreshing: string;\n /** Announced when a refresh completes. @default \"Refreshed\" */\n refreshed: string;\n}\n\nconst DEFAULT_LABELS: PullToRefreshLabels = {\n pull: 'Pull to refresh',\n refreshing: 'Refreshing',\n refreshed: 'Refreshed',\n};\n\n/** How far a gesture moves before it's a pull or the browser's, px. */\nconst POINTER_SLOP = 4;\n\ninterface PullToRefreshOwnProps {\n /**\n * Whether a refresh is running. While it is, the indicator rests at the threshold as an\n * indeterminate loading indicator; when it turns `false` the indicator leaves. Set it `true`\n * from a refresh button too, and the indicator comes in the same way. Leave it out to let a\n * promise returned by `onRefresh` drive it.\n */\n refreshing?: boolean;\n /**\n * Called when a pull is released past the threshold. Start the refresh and set\n * `refreshing`, or return a promise: without `refreshing`, the indicator stays until it\n * settles.\n */\n onRefresh: () => void | Promise<unknown>;\n /** How far the indicator travels before a release refreshes, px. @default 80 */\n threshold?: number;\n /** Turns the gesture off. A running refresh still shows its indicator. @default false */\n disabled?: boolean;\n /**\n * Words the pull to refresh announces, with English defaults; name them for what refreshes\n * (\"Refreshing messages\").\n */\n labels?: Partial<PullToRefreshLabels>;\n /**\n * The content: a scrolling list, grid or card collection, or a whole page. On a grid root\n * (`grid` on `className`) the indicator's layer is a grid item, so one row gap opens above\n * the content; put the grid on an element inside instead. Flex, block and `space-y-*` roots\n * are exact.\n */\n children?: ReactNode;\n /** Classes for the parts: `root`, `layer` (the sticky layer at the top) and `indicator`. */\n classNames?: PullToRefreshClassNames;\n}\n\nexport type PullToRefreshProps = PullToRefreshOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof PullToRefreshOwnProps>;\n\n/** A pointer or touch gesture over the area. */\ninterface Gesture {\n /** The touch's identifier, or the pointer's id. */\n id: number;\n /** `touch` for touch events, otherwise the pointer type (mouse or pen). */\n source: 'touch' | 'pointer';\n startX: number;\n startY: number;\n lastY: number;\n /**\n * `tracking`: not pulling (yet). `pulling`: the indicator follows the gesture. `inert`: the\n * area claims the gesture (so the browser doesn't overscroll) but it does nothing, because a\n * refresh or a settle animation is running. `native`: the browser scrolls; hands off.\n */\n phase: 'tracking' | 'pulling' | 'inert' | 'native';\n /** Every scroll container between the gesture's target and the area is at its top. */\n atTop: boolean;\n /** How far the gesture has pulled, px (Compose's `distancePulled`). */\n pulled: number;\n}\n\nconst isScrollContainer = (element: Element) =>\n /^(auto|scroll|overlay)$/.test(getComputedStyle(element).overflowY);\n\n/**\n * Whether every scroll container from `target` up to the area's (the area itself if it\n * scrolls, else the nearest one above it, or the page) is scrolled to its top, so a downward\n * drag can't scroll anything and pulls instead.\n */\nfunction isAtTop(target: EventTarget | null, root: HTMLElement): boolean {\n let passedRoot = false;\n for (\n let node: Element | null = target instanceof Element ? target : root;\n node;\n node = node.parentElement\n ) {\n if (node.scrollTop >= 1) return false;\n if (node === root) {\n if (isScrollContainer(node)) return true;\n passedRoot = true;\n } else if (passedRoot && isScrollContainer(node)) {\n return true;\n }\n }\n return true;\n}\n\n/**\n * Whether something between `target` and the area handles vertical drags itself (its\n * `touch-action` doesn't allow vertical panning: a vertical slider, a drawing surface), so\n * the gesture is left to it.\n */\nfunction ownsVerticalDrag(target: EventTarget | null, root: HTMLElement): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node && node !== root;\n node = node.parentElement\n ) {\n const touchAction = getComputedStyle(node).touchAction;\n if (touchAction === 'none') return true;\n if (/pan-x/.test(touchAction) && !/pan-y|pan-up|pan-down/.test(touchAction)) return true;\n }\n return false;\n}\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * M3 pull to refresh: at the top of its content, a downward drag (touch, mouse or pen) pulls\n * a contained loading indicator in from the top edge, morphing with the pull. Releasing past\n * the threshold calls `onRefresh`; the indicator then rests at the threshold, as an\n * indeterminate loading indicator, until `refreshing` turns `false`. Releasing short of the\n * threshold, or pushing back up, cancels.\n *\n * Wrap a scroll container (give the root `overflow-y-auto` and a height, or put the scrolling\n * element inside it) or a whole page. Scrolling is untouched: the gesture only pulls when the\n * content is at its top and the drag goes down.\n *\n * A `gap-*` on a flex root or `space-y-*` on a block root spaces the content exactly. On a\n * grid root, every in-flow child is a grid item, so the indicator's sticky layer takes a\n * zero-height row of its own and one `row-gap` opens above the content (removing it would\n * mean positioning the layer, which layout safety rules out). Put the grid on an element\n * inside: `<PullToRefresh><div className=\"grid gap-4\">…</div></PullToRefresh>`.\n *\n * Pulling isn't available to everyone, so also offer a refresh button that sets `refreshing`,\n * in an app bar, a menu or next to the content (the spec's accessibility guidance).\n *\n * The motion and sizes follow Compose's `PullToRefreshBox` with\n * `PullToRefreshDefaults.LoadingIndicator`: an 80px threshold, the indicator moving at half\n * the finger's speed and meeting resistance past the threshold, a 48px `primary-container`\n * circle at elevation 0, and a crossfade to the indeterminate indicator while refreshing.\n *\n * @example\n * <PullToRefresh refreshing={loading} onRefresh={reload} className=\"h-dvh overflow-y-auto\">\n * <List aria-label=\"Messages\">…</List>\n * </PullToRefresh>\n */\nexport function PullToRefresh({\n refreshing: refreshingProp,\n onRefresh,\n threshold = POSITIONAL_THRESHOLD,\n disabled = false,\n labels: labelsProp,\n children,\n classNames,\n className,\n ref,\n ...rest\n}: PullToRefreshProps) {\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const [pendingRefresh, setPendingRefresh] = useState(false);\n const refreshing = refreshingProp ?? pendingRefresh;\n const fraction = useMotionValue(refreshing ? 1 : 0);\n // Settling to the threshold or away never bounces: Compose animates it with\n // `Animatable.animateTo`'s default, non-bouncing spring, which the default effects spring\n // matches (2026-10-15; the fast spatial one dipped 3px under the threshold and back).\n const spring = useM3Spring('effects', 'default');\n // Under reduced motion the indicator jumps to the threshold or away; a pull still moves it.\n const settle = useReducedMotion() ? { duration: 0 } : spring;\n const [pulling, setPulling] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n // The indicator's code (its shapes and morphing) loads on demand: when the browser is first\n // idle, or when a pull starts, whichever comes first.\n usePreloadWhenIdle(preloadLoadingIndicator, !disabled);\n\n const rootRef = useObjectRef(ref);\n const animationRef = useRef<{ stop: () => void } | null>(null);\n const animatingRef = useRef(false);\n const mountedRef = useRef(true);\n // The latest props and helpers, for the native listeners added once.\n const latest = useRef({ refreshing, disabled, threshold, onRefresh, refreshingProp, settle });\n useLayoutEffect(() => {\n latest.current = { refreshing, disabled, threshold, onRefresh, refreshingProp, settle };\n });\n\n // The layer is the root's first child, so a consumer `gap-*` on a flex root would open a gap\n // below it and push the content down. It's cancelled after every render and whenever the\n // root resizes (a responsive class may change the gap).\n const layerRef = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n if (rootRef.current && layerRef.current) cancelRootGap(rootRef.current, layerRef.current);\n });\n useEffect(() => {\n const root = rootRef.current;\n if (!root || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n if (layerRef.current) cancelRootGap(root, layerRef.current);\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, [rootRef]);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n animationRef.current?.stop();\n };\n }, []);\n\n /** Springs the indicator to `target` (Compose's `animateToThreshold` / `animateToHidden`). */\n const settleRef = useRef((target: number) => {\n animationRef.current?.stop();\n animatingRef.current = true;\n const controls = animate(fraction, target, latest.current.settle);\n animationRef.current = controls;\n void controls.then(() => {\n if (animationRef.current === controls) animatingRef.current = false;\n });\n });\n\n // A refresh starting or ending moves the indicator to the threshold or away, and is\n // announced. The first render places it without either (Compose's `onAttach`).\n const previousRefreshing = useRef(refreshing);\n useEffect(() => {\n if (previousRefreshing.current === refreshing) return;\n previousRefreshing.current = refreshing;\n settleRef.current(refreshing ? 1 : 0);\n setAnnouncement(refreshing ? labels.refreshing : labels.refreshed);\n }, [refreshing, labels.refreshing, labels.refreshed]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n let gesture: Gesture | null = null;\n let suppressClick = false;\n\n const begin = (\n source: Gesture['source'],\n id: number,\n x: number,\n y: number,\n target: EventTarget | null,\n ) => {\n const { disabled: off } = latest.current;\n gesture =\n off || ownsVerticalDrag(target, root)\n ? null\n : {\n id,\n source,\n startX: x,\n startY: y,\n lastY: y,\n phase: 'tracking',\n atTop: isAtTop(target, root),\n pulled: 0,\n };\n // A pull may start: the indicator's code loads now (if the idle preload hasn't loaded it\n // yet), well before the gesture passes the slop and the indicator shows. A failed load is\n // tried again by the indicator itself.\n if (gesture?.atTop) void preloadLoadingIndicator().catch(() => undefined);\n };\n\n /** Moves the gesture to `y`; returns whether the area took the movement. */\n const move = (x: number, y: number, cancelable: boolean): boolean => {\n const current = gesture;\n if (!current || current.phase === 'native') return false;\n const dy = y - current.lastY;\n current.lastY = y;\n if (current.phase === 'tracking') {\n const totalX = x - current.startX;\n const totalY = y - current.startY;\n const distance = Math.max(Math.abs(totalX), Math.abs(totalY));\n // A mouse or pen must move past the slop first, so a click stays a click.\n if (current.source === 'pointer' && distance <= POINTER_SLOP) return false;\n const downward = totalY > 0 && totalY >= Math.abs(totalX) && current.atTop;\n if (!downward || !cancelable) {\n // Sideways, upward or mid-scroll: the browser's for the rest of the gesture. A\n // touch gets the same slop before it's decided, against a finger's jitter.\n if (!cancelable || distance > POINTER_SLOP) current.phase = 'native';\n return false;\n }\n const { refreshing: busy } = latest.current;\n if (busy || animatingRef.current) {\n current.phase = 'inert';\n return true;\n }\n current.phase = 'pulling';\n // A mouse or pen pull starts at the slop, so the indicator doesn't jump.\n current.pulled = Math.max(0, totalY - (current.source === 'pointer' ? POINTER_SLOP : 0));\n setPulling(true);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n return true;\n }\n if (current.phase === 'inert') return true;\n // Pulling: up and down both move the indicator (Compose's pre- and post-scroll).\n current.pulled = Math.max(0, current.pulled + dy);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n if (current.pulled === 0) {\n // Pushed all the way back: the gesture may scroll the content again.\n current.phase = 'tracking';\n current.startX = x;\n current.startY = y;\n current.atTop = true;\n setPulling(false);\n }\n return true;\n };\n\n /** Ends the gesture (Compose's `onRelease`); `cancel` never refreshes. */\n const end = (cancel: boolean) => {\n const current = gesture;\n gesture = null;\n if (!current || current.phase !== 'pulling') return;\n setPulling(false);\n if (current.source === 'pointer') {\n // A mouse pull ends with a click on whatever is under the pointer; it isn't one. The\n // click comes before any timer.\n suppressClick = true;\n setTimeout(() => {\n suppressClick = false;\n });\n }\n const { threshold: limit, refreshingProp: controlled } = latest.current;\n if (!cancel && passesThreshold(current.pulled, limit)) {\n const result = latest.current.onRefresh();\n if (controlled === undefined && isThenable(result)) {\n setPendingRefresh(true);\n const done = () => {\n if (mountedRef.current) setPendingRefresh(false);\n };\n result.then(done, done);\n }\n }\n // Compose hides the indicator here; a refresh that starts moves it to the threshold.\n settleRef.current(0);\n };\n\n const touchOf = (event: TouchEvent) =>\n gesture === null\n ? undefined\n : [...event.changedTouches].find((touch) => touch.identifier === gesture?.id);\n\n const onTouchStart = (event: TouchEvent) => {\n if (event.touches.length > 1) {\n // A second finger (pinch to zoom) ends the pull without refreshing.\n end(true);\n return;\n }\n const touch = event.changedTouches[0];\n if (touch) begin('touch', touch.identifier, touch.clientX, touch.clientY, event.target);\n };\n const onTouchMove = (event: TouchEvent) => {\n const touch = touchOf(event);\n if (!touch || event.defaultPrevented) return;\n if (move(touch.clientX, touch.clientY, event.cancelable) && event.cancelable) {\n event.preventDefault();\n }\n };\n const onTouchEnd = (event: TouchEvent) => {\n if (touchOf(event)) end(event.type === 'touchcancel');\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (event.pointerType === 'touch' || event.button !== 0 || !event.isPrimary) return;\n begin('pointer', event.pointerId, event.clientX, event.clientY, event.target);\n };\n const onPointerMove = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n const wasPulling = gesture.phase === 'pulling';\n if (!move(event.clientX, event.clientY, true)) return;\n event.preventDefault();\n if (!wasPulling && gesture?.phase === 'pulling') {\n root.setPointerCapture?.(event.pointerId);\n // A mouse drag would otherwise select the text it passes.\n window.getSelection()?.removeAllRanges();\n }\n };\n const onPointerEnd = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n end(event.type === 'pointercancel');\n };\n const onClick = (event: MouseEvent) => {\n if (!suppressClick) return;\n suppressClick = false;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const options = { passive: false } as const;\n root.addEventListener('touchstart', onTouchStart, { passive: true });\n root.addEventListener('touchmove', onTouchMove, options);\n root.addEventListener('touchend', onTouchEnd);\n root.addEventListener('touchcancel', onTouchEnd);\n root.addEventListener('pointerdown', onPointerDown);\n root.addEventListener('pointermove', onPointerMove);\n root.addEventListener('pointerup', onPointerEnd);\n root.addEventListener('pointercancel', onPointerEnd);\n root.addEventListener('click', onClick, { capture: true });\n return () => {\n root.removeEventListener('touchstart', onTouchStart);\n root.removeEventListener('touchmove', onTouchMove);\n root.removeEventListener('touchend', onTouchEnd);\n root.removeEventListener('touchcancel', onTouchEnd);\n root.removeEventListener('pointerdown', onPointerDown);\n root.removeEventListener('pointermove', onPointerMove);\n root.removeEventListener('pointerup', onPointerEnd);\n root.removeEventListener('pointercancel', onPointerEnd);\n root.removeEventListener('click', onClick, { capture: true });\n };\n }, [fraction, rootRef]);\n\n const styles = pullToRefreshStyles();\n\n return (\n <div\n {...rest}\n ref={rootRef}\n data-pulling={pulling || undefined}\n data-refreshing={refreshing || undefined}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={layerRef}\n className={styles.layer({ class: classNames?.layer })}\n style={{ '--m3-pull-to-refresh-threshold': `${threshold}px` } as CSSProperties}\n >\n <div className={styles.clip()}>\n <PullIndicator\n fraction={fraction}\n refreshing={refreshing}\n threshold={threshold}\n labels={labels}\n className={styles.indicator({ class: classNames?.indicator })}\n faceClassName={styles.face()}\n />\n </div>\n <span role=\"status\" className=\"sr-only\">\n {announcement}\n </span>\n </div>\n {children}\n </div>\n );\n}\n\n/**\n * Keeps the root's own spacing from opening below the zero-height layer. Its margins are the\n * library's: zero, so a `space-y-*` root doesn't push the content down, and in a flex root a\n * negative margin as large as the gap along the main axis. In a grid root it moves to an\n * implicit row of its own before the first one, across every column, so the content keeps its\n * rows and columns; a grid row's size can't go below zero, so one row gap stays above the\n * content (documented: put the grid on an element inside the root).\n */\nfunction cancelRootGap(root: HTMLElement, layer: HTMLElement) {\n const style = getComputedStyle(root);\n const grid = style.display.endsWith('grid');\n const flex = style.display.endsWith('flex');\n const row = flex && style.flexDirection.startsWith('row');\n const reverse = flex && style.flexDirection.endsWith('reverse');\n const gap = Number.parseFloat(row ? style.columnGap : style.rowGap);\n for (const side of ['BlockStart', 'BlockEnd', 'InlineStart', 'InlineEnd'] as const) {\n layer.style[`margin${side}`] = '0px';\n }\n if (flex && gap > 0) {\n layer.style[`margin${row ? 'Inline' : 'Block'}${reverse ? 'Start' : 'End'}`] = `${-gap}px`;\n }\n layer.style.gridRow = grid ? 'span 1 / 1' : '';\n layer.style.gridColumn = grid ? '1 / -1' : '';\n}\n\n/**\n * The indicator, subscribed to the pull on its own so a pull re-renders only it, never the\n * content. Hidden at rest; while pulled it is a determinate loading indicator following the\n * pull, and while refreshing an indeterminate one, crossfading between them.\n */\nfunction PullIndicator({\n fraction,\n refreshing,\n threshold,\n labels,\n className,\n faceClassName,\n}: {\n fraction: MotionValue<number>;\n refreshing: boolean;\n threshold: number;\n labels: PullToRefreshLabels;\n className: string;\n faceClassName: string;\n}) {\n const [value, setValue] = useState(() => fraction.get());\n useMotionValueEvent(fraction, 'change', setValue);\n if (value <= 0 && !refreshing) return null;\n const rotation = overshootRotation(value);\n return (\n <div\n data-indicator=\"\"\n className={className}\n // Compose's `translationY = distanceFraction × maxDistance − height`.\n style={{ transform: `translateY(calc(${value * threshold}px - 100%))` }}\n >\n <div\n data-active={!refreshing}\n aria-hidden={refreshing || undefined}\n className={faceClassName}\n style={rotation === 0 ? undefined : { transform: `rotate(${rotation}deg)` }}\n >\n <LazyLoadingIndicator\n variant=\"contained\"\n value={Math.min(Math.max(value, 0), 1)}\n aria-label={labels.pull}\n />\n </div>\n <div\n data-active={refreshing}\n aria-hidden={!refreshing || undefined}\n className={faceClassName}\n >\n <LazyLoadingIndicator variant=\"contained\" aria-label={labels.refreshing} />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PullToRefresh.js","names":[],"sources":["../../../src/components/pull-to-refresh/PullToRefresh.tsx"],"sourcesContent":["'use client';\n\nimport {\n animate,\n useMotionValue,\n useMotionValueEvent,\n useReducedMotion,\n type MotionValue,\n} from 'motion/react';\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { useM3Spring } from '../../motion/use-m3-spring';\nimport { cn } from '../../utils/cn';\nimport { usePreloadWhenIdle } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport {\n POSITIONAL_THRESHOLD,\n distanceFraction,\n overshootRotation,\n passesThreshold,\n} from './pull-to-refresh-math';\nimport { pullToRefreshStyles } from './pull-to-refresh-styles';\n\nexport interface PullToRefreshClassNames {\n /** The outer element, around the content. */\n root?: string;\n /**\n * The zero-height sticky layer the indicator hangs from, at the top of the area. Give it a\n * `top-*` class when a sticky app bar covers the top of a scrolling page. Its margins are\n * the library's, so a flex root's `gap-*` or a block root's `space-y-*` never moves the\n * content. In a grid root it is a grid item, so one row gap opens above the content.\n */\n layer?: string;\n /** The 48px circle holding the loading indicator. */\n indicator?: string;\n}\n\n/** Words the pull to refresh announces. */\nexport interface PullToRefreshLabels {\n /** The indicator's name while it follows a pull. @default \"Pull to refresh\" */\n pull: string;\n /**\n * The indicator's name while refreshing, also announced when a refresh starts.\n * @default \"Refreshing\"\n */\n refreshing: string;\n /** Announced when a refresh completes. @default \"Refreshed\" */\n refreshed: string;\n}\n\nconst DEFAULT_LABELS: PullToRefreshLabels = {\n pull: 'Pull to refresh',\n refreshing: 'Refreshing',\n refreshed: 'Refreshed',\n};\n\n/** How far a gesture moves before it's a pull or the browser's, px. */\nconst POINTER_SLOP = 4;\n\ninterface PullToRefreshOwnProps {\n /**\n * Whether a refresh is running. While it is, the indicator rests at the threshold as an\n * indeterminate loading indicator; when it turns `false` the indicator leaves. Set it `true`\n * from a refresh button too, and the indicator comes in the same way. Leave it out to let a\n * promise returned by `onRefresh` drive it.\n */\n refreshing?: boolean;\n /**\n * Called when a pull is released past the threshold. Start the refresh and set\n * `refreshing`, or return a promise: without `refreshing`, the indicator stays until it\n * settles.\n */\n onRefresh: () => void | Promise<unknown>;\n /** How far the indicator travels before a release refreshes, px. @default 80 */\n threshold?: number;\n /** Turns the gesture off. A running refresh still shows its indicator. @default false */\n disabled?: boolean;\n /**\n * Words the pull to refresh announces, with English defaults; name them for what refreshes\n * (\"Refreshing messages\"). A {@link LabelsProvider} sets them for the whole app.\n */\n labels?: Partial<PullToRefreshLabels>;\n /**\n * The content: a scrolling list, grid or card collection, or a whole page. On a grid root\n * (`grid` on `className`) the indicator's layer is a grid item, so one row gap opens above\n * the content; put the grid on an element inside instead. Flex, block and `space-y-*` roots\n * are exact.\n */\n children?: ReactNode;\n /** Classes for the parts: `root`, `layer` (the sticky layer at the top) and `indicator`. */\n classNames?: PullToRefreshClassNames;\n}\n\nexport type PullToRefreshProps = PullToRefreshOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof PullToRefreshOwnProps>;\n\n/** A pointer or touch gesture over the area. */\ninterface Gesture {\n /** The touch's identifier, or the pointer's id. */\n id: number;\n /** `touch` for touch events, otherwise the pointer type (mouse or pen). */\n source: 'touch' | 'pointer';\n startX: number;\n startY: number;\n lastY: number;\n /**\n * `tracking`: not pulling (yet). `pulling`: the indicator follows the gesture. `inert`: the\n * area claims the gesture (so the browser doesn't overscroll) but it does nothing, because a\n * refresh or a settle animation is running. `native`: the browser scrolls; hands off.\n */\n phase: 'tracking' | 'pulling' | 'inert' | 'native';\n /** Every scroll container between the gesture's target and the area is at its top. */\n atTop: boolean;\n /** How far the gesture has pulled, px (Compose's `distancePulled`). */\n pulled: number;\n}\n\nconst isScrollContainer = (element: Element) =>\n /^(auto|scroll|overlay)$/.test(getComputedStyle(element).overflowY);\n\n/**\n * Whether every scroll container from `target` up to the area's (the area itself if it\n * scrolls, else the nearest one above it, or the page) is scrolled to its top, so a downward\n * drag can't scroll anything and pulls instead.\n */\nfunction isAtTop(target: EventTarget | null, root: HTMLElement): boolean {\n let passedRoot = false;\n for (\n let node: Element | null = target instanceof Element ? target : root;\n node;\n node = node.parentElement\n ) {\n if (node.scrollTop >= 1) return false;\n if (node === root) {\n if (isScrollContainer(node)) return true;\n passedRoot = true;\n } else if (passedRoot && isScrollContainer(node)) {\n return true;\n }\n }\n return true;\n}\n\n/**\n * Whether something between `target` and the area handles vertical drags itself (its\n * `touch-action` doesn't allow vertical panning: a vertical slider, a drawing surface), so\n * the gesture is left to it.\n */\nfunction ownsVerticalDrag(target: EventTarget | null, root: HTMLElement): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node && node !== root;\n node = node.parentElement\n ) {\n const touchAction = getComputedStyle(node).touchAction;\n if (touchAction === 'none') return true;\n if (/pan-x/.test(touchAction) && !/pan-y|pan-up|pan-down/.test(touchAction)) return true;\n }\n return false;\n}\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * M3 pull to refresh: at the top of its content, a downward drag (touch, mouse or pen) pulls\n * a contained loading indicator in from the top edge, morphing with the pull. Releasing past\n * the threshold calls `onRefresh`; the indicator then rests at the threshold, as an\n * indeterminate loading indicator, until `refreshing` turns `false`. Releasing short of the\n * threshold, or pushing back up, cancels.\n *\n * Wrap a scroll container (give the root `overflow-y-auto` and a height, or put the scrolling\n * element inside it) or a whole page. Scrolling is untouched: the gesture only pulls when the\n * content is at its top and the drag goes down.\n *\n * A `gap-*` on a flex root or `space-y-*` on a block root spaces the content exactly. On a\n * grid root, every in-flow child is a grid item, so the indicator's sticky layer takes a\n * zero-height row of its own and one `row-gap` opens above the content (removing it would\n * mean positioning the layer, which layout safety rules out). Put the grid on an element\n * inside: `<PullToRefresh><div className=\"grid gap-4\">…</div></PullToRefresh>`.\n *\n * Pulling isn't available to everyone, so also offer a refresh button that sets `refreshing`,\n * in an app bar, a menu or next to the content (the spec's accessibility guidance).\n *\n * The motion and sizes follow Compose's `PullToRefreshBox` with\n * `PullToRefreshDefaults.LoadingIndicator`: an 80px threshold, the indicator moving at half\n * the finger's speed and meeting resistance past the threshold, a 48px `primary-container`\n * circle at elevation 0, and a crossfade to the indeterminate indicator while refreshing.\n *\n * @example\n * <PullToRefresh refreshing={loading} onRefresh={reload} className=\"h-dvh overflow-y-auto\">\n * <List aria-label=\"Messages\">…</List>\n * </PullToRefresh>\n */\nexport function PullToRefresh({\n refreshing: refreshingProp,\n onRefresh,\n threshold = POSITIONAL_THRESHOLD,\n disabled = false,\n labels: labelsProp,\n children,\n classNames,\n className,\n ref,\n ...rest\n}: PullToRefreshProps) {\n const labels = useLabels('PullToRefresh', DEFAULT_LABELS, labelsProp);\n const [pendingRefresh, setPendingRefresh] = useState(false);\n const refreshing = refreshingProp ?? pendingRefresh;\n const fraction = useMotionValue(refreshing ? 1 : 0);\n // Settling to the threshold or away never bounces: Compose animates it with\n // `Animatable.animateTo`'s default, non-bouncing spring, which the default effects spring\n // matches (2026-10-15; the fast spatial one dipped 3px under the threshold and back).\n const spring = useM3Spring('effects', 'default');\n // Under reduced motion the indicator jumps to the threshold or away; a pull still moves it.\n const settle = useReducedMotion() ? { duration: 0 } : spring;\n const [pulling, setPulling] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n // The indicator's code (its shapes and morphing) loads on demand: when the browser is first\n // idle, or when a pull starts, whichever comes first.\n usePreloadWhenIdle(preloadLoadingIndicator, !disabled);\n\n const rootRef = useObjectRef(ref);\n const animationRef = useRef<{ stop: () => void } | null>(null);\n const animatingRef = useRef(false);\n const mountedRef = useRef(true);\n // The latest props and helpers, for the native listeners added once.\n const latest = useRef({ refreshing, disabled, threshold, onRefresh, refreshingProp, settle });\n useLayoutEffect(() => {\n latest.current = { refreshing, disabled, threshold, onRefresh, refreshingProp, settle };\n });\n\n // The layer is the root's first child, so a consumer `gap-*` on a flex root would open a gap\n // below it and push the content down. It's cancelled after every render and whenever the\n // root resizes (a responsive class may change the gap).\n const layerRef = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n if (rootRef.current && layerRef.current) cancelRootGap(rootRef.current, layerRef.current);\n });\n useEffect(() => {\n const root = rootRef.current;\n if (!root || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n if (layerRef.current) cancelRootGap(root, layerRef.current);\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, [rootRef]);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n animationRef.current?.stop();\n };\n }, []);\n\n /** Springs the indicator to `target` (Compose's `animateToThreshold` / `animateToHidden`). */\n const settleRef = useRef((target: number) => {\n animationRef.current?.stop();\n animatingRef.current = true;\n const controls = animate(fraction, target, latest.current.settle);\n animationRef.current = controls;\n void controls.then(() => {\n if (animationRef.current === controls) animatingRef.current = false;\n });\n });\n\n // A refresh starting or ending moves the indicator to the threshold or away, and is\n // announced. The first render places it without either (Compose's `onAttach`).\n const previousRefreshing = useRef(refreshing);\n useEffect(() => {\n if (previousRefreshing.current === refreshing) return;\n previousRefreshing.current = refreshing;\n settleRef.current(refreshing ? 1 : 0);\n setAnnouncement(refreshing ? labels.refreshing : labels.refreshed);\n }, [refreshing, labels.refreshing, labels.refreshed]);\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n let gesture: Gesture | null = null;\n let suppressClick = false;\n\n const begin = (\n source: Gesture['source'],\n id: number,\n x: number,\n y: number,\n target: EventTarget | null,\n ) => {\n const { disabled: off } = latest.current;\n gesture =\n off || ownsVerticalDrag(target, root)\n ? null\n : {\n id,\n source,\n startX: x,\n startY: y,\n lastY: y,\n phase: 'tracking',\n atTop: isAtTop(target, root),\n pulled: 0,\n };\n // A pull may start: the indicator's code loads now (if the idle preload hasn't loaded it\n // yet), well before the gesture passes the slop and the indicator shows. A failed load is\n // tried again by the indicator itself.\n if (gesture?.atTop) void preloadLoadingIndicator().catch(() => undefined);\n };\n\n /** Moves the gesture to `y`; returns whether the area took the movement. */\n const move = (x: number, y: number, cancelable: boolean): boolean => {\n const current = gesture;\n if (!current || current.phase === 'native') return false;\n const dy = y - current.lastY;\n current.lastY = y;\n if (current.phase === 'tracking') {\n const totalX = x - current.startX;\n const totalY = y - current.startY;\n const distance = Math.max(Math.abs(totalX), Math.abs(totalY));\n // A mouse or pen must move past the slop first, so a click stays a click.\n if (current.source === 'pointer' && distance <= POINTER_SLOP) return false;\n const downward = totalY > 0 && totalY >= Math.abs(totalX) && current.atTop;\n if (!downward || !cancelable) {\n // Sideways, upward or mid-scroll: the browser's for the rest of the gesture. A\n // touch gets the same slop before it's decided, against a finger's jitter.\n if (!cancelable || distance > POINTER_SLOP) current.phase = 'native';\n return false;\n }\n const { refreshing: busy } = latest.current;\n if (busy || animatingRef.current) {\n current.phase = 'inert';\n return true;\n }\n current.phase = 'pulling';\n // A mouse or pen pull starts at the slop, so the indicator doesn't jump.\n current.pulled = Math.max(0, totalY - (current.source === 'pointer' ? POINTER_SLOP : 0));\n setPulling(true);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n return true;\n }\n if (current.phase === 'inert') return true;\n // Pulling: up and down both move the indicator (Compose's pre- and post-scroll).\n current.pulled = Math.max(0, current.pulled + dy);\n fraction.set(distanceFraction(current.pulled, latest.current.threshold));\n if (current.pulled === 0) {\n // Pushed all the way back: the gesture may scroll the content again.\n current.phase = 'tracking';\n current.startX = x;\n current.startY = y;\n current.atTop = true;\n setPulling(false);\n }\n return true;\n };\n\n /** Ends the gesture (Compose's `onRelease`); `cancel` never refreshes. */\n const end = (cancel: boolean) => {\n const current = gesture;\n gesture = null;\n if (!current || current.phase !== 'pulling') return;\n setPulling(false);\n if (current.source === 'pointer') {\n // A mouse pull ends with a click on whatever is under the pointer; it isn't one. The\n // click comes before any timer.\n suppressClick = true;\n setTimeout(() => {\n suppressClick = false;\n });\n }\n const { threshold: limit, refreshingProp: controlled } = latest.current;\n if (!cancel && passesThreshold(current.pulled, limit)) {\n const result = latest.current.onRefresh();\n if (controlled === undefined && isThenable(result)) {\n setPendingRefresh(true);\n const done = () => {\n if (mountedRef.current) setPendingRefresh(false);\n };\n result.then(done, done);\n }\n }\n // Compose hides the indicator here; a refresh that starts moves it to the threshold.\n settleRef.current(0);\n };\n\n const touchOf = (event: TouchEvent) =>\n gesture === null\n ? undefined\n : [...event.changedTouches].find((touch) => touch.identifier === gesture?.id);\n\n const onTouchStart = (event: TouchEvent) => {\n if (event.touches.length > 1) {\n // A second finger (pinch to zoom) ends the pull without refreshing.\n end(true);\n return;\n }\n const touch = event.changedTouches[0];\n if (touch) begin('touch', touch.identifier, touch.clientX, touch.clientY, event.target);\n };\n const onTouchMove = (event: TouchEvent) => {\n const touch = touchOf(event);\n if (!touch || event.defaultPrevented) return;\n if (move(touch.clientX, touch.clientY, event.cancelable) && event.cancelable) {\n event.preventDefault();\n }\n };\n const onTouchEnd = (event: TouchEvent) => {\n if (touchOf(event)) end(event.type === 'touchcancel');\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (event.pointerType === 'touch' || event.button !== 0 || !event.isPrimary) return;\n begin('pointer', event.pointerId, event.clientX, event.clientY, event.target);\n };\n const onPointerMove = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n const wasPulling = gesture.phase === 'pulling';\n if (!move(event.clientX, event.clientY, true)) return;\n event.preventDefault();\n if (!wasPulling && gesture?.phase === 'pulling') {\n root.setPointerCapture?.(event.pointerId);\n // A mouse drag would otherwise select the text it passes.\n window.getSelection()?.removeAllRanges();\n }\n };\n const onPointerEnd = (event: PointerEvent) => {\n if (gesture?.source !== 'pointer' || event.pointerId !== gesture.id) return;\n end(event.type === 'pointercancel');\n };\n const onClick = (event: MouseEvent) => {\n if (!suppressClick) return;\n suppressClick = false;\n event.preventDefault();\n event.stopPropagation();\n };\n\n const options = { passive: false } as const;\n root.addEventListener('touchstart', onTouchStart, { passive: true });\n root.addEventListener('touchmove', onTouchMove, options);\n root.addEventListener('touchend', onTouchEnd);\n root.addEventListener('touchcancel', onTouchEnd);\n root.addEventListener('pointerdown', onPointerDown);\n root.addEventListener('pointermove', onPointerMove);\n root.addEventListener('pointerup', onPointerEnd);\n root.addEventListener('pointercancel', onPointerEnd);\n root.addEventListener('click', onClick, { capture: true });\n return () => {\n root.removeEventListener('touchstart', onTouchStart);\n root.removeEventListener('touchmove', onTouchMove);\n root.removeEventListener('touchend', onTouchEnd);\n root.removeEventListener('touchcancel', onTouchEnd);\n root.removeEventListener('pointerdown', onPointerDown);\n root.removeEventListener('pointermove', onPointerMove);\n root.removeEventListener('pointerup', onPointerEnd);\n root.removeEventListener('pointercancel', onPointerEnd);\n root.removeEventListener('click', onClick, { capture: true });\n };\n }, [fraction, rootRef]);\n\n const styles = pullToRefreshStyles();\n\n return (\n <div\n {...rest}\n ref={rootRef}\n data-pulling={pulling || undefined}\n data-refreshing={refreshing || undefined}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n ref={layerRef}\n className={styles.layer({ class: classNames?.layer })}\n style={{ '--m3-pull-to-refresh-threshold': `${threshold}px` } as CSSProperties}\n >\n <div className={styles.clip()}>\n <PullIndicator\n fraction={fraction}\n refreshing={refreshing}\n threshold={threshold}\n labels={labels}\n className={styles.indicator({ class: classNames?.indicator })}\n faceClassName={styles.face()}\n />\n </div>\n <span role=\"status\" className=\"sr-only\">\n {announcement}\n </span>\n </div>\n {children}\n </div>\n );\n}\n\n/**\n * Keeps the root's own spacing from opening below the zero-height layer. Its margins are the\n * library's: zero, so a `space-y-*` root doesn't push the content down, and in a flex root a\n * negative margin as large as the gap along the main axis. In a grid root it moves to an\n * implicit row of its own before the first one, across every column, so the content keeps its\n * rows and columns; a grid row's size can't go below zero, so one row gap stays above the\n * content (documented: put the grid on an element inside the root).\n */\nfunction cancelRootGap(root: HTMLElement, layer: HTMLElement) {\n const style = getComputedStyle(root);\n const grid = style.display.endsWith('grid');\n const flex = style.display.endsWith('flex');\n const row = flex && style.flexDirection.startsWith('row');\n const reverse = flex && style.flexDirection.endsWith('reverse');\n const gap = Number.parseFloat(row ? style.columnGap : style.rowGap);\n for (const side of ['BlockStart', 'BlockEnd', 'InlineStart', 'InlineEnd'] as const) {\n layer.style[`margin${side}`] = '0px';\n }\n if (flex && gap > 0) {\n layer.style[`margin${row ? 'Inline' : 'Block'}${reverse ? 'Start' : 'End'}`] = `${-gap}px`;\n }\n layer.style.gridRow = grid ? 'span 1 / 1' : '';\n layer.style.gridColumn = grid ? '1 / -1' : '';\n}\n\n/**\n * The indicator, subscribed to the pull on its own so a pull re-renders only it, never the\n * content. Hidden at rest; while pulled it is a determinate loading indicator following the\n * pull, and while refreshing an indeterminate one, crossfading between them.\n */\nfunction PullIndicator({\n fraction,\n refreshing,\n threshold,\n labels,\n className,\n faceClassName,\n}: {\n fraction: MotionValue<number>;\n refreshing: boolean;\n threshold: number;\n labels: PullToRefreshLabels;\n className: string;\n faceClassName: string;\n}) {\n const [value, setValue] = useState(() => fraction.get());\n useMotionValueEvent(fraction, 'change', setValue);\n if (value <= 0 && !refreshing) return null;\n const rotation = overshootRotation(value);\n return (\n <div\n data-indicator=\"\"\n className={className}\n // Compose's `translationY = distanceFraction × maxDistance − height`.\n style={{ transform: `translateY(calc(${value * threshold}px - 100%))` }}\n >\n <div\n data-active={!refreshing}\n aria-hidden={refreshing || undefined}\n className={faceClassName}\n style={rotation === 0 ? undefined : { transform: `rotate(${rotation}deg)` }}\n >\n <LazyLoadingIndicator\n variant=\"contained\"\n value={Math.min(Math.max(value, 0), 1)}\n aria-label={labels.pull}\n />\n </div>\n <div\n data-active={refreshing}\n aria-hidden={!refreshing || undefined}\n className={faceClassName}\n >\n <LazyLoadingIndicator variant=\"contained\" aria-label={labels.refreshing} />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -82,7 +82,10 @@ interface SearchFieldOwnProps extends WrapperRootDOMProps {
|
|
|
82
82
|
* its role, name, value and type always win.
|
|
83
83
|
*/
|
|
84
84
|
inputProps?: SearchFieldInputProps;
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Words the field shows or announces, in place of the English defaults. A {@link LabelsProvider}
|
|
87
|
+
* sets them for the whole app.
|
|
88
|
+
*/
|
|
86
89
|
labels?: Partial<SearchFieldLabels>;
|
|
87
90
|
/** Ref to the root `<div>` element. */
|
|
88
91
|
ref?: Ref<HTMLDivElement>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { ButtonContext, standardIconButtons } from "../button/ButtonContext.js";
|
|
5
6
|
import { IconButton } from "../icon-button/IconButton.js";
|
|
6
7
|
import { pickWidgetAria, splitRootProps } from "../../utils/root-props.js";
|
|
@@ -33,10 +34,7 @@ const INTERACTIVE = "input, button, a, [role=\"button\"]";
|
|
|
33
34
|
*/
|
|
34
35
|
function SearchField(props) {
|
|
35
36
|
const { value, defaultValue, onChange, onSubmit, placeholder, leadingIcon, trailingIcon, clearIcon, density, disabled = false, autoFocus, inputRef, inputProps: consumerInputProps, labels: labelsProp, ref, className, style, classNames, ...rest } = props;
|
|
36
|
-
const labels =
|
|
37
|
-
...DEFAULT_LABELS,
|
|
38
|
-
...labelsProp
|
|
39
|
-
};
|
|
37
|
+
const labels = useLabels("SearchField", DEFAULT_LABELS, labelsProp);
|
|
40
38
|
const [query, setQuery] = useControlledState(value, defaultValue ?? "", onChange);
|
|
41
39
|
const objectInputRef = useObjectRef(inputRef);
|
|
42
40
|
const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchField.js","names":[],"sources":["../../../src/components/search/SearchField.tsx"],"sourcesContent":["'use client';\n\nimport {\n type CSSProperties,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { mergeProps, useObjectRef, useSearchField } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { IconButton } from '../icon-button/IconButton';\nimport type { SearchBarInputProps } from './SearchBar';\nimport { SearchFieldFrame } from './search-field-frame';\nimport { searchBarStyles, searchFieldStyles } from './search-styles';\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\nexport interface SearchFieldClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input. */\n field?: string;\n /** The text input. */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing slot, holding the clear button and `trailingIcon`. */\n trailing?: string;\n /** The clear button. */\n clear?: string;\n}\n\nexport interface SearchFieldLabels {\n /** The clear button's name, also its tooltip. @default \"Clear search\" */\n clear: string;\n}\n\nconst DEFAULT_LABELS: SearchFieldLabels = { clear: 'Clear search' };\n\n/**\n * Attributes for the input, e.g. `aria-keyshortcuts`, `enterKeyHint`, `autoComplete`,\n * `spellCheck`, `inputMode` or event handlers: those of {@link SearchBarInputProps}, less\n * `disabled` and `autoFocus`, which are the field's own props.\n */\nexport type SearchFieldInputProps = Omit<SearchBarInputProps, 'disabled' | 'autoFocus'>;\n\ntype Naming =\n | {\n /** The search box's accessible name. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search box. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\ninterface SearchFieldOwnProps extends WrapperRootDOMProps {\n /** The input's id. */\n id?: string;\n /** The ids of elements that describe the search, before any `inputProps['aria-describedby']`. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search. */\n 'aria-details'?: string;\n /** The id of what the search filters, e.g. a list of results. */\n 'aria-controls'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes, by typing, Escape or the clear button. */\n onChange?: (value: string) => void;\n /** Called with the query when the user presses Enter (the keyboard's search action). */\n onSubmit?: (value: string) => void;\n /** Hint text shown in the empty input, e.g. \"Search settings\". */\n placeholder?: string;\n /** Leading icon, usually a search icon, or a back button. */\n leadingIcon?: ReactNode;\n /**\n * Trailing content after the clear button: an icon button (e.g. voice search), or an avatar\n * (an `img`, drawn at 30px), as in the search bar.\n */\n trailingIcon?: ReactNode;\n /** Replaces the clear button's icon (Material Symbols `close`). */\n clearIcon?: ReactElement;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the pill (56px at 0, 44px at -3), as on the\n * other fields; icon buttons in it keep their 48px touch targets. Left out, the field takes the\n * density of the nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the\n * reader's choice rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** Whether the field is disabled: it takes no input and shows no clear button. */\n disabled?: boolean;\n /** Focus the input when the field mounts. */\n autoFocus?: boolean;\n /** The input element. */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for the input. They replace the field's defaults (`enterKeyHint=\"search\"`,\n * `autoComplete=\"off\"`, `spellCheck={false}`); event handlers run before the field's own, and\n * its role, name, value and type always win.\n */\n inputProps?: SearchFieldInputProps;\n /** Words the field shows or announces, in place of the English defaults. */\n labels?: Partial<SearchFieldLabels>;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: SearchFieldClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchFieldProps = SearchFieldOwnProps & Naming;\n\n/** Presses on these keep their own behaviour; a press anywhere else on the pill focuses the input. */\nconst INTERACTIVE = 'input, button, a, [role=\"button\"]';\n\n/**\n * A search box that filters content in place (a settings page, a table, a list pane), drawn as\n * the M3 search bar's 56px pill and filling its container. It never opens a search view: use\n * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a\n * clear button shows while there is text.\n *\n * @example\n * <SearchField aria-label=\"Search settings\" placeholder=\"Search settings\"\n * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />\n */\nexport function SearchField(props: SearchFieldProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n placeholder,\n leadingIcon,\n trailingIcon,\n clearIcon,\n density,\n disabled = false,\n autoFocus,\n inputRef,\n inputProps: consumerInputProps,\n labels: labelsProp,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const objectInputRef = useObjectRef(inputRef);\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n const {\n id,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n } = own as {\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n };\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n\n const { inputProps } = useSearchField(\n {\n id,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n 'aria-describedby': describedByInput,\n 'aria-details': details,\n placeholder,\n isDisabled: disabled,\n autoFocus,\n },\n { value: query, setValue: setQuery },\n objectInputRef,\n );\n\n // Enter submits, but not while an input method is composing (it confirms the composition).\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (!onSubmit || event.key !== 'Enter' || event.nativeEvent.isComposing) return;\n event.preventDefault();\n onSubmit(query);\n };\n\n const clear = () => {\n setQuery('');\n // The button goes with the text, so focus stays in the input.\n objectInputRef.current?.focus({ preventScroll: true });\n };\n\n // A press on the pill around the input (its padding, a decorative icon) puts the caret in it.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if (disabled || event.button !== 0) return;\n if ((event.target as Element).closest(INTERACTIVE)) return;\n event.preventDefault();\n objectInputRef.current?.focus();\n };\n\n const showClear = query !== '' && !disabled;\n const hasTrailing = showClear || trailingIcon != null;\n const styles = searchBarStyles({\n hasLeading: leadingIcon != null,\n hasTrailing,\n disabled,\n });\n const rootStyles = searchFieldStyles();\n\n return (\n <div\n {...data}\n ref={ref}\n style={style}\n {...densityAttribute(density)}\n data-disabled={disabled || undefined}\n className={rootStyles.root({ class: cn(classNames?.root, className) })}\n >\n {/* Icon buttons in a search bar are standard (M3), not the filled default. */}\n <ButtonContext value={standardIconButtons}>\n <SearchFieldFrame\n disabled={disabled}\n onPointerDown={focusInput}\n className={classNames?.field}\n leading={\n leadingIcon != null && (\n <span\n data-slot=\"leading\"\n className={styles.leading({ class: cn(rootStyles.slot(), classNames?.leading) })}\n >\n {leadingIcon}\n </span>\n )\n }\n trailing={\n hasTrailing && (\n <span\n data-slot=\"trailing\"\n className={styles.trailing({ class: cn(rootStyles.slot(), classNames?.trailing) })}\n >\n {showClear && (\n <IconButton\n aria-label={labels.clear}\n icon={clearIcon ?? <ClearIcon />}\n data-slot=\"clear\"\n // A pointer press keeps focus (and a phone's keyboard) in the input.\n preventFocusOnPress\n onPress={clear}\n className={classNames?.clear}\n />\n )}\n {trailingIcon}\n </span>\n )\n }\n >\n <input\n {...mergeProps(\n { enterKeyHint: 'search', autoComplete: 'off', spellCheck: false },\n consumerInputProps,\n inputProps,\n pickWidgetAria(own),\n { onKeyDown: submitOnEnter },\n )}\n ref={objectInputRef}\n data-slot=\"input\"\n className={styles.input({ class: classNames?.input })}\n />\n </SearchFieldFrame>\n </ButtonContext>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SearchField.js","names":[],"sources":["../../../src/components/search/SearchField.tsx"],"sourcesContent":["'use client';\n\nimport {\n type CSSProperties,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { mergeProps, useObjectRef, useSearchField } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { useLabels } from '../labels/use-labels';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { IconButton } from '../icon-button/IconButton';\nimport type { SearchBarInputProps } from './SearchBar';\nimport { SearchFieldFrame } from './search-field-frame';\nimport { searchBarStyles, searchFieldStyles } from './search-styles';\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\nexport interface SearchFieldClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input. */\n field?: string;\n /** The text input. */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing slot, holding the clear button and `trailingIcon`. */\n trailing?: string;\n /** The clear button. */\n clear?: string;\n}\n\nexport interface SearchFieldLabels {\n /** The clear button's name, also its tooltip. @default \"Clear search\" */\n clear: string;\n}\n\nconst DEFAULT_LABELS: SearchFieldLabels = { clear: 'Clear search' };\n\n/**\n * Attributes for the input, e.g. `aria-keyshortcuts`, `enterKeyHint`, `autoComplete`,\n * `spellCheck`, `inputMode` or event handlers: those of {@link SearchBarInputProps}, less\n * `disabled` and `autoFocus`, which are the field's own props.\n */\nexport type SearchFieldInputProps = Omit<SearchBarInputProps, 'disabled' | 'autoFocus'>;\n\ntype Naming =\n | {\n /** The search box's accessible name. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search box. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\ninterface SearchFieldOwnProps extends WrapperRootDOMProps {\n /** The input's id. */\n id?: string;\n /** The ids of elements that describe the search, before any `inputProps['aria-describedby']`. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search. */\n 'aria-details'?: string;\n /** The id of what the search filters, e.g. a list of results. */\n 'aria-controls'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes, by typing, Escape or the clear button. */\n onChange?: (value: string) => void;\n /** Called with the query when the user presses Enter (the keyboard's search action). */\n onSubmit?: (value: string) => void;\n /** Hint text shown in the empty input, e.g. \"Search settings\". */\n placeholder?: string;\n /** Leading icon, usually a search icon, or a back button. */\n leadingIcon?: ReactNode;\n /**\n * Trailing content after the clear button: an icon button (e.g. voice search), or an avatar\n * (an `img`, drawn at 30px), as in the search bar.\n */\n trailingIcon?: ReactNode;\n /** Replaces the clear button's icon (Material Symbols `close`). */\n clearIcon?: ReactElement;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the pill (56px at 0, 44px at -3), as on the\n * other fields; icon buttons in it keep their 48px touch targets. Left out, the field takes the\n * density of the nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the\n * reader's choice rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** Whether the field is disabled: it takes no input and shows no clear button. */\n disabled?: boolean;\n /** Focus the input when the field mounts. */\n autoFocus?: boolean;\n /** The input element. */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for the input. They replace the field's defaults (`enterKeyHint=\"search\"`,\n * `autoComplete=\"off\"`, `spellCheck={false}`); event handlers run before the field's own, and\n * its role, name, value and type always win.\n */\n inputProps?: SearchFieldInputProps;\n /**\n * Words the field shows or announces, in place of the English defaults. A {@link LabelsProvider}\n * sets them for the whole app.\n */\n labels?: Partial<SearchFieldLabels>;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: SearchFieldClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchFieldProps = SearchFieldOwnProps & Naming;\n\n/** Presses on these keep their own behaviour; a press anywhere else on the pill focuses the input. */\nconst INTERACTIVE = 'input, button, a, [role=\"button\"]';\n\n/**\n * A search box that filters content in place (a settings page, a table, a list pane), drawn as\n * the M3 search bar's 56px pill and filling its container. It never opens a search view: use\n * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a\n * clear button shows while there is text.\n *\n * @example\n * <SearchField aria-label=\"Search settings\" placeholder=\"Search settings\"\n * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />\n */\nexport function SearchField(props: SearchFieldProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n placeholder,\n leadingIcon,\n trailingIcon,\n clearIcon,\n density,\n disabled = false,\n autoFocus,\n inputRef,\n inputProps: consumerInputProps,\n labels: labelsProp,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const labels = useLabels('SearchField', DEFAULT_LABELS, labelsProp);\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const objectInputRef = useObjectRef(inputRef);\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n const {\n id,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n } = own as {\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n };\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n\n const { inputProps } = useSearchField(\n {\n id,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n 'aria-describedby': describedByInput,\n 'aria-details': details,\n placeholder,\n isDisabled: disabled,\n autoFocus,\n },\n { value: query, setValue: setQuery },\n objectInputRef,\n );\n\n // Enter submits, but not while an input method is composing (it confirms the composition).\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (!onSubmit || event.key !== 'Enter' || event.nativeEvent.isComposing) return;\n event.preventDefault();\n onSubmit(query);\n };\n\n const clear = () => {\n setQuery('');\n // The button goes with the text, so focus stays in the input.\n objectInputRef.current?.focus({ preventScroll: true });\n };\n\n // A press on the pill around the input (its padding, a decorative icon) puts the caret in it.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if (disabled || event.button !== 0) return;\n if ((event.target as Element).closest(INTERACTIVE)) return;\n event.preventDefault();\n objectInputRef.current?.focus();\n };\n\n const showClear = query !== '' && !disabled;\n const hasTrailing = showClear || trailingIcon != null;\n const styles = searchBarStyles({\n hasLeading: leadingIcon != null,\n hasTrailing,\n disabled,\n });\n const rootStyles = searchFieldStyles();\n\n return (\n <div\n {...data}\n ref={ref}\n style={style}\n {...densityAttribute(density)}\n data-disabled={disabled || undefined}\n className={rootStyles.root({ class: cn(classNames?.root, className) })}\n >\n {/* Icon buttons in a search bar are standard (M3), not the filled default. */}\n <ButtonContext value={standardIconButtons}>\n <SearchFieldFrame\n disabled={disabled}\n onPointerDown={focusInput}\n className={classNames?.field}\n leading={\n leadingIcon != null && (\n <span\n data-slot=\"leading\"\n className={styles.leading({ class: cn(rootStyles.slot(), classNames?.leading) })}\n >\n {leadingIcon}\n </span>\n )\n }\n trailing={\n hasTrailing && (\n <span\n data-slot=\"trailing\"\n className={styles.trailing({ class: cn(rootStyles.slot(), classNames?.trailing) })}\n >\n {showClear && (\n <IconButton\n aria-label={labels.clear}\n icon={clearIcon ?? <ClearIcon />}\n data-slot=\"clear\"\n // A pointer press keeps focus (and a phone's keyboard) in the input.\n preventFocusOnPress\n onPress={clear}\n className={classNames?.clear}\n />\n )}\n {trailingIcon}\n </span>\n )\n }\n >\n <input\n {...mergeProps(\n { enterKeyHint: 'search', autoComplete: 'off', spellCheck: false },\n consumerInputProps,\n inputProps,\n pickWidgetAria(own),\n { onKeyDown: submitOnEnter },\n )}\n ref={objectInputRef}\n data-slot=\"input\"\n className={styles.input({ class: classNames?.input })}\n />\n </SearchFieldFrame>\n </ButtonContext>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -156,7 +156,10 @@ type SelectProps$1<T extends object, M extends SelectionMode = 'single'> = Omit<
|
|
|
156
156
|
* leaving it out) on a field that loads options.
|
|
157
157
|
*/
|
|
158
158
|
loading?: boolean;
|
|
159
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Words the Select shows or announces, for other languages. A {@link LabelsProvider} sets them
|
|
161
|
+
* for the whole app.
|
|
162
|
+
*/
|
|
160
163
|
labels?: Partial<SelectLabels>;
|
|
161
164
|
/** Classes for the root element, merged with `classNames.root`; consumer classes win. */
|
|
162
165
|
className?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { usePreloadLoadingIndicator } from "../loading-indicator/lazy-loading-indicator.js";
|
|
5
6
|
import { FOCUSABLE_DISABLED_OPTIONS } from "../../utils/focusable-disabled.js";
|
|
6
7
|
import { assertCollectionChildren } from "../../utils/assert-collection-children.js";
|
|
@@ -67,10 +68,7 @@ function Select(props) {
|
|
|
67
68
|
const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });
|
|
68
69
|
const invalid = invalidProp ?? ariaFlag(ariaInvalid);
|
|
69
70
|
const required = requiredProp ?? ariaFlag(ariaRequired);
|
|
70
|
-
const labels =
|
|
71
|
-
...DEFAULT_LABELS,
|
|
72
|
-
...labelsProp
|
|
73
|
-
};
|
|
71
|
+
const labels = useLabels("Select", DEFAULT_LABELS, labelsProp);
|
|
74
72
|
const stately = rest;
|
|
75
73
|
const cap = useSelectionCap({
|
|
76
74
|
multiple: stately.selectionMode === "multiple",
|