@mortenbrudvik/waveui 0.14.0 → 0.15.0

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.
Files changed (125) hide show
  1. package/README.md +142 -32
  2. package/dist/components/data-display/InfoButton.cjs +1 -1
  3. package/dist/components/data-display/InfoButton.cjs.map +1 -1
  4. package/dist/components/data-display/InfoButton.mjs +90 -82
  5. package/dist/components/data-display/InfoButton.mjs.map +1 -1
  6. package/dist/components/data-display/InfoLabel.cjs +1 -1
  7. package/dist/components/data-display/InfoLabel.cjs.map +1 -1
  8. package/dist/components/data-display/InfoLabel.mjs +19 -18
  9. package/dist/components/data-display/InfoLabel.mjs.map +1 -1
  10. package/dist/components/feedback/MessageBarGroup.cjs +2 -0
  11. package/dist/components/feedback/MessageBarGroup.cjs.map +1 -0
  12. package/dist/components/feedback/MessageBarGroup.mjs +63 -0
  13. package/dist/components/feedback/MessageBarGroup.mjs.map +1 -0
  14. package/dist/components/feedback/ProgressBar.cjs +1 -1
  15. package/dist/components/feedback/ProgressBar.cjs.map +1 -1
  16. package/dist/components/feedback/ProgressBar.mjs +41 -39
  17. package/dist/components/feedback/ProgressBar.mjs.map +1 -1
  18. package/dist/components/feedback/Toaster.cjs +1 -1
  19. package/dist/components/feedback/Toaster.cjs.map +1 -1
  20. package/dist/components/feedback/Toaster.mjs +222 -216
  21. package/dist/components/feedback/Toaster.mjs.map +1 -1
  22. package/dist/components/layout/Accordion.cjs +1 -1
  23. package/dist/components/layout/Accordion.cjs.map +1 -1
  24. package/dist/components/layout/Accordion.mjs +124 -109
  25. package/dist/components/layout/Accordion.mjs.map +1 -1
  26. package/dist/components/layout/Carousel.cjs +1 -1
  27. package/dist/components/layout/Carousel.cjs.map +1 -1
  28. package/dist/components/layout/Carousel.mjs +207 -193
  29. package/dist/components/layout/Carousel.mjs.map +1 -1
  30. package/dist/components/layout/TabList.cjs +1 -1
  31. package/dist/components/layout/TabList.cjs.map +1 -1
  32. package/dist/components/layout/TabList.mjs +149 -108
  33. package/dist/components/layout/TabList.mjs.map +1 -1
  34. package/dist/components/layout/Tree.cjs.map +1 -1
  35. package/dist/components/layout/Tree.context.cjs +1 -1
  36. package/dist/components/layout/Tree.context.cjs.map +1 -1
  37. package/dist/components/layout/Tree.context.mjs +2 -1
  38. package/dist/components/layout/Tree.context.mjs.map +1 -1
  39. package/dist/components/layout/Tree.core.cjs +1 -1
  40. package/dist/components/layout/Tree.core.cjs.map +1 -1
  41. package/dist/components/layout/Tree.core.mjs +178 -165
  42. package/dist/components/layout/Tree.core.mjs.map +1 -1
  43. package/dist/components/layout/Tree.mjs.map +1 -1
  44. package/dist/components/navigation/Menu.popover.cjs +1 -1
  45. package/dist/components/navigation/Menu.popover.cjs.map +1 -1
  46. package/dist/components/navigation/Menu.popover.mjs +154 -168
  47. package/dist/components/navigation/Menu.popover.mjs.map +1 -1
  48. package/dist/components/navigation/Nav.cjs +1 -1
  49. package/dist/components/navigation/Nav.cjs.map +1 -1
  50. package/dist/components/navigation/Nav.mjs +156 -136
  51. package/dist/components/navigation/Nav.mjs.map +1 -1
  52. package/dist/components/navigation/NavDrawer.cjs +1 -1
  53. package/dist/components/navigation/NavDrawer.cjs.map +1 -1
  54. package/dist/components/navigation/NavDrawer.mjs +48 -45
  55. package/dist/components/navigation/NavDrawer.mjs.map +1 -1
  56. package/dist/components/overlays/Dialog.cjs +1 -1
  57. package/dist/components/overlays/Dialog.cjs.map +1 -1
  58. package/dist/components/overlays/Dialog.mjs +128 -122
  59. package/dist/components/overlays/Dialog.mjs.map +1 -1
  60. package/dist/components/overlays/Dialog.slots.cjs +1 -1
  61. package/dist/components/overlays/Dialog.slots.cjs.map +1 -1
  62. package/dist/components/overlays/Dialog.slots.mjs +12 -12
  63. package/dist/components/overlays/Dialog.slots.mjs.map +1 -1
  64. package/dist/components/overlays/Drawer.cjs.map +1 -1
  65. package/dist/components/overlays/Drawer.mjs.map +1 -1
  66. package/dist/components/overlays/Drawer.panel.cjs +1 -1
  67. package/dist/components/overlays/Drawer.panel.cjs.map +1 -1
  68. package/dist/components/overlays/Drawer.panel.mjs +97 -81
  69. package/dist/components/overlays/Drawer.panel.mjs.map +1 -1
  70. package/dist/components/overlays/Drawer.root.cjs +1 -1
  71. package/dist/components/overlays/Drawer.root.cjs.map +1 -1
  72. package/dist/components/overlays/Drawer.root.mjs +34 -31
  73. package/dist/components/overlays/Drawer.root.mjs.map +1 -1
  74. package/dist/components/overlays/Popover.cjs +1 -1
  75. package/dist/components/overlays/Popover.cjs.map +1 -1
  76. package/dist/components/overlays/Popover.mjs +231 -216
  77. package/dist/components/overlays/Popover.mjs.map +1 -1
  78. package/dist/components/overlays/TeachingPopover.cjs.map +1 -1
  79. package/dist/components/overlays/TeachingPopover.mjs.map +1 -1
  80. package/dist/components/overlays/TeachingPopover.parts.cjs +1 -1
  81. package/dist/components/overlays/TeachingPopover.parts.cjs.map +1 -1
  82. package/dist/components/overlays/TeachingPopover.parts.mjs +202 -173
  83. package/dist/components/overlays/TeachingPopover.parts.mjs.map +1 -1
  84. package/dist/components/overlays/TeachingPopover.root.cjs +1 -1
  85. package/dist/components/overlays/TeachingPopover.root.cjs.map +1 -1
  86. package/dist/components/overlays/TeachingPopover.root.mjs +108 -99
  87. package/dist/components/overlays/TeachingPopover.root.mjs.map +1 -1
  88. package/dist/components/overlays/Tooltip.cjs +1 -1
  89. package/dist/components/overlays/Tooltip.cjs.map +1 -1
  90. package/dist/components/overlays/Tooltip.mjs +66 -61
  91. package/dist/components/overlays/Tooltip.mjs.map +1 -1
  92. package/dist/components/provider/WaveProvider.cjs +1 -1
  93. package/dist/components/provider/WaveProvider.cjs.map +1 -1
  94. package/dist/components/provider/WaveProvider.mjs +26 -23
  95. package/dist/components/provider/WaveProvider.mjs.map +1 -1
  96. package/dist/hooks/useCollapsePresence.cjs +2 -0
  97. package/dist/hooks/useCollapsePresence.cjs.map +1 -0
  98. package/dist/hooks/useCollapsePresence.mjs +21 -0
  99. package/dist/hooks/useCollapsePresence.mjs.map +1 -0
  100. package/dist/hooks/useFocusBeforeClose.cjs +2 -0
  101. package/dist/hooks/useFocusBeforeClose.cjs.map +1 -0
  102. package/dist/hooks/useFocusBeforeClose.mjs +33 -0
  103. package/dist/hooks/useFocusBeforeClose.mjs.map +1 -0
  104. package/dist/hooks/useMotion.cjs +2 -0
  105. package/dist/hooks/useMotion.cjs.map +1 -0
  106. package/dist/hooks/useMotion.mjs +14 -0
  107. package/dist/hooks/useMotion.mjs.map +1 -0
  108. package/dist/hooks/usePresenceGroup.cjs +2 -0
  109. package/dist/hooks/usePresenceGroup.cjs.map +1 -0
  110. package/dist/hooks/usePresenceGroup.mjs +68 -0
  111. package/dist/hooks/usePresenceGroup.mjs.map +1 -0
  112. package/dist/index.cjs +1 -1
  113. package/dist/index.d.cts +721 -131
  114. package/dist/index.d.ts +721 -131
  115. package/dist/index.mjs +136 -135
  116. package/dist/lib/classNames.cjs +1 -1
  117. package/dist/lib/classNames.cjs.map +1 -1
  118. package/dist/lib/classNames.mjs +101 -90
  119. package/dist/lib/classNames.mjs.map +1 -1
  120. package/dist/lib/motion.cjs +2 -0
  121. package/dist/lib/motion.cjs.map +1 -0
  122. package/dist/lib/motion.mjs +43 -0
  123. package/dist/lib/motion.mjs.map +1 -0
  124. package/dist/styles.css +1 -1
  125. package/package.json +2 -1
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @mortenbrudvik/waveui
2
2
 
3
- A React component library inspired by the Fluent UI 2 design language: 78 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
3
+ A React component library inspired by the Fluent UI 2 design language: 79 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
4
4
 
5
5
  > This is **not** Microsoft's official `@fluentui/react-components` package.
6
6
 
7
7
  - [Requirements](#requirements) · [Installation](#installation) · [Quick start](#quick-start)
8
8
  - [Styles](#styles) · [Theming](#theming) · [Global effects](#global-effects) · [Styling hooks](#styling-hooks)
9
9
  - [React Server Components](#react-server-components) · [Components](#components) · [Usage notes](#usage-notes)
10
- - [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.13](#upgrading-from-013) · [Upgrading from 0.12](#upgrading-from-012) · [Upgrading from 0.11](#upgrading-from-011) · [Upgrading from 0.10](#upgrading-from-010) · [Upgrading from 0.9](#upgrading-from-09) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
10
+ - [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.14](#upgrading-from-014) · [Upgrading from 0.13](#upgrading-from-013) · [Upgrading from 0.12](#upgrading-from-012) · [Upgrading from 0.11](#upgrading-from-011) · [Upgrading from 0.10](#upgrading-from-010) · [Upgrading from 0.9](#upgrading-from-09) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
11
11
 
12
12
  ## Requirements
13
13
 
@@ -372,7 +372,7 @@ export function checkedRows(tree: HTMLElement) {
372
372
 
373
373
  - Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster`, the date helpers (`addDays`, `addMonths`, `isSameDay`, `startOfWeek`, `getWeekNumber`), `getInitials`, `partitionAvatarGroupItems`, the breadcrumb helpers (`partitionBreadcrumbItems`, `truncateBreadcrumbLongName`, `truncateBreadcrumbLongTooltip`, `isTruncatableBreadcrumbContent`), `createTableColumn` and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle, compute dates and render Wave components with serializable props. A DataGrid column with functions (`compare`, `renderCell`) is not serializable: define such columns in a client component.
374
374
  - A Server Component **cannot dot into a client component**: `Card.Header` throws "Cannot access Header on the server". Every sub-component is therefore also exported under a flat name. Use the flat names in Server Components and either form in client components.
375
- - Every compound can be composed in a Server Component. Parts written there reach the client as lazy references, and the compounds recognise them, so the server HTML and the client behaviour are the same as in a client file: Accordion items, Carousel slides, the options of Combobox, Dropdown and Listbox, the parts and options of TagPicker, DataGrid parts (`DataGrid.SelectionCell` included), the Table parts, Overflow items and dividers, Breadcrumb items (collapsed into the overflow menu too), the TeachingPopover parts, Drawer and Menu triggers, the Drawer header, body and footer parts, the tags of a TagGroup and the halves of an InteractionTag, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, TabList and Tree items, Nav items, categories, section headers, dividers and app items, the header, body and footer of a NavDrawer, the stars of a Rating or RatingDisplay, the parts of Toast and MessageBar, the ColorPicker parts, `SwatchPicker.Row` and the swatches (`ColorSwatch`, `ImageSwatch`, `EmptySwatch`, in rows of the grid layout or the implicit ones), `Tree.ItemPersonaLayout`, a `Label` or `InfoLabel` passed as the `label` of a Field, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
375
+ - Every compound can be composed in a Server Component. Parts written there reach the client as lazy references, and the compounds recognise them, so the server HTML and the client behaviour are the same as in a client file: Accordion items, Carousel slides, the options of Combobox, Dropdown and Listbox, the parts and options of TagPicker, DataGrid parts (`DataGrid.SelectionCell` included), the Table parts, Overflow items and dividers, Breadcrumb items (collapsed into the overflow menu too), the TeachingPopover parts, Drawer and Menu triggers, the Drawer header, body and footer parts, the tags of a TagGroup and the halves of an InteractionTag, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, TabList and Tree items, Nav items, categories, section headers, dividers and app items, the header, body and footer of a NavDrawer, the stars of a Rating or RatingDisplay, the parts of Toast and MessageBar, the bars of a MessageBarGroup, the ColorPicker parts, `SwatchPicker.Row` and the swatches (`ColorSwatch`, `ImageSwatch`, `EmptySwatch`, in rows of the grid layout or the implicit ones), `Tree.ItemPersonaLayout`, a `Label` or `InfoLabel` passed as the `label` of a Field, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
376
376
  - Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component.
377
377
  - A `Popover`, `Dialog`, overlay `Drawer` or `NavDrawer`, popup `Menu`, `Combobox`, `Dropdown`, `DatePicker`, `TimePicker` or `TagPicker` rendered on the server with `defaultOpen` (or `open`) is closed in the server HTML and opens once hydrated, so no ARIA reference points at a popup that is not there. `onOpenChange` is not called for it.
378
378
  - An inline `Drawer` or `NavDrawer` (`type="inline"`) renders in the server HTML: open, or `hidden` when it is closed and kept mounted (`unmountOnClose={false}`).
@@ -482,7 +482,7 @@ export function SaveArea() {
482
482
 
483
483
  ## Components
484
484
 
485
- 78 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
485
+ 79 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
486
486
 
487
487
  Your `className` is merged last with `cn()`, so it replaces a conflicting class **of the same variant**: `bg-error` replaces a resting `bg-primary`. A class behind a variant is replaced only by a class with the same variant. That matters for the gated hover and pressed classes (`not-disabled:not-aria-disabled:hover:…` / `…:active:…`) and the state classes (`data-[selected]:…`, `aria-disabled:…`): they stay next to yours and win while their state applies, because they are more specific. A bare `hover:bg-error` therefore does not override a Button's built-in hover color while the Button is enabled. It still paints on hover while the Button is disabled or `aria-disabled`, and on `appearance="transparent"`, which has no hover background. To override the gated and state classes, use the same prefix (`not-disabled:not-aria-disabled:hover:bg-error`, `data-[selected]:bg-error`) or the important modifier (`hover:bg-error!`); see [Buttons](#buttons).
488
488
 
@@ -542,8 +542,8 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
542
542
  | `Tag` | Chip: `appearance` (`filled`, `outline`, `brand`), `size` (`medium` 28px, `small`, `extra-small`), `shape` (`circular`, `rounded`), `disabled`, a look-only `selected`, `value` (its key in a TagGroup) and the `media` (an Avatar), `icon` and `secondaryText` slots; an optional Dismiss button (`dismissible`, `onDismiss`, `dismissLabel`) that also dismisses on Delete and Backspace. `onDismiss` and `dismissIcon` need `dismissible` (a development warning says so). In a TagGroup focus moves for you when a tag is removed; on its own, move it yourself (see [Tags](#tags)). |
543
543
  | `TagGroup` | A set of tags: a toolbar (one Tab stop, arrow keys) or, with `role="list"`, a list; default `size`, `appearance`, `shape`, `dismissible` and `disabled` for its tags; `onDismiss(value)`, focus to the next tag (else the previous one, else `fallbackFocusRef`) when the focused tag is removed; selectable InteractionTags through `value`/`defaultValue`/`onValueChange`, with `name`, `form` and `required`. |
544
544
  | `InteractionTag` | A tag with an action: `InteractionTag.Primary` (a button, or a toggle in a selecting TagGroup; `media`, `icon`, `secondaryText`) and an optional `InteractionTag.Secondary` (a separate Dismiss button named "Dismiss <text>"); `size`, `appearance`, `shape`, `disabled`, `selected`, `value`. |
545
- | `InfoLabel` | A real `<label>` (`children`, `htmlFor`, `required`, `size`, `weight`, `disabled`) with an info button after it that shows `info` (text, links or other content) in a note; `openOnHover`, `infoButtonLabel`. Usable as the `label` of a Field. |
546
- | `InfoButton` | A button with the info glyph that shows `info` in a `role="note"` next to it, without moving focus: opens on click, on mouse hover and on keyboard focus (`openOnHover={false}`: click only); Tab from the button enters the note; `size`. Named "Information" (`aria-label`). |
545
+ | `InfoLabel` | A real `<label>` (`children`, `htmlFor`, `required`, `size`, `weight`, `disabled`) with an info button after it that shows `info` (text, links or other content) in a note; `openOnHover`, `infoButtonLabel`, `motion` (its note's). Usable as the `label` of a Field. |
546
+ | `InfoButton` | A button with the info glyph that shows `info` in a `role="note"` next to it, without moving focus: opens on click, on mouse hover and on keyboard focus (`openOnHover={false}`: click only); Tab from the button enters the note; `size`; the note fades and slides in (`motion`). Named "Information" (`aria-label`). |
547
547
  | `Persona` | Avatar with up to four lines of text (`primaryText` to `quaternaryText`), `textPosition` (`after`, `before`, `below`), `presenceOnly` and presence; sizes up to `huge`. |
548
548
  | `Divider` | Horizontal or vertical separator, optionally labelled. |
549
549
  | `Image` | `<img>` with `fit`, `shape`, `shadow`, `bordered`; warns in development when `alt` is missing. Never wider than its parent; with the default `fit` its height follows its width. `fit="none"` keeps the top-left corner in view, `"center"` the middle. |
@@ -560,10 +560,10 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
560
560
  | Component | Description |
561
561
  | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
562
562
  | `Card` | Surface with `Card.Header` (`image`, `action`), `Card.Preview`, `Card.Body`, `Card.Footer`; `appearance`, `size`, `orientation`; optionally selectable (`onSelect`, `selected`, `selectionControl`, `checkbox`) with a `floatingAction` (see [Cards, tabs and carousels](#cards-tabs-and-carousels)). |
563
- | `Accordion` | Disclosure sections (`Accordion.Item`, `.Trigger`, `.Panel`); single (default) or `type="multiple"`; `headingLevel`; item values are unique (development warning). |
564
- | `TabList` | Tabs (`TabList.Tab`, `.Panel`, `.Panels`) with automatic activation, or manual with `selectTabOnFocus={false}`; `orientation`, `appearance` (`transparent`, `subtle`, `subtle-circular`, `filled-circular`), `size`, a `Tab` `icon` (alone: icon-only); tab values are unique (development warning). |
565
- | `Tree` | Hierarchical tree (`Tree.Item`) with expand/collapse and typeahead; single selection, or checkboxes with a cascading mixed state (`selectionMode="multiple"`); row `actions`, `aside` and `Tree.ItemPersonaLayout`; `navigationMode="treegrid"` (see [Trees](#trees)). Nested items are `Tree.Item` elements in their parent's children (directly, in Fragments or from a render function), not rendered by a recursive component; values are unique. |
566
- | `Carousel` | Slides (`Carousel.Item` children, directly or in Fragments; a component that renders `Carousel.Item` is not a slide) with previous/next, a slide picker and optional auto-rotation with a pause control. `groupSize` (a number or `auto`) shows several at once, `draggable` drags them, `Carousel.Item` `thumbnail` shows thumbnails, `controlsLayout` places the buttons and `Carousel.Button` replaces one. |
563
+ | `Accordion` | Disclosure sections (`Accordion.Item`, `.Trigger`, `.Panel`); single (default) or `type="multiple"`; `headingLevel`; panels collapse as they open and close (`Accordion.Panel motion`); item values are unique (development warning). |
564
+ | `TabList` | Tabs (`TabList.Tab`, `.Panel`, `.Panels`) with automatic activation, or manual with `selectTabOnFocus={false}`; `orientation`, `appearance` (`transparent`, `subtle`, `subtle-circular`, `filled-circular`), `size`, a `Tab` `icon` (alone: icon-only); the selection line slides to the new tab (`motion`); tab values are unique (development warning). |
565
+ | `Tree` | Hierarchical tree (`Tree.Item`) with expand/collapse and typeahead; single selection, or checkboxes with a cascading mixed state (`selectionMode="multiple"`); row `actions`, `aside` and `Tree.ItemPersonaLayout`; `navigationMode="treegrid"`; groups collapse as they open and close (`motion`) (see [Trees](#trees)). Nested items are `Tree.Item` elements in their parent's children (directly, in Fragments or from a render function), not rendered by a recursive component; values are unique. |
566
+ | `Carousel` | Slides (`Carousel.Item` children, directly or in Fragments; a component that renders `Carousel.Item` is not a slide) with previous/next, a slide picker and optional auto-rotation with a pause control. `groupSize` (a number or `auto`) shows several at once, `draggable` drags them, `Carousel.Item` `thumbnail` shows thumbnails, `controlsLayout` places the buttons, `Carousel.Button` replaces one, and `motion` (`slide`, `fade` or `null`) with `motionDuration` sets how the page changes. |
567
567
  | `Overflow` | Hides items that do not fit in one row (or column, `orientation="vertical"`) and renders an overflow button (`useOverflowMenu` lists the hidden items); items hide by `priority` unless `pinned`, from the end or the start (`overflowDirection`), in groups with `Overflow.Divider`; `buttonPosition`, `minimumVisible`, `asChild` items, `onOverflowChange`; item ids are unique (development warning). The row has a 4px padding, so its items' focus rings are not clipped (see [Overflow](#overflow)). |
568
568
  | `Grid` | CSS grid with a column count and token gaps. |
569
569
  | `Stack` | Vertical or horizontal stack (`orientation`, `gap`). |
@@ -574,19 +574,20 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
574
574
  | Component | Description |
575
575
  | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
576
576
  | `MessageBar` | Inline status message (`info`, `success`, `warning`, `error`) with hidden status text, `MessageBar.Title`, `MessageBar.Actions` that reflow with `layout`, and an optional dismiss button (see [Message bars](#message-bars)). |
577
- | `ProgressBar` | Determinate or indeterminate progress; name it with `label` (inside a `Field`, the Field's label names it); `color`. |
577
+ | `MessageBarGroup` | A stack of MessageBars that animates the bars it gains and loses: a removed bar fades out while its row closes, so the bars below glide up; `animate` (`exit-only`, `both`), `motion`. A component of its own, so an import of only MessageBar loads no motion (see [Message bars](#message-bars)). |
578
+ | `ProgressBar` | Determinate or indeterminate progress; name it with `label` (inside a `Field`, the Field's label names it); `color`; `indeterminateMotion` replaces the indeterminate sweep. |
578
579
  | `Skeleton` | Loading placeholder (`width`, `height`, `shape`); `Skeleton.Group` marks the loading region busy. |
579
580
  | `Spinner` | Loading indicator announced as "Loading" (localize with `label`); `appearance="inverted"` for brand surfaces; `delay`. |
580
- | `Toast` | Notifications shown by `<Toaster>` (`limit` queues the rest): a `<Toast>` with `Toast.Title`, `.Body`, `.Footer` and `.Close`, or text, dispatched through `useToastController()` or a `createToaster()` handle; a keyboard shortcut, Escape and Delete (see [Toasts](#toasts)). |
581
+ | `Toast` | Notifications shown by `<Toaster>` (`limit` queues the rest; `motion` replaces their slide, fade and collapse): a `<Toast>` with `Toast.Title`, `.Body`, `.Footer` and `.Close`, or text, dispatched through `useToastController()` or a `createToaster()` handle; a keyboard shortcut, Escape and Delete (see [Toasts](#toasts)). |
581
582
 
582
583
  ### Navigation
583
584
 
584
585
  | Component | Description |
585
586
  | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
586
587
  | `Breadcrumb` | Trail of links, buttons or text (`Breadcrumb.Item`, `current`); `maxDisplayedItems` or `overflow="auto"` collapse the middle items into a "…" menu, `size`, `focusMode` (`tab`, `arrow`), `divider` (see [Breadcrumbs](#breadcrumbs)). |
587
- | `Menu` | Popup menu (`Menu.Trigger`, `Menu.Popover`, which scrolls when it is taller than the viewport) or static menu; `Menu.Item`, `Menu.Divider`; checkbox, radio and switch items bound to `checkedValues`, `Menu.Group`, `Menu.ItemLink`, submenus, `Menu.SplitGroup`, hover opening and context menus (see [Menus](#menus)). |
588
- | `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked. `Nav.SectionHeader` (a heading, `headingLevel`), `Nav.Divider` (decorative), `Nav.AppItem` and `Nav.AppItemStatic` (the app's name at the top), and `actions` on `Nav.Item` and `Nav.SubItem` (your buttons beside the link, shown on hover and focus). Every child of its list is a list item (a development warning says so). |
589
- | `NavDrawer` | Nav in a drawer panel (see [Navigation drawers and app shells](#navigation-drawers-and-app-shells)): `type` `overlay` (a modal or non-modal dialog around a `<nav>`) or `inline` (part of the page layout), `position` (default `start`), `size` (default `small`, 320px), `separator`, `modalType`, `unmountOnClose`, `finalFocusRef`, `backdrop`, Nav's value and category props and `onItemSelect(value)` on every activation; `NavDrawer.Header`, `NavDrawer.Body` (`focusMode` `tab` or `arrow`) and `NavDrawer.Footer`. |
588
+ | `Menu` | Popup menu (`Menu.Trigger`, `Menu.Popover`, which scrolls when it is taller than the viewport) or static menu; `Menu.Item`, `Menu.Divider`; checkbox, radio and switch items bound to `checkedValues`, `Menu.Group`, `Menu.ItemLink`, submenus, `Menu.SplitGroup`, hover opening and context menus; menus fade and slide in (`Menu.Popover motion`) (see [Menus](#menus)). |
589
+ | `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked; categories collapse as they open and close (`Nav.Category motion`, and `expandIcon` with `expandIconMotion` for its chevron). `Nav.SectionHeader` (a heading, `headingLevel`), `Nav.Divider` (decorative), `Nav.AppItem` and `Nav.AppItemStatic` (the app's name at the top), and `actions` on `Nav.Item` and `Nav.SubItem` (your buttons beside the link, shown on hover and focus). Every child of its list is a list item (a development warning says so). |
590
+ | `NavDrawer` | Nav in a drawer panel (see [Navigation drawers and app shells](#navigation-drawers-and-app-shells)): `type` `overlay` (a modal or non-modal dialog around a `<nav>`) or `inline` (part of the page layout), `position` (default `start`), `size` (default `small`, 320px), `separator`, `modalType`, `unmountOnClose`, `finalFocusRef`, `backdrop`, `motion`, `backdropMotion`, Nav's value and category props and `onItemSelect(value)` on every activation; `NavDrawer.Header`, `NavDrawer.Body` (`focusMode` `tab` or `arrow`) and `NavDrawer.Footer`. |
590
591
  | `Hamburger` | A `subtle` icon-only Button with the navigation glyph, named "Navigation"; inside a NavDrawer it toggles the drawer (`aria-expanded`, `aria-controls`). |
591
592
  | `Stepper` | Multi-step progress (`Stepper.Step`), horizontal or vertical, optionally `linear`. |
592
593
  | `Pagination` | Page buttons with ellipses, previous/next and optional first/last. |
@@ -595,11 +596,11 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
595
596
 
596
597
  | Component | Description |
597
598
  | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
598
- | `Dialog` | Modal dialog (`modalType="alert"` for a confirmation, `"non-modal"` for one the page stays usable around): `Dialog.Trigger` (toggles), `Dialog.Content` (`size` `small`, `medium`, `large`, `full`; `closeLabel` names its Close button, `closeButton` replaces or hides it, `backdrop`, `titleAction`, `unmountOnClose`), `Dialog.Title` (`action`), `Dialog.Footer` (stays in view), `Dialog.Close` (see [Dialogs and triggers](#dialogs-and-triggers)). |
599
- | `Drawer` | Side or bottom panel (see [Drawers](#drawers)): `type` `overlay` (`modalType` `modal`, `alert` or `non-modal`) or `inline`; `position` (`start`, `end`, `left`, `right`, `bottom`), `size` (`small` 320px, `medium`, `large`, `full`), `separator`, `backdrop`, `closeButton`, `closeLabel`, `unmountOnClose`; `Drawer.Trigger` (toggles), `Drawer.Title`, `Drawer.Close`, and the layout parts `Drawer.Header`, `Drawer.HeaderTitle`, `Drawer.HeaderNavigation`, `Drawer.Body` and `Drawer.Footer`, whose separators show while content is scrolled under them. |
600
- | `Popover` | Non-modal popup anchored to `Popover.Trigger` or a `target`; `side`, `align`; hover cards (`openOnHover`) and context popovers (`openOnContext`). |
601
- | `Tooltip` | Hover and focus text for its child, as a description or (`relationship="label"`) a name; `open`/`defaultOpen`/`onOpenChange`; `openDelay`, `closeDelay`. |
602
- | `TeachingPopover` | Step-by-step onboarding popover, optionally pointing at a `target`; `primaryAction` and `secondaryAction` replace its buttons, `footerLayout`, `onFinish`; composable from parts: `TeachingPopover.Trigger`, `.Surface`, `.Header`, `.Title`, `.Body`, `.Footer` and the carousel parts `.Carousel`, `.CarouselNav`, `.CarouselPageCount`, `.CarouselFooter` (see [Teaching popovers and tours](#teaching-popovers-and-tours)). |
599
+ | `Dialog` | Modal dialog (`modalType="alert"` for a confirmation, `"non-modal"` for one the page stays usable around): `Dialog.Trigger` (toggles), `Dialog.Content` (`size` `small`, `medium`, `large`, `full`; `closeLabel` names its Close button, `closeButton` replaces or hides it, `backdrop`, `titleAction`, `unmountOnClose`, `motion`, `backdropMotion`), `Dialog.Title` (`action`), `Dialog.Footer` (stays in view), `Dialog.Close` (see [Dialogs and triggers](#dialogs-and-triggers)). |
600
+ | `Drawer` | Side or bottom panel (see [Drawers](#drawers)): `type` `overlay` (`modalType` `modal`, `alert` or `non-modal`) or `inline`; `position` (`start`, `end`, `left`, `right`, `bottom`), `size` (`small` 320px, `medium`, `large`, `full`), `separator`, `backdrop`, `closeButton`, `closeLabel`, `unmountOnClose`, `motion` (an overlay's slide, an inline drawer's collapse), `backdropMotion`; `Drawer.Trigger` (toggles), `Drawer.Title`, `Drawer.Close`, and the layout parts `Drawer.Header`, `Drawer.HeaderTitle`, `Drawer.HeaderNavigation`, `Drawer.Body` and `Drawer.Footer`, whose separators show while content is scrolled under them. |
601
+ | `Popover` | Non-modal popup anchored to `Popover.Trigger` or a `target`; `side`, `align`; hover cards (`openOnHover`) and context popovers (`openOnContext`); the content fades and slides in (`Popover.Content motion`). |
602
+ | `Tooltip` | Hover and focus text for its child, as a description or (`relationship="label"`) a name; `open`/`defaultOpen`/`onOpenChange`; `openDelay`, `closeDelay`; a fade (`motion`). |
603
+ | `TeachingPopover` | Step-by-step onboarding popover, optionally pointing at a `target`; `primaryAction` and `secondaryAction` replace its buttons, `footerLayout`, `onFinish`, `motion`; composable from parts: `TeachingPopover.Trigger`, `.Surface`, `.Header`, `.Title`, `.Body`, `.Footer` and the carousel parts `.Carousel`, `.CarouselNav`, `.CarouselPageCount`, `.CarouselFooter` (see [Teaching popovers and tours](#teaching-popovers-and-tours)). |
603
604
 
604
605
  ### Tables
605
606
 
@@ -612,7 +613,7 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
612
613
 
613
614
  | Component | Description |
614
615
  | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
615
- | `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction, portal container and `inputDefaults` (the default `size` and `appearance` of the text inputs and pickers). |
616
+ | `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction, portal container and `inputDefaults` (the default `size` and `appearance` of the text inputs and pickers) and `motion` (`'none'` turns the default motion of every animated component below it off). |
616
617
 
617
618
  `Portal` renders its children into `document.body` (or the provider's `portalContainer`) inside a themed `wave-portal` wrapper; use it for your own overlays.
618
619
 
@@ -620,7 +621,7 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
620
621
 
621
622
  | Component | Description |
622
623
  | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
623
- | `Presence` | Shows and hides its child with CSS enter and exit motion: `data-presence` phases, `inert` while it exits, unmounted when the motion ends; `usePresence` is the hook form (see [Enter and exit motion](#enter-and-exit-motion)). |
624
+ | `Presence` | Shows and hides its child with CSS enter and exit motion: `data-presence` phases, `inert` while it exits, unmounted when the motion ends; `usePresence` is the hook form. The components' own motion is built on it (see [Enter and exit motion](#enter-and-exit-motion)). |
624
625
 
625
626
  ## Usage notes
626
627
 
@@ -1419,7 +1420,7 @@ export function ViewMenu() {
1419
1420
 
1420
1421
  - **A menu at another element.** `Menu.Popover` `target` (`PopupTarget`) places the menu at an element (hold it in state: `const [button, setButton] = useState<HTMLElement | null>(null)`) or at a `VirtualElement`, a rectangle such as a point (`target={{ getBoundingClientRect: () => rect }}`; an inline object is fine). A controlled menu with a `target` needs no `Menu.Trigger`: name it with `aria-label`, and give a toggle button used as the target `aria-haspopup="menu"` and `aria-expanded` yourself. A press on a target element does not count as outside, so the toggle closes the menu with one click. Without `Menu.Trigger`, Escape (or a close by your app) returns focus to the element that had it when the menu opened, while activating an item or pressing Tab focuses the target element when it can take focus; when you open the menu from the toggle, both are the toggle. The menu's `id` is generated, so the toggle cannot point `aria-controls` at it.
1421
1422
  - **Columns.** The item labels line up: once any item of a menu list shows a check or an icon, every item of that list keeps that column (through groups and wrappers too), and the others render an empty placeholder there. A `shortcut` takes its direction from its own text (`dir="auto"`), so "Ctrl+," reads the same in an RTL menu.
1422
- - **Exit motion.** A `Menu.Popover` stays mounted while an exit motion of yours runs, with `data-presence="exiting"`, `data-state="closed"` and `inert`; focus is already back and Escape already reaches the next layer. Style it with the presence attributes (Tailwind path): `<Menu.Popover className="transition-opacity duration-wave-fast data-[presence=exiting]:opacity-0 motion-reduce:transition-none">`. Without such classes it unmounts at once. See [Enter and exit motion](#enter-and-exit-motion).
1423
+ - **Motion.** A menu, and each submenu, fades in and slides 4px away from its trigger (a submenu away from its parent menu, on the side it opened on) over 200ms, and fades and slides back over 100ms. While it exits it stays mounted with `data-presence="exiting"`, `data-state="closed"` and `inert`; focus is already back and Escape already reaches the next layer. `Menu.Popover motion` replaces the motion, or turns it off with `null`: `<Menu.Popover motion="transition-opacity duration-wave-fast data-[presence=exiting]:opacity-0 motion-reduce:transition-none">` only fades out (Tailwind path). 0.14's form, the same classes in `className`, still works and turns the default off. See [Enter and exit motion](#enter-and-exit-motion).
1423
1424
 
1424
1425
  ### Toolbars
1425
1426
 
@@ -1559,7 +1560,8 @@ export function DeleteDialog({ onDelete }: { onDelete: () => void }) {
1559
1560
  - **Backdrop.** The backdrop is the surface's sibling, `aria-hidden` (`.wave-dialog-content__backdrop`, `.wave-drawer__backdrop`), and a slot on `Dialog.Content` and Drawer: content renders inside it, a slot object's `className`, `style` and handlers go to it, and its `appearance` (`'dimmed'` or `'transparent'`, Fluent's name) picks the fill. A modal opened while another modal Dialog or Drawer is open below it gets a transparent backdrop, so the page dims once; `backdrop={{ appearance: 'dimmed' }}` dims it again. `null` renders no backdrop (the page stays inert, and a press outside still closes a modal); a boolean is no slot. A non-modal surface or an inline drawer ignores `backdrop`, with a development warning.
1560
1561
  - **Close button.** `closeButton` sets the content of the built-in Close button (decorative: the button keeps `closeLabel` as its name, unless a slot object's `aria-label`, `aria-labelledby` or `title` names it); `null` hides it (Escape and `.Close` parts still close). A `<button>` or `Button` passed there is merged into the built-in button, never nested, and its text label names it. Fluent's dialogs have no Close button by default: pass `closeButton={null}` and keep yours, or drop yours.
1561
1562
  - **Title actions.** `Dialog.Content`'s `titleAction` (for the `title` prop) and `Dialog.Title`'s `action` place header buttons before the Close button, in the top end corner, outside the heading, so they never join the dialog's name; the title's end padding follows their width. Drawer's `titleAction` and `Drawer.HeaderTitle`'s `action` do the same in a drawer header. Name icon-only buttons.
1562
- - **Kept mounted.** `unmountOnClose={false}` keeps `Dialog.Content` mounted while closed, `hidden` and `inert`, so a form keeps what was typed; a popup opened inside that is still open when the dialog closes stays open over the page (a development warning says so). Drawers take the same prop. See [Enter and exit motion](#enter-and-exit-motion) for the `data-presence` phases both now carry, and [Drawers](#drawers) for side and bottom panels.
1563
+ - **Kept mounted.** `unmountOnClose={false}` keeps `Dialog.Content` mounted while closed, `hidden` and `inert`, so a form keeps what was typed; a popup opened inside that is still open when the dialog closes stays open over the page (a development warning says so). Drawers take the same prop. See [Drawers](#drawers) for side and bottom panels.
1564
+ - **Motion.** The surface scales up from 85% and fades in over 250ms, and scales down and fades out over 200ms, while the backdrop fades over the same times. A closing dialog unmounts, or hides with `unmountOnClose={false}`, once the surface's motion ends; focus, the trap and the isolation follow the open state at once, and a stacked dialog's backdrop stays transparent while it fades. `Dialog.Content` `motion` and `backdropMotion` replace them, or turn them off with `null`; a backdrop motion longer than the surface's exit is cut off when the surface goes. `backdropMotion` counts as given on a non-modal dialog even when it is `null`, so leave it unset there (a development warning names it). See [Enter and exit motion](#enter-and-exit-motion).
1563
1565
 
1564
1566
  ```tsx
1565
1567
  import * as React from 'react';
@@ -1635,7 +1637,7 @@ export function FiltersDrawer() {
1635
1637
  - **Non-modal.** `modalType="non-modal"` leaves the page usable (see [Dialogs and triggers](#dialogs-and-triggers)); reserve the panel's width (its height at the bottom) in your layout while it is open, so it hides no page control. `modalType="alert"` renders `role="alertdialog"`.
1636
1638
  - **Inline.** `type="inline"` renders the panel where the Drawer is, after its direct triggers, as an `<aside>` (a complementary landmark named by its title), also in the server HTML. It is page content: no portal, backdrop, focus trap, inert page or scroll lock, Escape and presses outside never close it, and toasts keep their corner. Its trigger toggles it; opening it moves no focus, and closing it returns focus only when focus was inside. `position` picks its inner edge and `size` its width; `separator` draws the inner edge. Place it in your layout, for example a flex row of the panel and the content. Changing `type` (an overlay below a breakpoint, inline above it) remounts the panel and its content.
1637
1639
  - **Kept mounted.** `unmountOnClose={false}` keeps the panel and its content mounted while closed, `hidden` and `inert`, so a form keeps what was typed; an `autoFocus` inside never applies (each open of an overlay focuses its first focusable element), and a kept inline panel is in the server HTML, hidden. A `required` empty field in a closed kept drawer inside a page `<form>` still blocks its submission.
1638
- - **Motion.** The panel and its backdrop mount through the presence core and render `data-presence` (see [Enter and exit motion](#enter-and-exit-motion)): `data-[presence=…]:` classes in `className` animate the panel, and a closing drawer waits for that motion before it unmounts or hides. No motion ships.
1640
+ - **Motion.** An overlay panel slides in from its edge by its own size and fades, while its backdrop fades, over 250ms at `small`, 300ms at `medium`, 400ms at `large` and 500ms at `full`, both ways; `start` and `end` mirror in right-to-left layouts, `left` and `right` do not. An inline drawer opens and closes the space it takes in the layout over the same durations while its panel fades: the `<aside>` sits in an element of its own (`wave-drawer__collapse`), which renders no box while the drawer is open and is a clipping grid while it moves, so the panel keeps its size against its inner edge and the page beside it moves with the space. A `full` inline drawer has no motion, and one whose width (height at the bottom) in `className` is a percentage resizes oddly during the motion, correctly once open (give it a length, or pass `motion={null}`). `motion` and `backdropMotion` replace them, or turn them off with `null`. A closing drawer waits for its motion before it unmounts or hides. A `className` with `data-[presence=…]` classes while `motion` is unset keeps 0.14's behaviour: no default motion on the panel, and your classes animate it (an inline one inside the clipping element). See [Enter and exit motion](#enter-and-exit-motion).
1639
1641
 
1640
1642
  ### Popovers and tooltips
1641
1643
 
@@ -1663,6 +1665,7 @@ export function Author() {
1663
1665
  - **Context popovers.** `openOnContext` works as on Menu, with Popover's focus rules: a right click opens the popover at the pointer and leaves focus where it is (Tab from that element enters the content); Shift+F10 or the ContextMenu key opens it at the focused element and moves focus into the content, and Escape returns focus to that element. The region gets no `aria-haspopup` or `aria-expanded`, so name the content with `title`, `aria-label` or `aria-labelledby`.
1664
1666
  - **`target`** on the Popover root (next to `side` and `align`; `PopupTarget`, as on `Menu.Popover`) places the content at an element held in state or at a `VirtualElement` instead of the trigger. A controlled popover with a `target` needs no trigger; a toggle button used as the target carries its own `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls` (give `Popover.Content` an `id`), a press on it does not count as outside, and Tab from it enters the content. `Popover`, `Menu.Popover` and TeachingPopover take the same `target`: an element held in state, a `VirtualElement` or a ref object (a ref is read after each render of the popup, and the popup stays hidden while it holds no element).
1665
1667
  - **Tooltip timing.** `openDelay` (default 200ms) is how long the pointer rests on the child, or the child has keyboard focus, before the tooltip appears: unlike Menu's and Popover's `openDelay`, it applies to focus too. `closeDelay` (default 100ms) is how long it stays once the pointer has left the child and the tooltip; blur and Escape hide it at once. `delay` is the deprecated name of `openDelay` (it warns once in development).
1668
+ - **Motion.** `Popover.Content` (and InfoButton's note) fades in and slides 4px away from the trigger, on the side it opened on, over 200ms, and back over 100ms; the Tooltip surface fades over 100ms both ways, without a slide (Fluent's Tooltip has no motion). While they exit they stay mounted, `inert`, with `data-presence` and `data-state="closed"`; focus leaves a closing popover in the same commit, and a tooltip pointed at again during its exit shows again on the same element. `motion` on `Popover.Content`, InfoButton, InfoLabel and Tooltip (its portaled surface; `className` stays on its wrapper) replaces it, or turns it off with `null`. A tooltip whose `content` you change as it closes (a "Copied" text reset in `onOpenChange`) shows the new text while it fades: reset it once the exit is over, or pass `motion={null}`.
1666
1669
 
1667
1670
  ### Teaching popovers and tours
1668
1671
 
@@ -1703,6 +1706,7 @@ export function Tour({ onSeen }: { onSeen: () => void }) {
1703
1706
  - **Parts.** `TeachingPopover.Trigger` opens and closes the popover (`aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`). While `target` is unset the popover points at it and follows it in the Tab order. Focus returns to the Trigger when the popover closes, also when `target` is set. Written directly inside TeachingPopover (a Fragment is fine), it makes the popover start closed. `TeachingPopover.Surface` is the popover and takes the `className`, `style`, `ref` and other element props that the `steps` form takes on TeachingPopover. Inside it, `Header` (the Close button, an optional caption and `icon`; `dismissButton` replaces the Close glyph or merges a Button into the Close button, and `null` removes it), `Title` (`as`; it names the popover), `Body` (`media` above the text) and `Footer` each show their share of the current step unless you give them children; a Surface without children shows all four, which is what the `steps` form renders. The steps, labels, actions and callbacks stay on TeachingPopover.
1704
1707
  - **Carousel parts.** `TeachingPopover.Carousel` holds the current page (the title, the body and a `CarouselFooter`). `CarouselFooter` lays out an indicator and the buttons: `layout="offset"` (the default, the indicator at the start, as in the Footer) or `"centered"` (the indicator between the buttons; Fluent's default). `finalStepText` relabels Done, and `initialStepText` turns the first step's unavailable Back into an enabled button that dismisses the popover (`onOpenChange(false)`, then `onDismiss`; Fluent's calls `onFinish`). It is the same element as Back, so focus stays on it when Back leads to the first step, where a second press dismisses. The indicator is the decorative step dots unless you put `CarouselNav` there (a group of step buttons named "Step 2 of 3", each a Tab stop, the current one with `aria-current="step"`; `labels.nav` and `labels.navButton` translate them) or `CarouselPageCount` (a visible "2 of 3", worded by its render function child, `(current, total) => …`).
1705
1708
  - **Placement.** Write the Trigger and the Surface directly inside TeachingPopover. A Trigger nested deeper does not make the popover start closed, and an open popover renders in place until that Trigger has mounted (in the server HTML too), then moves next to it, with focus passing through the Trigger. A Surface inside a component of yours works once mounted, but the server HTML holds the popover's own surface as well until then; a second Surface renders a second popover (development warning `TeachingPopover.Surface:placement`).
1709
+ - **Motion.** The surface fades in over 200ms and out over 100ms, and, pointed at a `target` or the Trigger, slides 4px away from it and back; rendered in place, it only fades. It stays visible, with the step it showed, while it exits, and focus is back on the Trigger at once. `motion` on the TeachingPopover (for the surface it renders) or on `TeachingPopover.Surface` replaces it, or turns it off with `null`.
1706
1710
 
1707
1711
  ### Custom overlays
1708
1712
 
@@ -1782,7 +1786,67 @@ function Details() {
1782
1786
 
1783
1787
  ### Enter and exit motion
1784
1788
 
1785
- Motion tokens are CSS variables on `:root`, the same in every theme: `--wave-duration-ultra-fast` (50ms), `-faster` (100ms), `-fast` (150ms), `-normal` (200ms), `-gentle` (250ms), `-slow` (300ms), `-slower` (400ms), `-ultra-slow` (500ms), and the curves `--wave-curve-accelerate-max`, `-accelerate-mid`, `-accelerate-min`, `-decelerate-max`, `-decelerate-mid`, `-decelerate-min`, `-easy-ease-max`, `-easy-ease` and `-linear`, with the values of Fluent's motion tokens (the guide's "Motion & Animation" chapter lists them). On the Tailwind path they are the utilities `duration-wave-*` and `ease-wave-*`; the precompiled `./styles` contains only the utilities Wave itself uses, so there use the variables in your own CSS.
1789
+ Dialog, Drawer, NavDrawer, Menu, Popover, InfoButton, Tooltip, TeachingPopover, Accordion, Tree, Nav, the toasts and MessageBarGroup animate in and out by default, with Fluent's motion tokens, and TabList, Carousel and ProgressBar take motion options. The listbox popups of Combobox, Dropdown, TagPicker and TimePicker, the DatePicker calendar and the AvatarGroup popover (and a popup of your own on `ListboxSurface`) do not animate yet ([ROADMAP §8.5](docs/ROADMAP.md#85-follow-ups-outside-the-gap-list)). The guide's "Component Motion" section lists the curves and every class.
1790
+
1791
+ | Part | Motion | In | Out |
1792
+ |---|---|---|---|
1793
+ | `Dialog.Content` | scales up from 85% and fades; its backdrop fades | 250ms | 200ms |
1794
+ | Drawer and NavDrawer, overlay | slides in from its edge by its own size and fades; its backdrop fades | 250, 300, 400 or 500ms by `size` | the same |
1795
+ | Drawer and NavDrawer, inline | the space it takes opens and closes while the panel fades (none at `full`) | by `size`, as above | the same |
1796
+ | `Popover.Content`, `Menu.Popover` and its submenus, InfoButton's note, the TeachingPopover surface | fades and slides 4px away from its anchor, on the side it opened on | 200ms | 100ms |
1797
+ | Tooltip | fades | 100ms | 100ms |
1798
+ | `Accordion.Panel`, a Tree group, a `Nav.Category`'s list | the space it takes opens and closes while it fades | 200ms | 200ms |
1799
+ | A toast (Toaster) | its place opens while it fades in and slides 8px from the stack's edge | 250ms | fades over 200ms, then its place closes over 200ms |
1800
+ | A bar of a MessageBarGroup | its row closes, or (`animate="both"`) opens, while it fades | 250ms | 250ms |
1801
+ | Nav item actions | fade as their row is hovered or takes focus | 150ms | 150ms |
1802
+ | `Nav.Category`'s chevron | its half turn | 150ms | 150ms |
1803
+ | TabList, `transparent` | the selection line slides and stretches to the new tab | 300ms | — |
1804
+ | Carousel | the track slides, or (`motion="fade"`) the slides cross-fade | `motionDuration`, 300ms | — |
1805
+
1806
+ - **Exits.** A closing surface stays mounted for its exit, `inert`, with `data-presence="exiting"` (a popup also with `data-state="closed"`), so it takes no focus, click or key while it fades and is out of the accessibility tree. Everything else follows the open state: focus is back on the trigger, and the next Escape reaches the layer below, as soon as it closes, and its dismiss layer, focus trap, `inert` isolation, scroll lock and positioning are released then, not when its motion ends. Focus inside a collapsing panel moves to its trigger. Because `inert` hit-tests like `pointer-events: none`, a second click during an exit reaches what is under the surface (the page behind a fading Dialog); the page's scroll bar returns as a modal starts to fade; and an exiting popup keeps its last position while the page scrolls.
1807
+ - **Turning it off, or replacing it.** Each animated part takes a motion slot (`MotionSlot`): `motion` on the part that moves, `backdropMotion` on the backdrops of Dialog, Drawer and NavDrawer, `expandIconMotion` on `Nav.Category` and `indeterminateMotion` on ProgressBar (Carousel's `motion` takes `'slide'`, `'fade'` or `null` instead). Unset is the default; `null` turns it off (a part with presence phases still renders them, and they end at once; Nav's expand icon, ProgressBar's fill and TabList's line just do not move); a class string or `{ className }` replaces the default classes with yours:
1808
+
1809
+ ```tsx
1810
+ import { Button, Dialog, Popover } from '@mortenbrudvik/waveui';
1811
+
1812
+ export function Examples() {
1813
+ return (
1814
+ <>
1815
+ <Dialog>
1816
+ <Dialog.Trigger>
1817
+ <Button>Rename</Button>
1818
+ </Dialog.Trigger>
1819
+ <Dialog.Content title="Rename" motion={null} backdropMotion={null}>
1820
+ The dialog appears and disappears at once.
1821
+ </Dialog.Content>
1822
+ </Dialog>
1823
+ <Popover>
1824
+ <Popover.Trigger>
1825
+ <Button>Details</Button>
1826
+ </Popover.Trigger>
1827
+ <Popover.Content
1828
+ title="Details"
1829
+ motion="transition-opacity duration-wave-slow ease-wave-easy-ease motion-reduce:transition-none data-[presence=entering]:starting:opacity-0 data-[presence=exiting]:opacity-0"
1830
+ >
1831
+ A slower fade, without the slide.
1832
+ </Popover.Content>
1833
+ </Popover>
1834
+ </>
1835
+ );
1836
+ }
1837
+ ```
1838
+
1839
+ Write a replacement as the defaults are written: the enter's start with `starting:` gated on the entering phase (`data-[presence=entering]:starting:…`), the exit with `data-[presence=exiting]:…`, and a `motion-reduce:` counterpart. A positioned surface (Popover, Menu, Tooltip, InfoButton's note, a TeachingPopover with a target) is placed with an inline `transform`: move it with `translate` or `scale`, and never transition `transform` (`transition`, `transition-all`, `transition-transform`), or it glides from its last position. The surface's motion decides when it unmounts, so a backdrop motion longer than the surface's exit is cut off.
1840
+ - **Replacements and the precompiled CSS.** The default motion works on both style paths. A replacement's classes must exist in your CSS: your Tailwind build generates them, but the precompiled `./styles` holds only Wave's own classes, so there pass a class of your own CSS written against `[data-presence]` (as in the CSS example below).
1841
+ - **Turning it all off.** `<WaveProvider motion="none">` makes every motion slot below it act as `null` (TabList's line included) and stops Carousel's slide or fade (a Carousel without its own `motion`) and the Nav item actions' fade; a part's own value still wins, and transitions that are no slot keep running (the turn of Accordion's and Tree's chevrons, colour changes), and a nested provider inherits `motion` like `theme` (`motion="default"` turns the defaults back on below it). It is Fluent's `MotionBehaviourProvider` with `'skip'`. End-to-end tests that expect a closed surface to be gone at once can render under it. Classes of yours on a `Presence` child are no motion slot and keep running.
1842
+ - **Tuning.** The durations and curves are the `--wave-duration-*` and `--wave-curve-*` variables: redefine one in your CSS to retime every motion that uses it (`:root { --wave-duration-gentle: 200ms; }`), or replace one part's classes. There is no parameter form (Fluent's `surfaceMotion={{ duration }}`).
1843
+ - **Reduced motion.** Under `prefers-reduced-motion: reduce` every animated surface and part appears and disappears at once: the presence core ends each phase at once, and every default class has a `motion-reduce:` counterpart. The indeterminate ProgressBar pulses at full width instead of sweeping, and a Carousel with `autoPlay` starts stopped.
1844
+ - **0.14's classes in `className`.** A `className` with `data-[presence=…]` classes on `Menu.Popover`, `Dialog.Content`, Drawer or NavDrawer, while `motion` is unset, turns that part's default motion off, so the classes animate it as before (its backdrop still fades by default). Prefer `motion`.
1845
+ - **Collapsing parts.** Accordion's region, a Tree group, a Nav category's list and an inline drawer's panel sit inside an element of their own, which carries the phases and the motion (`wave-accordion-panel__collapse`, `wave-tree-item__collapse`, `wave-nav-category__collapse`, `wave-drawer__collapse`). It renders no box while the part is open (`display: contents`), so the part lays out as without it, and is a clipping grid of one track while the part opens or closes, so the space changes while the part keeps its size.
1846
+ - **Mounting.** A surface that is open when it mounts (`defaultOpen`) animates in, after hydration and on a client render alike. In-flow content (inline drawers, Accordion panels, Tree groups, Nav categories, the bars a MessageBarGroup mounts with) never animates on mount, on the server or the client.
1847
+ - **Browser support.** `starting:` needs `@starting-style` (Chrome and Edge 117+, Safari 17.5+, Firefox 129+); older browsers show an element at once, without its enter motion, and the exit still runs.
1848
+
1849
+ **Your own motion.** Motion tokens are CSS variables on `:root`, the same in every theme: `--wave-duration-ultra-fast` (50ms), `-faster` (100ms), `-fast` (150ms), `-normal` (200ms), `-gentle` (250ms), `-slow` (300ms), `-slower` (400ms), `-ultra-slow` (500ms), and the curves `--wave-curve-accelerate-max`, `-accelerate-mid`, `-accelerate-min`, `-decelerate-max`, `-decelerate-mid`, `-decelerate-min`, `-easy-ease-max`, `-easy-ease` and `-linear`, with the values of Fluent's motion tokens (the guide's "Motion & Animation" chapter lists them). On the Tailwind path they are the utilities `duration-wave-*` and `ease-wave-*`; the precompiled `./styles` contains only the utilities Wave itself uses, so there use the variables in your own CSS.
1786
1850
 
1787
1851
  `Presence` (and its hook, `usePresence`) mounts and unmounts an element with CSS enter and exit motion. The element carries `data-presence`: `entering`, `entered`, `exiting` or `exited`. Style its enter with the `starting:` variant (CSS `@starting-style`) gated on the entering phase, `data-[presence=entering]:starting:`, and its exit with `data-[presence=exiting]:` classes. It stays mounted, and `inert`, until its own transitions and animations have finished, and it unmounts at once when it has none.
1788
1852
 
@@ -1831,8 +1895,6 @@ export function SavedNotice() {
1831
1895
  - **Phases.** On the server and while hydrating the element renders its `entered` phase (a hidden one its `exited` phase), so no enter motion replays after hydration; `appear` runs it for an element that mounts visible on the client. Showing the element again while it exits returns it to `entering`, on the same element. `unmountOnExit={false}` keeps an exited element mounted, `hidden` and `inert`, so its state (a typed value, a scroll position) survives. `onEntered` and `onExited` report the end of a phase.
1832
1896
  - **Gate the enter on the phase.** `@starting-style` applies to every first style of an element, whatever its phase. An ungated `starting:` class, or a `@starting-style` rule without `[data-presence='entering']`, therefore also fades in an element that mounts `entered`: without `appear`, and server-rendered content after hydration, while the core already reports `entered` and has called `onEntered`. The gated form runs only when the core runs the enter phase: a show after mount, `appear`, and a kept-mounted element shown again.
1833
1897
  - **The hook.** `const { isMounted, ref, presenceProps } = usePresence(open)` for your own components: render the element while `isMounted`, pass it `ref` and spread `presenceProps` (`data-presence`, and `inert`/`hidden` when they apply). Destructure the result: `eslint-plugin-react-hooks` treats the whole object as a ref once its `ref` is passed on, and reports reads of its other members during render. Keep focus, dismissal and positioning on your open state, not on `isMounted`, so they happen on close and not after the exit motion.
1834
- - **Browser support.** `starting:` needs `@starting-style` (Chrome and Edge 117+, Safari 17.5+, Firefox 129+); older browsers show the element at once, without its enter motion, and the exit still runs.
1835
- - `Menu.Popover`, the toasts in a `Toaster`, and since 0.11 `Dialog.Content` and Drawer (the panel and its backdrop, overlay or inline, NavDrawer's too) mount through the presence core, so they take exit classes too (see [Menus](#menus) and [Toasts](#toasts)). An enter class for them takes the same gate; a surface that is open when it mounts (`defaultOpen`) mounts `entered`, so it does not animate that first time. A closing Dialog or Drawer waits for its motion before it unmounts, or hides with `unmountOnClose={false}` (`exited`, `hidden` and `inert`); focus, the trap and the isolation follow the open state at once. Every top-level element of their portal carries the presence attributes. No component ships enter or exit classes yet; Popover and Tooltip get their motion in a later release.
1836
1898
 
1837
1899
  ### Toasts
1838
1900
 
@@ -1892,7 +1954,7 @@ export function App({ undoSend }: { undoSend: () => void }) {
1892
1954
  - Toasts stay reachable by Tab over an open Dialog and move out of an open overlay Drawer's way, modal or not: beside a side drawer at their corner, above a bottom one (below it for top toasts; `--wave-toaster-offset-block`), and they keep their corner when there is no room. Next to an inline drawer, which is page content, they keep their corner. `statusLabel` and `dismissLabel` (props of `<Toast>`; `statusLabel` is also an option of both `dispatchToast` forms, `dismissLabel` of the text form) translate the hidden status text and the Dismiss button's name.
1893
1955
  - **`limit`** caps how many toasts show at once (`<Toaster limit={3}>`). Further toasts wait in a queue, in dispatch order, and appear as shown toasts go; a waiting toast is not announced and its timer does not run until it shows. A dismissed toast stops counting at once, also while an exit motion of yours runs. Lowering the limit never hides a shown toast. A value below 1 counts as 1 (with a development warning), and a fraction rounds down. Dispatching the id of a waiting toast again replaces it and keeps its place; `dismissToast(id)` removes a waiting toast silently.
1894
1956
  - **`dismissAllToasts()`** removes every toast, shown and waiting, and cancels their timers. When focus was in a toast, it returns to the element that had it before.
1895
- - **Motion.** Each toast mounts through the presence core inside a container with the class `wave-toaster__toast`: style its `data-presence` phases for an enter and exit motion of yours (see [Enter and exit motion](#enter-and-exit-motion)). A dismissed toast stays mounted and `inert` until the exit ends; focus has already moved on. Wave ships no toast motion.
1957
+ - **Motion.** A toast opens its place in the stack, fades in and slides 8px in from the stack's edge (from below at the bottom positions, from above at the top) over 250ms. Dismissed, it fades out over 200ms, then its place closes over 200ms more, so the toasts around it glide together and nothing jumps when it unmounts. It stays mounted and `inert` until the exit ends; focus has already moved on, and it no longer counts toward `limit`. The motion is on each toast's container (`wave-toaster__toast`, a one-cell grid around `wave-toaster__toast-content`); `<Toaster motion={…}>` replaces it, or turns it off with `null`. The timer starts when a toast is shown, as before. See [Enter and exit motion](#enter-and-exit-motion).
1896
1958
 
1897
1959
  **Toasts from code outside components.** A query cache, an event handler or a helper module cannot call a hook. Create a handle with `createToaster()` at module scope and bind a Toaster to it:
1898
1960
 
@@ -1946,6 +2008,35 @@ export function UpdateNotice({ onUpdate, onLater }: { onUpdate: () => void; onLa
1946
2008
  - **Layouts.** `layout="singleline"` keeps the actions on the text's row, `"multiline"` puts them on a row of their own under the text, and `"auto"` (the default) keeps them on the text's row while the text fits on one line and moves them below when it does not (measured after mount with a ResizeObserver; the server renders one row). No layout cuts text off: on one row it wraps inside its column. The DOM order is the same in every layout (icon, text, actions, Dismiss), so a reflow never moves the focused button, and the Tab order is the text's links, the actions, the Dismiss button. A bar without actions looks as in 0.7. The root carries `data-layout` with the layout in use (`singleline` or `multiline`).
1947
2009
  - **Names.** With its default role (`alert` for warnings and errors, `status` otherwise) the bar is a live region, and its title is read as the start of its content, once: "Warning: Update available A new version is ready to install.", then the actions' labels. A notice that stays on the page can take `role="region"`: then the title names the bar ("Warning: Update available"), which makes it a landmark; such a bar needs a title or an `aria-label` (a development warning says so). A consumer `aria-label` or `aria-labelledby` always wins.
1948
2010
 
2011
+ **Groups.** `MessageBarGroup` stacks bars with an 8px gap and animates the ones it gains and loses:
2012
+
2013
+ ```tsx
2014
+ import { MessageBar, MessageBarGroup, type MessageBarProps } from '@mortenbrudvik/waveui';
2015
+
2016
+ interface Notice {
2017
+ id: string;
2018
+ status: MessageBarProps['status'];
2019
+ text: string;
2020
+ }
2021
+
2022
+ export function Notices({ notices, onDismiss }: { notices: Notice[]; onDismiss: (id: string) => void }) {
2023
+ return (
2024
+ <MessageBarGroup animate="both">
2025
+ {notices.map((notice) => (
2026
+ <MessageBar key={notice.id} status={notice.status} onDismiss={() => onDismiss(notice.id)}>
2027
+ {notice.text}
2028
+ </MessageBar>
2029
+ ))}
2030
+ </MessageBarGroup>
2031
+ );
2032
+ }
2033
+ ```
2034
+
2035
+ - **Motion.** A bar removed from the children stays in its place, `inert`, while it fades out and its row closes over 250ms, so the bars below glide up instead of jumping; a negative bottom margin makes up for the gap, so nothing moves when it unmounts. `animate="both"` also opens the row of a bar added after mount and fades it in (`'exit-only'`, the default, adds it at once). Bars present at the group's first render, on the server too, never animate. The root renders `data-animate`; each bar sits in an element of its own (`wave-message-bar-group__item`, no box while the bar is shown), and `motion` replaces the motion of both directions, or turns it off with `null`. The margin matches the group's own 8px gap (`gap-2`): with another gap in `className` (`gap-4`), pass a `motion` whose margin matches it (`data-[presence=exiting]:-mb-4`, and `data-[presence=entering]:starting:-mb-4` with `animate="both"`).
2036
+ - **Keys.** Give every bar a stable `key` (`key={notice.id}`): the group follows its children by key, so it keeps a removed bar in its place, moves a reordered one without remounting it, and shows again a bar that comes back during its exit. A child without a key of its own, or text, renders after the bars without motion, with a development warning (`MessageBarGroup:children`).
2037
+ - **Focus.** When a bar leaves with focus inside it (its Dismiss button was pressed), focus moves in the same commit to the first tabbable element of the nearest remaining bar after it that has one, else of the nearest before it; when no bar is left, move it yourself.
2038
+ - **Markup and bundles.** The group is a plain `<div>` without a role (`wave-message-bar-group`), and each MessageBar stays its own live region. It is a component of its own (Fluent's `MessageBarGroup`), not `MessageBar.Group`, so a page that imports only MessageBar loads none of its motion.
2039
+
1949
2040
  ### Badges
1950
2041
 
1951
2042
  - `color` takes `brand` (the default), `danger`, `important`, `informative`, `severe`, `subtle`, `success` and `warning`, with `appearance` `filled` (the default), `tint` or `outline`. `severe` is dark orange; `subtle` is the page background with foreground text, for a badge on a colored surface. CounterBadge takes the same palette (`<CounterBadge count={3} color="danger" />`), and `dot` makes it a small dot without a number. A dot keeps 3:1 against the page: the `informative` and `warning` dots are darker than those counts, and `subtle` is for dots on colored surfaces.
@@ -2248,6 +2339,7 @@ export function FileTree() {
2248
2339
  - **Dialogs opened from an action.** The actions also stay shown while a Menu, Popover or Dialog rendered inside them is open or has focus, so its focus return reaches the action. A Dialog rendered elsewhere and opened from an action through state (a "Delete…" menu item that opens a confirm Dialog outside the tree) closes after the actions have hidden again, on devices with hover and outside forced colors: a hidden action cannot take focus, so focus returns to the nearest element that can, normally the item. To return focus to the action, render the Dialog inside `actions`, with its `Dialog.Trigger` there or as a controlled `Dialog` next to the Menu that opens it (`finalFocusRef` cannot name the action, which is still hidden when the dialog closes).
2249
2340
  - **Navigation modes.** With `navigationMode="tree"` (the default), Tab from the focused item enters its actions (one stop; the arrow keys move inside them) and Shift+Tab returns. With `"treegrid"`, the "next" arrow (Right, Left in RTL) moves into the actions of an expanded parent or a leaf (a collapsed parent still expands first), "previous" returns, and Up and Down move between items from an action; the actions then take no Tab stop. This replaces APG's "next on an open item moves to its first child" (Down does that). The roles stay `tree` and `treeitem` in both modes, as in Fluent.
2250
2341
  - **Persona rows.** `Tree.ItemPersonaLayout` (flat `TreeItemPersonaLayout`, Fluent's name) lays out an item's label as a person: `media` (an Avatar of size 32 fits a row), the name as children and a `description` under it; the item is named by the name and described by the description.
2342
+ - **Motion.** A child group opens and closes the space it takes while it fades, over 200ms both ways, in an element of its own around the `role="group"` element (`wave-tree-item__collapse`, `display: contents` while open); a nested group goes with its parent. The items of a closing group leave the arrow order and typeahead at once, and focus inside it moves to the item that collapsed. `Tree motion` replaces the motion of every group, or turns it off with `null`; the chevron's turn is not part of it. A pointer click that falls through an exiting group onto its item (an `inert` element lets the pointer through) does nothing; Enter and a screen reader's click on the item still activate it.
2251
2343
 
2252
2344
  ### Breadcrumbs
2253
2345
 
@@ -2308,7 +2400,8 @@ export function Sidebar() {
2308
2400
 
2309
2401
  - **Section headers** (`Nav.SectionHeader`) are headings inside list items, `<h3>` by default (`headingLevel`); pick the level that fits the page's outline. **`Nav.Divider`** is a decorative line, hidden from assistive technology (the section header carries the structure).
2310
2402
  - **App items.** `Nav.AppItem` is the app's name at the top, with an `icon`: a link with `href`, else a button, never selected. `Nav.AppItemStatic` is the same row without an action.
2311
- - **Item actions.** `actions` on `Nav.Item` and `Nav.SubItem` (Fluent's SplitNavItem) renders your own controls beside the item's link, never inside it, in a group named by the item: a ToggleButton that pins the page, a MenuButton with more commands, each with its own name (a Tooltip with `relationship="label"` names an icon-only one). Each is a Tab stop after its item, and a click on one never selects the item. They are drawn while the row is hovered or has focus, while one is pressed or has its popup open, and always on touch screens and in forced colours; otherwise they are transparent and still focusable.
2403
+ - **Item actions.** `actions` on `Nav.Item` and `Nav.SubItem` (Fluent's SplitNavItem) renders your own controls beside the item's link, never inside it, in a group named by the item: a ToggleButton that pins the page, a MenuButton with more commands, each with its own name (a Tooltip with `relationship="label"` names an icon-only one). Each is a Tab stop after its item, and a click on one never selects the item. They are drawn while the row is hovered or has focus, while one is pressed or has its popup open, and always on touch screens and in forced colours; otherwise they are transparent and still focusable. They fade in and out over 150ms (at once under reduced motion and below `WaveProvider motion="none"`); the fade sets the transition of each action, so a transition class of yours on one is replaced.
2404
+ - **Categories.** A category's list opens and closes the space it takes while it fades, over 200ms both ways, in an element of its own around the `<ul>` (`wave-nav-category__collapse`, `display: contents` while open); focus inside a closing list moves to the category's button. `Nav.Category motion` replaces the collapse, or turns it off with `null`. `expandIcon` replaces the chevron (an optional indicator: `false` hides it; a glyph of yours is not turned, so a plus or a minus picked from `openCategories` works; a `<button>` or `Button` passed there is unwrapped, with a development warning), and `expandIconMotion` the transition of its half turn (150ms; `null` makes the turn snap).
2312
2405
 
2313
2406
  ### Navigation drawers and app shells
2314
2407
 
@@ -2377,15 +2470,17 @@ export function Shell({ narrow }: { narrow: boolean }) {
2377
2470
  - **`focusMode`.** Every item is a Tab stop by default (`NavDrawer.Body focusMode="tab"`). `focusMode="arrow"` makes the body one Tab stop, at first the current page, with both arrow pairs (wrapping), Home and End; screen readers do not announce arrow keys in a navigation landmark, so prefer the default unless your users expect an app pane. Fluent's NavDrawer uses arrow keys by default (its `tabbable` adds Tab stops).
2378
2471
  - **Width.** `size` defaults to `small` (320px), Drawer's scale; Fluent's navigation pane is 260px wide, `className="w-65"` on your Tailwind build (or a class of your own with `width: 260px`). `position` defaults to `start` (Drawer's to `end`), and `separator` to on.
2379
2472
  - **Server rendering.** An inline NavDrawer renders in the server HTML (open, or hidden when kept mounted); with `focusMode="arrow"` its Tab stop is set once it has mounted.
2473
+ - **Motion.** As Drawer's: an overlay slides in from its edge and fades while its backdrop fades, and an inline drawer opens and closes its space (`wave-drawer__collapse` and `wave-nav-drawer__collapse` on the element around its panel); a NavDrawer without `size` takes the `small` duration, 250ms. `motion` and `backdropMotion` replace them, or turn them off with `null`.
2380
2474
 
2381
2475
  ### Cards, tabs and carousels
2382
2476
 
2383
2477
  - **Card** `appearance` is `filled` (the default), `filled-alternative`, `outline` or `subtle`; `size` (`small`, `medium`, `large`) sets the padding of its parts through `--wave-card-padding`; `orientation="horizontal"` puts a `Card.Preview` beside the text. `Card.Preview` takes a `logo` over its bottom start corner. `Card.Header` takes an `image` (an Avatar, say) and an `action` (a menu button). With `selectionControl="checkbox"`, `checkbox` merges onto the built-in checkbox (an object), replaces it (an element) or removes it (`null`, when you render the control elsewhere); `floatingAction` sits over the top end corner and moves the checkbox to the start corner.
2384
- - **TabList** `appearance` is `transparent` (the default), `subtle`, `subtle-circular` or `filled-circular`, and `size` `small`, `medium` or `large`. A `TabList.Tab` with an `icon` and no text is icon-only: give it an `aria-label` (development warning `TabList.Tab:icon-only-name`).
2478
+ - **TabList** `appearance` is `transparent` (the default), `subtle`, `subtle-circular` or `filled-circular`, and `size` `small`, `medium` or `large`. A `TabList.Tab` with an `icon` and no text is icon-only: give it an `aria-label` (development warning `TabList.Tab:icon-only-name`). In the `transparent` appearance the selection line slides and stretches from the tab selected before to the new one over 300ms; the line is the selected tab's `::after` (recolour it with `after:border-b-…`, `after:border-s-…` when vertical), and `motion` replaces its timing, written for the `after:` variant, or turns it off with `null`.
2385
2479
  - **Carousel groups.** `groupSize={n}` shows `n` slides per page and `"auto"` as many as fit (the slides keep their width, which must be equal; `align` places a page that does not fill the width). `value`, `onValueChange` and `defaultValue` are the index of the first slide of the page; previous, next, the picker (one button per page), looping and autoplay move by page. `labels.slides(start, end, total)` names a page.
2386
2480
  - **Dragging.** `draggable` follows the pointer (mouse, touch, pen); a drag past a quarter of the width, or a release faster than 0.5 px/ms, changes the page, else the track snaps back. The click that ends a drag is dropped, a vertical swipe still scrolls the page, and autoplay pauses while dragging.
2387
2481
  - **Thumbnails.** `<Carousel.Item thumbnail="/a.png">` (or an object of `<img>` attributes) turns the picker into a row of thumbnails; each button keeps its slide name.
2388
2482
  - **Controls.** `controlsLayout` is `overlay` (the default, round buttons over the slides), `overlay-wide` (full-height edge strips), `inline` (buttons beside the slides) or `inline-wide`. `Carousel.Button` (`direction="previous"` or `"next"`) replaces the built-in button of that direction and keeps its name, `aria-controls` and disabled behaviour; its children replace the chevron. `appearance="elevated"` rounds the carousel and adds a shadow.
2483
+ - **Carousel motion.** `motion` is `slide` (the default: the slides glide in from the side of the page order, mirrored in right-to-left layouts), `fade` (the slides cross-fade in place; one slide per page, else they slide, with a development warning; a drag still pages on release) or `null` (the page changes at once); the root renders it as `data-motion` (`slide`, `fade`, `none`). `motionDuration` is the time in milliseconds (300 by default; Fluent's `duration` is another unit, Embla's speed of 20 to 60). Below `WaveProvider motion="none"` a carousel moves only with its own `motion`, and under reduced motion it never moves.
2389
2484
 
2390
2485
  ### Slots
2391
2486
 
@@ -2532,7 +2627,7 @@ Typeahead (Menu, a selectable or navigable List, Tree, Dropdown, Listbox) accept
2532
2627
  | `useListboxPopup(options)`, `ListboxSurface` | The popup of a custom picker: dismissal (`onDismiss(reason)`, a `DismissReason`: `'escape'`, `'outside-press'` or `'focus-outside'`) and positioning, and the surface that renders the list (`listClassName`, `surfaceClassName`). |
2533
2628
  | `ListboxProvider` | Provides a listbox's context to its options when you render the list yourself. |
2534
2629
  | `collectOptionLabels(children)`, `markListboxElement(component, kind)` | Read option labels from children before the options register (server HTML, first render); mark your own option and group components so they are read. |
2535
- | `useWaveTheme()` | Current theme, direction, portal container and `inputDefaults`. |
2630
+ | `useWaveTheme()` | Current theme, direction, portal container, `inputDefaults` and `motion` (`'default'` or `'none'`). |
2536
2631
  | `useToastController()` | `dispatchToast` (content or options) / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
2537
2632
  | `createToaster()` | A toast controller for code outside components; bind it with `<Toaster toaster={handle}>` (see [Toasts](#toasts)). Server-safe. |
2538
2633
  | `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
@@ -2551,6 +2646,21 @@ Typeahead (Menu, a selectable or navigable List, Tree, Dropdown, Listbox) accept
2551
2646
  | `resolveSlot`, `renderSlot` | The slot helpers the components use. |
2552
2647
  | `getThemeClassName(theme)` | The theme classes of a theme. |
2553
2648
 
2649
+ ## Upgrading from 0.14
2650
+
2651
+ 0.15 removes nothing public and every type only widens, but the overlays and disclosures of item 1 (not yet the listbox popups of Combobox, Dropdown, TagPicker and TimePicker, the DatePicker calendar and the AvatarGroup popover) now animate by default; these changes can affect your end-to-end tests, styles and type mocks:
2652
+
2653
+ 1. **Surfaces animate by default** in a browser: a closed Dialog, Drawer, NavDrawer, Menu, Popover, InfoButton note, Tooltip or TeachingPopover stays in the document, `inert`, for its exit (100 to 500ms). End-to-end tests that look for its absence right after a close wait for it, or render under `<WaveProvider motion="none">`; unit tests in jsdom are unaffected. A `defaultOpen` surface animates in on a client render too.
2654
+ 2. **Popover.Content, the Tooltip surface and the TeachingPopover surface** mount through the presence core and render `data-presence` and `data-state`.
2655
+ 3. **New wrapper elements** around Accordion's region, a Tree group, a Nav category's list and an inline drawer's panel, which are `display: contents` while open (so layout is unchanged then) and a clipping grid while they move; and a content element inside each toast container (`wave-toaster__toast-content`), around which the container is a one-cell grid in every phase. Child-combinator selectors across them, and tests that walk `parentElement` or `nextElementSibling`, change. An inline drawer with a percentage width resizes oddly during its motion: give it a length, or pass `motion={null}`.
2656
+ 4. **0.14's `data-[presence=…]` classes in `className`** (`Menu.Popover`, `Dialog.Content`, Drawer, NavDrawer) keep working and turn that part's default motion off; its backdrop now fades by default (`backdropMotion={null}` keeps 0.14's). CSS of yours on `.wave-toaster__toast[data-presence=…]` now runs beside the toasts' default motion (a `transition` shorthand can reset the recipe's property list): pass `<Toaster motion={null}>` or move it to `motion`. The same goes for CSS on `[data-wave-infolabel-surface][data-presence=…]` (`motion={null}` on InfoButton or InfoLabel).
2657
+ 5. **TabList's indicator** is the selected tab's `::after`: recolour it with `after:` border classes.
2658
+ 6. **Focus in a closing Accordion panel, Tree group or Nav category** moves to its trigger (0.14: `<body>`).
2659
+ 7. **Carousel's slide and Nav's chevron** use the `easy-ease` curve, and Nav item actions fade in over 150ms.
2660
+ 8. **`WaveContextValue` has a new member, `motion`:** a hand-built value (a test mock of `useWaveTheme`) adds it.
2661
+
2662
+ The CHANGELOG's [0.15.0 section](CHANGELOG.md) lists the additions and the full upgrade notes.
2663
+
2554
2664
  ## Upgrading from 0.13
2555
2665
 
2556
2666
  0.14 removes nothing public, and every new behaviour is opt-in; these changes can still affect your types, tests and styles:
@@ -1,2 +1,2 @@
1
- "use client";const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../lib/cn.cjs"),n=require("../../lib/composeEventHandlers.cjs"),r=require("../../lib/classNames.cjs"),i=require("../../hooks/useEventCallback.cjs"),a=require("../../hooks/useId.cjs"),o=require("../../lib/focus.cjs"),s=require("../../hooks/useMergedRefs.cjs"),ee=require("../../lib/aria.cjs"),te=require("../../hooks/usePresence.cjs"),ne=require("../../hooks/usePopupPosition.cjs"),c=require("../../hooks/useDismiss.cjs"),l=require("../../hooks/useRestoreFocus.cjs"),u=require("../../lib/styles.cjs"),d=require("../portal/Portal.cjs"),f=require("../../lib/icons.cjs"),p=require("../../hooks/useHoverIntent.cjs"),m=require("../overlays/Popover.shared.cjs");let h=require("react");h=e.__toESM(h,1);let g=require("react/jsx-runtime");var _={small:{size:12,margin:`-my-1`},medium:{size:16,margin:`-my-0.5`},large:{size:20,margin:`-my-px`}},v=250,y=h.createContext(null);y.displayName=`InfoNoteClassContext`;var b=({info:e,size:b,openOnHover:x=!0,id:re,type:ie=`button`,"aria-label":ae,"aria-labelledby":S,"aria-describedby":C,className:w,disabled:T,onClick:oe,onFocus:se,onBlur:ce,onKeyDown:le,onPointerDown:ue,onMouseDown:de,onPointerEnter:fe,onPointerMove:pe,onPointerLeave:me,ref:he,...E})=>{let D=b===`small`||b===`large`?b:`medium`,O=a.useId(`info-button`),k=re||O,A=h.useContext(y),j=A?.buttonId===k?A.className:void 0,M=ae||`Information`,N=a.useId(`info-note`),P=a.useId(`info`),[F,I]=h.useState(null),L=F!==null,R=h.useRef(null),z=h.useRef(null),B=h.useRef(null),[ge,_e]=h.useState(null),[V,H]=h.useState(null),U=h.useRef(F);h.useLayoutEffect(()=>{U.current=F});let W=h.useRef(!1),G=h.useRef(!1),K=h.useRef(void 0),q=h.useRef(!1),{isMounted:J,ref:ve,presenceProps:ye}=te.usePresence(L),{setReference:be,setFloating:xe,floatingProps:Se,arrowStyles:Ce,side:we}=ne.usePopupPosition({open:L,side:`top`,align:`center`,offset:8,arrowRef:B}),{layerId:Y}=c.useDismiss({open:L,onDismiss:e=>{e!==`focus-outside`&&(W.current=!0),q.current=!1,Q(),I(null)},refs:[R,z],anchorRef:R,focusOutside:!0});l.useRestoreFocus({enabled:L,container:V,triggerRef:R,onlyIfFocusInside:!0,isHoverClose:()=>q.current});let{triggerHandlers:X,surfaceHandlers:Z,cancel:Q}=p.useHoverIntent({enabled:x&&!T,open:L,openDelay:v,closeDelay:v,trigger:ge,surface:V,onOpen:()=>{q.current=!1,I(e=>e??`hover`)},onClose:()=>{q.current=!0,I(e=>e===`hover`?null:e)},canClose:()=>U.current===`hover`&&!m.isFocusInSurfaceTree(V,Y)});h.useEffect(()=>()=>clearTimeout(K.current),[]),h.useEffect(()=>{if(!V)return;let e=e=>{let t=e.relatedTarget;!t||V.contains(t)||R.current?.contains(t)||(W.current=!1)};return V.addEventListener(`focusout`,e),()=>V.removeEventListener(`focusout`,e)},[V]);let Te=i.useEventCallback(()=>{Q(),q.current=!1;let e=F===`click`;e&&(W.current=!0),I(e?null:`click`)}),Ee=i.useEventCallback(()=>{!x||G.current||W.current&&!L||(q.current=!1,Q(),I(e=>e===`click`?e:`focus`))}),De=i.useEventCallback(e=>{let t=e.currentTarget,n=e.relatedTarget;n&&V?.contains(n)||(n||t.ownerDocument.activeElement!==t)&&(W.current=!1)}),$=i.useEventCallback(()=>{G.current=!0,clearTimeout(K.current),K.current=setTimeout(()=>{K.current=void 0,G.current=!1},0)}),Oe=e=>{if(e.key!==`Tab`||e.shiftKey||!L||!V)return;let t=o.getFirstTabbable(V);t&&(e.preventDefault(),t.focus())},ke=m.usePopoverTabOrder({enabled:L,surface:V,anchorRef:R,getPreviousStop:()=>R.current}),Ae=s.useMergedRefs(he,R,_e,be),je=s.useMergedRefs(z,H,xe,ve);return(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`button`,{type:ie,id:k,"data-size":D,...E,ref:Ae,disabled:T,"aria-label":M,"aria-labelledby":S,"aria-describedby":ee.joinIds(C,P),"aria-expanded":L,"aria-controls":L?N:void 0,className:t.cn(r.infoButtonClassNames.root,`inline-flex size-6 shrink-0 items-center justify-center rounded-full border-0 bg-transparent p-0 text-muted-foreground`,_[D].margin,`not-disabled:not-aria-disabled:hover:text-foreground`,u.focusRing,w),onClick:n.composeEventHandlers(oe,Te),onFocus:n.composeEventHandlers(se,Ee),onKeyDown:n.composeEventHandlers(le,Oe),onPointerEnter:n.composeEventHandlers(fe,X.onPointerEnter),onPointerMove:n.composeEventHandlers(pe,X.onPointerMove),onPointerLeave:n.composeEventHandlers(me,X.onPointerLeave),onBlur:n.composeEventHandlers(ce,De,{checkDefaultPrevented:!1}),onPointerDown:n.composeEventHandlers(ue,$,{checkDefaultPrevented:!1}),onMouseDown:n.composeEventHandlers(de,$,{checkDefaultPrevented:!1}),children:(0,g.jsx)(f.InfoIcon,{size:_[D].size})}),(0,g.jsx)(`span`,{id:J?void 0:P,hidden:!0,children:J?null:e}),J&&(0,g.jsx)(d.Portal,{layerId:Y,children:(0,g.jsxs)(`div`,{ref:je,id:N,role:`note`,"aria-labelledby":S||k,"data-wave-infolabel-surface":``,"data-state":L?`open`:`closed`,...ye,...Se,onKeyDown:ke,onPointerEnter:Z.onPointerEnter,onPointerLeave:Z.onPointerLeave,className:t.cn(r.infoButtonClassNames.info,j,`w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-border bg-background px-3 py-2 text-caption-1 text-foreground shadow-16`),children:[(0,g.jsx)(m.PopoverBeak,{ref:B,side:we,style:Ce,"data-wave-popover-arrow":``,className:r.infoButtonClassNames.beak}),(0,g.jsx)(`div`,{id:P,children:e})]})})]})};b.displayName=`InfoButton`,exports.InfoButton=b,exports.InfoNoteClassContext=y;
1
+ "use client";const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../lib/cn.cjs"),n=require("../../lib/composeEventHandlers.cjs"),r=require("../../lib/classNames.cjs"),i=require("../../hooks/useEventCallback.cjs"),a=require("../../hooks/useId.cjs"),o=require("../../lib/focus.cjs"),s=require("../../hooks/useMergedRefs.cjs"),c=require("../../lib/aria.cjs"),ee=require("../../hooks/usePresence.cjs"),te=require("../../hooks/usePopupPosition.cjs"),ne=require("../../hooks/useDismiss.cjs"),re=require("../../hooks/useRestoreFocus.cjs"),ie=require("../../lib/styles.cjs"),ae=require("../portal/Portal.cjs"),oe=require("../../lib/icons.cjs"),se=require("../../lib/motion.cjs"),l=require("../../hooks/useFocusBeforeClose.cjs"),u=require("../../hooks/useHoverIntent.cjs"),d=require("../../hooks/useMotion.cjs"),f=require("../overlays/Popover.shared.cjs");let p=require("react");p=e.__toESM(p,1);let m=require("react/jsx-runtime");var h={small:{size:12,margin:`-my-1`},medium:{size:16,margin:`-my-0.5`},large:{size:20,margin:`-my-px`}},g=250,_=p.createContext(null);_.displayName=`InfoNoteClassContext`;var v=({info:e,size:v,openOnHover:y=!0,id:ce,type:le=`button`,"aria-label":ue,motion:de,"aria-labelledby":b,"aria-describedby":fe,className:pe,disabled:x,onClick:me,onFocus:he,onBlur:ge,onKeyDown:_e,onPointerDown:ve,onMouseDown:ye,onPointerEnter:S,onPointerMove:C,onPointerLeave:w,ref:T,...E})=>{let D=v===`small`||v===`large`?v:`medium`,O=a.useId(`info-button`),k=ce||O,A=p.useContext(_),j=A?.buttonId===k?A.className:void 0,M=ue||`Information`,N=a.useId(`info-note`),P=a.useId(`info`),[F,I]=p.useState(null),L=F!==null,R=p.useRef(null),z=p.useRef(null),B=p.useRef(null),[be,xe]=p.useState(null),[V,Se]=p.useState(null),H=p.useRef(F);p.useLayoutEffect(()=>{H.current=F});let U=p.useRef(!1),W=p.useRef(!1),G=p.useRef(void 0),K=p.useRef(!1),{isMounted:q,ref:Ce,presenceProps:we}=ee.usePresence(L,{appear:!0}),{setReference:Te,setFloating:Ee,floatingProps:De,arrowStyles:Oe,side:ke}=te.usePopupPosition({open:L,side:`top`,align:`center`,offset:8,arrowRef:B}),{layerId:J}=ne.useDismiss({open:L,onDismiss:e=>{e!==`focus-outside`&&(U.current=!0),K.current=!1,Z(),I(null)},refs:[R,z],anchorRef:R,focusOutside:!0});re.useRestoreFocus({enabled:L,container:V,triggerRef:R,onlyIfFocusInside:!0,isHoverClose:()=>K.current}),l.useFocusBeforeClose({open:L,surfaceRef:z,captureOpener:()=>R.current,candidates:()=>[R.current]});let Ae=d.useMotionClasses(de,se.popupMotion),{triggerHandlers:Y,surfaceHandlers:X,cancel:Z}=u.useHoverIntent({enabled:y&&!x,open:L,openDelay:g,closeDelay:g,trigger:be,surface:V,onOpen:()=>{K.current=!1,I(e=>e??`hover`)},onClose:()=>{K.current=!0,I(e=>e===`hover`?null:e)},canClose:()=>H.current===`hover`&&!f.isFocusInSurfaceTree(V,J)});p.useEffect(()=>()=>clearTimeout(G.current),[]),p.useEffect(()=>{if(!V)return;let e=e=>{let t=e.relatedTarget;!t||V.contains(t)||R.current?.contains(t)||(U.current=!1)};return V.addEventListener(`focusout`,e),()=>V.removeEventListener(`focusout`,e)},[V]);let je=i.useEventCallback(()=>{Z(),K.current=!1;let e=F===`click`;e&&(U.current=!0),I(e?null:`click`)}),Q=i.useEventCallback(()=>{!y||W.current||U.current&&!L||(K.current=!1,Z(),I(e=>e===`click`?e:`focus`))}),Me=i.useEventCallback(e=>{let t=e.currentTarget,n=e.relatedTarget;n&&V?.contains(n)||(n||t.ownerDocument.activeElement!==t)&&(U.current=!1)}),$=i.useEventCallback(()=>{W.current=!0,clearTimeout(G.current),G.current=setTimeout(()=>{G.current=void 0,W.current=!1},0)}),Ne=e=>{if(e.key!==`Tab`||e.shiftKey||!L||!V)return;let t=o.getFirstTabbable(V);t&&(e.preventDefault(),t.focus())},Pe=f.usePopoverTabOrder({enabled:L,surface:V,anchorRef:R,getPreviousStop:()=>R.current}),Fe=s.useMergedRefs(T,R,xe,Te),Ie=s.useMergedRefs(z,Se,Ee,Ce);return(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`button`,{type:le,id:k,"data-size":D,...E,ref:Fe,disabled:x,"aria-label":M,"aria-labelledby":b,"aria-describedby":c.joinIds(fe,P),"aria-expanded":L,"aria-controls":L?N:void 0,className:t.cn(r.infoButtonClassNames.root,`inline-flex size-6 shrink-0 items-center justify-center rounded-full border-0 bg-transparent p-0 text-muted-foreground`,h[D].margin,`not-disabled:not-aria-disabled:hover:text-foreground`,ie.focusRing,pe),onClick:n.composeEventHandlers(me,je),onFocus:n.composeEventHandlers(he,Q),onKeyDown:n.composeEventHandlers(_e,Ne),onPointerEnter:n.composeEventHandlers(S,Y.onPointerEnter),onPointerMove:n.composeEventHandlers(C,Y.onPointerMove),onPointerLeave:n.composeEventHandlers(w,Y.onPointerLeave),onBlur:n.composeEventHandlers(ge,Me,{checkDefaultPrevented:!1}),onPointerDown:n.composeEventHandlers(ve,$,{checkDefaultPrevented:!1}),onMouseDown:n.composeEventHandlers(ye,$,{checkDefaultPrevented:!1}),children:(0,m.jsx)(oe.InfoIcon,{size:h[D].size})}),(0,m.jsx)(`span`,{id:q?void 0:P,hidden:!0,children:q?null:e}),q&&(0,m.jsx)(ae.Portal,{layerId:J,children:(0,m.jsxs)(`div`,{ref:Ie,id:N,role:`note`,"aria-labelledby":b||k,"data-wave-infolabel-surface":``,"data-state":L?`open`:`closed`,...we,...De,onKeyDown:Pe,onPointerEnter:X.onPointerEnter,onPointerLeave:X.onPointerLeave,className:t.cn(r.infoButtonClassNames.info,j,`w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-border bg-background px-3 py-2 text-caption-1 text-foreground shadow-16`,Ae),children:[(0,m.jsx)(f.PopoverBeak,{ref:B,side:ke,style:Oe,"data-wave-popover-arrow":``,className:r.infoButtonClassNames.beak}),(0,m.jsx)(`div`,{id:P,children:e})]})})]})};v.displayName=`InfoButton`,exports.InfoButton=v,exports.InfoNoteClassContext=_;
2
2
  //# sourceMappingURL=InfoButton.cjs.map