@mortenbrudvik/waveui 0.4.0 → 0.7.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.
- package/README.md +1221 -173
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/components/button/Button.cjs +2 -0
- package/dist/components/button/Button.cjs.map +1 -0
- package/dist/components/button/Button.mjs +50 -0
- package/dist/components/button/Button.mjs.map +1 -0
- package/dist/components/button/Button.semantics.cjs +2 -0
- package/dist/components/button/Button.semantics.cjs.map +1 -0
- package/dist/components/button/Button.semantics.mjs +61 -0
- package/dist/components/button/Button.semantics.mjs.map +1 -0
- package/dist/components/button/Button.slots.cjs +2 -0
- package/dist/components/button/Button.slots.cjs.map +1 -0
- package/dist/components/button/Button.slots.mjs +47 -0
- package/dist/components/button/Button.slots.mjs.map +1 -0
- package/dist/components/button/CompoundButton.cjs +2 -0
- package/dist/components/button/CompoundButton.cjs.map +1 -0
- package/dist/components/button/CompoundButton.mjs +53 -0
- package/dist/components/button/CompoundButton.mjs.map +1 -0
- package/dist/components/button/Link.cjs +2 -0
- package/dist/components/button/Link.cjs.map +1 -0
- package/dist/components/button/Link.mjs +37 -0
- package/dist/components/button/Link.mjs.map +1 -0
- package/dist/components/button/MenuButton.cjs +2 -0
- package/dist/components/button/MenuButton.cjs.map +1 -0
- package/dist/components/button/MenuButton.mjs +45 -0
- package/dist/components/button/MenuButton.mjs.map +1 -0
- package/dist/components/button/SplitButton.cjs +2 -0
- package/dist/components/button/SplitButton.cjs.map +1 -0
- package/dist/components/button/SplitButton.mjs +66 -0
- package/dist/components/button/SplitButton.mjs.map +1 -0
- package/dist/components/button/ToggleButton.cjs +2 -0
- package/dist/components/button/ToggleButton.cjs.map +1 -0
- package/dist/components/button/ToggleButton.mjs +65 -0
- package/dist/components/button/ToggleButton.mjs.map +1 -0
- package/dist/components/button/Toolbar.cjs +2 -0
- package/dist/components/button/Toolbar.cjs.map +1 -0
- package/dist/components/button/Toolbar.context.cjs +2 -0
- package/dist/components/button/Toolbar.context.cjs.map +1 -0
- package/dist/components/button/Toolbar.context.mjs +31 -0
- package/dist/components/button/Toolbar.context.mjs.map +1 -0
- package/dist/components/button/Toolbar.mjs +63 -0
- package/dist/components/button/Toolbar.mjs.map +1 -0
- package/dist/components/button/Toolbar.parts.cjs +2 -0
- package/dist/components/button/Toolbar.parts.cjs.map +1 -0
- package/dist/components/button/Toolbar.parts.mjs +131 -0
- package/dist/components/button/Toolbar.parts.mjs.map +1 -0
- package/dist/components/button/buttonStyles.cjs +2 -0
- package/dist/components/button/buttonStyles.cjs.map +1 -0
- package/dist/components/button/buttonStyles.mjs +39 -0
- package/dist/components/button/buttonStyles.mjs.map +1 -0
- package/dist/components/data-display/Avatar.cjs +2 -0
- package/dist/components/data-display/Avatar.cjs.map +1 -0
- package/dist/components/data-display/Avatar.mjs +157 -0
- package/dist/components/data-display/Avatar.mjs.map +1 -0
- package/dist/components/data-display/AvatarGroup.cjs +2 -0
- package/dist/components/data-display/AvatarGroup.cjs.map +1 -0
- package/dist/components/data-display/AvatarGroup.mjs +134 -0
- package/dist/components/data-display/AvatarGroup.mjs.map +1 -0
- package/dist/components/data-display/Badge.cjs +2 -0
- package/dist/components/data-display/Badge.cjs.map +1 -0
- package/dist/components/data-display/Badge.colors.cjs +2 -0
- package/dist/components/data-display/Badge.colors.cjs.map +1 -0
- package/dist/components/data-display/Badge.colors.mjs +58 -0
- package/dist/components/data-display/Badge.colors.mjs.map +1 -0
- package/dist/components/data-display/Badge.mjs +32 -0
- package/dist/components/data-display/Badge.mjs.map +1 -0
- package/dist/components/data-display/CounterBadge.cjs +2 -0
- package/dist/components/data-display/CounterBadge.cjs.map +1 -0
- package/dist/components/data-display/CounterBadge.mjs +28 -0
- package/dist/components/data-display/CounterBadge.mjs.map +1 -0
- package/dist/components/data-display/Divider.cjs +2 -0
- package/dist/components/data-display/Divider.cjs.map +1 -0
- package/dist/components/data-display/Divider.mjs +52 -0
- package/dist/components/data-display/Divider.mjs.map +1 -0
- package/dist/components/data-display/Image.cjs +2 -0
- package/dist/components/data-display/Image.cjs.map +1 -0
- package/dist/components/data-display/Image.mjs +32 -0
- package/dist/components/data-display/Image.mjs.map +1 -0
- package/dist/components/data-display/InfoLabel.cjs +2 -0
- package/dist/components/data-display/InfoLabel.cjs.map +1 -0
- package/dist/components/data-display/InfoLabel.mjs +104 -0
- package/dist/components/data-display/InfoLabel.mjs.map +1 -0
- package/dist/components/data-display/List.cjs +2 -0
- package/dist/components/data-display/List.cjs.map +1 -0
- package/dist/components/data-display/List.mjs +381 -0
- package/dist/components/data-display/List.mjs.map +1 -0
- package/dist/components/data-display/List.registry.cjs +2 -0
- package/dist/components/data-display/List.registry.cjs.map +1 -0
- package/dist/components/data-display/List.registry.mjs +101 -0
- package/dist/components/data-display/List.registry.mjs.map +1 -0
- package/dist/components/data-display/Persona.cjs +2 -0
- package/dist/components/data-display/Persona.cjs.map +1 -0
- package/dist/components/data-display/Persona.mjs +48 -0
- package/dist/components/data-display/Persona.mjs.map +1 -0
- package/dist/components/data-display/PresenceBadge.cjs +2 -0
- package/dist/components/data-display/PresenceBadge.cjs.map +1 -0
- package/dist/components/data-display/PresenceBadge.mjs +71 -0
- package/dist/components/data-display/PresenceBadge.mjs.map +1 -0
- package/dist/components/data-display/Tag.cjs +2 -0
- package/dist/components/data-display/Tag.cjs.map +1 -0
- package/dist/components/data-display/Tag.mjs +147 -0
- package/dist/components/data-display/Tag.mjs.map +1 -0
- package/dist/components/feedback/MessageBar.cjs +2 -0
- package/dist/components/feedback/MessageBar.cjs.map +1 -0
- package/dist/components/feedback/MessageBar.mjs +144 -0
- package/dist/components/feedback/MessageBar.mjs.map +1 -0
- package/dist/components/feedback/MessageBar.status.cjs +2 -0
- package/dist/components/feedback/MessageBar.status.cjs.map +1 -0
- package/dist/components/feedback/MessageBar.status.mjs +45 -0
- package/dist/components/feedback/MessageBar.status.mjs.map +1 -0
- package/dist/components/feedback/ProgressBar.cjs +2 -0
- package/dist/components/feedback/ProgressBar.cjs.map +1 -0
- package/dist/components/feedback/ProgressBar.mjs +72 -0
- package/dist/components/feedback/ProgressBar.mjs.map +1 -0
- package/dist/components/feedback/Skeleton.cjs +2 -0
- package/dist/components/feedback/Skeleton.cjs.map +1 -0
- package/dist/components/feedback/Skeleton.mjs +47 -0
- package/dist/components/feedback/Skeleton.mjs.map +1 -0
- package/dist/components/feedback/Spinner.cjs +2 -0
- package/dist/components/feedback/Spinner.cjs.map +1 -0
- package/dist/components/feedback/Spinner.mjs +60 -0
- package/dist/components/feedback/Spinner.mjs.map +1 -0
- package/dist/components/feedback/Toast.cjs +2 -0
- package/dist/components/feedback/Toast.cjs.map +1 -0
- package/dist/components/feedback/Toast.mjs +347 -0
- package/dist/components/feedback/Toast.mjs.map +1 -0
- package/dist/components/input/Checkbox.cjs +2 -0
- package/dist/components/input/Checkbox.cjs.map +1 -0
- package/dist/components/input/Checkbox.mjs +96 -0
- package/dist/components/input/Checkbox.mjs.map +1 -0
- package/dist/components/input/ColorPicker.cjs +6 -0
- package/dist/components/input/ColorPicker.cjs.map +1 -0
- package/dist/components/input/ColorPicker.mjs +236 -0
- package/dist/components/input/ColorPicker.mjs.map +1 -0
- package/dist/components/input/Combobox.cjs +2 -0
- package/dist/components/input/Combobox.cjs.map +1 -0
- package/dist/components/input/Combobox.expand.cjs +2 -0
- package/dist/components/input/Combobox.expand.cjs.map +1 -0
- package/dist/components/input/Combobox.expand.mjs +38 -0
- package/dist/components/input/Combobox.expand.mjs.map +1 -0
- package/dist/components/input/Combobox.mjs +187 -0
- package/dist/components/input/Combobox.mjs.map +1 -0
- package/dist/components/input/DatePicker.cjs +2 -0
- package/dist/components/input/DatePicker.cjs.map +1 -0
- package/dist/components/input/DatePicker.mjs +340 -0
- package/dist/components/input/DatePicker.mjs.map +1 -0
- package/dist/components/input/Dropdown.cjs +2 -0
- package/dist/components/input/Dropdown.cjs.map +1 -0
- package/dist/components/input/Dropdown.mjs +128 -0
- package/dist/components/input/Dropdown.mjs.map +1 -0
- package/dist/components/input/Field.cjs +2 -0
- package/dist/components/input/Field.cjs.map +1 -0
- package/dist/components/input/Field.mjs +200 -0
- package/dist/components/input/Field.mjs.map +1 -0
- package/dist/components/input/Input.cjs +2 -0
- package/dist/components/input/Input.cjs.map +1 -0
- package/dist/components/input/Input.mjs +81 -0
- package/dist/components/input/Input.mjs.map +1 -0
- package/dist/components/input/Label.cjs +2 -0
- package/dist/components/input/Label.cjs.map +1 -0
- package/dist/components/input/Label.mjs +28 -0
- package/dist/components/input/Label.mjs.map +1 -0
- package/dist/components/input/Option.cjs +2 -0
- package/dist/components/input/Option.cjs.map +1 -0
- package/dist/components/input/Option.mjs +169 -0
- package/dist/components/input/Option.mjs.map +1 -0
- package/dist/components/input/RadioGroup.cjs +2 -0
- package/dist/components/input/RadioGroup.cjs.map +1 -0
- package/dist/components/input/RadioGroup.mjs +139 -0
- package/dist/components/input/RadioGroup.mjs.map +1 -0
- package/dist/components/input/Rating.cjs +2 -0
- package/dist/components/input/Rating.cjs.map +1 -0
- package/dist/components/input/Rating.mjs +196 -0
- package/dist/components/input/Rating.mjs.map +1 -0
- package/dist/components/input/SearchBox.cjs +2 -0
- package/dist/components/input/SearchBox.cjs.map +1 -0
- package/dist/components/input/SearchBox.mjs +205 -0
- package/dist/components/input/SearchBox.mjs.map +1 -0
- package/dist/components/input/Select.cjs +2 -0
- package/dist/components/input/Select.cjs.map +1 -0
- package/dist/components/input/Select.mjs +33 -0
- package/dist/components/input/Select.mjs.map +1 -0
- package/dist/components/input/Slider.cjs +2 -0
- package/dist/components/input/Slider.cjs.map +1 -0
- package/dist/components/input/Slider.mjs +79 -0
- package/dist/components/input/Slider.mjs.map +1 -0
- package/dist/components/input/SpinButton.cjs +2 -0
- package/dist/components/input/SpinButton.cjs.map +1 -0
- package/dist/components/input/SpinButton.mjs +157 -0
- package/dist/components/input/SpinButton.mjs.map +1 -0
- package/dist/components/input/SwatchPicker.cjs +2 -0
- package/dist/components/input/SwatchPicker.cjs.map +1 -0
- package/dist/components/input/SwatchPicker.mjs +113 -0
- package/dist/components/input/SwatchPicker.mjs.map +1 -0
- package/dist/components/input/Switch.cjs +2 -0
- package/dist/components/input/Switch.cjs.map +1 -0
- package/dist/components/input/Switch.mjs +92 -0
- package/dist/components/input/Switch.mjs.map +1 -0
- package/dist/components/input/TagPicker.cjs +2 -0
- package/dist/components/input/TagPicker.cjs.map +1 -0
- package/dist/components/input/TagPicker.mjs +249 -0
- package/dist/components/input/TagPicker.mjs.map +1 -0
- package/dist/components/input/Textarea.cjs +2 -0
- package/dist/components/input/Textarea.cjs.map +1 -0
- package/dist/components/input/Textarea.mjs +32 -0
- package/dist/components/input/Textarea.mjs.map +1 -0
- package/dist/components/input/TimePicker.cjs +2 -0
- package/dist/components/input/TimePicker.cjs.map +1 -0
- package/dist/components/input/TimePicker.mjs +292 -0
- package/dist/components/input/TimePicker.mjs.map +1 -0
- package/dist/components/input/colorUtils.cjs +2 -0
- package/dist/components/input/colorUtils.cjs.map +1 -0
- package/dist/components/input/colorUtils.mjs +96 -0
- package/dist/components/input/colorUtils.mjs.map +1 -0
- package/dist/components/input/dateUtils.cjs +2 -0
- package/dist/components/input/dateUtils.cjs.map +1 -0
- package/dist/components/input/dateUtils.mjs +250 -0
- package/dist/components/input/dateUtils.mjs.map +1 -0
- package/dist/components/input/pickerStyles.cjs +2 -0
- package/dist/components/input/pickerStyles.cjs.map +1 -0
- package/dist/components/input/pickerStyles.mjs +10 -0
- package/dist/components/input/pickerStyles.mjs.map +1 -0
- package/dist/components/internal/HiddenInput.cjs +2 -0
- package/dist/components/internal/HiddenInput.cjs.map +1 -0
- package/dist/components/internal/HiddenInput.mjs +88 -0
- package/dist/components/internal/HiddenInput.mjs.map +1 -0
- package/dist/components/layout/Accordion.cjs +2 -0
- package/dist/components/layout/Accordion.cjs.map +1 -0
- package/dist/components/layout/Accordion.mjs +236 -0
- package/dist/components/layout/Accordion.mjs.map +1 -0
- package/dist/components/layout/Card.cjs +2 -0
- package/dist/components/layout/Card.cjs.map +1 -0
- package/dist/components/layout/Card.mjs +121 -0
- package/dist/components/layout/Card.mjs.map +1 -0
- package/dist/components/layout/Carousel.cjs +2 -0
- package/dist/components/layout/Carousel.cjs.map +1 -0
- package/dist/components/layout/Carousel.mjs +205 -0
- package/dist/components/layout/Carousel.mjs.map +1 -0
- package/dist/components/layout/Flex.cjs +2 -0
- package/dist/components/layout/Flex.cjs.map +1 -0
- package/dist/components/layout/Flex.mjs +54 -0
- package/dist/components/layout/Flex.mjs.map +1 -0
- package/dist/components/layout/Grid.cjs +2 -0
- package/dist/components/layout/Grid.cjs.map +1 -0
- package/dist/components/layout/Grid.mjs +62 -0
- package/dist/components/layout/Grid.mjs.map +1 -0
- package/dist/components/layout/Overflow.cjs +2 -0
- package/dist/components/layout/Overflow.cjs.map +1 -0
- package/dist/components/layout/Overflow.mjs +269 -0
- package/dist/components/layout/Overflow.mjs.map +1 -0
- package/dist/components/layout/Stack.cjs +2 -0
- package/dist/components/layout/Stack.cjs.map +1 -0
- package/dist/components/layout/Stack.mjs +39 -0
- package/dist/components/layout/Stack.mjs.map +1 -0
- package/dist/components/layout/TabList.cjs +2 -0
- package/dist/components/layout/TabList.cjs.map +1 -0
- package/dist/components/layout/TabList.mjs +238 -0
- package/dist/components/layout/TabList.mjs.map +1 -0
- package/dist/components/layout/Tree.cjs +2 -0
- package/dist/components/layout/Tree.cjs.map +1 -0
- package/dist/components/layout/Tree.mjs +220 -0
- package/dist/components/layout/Tree.mjs.map +1 -0
- package/dist/components/layout/disclosureIds.cjs +2 -0
- package/dist/components/layout/disclosureIds.cjs.map +1 -0
- package/dist/components/layout/disclosureIds.mjs +12 -0
- package/dist/components/layout/disclosureIds.mjs.map +1 -0
- package/dist/components/motion/Presence.cjs +2 -0
- package/dist/components/motion/Presence.cjs.map +1 -0
- package/dist/components/motion/Presence.mjs +35 -0
- package/dist/components/motion/Presence.mjs.map +1 -0
- package/dist/components/navigation/Breadcrumb.cjs +2 -0
- package/dist/components/navigation/Breadcrumb.cjs.map +1 -0
- package/dist/components/navigation/Breadcrumb.mjs +144 -0
- package/dist/components/navigation/Breadcrumb.mjs.map +1 -0
- package/dist/components/navigation/Menu.cjs +2 -0
- package/dist/components/navigation/Menu.cjs.map +1 -0
- package/dist/components/navigation/Menu.context.cjs +2 -0
- package/dist/components/navigation/Menu.context.cjs.map +1 -0
- package/dist/components/navigation/Menu.context.mjs +82 -0
- package/dist/components/navigation/Menu.context.mjs.map +1 -0
- package/dist/components/navigation/Menu.group.cjs +2 -0
- package/dist/components/navigation/Menu.group.cjs.map +1 -0
- package/dist/components/navigation/Menu.group.mjs +58 -0
- package/dist/components/navigation/Menu.group.mjs.map +1 -0
- package/dist/components/navigation/Menu.items.cjs +2 -0
- package/dist/components/navigation/Menu.items.cjs.map +1 -0
- package/dist/components/navigation/Menu.items.mjs +127 -0
- package/dist/components/navigation/Menu.items.mjs.map +1 -0
- package/dist/components/navigation/Menu.link.cjs +2 -0
- package/dist/components/navigation/Menu.link.cjs.map +1 -0
- package/dist/components/navigation/Menu.link.mjs +41 -0
- package/dist/components/navigation/Menu.link.mjs.map +1 -0
- package/dist/components/navigation/Menu.mjs +27 -0
- package/dist/components/navigation/Menu.mjs.map +1 -0
- package/dist/components/navigation/Menu.popover.cjs +2 -0
- package/dist/components/navigation/Menu.popover.cjs.map +1 -0
- package/dist/components/navigation/Menu.popover.mjs +231 -0
- package/dist/components/navigation/Menu.popover.mjs.map +1 -0
- package/dist/components/navigation/Menu.root.cjs +2 -0
- package/dist/components/navigation/Menu.root.cjs.map +1 -0
- package/dist/components/navigation/Menu.root.mjs +308 -0
- package/dist/components/navigation/Menu.root.mjs.map +1 -0
- package/dist/components/navigation/Menu.selectable.cjs +2 -0
- package/dist/components/navigation/Menu.selectable.cjs.map +1 -0
- package/dist/components/navigation/Menu.selectable.mjs +89 -0
- package/dist/components/navigation/Menu.selectable.mjs.map +1 -0
- package/dist/components/navigation/Menu.shared.cjs +2 -0
- package/dist/components/navigation/Menu.shared.cjs.map +1 -0
- package/dist/components/navigation/Menu.shared.mjs +23 -0
- package/dist/components/navigation/Menu.shared.mjs.map +1 -0
- package/dist/components/navigation/Menu.splitGroup.cjs +2 -0
- package/dist/components/navigation/Menu.splitGroup.cjs.map +1 -0
- package/dist/components/navigation/Menu.splitGroup.mjs +51 -0
- package/dist/components/navigation/Menu.splitGroup.mjs.map +1 -0
- package/dist/components/navigation/Menu.trigger.cjs +2 -0
- package/dist/components/navigation/Menu.trigger.cjs.map +1 -0
- package/dist/components/navigation/Menu.trigger.mjs +79 -0
- package/dist/components/navigation/Menu.trigger.mjs.map +1 -0
- package/dist/components/navigation/Nav.cjs +2 -0
- package/dist/components/navigation/Nav.cjs.map +1 -0
- package/dist/components/navigation/Nav.mjs +276 -0
- package/dist/components/navigation/Nav.mjs.map +1 -0
- package/dist/components/navigation/Pagination.cjs +2 -0
- package/dist/components/navigation/Pagination.cjs.map +1 -0
- package/dist/components/navigation/Pagination.mjs +110 -0
- package/dist/components/navigation/Pagination.mjs.map +1 -0
- package/dist/components/navigation/Stepper.cjs +2 -0
- package/dist/components/navigation/Stepper.cjs.map +1 -0
- package/dist/components/navigation/Stepper.mjs +204 -0
- package/dist/components/navigation/Stepper.mjs.map +1 -0
- package/dist/components/overlays/Dialog.cjs +2 -0
- package/dist/components/overlays/Dialog.cjs.map +1 -0
- package/dist/components/overlays/Dialog.mjs +180 -0
- package/dist/components/overlays/Dialog.mjs.map +1 -0
- package/dist/components/overlays/Dialog.shared.cjs +2 -0
- package/dist/components/overlays/Dialog.shared.cjs.map +1 -0
- package/dist/components/overlays/Dialog.shared.mjs +227 -0
- package/dist/components/overlays/Dialog.shared.mjs.map +1 -0
- package/dist/components/overlays/Drawer.cjs +2 -0
- package/dist/components/overlays/Drawer.cjs.map +1 -0
- package/dist/components/overlays/Drawer.mjs +192 -0
- package/dist/components/overlays/Drawer.mjs.map +1 -0
- package/dist/components/overlays/Popover.cjs +2 -0
- package/dist/components/overlays/Popover.cjs.map +1 -0
- package/dist/components/overlays/Popover.mjs +365 -0
- package/dist/components/overlays/Popover.mjs.map +1 -0
- package/dist/components/overlays/Popover.shared.cjs +2 -0
- package/dist/components/overlays/Popover.shared.cjs.map +1 -0
- package/dist/components/overlays/Popover.shared.mjs +182 -0
- package/dist/components/overlays/Popover.shared.mjs.map +1 -0
- package/dist/components/overlays/TeachingPopover.cjs +2 -0
- package/dist/components/overlays/TeachingPopover.cjs.map +1 -0
- package/dist/components/overlays/TeachingPopover.mjs +181 -0
- package/dist/components/overlays/TeachingPopover.mjs.map +1 -0
- package/dist/components/overlays/Tooltip.cjs +2 -0
- package/dist/components/overlays/Tooltip.cjs.map +1 -0
- package/dist/components/overlays/Tooltip.mjs +160 -0
- package/dist/components/overlays/Tooltip.mjs.map +1 -0
- package/dist/components/portal/Portal.cjs +2 -0
- package/dist/components/portal/Portal.cjs.map +1 -0
- package/dist/components/portal/Portal.mjs +45 -0
- package/dist/components/portal/Portal.mjs.map +1 -0
- package/dist/components/provider/WaveProvider.cjs +2 -0
- package/dist/components/provider/WaveProvider.cjs.map +1 -0
- package/dist/components/provider/WaveProvider.mjs +51 -0
- package/dist/components/provider/WaveProvider.mjs.map +1 -0
- package/dist/components/table/DataGrid.cjs +2 -0
- package/dist/components/table/DataGrid.cjs.map +1 -0
- package/dist/components/table/DataGrid.mjs +489 -0
- package/dist/components/table/DataGrid.mjs.map +1 -0
- package/dist/components/table/Table.cjs +2 -0
- package/dist/components/table/Table.cjs.map +1 -0
- package/dist/components/table/Table.mjs +99 -0
- package/dist/components/table/Table.mjs.map +1 -0
- package/dist/components/table/useGridNavigation.cjs +2 -0
- package/dist/components/table/useGridNavigation.cjs.map +1 -0
- package/dist/components/table/useGridNavigation.mjs +336 -0
- package/dist/components/table/useGridNavigation.mjs.map +1 -0
- package/dist/components/typography/Text.cjs +2 -0
- package/dist/components/typography/Text.cjs.map +1 -0
- package/dist/components/typography/Text.mjs +38 -0
- package/dist/components/typography/Text.mjs.map +1 -0
- package/dist/hooks/useAnnounce.cjs +2 -0
- package/dist/hooks/useAnnounce.cjs.map +1 -0
- package/dist/hooks/useAnnounce.mjs +88 -0
- package/dist/hooks/useAnnounce.mjs.map +1 -0
- package/dist/hooks/useCheckedValues.cjs +2 -0
- package/dist/hooks/useCheckedValues.cjs.map +1 -0
- package/dist/hooks/useCheckedValues.mjs +102 -0
- package/dist/hooks/useCheckedValues.mjs.map +1 -0
- package/dist/hooks/useContextMenuAnchor.cjs +2 -0
- package/dist/hooks/useContextMenuAnchor.cjs.map +1 -0
- package/dist/hooks/useContextMenuAnchor.mjs +132 -0
- package/dist/hooks/useContextMenuAnchor.mjs.map +1 -0
- package/dist/hooks/useControllable.cjs +2 -0
- package/dist/hooks/useControllable.cjs.map +1 -0
- package/dist/hooks/useControllable.mjs +37 -0
- package/dist/hooks/useControllable.mjs.map +1 -0
- package/dist/hooks/useDirection.cjs +2 -0
- package/dist/hooks/useDirection.cjs.map +1 -0
- package/dist/hooks/useDirection.mjs +57 -0
- package/dist/hooks/useDirection.mjs.map +1 -0
- package/dist/hooks/useDismiss.cjs +2 -0
- package/dist/hooks/useDismiss.cjs.map +1 -0
- package/dist/hooks/useDismiss.mjs +57 -0
- package/dist/hooks/useDismiss.mjs.map +1 -0
- package/dist/hooks/useEventCallback.cjs +2 -0
- package/dist/hooks/useEventCallback.cjs.map +1 -0
- package/dist/hooks/useEventCallback.mjs +16 -0
- package/dist/hooks/useEventCallback.mjs.map +1 -0
- package/dist/hooks/useFieldControl.cjs +2 -0
- package/dist/hooks/useFieldControl.cjs.map +1 -0
- package/dist/hooks/useFieldControl.mjs +62 -0
- package/dist/hooks/useFieldControl.mjs.map +1 -0
- package/dist/hooks/useFocusTrap.cjs +2 -0
- package/dist/hooks/useFocusTrap.cjs.map +1 -0
- package/dist/hooks/useFocusTrap.mjs +266 -0
- package/dist/hooks/useFocusTrap.mjs.map +1 -0
- package/dist/hooks/useFormReset.cjs +2 -0
- package/dist/hooks/useFormReset.cjs.map +1 -0
- package/dist/hooks/useFormReset.mjs +33 -0
- package/dist/hooks/useFormReset.mjs.map +1 -0
- package/dist/hooks/useHoverIntent.cjs +2 -0
- package/dist/hooks/useHoverIntent.cjs.map +1 -0
- package/dist/hooks/useHoverIntent.mjs +201 -0
- package/dist/hooks/useHoverIntent.mjs.map +1 -0
- package/dist/hooks/useId.cjs +2 -0
- package/dist/hooks/useId.cjs.map +1 -0
- package/dist/hooks/useId.mjs +11 -0
- package/dist/hooks/useId.mjs.map +1 -0
- package/dist/hooks/useIsClient.cjs +2 -0
- package/dist/hooks/useIsClient.cjs.map +1 -0
- package/dist/hooks/useIsClient.mjs +19 -0
- package/dist/hooks/useIsClient.mjs.map +1 -0
- package/dist/hooks/useListbox.cjs +2 -0
- package/dist/hooks/useListbox.cjs.map +1 -0
- package/dist/hooks/useListbox.mjs +492 -0
- package/dist/hooks/useListbox.mjs.map +1 -0
- package/dist/hooks/useMergedRefs.cjs +2 -0
- package/dist/hooks/useMergedRefs.cjs.map +1 -0
- package/dist/hooks/useMergedRefs.mjs +57 -0
- package/dist/hooks/useMergedRefs.mjs.map +1 -0
- package/dist/hooks/useModalIsolation.cjs +2 -0
- package/dist/hooks/useModalIsolation.cjs.map +1 -0
- package/dist/hooks/useModalIsolation.mjs +87 -0
- package/dist/hooks/useModalIsolation.mjs.map +1 -0
- package/dist/hooks/useModalLayer.cjs +2 -0
- package/dist/hooks/useModalLayer.cjs.map +1 -0
- package/dist/hooks/useModalLayer.mjs +30 -0
- package/dist/hooks/useModalLayer.mjs.map +1 -0
- package/dist/hooks/usePopupPosition.cjs +2 -0
- package/dist/hooks/usePopupPosition.cjs.map +1 -0
- package/dist/hooks/usePopupPosition.mjs +117 -0
- package/dist/hooks/usePopupPosition.mjs.map +1 -0
- package/dist/hooks/usePrefersReducedMotion.cjs +2 -0
- package/dist/hooks/usePrefersReducedMotion.cjs.map +1 -0
- package/dist/hooks/usePrefersReducedMotion.mjs +24 -0
- package/dist/hooks/usePrefersReducedMotion.mjs.map +1 -0
- package/dist/hooks/usePresence.cjs +2 -0
- package/dist/hooks/usePresence.cjs.map +1 -0
- package/dist/hooks/usePresence.mjs +136 -0
- package/dist/hooks/usePresence.mjs.map +1 -0
- package/dist/hooks/usePreserveFocus.cjs +2 -0
- package/dist/hooks/usePreserveFocus.cjs.map +1 -0
- package/dist/hooks/usePreserveFocus.mjs +50 -0
- package/dist/hooks/usePreserveFocus.mjs.map +1 -0
- package/dist/hooks/useRestoreFocus.cjs +2 -0
- package/dist/hooks/useRestoreFocus.cjs.map +1 -0
- package/dist/hooks/useRestoreFocus.mjs +271 -0
- package/dist/hooks/useRestoreFocus.mjs.map +1 -0
- package/dist/hooks/useRovingTabIndex.cjs +2 -0
- package/dist/hooks/useRovingTabIndex.cjs.map +1 -0
- package/dist/hooks/useRovingTabIndex.mjs +389 -0
- package/dist/hooks/useRovingTabIndex.mjs.map +1 -0
- package/dist/hooks/useScrollLock.cjs +2 -0
- package/dist/hooks/useScrollLock.cjs.map +1 -0
- package/dist/hooks/useScrollLock.mjs +71 -0
- package/dist/hooks/useScrollLock.mjs.map +1 -0
- package/dist/hooks/useTriggerElement.cjs +2 -0
- package/dist/hooks/useTriggerElement.cjs.map +1 -0
- package/dist/hooks/useTriggerElement.mjs +126 -0
- package/dist/hooks/useTriggerElement.mjs.map +1 -0
- package/dist/hooks/useTypeahead.cjs +2 -0
- package/dist/hooks/useTypeahead.cjs.map +1 -0
- package/dist/hooks/useTypeahead.mjs +38 -0
- package/dist/hooks/useTypeahead.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +9099 -0
- package/dist/index.d.ts +7688 -1027
- package/dist/index.mjs +91 -5695
- package/dist/lib/aria.cjs +2 -0
- package/dist/lib/aria.cjs.map +1 -0
- package/dist/lib/aria.mjs +29 -0
- package/dist/lib/aria.mjs.map +1 -0
- package/dist/lib/children.cjs +2 -0
- package/dist/lib/children.cjs.map +1 -0
- package/dist/lib/children.mjs +37 -0
- package/dist/lib/children.mjs.map +1 -0
- package/dist/lib/cn.cjs +2 -0
- package/dist/lib/cn.cjs.map +1 -0
- package/dist/lib/cn.mjs +64 -0
- package/dist/lib/cn.mjs.map +1 -0
- package/dist/lib/composeEventHandlers.cjs +2 -0
- package/dist/lib/composeEventHandlers.cjs.map +1 -0
- package/dist/lib/composeEventHandlers.mjs +11 -0
- package/dist/lib/composeEventHandlers.mjs.map +1 -0
- package/dist/lib/dev.cjs +2 -0
- package/dist/lib/dev.cjs.map +1 -0
- package/dist/lib/dev.mjs +42 -0
- package/dist/lib/dev.mjs.map +1 -0
- package/dist/lib/direction.cjs +2 -0
- package/dist/lib/direction.cjs.map +1 -0
- package/dist/lib/direction.mjs +35 -0
- package/dist/lib/direction.mjs.map +1 -0
- package/dist/lib/events.cjs +2 -0
- package/dist/lib/events.cjs.map +1 -0
- package/dist/lib/events.mjs +31 -0
- package/dist/lib/events.mjs.map +1 -0
- package/dist/lib/focus.cjs +2 -0
- package/dist/lib/focus.cjs.map +1 -0
- package/dist/lib/focus.mjs +119 -0
- package/dist/lib/focus.mjs.map +1 -0
- package/dist/lib/globalRegistry.cjs +2 -0
- package/dist/lib/globalRegistry.cjs.map +1 -0
- package/dist/lib/globalRegistry.mjs +13 -0
- package/dist/lib/globalRegistry.mjs.map +1 -0
- package/dist/lib/icons.cjs +2 -0
- package/dist/lib/icons.cjs.map +1 -0
- package/dist/lib/icons.mjs +254 -0
- package/dist/lib/icons.mjs.map +1 -0
- package/dist/lib/labelInName.cjs +2 -0
- package/dist/lib/labelInName.cjs.map +1 -0
- package/dist/lib/labelInName.mjs +54 -0
- package/dist/lib/labelInName.mjs.map +1 -0
- package/dist/lib/layers.cjs +2 -0
- package/dist/lib/layers.cjs.map +1 -0
- package/dist/lib/layers.mjs +224 -0
- package/dist/lib/layers.mjs.map +1 -0
- package/dist/lib/mergeProps.cjs +2 -0
- package/dist/lib/mergeProps.cjs.map +1 -0
- package/dist/lib/mergeProps.mjs +21 -0
- package/dist/lib/mergeProps.mjs.map +1 -0
- package/dist/lib/mergeRefs.cjs +2 -0
- package/dist/lib/mergeRefs.cjs.map +1 -0
- package/dist/lib/mergeRefs.mjs +29 -0
- package/dist/lib/mergeRefs.mjs.map +1 -0
- package/dist/lib/renderTrigger.cjs +2 -0
- package/dist/lib/renderTrigger.cjs.map +1 -0
- package/dist/lib/renderTrigger.mjs +29 -0
- package/dist/lib/renderTrigger.mjs.map +1 -0
- package/dist/lib/slot.cjs +2 -0
- package/dist/lib/slot.cjs.map +1 -0
- package/dist/lib/slot.mjs +122 -0
- package/dist/lib/slot.mjs.map +1 -0
- package/dist/lib/styles.cjs +2 -0
- package/dist/lib/styles.cjs.map +1 -0
- package/dist/lib/styles.mjs +16 -0
- package/dist/lib/styles.mjs.map +1 -0
- package/dist/lib/theme.cjs +2 -0
- package/dist/lib/theme.cjs.map +1 -0
- package/dist/lib/theme.mjs +16 -0
- package/dist/lib/theme.mjs.map +1 -0
- package/dist/preflight.css +2 -0
- package/dist/styles.css +2 -0
- package/package.json +149 -100
- package/src/styles/animations.css +7 -39
- package/src/styles/base.css +103 -0
- package/src/styles/globals.css +10 -22
- package/src/styles/legacy-tokens.css +184 -0
- package/src/styles/preflight.css +7 -0
- package/src/styles/styles.css +32 -0
- package/src/styles/tailwind.css +17 -0
- package/src/styles/tokens.css +561 -221
- package/src/styles/variants.css +29 -0
package/README.md
CHANGED
|
@@ -1,261 +1,1309 @@
|
|
|
1
|
-
# waveui
|
|
1
|
+
# @mortenbrudvik/waveui
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A React component library inspired by the Fluent UI 2 design language: 65 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
|
+
|
|
5
|
+
> This is **not** Microsoft's official `@fluentui/react-components` package.
|
|
6
|
+
|
|
7
|
+
- [Requirements](#requirements) · [Installation](#installation) · [Quick start](#quick-start)
|
|
8
|
+
- [Styles](#styles) · [Theming](#theming) · [Global effects](#global-effects)
|
|
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.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- React 19 (`react` and `react-dom` `^19.0.0`)
|
|
15
|
+
- Tailwind CSS `^4.1.0` only if you use the [Tailwind path](#tailwind-css-4); the precompiled stylesheet needs no Tailwind
|
|
16
|
+
- Browsers: Chrome/Edge 111+, Safari 16.4+, Firefox 128+ (the CSS uses `@property`, `color-mix()`, `:where()` and `:has()`)
|
|
17
|
+
- Node.js 22.12+ (`engines`)
|
|
4
18
|
|
|
5
19
|
## Installation
|
|
6
20
|
|
|
7
21
|
```bash
|
|
8
|
-
npm install waveui
|
|
22
|
+
npm install @mortenbrudvik/waveui
|
|
9
23
|
```
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
The npm package has always been `@mortenbrudvik/waveui`. Before 0.5 the repository's `package.json` used the name `waveui`, and the 0.4 guide imported from `'waveui'`; import from `@mortenbrudvik/waveui` instead.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Pick **one** styling path.
|
|
30
|
+
|
|
31
|
+
### Without Tailwind (precompiled CSS)
|
|
32
|
+
|
|
33
|
+
Import the precompiled stylesheet once and wrap your app in `WaveProvider`:
|
|
12
34
|
|
|
13
35
|
```tsx
|
|
14
|
-
import
|
|
15
|
-
import 'waveui
|
|
36
|
+
import '@mortenbrudvik/waveui/styles';
|
|
37
|
+
import { Button, WaveProvider } from '@mortenbrudvik/waveui';
|
|
16
38
|
|
|
17
|
-
function App() {
|
|
39
|
+
export function App() {
|
|
18
40
|
return (
|
|
19
41
|
<WaveProvider theme="light">
|
|
20
|
-
<Button appearance="primary" onClick={() =>
|
|
21
|
-
Get
|
|
42
|
+
<Button appearance="primary" onClick={() => console.log('clicked')}>
|
|
43
|
+
Get started
|
|
22
44
|
</Button>
|
|
23
45
|
</WaveProvider>
|
|
24
46
|
);
|
|
25
47
|
}
|
|
26
48
|
```
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
50
|
+
`WaveProvider` is **required** on this path: the stylesheet scopes its base styles (font, colors, box sizing) and its reset of native elements (`button`, `input`, lists, headings, …) to the provider root and to portaled overlays. Components rendered outside a provider are not reset.
|
|
51
|
+
|
|
52
|
+
### Tailwind CSS 4
|
|
53
|
+
|
|
54
|
+
Add Wave to your Tailwind entry instead:
|
|
55
|
+
|
|
56
|
+
```css
|
|
57
|
+
/* app.css */
|
|
58
|
+
@import 'tailwindcss';
|
|
59
|
+
@import '@mortenbrudvik/waveui/tailwind';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This adds Wave's tokens to your theme (`layer(theme)`), its scoped base styles to `layer(base)`, and an `@source` for the compiled components, so Tailwind generates every class they use. **Never import `@mortenbrudvik/waveui/styles` as well** in a Tailwind app: the two would define the same utilities twice. The one exception is an app whose own Tailwind build uses a prefix: it imports `./styles` **instead of** `./tailwind`, into a cascade layer between Tailwind's `base` and `utilities` (the CSS is in [Global effects](#global-effects)), and its own utilities cannot collide with Wave's.
|
|
63
|
+
|
|
64
|
+
Wrap the app in `WaveProvider` here too. It selects the theme and direction, paints the themed background, text color and font, and passes the theme to portaled overlays (dialogs, menus, popovers, toasts).
|
|
65
|
+
|
|
66
|
+
## Styles
|
|
67
|
+
|
|
68
|
+
| Import | What it is |
|
|
69
|
+
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `@mortenbrudvik/waveui/styles` (also `/styles.css`) | Precompiled, **unlayered** CSS: tokens, the provider-scoped base styles and every utility the components use (the only layered block is Tailwind's `--tw-*` fallback, see [Global effects](#global-effects)). No Preflight. For apps without Tailwind, and, imported into a layer, for apps with a prefixed Tailwind build. |
|
|
71
|
+
| `@mortenbrudvik/waveui/tailwind` (also `/tailwind.css`) | Tailwind 4 source entry: tokens in `layer(theme)`, base in `layer(base)`, `@source` for the components, the `wave-rtl:` variant. Import it after `@import 'tailwindcss'`. |
|
|
72
|
+
| `@mortenbrudvik/waveui/tokens` (also `/tokens.css`) | The token source alone (`--wave-*` variables, theme classes and the `@theme inline` mapping), without base styles or `@source`. For custom Tailwind setups, together with `./variants.css`; `./tailwind` is the complete Tailwind entry. |
|
|
73
|
+
| `@mortenbrudvik/waveui/variants` (also `/variants.css`) | Wave's `wave-rtl:` direction variant. A custom Tailwind setup built on `./tokens` imports it too, with its own `@source` for the package's `dist` (see below). |
|
|
74
|
+
| `@mortenbrudvik/waveui/preflight.css` | Opt-in Tailwind Preflight for the **whole page** (unlayered). Wave does not need it. |
|
|
75
|
+
| `@mortenbrudvik/waveui/legacy-tokens.css` | Deprecated 0.4 compatibility layer (see [Theming](#theming)). |
|
|
76
|
+
|
|
77
|
+
**Cascade.** `./styles` is unlayered on purpose: unlayered CSS beats any layered CSS, and Wave's class selectors beat element-level resets such as `*{padding:0}` or `button{background:none}`, so ordinary app resets cannot strip the components. To put Wave inside your own layer order, import it into a layer yourself. Unlayered app CSS (including resets) then wins over it:
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
@import '@mortenbrudvik/waveui/styles.css' layer(wave);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The other side of this: **layered** app CSS, such as a Tailwind build's utilities, loses to the unlayered `./styles` whatever its specificity, so inside `WaveProvider` Wave's zero-specificity base styles override it. If your own CSS is layered, import Wave into a layer ordered before yours; [Global effects](#global-effects) has the recipe for an app with a prefixed Tailwind build.
|
|
84
|
+
|
|
85
|
+
**Custom Tailwind setups.** An app that builds its own Tailwind entry from `./tokens` instead of `./tailwind` brings its own base styles, and must import the variant and scan the package itself, or Tailwind silently drops the components' mirroring classes (the Switch thumb, the chevrons, the Select arrow):
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
@import 'tailwindcss';
|
|
89
|
+
@import '@mortenbrudvik/waveui/tokens' layer(theme);
|
|
90
|
+
@import '@mortenbrudvik/waveui/variants.css';
|
|
91
|
+
@source '../node_modules/@mortenbrudvik/waveui/dist';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Preflight.** 0.4 applied Tailwind's Preflight to the whole page; 0.5 does not. If you want it without Tailwind, import it before the Wave styles:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import '@mortenbrudvik/waveui/preflight.css';
|
|
98
|
+
import '@mortenbrudvik/waveui/styles';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Theming
|
|
102
|
+
|
|
103
|
+
### Themes and direction
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { WaveProvider } from '@mortenbrudvik/waveui';
|
|
107
|
+
|
|
108
|
+
export function Shell({ children }: { children: React.ReactNode }) {
|
|
109
|
+
return (
|
|
110
|
+
<WaveProvider theme="dark" dir="rtl">
|
|
111
|
+
{children}
|
|
112
|
+
{/* Providers nest in any order: a light panel inside a dark app works. */}
|
|
113
|
+
<WaveProvider theme="light">{children}</WaveProvider>
|
|
114
|
+
</WaveProvider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `theme`: `'light'` (default), `'dark'` or `'high-contrast'`. `dir`: `'ltr'` (default) or `'rtl'`. `portalContainer`: where overlays render (default `document.body`).
|
|
120
|
+
- A nested provider inherits every prop it omits (`theme`, `dir`, `portalContainer`) from the enclosing provider, so the light panel above stays right-to-left. `portalContainer={null}` sends a nested subtree's overlays to `document.body`.
|
|
121
|
+
- The provider renders `<div class="wave-root wave-<theme>" dir data-wave-theme>` with `bg-background text-foreground font-wave text-body-1`; a `className` you pass wins.
|
|
122
|
+
- Portaled overlays (Dialog, Drawer, Popover, Menu, listboxes, Tooltip, Toaster) render in a `wave-portal` wrapper that carries the provider's theme classes, direction and font.
|
|
123
|
+
- `useWaveTheme()` returns `{ theme, dir, themeClassName, portalContainer, hasProvider }`.
|
|
124
|
+
|
|
125
|
+
**Theme classes.** The tokens are selected by `wave-light` (also `:root`), `wave-dark` and `wave-high-contrast`; `getThemeClassName(theme)` returns them. For dark and high contrast the provider also emits the deprecated 0.4 classes `dark` and `high-contrast`, so 0.4 `.dark` selectors in your CSS and class-based `dark:` variants (`@custom-variant dark (&:where(.dark, .dark *));`) keep working. Tailwind 4's default `dark:` variant follows `prefers-color-scheme`, not a class, and Wave does not define a class-based one. Wave's CSS still honours the two legacy classes, but they set only `--wave-*` variables. `color-scheme` is set on the three `wave-*` theme classes only, never on `:root`, so a page without a provider keeps its own scrollbar and form-control scheme.
|
|
126
|
+
|
|
127
|
+
### Customizing tokens
|
|
128
|
+
|
|
129
|
+
Every color is a runtime CSS variable prefixed `--wave-`. The Tailwind color utilities use the same name without the prefix: `--wave-muted-foreground` is `text-muted-foreground`, `--wave-primary` is `bg-primary`, `border-primary`, `ring-primary`, and so on.
|
|
130
|
+
|
|
131
|
+
Override a token for one theme by targeting its theme selector. With `./styles`, load your CSS after Wave's (the selectors have the same specificity); on the Tailwind path any unlayered rule wins over Wave's `theme` layer.
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
/* All light surfaces */
|
|
135
|
+
:root,
|
|
136
|
+
.wave-light {
|
|
137
|
+
--wave-muted-foreground: #5c5c5c;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Dark theme only */
|
|
141
|
+
.wave-dark {
|
|
142
|
+
--wave-primary: #7fb8f7;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Re-branding.** The primary, hover, pressed, focus-ring and selected colors of the light and dark themes are built from the brand ramp (`--wave-brand-10` … `--wave-brand-160`, declared on `:root`), so overriding the ramp re-themes them: light uses brand-80/70/40 for primary/hover/pressed, brand-160 and brand-60 for selected surfaces and text; dark uses brand-110/120/90 and brand-20/110. High contrast uses fixed colors. `--wave-info` is a fixed color and does not follow the ramp. Check the contrast of your brand against `--wave-primary-foreground` (white in light, black in dark).
|
|
147
|
+
|
|
148
|
+
```css
|
|
149
|
+
:root {
|
|
150
|
+
--wave-brand-40: #3b1a6b;
|
|
151
|
+
--wave-brand-60: #53258f;
|
|
152
|
+
--wave-brand-70: #5f2aa3;
|
|
153
|
+
--wave-brand-80: #6b2fb3;
|
|
154
|
+
--wave-brand-160: #f3edfb;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**0.4 variable names.** The 0.4 ramp names (`--brand-80`, `--grey-38`, …) are still read as fallbacks, so overrides of them keep working without any change. Wave no longer defines them, though: app CSS that **reads** them (`var(--brand-80)`, `var(--grey-14)`) gets nothing. The 0.4 **semantic** names (`--primary`, `--background`, `--border`, `--ring`, …) are neither read nor defined, so overrides of them have no effect and reads such as `outline: 2px solid var(--ring)` resolve to nothing. Rename those overrides and reads to `--wave-*` (preferred), or load the deprecated compatibility layer after the Wave styles, which defines the 0.4 names again:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import '@mortenbrudvik/waveui/styles';
|
|
162
|
+
import '@mortenbrudvik/waveui/legacy-tokens.css';
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Never load `legacy-tokens.css` in an app that defines shadcn/ui-style variables (`--primary`, `--background`, …): those names collide, which is why 0.5 prefixed them.
|
|
166
|
+
|
|
167
|
+
### Color tokens
|
|
168
|
+
|
|
169
|
+
Generated from `src/styles/tokens.css`. A value followed by a ramp or token name is a reference (`var(--wave-brand-80)`); the hex is its default.
|
|
170
|
+
|
|
171
|
+
<details>
|
|
172
|
+
<summary>All 61 color tokens per theme</summary>
|
|
173
|
+
|
|
174
|
+
| Token (`--wave-…`) | Light | Dark | High contrast |
|
|
175
|
+
| ------------------------- | ------------------------------ | ------------------------------ | ------------------------------ |
|
|
176
|
+
| `background` | `#ffffff` | `#292929` | `#000000` |
|
|
177
|
+
| `foreground` | `#242424` | `#ffffff` | `#ffffff` |
|
|
178
|
+
| `card` | `#fafafa` | `#333333` | `#000000` |
|
|
179
|
+
| `secondary` | `#f5f5f5` | `#333333` | `#000000` |
|
|
180
|
+
| `muted` | `#f0f0f0` | `#383838` | `#1a1a1a` |
|
|
181
|
+
| `muted-foreground` | `#616161` | `#adadad` | `#ffffff` |
|
|
182
|
+
| `primary` | `#0f6cbd` (brand-80) | `#62abf5` (brand-110) | `#1aebff` |
|
|
183
|
+
| `primary-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
184
|
+
| `primary-hover` | `#115ea3` (brand-70) | `#77b7f7` (brand-120) | `#6ef3ff` |
|
|
185
|
+
| `primary-pressed` | `#0c3b5e` (brand-40) | `#2886de` (brand-90) | `#00c4d6` |
|
|
186
|
+
| `destructive` | `#c50f1f` | `#f48a94` | `#ff6e6e` |
|
|
187
|
+
| `destructive-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
188
|
+
| `error` | `#c50f1f` | `#f48a94` | `#ff6e6e` |
|
|
189
|
+
| `error-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
190
|
+
| `subtle` | `transparent` | `transparent` | `transparent` |
|
|
191
|
+
| `subtle-hover` | `#f5f5f5` | `#333333` | `#1f1f1f` |
|
|
192
|
+
| `subtle-pressed` | `#ebebeb` | `#2e2e2e` | `#333333` |
|
|
193
|
+
| `subtle-selected` | `#ebebeb` | `#383838` | `#333333` |
|
|
194
|
+
| `selected` | `#ebf3fc` (brand-160) | `#082338` (brand-20) | `#003a40` |
|
|
195
|
+
| `selected-foreground` | `#0f548c` (brand-60) | `#62abf5` (brand-110) | `#ffffff` |
|
|
196
|
+
| `border` | `#e0e0e0` | `#666666` | `#ffffff` |
|
|
197
|
+
| `stroke` | `#d1d1d1` | `#666666` | `#ffffff` |
|
|
198
|
+
| `stroke-hover` | `#c7c7c7` | `#757575` | `#ffffff` |
|
|
199
|
+
| `stroke-accessible` | `#616161` | `#adadad` | `#ffffff` |
|
|
200
|
+
| `input` | `#d1d1d1` | `#666666` | `#ffffff` |
|
|
201
|
+
| `ring` | `#0f6cbd` (brand-80) | `#479ef5` (brand-100) | `#ffff00` |
|
|
202
|
+
| `success` | `#107c10` | `#5db55d` | `#3ff23f` |
|
|
203
|
+
| `success-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
204
|
+
| `success-tint` | `#f1faf1` | `#052505` | `#000000` |
|
|
205
|
+
| `success-tint-foreground` | `#0e700e` | `#54b054` | `#3ff23f` |
|
|
206
|
+
| `warning` | `#fde300` | `#fde300` | `#ffff00` |
|
|
207
|
+
| `warning-foreground` | `#242424` | `#000000` | `#000000` |
|
|
208
|
+
| `warning-tint` | `#fffbe6` | `#463100` | `#000000` |
|
|
209
|
+
| `warning-tint-foreground` | `#6d5b00` | `#fde300` | `#ffff00` |
|
|
210
|
+
| `error-tint` | `#fdf3f4` | `#3b0509` | `#000000` |
|
|
211
|
+
| `error-tint-foreground` | `#b10e1c` | `#f48a94` | `#ff6060` |
|
|
212
|
+
| `severe` | `#da3b01` | `#e97548` | `#ff8c00` |
|
|
213
|
+
| `severe-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
214
|
+
| `severe-tint` | `#fdf6f3` | `#411200` | `#000000` |
|
|
215
|
+
| `severe-tint-foreground` | `#a52c00` | `#e97548` | `#ff8c00` |
|
|
216
|
+
| `info` | `#0f6cbd` | `#479ef5` | `#1aebff` |
|
|
217
|
+
| `info-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
218
|
+
| `info-tint` | `#ebf3fc` | `#082338` | `#000000` |
|
|
219
|
+
| `info-tint-foreground` | `#0f548c` | `#62abf5` | `#1aebff` |
|
|
220
|
+
| `inverted` | `#292929` | `#ffffff` | `#000000` |
|
|
221
|
+
| `inverted-foreground` | `#ffffff` | `#242424` | `#ffffff` |
|
|
222
|
+
| `inverted-border` | `transparent` | `transparent` | `#ffffff` |
|
|
223
|
+
| `track` | `#e0e0e0` | `#3d3d3d` | `#4d4d4d` |
|
|
224
|
+
| `skeleton` | `#e0e0e0` | `#3d3d3d` | `#333333` |
|
|
225
|
+
| `rating` | `#b86e00` | `#f7b538` | `#ffff00` |
|
|
226
|
+
| `presence-available` | `#107c10` | `#54b054` | `#3ff23f` |
|
|
227
|
+
| `presence-busy` | `#c50f1f` | `#f48a94` | `#ff6060` |
|
|
228
|
+
| `presence-away` | `#a67c00` | `#f7b538` | `#ffff00` |
|
|
229
|
+
| `presence-offline` | `#616161` | `#adadad` | `#ffffff` |
|
|
230
|
+
| `presence-oof` | `#b4009e` | `#d696c8` | `#ff80ff` |
|
|
231
|
+
| `presence-glyph` | `#ffffff` | `#000000` | `#000000` |
|
|
232
|
+
| `backdrop` | `rgb(0 0 0 / 0.4)` | `rgb(0 0 0 / 0.5)` | `rgb(0 0 0 / 0.8)` |
|
|
233
|
+
| `card-foreground` | `#242424` (foreground) | `#ffffff` (foreground) | `#ffffff` (foreground) |
|
|
234
|
+
| `secondary-foreground` | `#242424` (foreground) | `#ffffff` (foreground) | `#ffffff` (foreground) |
|
|
235
|
+
| `accent` | `#0f6cbd` (primary) | `#62abf5` (primary) | `#1aebff` (primary) |
|
|
236
|
+
| `accent-foreground` | `#ffffff` (primary-foreground) | `#000000` (primary-foreground) | `#000000` (primary-foreground) |
|
|
237
|
+
|
|
238
|
+
</details>
|
|
239
|
+
|
|
240
|
+
Theme-independent variables on `:root`: `--wave-brand-10` … `--wave-brand-160` (also utilities such as `bg-brand-80`), `--wave-grey-2` … `--wave-grey-98`, `--wave-font-family` (utility `font-wave`), the stacking layers `--wave-z-overlay` (1000), `--wave-z-toast` (1100) and `--wave-z-tooltip` (1200), and the motion durations `--wave-duration-*` and curves `--wave-curve-*` (see [Enter and exit motion](#enter-and-exit-motion)).
|
|
241
|
+
|
|
242
|
+
Contrast of every text and non-text pair is checked per theme by `src/styles/__tests__/tokens.test.ts`. One known limit: the `warning` fill (`#fde300` in light) is a background and accent color only; use `warning-tint-foreground` for warning text and icons.
|
|
243
|
+
|
|
244
|
+
Other Wave theme values: the type ramp `text-caption-2`, `text-caption-1`, `text-body-1`, `text-body-2`, `text-subtitle-2`, `text-subtitle-1`, `text-title-3`, `text-title-2`, `text-title-1`, `text-large-title`, `text-display`; shadows `shadow-2`, `shadow-4`, `shadow-8`, `shadow-16`, `shadow-28`, `shadow-64`; animations `animate-wave-spin`, `animate-wave-spin-slow`, `animate-wave-pulse`, `animate-wave-indeterminate`, `animate-wave-indeterminate-rtl`; on the Tailwind path, the motion utilities `duration-wave-ultra-fast` … `duration-wave-ultra-slow` and `ease-wave-accelerate-max` … `ease-wave-linear`. Border radius uses Tailwind's default scale (Wave no longer overrides it).
|
|
245
|
+
|
|
246
|
+
## Global effects
|
|
247
|
+
|
|
248
|
+
What adding Wave changes outside its own components:
|
|
249
|
+
|
|
250
|
+
- **Variables.** `--wave-*` custom properties on `:root` and on the theme classes (`.wave-light`, `.wave-dark`, `.wave-high-contrast`, and the deprecated `.dark` and `.high-contrast`). No unprefixed variables are defined.
|
|
251
|
+
- **Base styles and reset, provider-scoped.** They apply inside `.wave-root` (WaveProvider) and `.wave-portal` (overlay wrappers) only, and your own content inside the provider receives them too:
|
|
252
|
+
- Two root rules are plain class selectors (specificity 0,1,0): `.wave-root, .wave-portal` set the font family, text color, 14px font size and 20px line height, and `.wave-root` sets the background color. Override them with a class selector loaded after Wave's CSS, or with the provider's `className`.
|
|
253
|
+
- Every other selector is wrapped in `:where()` (zero specificity), so any unlayered style of yours wins, except the descendant `::before`/`::after` selectors of the box-sizing rule. Pseudo-elements cannot sit inside `:where()` (the selector would be dropped), so those two are specificity (0, 0, 1): an author `::before` or `::after` of the same specificity wins or loses by source order, and any more specific one (a class, attribute, ID or type selector before the pseudo-element) wins. Layered CSS, such as Tailwind utilities, loses to the unlayered `./styles` whatever its specificity, unless you import `./styles` into a layer (see [Cascade](#styles) and the workarounds below). These rules set `box-sizing: border-box` and `border-color: var(--wave-border)` on every descendant (and its `::before`/`::after`), reset the native elements Wave renders (`button`, `input`, `select`, `textarea`, lists, headings, `p`, `figure`, `blockquote`, `dl`, `dd`, `fieldset`, `legend`, `table`, `hr`), set `vertical-align: middle` on `img`, `svg` and `video`, and give `button` and `[role=button]` a pointer cursor.
|
|
254
|
+
- One rule is `!important`, as in Preflight: an element with the `hidden` attribute gets `display: none` (except `hidden="until-found"`), so a display utility (`flex`, `inline-flex`, …) on a component root or on your own element inside the provider cannot keep it visible. Like every rule here it is scoped, so elsewhere `hidden` on a Wave component that sets a display utility relies on Preflight: outside `WaveProvider` (and the overlays' portal wrappers) on the precompiled `./styles` path it takes effect only when you import `preflight.css`, and a custom Tailwind setup on `./tokens` loads no Wave base styles at all, so there it relies on Tailwind's Preflight (part of `@import 'tailwindcss'`); a setup without Preflight adds `[hidden]:where(:not([hidden='until-found'])) { display: none !important; }` itself.
|
|
255
|
+
- This is not a subset of Tailwind's Preflight: Preflight leaves borders `currentColor` and buttons with the default cursor, while Wave uses its border token and a pointer.
|
|
256
|
+
- On the Tailwind path all of them sit in `layer(base)`, so your utilities and unlayered CSS win over them.
|
|
257
|
+
|
|
258
|
+
There are no `html` or `body` rules, no global reduced-motion override (each component handles reduced motion itself), and no Preflight unless you import it. The only global rule is Tailwind's `@layer properties` fallback in `./styles`: in browsers without `@property` support (detected by an `@supports` query) it sets the initial values of Tailwind's `--tw-*` custom properties on `*`, `::before`, `::after` and `::backdrop`, and nothing else. It is also the only layered block in that otherwise unlayered file.
|
|
259
|
+
|
|
260
|
+
- **Utility classes (`./styles`).** The precompiled stylesheet contains the Tailwind utilities the components use (`.flex`, `.p-4`, `.bg-primary`, …) as ordinary class selectors, unlayered, plus Tailwind's `@property --tw-*` registrations. An element of yours with the same class name gets the same style. It contains no other utility: generic names that Tailwind would generate from words in Wave's comments and strings (`.container`, `.collapse`, `.table`, `.shadow`, `.ring`, …) are excluded, so they never restyle your own markup.
|
|
261
|
+
- **Theme and variant (`./tailwind`).** Wave adds its color names, `font-wave`, the type ramp, the shadow scale, the `animate-wave-*` animations and the `duration-wave-*` and `ease-wave-*` motion tokens to your Tailwind theme, and defines the `wave-rtl:` variant the components use to mirror glyphs (`./variants` defines it alone, for custom setups on `./tokens`). It matches by the element's own direction (`:dir(rtl)`, with a `[dir=rtl]` fallback for browsers without `:dir()`), so unlike Tailwind's `rtl:` it does not apply inside an LTR subtree of an RTL page; your build can use it too. Tailwind's `rtl:` is unchanged.
|
|
262
|
+
- The variant writes the direction as `:nth-child(n of :dir(rtl))`, which matches the same elements, so your app's CSS minifier keeps it. Vite's default minifier, Lightning CSS, rewrites a bare `:dir()` to a `:lang()` list for targets below Chrome 120, which never matches a page that sets `dir` without a right-to-left `lang`. Your own `:dir()` rules still go through that rewrite in a Vite build (Tailwind's `rtl:` keeps working through its `[dir=rtl]` alternatives). To keep them, set `build.cssTarget` to browsers with `:dir()` (for example `['chrome120', 'edge120', 'firefox128', 'safari16.4']`), or exclude the feature: `css: { lightningcss: { exclude: Features.DirSelector } }` with `import { Features } from 'lightningcss'`.
|
|
263
|
+
- postcss-preset-env at its default stage 2 enables `dir-pseudo-class`, which rewrites `:dir()` in a way that breaks Wave's mirroring (the 0.5 form of the variant too). Turn it off with `features: { 'dir-pseudo-class': false }`. Next.js's built-in PostCSS defaults (stage 3) are not affected.
|
|
264
|
+
|
|
265
|
+
**Colliding utility names.** Wave's Tailwind color names are unprefixed, because they are the utility vocabulary 0.4 documented and that consumer code uses in `className` overrides. In a shared Tailwind build, or next to another stylesheet that defines the same class names, each of the following names resolves to a single definition in every utility that takes a color (`bg-`, `text-`, `border-`, `ring-`, `outline-`, `fill-`, `stroke-`, `accent-`, `caret-`, `decoration-`, `divide-`, `placeholder-`, `shadow-`, `from-`/`via-`/`to-`, …):
|
|
266
|
+
|
|
267
|
+
- Shared with shadcn/ui: `background`, `foreground`, `card`, `card-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`.
|
|
268
|
+
- Wave's other names: `primary-hover`, `primary-pressed`, `error`, `error-foreground`, `subtle`, `subtle-hover`, `subtle-pressed`, `subtle-selected`, `selected`, `selected-foreground`, `stroke`, `stroke-hover`, `stroke-accessible`, `success`, `success-foreground`, `success-tint`, `success-tint-foreground`, `warning`, `warning-foreground`, `warning-tint`, `warning-tint-foreground`, `error-tint`, `error-tint-foreground`, `severe`, `severe-foreground`, `severe-tint`, `severe-tint-foreground`, `info`, `info-foreground`, `info-tint`, `info-tint-foreground`, `inverted`, `inverted-foreground`, `inverted-border`, `track`, `skeleton`, `rating`, `presence-available`, `presence-busy`, `presence-away`, `presence-offline`, `presence-oof`, `presence-glyph`, `backdrop`, `brand-10` … `brand-160`.
|
|
269
|
+
|
|
270
|
+
Workarounds when your app has its own design system:
|
|
271
|
+
|
|
272
|
+
- Build your own Tailwind with a prefix, so your utilities cannot collide (`prefix(tw)` makes them `tw:bg-primary`), and import Wave's precompiled `./styles` instead of `./tailwind`, into a layer between Tailwind's `base` and `utilities`. The `@layer` statement comes first:
|
|
273
|
+
|
|
274
|
+
```css
|
|
275
|
+
/* app.css */
|
|
276
|
+
@layer theme, base, wave, components, utilities;
|
|
277
|
+
@import 'tailwindcss' prefix(tw);
|
|
278
|
+
@import '@mortenbrudvik/waveui/styles.css' layer(wave);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Do not import `./styles` unlayered (from JavaScript, or without `layer(wave)`) as well. Unlayered, it beats every layered rule, so inside `WaveProvider` its base styles would override your utilities: `tw:text-2xl` on a heading stays 14px, `tw:mb-4` stays 0, `tw:border-red-500` gets Wave's border color, `tw:p-4` and `tw:bg-red-500` on a button have no effect, and `tw:list-disc` stays `none`; a `className="tw:bg-red-500"` on a Wave `Button` loses to its `bg-primary`. In the `wave` layer your utilities win over Wave's base styles and over Wave's own classes (so that `className` works), Wave's styles still win over Tailwind's Preflight (`base`), and Wave's components look the same.
|
|
282
|
+
|
|
283
|
+
- Never import `legacy-tokens.css` in an app that defines shadcn/ui-style variables.
|
|
284
|
+
|
|
285
|
+
Namespaced Wave utilities are being considered for 1.0.
|
|
286
|
+
|
|
287
|
+
## React Server Components
|
|
288
|
+
|
|
289
|
+
- Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui` and render Wave components with serializable props.
|
|
290
|
+
- 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.
|
|
291
|
+
- 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 and Dropdown, DataGrid parts, Drawer and Menu triggers, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, Nav, TabList and Tree items, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
|
|
292
|
+
- Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component.
|
|
293
|
+
- A `Popover`, `Dialog`, `Drawer`, 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.
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
// app/layout.tsx (Server Component)
|
|
297
|
+
import '@mortenbrudvik/waveui/styles';
|
|
298
|
+
import { WaveProvider } from '@mortenbrudvik/waveui';
|
|
299
|
+
|
|
300
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
301
|
+
return (
|
|
302
|
+
<html lang="en">
|
|
303
|
+
<body>
|
|
304
|
+
<WaveProvider theme="light">{children}</WaveProvider>
|
|
305
|
+
</body>
|
|
306
|
+
</html>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// app/page.tsx (Server Component): flat names, serializable props only
|
|
313
|
+
import {
|
|
314
|
+
Card,
|
|
315
|
+
CardBody,
|
|
316
|
+
CardHeader,
|
|
317
|
+
Dialog,
|
|
318
|
+
DialogContent,
|
|
319
|
+
DialogTrigger,
|
|
320
|
+
Button,
|
|
321
|
+
} from '@mortenbrudvik/waveui';
|
|
322
|
+
|
|
323
|
+
export default function Page() {
|
|
324
|
+
return (
|
|
325
|
+
<Card>
|
|
326
|
+
<CardHeader title="Usage" subtitle="This month" />
|
|
327
|
+
<CardBody>
|
|
328
|
+
<Dialog>
|
|
329
|
+
<DialogTrigger>
|
|
330
|
+
<Button>Details</Button>
|
|
331
|
+
</DialogTrigger>
|
|
332
|
+
<DialogContent title="Usage details">
|
|
333
|
+
Rendered on the server, opened on the client.
|
|
334
|
+
</DialogContent>
|
|
335
|
+
</Dialog>
|
|
336
|
+
</CardBody>
|
|
337
|
+
</Card>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
// app/save-button.tsx (client component): handlers and dotted names are fine here
|
|
344
|
+
'use client';
|
|
345
|
+
import { Button, Toaster, useToastController } from '@mortenbrudvik/waveui';
|
|
346
|
+
|
|
347
|
+
function SaveButton() {
|
|
348
|
+
const { dispatchToast } = useToastController();
|
|
349
|
+
return <Button onClick={() => dispatchToast({ status: 'success', title: 'Saved' })}>Save</Button>;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function SaveArea() {
|
|
353
|
+
return (
|
|
354
|
+
<Toaster>
|
|
355
|
+
<SaveButton />
|
|
356
|
+
</Toaster>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Compound | Flat names |
|
|
362
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
363
|
+
| `Accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionPanel` |
|
|
364
|
+
| `Breadcrumb` | `BreadcrumbItem` |
|
|
365
|
+
| `Card` | `CardHeader`, `CardBody`, `CardFooter` |
|
|
366
|
+
| `Carousel` | `CarouselItem` |
|
|
367
|
+
| `Combobox` | `ComboboxOption`, `ComboboxOptionGroup` (also `Option`, `OptionGroup`) |
|
|
368
|
+
| `DataGrid` | `DataGridHeader`, `DataGridHeaderCell`, `DataGridBody`, `DataGridRow`, `DataGridCell` |
|
|
369
|
+
| `Dialog` | `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogFooter`, `DialogClose` |
|
|
370
|
+
| `Drawer` | `DrawerTrigger`, `DrawerTitle`, `DrawerClose` |
|
|
371
|
+
| `Dropdown` | `DropdownOption`, `DropdownOptionGroup` |
|
|
372
|
+
| `List` | `ListItem` |
|
|
373
|
+
| `Menu` | `MenuTrigger`, `MenuPopover`, `MenuItem`, `MenuDivider`, `MenuItemCheckbox`, `MenuItemRadio`, `MenuItemSwitch`, `MenuItemLink`, `MenuGroup`, `MenuGroupHeader`, `MenuSplitGroup` |
|
|
374
|
+
| `Nav` | `NavCategory`, `NavItem`, `NavSubItem` |
|
|
375
|
+
| `Overflow` | `OverflowItem` |
|
|
376
|
+
| `Popover` | `PopoverTrigger`, `PopoverContent` |
|
|
377
|
+
| `RadioGroup` | `RadioGroupItem` (also `RadioItem`) |
|
|
378
|
+
| `Skeleton` | `SkeletonGroup` |
|
|
379
|
+
| `Stepper` | `StepperStep` |
|
|
380
|
+
| `TabList` | `TabListTab`, `TabListPanel`, `TabListPanels` |
|
|
381
|
+
| `Table` | `TableHeader`, `TableHeaderCell`, `TableBody`, `TableRow`, `TableCell` |
|
|
382
|
+
| `Toolbar` | `ToolbarButton`, `ToolbarToggleButton`, `ToolbarRadioGroup`, `ToolbarRadioButton`, `ToolbarGroup`, `ToolbarDivider` |
|
|
383
|
+
| `Tree` | `TreeItem` |
|
|
384
|
+
|
|
385
|
+
## Components
|
|
386
|
+
|
|
387
|
+
65 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.
|
|
388
|
+
|
|
389
|
+
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).
|
|
390
|
+
|
|
391
|
+
### Buttons and actions
|
|
392
|
+
|
|
393
|
+
| Component | Description |
|
|
394
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
395
|
+
| `Button` | `appearance` `primary`, `outline` (default), `subtle`, `transparent`; five sizes; decorative `icon` slot with `iconPosition`; `disabled` or `disabledFocusable`; `as` renders a link or router link with correctly typed props. `type="button"` by default. |
|
|
396
|
+
| `CompoundButton` | Button with a `secondaryText` line and an optional `icon` beside the text. |
|
|
397
|
+
| `ToggleButton` | Pressed/unpressed button (`pressed`, `defaultPressed`, `onPressedChange`; `aria-pressed`, or `aria-checked` with a checked `role` such as `checkbox`); `isAccessible` draws the pressed state as a brand fill. |
|
|
398
|
+
| `SplitButton` | Primary action (with `icon`, `iconPosition`) joined to a menu chevron (`menuIcon` replaces it); `menuButtonProps` takes `Menu.Trigger`'s render props; `menuButtonLabel` names the chevron. |
|
|
399
|
+
| `MenuButton` | Button with a chevron and `aria-haspopup="menu"`, for use inside `Menu.Trigger`. Without a label (an icon and/or the chevron) it is compact and needs `aria-label`, `aria-labelledby` or `title` (a development warning asks for one). |
|
|
400
|
+
| `Link` | `appearance` `inline` (always underlined), `standalone` or `subtle`; without `href` it is a button (an action such as "Show more"); `disabled` removes the `href`; `as` renders router links. |
|
|
401
|
+
| `Toolbar` | `role="toolbar"` with one Tab stop and arrow-key navigation over any child controls (`disabledFocusable` ones included); `orientation`, `size`; toggle and radio state in `checkedValues`; parts `Toolbar.Button`, `.ToggleButton`, `.RadioGroup`, `.RadioButton`, `.Group`, `.Divider` (see [Toolbars](#toolbars)). |
|
|
402
|
+
|
|
403
|
+
### Inputs and forms
|
|
404
|
+
|
|
405
|
+
| Component | Description |
|
|
406
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
407
|
+
| `Field` | Label, hint, validation message (`validationState` error, warning, success or none; `error` is the shorthand for an error) and required indicator, wired to the control inside it (library controls read the Field context wherever they are in it); `orientation="horizontal"` puts the label beside the control. |
|
|
408
|
+
| `Label` | Form label with a decorative required asterisk, `size` and `weight`. |
|
|
409
|
+
| `Input` | Text input with `contentBefore`/`contentAfter` slots, `onValueChange(value)` next to the native `onChange`, and an `error` message. |
|
|
410
|
+
| `Textarea` | Multi-line text input with an `error` message. |
|
|
411
|
+
| `Select` | Styled native `<select>` with an `error` message. |
|
|
412
|
+
| `Checkbox` | `checked`/`defaultChecked`/`onCheckedChange`, `indeterminate`, a rich `label` before or after the box (`labelPosition`), `disabledFocusable`, native form support. |
|
|
413
|
+
| `Switch` | On/off toggle (`role="switch"`), `onCheckedChange`, a rich `label` before, after or above it (`labelPosition`), `disabledFocusable`, native form support. |
|
|
414
|
+
| `RadioGroup` | Single choice with `RadioGroup.Item` items (a rich `label` each); `orientation`, `disabled`, `onValueChange`, native form support. |
|
|
415
|
+
| `SearchBox` | Search input with a clear button; `onValueChange`; input attributes go to the `<input>`. The root draws the field, so `className` and `style` style the field box, and `contentBefore`/`contentAfter` sit beside the text. |
|
|
416
|
+
| `Slider` | Styled native range input, filled up to the thumb; `onValueChange(number)`. |
|
|
417
|
+
| `SpinButton` | Numeric input with step buttons; typed text is a draft committed on blur or Enter; `min`, `max`, `step`, `largeStep`. |
|
|
418
|
+
| `Combobox` | Editable combobox: typing filters the options; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup`. |
|
|
419
|
+
| `Dropdown` | Select-only combobox (a button) with typeahead; `clearable`. `Dropdown.Option`, `Dropdown.OptionGroup`. |
|
|
420
|
+
| `TagPicker` | Multi-select combobox that shows the selection as removable tags (a list named "Selected"); the input is described by a summary of the selection ("Selected: Apple, Banana"), and additions and removals are announced. |
|
|
421
|
+
| `DatePicker` | Date input with a calendar dialog; locale-aware format and parse, `minDate`, `maxDate`, `disabledDates`, `onInvalidInput`. |
|
|
422
|
+
| `TimePicker` | Time combobox (`12h`/`24h`, `step`, `minTime`, `maxTime`); value is `HH:mm`; `open`/`defaultOpen`/`onOpenChange` control the list; rejected text is kept and flagged (`onInvalidInput`). |
|
|
423
|
+
| `ColorPicker` | Hex field, preset swatches and an optional opacity slider; value (`defaultValue` included) reported as lowercase `#rrggbb`, or `#rrggbbaa` when not opaque. Picking a preset keeps the current opacity (a preset's alpha digits are ignored, with a development warning). |
|
|
424
|
+
| `SwatchPicker` | Radio group of color swatches (`items` with a `label` each). |
|
|
425
|
+
| `Rating` | Star rating (`role="radiogroup"`); `RatingDisplay` is the read-only version (a fractional value draws a partly filled star; `showValue`, `count` and `compact` add the value and the number of ratings as text). |
|
|
426
|
+
|
|
427
|
+
### Data display
|
|
428
|
+
|
|
429
|
+
| Component | Description |
|
|
430
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
|
+
| `Avatar` | Image, initials or icon, with an image-failure fallback and an optional `badge`. |
|
|
432
|
+
| `AvatarGroup` | Overlapping avatars; `max` adds an overflow button that lists the hidden members. |
|
|
433
|
+
| `Badge` | Status or category label: `appearance` `filled`, `tint`, `outline`; semantic `color` (see [Badges](#badges)). |
|
|
434
|
+
| `CounterBadge` | Count pill (`99+` above `overflowCount`) or, with `dot`, an unread dot; Badge's `color` palette; `showZero`. |
|
|
435
|
+
| `PresenceBadge` | Availability badge with a distinct shape per status. |
|
|
436
|
+
| `Tag` | Chip with an optional dismiss button (`dismissible`, `onDismiss`, `dismissLabel`); `onDismiss` and `dismissIcon` need `dismissible` (a development warning says so). Move focus when you remove a tag (see [Tags](#tags)). |
|
|
437
|
+
| `InfoLabel` | Label with an info button that shows extra text (a toggletip). |
|
|
438
|
+
| `Persona` | Avatar with name, secondary text and presence. |
|
|
439
|
+
| `Divider` | Horizontal or vertical separator, optionally labelled. |
|
|
440
|
+
| `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. |
|
|
441
|
+
| `List` | Plain list, selectable listbox (single or multiple) or, with item `action`s, a grid; in a selectable List item values are unique (development warning). |
|
|
83
442
|
|
|
84
443
|
### Typography
|
|
85
444
|
|
|
86
|
-
| Component | Description
|
|
87
|
-
|
|
88
|
-
| `Text`
|
|
445
|
+
| Component | Description |
|
|
446
|
+
| --------- | --------------------------------------------------------------------------- |
|
|
447
|
+
| `Text` | Type-ramp text (`variant`, `weight`); polymorphic `as`; inherits its color. |
|
|
89
448
|
|
|
90
449
|
### Layout
|
|
91
450
|
|
|
92
|
-
| Component
|
|
93
|
-
|
|
94
|
-
| `Card`
|
|
95
|
-
| `Accordion` |
|
|
96
|
-
| `TabList`
|
|
97
|
-
| `Tree`
|
|
98
|
-
| `Carousel`
|
|
99
|
-
| `Overflow`
|
|
100
|
-
| `Grid`
|
|
101
|
-
| `Stack`
|
|
102
|
-
| `Flex`
|
|
451
|
+
| Component | Description |
|
|
452
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
+
| `Card` | Surface with `Card.Header`, `Card.Body`, `Card.Footer`; optionally selectable (`onSelect`, `selected`, `selectionControl`). |
|
|
454
|
+
| `Accordion` | Disclosure sections (`Accordion.Item`, `.Trigger`, `.Panel`); single (default) or `type="multiple"`; `headingLevel`; item values are unique (development warning). |
|
|
455
|
+
| `TabList` | Tabs (`TabList.Tab`, `.Panel`, `.Panels`) with automatic activation, or manual with `selectTabOnFocus={false}`; `orientation`; tab values are unique (development warning). |
|
|
456
|
+
| `Tree` | Hierarchical tree (`Tree.Item`) with expand/collapse, selection and typeahead. 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. |
|
|
457
|
+
| `Carousel` | One slide at a time with previous/next, a slide picker and optional auto-rotation with a pause control. Slides are `Carousel.Item` children, directly or in Fragments (a component that renders `Carousel.Item` is not a slide). |
|
|
458
|
+
| `Overflow` | Hides items that do not fit in one row and renders an overflow button (`useOverflowMenu` lists the hidden items); item ids are unique (development warning). The row has a 4px padding, so its items' focus rings are not clipped. |
|
|
459
|
+
| `Grid` | CSS grid with a column count and token gaps. |
|
|
460
|
+
| `Stack` | Vertical or horizontal stack (`orientation`, `gap`). |
|
|
461
|
+
| `Flex` | Flexbox container (`direction`, `wrap`, `align`, `justify`, `gap`). |
|
|
103
462
|
|
|
104
463
|
### Feedback
|
|
105
464
|
|
|
106
|
-
| Component
|
|
107
|
-
|
|
108
|
-
| `MessageBar`
|
|
109
|
-
| `ProgressBar` |
|
|
110
|
-
| `Skeleton`
|
|
111
|
-
| `Spinner`
|
|
112
|
-
| `Toast`
|
|
465
|
+
| Component | Description |
|
|
466
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
467
|
+
| `MessageBar` | Inline status message (`info`, `success`, `warning`, `error`) with hidden status text and an optional dismiss button. |
|
|
468
|
+
| `ProgressBar` | Determinate or indeterminate progress; name it with `label` (inside a `Field`, the Field's label names it); `color`. |
|
|
469
|
+
| `Skeleton` | Loading placeholder (`width`, `height`, `shape`); `Skeleton.Group` marks the loading region busy. |
|
|
470
|
+
| `Spinner` | Loading indicator announced as "Loading" (localize with `label`); `appearance="inverted"` for brand surfaces; `delay`. |
|
|
471
|
+
| `Toast` | Notifications shown by `<Toaster>` (`limit` queues the rest) through `useToastController()`. |
|
|
113
472
|
|
|
114
473
|
### Navigation
|
|
115
474
|
|
|
116
|
-
| Component
|
|
117
|
-
|
|
118
|
-
| `Breadcrumb` |
|
|
119
|
-
| `Menu`
|
|
120
|
-
| `Nav`
|
|
121
|
-
| `Stepper`
|
|
122
|
-
| `Pagination` | Page
|
|
475
|
+
| Component | Description |
|
|
476
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
477
|
+
| `Breadcrumb` | Trail of links, buttons or text (`Breadcrumb.Item`, `current`). |
|
|
478
|
+
| `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)). |
|
|
479
|
+
| `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked. |
|
|
480
|
+
| `Stepper` | Multi-step progress (`Stepper.Step`), horizontal or vertical, optionally `linear`. |
|
|
481
|
+
| `Pagination` | Page buttons with ellipses, previous/next and optional first/last. |
|
|
123
482
|
|
|
124
483
|
### Overlays
|
|
125
484
|
|
|
126
|
-
| Component
|
|
127
|
-
|
|
128
|
-
| `Dialog`
|
|
129
|
-
| `Drawer`
|
|
130
|
-
| `Popover`
|
|
131
|
-
| `Tooltip`
|
|
132
|
-
| `TeachingPopover` | Step-
|
|
485
|
+
| Component | Description |
|
|
486
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
487
|
+
| `Dialog` | Modal dialog (`modalType="alert"` for a confirmation): `Dialog.Trigger`, `Dialog.Content` (`closeLabel` names its Close button), `Dialog.Title`, `Dialog.Footer` (stays in view), `Dialog.Close`. |
|
|
488
|
+
| `Drawer` | Modal side panel (`position` `start`, `end`, `left`, `right`; `closeLabel`); `Drawer.Trigger`, `Drawer.Title`, `Drawer.Close`. |
|
|
489
|
+
| `Popover` | Non-modal popup anchored to `Popover.Trigger` or a `target`; `side`, `align`; hover cards (`openOnHover`) and context popovers (`openOnContext`). |
|
|
490
|
+
| `Tooltip` | Hover and focus text for its child, as a description or (`relationship="label"`) a name; `open`/`defaultOpen`/`onOpenChange`; `openDelay`, `closeDelay`. |
|
|
491
|
+
| `TeachingPopover` | Step-by-step onboarding popover, optionally pointing at a `target`. |
|
|
133
492
|
|
|
134
|
-
###
|
|
493
|
+
### Tables
|
|
135
494
|
|
|
136
|
-
| Component
|
|
137
|
-
|
|
138
|
-
| `Table`
|
|
139
|
-
| `DataGrid` | Interactive
|
|
495
|
+
| Component | Description |
|
|
496
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
497
|
+
| `Table` | Static table: `Table.Header`, `Table.HeaderCell`, `Table.Body`, `Table.Row`, `Table.Cell`; `striped`; scrolls horizontally. |
|
|
498
|
+
| `DataGrid` | Interactive grid (APG grid keyboard model) with row selection and sortable headers; header from children or `columns`; row ids are unique (development warning). |
|
|
140
499
|
|
|
141
500
|
### Provider
|
|
142
501
|
|
|
143
|
-
| Component
|
|
144
|
-
|
|
145
|
-
| `WaveProvider` | Theme
|
|
502
|
+
| Component | Description |
|
|
503
|
+
| -------------- | ------------------------------------------------------------------------- |
|
|
504
|
+
| `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction and portal container. |
|
|
146
505
|
|
|
147
|
-
|
|
506
|
+
`Portal` renders its children into `document.body` (or the provider's `portalContainer`) inside a themed `wave-portal` wrapper; use it for your own overlays.
|
|
507
|
+
|
|
508
|
+
### Motion
|
|
509
|
+
|
|
510
|
+
| Component | Description |
|
|
511
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
512
|
+
| `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)). |
|
|
513
|
+
|
|
514
|
+
## Usage notes
|
|
515
|
+
|
|
516
|
+
### Buttons
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
import { Button } from '@mortenbrudvik/waveui';
|
|
520
|
+
|
|
521
|
+
// Tailwind path. The resting colors are plain classes; the hover and pressed colors use the gate
|
|
522
|
+
// prefix, because a bare `hover:` or `active:` class loses to the built-in one.
|
|
523
|
+
export const DeleteButton = () => (
|
|
524
|
+
<Button
|
|
525
|
+
appearance="primary"
|
|
526
|
+
className="bg-destructive text-destructive-foreground not-disabled:not-aria-disabled:hover:bg-destructive/90 not-disabled:not-aria-disabled:active:bg-destructive/80"
|
|
527
|
+
>
|
|
528
|
+
Delete
|
|
529
|
+
</Button>
|
|
530
|
+
);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
import { Button, Tooltip } from '@mortenbrudvik/waveui';
|
|
535
|
+
|
|
536
|
+
// Unavailable but focusable: keyboard users reach it and the Tooltip explains why.
|
|
537
|
+
export const PublishButton = ({ ready }: { ready: boolean }) => (
|
|
538
|
+
<Tooltip content={ready ? 'Publish the draft' : 'Fill in the required fields first'}>
|
|
539
|
+
<Button appearance="primary" disabledFocusable={!ready}>
|
|
540
|
+
Publish
|
|
541
|
+
</Button>
|
|
542
|
+
</Tooltip>
|
|
543
|
+
);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- **Hover and pressed colors.** They are gated (`not-disabled:not-aria-disabled:hover:` / `…:active:`), so they never apply to a disabled or `aria-disabled` Button, and they work for `as="a"`. The gate makes them more specific than a bare `hover:` class, so `className="hover:bg-error"` does not override the built-in hover color while the Button is enabled. Like `hover:bg-error!`, a bare `hover:` class still applies on hover while the Button is disabled or `aria-disabled` (the gate switches the built-in class off), and on `appearance="transparent"`, which has no hover background (a pressed ToggleButton has one). To change only the enabled hover color, use the same prefix, which replaces the built-in class; the important modifier (`hover:bg-error!`) also wins while enabled, but keeps applying while disabled. A plain `bg-error` replaces only the resting color. The same applies to CompoundButton, ToggleButton, MenuButton and SplitButton. Class overrides need Tailwind to generate the classes: the precompiled `./styles` contains only the classes Wave itself uses.
|
|
547
|
+
- **Non-interactive `as`.** `as="div"`, `as="span"` and other elements that are not interactive by themselves get `role="button"`, a tab stop (`tabIndex={0}`) and Enter (key down) / Space (key up) activation, like a native button; your own `role` or `tabIndex` wins. `as="a"` without `href` is treated the same way; with any `href` (`''` included) it stays a link. `as="a"` shows no link underline. A native `<button>` gets `type="button"` by default; a custom `as` component gets no `type` default, so pass `type` yourself inside a form.
|
|
548
|
+
- **`disabled` with a non-native `as`.** Only `button` (the default), `input`, `select` and `textarea` receive the native `disabled` attribute. Every other `as` gets `aria-disabled="true"` and `tabIndex={-1}` instead, which your props cannot override. That covers `a`, `div`, `span` and any custom component, including router links and styled or motion components that render a native `<button>`. Its clicks, Enter and Space are prevented, the click does not reach ancestor `onClick` handlers, and an `<a>` drops its `href` (it keeps `role="link"` when it had an `href`, and has `role="button"` without one). Such an element has no native `:disabled` state, so a component's own `:disabled` styling no longer applies: style it on `[aria-disabled="true"]` (the `aria-disabled:` variant). It leaves the tab order but can still take focus from a mouse click. Every disabled Button also renders `data-disabled`.
|
|
549
|
+
- **`disabledFocusable`** (Button, CompoundButton, ToggleButton, MenuButton, SplitButton and Link) marks the control unavailable but keeps it focusable and in the tab order: for a Tooltip that explains why (above), a toolbar item, or a button that disables itself when activated. It renders `aria-disabled="true"`, `data-disabled` and `data-disabled-focusable` instead of the native `disabled`. A click, Enter, Space and implicit form submission do nothing: your `onClick` is not called and the click does not reach ancestor click handlers, while other keys still reach your key handlers. Hover and pressed colors are off, and it wins over `disabled`. A `Toolbar` keeps it in its arrow-key order, and `Menu.Trigger` does not open its menu from it. While it shows its keyboard focus ring it is drawn at full opacity, so the ring keeps its contrast (the disabled look's opacity would dim it). On SplitButton it affects both halves; `primaryActionButtonProps={{ disabledFocusable: true }}` or `menuButtonProps` affect one.
|
|
550
|
+
- An `aria-disabled="true"` of yours (without `disabledFocusable`) on an enabled Button shows the disabled look but keeps the Button focusable and its handlers running: guard them yourself, or use `disabledFocusable`, which also blocks activation.
|
|
551
|
+
- The `icon` slot is decorative (`aria-hidden`). Give an icon-only Button an `aria-label`, `aria-labelledby` or `title`; it warns in development without one. `iconPosition="after"` renders the icon after the label (an "open in new window" glyph); it follows the writing direction and has no effect on an icon-only button. CompoundButton takes an `icon` too, beside its two lines of text (with only an icon it is an icon-only Button).
|
|
552
|
+
- **SplitButton** takes `icon` and `iconPosition` for its primary action, and `menuIcon` to replace the chevron of the menu half. The menu half always shows an indicator: a `menuIcon` that renders nothing (`false`, `''`, `[]`) keeps the chevron and warns in development. `MenuButton.menuIcon` is the opposite: there such a value hides the indicator. A `<button>` or `Button` passed as the `menuIcon` of either is unwrapped, not nested: its children become the glyph and its props are dropped, with a development warning.
|
|
553
|
+
- **A busy Button.** Put `<Spinner appearance="inverted" size="extra-small" />` in a primary Button's `icon` slot and say what is happening in the label (`Saving`): the slot is decorative, so the label stays the Button's name, and `inverted` draws the spinner in the Button's text color. A Spinner among the children would add its own label to the name.
|
|
554
|
+
- **ToggleButton state.** A ToggleButton reports its state with `aria-pressed`, which ARIA allows only on buttons. Give it `role="checkbox"`, `"radio"`, `"switch"`, `"menuitemcheckbox"`, `"menuitemradio"`, `"option"` or `"treeitem"` and it reports the state with `aria-checked` instead, plus `data-checked` while pressed; with any other role (`tab`, `link`, `menuitem`) it renders neither and warns in development (use the component that owns that role: a TabList tab, a Link, a `Menu.ItemCheckbox`). `data-pressed` is present while pressed in every case, so `data-[pressed]:` styles every toggle. `onPressedChange` is the same with either attribute.
|
|
555
|
+
- **`isAccessible`** draws a pressed ToggleButton as a brand fill with on-brand text (`appearance="primary"`: the pressed fill with an inset on-brand stroke) instead of a light tint, so the state does not depend on a subtle color change; use it for icon-only toggles in toolbars. Forced colors keep the usual pressed outline.
|
|
556
|
+
- **A `Link` without `href`** is an action, not a navigation: `<Link onClick={showMore}>Show more</Link>` keeps its `<a>` element and gets `role="button"`, a tab stop and Enter/Space activation, like `Button as="a"` without `href`. Give it an `href` whenever it navigates.
|
|
557
|
+
- **Types.** `ButtonProps` and `React.ComponentProps<typeof Button>` are the props of a Button rendered as `<button>`. For another element use `ButtonProps<'a'>` (a Storybook story with `as` args: `StoryObj<ButtonProps<'a'>>`). The other polymorphic components (CompoundButton, Link, Text, Toolbar, `Toolbar.Button`, `Menu.ItemLink`, Card and its parts, Stack, Flex, Grid, Tag, Divider) work the same way.
|
|
558
|
+
|
|
559
|
+
### Forms and `Field`
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
import * as React from 'react';
|
|
563
|
+
import { Button, Checkbox, Dropdown, Field, Input } from '@mortenbrudvik/waveui';
|
|
564
|
+
|
|
565
|
+
export function SignupForm() {
|
|
566
|
+
const [email, setEmail] = React.useState('');
|
|
567
|
+
const error = email.includes('@') ? undefined : 'Enter an email address';
|
|
568
|
+
return (
|
|
569
|
+
<form onSubmit={(event) => event.preventDefault()}>
|
|
570
|
+
<Field label="Email" hint="We never share it" error={error} required>
|
|
571
|
+
<Input type="email" name="email" value={email} onValueChange={setEmail} />
|
|
572
|
+
</Field>
|
|
573
|
+
<Field label="Role">
|
|
574
|
+
<Dropdown name="role" defaultValue="user">
|
|
575
|
+
<Dropdown.Option value="admin">Admin</Dropdown.Option>
|
|
576
|
+
<Dropdown.Option value="user">User</Dropdown.Option>
|
|
577
|
+
</Dropdown>
|
|
578
|
+
</Field>
|
|
579
|
+
<Checkbox name="terms" label="I accept the terms" required />
|
|
580
|
+
<Button appearance="primary" type="submit">
|
|
581
|
+
Sign up
|
|
582
|
+
</Button>
|
|
583
|
+
</form>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
import { Field, Input, ProgressBar, Switch } from '@mortenbrudvik/waveui';
|
|
590
|
+
|
|
591
|
+
export function AccountSettings({ backedUp }: { backedUp: number }) {
|
|
592
|
+
return (
|
|
593
|
+
<>
|
|
594
|
+
<Field
|
|
595
|
+
label="Password"
|
|
596
|
+
hint="At least 12 characters"
|
|
597
|
+
validationState="warning"
|
|
598
|
+
validationMessage="This password is common"
|
|
599
|
+
orientation="horizontal"
|
|
600
|
+
>
|
|
601
|
+
<Input type="password" />
|
|
602
|
+
</Field>
|
|
603
|
+
<Field label="Backup" validationState="success" validationMessage="Up to date">
|
|
604
|
+
<ProgressBar value={backedUp} />
|
|
605
|
+
</Field>
|
|
606
|
+
<Switch label="Email me about new sign-ins" labelPosition="before" />
|
|
607
|
+
</>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
- Library controls inside a `Field` are named by its label, described by its validation message and hint, and marked `aria-invalid`/`aria-required`. With `required`, native inputs (`Input`, `Select`, `Textarea`, `Slider`, `SpinButton`, `SearchBox`) also get the native `required` attribute, so the browser blocks an empty submit. The choice and picker controls (`Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SwatchPicker`, `ColorPicker`, `Combobox`, `Dropdown`, `TagPicker`, `DatePicker`, `TimePicker`) are required too, also without `name`: the browser blocks the submit until the control is checked, switched on or has a value, so `<Field required><Switch /></Field>` means "must be on". Like a native read-only input, a `readOnly` Combobox, TagPicker, DatePicker or TimePicker does not block the submit (it keeps `aria-required`). Add `noValidate` to a `<form>` that validates in its submit handler.
|
|
613
|
+
- **Validation states.** `validationMessage` renders below the control in the style of `validationState`: `error` (the default when a message is given) marks the control `aria-invalid` and announces the message (`role="alert"`); `warning` announces it without making the control invalid; `success` and `none` show it without announcing it. Each state but `none` shows an icon before the message; `validationMessageIcon` replaces it, and `null` removes it. `validationState="error"` without a message marks the control invalid and shows nothing. The Field root carries `data-validation-state`.
|
|
614
|
+
- **`error`** is the shorthand for an error message: `error="Enter an email address"`, or `error={true}` to mark the control invalid without a message. It wins over `validationMessage` and `validationState` (a development warning fires when both are set).
|
|
615
|
+
- **The hint** stays visible below the message, after it; the control is described by the message, then the hint.
|
|
616
|
+
- **`orientation="horizontal"`** puts the label in a start column (a third of the width) beside the control; the message and the hint stay below the control. The label's first line lines up with the control's first line: a Checkbox, Switch or RadioGroup as the first child (or a plain wrapper around them) gets 6px of padding above and below its first row, as tall as an Input. The root carries `data-orientation`. The horizontal layout wraps the control, so changing `orientation` on a mounted Field remounts the control (an uncontrolled control loses its state and focus).
|
|
617
|
+
- **ProgressBar** reads the Field: the Field's label names a bar that has no name of its own, the message and the hint describe it, and the validation state (error, warning, success) colors its fill unless you set `color`. A progress bar is never invalid or required. Next to another control in a Field's wrapper element, the bar leaves the Field's label to that control (it gets an id of its own).
|
|
618
|
+
- **Choice labels.** Checkbox, Switch and `RadioGroup.Item` take any phrasing content as `label` (a link to the terms, a second line of subtext; no other form controls). It names the control; clicking its text toggles the control, and clicking a link inside it follows the link. The box, track or radio lines up with the first line of a label that wraps or has a second line (`className="items-center"` centres it instead). Arrow keys, Home and End pressed on a link inside a `RadioGroup.Item` label stay the link's. `children` are not rendered (a development warning says so). `labelPosition` puts the label `"after"` (default) or `"before"` the Checkbox and Switch, or `"above"` the Switch (settings lists). Checkbox and Switch take `disabledFocusable` (focusable, but not toggled and not submitted) and route your `aria-disabled` to the control.
|
|
619
|
+
- A `label`, `hint`, `error` or `validationMessage` that renders nothing (`[]`, such as an empty `errors.map(…)`) counts as absent: no empty message renders, the hint shows, and without `validationState="error"` the control stays valid. A control's own `aria-invalid={false}` wins over the Field's error, which still describes it.
|
|
620
|
+
- One control per Field. Put wrapper or layout components (a `Tooltip`, your own row) **inside a plain `<div>`** in the Field; the library control inside is then labelled correctly.
|
|
621
|
+
- `error` on `Input`, `Select` and `Textarea`: a string renders the message after the control in a `role="alert"` element linked with `aria-describedby`/`aria-errormessage`; `error={true}` only marks the control invalid. Inside a Field that shows its own error, the message is not repeated.
|
|
622
|
+
- Value controls take part in native forms when you pass `name`, or when they are required (their own `required`, or a required `Field`): `Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SpinButton`, `SwatchPicker`, `ColorPicker`, `Combobox`, `Dropdown`, `TagPicker`, `DatePicker` (ISO `yyyy-mm-dd`) and `TimePicker` (`HH:mm`) render a hidden input, honour `required` and `form`, and reset with their form. `SearchBox` submits through its own `<input name>` and resets with its form too. No name is generated for you.
|
|
623
|
+
|
|
624
|
+
### Value callbacks
|
|
625
|
+
|
|
626
|
+
`onChange` is the native DOM change event (`Input`, `Textarea`, `Select`, `Slider`). State callbacks are named after the state and receive the value:
|
|
627
|
+
|
|
628
|
+
| State | Props |
|
|
629
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
630
|
+
| single value | `value` / `defaultValue` / `onValueChange(value)` |
|
|
631
|
+
| checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` |
|
|
632
|
+
| open | `open` / `defaultOpen` / `onOpenChange(open)` |
|
|
633
|
+
| checked values (Menu, Toolbar) | `checkedValues` / `defaultCheckedValues` / `onCheckedValuesChange(checkedValues, details)` |
|
|
148
634
|
|
|
149
|
-
|
|
635
|
+
Extra data goes in a second, optional `details` argument: Dialog and Drawer call `onOpenChange(open, details)` with `details.reason` and `details.event` (see [Dialogs and triggers](#dialogs-and-triggers)), and Menu and Toolbar call `onCheckedValuesChange(checkedValues, details)` with `details.name`, `details.checkedItems` (the group's new values) and `details.event` (see [Menus](#menus)). These fire only when the value changes. Event callbacks such as `onPageChange`, `onStepChange` and `Tree`'s `onItemSelect` fire on every activation, also when the current item is activated again. Array props that Wave only reads (`openItems`, `expandedItems`, `selectedItems`, `columns`, `options`, `items`, `steps`, …) accept readonly arrays such as `as const` values; the callbacks receive a new, mutable array. The 0.4 names still work as deprecated aliases that warn once in development; see the [CHANGELOG](CHANGELOG.md) for the full table.
|
|
636
|
+
|
|
637
|
+
### Composite controls
|
|
638
|
+
|
|
639
|
+
Checkbox, Switch, SearchBox, SpinButton, Combobox, Dropdown, TagPicker, DatePicker and TimePicker render a wrapper around their focusable element. `id`, the naming and validation ARIA attributes (`aria-label`, `aria-labelledby`, `aria-describedby`, `aria-invalid`, `aria-required`, `aria-errormessage`, `aria-details`; on Checkbox and Switch also `aria-disabled`), `tabIndex`, `autoFocus`, focus and key handlers (and native input attributes for text fields) go to the focusable element; `className`, `style`, `hidden`, `data-*`, other `aria-*` attributes and `ref` stay on the wrapper. `controlRef` gives you the focusable element. SearchBox and SpinButton draw their field (border, background, focus and invalid look) on that wrapper, so `className`, `style` and `hidden` size, style and hide the visible field, as on Input's slot wrapper.
|
|
640
|
+
|
|
641
|
+
`Input` with `contentBefore`/`contentAfter` content also renders a bordered wrapper `<span>`, which receives `className`, `style` and `hidden` (so they size and hide the visible field); unlike the controls above, `ref`, `id`, `aria-*` and `data-*` stay on the `<input>`.
|
|
642
|
+
|
|
643
|
+
### Menus
|
|
150
644
|
|
|
151
645
|
```tsx
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
646
|
+
import { Menu, MenuButton, SplitButton } from '@mortenbrudvik/waveui';
|
|
647
|
+
|
|
648
|
+
export function Actions({ onEdit, onSave }: { onEdit: () => void; onSave: () => void }) {
|
|
649
|
+
return (
|
|
650
|
+
<>
|
|
651
|
+
<Menu>
|
|
652
|
+
<Menu.Trigger>
|
|
653
|
+
<MenuButton>Actions</MenuButton>
|
|
654
|
+
</Menu.Trigger>
|
|
655
|
+
<Menu.Popover>
|
|
656
|
+
<Menu.Item onClick={onEdit}>Edit</Menu.Item>
|
|
657
|
+
<Menu.Divider />
|
|
658
|
+
<Menu.Item disabled>Archive</Menu.Item>
|
|
659
|
+
</Menu.Popover>
|
|
660
|
+
</Menu>
|
|
661
|
+
|
|
662
|
+
<Menu>
|
|
663
|
+
<Menu.Trigger>
|
|
664
|
+
{(triggerProps) => (
|
|
665
|
+
<SplitButton menuButtonProps={triggerProps} onClick={onSave}>
|
|
666
|
+
Save
|
|
667
|
+
</SplitButton>
|
|
668
|
+
)}
|
|
669
|
+
</Menu.Trigger>
|
|
670
|
+
<Menu.Popover>
|
|
671
|
+
<Menu.Item>Save as…</Menu.Item>
|
|
672
|
+
</Menu.Popover>
|
|
673
|
+
</Menu>
|
|
674
|
+
</>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
155
677
|
```
|
|
156
678
|
|
|
157
|
-
|
|
679
|
+
Use `Menu.Trigger`/`Menu.Popover` for menu buttons; `Popover` has no menu semantics. A Menu with `open`, `defaultOpen` or `onOpenChange` (even `false`) is a popup menu, which renders no element of its own: put its items in `Menu.Popover`. Without them and without Trigger/Popover parts it is a static `role="menu"`. A MenuButton without a label needs `aria-label`, `aria-labelledby` or `title`.
|
|
680
|
+
|
|
681
|
+
- **Long menus** scroll inside the viewport: `Menu.Popover` is limited to the height and width available next to its trigger, and the item that takes focus is scrolled into view. A `max-h-*` or `max-w-*` class on `Menu.Popover` (such as `max-h-64`) replaces the limit; so does a `style`.
|
|
682
|
+
- **Unavailable triggers.** A trigger marked `aria-disabled="true"` (a `disabledFocusable` MenuButton or SplitButton, or your own element) stays focusable but never opens its menu: its click, Enter, Space, ArrowDown and ArrowUp are ignored, also when it sits inside the trigger's wrapper span. An `aria-disabled` ancestor outside the trigger does not count.
|
|
683
|
+
- Compose a SplitButton through the render-prop child, as above (`menuButtonProps={triggerProps}`), so only the menu half is the trigger. Wrapping the whole SplitButton in `Menu.Trigger` (`asChild={false}`) makes both halves open the menu.
|
|
158
684
|
|
|
159
685
|
```tsx
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
686
|
+
import * as React from 'react';
|
|
687
|
+
import { Menu, MenuButton } from '@mortenbrudvik/waveui';
|
|
688
|
+
import type { CheckedValues } from '@mortenbrudvik/waveui';
|
|
689
|
+
|
|
690
|
+
export function ViewMenu() {
|
|
691
|
+
const [checked, setChecked] = React.useState<CheckedValues>({ show: ['ruler'], sort: ['name'] });
|
|
692
|
+
return (
|
|
693
|
+
<Menu checkedValues={checked} onCheckedValuesChange={setChecked}>
|
|
694
|
+
<Menu.Trigger>
|
|
695
|
+
<MenuButton>View</MenuButton>
|
|
696
|
+
</Menu.Trigger>
|
|
697
|
+
<Menu.Popover>
|
|
698
|
+
<Menu.Group>
|
|
699
|
+
<Menu.GroupHeader>Show</Menu.GroupHeader>
|
|
700
|
+
<Menu.ItemCheckbox name="show" value="ruler">
|
|
701
|
+
Ruler
|
|
702
|
+
</Menu.ItemCheckbox>
|
|
703
|
+
<Menu.ItemSwitch name="show" value="grid">
|
|
704
|
+
Grid
|
|
705
|
+
</Menu.ItemSwitch>
|
|
706
|
+
</Menu.Group>
|
|
707
|
+
<Menu.Divider />
|
|
708
|
+
<Menu.Group>
|
|
709
|
+
<Menu.GroupHeader>Sort by</Menu.GroupHeader>
|
|
710
|
+
<Menu.ItemRadio name="sort" value="name">
|
|
711
|
+
Name
|
|
712
|
+
</Menu.ItemRadio>
|
|
713
|
+
<Menu.ItemRadio name="sort" value="date">
|
|
714
|
+
Date
|
|
715
|
+
</Menu.ItemRadio>
|
|
716
|
+
</Menu.Group>
|
|
717
|
+
<Menu.Divider />
|
|
718
|
+
<Menu>
|
|
719
|
+
<Menu.Trigger>
|
|
720
|
+
<Menu.Item>Zoom</Menu.Item>
|
|
721
|
+
</Menu.Trigger>
|
|
722
|
+
<Menu.Popover>
|
|
723
|
+
<Menu.Item>Zoom in</Menu.Item>
|
|
724
|
+
<Menu.Item>Zoom out</Menu.Item>
|
|
725
|
+
</Menu.Popover>
|
|
726
|
+
</Menu>
|
|
727
|
+
<Menu.ItemLink href="/help/view">View help</Menu.ItemLink>
|
|
728
|
+
</Menu.Popover>
|
|
729
|
+
</Menu>
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
- **Checkable items.** `Menu.ItemCheckbox`, `Menu.ItemRadio` and `Menu.ItemSwitch` take a required `name` (the group, a key of `checkedValues`) and `value`. A checkbox, switch or radio item is checked while its `value` is in `checkedValues[name]`; checking a radio item makes its value the group's only value, which unchecks the other radio items of that `name` (so give a radio group at most one value). A `name` and `value` pair is unique within a menu list: a second item with the same pair would show as checked too (a development warning says so). Any string is a group name, `constructor` included. The state lives on the `Menu`: `checkedValues` (controlled) or `defaultCheckedValues`, and `onCheckedValuesChange(checkedValues, details)` fires only on change with a new object (so `setState` works), then `details`: the group's `name`, its new `checkedItems` and the click `event`. It works in static and popup menus, and does not make a Menu a popup menu; a static menu renders the checked state in the server HTML.
|
|
735
|
+
- **Space and Enter.** Space changes a checkable item and keeps the menu open, so several can be changed in a row; Enter and a click change it and close a popup menu (the APG menu pattern). `persistOnItemClick` on the Menu keeps it open after any item, and an item's own `persistOnClick` (`true` or `false`) wins. Activating a radio item whose value already is its group's only value changes nothing (no callback) and still closes the menu.
|
|
736
|
+
- **One group per radio set.** Put the radio items of each `name` in a `Menu.Group` (or at least separate two sets with a `Menu.Divider`): assistive technology counts a radio set by its group or separators, not by `name`, so two sets side by side are announced as one.
|
|
737
|
+
- **Indicators.** A checked checkbox or radio item shows a check; `checkmark` replaces the glyph, and since a checked item always shows an indicator, a `checkmark` that renders nothing keeps the default glyph (with a development warning). `Menu.ItemSwitch` draws a switch at the end of its row. All of them are decorative: the item's `aria-checked` carries the state, and `data-checked` is present while it is checked.
|
|
738
|
+
- **Groups.** `Menu.Group` is a `role="group"` labelled by its `Menu.GroupHeader` when the header is a direct child (or inside a Fragment). A header nested deeper, in your own component, does not label it (a development warning says so): give such a group `aria-label` instead, as you do for a group without a visible header. Headers are not items: the arrow keys and typeahead skip them. Give a group one header: only the first header among its direct children labels it, and any other header renders only its own `id` (a development warning says so). A group's own `aria-label` or `aria-labelledby` wins when it is defined (one that holds `undefined`, from a wrapper that forwards it, does not). Separate groups with `Menu.Divider`.
|
|
739
|
+
- **Links.** `Menu.ItemLink` renders an `<a href>` with `role="menuitem"` (or, with `as`, your router link component, which must forward `ref` and spread its props onto the anchor). Enter is the browser's own link activation, so Shift, Ctrl and Cmd open a new window or tab; Space follows the link too. Every click follows it and closes a popup menu, a Ctrl- or Cmd-click that opens a new tab included, and `persistOnItemClick` does not keep it open; middle click and the browser's link context menu stay native. A disabled link is `aria-disabled` and its click is prevented; an `<a>` also drops its `href`, but a router link still renders its `to` (so "open in new tab" can still follow it): render a disabled router link without its target. Name a link that opens a new window in its label; an icon that says so is decorative.
|
|
740
|
+
- **Submenus.** A `<Menu>` rendered in a menu list (inside `Menu.Popover`, as above, or among the items of a static menu) is a submenu. Its `Menu.Trigger` wraps a `Menu.Item`, which becomes its trigger item (`aria-haspopup="menu"`, `aria-expanded`, a chevron that mirrors in RTL). ArrowRight (ArrowLeft in RTL), Enter, Space and a click open it and focus its first item; ArrowLeft (ArrowRight in RTL) and Escape close only that submenu and return focus to its item; item activation and Tab close every level, focusing the root trigger first. One submenu of a list is open at a time. A submenu opens beside its item (`side="end"`, `offset={0}`) and flips to fit the viewport; with room on neither side (a wide menu, a phone), it overlaps its parent menu, inside the viewport. It shares its parent's checked values unless it sets `checkedValues` or `defaultCheckedValues` of its own (an `onCheckedValuesChange` alone only listens, after the parent's), and it inherits `persistOnItemClick`, `openDelay` and `closeDelay`. A Menu inside a Popover or Dialog that an item opens is not a submenu: it stays a root menu of its own.
|
|
741
|
+
- **Split rows.** `Menu.SplitGroup` puts an action and the button of its submenu in one row. The submenu half is a `Menu.Item` without children, named by its `aria-label` (it shows only the chevron):
|
|
742
|
+
|
|
743
|
+
```tsx
|
|
744
|
+
<Menu.SplitGroup>
|
|
745
|
+
<Menu.Item onClick={save}>Save</Menu.Item>
|
|
746
|
+
<Menu>
|
|
747
|
+
<Menu.Trigger>
|
|
748
|
+
<Menu.Item aria-label="More save options" />
|
|
749
|
+
</Menu.Trigger>
|
|
750
|
+
<Menu.Popover>
|
|
751
|
+
<Menu.Item onClick={saveAs}>Save as…</Menu.Item>
|
|
752
|
+
</Menu.Popover>
|
|
753
|
+
</Menu>
|
|
754
|
+
</Menu.SplitGroup>
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
ArrowDown and ArrowUp visit both halves; ArrowRight (ArrowLeft in RTL) moves from the action to the submenu button, and on the button opens the submenu.
|
|
758
|
+
|
|
759
|
+
- **Hover.** Submenus open when the mouse rests on their item (`openDelay`, 250ms) and close `closeDelay` (250ms) after the pointer has left the item and the submenu; `openOnHover={false}` turns that off for one submenu. `openOnHover` on a root popup menu opens it from its trigger the same way. A menu opened by hover takes no focus; a click, Enter, Space or ArrowDown on its trigger (ArrowRight, Enter, Space or a click on a submenu's item) keeps it open and moves focus into it. Focus inside the menu keeps it open; focus left on its trigger does not. A close by hover moves no focus, also for a submenu closing with its menu; Escape and the other closes return focus as usual. A triangle between the trigger and the menu keeps the menu open while the pointer moves diagonally towards it, across other items. After Escape or an outside press it stays closed until the pointer has left the trigger. Touch and pen never open anything by hover. Set the delays on the root (a static menu accepts them too); submenus inherit them.
|
|
760
|
+
- **Focus follows the mouse.** While focus is in a menu (an open popup menu and its submenus, or a static menu the user is in), the item under the mouse takes focus, as in native menus, so Enter and the arrow keys act on the item you point at. Hover never takes focus into a menu that focus is not in, and never out of a Popover, Dialog or other portal opened from an item: typing and Enter stay there.
|
|
761
|
+
- **Context menus.** `openOnContext` makes the trigger's child a context-menu region: a right click (a Ctrl+click on macOS, a long press where the browser fires `contextmenu`) opens the menu at the pointer, and Shift+F10 or the ContextMenu key opens it at the focused element inside the region. Focus goes to the first item and returns, on Escape or an item, to the element that had it at the gesture (the row, not the region); Tab closes the menu, and tabbing continues from that element. A menu your app opens without a gesture (a controlled `open`) forgets that element. A second gesture on another row moves the open menu there without closing it. The browser's context menu is suppressed on the region and in the menu, except in text fields inside the region, which keep it for paste and spelling suggestions. A primary press elsewhere (on another row too), a right click outside, or a scroll that moves the region or the row of the gesture (the row under the pointer, which a scroll of the region's own content moves, or the focused row) closes the menu. The region is no menu button, so it gets no `aria-haspopup` or `aria-expanded` (with a render-prop child, do not spread them): name the menu with `aria-label` on `Menu.Popover` (a development warning asks for one), and announce the gesture with `aria-keyshortcuts="Shift+F10"` on the region. iOS Safari fires no `contextmenu` on a long press, so there is no touch gesture there.
|
|
762
|
+
|
|
763
|
+
```tsx
|
|
764
|
+
import { Button, Menu } from '@mortenbrudvik/waveui';
|
|
765
|
+
|
|
766
|
+
export function FileList({ files }: { files: string[] }) {
|
|
767
|
+
return (
|
|
768
|
+
<Menu openOnContext>
|
|
769
|
+
<Menu.Trigger>
|
|
770
|
+
<div aria-keyshortcuts="Shift+F10">
|
|
771
|
+
{files.map((file) => (
|
|
772
|
+
<Button key={file} appearance="subtle">
|
|
773
|
+
{file}
|
|
774
|
+
</Button>
|
|
775
|
+
))}
|
|
776
|
+
</div>
|
|
777
|
+
</Menu.Trigger>
|
|
778
|
+
<Menu.Popover aria-label="File actions">
|
|
779
|
+
<Menu.Item>Open</Menu.Item>
|
|
780
|
+
<Menu.Item>Rename</Menu.Item>
|
|
781
|
+
<Menu.Item>Delete</Menu.Item>
|
|
782
|
+
</Menu.Popover>
|
|
783
|
+
</Menu>
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
- **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.
|
|
789
|
+
- **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.
|
|
790
|
+
- **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).
|
|
791
|
+
|
|
792
|
+
### Toolbars
|
|
793
|
+
|
|
794
|
+
```tsx
|
|
795
|
+
import { Toolbar } from '@mortenbrudvik/waveui';
|
|
796
|
+
|
|
797
|
+
export function FormatBar() {
|
|
798
|
+
return (
|
|
799
|
+
<Toolbar aria-label="Formatting" defaultCheckedValues={{ format: ['bold'], align: ['left'] }}>
|
|
800
|
+
<Toolbar.ToggleButton name="format" value="bold">
|
|
801
|
+
Bold
|
|
802
|
+
</Toolbar.ToggleButton>
|
|
803
|
+
<Toolbar.ToggleButton name="format" value="italic">
|
|
804
|
+
Italic
|
|
805
|
+
</Toolbar.ToggleButton>
|
|
806
|
+
<Toolbar.Divider />
|
|
807
|
+
<Toolbar.RadioGroup aria-label="Text alignment">
|
|
808
|
+
<Toolbar.RadioButton name="align" value="left">
|
|
809
|
+
Left
|
|
810
|
+
</Toolbar.RadioButton>
|
|
811
|
+
<Toolbar.RadioButton name="align" value="center">
|
|
812
|
+
Center
|
|
813
|
+
</Toolbar.RadioButton>
|
|
814
|
+
</Toolbar.RadioGroup>
|
|
815
|
+
<Toolbar.Divider />
|
|
816
|
+
<Toolbar.Group aria-label="Insert">
|
|
817
|
+
<Toolbar.Button>Link</Toolbar.Button>
|
|
818
|
+
<Toolbar.Button>Image</Toolbar.Button>
|
|
819
|
+
</Toolbar.Group>
|
|
820
|
+
</Toolbar>
|
|
821
|
+
);
|
|
822
|
+
}
|
|
163
823
|
```
|
|
164
824
|
|
|
165
|
-
|
|
825
|
+
- **Checked values.** The pressed `Toolbar.ToggleButton`s and the checked `Toolbar.RadioButton` of each group live on the Toolbar, per group `name`: `checkedValues` (controlled) or `defaultCheckedValues`, and `onCheckedValuesChange(checkedValues, details)` as on Menu. Both parts take a required `name` and `value`; a toggle is pressed, and a radio checked, while its `value` is in `checkedValues[name]`, and checking a radio makes its value the group's only value (so give a radio group at most one value). A `name` and `value` pair is unique within a toolbar (a development warning says so). The state is in the server HTML. A plain ToggleButton inside a Toolbar keeps its own `pressed` state.
|
|
826
|
+
- **Radio groups.** `Toolbar.RadioGroup` is a `role="radiogroup"` whose radios are part of the toolbar's arrow-key order: Left and Right (Up and Down in a vertical toolbar) move through buttons, toggles and radios alike, and Up and Down (Left and Right in a vertical toolbar) also move among the group's radios, wrapping inside the group. The arrow keys only move focus; Space, Enter or a click checks a radio. Name the group with `aria-label` (a development warning asks for one). The toolbar keeps one Tab stop.
|
|
827
|
+
- **Groups and dividers.** `Toolbar.Group` lays out related controls; it is `role="presentation"`, or `role="group"` once you name it with `aria-label` or `aria-labelledby`. `Toolbar.Divider` is a separator line across the toolbar (vertical in a horizontal toolbar); it is not focusable, so the arrow keys pass it.
|
|
828
|
+
- **Size and buttons.** `size` (default `'medium'`) is the default size of `Toolbar.Button`, `Toolbar.ToggleButton` and `Toolbar.RadioButton` and sets the toolbar's padding; a part's own `size` wins, and plain Buttons keep their own default. The three parts use the `subtle` appearance by default. `Toolbar.Button vertical` puts the icon above a caption-size label, for ribbons. `isAccessible` on the toggles and radios draws the pressed state as a brand fill (recommended for icon-only ones).
|
|
829
|
+
- Any other focusable control still works inside a Toolbar, as in 0.6: Inputs, Comboboxes and MenuButtons join the arrow-key order, and text fields keep their own arrow keys.
|
|
166
830
|
|
|
167
|
-
|
|
831
|
+
### Dialogs and triggers
|
|
168
832
|
|
|
169
833
|
```tsx
|
|
170
|
-
import {
|
|
834
|
+
import { Button, Dialog } from '@mortenbrudvik/waveui';
|
|
835
|
+
|
|
836
|
+
export function DeleteDialog({ onDelete }: { onDelete: () => void }) {
|
|
837
|
+
return (
|
|
838
|
+
<Dialog modalType="alert">
|
|
839
|
+
<Dialog.Trigger>
|
|
840
|
+
<Button>Delete</Button>
|
|
841
|
+
</Dialog.Trigger>
|
|
842
|
+
<Dialog.Content title="Delete item?" size="small">
|
|
843
|
+
This action cannot be undone.
|
|
844
|
+
<Dialog.Footer>
|
|
845
|
+
<Dialog.Close>
|
|
846
|
+
<Button autoFocus>Cancel</Button>
|
|
847
|
+
</Dialog.Close>
|
|
848
|
+
<Dialog.Close>
|
|
849
|
+
<Button appearance="primary" onClick={onDelete}>
|
|
850
|
+
Delete
|
|
851
|
+
</Button>
|
|
852
|
+
</Dialog.Close>
|
|
853
|
+
</Dialog.Footer>
|
|
854
|
+
</Dialog.Content>
|
|
855
|
+
</Dialog>
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
- `Dialog.Trigger`, `Drawer.Trigger`, `Popover.Trigger` and `Menu.Trigger` merge their props onto their single child (no wrapper element; a Fragment around one element counts as that element) or pass them to a render-prop child. `asChild={false}` renders the 0.4 wrapper `<span>`; a custom child that neither forwards `ref` nor spreads its props, and text or several children, fall back to that span automatically, with a development warning. The span keeps the trigger's other props, but `aria-haspopup`, `aria-expanded` and `aria-controls` go to the first element in the tab order inside it (after mount), so a button there is announced as opening the popup, and Menu, Popover, Dialog and Drawer return focus to that element (to the span only when nothing inside it can take focus). A span you made the trigger itself, with a `role` such as `button` and `tabIndex={0}`, keeps them and takes focus back; `tabIndex` alone leaves the span generic, so it never carries them. With `openOnContext` (Menu, Popover) the child is a context-menu region, not a popup button: it receives your forwarded props and, of the trigger's own, only its id, ref and context-gesture handlers (no state ARIA, no click toggle).
|
|
861
|
+
- Render `Dialog.Footer` **inside** `Dialog.Content`, one per content. It stays in view: it sticks to the bottom of the scrolling body with an opaque background, and the body reserves its height as scroll padding, so a focused field is never hidden behind it. It keeps its place in the DOM; inside a `<form>` that wraps the fields and the footer, make it the form's last child. Inside a Drawer, `Dialog.Footer` does not stick: it is an action row at the end of the content.
|
|
862
|
+
- Dialog and Drawer trap focus, close on Escape and a backdrop press, lock page scroll and make the rest of the page `inert` while open (toasts and live regions stay available). A backdrop press blurs the focused field before the surface closes, so a typed value (a SpinButton's, a picker's) is committed as with the Close button. When the press does not close the surface (an alert dialog, or a controlled one that refuses `outside-press`), focus ends up where it was.
|
|
863
|
+
- **Alert dialogs.** `modalType="alert"` is for a confirmation that needs an answer: the content is `role="alertdialog"`, and a backdrop press does not close it (Escape, the Close button and `Dialog.Close` still do). Initial focus goes to the first focusable element, the built-in Close button; the order of your buttons does not change that, so give the least destructive action `autoFocus` to start there, as above.
|
|
864
|
+
- **Why it opens or closes.** `onOpenChange(open, details)` gets `details.reason` (`'trigger'`, `'close'` for a `.Close` part, `'close-button'` for the built-in Close button, `'escape'`, `'outside-press'` for the backdrop) and `details.event`, the DOM event. A controlled dialog or drawer can refuse some of them (below). `details` is typed optional until 1.0, when it becomes required: read it as `details?.reason`. A render-prop trigger or close part that calls `onClick()` without its event reports a new `click` event with no target, so pass the event on.
|
|
865
|
+
- On close, focus goes to `finalFocusRef` when you pass one. Otherwise it returns to the element that had focus when the modal opened (a trigger's wrapper span resolves to the element inside it, see the trigger bullet above): the trigger for a trigger click, but also the parent's own button of a controlled Dialog, or a text field that had focus when a keyboard shortcut opened it. When nothing had focus (a modal opened from code, or a click in Safari, which does not focus the clicked button), it goes to the trigger that opened the modal, else the first mounted trigger. When that element is gone, focus falls back to a mounted trigger, then to an element next to where the opener was (the next row's action after a delete), then to the overlay below.
|
|
866
|
+
- An `autoFocus` element inside a Dialog or Drawer keeps focus, also in a dialog opened over another one or nested in its content, and in a `Popover.Content` opened from a dialog. `Dialog.Content` and `Drawer` take `closeLabel` (default `'Close'`) to localize the Close button's name.
|
|
171
867
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
868
|
+
```tsx
|
|
869
|
+
import * as React from 'react';
|
|
870
|
+
import { Button, Dialog, Field, Input } from '@mortenbrudvik/waveui';
|
|
871
|
+
import type { DialogOpenChangeDetails } from '@mortenbrudvik/waveui';
|
|
872
|
+
|
|
873
|
+
export function RenameDialog() {
|
|
874
|
+
const [open, setOpen] = React.useState(false);
|
|
875
|
+
const [name, setName] = React.useState('');
|
|
876
|
+
const handleOpenChange = (next: boolean, details?: DialogOpenChangeDetails) => {
|
|
877
|
+
// Unsaved changes: a backdrop press keeps the dialog open; Escape and the buttons close it.
|
|
878
|
+
if (!next && name !== '' && details?.reason === 'outside-press') return;
|
|
879
|
+
setOpen(next);
|
|
880
|
+
};
|
|
881
|
+
return (
|
|
882
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
883
|
+
<Dialog.Trigger>
|
|
884
|
+
<Button>Rename</Button>
|
|
885
|
+
</Dialog.Trigger>
|
|
886
|
+
<Dialog.Content title="Rename project">
|
|
887
|
+
<Field label="New name">
|
|
888
|
+
<Input value={name} onValueChange={setName} />
|
|
889
|
+
</Field>
|
|
890
|
+
<Dialog.Footer>
|
|
891
|
+
<Dialog.Close>
|
|
892
|
+
<Button>Cancel</Button>
|
|
893
|
+
</Dialog.Close>
|
|
894
|
+
</Dialog.Footer>
|
|
895
|
+
</Dialog.Content>
|
|
896
|
+
</Dialog>
|
|
897
|
+
);
|
|
176
898
|
}
|
|
177
899
|
```
|
|
178
900
|
|
|
179
|
-
|
|
901
|
+
### Popovers and tooltips
|
|
180
902
|
|
|
181
|
-
|
|
903
|
+
```tsx
|
|
904
|
+
import { Button, Popover, Tooltip } from '@mortenbrudvik/waveui';
|
|
182
905
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
906
|
+
export function Author() {
|
|
907
|
+
return (
|
|
908
|
+
<Popover openOnHover>
|
|
909
|
+
<Popover.Trigger>
|
|
910
|
+
<Button appearance="transparent">Maria Lopez</Button>
|
|
911
|
+
</Popover.Trigger>
|
|
912
|
+
<Popover.Content title="Maria Lopez">
|
|
913
|
+
Product designer, Oslo.
|
|
914
|
+
<Tooltip content="Get her updates in your feed" openDelay={500}>
|
|
915
|
+
<Button size="small">Follow</Button>
|
|
916
|
+
</Tooltip>
|
|
917
|
+
</Popover.Content>
|
|
918
|
+
</Popover>
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
```
|
|
193
922
|
|
|
194
|
-
|
|
923
|
+
- **Hover cards.** `openOnHover` opens a Popover when the mouse rests on its trigger (`openDelay`, 250ms) and closes it `closeDelay` (500ms) after the pointer has left the trigger and the content, unless focus is inside the content (or in a layer opened from it). Opening and closing by hover move no focus (neither does the close of a hover card opened inside it); Tab from the trigger enters the content, as for any open Popover. A click on the trigger pins it (it stays open and focus stays where it is), and a second click closes it. Focus left on the trigger does not keep an unpinned card open. The triangle safe zone, the touch rule and "stays closed after Escape until the pointer leaves" are Menu's.
|
|
924
|
+
- **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`.
|
|
925
|
+
- **`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. TeachingPopover's `target` takes an element or a ref instead (it takes no `VirtualElement`, and Menu and Popover take no ref); a later release unifies them.
|
|
926
|
+
- **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).
|
|
195
927
|
|
|
196
|
-
|
|
928
|
+
### Enter and exit motion
|
|
197
929
|
|
|
198
|
-
|
|
930
|
+
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.
|
|
199
931
|
|
|
200
|
-
|
|
932
|
+
`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.
|
|
201
933
|
|
|
202
|
-
|
|
934
|
+
```tsx
|
|
935
|
+
import * as React from 'react';
|
|
936
|
+
import { Button, Presence } from '@mortenbrudvik/waveui';
|
|
203
937
|
|
|
204
|
-
|
|
938
|
+
export function SavedNotice() {
|
|
939
|
+
const [shown, setShown] = React.useState(false);
|
|
940
|
+
return (
|
|
941
|
+
<>
|
|
942
|
+
<Button onClick={() => setShown((value) => !value)}>Toggle notice</Button>
|
|
943
|
+
<Presence visible={shown}>
|
|
944
|
+
<p className="transition-[opacity,translate] duration-wave-normal ease-wave-decelerate-mid data-[presence=entering]:starting:translate-y-1 data-[presence=entering]:starting:opacity-0 data-[presence=exiting]:translate-y-1 data-[presence=exiting]:opacity-0 data-[presence=exiting]:duration-wave-fast data-[presence=exiting]:ease-wave-accelerate-mid motion-reduce:transition-none">
|
|
945
|
+
Saved
|
|
946
|
+
</p>
|
|
947
|
+
</Presence>
|
|
948
|
+
</>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
```
|
|
205
952
|
|
|
206
|
-
|
|
953
|
+
```css
|
|
954
|
+
/* The fade without Tailwind: class="notice" on the child */
|
|
955
|
+
.notice {
|
|
956
|
+
transition: opacity var(--wave-duration-normal) var(--wave-curve-decelerate-mid);
|
|
957
|
+
}
|
|
958
|
+
@starting-style {
|
|
959
|
+
.notice[data-presence='entering'] {
|
|
960
|
+
opacity: 0;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
.notice[data-presence='exiting'] {
|
|
964
|
+
opacity: 0;
|
|
965
|
+
transition-duration: var(--wave-duration-fast);
|
|
966
|
+
transition-timing-function: var(--wave-curve-accelerate-mid);
|
|
967
|
+
}
|
|
968
|
+
@media (prefers-reduced-motion: reduce) {
|
|
969
|
+
.notice {
|
|
970
|
+
transition: none;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
```
|
|
207
974
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
975
|
+
- **Reduced motion.** Pair every motion class with a `motion-reduce:` variant, as above: the tokens are never zeroed, and Wave has no global reduced-motion rule. Under `prefers-reduced-motion: reduce` the presence core ends every phase at once, even while a motion runs.
|
|
976
|
+
- **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.
|
|
977
|
+
- **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.
|
|
978
|
+
- **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.
|
|
979
|
+
- **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.
|
|
980
|
+
- `Menu.Popover` mounts through the presence core, so a menu takes exit classes too (see [Menus](#menus)). An enter class for a menu takes the same gate; a menu that is open when it mounts (`defaultOpen`) mounts `entered`, so it does not animate that first time. The other overlays get their motion in a later release.
|
|
214
981
|
|
|
215
|
-
|
|
982
|
+
### Toasts
|
|
216
983
|
|
|
217
|
-
|
|
984
|
+
Wrap the app (or the part that shows toasts) in `<Toaster>`; `useToastController()` throws in development outside it.
|
|
218
985
|
|
|
219
986
|
```tsx
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
987
|
+
import { Button, Toaster, useToastController } from '@mortenbrudvik/waveui';
|
|
988
|
+
|
|
989
|
+
function SaveButton() {
|
|
990
|
+
const { dispatchToast } = useToastController();
|
|
991
|
+
return (
|
|
992
|
+
<Button
|
|
993
|
+
onClick={() =>
|
|
994
|
+
dispatchToast({ status: 'success', title: 'Saved', body: 'Your changes are saved.' })
|
|
995
|
+
}
|
|
996
|
+
>
|
|
997
|
+
Save
|
|
998
|
+
</Button>
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export function App() {
|
|
1003
|
+
return (
|
|
1004
|
+
<Toaster position="bottom-end">
|
|
1005
|
+
<SaveButton />
|
|
1006
|
+
</Toaster>
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
224
1009
|
```
|
|
225
1010
|
|
|
226
|
-
|
|
1011
|
+
`dispatchToast` returns the toast id; `dismissToast(id)` removes it. Toast timers pause while a toast is hovered or focused and while the page is in the background (the window has lost focus, or the tab is hidden, also when the Toaster mounted there). Toasts stay reachable by Tab over an open Dialog and are announced through permanent live regions. `statusLabel` and `dismissLabel` (options of `dispatchToast`, and props of `<Toast>`) translate the hidden status text and the dismiss button's name.
|
|
227
1012
|
|
|
228
|
-
|
|
1013
|
+
- **`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. 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 its options and keeps its place; `dismissToast(id)` removes a waiting toast silently.
|
|
1014
|
+
- **`dismissAllToasts()`** (from `useToastController()`) 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.
|
|
229
1015
|
|
|
230
|
-
|
|
231
|
-
# Install dependencies
|
|
232
|
-
npm install
|
|
1016
|
+
### Badges
|
|
233
1017
|
|
|
234
|
-
|
|
235
|
-
|
|
1018
|
+
- `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.
|
|
1019
|
+
- **`important` changes in 1.0.** Through 0.x it renders the orange severe colors, as in 0.5. In 1.0 it becomes Fluent's neutral high-emphasis color (near black in the light theme): use `color="severe"` to keep the orange look.
|
|
1020
|
+
- A badge whose text alone does not say what it means ("3") can be named: with `aria-label` or `aria-labelledby` a Badge or CounterBadge gets `role="img"`, so the name is announced (a `role` you pass wins; an empty or whitespace-only name counts as absent). `<CounterBadge dot aria-label="Unread messages" />` is a named unread dot.
|
|
1021
|
+
- The root carries `data-color` and `data-appearance` with the resolved values (and `data-dot` on a dot), for your own selectors.
|
|
236
1022
|
|
|
237
|
-
|
|
238
|
-
npx vitest run
|
|
1023
|
+
### Tags
|
|
239
1024
|
|
|
240
|
-
|
|
241
|
-
npx tsc --noEmit
|
|
1025
|
+
A tag cannot keep focus once you remove it. When `onDismiss` removes a tag, first move focus to the next tag's dismiss button, else the previous one, else a nearby control (a "Reset filters" or "Add" button), and then remove the tag: the neighbour is still mounted, so focus never falls to the page. TagGroup (planned) will do this for you. The `FilterGroup` story in `stories/Tag.stories.tsx` is the reference:
|
|
242
1026
|
|
|
243
|
-
|
|
244
|
-
|
|
1027
|
+
```tsx
|
|
1028
|
+
import * as React from 'react';
|
|
1029
|
+
import { Button, Tag } from '@mortenbrudvik/waveui';
|
|
1030
|
+
|
|
1031
|
+
const ALL_FILTERS = ['Red', 'Blue', 'Large'];
|
|
1032
|
+
|
|
1033
|
+
export function Filters() {
|
|
1034
|
+
const [filters, setFilters] = React.useState(ALL_FILTERS);
|
|
1035
|
+
const tags = React.useRef(new Map<string, HTMLElement>());
|
|
1036
|
+
const reset = React.useRef<HTMLButtonElement>(null);
|
|
1037
|
+
|
|
1038
|
+
const dismiss = (filter: string) => {
|
|
1039
|
+
const index = filters.indexOf(filter);
|
|
1040
|
+
const neighbour = filters[index + 1] ?? filters[index - 1];
|
|
1041
|
+
// The dismiss button is the tag's only button.
|
|
1042
|
+
const target =
|
|
1043
|
+
neighbour === undefined
|
|
1044
|
+
? reset.current
|
|
1045
|
+
: tags.current.get(neighbour)?.querySelector('button');
|
|
1046
|
+
target?.focus();
|
|
1047
|
+
setFilters((current) => current.filter((f) => f !== filter));
|
|
1048
|
+
};
|
|
245
1049
|
|
|
246
|
-
|
|
247
|
-
|
|
1050
|
+
return (
|
|
1051
|
+
<div>
|
|
1052
|
+
<div role="group" aria-label="Active filters">
|
|
1053
|
+
{filters.map((filter) => (
|
|
1054
|
+
<Tag
|
|
1055
|
+
key={filter}
|
|
1056
|
+
dismissible
|
|
1057
|
+
dismissLabel="Remove"
|
|
1058
|
+
ref={(element) => {
|
|
1059
|
+
if (element) tags.current.set(filter, element);
|
|
1060
|
+
return () => {
|
|
1061
|
+
tags.current.delete(filter);
|
|
1062
|
+
};
|
|
1063
|
+
}}
|
|
1064
|
+
onDismiss={() => dismiss(filter)}
|
|
1065
|
+
>
|
|
1066
|
+
{filter}
|
|
1067
|
+
</Tag>
|
|
1068
|
+
))}
|
|
1069
|
+
</div>
|
|
1070
|
+
{/* Nothing to reset while every filter is set: unavailable, but still focusable. */}
|
|
1071
|
+
<Button
|
|
1072
|
+
ref={reset}
|
|
1073
|
+
disabledFocusable={filters.length === ALL_FILTERS.length}
|
|
1074
|
+
onClick={() => setFilters(ALL_FILTERS)}
|
|
1075
|
+
>
|
|
1076
|
+
Reset filters
|
|
1077
|
+
</Button>
|
|
1078
|
+
</div>
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Sorting and selecting in a DataGrid
|
|
1084
|
+
|
|
1085
|
+
Sorting is controlled: the grid reports the requested sort and **you reorder the rows**.
|
|
1086
|
+
|
|
1087
|
+
```tsx
|
|
1088
|
+
import * as React from 'react';
|
|
1089
|
+
import { DataGrid } from '@mortenbrudvik/waveui';
|
|
1090
|
+
import type { DataGridColumn, DataGridSort } from '@mortenbrudvik/waveui';
|
|
1091
|
+
|
|
1092
|
+
interface Person {
|
|
1093
|
+
id: string;
|
|
1094
|
+
name: string;
|
|
1095
|
+
role: string;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const columns: DataGridColumn[] = [
|
|
1099
|
+
{ id: 'name', label: 'Name', sortable: true },
|
|
1100
|
+
{ id: 'role', label: 'Role' },
|
|
1101
|
+
];
|
|
1102
|
+
|
|
1103
|
+
export function People({ people }: { people: Person[] }) {
|
|
1104
|
+
const [sort, setSort] = React.useState<DataGridSort | null>(null);
|
|
1105
|
+
const rows = React.useMemo(() => {
|
|
1106
|
+
if (!sort) return people;
|
|
1107
|
+
const sorted = [...people].sort((a, b) => a.name.localeCompare(b.name));
|
|
1108
|
+
return sort.direction === 'ascending' ? sorted : sorted.reverse();
|
|
1109
|
+
}, [people, sort]);
|
|
1110
|
+
|
|
1111
|
+
return (
|
|
1112
|
+
<DataGrid
|
|
1113
|
+
aria-label="People"
|
|
1114
|
+
columns={columns}
|
|
1115
|
+
sort={sort}
|
|
1116
|
+
onSortChange={setSort}
|
|
1117
|
+
selectionMode="multiple"
|
|
1118
|
+
>
|
|
1119
|
+
<DataGrid.Body>
|
|
1120
|
+
{rows.map((person) => (
|
|
1121
|
+
<DataGrid.Row key={person.id} rowId={person.id}>
|
|
1122
|
+
<DataGrid.Cell>{person.name}</DataGrid.Cell>
|
|
1123
|
+
<DataGrid.Cell>{person.role}</DataGrid.Cell>
|
|
1124
|
+
</DataGrid.Row>
|
|
1125
|
+
))}
|
|
1126
|
+
</DataGrid.Body>
|
|
1127
|
+
</DataGrid>
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
Row selection: with `selectionMode="single"`, pass at most one id in `selectedItems`/`defaultSelectedItems` (with several, the first one whose row is rendered is selected, with a development warning). Each row's selection control is named after the row's first `DataGrid.Cell`, `<td>` or `<th>` child (Fragments are looked into); a cell rendered by another component is not seen, so pass `selectionLabel` to `DataGrid.Row` then.
|
|
1133
|
+
|
|
1134
|
+
### Slots
|
|
1135
|
+
|
|
1136
|
+
Props such as `icon`, `contentBefore` and `dismiss` are slots. A slot accepts content (rendered inside the slot's element) or an object with `as`, `className`, `children` and any attribute of the slot's element:
|
|
1137
|
+
|
|
1138
|
+
```tsx
|
|
1139
|
+
import { Input } from '@mortenbrudvik/waveui';
|
|
1140
|
+
|
|
1141
|
+
export const Price = () => (
|
|
1142
|
+
<Input
|
|
1143
|
+
aria-label="Price"
|
|
1144
|
+
contentBefore="$"
|
|
1145
|
+
contentAfter={{ children: 'USD', className: 'text-muted-foreground' }}
|
|
1146
|
+
/>
|
|
1147
|
+
);
|
|
248
1148
|
```
|
|
249
1149
|
|
|
250
|
-
|
|
1150
|
+
A slot takes any `React.ReactNode`. Spread an attributes object typed by an interface into a new object (`image={{ ...imgProps }}` for a `React.ImgHTMLAttributes<HTMLImageElement>`).
|
|
1151
|
+
|
|
1152
|
+
Icon slots are decorative (`aria-hidden`). Content that renders nothing (`''`, `[]`, `<></>`, or a collection of only `null`, booleans and `''`) counts as no content: a `contentAfter` that renders nothing takes no room, and SearchBox's `contentBefore` keeps the default search icon. The dismiss and clear slots of `MessageBar`, `SearchBox` and `Tag` render their content inside the component's own button; the 0.4 form that passed a button object is deprecated. A Wave `Button` passed there is merged into that button, and its own props never reach the DOM: its `icon` and `iconPosition` place the icon, and `disabledFocusable` makes the built-in button unavailable but focusable. Content that renders nothing keeps the default icon, and a slot object without children (`{ className: 'text-error' }`) wraps it. A slot object that renders a component or a void element (`{ as: CloseIcon }`, `{ as: 'img', src, alt: '' }`) or sets `dangerouslySetInnerHTML` is the icon itself. On `MessageBar`, `dismiss={null}` is the only value that hides the dismiss button (a boolean counts as no slot).
|
|
1153
|
+
|
|
1154
|
+
Slots that replace a built-in glyph follow one rule per kind:
|
|
1155
|
+
|
|
1156
|
+
- **Optional indicators** (`MenuButton.menuIcon`, the `expandIcon` of Combobox and TimePicker): `null` or `undefined` keep the default glyph; `false` or any value that renders nothing hides it (for `expandIcon`, the whole expand button).
|
|
1157
|
+
- **Required indicators** (`SplitButton.menuIcon`): the indicator always shows. A value that renders nothing keeps the default glyph, with a development warning.
|
|
1158
|
+
- **Status icons** (`MessageBar.icon`, Field `validationMessageIcon`): only `undefined` keeps the default icon; `null` or a value that renders nothing shows none.
|
|
1159
|
+
- A glyph slot inside a built-in button (`expandIcon`, and the `menuIcon` of SplitButton and MenuButton) is decorative content. A `<button>` or `Button` passed there, or a slot object whose `as` is one, is not nested: its children become the glyph and its props are dropped, with a development warning.
|
|
1160
|
+
|
|
1161
|
+
### Built-in text
|
|
1162
|
+
|
|
1163
|
+
The names and hidden texts that components render themselves are English by default. These props translate them:
|
|
1164
|
+
|
|
1165
|
+
| Component | Props |
|
|
1166
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1167
|
+
| `Dialog.Content`, `Drawer` | `closeLabel` (the Close button) |
|
|
1168
|
+
| `TeachingPopover` | `closeLabel` (the Close button), `labels` (`back`, `next`, `done`, `step(index, count)` with a zero-based index) |
|
|
1169
|
+
| `MessageBar`, `Toast` | `statusLabel` (the hidden severity text); `Toast` also `dismissLabel` (also options of `dispatchToast`) |
|
|
1170
|
+
| `Tag` | `dismissLabel` |
|
|
1171
|
+
| `Carousel` | `labels` (`previous`, `next`, `picker`, `slide(index, total)` with a zero-based index, `carouselRoleDescription`, `slideRoleDescription`), `autoPlayLabels` |
|
|
1172
|
+
| `Stepper` | `statusLabels` (`completed`, `error`; `''` omits the text) |
|
|
1173
|
+
| `Pagination` | `getItemAriaLabel(type, page, selected)` |
|
|
1174
|
+
| `ColorPicker` | `labels` |
|
|
1175
|
+
| `Combobox` | `labels` (`noMatches`, `clear` (the clear button), `expand` (the chevron button)) |
|
|
1176
|
+
| `Dropdown` | `labels` (`clear`, the clear button) |
|
|
1177
|
+
| `DatePicker` | `labels` (`clear`, `openCalendar`, `previousMonth`, `nextMonth`, and the error texts `invalidDate(pattern)`, `outOfRange(min, max)`, `unavailableDate`) |
|
|
1178
|
+
| `TimePicker` | `labels` (`clear`, `expand`, `list`, `noTimes`, `noMatches`, and the error texts `invalidTime(format)`, `outOfRange(min, max)`) |
|
|
1179
|
+
| `TagPicker` | `labels` (`remove(label)`, `selected`, `summary(labels)`, `added(label, count)`, `removed(label, count)`, `noMatches`) |
|
|
1180
|
+
| `Rating` | `labels` (`star(value, max)`) |
|
|
1181
|
+
| `RatingDisplay` | `labels` (`rating(value, max, formattedValue)`, `count(count, formattedCount)`); `locale` formats the value and the count (pass it when rendering on the server) |
|
|
1182
|
+
| `SpinButton` | `labels` (`increment`, `decrement`) |
|
|
1183
|
+
| `DataGrid` | `labels` (`selectAll`, `selectionHeader`); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
|
|
1184
|
+
| `AvatarGroup` | `overflowLabel`, `unnamedMemberLabel` |
|
|
1185
|
+
| `InfoLabel`, `SplitButton` | `infoButtonLabel`, `menuButtonLabel` |
|
|
1186
|
+
| `Spinner`, `Skeleton.Group` | `label` |
|
|
1187
|
+
|
|
1188
|
+
Without a text prop, these defaults are replaced through `aria-label`: the PresenceBadge status names ("Available", …: `aria-label` on the badge), the MessageBar "Dismiss" and SearchBox "Clear search" button names (`dismiss={{ 'aria-label': 'Lukk' }}`, which keeps the default icon), and the names of the Breadcrumb, Pagination, Nav, Stepper, Carousel and Toaster regions ("Breadcrumb", "Pagination", "Navigation", "Progress", "Carousel", "Notifications"). The placeholders of Dropdown, DatePicker, TimePicker and SearchBox are `placeholder` props, and ColorPicker's default presets are replaced through `presets`.
|
|
1189
|
+
|
|
1190
|
+
0.7 adds no built-in text: the submenu half of a `Menu.SplitGroup`, a context menu (`Menu.Popover`) and a `Toolbar.RadioGroup` are named by the `aria-label` you pass.
|
|
1191
|
+
|
|
1192
|
+
## Keyboard support
|
|
1193
|
+
|
|
1194
|
+
As implemented in 0.7. Buttons, links, checkboxes, switches and the trigger buttons of Accordion, Carousel, Nav, Breadcrumb and Pagination are native elements: Tab to reach them, Enter or Space to activate. A Link or `Button as="a"` without `href`, and a Button or Link rendered as a `div` or `span`, are buttons: Tab reaches them, Enter (key down) and Space (key up) activate them. A `disabledFocusable` control stays in the Tab order but Enter, Space and clicks do nothing.
|
|
1195
|
+
|
|
1196
|
+
| Component | Keys |
|
|
1197
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1198
|
+
| Toolbar | One Tab stop (the last focused control; focusing a nested composite, such as a radio group or tab list, or a text field, select, slider or spin button keeps it where it was). Left/Right (Up/Down when vertical) move and wrap, Home/End jump; mirrored in RTL; natively disabled controls and controls hidden with CSS are skipped, while `disabledFocusable` controls stay reachable (a Tooltip on them opens on focus). Text fields, selects, sliders, spin buttons and editable comboboxes keep their own arrow keys; a Dropdown keeps Up/Down, Home and End for its list, but Left/Right move past it. A nested composite keeps its own Tab stop and arrow keys; a `Toolbar.RadioGroup` is none: its radios are part of the arrow order, and Up/Down (Left/Right when vertical, mirrored in RTL) also move among them, wrapping inside the group. The arrows never press a toggle or check a radio: Space and Enter do. |
|
|
1199
|
+
| RadioGroup, SwatchPicker | One Tab stop. All four arrows move and select (wrapping), Home/End jump; Left/Right mirrored in RTL. Keys pressed on a link inside a radio's label stay the link's. |
|
|
1200
|
+
| Rating | Right/Up one star more, Left/Down one fewer (never below 1), Home/End first/last; mirrored in RTL. |
|
|
1201
|
+
| TabList | One Tab stop (the selected tab). Left/Right (Up/Down when vertical) move and select, Home/End; with `selectTabOnFocus={false}` they only move focus, and Enter or Space selects the focused tab; disabled tabs skipped; mirrored in RTL. |
|
|
1202
|
+
| Tree | One Tab stop. Up/Down, Home/End; Right expands or moves to the first child, Left collapses or moves to the parent (mirrored in RTL); Enter/Space activate; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). |
|
|
1203
|
+
| List (selectable) | One Tab stop. Up/Down (wrapping), Home/End, Enter/Space toggle, typeahead above 7 items (a Space within 500 ms of a letter continues the search). With item actions (grid): Up/Down between rows, Left/Right into the actions, Enter/F2 into a text field, Escape back. |
|
|
1204
|
+
| DataGrid | One Tab stop. Arrows between cells (Left/Right to the previous/next cell of the row, mirrored in RTL; Up/Down and PageUp/PageDown keep the visual column across `colSpan`/`rowSpan` cells of grouped headers), Home/End row start/end, Ctrl+Home/Ctrl+End grid start/end, PageUp/PageDown 10 rows; Enter/F2 into a cell's widgets, Escape back; Space on a cell toggles its row's selection. |
|
|
1205
|
+
| Menu | Trigger: Enter/Space/Down open and focus the first item, Up opens on the last; a trigger with `aria-disabled="true"` (a `disabledFocusable` MenuButton or SplitButton) ignores them and its click. Menu: Up/Down (wrapping), Home/End, typeahead, Enter/Space activate (a Space within 500 ms of a letter continues the search); on a checkbox, radio or switch item Space changes it and keeps the menu open, Enter changes it and closes a popup menu; on `Menu.ItemLink` Enter is the browser's link activation. A submenu's item: Right (Left in RTL), Enter and Space open the submenu and focus its first item; in a submenu Left (Right in RTL) and Escape close only it and return focus to its item. `Menu.SplitGroup`: Right (Left in RTL) moves from the action to the submenu button, Left back. Escape closes and returns focus, Tab closes every level. Context menu (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it at the focused element; Escape and item activation return focus there, and Tab moves on from it. |
|
|
1206
|
+
| Combobox, TagPicker, TimePicker | Down/Up open and move, Alt+Down opens, typing makes the first match active (not in a `freeform` Combobox), Enter commits the active option (with no active option, Enter submits the form; TimePicker instead never submits while its text is edited: a complete time within the bounds is committed, erased text clears the value, other text is kept and flagged invalid, and the list closes; leaving the input with edited text settles it the same way and closes the list), Alt+Up and Tab close, Escape closes and then discards typed text. The chevron button (Combobox, TimePicker) is not a Tab stop; the clear button (`clearable`) is a Tab stop after the input. Tab from erased TimePicker text clears the value first, so focus moves past the clear button that goes with it. TagPicker: Backspace in the empty input focuses the last tag, Backspace/Delete removes it. |
|
|
1207
|
+
| Dropdown | Down/Up/Home/End and typing open and move, PageUp/PageDown move by 10, Enter/Space commit (a Space within 500 ms of a letter continues the search), Alt+Up and Tab commit and close, Escape closes. The clear button (`clearable`) is a Tab stop after the combobox button. |
|
|
1208
|
+
| SpinButton | Up/Down step, PageUp/PageDown large step, Home/End jump to a finite min/max (otherwise they move the caret); Enter commits typed text, Escape reverts it. |
|
|
1209
|
+
| SearchBox | Escape clears the text and keeps focus (the key is consumed, so an enclosing Dialog, Drawer or Popover stays open); in an empty field Escape reaches the enclosing overlay. |
|
|
1210
|
+
| DatePicker | Alt+Down opens the calendar. The clear button (`clearable`) is a Tab stop between the input and the calendar button; Tab from erased text clears the value first and moves on to the calendar button. Calendar: arrows by day/week (mirrored in RTL), PageUp/PageDown by month, Shift+PageUp/PageDown by year, Home/End week start/end, Enter/Space select, Escape closes; Tab stays inside. |
|
|
1211
|
+
| Dialog, Drawer | Focus is trapped; Escape closes; focus returns to the first of these that can take focus: `finalFocusRef`, the element that had focus when the modal opened (a trigger's wrapper span resolves to the element inside it), the trigger, an element next to where that opener was. |
|
|
1212
|
+
| Popover, TeachingPopover | Escape closes. When focus was inside (or lost to `<body>`), Popover returns it to its trigger, TeachingPopover to where focus was before it opened; a close by hover moves no focus. In the Tab order the content follows its trigger (TeachingPopover with `target`: the target): Tab from the trigger enters it, Tab past its end continues after the trigger, Shift+Tab from the element after the trigger enters it at its last element. For a trigger outside the tab order it follows the tab stop before the trigger; with no tab stop before the trigger or target, it is reached at the end of the page. A Tab lap visits it once: Tab from the last element of the page leaves the page, and Shift+Tab from outside the page reaches the page's last element. Focus leaving it does not close it. A context popover (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it and moves focus into the content, and Escape returns focus to the element that had it; opened by a right click, it is reached by Tab from that element. |
|
|
1213
|
+
| Tooltip, InfoLabel | Open on keyboard focus (Tooltip after `openDelay`); Escape closes. InfoLabel: click pins it. |
|
|
1214
|
+
| Card (selectable) | With the default `selectionControl="card"`: Enter (key down) or Space (key up) selects. With `"checkbox"`: the built-in checkbox. |
|
|
1215
|
+
| Stepper | Each reachable step is a Tab stop; Enter/Space activate. |
|
|
1216
|
+
| Slider | Native range keys. |
|
|
1217
|
+
|
|
1218
|
+
Accordion and Carousel have no arrow-key navigation: their triggers and controls are Tab stops.
|
|
1219
|
+
|
|
1220
|
+
Typeahead (Menu, selectable List, Tree, Dropdown) accepts characters typed with AltGr, which Windows reports as Ctrl+Alt (Polish `ł`, Romanian `ș`); Ctrl+Alt with an arrow, Home, End or Space is left to the browser. An item that cannot take focus is passed over for the next match. Menu typeahead matches the item labels, not icons or shortcuts. Options and option groups with `hidden` are skipped like a native `<option hidden>`.
|
|
1221
|
+
|
|
1222
|
+
## Hooks and utilities
|
|
1223
|
+
|
|
1224
|
+
| Export | Purpose |
|
|
1225
|
+
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1226
|
+
| `useControllable(value, defaultValue, onChange)` | Controlled/uncontrolled state; returns `[value, setValue, isControlled]`; `setValue` accepts a value or an updater and skips no-op updates. |
|
|
1227
|
+
| `useRovingTabIndex(options)` | Roving tab stop and arrow-key navigation for composite widgets; items marked `data-disabled-focusable` stay reachable, and a composite marked `data-roving-transparent` (a toolbar radio group) adds its items to the enclosing arrow order. |
|
|
1228
|
+
| `useId(prefix?)` | SSR-safe ids (`prefix-<react id>`; never parse the React part). |
|
|
1229
|
+
| `useEventCallback(fn)` | Stable callback that always calls the latest `fn`. |
|
|
1230
|
+
| `useMergedRefs(...refs)` | One stable callback ref for several refs. |
|
|
1231
|
+
| `useIsClient()` | `false` on the server and during hydration. |
|
|
1232
|
+
| `usePresence(visible, options?)`, `<Presence visible>` | Mount and unmount an element with CSS enter and exit motion (`data-presence` phases; see [Enter and exit motion](#enter-and-exit-motion)). |
|
|
1233
|
+
| `useFieldControl(props, options?)` | Merge a custom control's labelling props with the surrounding `Field` (its label, validation message, hint and state). |
|
|
1234
|
+
| `useAnnounce()`, `announce(message, politeness?)` | Screen-reader announcements through a shared live region. |
|
|
1235
|
+
| `useWaveTheme()` | Current theme, direction and portal container. |
|
|
1236
|
+
| `useToastController()` | `dispatchToast` / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
|
|
1237
|
+
| `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
|
|
1238
|
+
| `cn(...classes)` | clsx + tailwind-merge that knows Wave's type ramp and shadows; the last class wins over a conflicting class of the same variant. |
|
|
1239
|
+
| `composeEventHandlers(theirs, ours)` | Run a consumer handler, then yours unless it called `preventDefault()`. |
|
|
1240
|
+
| `mergeRefs(...refs)` | Merge refs outside components. |
|
|
1241
|
+
| `resolveSlot`, `renderSlot` | The slot helpers the components use. |
|
|
1242
|
+
| `getThemeClassName(theme)` | The theme classes of a theme. |
|
|
1243
|
+
|
|
1244
|
+
## Upgrading from 0.6
|
|
1245
|
+
|
|
1246
|
+
0.7 removes nothing public; these changes can still affect your setup, tests and styles:
|
|
1247
|
+
|
|
1248
|
+
1. Menu surfaces render `data-presence` next to `data-state`, and stay mounted (`data-state="closed"`, `inert`) while an exit motion of yours runs; without exit classes a menu still unmounts at once.
|
|
1249
|
+
2. Menu items render a hidden placeholder before the label for each column they do not fill (the check column, the icon column), and a menu that mixes items with and without icons now lines the labels up.
|
|
1250
|
+
3. Toolbar renders `data-size` and `data-orientation`, and `Toolbar` has static parts.
|
|
1251
|
+
4. A ToggleButton with a checked `role` (`checkbox`, `radio`, `switch`, `menuitemcheckbox`, `menuitemradio`, `option`, `treeitem`) renders `aria-checked` instead of `aria-pressed`, and one with another role renders neither and warns; every pressed ToggleButton renders `data-pressed`.
|
|
1252
|
+
5. Tooltip `delay` is deprecated: rename it to `openDelay` (it still works and warns once in development).
|
|
1253
|
+
6. `Menu.Item` `persistOnClick` defaults to the Menu's new `persistOnItemClick` (`false`): no change unless you set it.
|
|
1254
|
+
7. While focus is in a menu, the item under the mouse takes focus: a test that hovers an item and then presses Enter activates the hovered item.
|
|
1255
|
+
8. Wave now requires tailwind-merge 3.7, so an axis shorthand in your `className` (`px-6`) replaces a component's own logical side class of the same variant (`pe-8` on `Dialog.Title`) instead of losing to it in the CSS: write `px-6 pe-8` to keep the side.
|
|
1256
|
+
9. Wave requires Node.js 22.12 or later (`engines`; Node.js 20 reached its end of life on 2026-04-30). npm and pnpm warn on an older Node.js (npm stops with `engine-strict`), and Yarn 1 refuses to install.
|
|
1257
|
+
|
|
1258
|
+
The CHANGELOG's [0.7.0 "Changed" section](CHANGELOG.md#changed) lists every behaviour, DOM and type change.
|
|
1259
|
+
|
|
1260
|
+
## Upgrading from 0.5
|
|
1261
|
+
|
|
1262
|
+
0.6 removes nothing public; these changes can still affect tests and styles:
|
|
1263
|
+
|
|
1264
|
+
1. Dialog and Drawer call `onOpenChange(open, details)`: test assertions such as `toHaveBeenCalledWith(false)` need a second argument (`expect.anything()`, or `expect.objectContaining({ reason: 'escape' })`).
|
|
1265
|
+
2. A Field keeps its hint visible below a validation message, and error messages show an icon (`validationMessageIcon={null}` removes it).
|
|
1266
|
+
3. A Link or `Button as="a"` without `href` is a button (`role="button"`, a tab stop, Enter and Space). A disabled one is `role="button"` too (was `link`), with no tab stop and no activation. A disabled Link no longer lets its click reach ancestor handlers.
|
|
1267
|
+
4. TimePicker keeps rejected text on blur and flags it instead of reverting it; the list closes.
|
|
1268
|
+
5. Combobox and TimePicker show a chevron button (`expandIcon={false}` hides it), and Combobox and Dropdown wrap their control in a new `<div>`.
|
|
1269
|
+
6. Code that implements `ToastController` (a test double) adds `dismissAllToasts`.
|
|
1270
|
+
7. RatingDisplay formats its accessible name with the runtime locale when `locale` is omitted: pass `locale` when rendering on the server.
|
|
1271
|
+
8. Badge `color="important"` becomes a neutral color in 1.0: switch to `color="severe"` to keep the orange look.
|
|
1272
|
+
|
|
1273
|
+
The CHANGELOG's [0.6.0 "Changed" section](CHANGELOG.md#changed-1) lists every behaviour, DOM and type change.
|
|
1274
|
+
|
|
1275
|
+
## Upgrading from 0.4
|
|
1276
|
+
|
|
1277
|
+
npm went from 0.4.0 to 0.7.0: 0.5.0 and 0.6.0 were never published, and their changes ship in 0.7.0. From 0.4.0, take these steps, then those of [Upgrading from 0.5](#upgrading-from-05) and [Upgrading from 0.6](#upgrading-from-06).
|
|
1278
|
+
|
|
1279
|
+
1. The npm package name is unchanged (`@mortenbrudvik/waveui`, as in 0.4.0). Replace `waveui` imports copied from the 0.4 guide, and a git or local dependency named `waveui`, with `@mortenbrudvik/waveui`.
|
|
1280
|
+
2. Styles: without Tailwind, keep `import '@mortenbrudvik/waveui/styles'` and make sure a `WaveProvider` wraps the app; with Tailwind 4, switch to `@import '@mortenbrudvik/waveui/tailwind';` after `@import 'tailwindcss';`. Import `preflight.css` if you relied on Wave's Preflight.
|
|
1281
|
+
3. Rename CSS overrides of the 0.4 semantic variables (`--primary`, `--border`, …) to `--wave-*`. Code that reads 0.4 variables (`var(--primary)`, `var(--ring)`, `var(--brand-80)`, `var(--grey-14)`) must switch to the `--wave-*` names too: 0.5 no longer defines them. Until then, import `legacy-tokens.css`. Overrides of the ramp names (`--brand-*`, `--grey-*`) keep working without it.
|
|
1282
|
+
4. A required `Field` now turns on native constraint validation for the control inside it, including Checkbox, Switch and the other choice and picker controls (see [Forms and `Field`](#forms-and-field)). Add `noValidate` to forms that validate in their submit handler.
|
|
1283
|
+
5. Replace deprecated props at your own pace; each warns once in development.
|
|
1284
|
+
|
|
1285
|
+
The [CHANGELOG](CHANGELOG.md) lists every change, including behaviour and DOM changes that can affect tests and styles.
|
|
1286
|
+
|
|
1287
|
+
## Development
|
|
251
1288
|
|
|
252
|
-
|
|
1289
|
+
Working on Wave needs Node.js 22.22.2+, 24.15+ or 26+ (not 25) and npm 11.11 or later (`devEngines` in `package.json`). npm stops on other Node.js versions, which the development dependencies do not support, and warns about an older npm, which drops the `libc` fields from `package-lock.json`. `allowScripts` in `package.json` denies the install scripts of esbuild and @parcel/watcher: their prebuilt binaries come from optional dependencies. npm 12 blocks every other dependency install script until it is approved; `npm install-scripts ls` lists them.
|
|
253
1290
|
|
|
254
1291
|
```bash
|
|
255
|
-
|
|
1292
|
+
npm install
|
|
1293
|
+
npm run dev # Storybook on http://localhost:6006 (theme and direction toolbars)
|
|
1294
|
+
npm test # unit, integration, conventions and stories accessibility tests (Vitest)
|
|
1295
|
+
npm run typecheck # library, dev (tests + stories) and node TypeScript programs
|
|
1296
|
+
npm run lint # ESLint (fails on any warning)
|
|
1297
|
+
npm run build # type-check, library build, CSS build, dist verification
|
|
1298
|
+
npm run check:package # publint + are-the-types-wrong on a packed tarball
|
|
1299
|
+
npm run test:pack # pack the tarball and smoke-test it in plain and Tailwind fixtures
|
|
256
1300
|
```
|
|
257
1301
|
|
|
258
|
-
|
|
1302
|
+
GitHub Actions runs these checks, `npm run format:check` and a Storybook build on every push to `main` and every pull request (`.github/workflows/ci.yml`), and the tests, build and pack smoke test also on Node.js 22 and 26 and on Windows.
|
|
1303
|
+
|
|
1304
|
+
### Releasing
|
|
1305
|
+
|
|
1306
|
+
Releases are published by GitHub Actions, never from a local machine: set the version (`npm version <x.y.z> --no-git-tag-version`), date its CHANGELOG section (`## [x.y.z] - YYYY-MM-DD`), merge to `main`, then tag that commit `vX.Y.Z` and push the tag. The release workflow checks the tag against `package.json` and the CHANGELOG, runs the full gate, publishes to npm through trusted publishing with a provenance attestation, and creates the GitHub release. [docs/RELEASING.md](docs/RELEASING.md) has the exact steps, the one-time setup and how to verify a release.
|
|
259
1307
|
|
|
260
1308
|
## License
|
|
261
1309
|
|