@mortenbrudvik/waveui 0.4.0 → 0.8.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 +1484 -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 +51 -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 +57 -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 +55 -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 +38 -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 +46 -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 +67 -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 +68 -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 +64 -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 +126 -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 +162 -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 +135 -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 +33 -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 +29 -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 +53 -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 +33 -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 +105 -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 +382 -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 +50 -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 +72 -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 +152 -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 +202 -0
- package/dist/components/feedback/MessageBar.mjs.map +1 -0
- package/dist/components/feedback/MessageBar.parts.cjs +2 -0
- package/dist/components/feedback/MessageBar.parts.cjs.map +1 -0
- package/dist/components/feedback/MessageBar.parts.mjs +84 -0
- package/dist/components/feedback/MessageBar.parts.mjs.map +1 -0
- package/dist/components/feedback/MessageBar.reflow.cjs +2 -0
- package/dist/components/feedback/MessageBar.reflow.cjs.map +1 -0
- package/dist/components/feedback/MessageBar.reflow.mjs +74 -0
- package/dist/components/feedback/MessageBar.reflow.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 +46 -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 +73 -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 +49 -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 +61 -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.context.cjs +2 -0
- package/dist/components/feedback/Toast.context.cjs.map +1 -0
- package/dist/components/feedback/Toast.context.mjs +23 -0
- package/dist/components/feedback/Toast.context.mjs.map +1 -0
- package/dist/components/feedback/Toast.mjs +15 -0
- package/dist/components/feedback/Toast.mjs.map +1 -0
- package/dist/components/feedback/Toast.parts.cjs +2 -0
- package/dist/components/feedback/Toast.parts.cjs.map +1 -0
- package/dist/components/feedback/Toast.parts.mjs +98 -0
- package/dist/components/feedback/Toast.parts.mjs.map +1 -0
- package/dist/components/feedback/Toast.root.cjs +2 -0
- package/dist/components/feedback/Toast.root.cjs.map +1 -0
- package/dist/components/feedback/Toast.root.mjs +109 -0
- package/dist/components/feedback/Toast.root.mjs.map +1 -0
- package/dist/components/feedback/Toaster.cjs +2 -0
- package/dist/components/feedback/Toaster.cjs.map +1 -0
- package/dist/components/feedback/Toaster.mjs +564 -0
- package/dist/components/feedback/Toaster.mjs.map +1 -0
- package/dist/components/input/AlphaSlider.cjs +2 -0
- package/dist/components/input/AlphaSlider.cjs.map +1 -0
- package/dist/components/input/AlphaSlider.mjs +115 -0
- package/dist/components/input/AlphaSlider.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 +97 -0
- package/dist/components/input/Checkbox.mjs.map +1 -0
- package/dist/components/input/ColorArea.cjs +2 -0
- package/dist/components/input/ColorArea.cjs.map +1 -0
- package/dist/components/input/ColorArea.mjs +204 -0
- package/dist/components/input/ColorArea.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 +213 -0
- package/dist/components/input/ColorPicker.mjs.map +1 -0
- package/dist/components/input/ColorPicker.parts.cjs +2 -0
- package/dist/components/input/ColorPicker.parts.cjs.map +1 -0
- package/dist/components/input/ColorPicker.parts.mjs +192 -0
- package/dist/components/input/ColorPicker.parts.mjs.map +1 -0
- package/dist/components/input/ColorSlider.cjs +2 -0
- package/dist/components/input/ColorSlider.cjs.map +1 -0
- package/dist/components/input/ColorSlider.mjs +138 -0
- package/dist/components/input/ColorSlider.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 +192 -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 +341 -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 +131 -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 +201 -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 +82 -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 +29 -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 +171 -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 +140 -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 +197 -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 +204 -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 +34 -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 +80 -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 +158 -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 +114 -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 +93 -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 +253 -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 +33 -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 +295 -0
- package/dist/components/input/TimePicker.mjs.map +1 -0
- package/dist/components/input/colorName.cjs +2 -0
- package/dist/components/input/colorName.cjs.map +1 -0
- package/dist/components/input/colorName.mjs +30 -0
- package/dist/components/input/colorName.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 +194 -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 +237 -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 +122 -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 +206 -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 +55 -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 +63 -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 +270 -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 +40 -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 +239 -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.context.cjs +2 -0
- package/dist/components/layout/Tree.context.cjs.map +1 -0
- package/dist/components/layout/Tree.context.mjs +33 -0
- package/dist/components/layout/Tree.context.mjs.map +1 -0
- package/dist/components/layout/Tree.core.cjs +2 -0
- package/dist/components/layout/Tree.core.cjs.map +1 -0
- package/dist/components/layout/Tree.core.mjs +416 -0
- package/dist/components/layout/Tree.core.mjs.map +1 -0
- package/dist/components/layout/Tree.itemPersonaLayout.cjs +2 -0
- package/dist/components/layout/Tree.itemPersonaLayout.cjs.map +1 -0
- package/dist/components/layout/Tree.itemPersonaLayout.mjs +46 -0
- package/dist/components/layout/Tree.itemPersonaLayout.mjs.map +1 -0
- package/dist/components/layout/Tree.mjs +12 -0
- package/dist/components/layout/Tree.mjs.map +1 -0
- package/dist/components/layout/Tree.selection.cjs +2 -0
- package/dist/components/layout/Tree.selection.cjs.map +1 -0
- package/dist/components/layout/Tree.selection.mjs +73 -0
- package/dist/components/layout/Tree.selection.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 +145 -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 +60 -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 +133 -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 +47 -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 +232 -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 +309 -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 +111 -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 +52 -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 +291 -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 +118 -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 +205 -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 +181 -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 +193 -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 +367 -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 +185 -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 +161 -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 +46 -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 +52 -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 +490 -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 +100 -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 +39 -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/useMediaQuery.cjs +2 -0
- package/dist/hooks/useMediaQuery.cjs.map +1 -0
- package/dist/hooks/useMediaQuery.mjs +20 -0
- package/dist/hooks/useMediaQuery.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 +10 -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 +11086 -0
- package/dist/index.d.ts +10405 -1757
- package/dist/index.mjs +103 -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/classNames.cjs +2 -0
- package/dist/lib/classNames.cjs.map +1 -0
- package/dist/lib/classNames.mjs +415 -0
- package/dist/lib/classNames.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/color.cjs +2 -0
- package/dist/lib/color.cjs.map +1 -0
- package/dist/lib/color.mjs +86 -0
- package/dist/lib/color.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/lib/toaster.cjs +2 -0
- package/dist/lib/toaster.cjs.map +1 -0
- package/dist/lib/toaster.mjs +77 -0
- package/dist/lib/toaster.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,1572 @@
|
|
|
1
|
-
# waveui
|
|
1
|
+
# @mortenbrudvik/waveui
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A React component library inspired by the Fluent UI 2 design language: 68 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) · [Styling hooks](#styling-hooks)
|
|
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.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
|
|
11
|
+
|
|
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
|
+
**The page outside the provider.** The page's scrollbar and its canvas (the overscroll area and the space below short content) take their colours from `<html>`, which a provider never touches. To match them, put the theme classes on `<html>` as well, which sets `color-scheme` there; in a server-rendered layout that is `<html className={getThemeClassName(theme)}>` (safe in a Server Component). `color-scheme` gives the canvas the browser's default colour for the scheme; for the theme's own background, add `html { background-color: var(--wave-background); }` to your CSS.
|
|
128
|
+
|
|
129
|
+
**Following the operating system.** `theme` takes one of the named themes, so resolve the OS setting in your app and pass the result:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import * as React from 'react';
|
|
133
|
+
import { WaveProvider, getThemeClassName, type WaveTheme } from '@mortenbrudvik/waveui';
|
|
134
|
+
|
|
135
|
+
const DARK = '(prefers-color-scheme: dark)';
|
|
136
|
+
|
|
137
|
+
function subscribe(onChange: () => void) {
|
|
138
|
+
const query = window.matchMedia(DARK);
|
|
139
|
+
query.addEventListener('change', onChange);
|
|
140
|
+
return () => query.removeEventListener('change', onChange);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function useSystemTheme(): WaveTheme {
|
|
144
|
+
const dark = React.useSyncExternalStore(
|
|
145
|
+
subscribe,
|
|
146
|
+
() => window.matchMedia(DARK).matches,
|
|
147
|
+
() => false, // the server renders light
|
|
148
|
+
);
|
|
149
|
+
return dark ? 'dark' : 'light';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function Shell({ children }: { children: React.ReactNode }) {
|
|
153
|
+
const theme = useSystemTheme();
|
|
154
|
+
// The page outside the provider: its scrollbar and canvas.
|
|
155
|
+
React.useLayoutEffect(() => {
|
|
156
|
+
const classes = getThemeClassName(theme).split(' ');
|
|
157
|
+
document.documentElement.classList.add(...classes);
|
|
158
|
+
return () => document.documentElement.classList.remove(...classes);
|
|
159
|
+
}, [theme]);
|
|
160
|
+
return <WaveProvider theme={theme}>{children}</WaveProvider>;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A server cannot know the setting, so a server-rendered page paints light first and switches to dark after hydration on a dark system. `<meta name="color-scheme" content="light dark">` in the document head at least draws the scrollbar and canvas in the system's scheme from the start (the theme class on `<html>` takes over once it is set).
|
|
165
|
+
|
|
166
|
+
### Customizing tokens
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
```css
|
|
173
|
+
/* All light surfaces */
|
|
174
|
+
:root,
|
|
175
|
+
.wave-light {
|
|
176
|
+
--wave-muted-foreground: #5c5c5c;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Dark theme only */
|
|
180
|
+
.wave-dark {
|
|
181
|
+
--wave-primary: #7fb8f7;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**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).
|
|
186
|
+
|
|
187
|
+
```css
|
|
188
|
+
:root {
|
|
189
|
+
--wave-brand-40: #3b1a6b;
|
|
190
|
+
--wave-brand-60: #53258f;
|
|
191
|
+
--wave-brand-70: #5f2aa3;
|
|
192
|
+
--wave-brand-80: #6b2fb3;
|
|
193
|
+
--wave-brand-160: #f3edfb;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**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:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import '@mortenbrudvik/waveui/styles';
|
|
201
|
+
import '@mortenbrudvik/waveui/legacy-tokens.css';
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
### Color tokens
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
<summary>All 61 color tokens per theme</summary>
|
|
212
|
+
|
|
213
|
+
| Token (`--wave-…`) | Light | Dark | High contrast |
|
|
214
|
+
| ------------------------- | ------------------------------ | ------------------------------ | ------------------------------ |
|
|
215
|
+
| `background` | `#ffffff` | `#292929` | `#000000` |
|
|
216
|
+
| `foreground` | `#242424` | `#ffffff` | `#ffffff` |
|
|
217
|
+
| `card` | `#fafafa` | `#333333` | `#000000` |
|
|
218
|
+
| `secondary` | `#f5f5f5` | `#333333` | `#000000` |
|
|
219
|
+
| `muted` | `#f0f0f0` | `#383838` | `#1a1a1a` |
|
|
220
|
+
| `muted-foreground` | `#616161` | `#adadad` | `#ffffff` |
|
|
221
|
+
| `primary` | `#0f6cbd` (brand-80) | `#62abf5` (brand-110) | `#1aebff` |
|
|
222
|
+
| `primary-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
223
|
+
| `primary-hover` | `#115ea3` (brand-70) | `#77b7f7` (brand-120) | `#6ef3ff` |
|
|
224
|
+
| `primary-pressed` | `#0c3b5e` (brand-40) | `#2886de` (brand-90) | `#00c4d6` |
|
|
225
|
+
| `destructive` | `#c50f1f` | `#f48a94` | `#ff6e6e` |
|
|
226
|
+
| `destructive-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
227
|
+
| `error` | `#c50f1f` | `#f48a94` | `#ff6e6e` |
|
|
228
|
+
| `error-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
229
|
+
| `subtle` | `transparent` | `transparent` | `transparent` |
|
|
230
|
+
| `subtle-hover` | `#f5f5f5` | `#333333` | `#1f1f1f` |
|
|
231
|
+
| `subtle-pressed` | `#ebebeb` | `#2e2e2e` | `#333333` |
|
|
232
|
+
| `subtle-selected` | `#ebebeb` | `#383838` | `#333333` |
|
|
233
|
+
| `selected` | `#ebf3fc` (brand-160) | `#082338` (brand-20) | `#003a40` |
|
|
234
|
+
| `selected-foreground` | `#0f548c` (brand-60) | `#62abf5` (brand-110) | `#ffffff` |
|
|
235
|
+
| `border` | `#e0e0e0` | `#666666` | `#ffffff` |
|
|
236
|
+
| `stroke` | `#d1d1d1` | `#666666` | `#ffffff` |
|
|
237
|
+
| `stroke-hover` | `#c7c7c7` | `#757575` | `#ffffff` |
|
|
238
|
+
| `stroke-accessible` | `#616161` | `#adadad` | `#ffffff` |
|
|
239
|
+
| `input` | `#d1d1d1` | `#666666` | `#ffffff` |
|
|
240
|
+
| `ring` | `#0f6cbd` (brand-80) | `#479ef5` (brand-100) | `#ffff00` |
|
|
241
|
+
| `success` | `#107c10` | `#5db55d` | `#3ff23f` |
|
|
242
|
+
| `success-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
243
|
+
| `success-tint` | `#f1faf1` | `#052505` | `#000000` |
|
|
244
|
+
| `success-tint-foreground` | `#0e700e` | `#54b054` | `#3ff23f` |
|
|
245
|
+
| `warning` | `#fde300` | `#fde300` | `#ffff00` |
|
|
246
|
+
| `warning-foreground` | `#242424` | `#000000` | `#000000` |
|
|
247
|
+
| `warning-tint` | `#fffbe6` | `#463100` | `#000000` |
|
|
248
|
+
| `warning-tint-foreground` | `#6d5b00` | `#fde300` | `#ffff00` |
|
|
249
|
+
| `error-tint` | `#fdf3f4` | `#3b0509` | `#000000` |
|
|
250
|
+
| `error-tint-foreground` | `#b10e1c` | `#f48a94` | `#ff6060` |
|
|
251
|
+
| `severe` | `#da3b01` | `#e97548` | `#ff8c00` |
|
|
252
|
+
| `severe-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
253
|
+
| `severe-tint` | `#fdf6f3` | `#411200` | `#000000` |
|
|
254
|
+
| `severe-tint-foreground` | `#a52c00` | `#e97548` | `#ff8c00` |
|
|
255
|
+
| `info` | `#0f6cbd` | `#479ef5` | `#1aebff` |
|
|
256
|
+
| `info-foreground` | `#ffffff` | `#000000` | `#000000` |
|
|
257
|
+
| `info-tint` | `#ebf3fc` | `#082338` | `#000000` |
|
|
258
|
+
| `info-tint-foreground` | `#0f548c` | `#62abf5` | `#1aebff` |
|
|
259
|
+
| `inverted` | `#292929` | `#ffffff` | `#000000` |
|
|
260
|
+
| `inverted-foreground` | `#ffffff` | `#242424` | `#ffffff` |
|
|
261
|
+
| `inverted-border` | `transparent` | `transparent` | `#ffffff` |
|
|
262
|
+
| `track` | `#e0e0e0` | `#3d3d3d` | `#4d4d4d` |
|
|
263
|
+
| `skeleton` | `#e0e0e0` | `#3d3d3d` | `#333333` |
|
|
264
|
+
| `rating` | `#b86e00` | `#f7b538` | `#ffff00` |
|
|
265
|
+
| `presence-available` | `#107c10` | `#54b054` | `#3ff23f` |
|
|
266
|
+
| `presence-busy` | `#c50f1f` | `#f48a94` | `#ff6060` |
|
|
267
|
+
| `presence-away` | `#a67c00` | `#f7b538` | `#ffff00` |
|
|
268
|
+
| `presence-offline` | `#616161` | `#adadad` | `#ffffff` |
|
|
269
|
+
| `presence-oof` | `#b4009e` | `#d696c8` | `#ff80ff` |
|
|
270
|
+
| `presence-glyph` | `#ffffff` | `#000000` | `#000000` |
|
|
271
|
+
| `backdrop` | `rgb(0 0 0 / 0.4)` | `rgb(0 0 0 / 0.5)` | `rgb(0 0 0 / 0.8)` |
|
|
272
|
+
| `card-foreground` | `#242424` (foreground) | `#ffffff` (foreground) | `#ffffff` (foreground) |
|
|
273
|
+
| `secondary-foreground` | `#242424` (foreground) | `#ffffff` (foreground) | `#ffffff` (foreground) |
|
|
274
|
+
| `accent` | `#0f6cbd` (primary) | `#62abf5` (primary) | `#1aebff` (primary) |
|
|
275
|
+
| `accent-foreground` | `#ffffff` (primary-foreground) | `#000000` (primary-foreground) | `#000000` (primary-foreground) |
|
|
276
|
+
|
|
277
|
+
</details>
|
|
278
|
+
|
|
279
|
+
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)).
|
|
280
|
+
|
|
281
|
+
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.
|
|
282
|
+
|
|
283
|
+
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).
|
|
284
|
+
|
|
285
|
+
## Global effects
|
|
286
|
+
|
|
287
|
+
What adding Wave changes outside its own components:
|
|
288
|
+
|
|
289
|
+
- **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.
|
|
290
|
+
- **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:
|
|
291
|
+
- 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`.
|
|
292
|
+
- 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.
|
|
293
|
+
- 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.
|
|
294
|
+
- 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.
|
|
295
|
+
- On the Tailwind path all of them sit in `layer(base)`, so your utilities and unlayered CSS win over them.
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
- **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.
|
|
300
|
+
- **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.
|
|
301
|
+
- 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'`.
|
|
302
|
+
- 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.
|
|
303
|
+
|
|
304
|
+
**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-`, …):
|
|
305
|
+
|
|
306
|
+
- 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`.
|
|
307
|
+
- 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`.
|
|
308
|
+
|
|
309
|
+
Workarounds when your app has its own design system:
|
|
310
|
+
|
|
311
|
+
- 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:
|
|
312
|
+
|
|
313
|
+
```css
|
|
314
|
+
/* app.css */
|
|
315
|
+
@layer theme, base, wave, components, utilities;
|
|
316
|
+
@import 'tailwindcss' prefix(tw);
|
|
317
|
+
@import '@mortenbrudvik/waveui/styles.css' layer(wave);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
- Never import `legacy-tokens.css` in an app that defines shadcn/ui-style variables.
|
|
323
|
+
|
|
324
|
+
Namespaced Wave utilities are being considered for 1.0.
|
|
325
|
+
|
|
326
|
+
## Styling hooks
|
|
327
|
+
|
|
328
|
+
Every component root and every named part carries a stable class that your own CSS can target: `wave-<component>` on the root and `wave-<component>__<part>` on a part.
|
|
329
|
+
|
|
330
|
+
```css
|
|
331
|
+
/* app.css */
|
|
332
|
+
.wave-button__icon {
|
|
333
|
+
color: var(--wave-primary);
|
|
334
|
+
}
|
|
335
|
+
.wave-tree-item__actions {
|
|
336
|
+
gap: 2px;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The package exports the same names as typed constants, one per component (`as const`), so a selector built from them is checked by TypeScript:
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
import { buttonClassNames, treeItemClassNames } from '@mortenbrudvik/waveui';
|
|
344
|
+
|
|
345
|
+
// `.${buttonClassNames.icon}` is '.wave-button__icon'.
|
|
346
|
+
export const iconSelector = `.${buttonClassNames.icon}`;
|
|
347
|
+
|
|
348
|
+
export function checkedRows(tree: HTMLElement) {
|
|
349
|
+
return tree.querySelectorAll(`.${treeItemClassNames.root}[aria-checked='true']`);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- **Names.** The root class is `wave-` and the kebab-cased component name (`CardHeader` → `wave-card-header`, `Menu.ItemCheckbox` → `wave-menu-item-checkbox`); a part adds `__` and its name (`wave-tag__dismiss-button`). The constant is the camel-cased flat name plus `ClassNames` (`cardHeaderClassNames`). `WaveProvider` keeps `wave-root` and `Portal` `wave-portal`. Components that render no element of their own (Dialog, Popover, Presence, and the trigger and close parts, which clone your element) have none.
|
|
354
|
+
- **Where they sit.** The root class is on the element that receives your `className` (Input with `contentBefore` or `contentAfter`: the wrapper; `RadioGroup.Item`: its label). A slot's part class stays when a slot object swaps the element (`icon={{ as: 'img', … }}`).
|
|
355
|
+
- **Variants and nesting.** A component built on another carries both classes: ToggleButton is `wave-button wave-toggle-button` (its icon `wave-button__icon wave-toggle-button__icon`), so `.wave-button` styles every button; the menu item kinds carry `wave-menu-item`, and CounterBadge carries `wave-badge`. A public component rendered inside another also gets the parent's part class: SplitButton's halves are `wave-button wave-split-button__primary-action-button` and `wave-button wave-split-button__menu-button`.
|
|
356
|
+
- **No state classes.** State is in the `data-*` attributes and ARIA (`.wave-button[data-pressed]`, `.wave-tree-item[aria-checked='mixed']`); there is no `wave-button--pressed`.
|
|
357
|
+
- **Cascade.** Wave never styles through these classes and Tailwind generates nothing for them, so your rules are the only ones on them. With the precompiled `./styles` (unlayered), a one-class rule of yours ties with Wave's utility classes and wins when it comes later; on the Tailwind path Wave's utilities sit in Tailwind's `utilities` layer, and any unlayered rule of yours wins.
|
|
358
|
+
- **Stable.** The names are public API: no class is removed before 1.0. A renamed part, or a part that becomes a compound member of its own, keeps its old class next to the new one until 1.0; moving a class to another element is listed in the CHANGELOG as a DOM change. Per-part documentation comes with the part props of a later release (P13-01 in the [roadmap](docs/ROADMAP.md)).
|
|
359
|
+
- **Server Components.** The constants come from a server-safe module: a Server Component can import them from the entry.
|
|
360
|
+
|
|
361
|
+
## React Server Components
|
|
362
|
+
|
|
363
|
+
- Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster` and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle and render Wave components with serializable props.
|
|
364
|
+
- 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.
|
|
365
|
+
- 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, the parts of Toast and MessageBar, the ColorPicker parts, `Tree.ItemPersonaLayout`, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
|
|
366
|
+
- Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component.
|
|
367
|
+
- 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.
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
// app/layout.tsx (Server Component)
|
|
371
|
+
import '@mortenbrudvik/waveui/styles';
|
|
372
|
+
import { WaveProvider } from '@mortenbrudvik/waveui';
|
|
373
|
+
|
|
374
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
375
|
+
return (
|
|
376
|
+
<html lang="en">
|
|
377
|
+
<body>
|
|
378
|
+
<WaveProvider theme="light">{children}</WaveProvider>
|
|
379
|
+
</body>
|
|
380
|
+
</html>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
// app/page.tsx (Server Component): flat names, serializable props only
|
|
387
|
+
import {
|
|
388
|
+
Card,
|
|
389
|
+
CardBody,
|
|
390
|
+
CardHeader,
|
|
391
|
+
Dialog,
|
|
392
|
+
DialogContent,
|
|
393
|
+
DialogTrigger,
|
|
394
|
+
Button,
|
|
395
|
+
} from '@mortenbrudvik/waveui';
|
|
396
|
+
|
|
397
|
+
export default function Page() {
|
|
398
|
+
return (
|
|
399
|
+
<Card>
|
|
400
|
+
<CardHeader title="Usage" subtitle="This month" />
|
|
401
|
+
<CardBody>
|
|
402
|
+
<Dialog>
|
|
403
|
+
<DialogTrigger>
|
|
404
|
+
<Button>Details</Button>
|
|
405
|
+
</DialogTrigger>
|
|
406
|
+
<DialogContent title="Usage details">
|
|
407
|
+
Rendered on the server, opened on the client.
|
|
408
|
+
</DialogContent>
|
|
409
|
+
</Dialog>
|
|
410
|
+
</CardBody>
|
|
411
|
+
</Card>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
// app/save-button.tsx (client component): handlers and dotted names are fine here
|
|
418
|
+
'use client';
|
|
419
|
+
import { Button, Toaster, useToastController } from '@mortenbrudvik/waveui';
|
|
420
|
+
|
|
421
|
+
function SaveButton() {
|
|
422
|
+
const { dispatchToast } = useToastController();
|
|
423
|
+
return <Button onClick={() => dispatchToast({ status: 'success', title: 'Saved' })}>Save</Button>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function SaveArea() {
|
|
427
|
+
return (
|
|
428
|
+
<Toaster>
|
|
429
|
+
<SaveButton />
|
|
430
|
+
</Toaster>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
| Compound | Flat names |
|
|
436
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
437
|
+
| `Accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionPanel` |
|
|
438
|
+
| `Breadcrumb` | `BreadcrumbItem` |
|
|
439
|
+
| `Card` | `CardHeader`, `CardBody`, `CardFooter` |
|
|
440
|
+
| `Carousel` | `CarouselItem` |
|
|
441
|
+
| `ColorPicker` | `ColorPickerHexField`, `ColorPickerSwatches`, `ColorPickerPreview` |
|
|
442
|
+
| `Combobox` | `ComboboxOption`, `ComboboxOptionGroup` (also `Option`, `OptionGroup`) |
|
|
443
|
+
| `DataGrid` | `DataGridHeader`, `DataGridHeaderCell`, `DataGridBody`, `DataGridRow`, `DataGridCell` |
|
|
444
|
+
| `Dialog` | `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogFooter`, `DialogClose` |
|
|
445
|
+
| `Drawer` | `DrawerTrigger`, `DrawerTitle`, `DrawerClose` |
|
|
446
|
+
| `Dropdown` | `DropdownOption`, `DropdownOptionGroup` |
|
|
447
|
+
| `List` | `ListItem` |
|
|
448
|
+
| `Menu` | `MenuTrigger`, `MenuPopover`, `MenuItem`, `MenuDivider`, `MenuItemCheckbox`, `MenuItemRadio`, `MenuItemSwitch`, `MenuItemLink`, `MenuGroup`, `MenuGroupHeader`, `MenuSplitGroup` |
|
|
449
|
+
| `MessageBar` | `MessageBarTitle`, `MessageBarActions` |
|
|
450
|
+
| `Nav` | `NavCategory`, `NavItem`, `NavSubItem` |
|
|
451
|
+
| `Overflow` | `OverflowItem` |
|
|
452
|
+
| `Popover` | `PopoverTrigger`, `PopoverContent` |
|
|
453
|
+
| `RadioGroup` | `RadioGroupItem` (also `RadioItem`) |
|
|
454
|
+
| `Skeleton` | `SkeletonGroup` |
|
|
455
|
+
| `Stepper` | `StepperStep` |
|
|
456
|
+
| `TabList` | `TabListTab`, `TabListPanel`, `TabListPanels` |
|
|
457
|
+
| `Table` | `TableHeader`, `TableHeaderCell`, `TableBody`, `TableRow`, `TableCell` |
|
|
458
|
+
| `Toast` | `ToastTitle`, `ToastBody`, `ToastFooter`, `ToastClose` |
|
|
459
|
+
| `Toolbar` | `ToolbarButton`, `ToolbarToggleButton`, `ToolbarRadioGroup`, `ToolbarRadioButton`, `ToolbarGroup`, `ToolbarDivider` |
|
|
460
|
+
| `Tree` | `TreeItem`, `TreeItemPersonaLayout` |
|
|
461
|
+
|
|
462
|
+
## Components
|
|
463
|
+
|
|
464
|
+
68 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.
|
|
465
|
+
|
|
466
|
+
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).
|
|
467
|
+
|
|
468
|
+
### Buttons and actions
|
|
469
|
+
|
|
470
|
+
| Component | Description |
|
|
471
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
472
|
+
| `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. |
|
|
473
|
+
| `CompoundButton` | Button with a `secondaryText` line and an optional `icon` beside the text. |
|
|
474
|
+
| `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. |
|
|
475
|
+
| `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. |
|
|
476
|
+
| `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). |
|
|
477
|
+
| `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. |
|
|
478
|
+
| `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)). |
|
|
479
|
+
|
|
480
|
+
### Inputs and forms
|
|
481
|
+
|
|
482
|
+
| Component | Description |
|
|
483
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
484
|
+
| `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. |
|
|
485
|
+
| `Label` | Form label with a decorative required asterisk, `size` and `weight`. |
|
|
486
|
+
| `Input` | Text input with `contentBefore`/`contentAfter` slots, `onValueChange(value)` next to the native `onChange`, and an `error` message. |
|
|
487
|
+
| `Textarea` | Multi-line text input with an `error` message. |
|
|
488
|
+
| `Select` | Styled native `<select>` with an `error` message. |
|
|
489
|
+
| `Checkbox` | `checked`/`defaultChecked`/`onCheckedChange`, `indeterminate`, a rich `label` before or after the box (`labelPosition`), `disabledFocusable`, native form support. |
|
|
490
|
+
| `Switch` | On/off toggle (`role="switch"`), `onCheckedChange`, a rich `label` before, after or above it (`labelPosition`), `disabledFocusable`, native form support. |
|
|
491
|
+
| `RadioGroup` | Single choice with `RadioGroup.Item` items (a rich `label` each); `orientation`, `disabled`, `onValueChange`, native form support. |
|
|
492
|
+
| `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. |
|
|
493
|
+
| `Slider` | Styled native range input, filled up to the thumb; `onValueChange(number)`. |
|
|
494
|
+
| `SpinButton` | Numeric input with step buttons; typed text is a draft committed on blur or Enter; `min`, `max`, `step`, `largeStep`. |
|
|
495
|
+
| `Combobox` | Editable combobox: typing filters the options; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup`. |
|
|
496
|
+
| `Dropdown` | Select-only combobox (a button) with typeahead; `clearable`. `Dropdown.Option`, `Dropdown.OptionGroup`. |
|
|
497
|
+
| `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. |
|
|
498
|
+
| `DatePicker` | Date input with a calendar dialog; locale-aware format and parse, `minDate`, `maxDate`, `disabledDates`, `onInvalidInput`. |
|
|
499
|
+
| `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`). |
|
|
500
|
+
| `ColorPicker` | Hex value (lowercase `#rrggbb`, or `#rrggbbaa` when not opaque) with a color area, a hue slider, a hex field, preset swatches and an optional opacity slider; with children, your own layout of `ColorArea`, `ColorSlider` and `AlphaSlider` (bound to the picker) and `ColorPicker.HexField`, `.Swatches`, `.Preview` (see [Color](#color)); `shape`. Picking a preset keeps the current opacity (a preset's alpha digits are ignored, with a development warning). |
|
|
501
|
+
| `ColorArea` | 2-D saturation and brightness field for an `HsvColor`, with two hidden range inputs (one Tab stop), pointer and touch dragging; mirrored in RTL. |
|
|
502
|
+
| `ColorSlider` | Hue (default), saturation or brightness slider (`channel`), horizontal or vertical. |
|
|
503
|
+
| `AlphaSlider` | Opacity slider over a checkerboard; `transparency` shows transparency. |
|
|
504
|
+
| `SwatchPicker` | Radio group of color swatches (`items` with a `label` each). |
|
|
505
|
+
| `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). |
|
|
506
|
+
|
|
507
|
+
### Data display
|
|
508
|
+
|
|
509
|
+
| Component | Description |
|
|
510
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
511
|
+
| `Avatar` | Image, initials or icon, with an image-failure fallback and an optional `badge`. |
|
|
512
|
+
| `AvatarGroup` | Overlapping avatars; `max` adds an overflow button that lists the hidden members. |
|
|
513
|
+
| `Badge` | Status or category label: `appearance` `filled`, `tint`, `outline`; semantic `color` (see [Badges](#badges)). |
|
|
514
|
+
| `CounterBadge` | Count pill (`99+` above `overflowCount`) or, with `dot`, an unread dot; Badge's `color` palette; `showZero`. |
|
|
515
|
+
| `PresenceBadge` | Availability badge with a distinct shape per status. |
|
|
516
|
+
| `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)). |
|
|
517
|
+
| `InfoLabel` | Label with an info button that shows extra text (a toggletip). |
|
|
518
|
+
| `Persona` | Avatar with name, secondary text and presence. |
|
|
519
|
+
| `Divider` | Horizontal or vertical separator, optionally labelled. |
|
|
520
|
+
| `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. |
|
|
521
|
+
| `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
522
|
|
|
84
523
|
### Typography
|
|
85
524
|
|
|
86
|
-
| Component | Description
|
|
87
|
-
|
|
88
|
-
| `Text`
|
|
525
|
+
| Component | Description |
|
|
526
|
+
| --------- | --------------------------------------------------------------------------- |
|
|
527
|
+
| `Text` | Type-ramp text (`variant`, `weight`); polymorphic `as`; inherits its color. |
|
|
89
528
|
|
|
90
529
|
### Layout
|
|
91
530
|
|
|
92
|
-
| Component
|
|
93
|
-
|
|
94
|
-
| `Card`
|
|
95
|
-
| `Accordion` |
|
|
96
|
-
| `TabList`
|
|
97
|
-
| `Tree`
|
|
98
|
-
| `Carousel`
|
|
99
|
-
| `Overflow`
|
|
100
|
-
| `Grid`
|
|
101
|
-
| `Stack`
|
|
102
|
-
| `Flex`
|
|
531
|
+
| Component | Description |
|
|
532
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
533
|
+
| `Card` | Surface with `Card.Header`, `Card.Body`, `Card.Footer`; optionally selectable (`onSelect`, `selected`, `selectionControl`). |
|
|
534
|
+
| `Accordion` | Disclosure sections (`Accordion.Item`, `.Trigger`, `.Panel`); single (default) or `type="multiple"`; `headingLevel`; item values are unique (development warning). |
|
|
535
|
+
| `TabList` | Tabs (`TabList.Tab`, `.Panel`, `.Panels`) with automatic activation, or manual with `selectTabOnFocus={false}`; `orientation`; tab values are unique (development warning). |
|
|
536
|
+
| `Tree` | Hierarchical tree (`Tree.Item`) with expand/collapse and typeahead; single selection, or checkboxes with a cascading mixed state (`selectionMode="multiple"`); row `actions`, `aside` and `Tree.ItemPersonaLayout`; `navigationMode="treegrid"` (see [Trees](#trees)). Nested items are `Tree.Item` elements in their parent's children (directly, in Fragments or from a render function), not rendered by a recursive component; values are unique. |
|
|
537
|
+
| `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). |
|
|
538
|
+
| `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. |
|
|
539
|
+
| `Grid` | CSS grid with a column count and token gaps. |
|
|
540
|
+
| `Stack` | Vertical or horizontal stack (`orientation`, `gap`). |
|
|
541
|
+
| `Flex` | Flexbox container (`direction`, `wrap`, `align`, `justify`, `gap`). |
|
|
103
542
|
|
|
104
543
|
### Feedback
|
|
105
544
|
|
|
106
|
-
| Component
|
|
107
|
-
|
|
108
|
-
| `MessageBar`
|
|
109
|
-
| `ProgressBar` |
|
|
110
|
-
| `Skeleton`
|
|
111
|
-
| `Spinner`
|
|
112
|
-
| `Toast`
|
|
545
|
+
| Component | Description |
|
|
546
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
547
|
+
| `MessageBar` | Inline status message (`info`, `success`, `warning`, `error`) with hidden status text, `MessageBar.Title`, `MessageBar.Actions` that reflow with `layout`, and an optional dismiss button (see [Message bars](#message-bars)). |
|
|
548
|
+
| `ProgressBar` | Determinate or indeterminate progress; name it with `label` (inside a `Field`, the Field's label names it); `color`. |
|
|
549
|
+
| `Skeleton` | Loading placeholder (`width`, `height`, `shape`); `Skeleton.Group` marks the loading region busy. |
|
|
550
|
+
| `Spinner` | Loading indicator announced as "Loading" (localize with `label`); `appearance="inverted"` for brand surfaces; `delay`. |
|
|
551
|
+
| `Toast` | Notifications shown by `<Toaster>` (`limit` queues the rest): a `<Toast>` with `Toast.Title`, `.Body`, `.Footer` and `.Close`, or text, dispatched through `useToastController()` or a `createToaster()` handle; a keyboard shortcut, Escape and Delete (see [Toasts](#toasts)). |
|
|
113
552
|
|
|
114
553
|
### Navigation
|
|
115
554
|
|
|
116
|
-
| Component
|
|
117
|
-
|
|
118
|
-
| `Breadcrumb` |
|
|
119
|
-
| `Menu`
|
|
120
|
-
| `Nav`
|
|
121
|
-
| `Stepper`
|
|
122
|
-
| `Pagination` | Page
|
|
555
|
+
| Component | Description |
|
|
556
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
557
|
+
| `Breadcrumb` | Trail of links, buttons or text (`Breadcrumb.Item`, `current`). |
|
|
558
|
+
| `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)). |
|
|
559
|
+
| `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked. |
|
|
560
|
+
| `Stepper` | Multi-step progress (`Stepper.Step`), horizontal or vertical, optionally `linear`. |
|
|
561
|
+
| `Pagination` | Page buttons with ellipses, previous/next and optional first/last. |
|
|
123
562
|
|
|
124
563
|
### Overlays
|
|
125
564
|
|
|
126
|
-
| Component
|
|
127
|
-
|
|
128
|
-
| `Dialog`
|
|
129
|
-
| `Drawer`
|
|
130
|
-
| `Popover`
|
|
131
|
-
| `Tooltip`
|
|
132
|
-
| `TeachingPopover` | Step-
|
|
565
|
+
| Component | Description |
|
|
566
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
567
|
+
| `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`. |
|
|
568
|
+
| `Drawer` | Modal side panel (`position` `start`, `end`, `left`, `right`; `closeLabel`); `Drawer.Trigger`, `Drawer.Title`, `Drawer.Close`. |
|
|
569
|
+
| `Popover` | Non-modal popup anchored to `Popover.Trigger` or a `target`; `side`, `align`; hover cards (`openOnHover`) and context popovers (`openOnContext`). |
|
|
570
|
+
| `Tooltip` | Hover and focus text for its child, as a description or (`relationship="label"`) a name; `open`/`defaultOpen`/`onOpenChange`; `openDelay`, `closeDelay`. |
|
|
571
|
+
| `TeachingPopover` | Step-by-step onboarding popover, optionally pointing at a `target`. |
|
|
133
572
|
|
|
134
|
-
###
|
|
573
|
+
### Tables
|
|
135
574
|
|
|
136
|
-
| Component
|
|
137
|
-
|
|
138
|
-
| `Table`
|
|
139
|
-
| `DataGrid` | Interactive
|
|
575
|
+
| Component | Description |
|
|
576
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
577
|
+
| `Table` | Static table: `Table.Header`, `Table.HeaderCell`, `Table.Body`, `Table.Row`, `Table.Cell`; `striped`; scrolls horizontally. |
|
|
578
|
+
| `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
579
|
|
|
141
580
|
### Provider
|
|
142
581
|
|
|
143
|
-
| Component
|
|
144
|
-
|
|
145
|
-
| `WaveProvider` | Theme
|
|
582
|
+
| Component | Description |
|
|
583
|
+
| -------------- | ------------------------------------------------------------------------- |
|
|
584
|
+
| `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction and portal container. |
|
|
146
585
|
|
|
147
|
-
|
|
586
|
+
`Portal` renders its children into `document.body` (or the provider's `portalContainer`) inside a themed `wave-portal` wrapper; use it for your own overlays.
|
|
587
|
+
|
|
588
|
+
### Motion
|
|
589
|
+
|
|
590
|
+
| Component | Description |
|
|
591
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
592
|
+
| `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)). |
|
|
593
|
+
|
|
594
|
+
## Usage notes
|
|
595
|
+
|
|
596
|
+
### Buttons
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { Button } from '@mortenbrudvik/waveui';
|
|
600
|
+
|
|
601
|
+
// Tailwind path. The resting colors are plain classes; the hover and pressed colors use the gate
|
|
602
|
+
// prefix, because a bare `hover:` or `active:` class loses to the built-in one.
|
|
603
|
+
export const DeleteButton = () => (
|
|
604
|
+
<Button
|
|
605
|
+
appearance="primary"
|
|
606
|
+
className="bg-destructive text-destructive-foreground not-disabled:not-aria-disabled:hover:bg-destructive/90 not-disabled:not-aria-disabled:active:bg-destructive/80"
|
|
607
|
+
>
|
|
608
|
+
Delete
|
|
609
|
+
</Button>
|
|
610
|
+
);
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
import { Button, Tooltip } from '@mortenbrudvik/waveui';
|
|
615
|
+
|
|
616
|
+
// Unavailable but focusable: keyboard users reach it and the Tooltip explains why.
|
|
617
|
+
export const PublishButton = ({ ready }: { ready: boolean }) => (
|
|
618
|
+
<Tooltip content={ready ? 'Publish the draft' : 'Fill in the required fields first'}>
|
|
619
|
+
<Button appearance="primary" disabledFocusable={!ready}>
|
|
620
|
+
Publish
|
|
621
|
+
</Button>
|
|
622
|
+
</Tooltip>
|
|
623
|
+
);
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
- **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.
|
|
627
|
+
- **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.
|
|
628
|
+
- **`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`.
|
|
629
|
+
- **`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.
|
|
630
|
+
- 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.
|
|
631
|
+
- 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).
|
|
632
|
+
- **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.
|
|
633
|
+
- **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.
|
|
634
|
+
- **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.
|
|
635
|
+
- **`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.
|
|
636
|
+
- **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.
|
|
637
|
+
- **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.
|
|
638
|
+
|
|
639
|
+
### Forms and `Field`
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
import * as React from 'react';
|
|
643
|
+
import { Button, Checkbox, Dropdown, Field, Input } from '@mortenbrudvik/waveui';
|
|
644
|
+
|
|
645
|
+
export function SignupForm() {
|
|
646
|
+
const [email, setEmail] = React.useState('');
|
|
647
|
+
const error = email.includes('@') ? undefined : 'Enter an email address';
|
|
648
|
+
return (
|
|
649
|
+
<form onSubmit={(event) => event.preventDefault()}>
|
|
650
|
+
<Field label="Email" hint="We never share it" error={error} required>
|
|
651
|
+
<Input type="email" name="email" value={email} onValueChange={setEmail} />
|
|
652
|
+
</Field>
|
|
653
|
+
<Field label="Role">
|
|
654
|
+
<Dropdown name="role" defaultValue="user">
|
|
655
|
+
<Dropdown.Option value="admin">Admin</Dropdown.Option>
|
|
656
|
+
<Dropdown.Option value="user">User</Dropdown.Option>
|
|
657
|
+
</Dropdown>
|
|
658
|
+
</Field>
|
|
659
|
+
<Checkbox name="terms" label="I accept the terms" required />
|
|
660
|
+
<Button appearance="primary" type="submit">
|
|
661
|
+
Sign up
|
|
662
|
+
</Button>
|
|
663
|
+
</form>
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
```tsx
|
|
669
|
+
import { Field, Input, ProgressBar, Switch } from '@mortenbrudvik/waveui';
|
|
670
|
+
|
|
671
|
+
export function AccountSettings({ backedUp }: { backedUp: number }) {
|
|
672
|
+
return (
|
|
673
|
+
<>
|
|
674
|
+
<Field
|
|
675
|
+
label="Password"
|
|
676
|
+
hint="At least 12 characters"
|
|
677
|
+
validationState="warning"
|
|
678
|
+
validationMessage="This password is common"
|
|
679
|
+
orientation="horizontal"
|
|
680
|
+
>
|
|
681
|
+
<Input type="password" />
|
|
682
|
+
</Field>
|
|
683
|
+
<Field label="Backup" validationState="success" validationMessage="Up to date">
|
|
684
|
+
<ProgressBar value={backedUp} />
|
|
685
|
+
</Field>
|
|
686
|
+
<Switch label="Email me about new sign-ins" labelPosition="before" />
|
|
687
|
+
</>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
- 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`, `ColorArea`, `ColorSlider`, `AlphaSlider`, `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.
|
|
693
|
+
- **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`.
|
|
694
|
+
- **`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).
|
|
695
|
+
- **The hint** stays visible below the message, after it; the control is described by the message, then the hint.
|
|
696
|
+
- **`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).
|
|
697
|
+
- **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).
|
|
698
|
+
- **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.
|
|
699
|
+
- 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.
|
|
700
|
+
- 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.
|
|
701
|
+
- `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.
|
|
702
|
+
- 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`, `ColorArea`, `ColorSlider` and `AlphaSlider` (the hex of the color; inside a ColorPicker they are bound and the picker submits), `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.
|
|
703
|
+
|
|
704
|
+
### Value callbacks
|
|
705
|
+
|
|
706
|
+
`onChange` is the native DOM change event (`Input`, `Textarea`, `Select`, `Slider`). State callbacks are named after the state and receive the value:
|
|
707
|
+
|
|
708
|
+
| State | Props |
|
|
709
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
710
|
+
| single value | `value` / `defaultValue` / `onValueChange(value)` |
|
|
711
|
+
| checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` |
|
|
712
|
+
| open | `open` / `defaultOpen` / `onOpenChange(open)` |
|
|
713
|
+
| checked values (Menu, Toolbar) | `checkedValues` / `defaultCheckedValues` / `onCheckedValuesChange(checkedValues, details)` |
|
|
714
|
+
| checked items (Tree) | `checkedItems` / `defaultCheckedItems` / `onCheckedItemsChange(checkedItems, details)` |
|
|
715
|
+
|
|
716
|
+
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)), Menu and Toolbar call `onCheckedValuesChange(checkedValues, details)` with `details.name`, `details.checkedItems` (the group's new values) and `details.event` (see [Menus](#menus)), and Tree calls `onCheckedItemsChange(checkedItems, details)` with `details.value`, `details.checked` and `details.event` (see [Trees](#trees)). 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`, `checkedItems`, `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.
|
|
148
717
|
|
|
149
|
-
###
|
|
718
|
+
### Composite controls
|
|
719
|
+
|
|
720
|
+
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.
|
|
721
|
+
|
|
722
|
+
`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>`.
|
|
723
|
+
|
|
724
|
+
`ColorSlider`, `AlphaSlider` and `ColorPicker.HexField` route the same props to their `<input>` and take `controlRef`. `ColorArea` is a group around two inputs: the group keeps `aria-label`, `aria-labelledby` and `aria-describedby` (a Field's label names it), with `className`, `style`, `data-*` and `ref`; both inputs get `aria-invalid` and `aria-errormessage`; `autoFocus` and `controlRef` reach the saturation input; `tabIndex` reaches the input that holds the Tab stop.
|
|
725
|
+
|
|
726
|
+
### Color
|
|
150
727
|
|
|
151
728
|
```tsx
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
729
|
+
import * as React from 'react';
|
|
730
|
+
import { AlphaSlider, ColorArea, ColorPicker, ColorSlider, Field } from '@mortenbrudvik/waveui';
|
|
731
|
+
|
|
732
|
+
export function AccentPicker() {
|
|
733
|
+
const [accent, setAccent] = React.useState('#0f6cbd');
|
|
734
|
+
return (
|
|
735
|
+
<Field label="Accent color">
|
|
736
|
+
<ColorPicker value={accent} onValueChange={setAccent} name="accent">
|
|
737
|
+
<ColorArea />
|
|
738
|
+
<ColorSlider />
|
|
739
|
+
<AlphaSlider />
|
|
740
|
+
<ColorPicker.HexField />
|
|
741
|
+
<ColorPicker.Swatches />
|
|
742
|
+
</ColorPicker>
|
|
743
|
+
</Field>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
155
746
|
```
|
|
156
747
|
|
|
157
|
-
|
|
748
|
+
- **The picker and its parts.** Without children, a ColorPicker renders a color area and a hue slider, then its 0.7 layout: the preview and hex field, the presets and, with `showOpacity`, the opacity slider. With children it renders your layout: `ColorArea`, `ColorSlider` and `AlphaSlider` bind to the picker when you pass them neither `value` nor `defaultValue` (Fluent's composition), and `ColorPicker.HexField`, `ColorPicker.Swatches` (the picker's `presets`) and `ColorPicker.Preview` are the picker's own parts. Bound parts read and write the picker's color, take its `shape`, `labels` and disabled state, and submit nothing themselves: the picker's hidden input submits the hex value (`name`, `form`, `required`), and a form reset restores its `defaultValue`. A part with its own `value` or `defaultValue` stays independent.
|
|
749
|
+
- **Hex and HSV.** The picker's value is a hex string, as in 0.7 (`#rrggbb`, or `#rrggbbaa` below full opacity). The standalone parts take an `HsvColor` (`{ h, s, v, a? }`: `h` from 0 to 360, `s`, `v` and `a` from 0 to 1, no `a` meaning opaque; Fluent's type), and `hsvToHex(color)` and `hexToHsv(hex)` convert (`hexToHsv` returns `null` for text that is not a hex color). Inside the picker, the hue and saturation survive grays, white and black, which a hex value cannot carry. A value that is not a hex color disables the bound parts until a hex value arrives.
|
|
750
|
+
- **Keys and pointer.** `ColorArea` is one Tab stop: Left and Right change saturation (mirrored in RTL), Up and Down brightness, Shift with an arrow by 10, Page Up and Page Down brightness by 10, Home and End saturation to 0 and 100. `ColorSlider` and `AlphaSlider` take the arrows of their axis (by 1, Shift by 10), Page Up and Page Down (by 10), Home and End. A press on the area jumps there and drags, with a mouse, touch or a pen.
|
|
751
|
+
- **Assistive technology.** The area is a group of two sliders, "Saturation" and "Brightness": desktop screen readers list one 2-D slider, and touch screen readers can swipe each. Each value text starts with its own channel and names the color ("Saturation 72%, Brightness 74%, dark blue"); a hue slider says "210°, blue". `labels.colorName(color)` replaces the English color names (return `''` to leave them out), and `labels.valueText(color, channel)` a whole value text, for languages whose word order differs.
|
|
752
|
+
- `ColorSlider` takes `channel` (`'hue'`, `'saturation'` or `'value'`) and `orientation` (`'vertical'` draws the rail bottom to top); `AlphaSlider` shows opacity, or with `transparency` transparency (the value's `a` is still the opacity). The roots carry `data-orientation`, `data-channel` (ColorSlider) and, while disabled, `data-disabled`. `shape="square"` removes the corner radius. Inside a `Field`, a standalone slider is named by the Field's label, else by its `labels` ("Hue", "Saturation", "Brightness").
|
|
753
|
+
|
|
754
|
+
### Menus
|
|
158
755
|
|
|
159
756
|
```tsx
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
757
|
+
import { Menu, MenuButton, SplitButton } from '@mortenbrudvik/waveui';
|
|
758
|
+
|
|
759
|
+
export function Actions({ onEdit, onSave }: { onEdit: () => void; onSave: () => void }) {
|
|
760
|
+
return (
|
|
761
|
+
<>
|
|
762
|
+
<Menu>
|
|
763
|
+
<Menu.Trigger>
|
|
764
|
+
<MenuButton>Actions</MenuButton>
|
|
765
|
+
</Menu.Trigger>
|
|
766
|
+
<Menu.Popover>
|
|
767
|
+
<Menu.Item onClick={onEdit}>Edit</Menu.Item>
|
|
768
|
+
<Menu.Divider />
|
|
769
|
+
<Menu.Item disabled>Archive</Menu.Item>
|
|
770
|
+
</Menu.Popover>
|
|
771
|
+
</Menu>
|
|
772
|
+
|
|
773
|
+
<Menu>
|
|
774
|
+
<Menu.Trigger>
|
|
775
|
+
{(triggerProps) => (
|
|
776
|
+
<SplitButton menuButtonProps={triggerProps} onClick={onSave}>
|
|
777
|
+
Save
|
|
778
|
+
</SplitButton>
|
|
779
|
+
)}
|
|
780
|
+
</Menu.Trigger>
|
|
781
|
+
<Menu.Popover>
|
|
782
|
+
<Menu.Item>Save as…</Menu.Item>
|
|
783
|
+
</Menu.Popover>
|
|
784
|
+
</Menu>
|
|
785
|
+
</>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
163
788
|
```
|
|
164
789
|
|
|
165
|
-
|
|
790
|
+
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`.
|
|
166
791
|
|
|
167
|
-
|
|
792
|
+
- **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`.
|
|
793
|
+
- **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.
|
|
794
|
+
- 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.
|
|
168
795
|
|
|
169
796
|
```tsx
|
|
170
|
-
import
|
|
797
|
+
import * as React from 'react';
|
|
798
|
+
import { Menu, MenuButton } from '@mortenbrudvik/waveui';
|
|
799
|
+
import type { CheckedValues } from '@mortenbrudvik/waveui';
|
|
171
800
|
|
|
172
|
-
function
|
|
173
|
-
const {
|
|
174
|
-
|
|
175
|
-
|
|
801
|
+
export function ViewMenu() {
|
|
802
|
+
const [checked, setChecked] = React.useState<CheckedValues>({ show: ['ruler'], sort: ['name'] });
|
|
803
|
+
return (
|
|
804
|
+
<Menu checkedValues={checked} onCheckedValuesChange={setChecked}>
|
|
805
|
+
<Menu.Trigger>
|
|
806
|
+
<MenuButton>View</MenuButton>
|
|
807
|
+
</Menu.Trigger>
|
|
808
|
+
<Menu.Popover>
|
|
809
|
+
<Menu.Group>
|
|
810
|
+
<Menu.GroupHeader>Show</Menu.GroupHeader>
|
|
811
|
+
<Menu.ItemCheckbox name="show" value="ruler">
|
|
812
|
+
Ruler
|
|
813
|
+
</Menu.ItemCheckbox>
|
|
814
|
+
<Menu.ItemSwitch name="show" value="grid">
|
|
815
|
+
Grid
|
|
816
|
+
</Menu.ItemSwitch>
|
|
817
|
+
</Menu.Group>
|
|
818
|
+
<Menu.Divider />
|
|
819
|
+
<Menu.Group>
|
|
820
|
+
<Menu.GroupHeader>Sort by</Menu.GroupHeader>
|
|
821
|
+
<Menu.ItemRadio name="sort" value="name">
|
|
822
|
+
Name
|
|
823
|
+
</Menu.ItemRadio>
|
|
824
|
+
<Menu.ItemRadio name="sort" value="date">
|
|
825
|
+
Date
|
|
826
|
+
</Menu.ItemRadio>
|
|
827
|
+
</Menu.Group>
|
|
828
|
+
<Menu.Divider />
|
|
829
|
+
<Menu>
|
|
830
|
+
<Menu.Trigger>
|
|
831
|
+
<Menu.Item>Zoom</Menu.Item>
|
|
832
|
+
</Menu.Trigger>
|
|
833
|
+
<Menu.Popover>
|
|
834
|
+
<Menu.Item>Zoom in</Menu.Item>
|
|
835
|
+
<Menu.Item>Zoom out</Menu.Item>
|
|
836
|
+
</Menu.Popover>
|
|
837
|
+
</Menu>
|
|
838
|
+
<Menu.ItemLink href="/help/view">View help</Menu.ItemLink>
|
|
839
|
+
</Menu.Popover>
|
|
840
|
+
</Menu>
|
|
841
|
+
);
|
|
176
842
|
}
|
|
177
843
|
```
|
|
178
844
|
|
|
179
|
-
|
|
845
|
+
- **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.
|
|
846
|
+
- **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.
|
|
847
|
+
- **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.
|
|
848
|
+
- **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.
|
|
849
|
+
- **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`.
|
|
850
|
+
- **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.
|
|
851
|
+
- **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.
|
|
852
|
+
- **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):
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
<Menu.SplitGroup>
|
|
856
|
+
<Menu.Item onClick={save}>Save</Menu.Item>
|
|
857
|
+
<Menu>
|
|
858
|
+
<Menu.Trigger>
|
|
859
|
+
<Menu.Item aria-label="More save options" />
|
|
860
|
+
</Menu.Trigger>
|
|
861
|
+
<Menu.Popover>
|
|
862
|
+
<Menu.Item onClick={saveAs}>Save as…</Menu.Item>
|
|
863
|
+
</Menu.Popover>
|
|
864
|
+
</Menu>
|
|
865
|
+
</Menu.SplitGroup>
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
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.
|
|
869
|
+
|
|
870
|
+
- **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.
|
|
871
|
+
- **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.
|
|
872
|
+
- **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.
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
import { Button, Menu } from '@mortenbrudvik/waveui';
|
|
876
|
+
|
|
877
|
+
export function FileList({ files }: { files: string[] }) {
|
|
878
|
+
return (
|
|
879
|
+
<Menu openOnContext>
|
|
880
|
+
<Menu.Trigger>
|
|
881
|
+
<div aria-keyshortcuts="Shift+F10">
|
|
882
|
+
{files.map((file) => (
|
|
883
|
+
<Button key={file} appearance="subtle">
|
|
884
|
+
{file}
|
|
885
|
+
</Button>
|
|
886
|
+
))}
|
|
887
|
+
</div>
|
|
888
|
+
</Menu.Trigger>
|
|
889
|
+
<Menu.Popover aria-label="File actions">
|
|
890
|
+
<Menu.Item>Open</Menu.Item>
|
|
891
|
+
<Menu.Item>Rename</Menu.Item>
|
|
892
|
+
<Menu.Item>Delete</Menu.Item>
|
|
893
|
+
</Menu.Popover>
|
|
894
|
+
</Menu>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
- **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.
|
|
900
|
+
- **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.
|
|
901
|
+
- **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).
|
|
902
|
+
|
|
903
|
+
### Toolbars
|
|
180
904
|
|
|
181
|
-
|
|
905
|
+
```tsx
|
|
906
|
+
import { Toolbar } from '@mortenbrudvik/waveui';
|
|
182
907
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
908
|
+
export function FormatBar() {
|
|
909
|
+
return (
|
|
910
|
+
<Toolbar aria-label="Formatting" defaultCheckedValues={{ format: ['bold'], align: ['left'] }}>
|
|
911
|
+
<Toolbar.ToggleButton name="format" value="bold">
|
|
912
|
+
Bold
|
|
913
|
+
</Toolbar.ToggleButton>
|
|
914
|
+
<Toolbar.ToggleButton name="format" value="italic">
|
|
915
|
+
Italic
|
|
916
|
+
</Toolbar.ToggleButton>
|
|
917
|
+
<Toolbar.Divider />
|
|
918
|
+
<Toolbar.RadioGroup aria-label="Text alignment">
|
|
919
|
+
<Toolbar.RadioButton name="align" value="left">
|
|
920
|
+
Left
|
|
921
|
+
</Toolbar.RadioButton>
|
|
922
|
+
<Toolbar.RadioButton name="align" value="center">
|
|
923
|
+
Center
|
|
924
|
+
</Toolbar.RadioButton>
|
|
925
|
+
</Toolbar.RadioGroup>
|
|
926
|
+
<Toolbar.Divider />
|
|
927
|
+
<Toolbar.Group aria-label="Insert">
|
|
928
|
+
<Toolbar.Button>Link</Toolbar.Button>
|
|
929
|
+
<Toolbar.Button>Image</Toolbar.Button>
|
|
930
|
+
</Toolbar.Group>
|
|
931
|
+
</Toolbar>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
```
|
|
193
935
|
|
|
194
|
-
|
|
936
|
+
- **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.
|
|
937
|
+
- **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.
|
|
938
|
+
- **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.
|
|
939
|
+
- **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).
|
|
940
|
+
- 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.
|
|
195
941
|
|
|
196
|
-
|
|
942
|
+
### Dialogs and triggers
|
|
197
943
|
|
|
198
|
-
|
|
944
|
+
```tsx
|
|
945
|
+
import { Button, Dialog } from '@mortenbrudvik/waveui';
|
|
199
946
|
|
|
200
|
-
|
|
947
|
+
export function DeleteDialog({ onDelete }: { onDelete: () => void }) {
|
|
948
|
+
return (
|
|
949
|
+
<Dialog modalType="alert">
|
|
950
|
+
<Dialog.Trigger>
|
|
951
|
+
<Button>Delete</Button>
|
|
952
|
+
</Dialog.Trigger>
|
|
953
|
+
<Dialog.Content title="Delete item?" size="small">
|
|
954
|
+
This action cannot be undone.
|
|
955
|
+
<Dialog.Footer>
|
|
956
|
+
<Dialog.Close>
|
|
957
|
+
<Button autoFocus>Cancel</Button>
|
|
958
|
+
</Dialog.Close>
|
|
959
|
+
<Dialog.Close>
|
|
960
|
+
<Button appearance="primary" onClick={onDelete}>
|
|
961
|
+
Delete
|
|
962
|
+
</Button>
|
|
963
|
+
</Dialog.Close>
|
|
964
|
+
</Dialog.Footer>
|
|
965
|
+
</Dialog.Content>
|
|
966
|
+
</Dialog>
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
- `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).
|
|
972
|
+
- 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.
|
|
973
|
+
- 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.
|
|
974
|
+
- **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.
|
|
975
|
+
- **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.
|
|
976
|
+
- 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.
|
|
977
|
+
- 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.
|
|
978
|
+
- **Size.** `size` caps the dialog's width at 400px (`'small'`) or 600px (`'medium'`, the default). Below the cap it takes the full width, and it never exceeds the viewport: a 1rem margin keeps it off the viewport edges, its height is capped, and the body scrolls (sideways too, for content wider than the dialog). Because the margin and the caps are on the surface, a `className` on `Dialog.Content` replaces them: `max-w-[72rem]` for a wider dialog, `m-0 h-dvh max-h-none max-w-none rounded-none border-0` for a full-screen one, and the same classes behind a breakpoint (`max-[48rem]:m-0 max-[48rem]:h-dvh …`) for a dialog that goes full screen only on narrow screens. These are Tailwind classes for your own Tailwind build. The precompiled `./styles` contains only the utilities Wave uses: there, give `Dialog.Content` a class of your own that sets the same properties, in unlayered CSS loaded after Wave's, because Wave's classes are unlayered and as specific as yours ([Cascade](#styles)).
|
|
979
|
+
|
|
980
|
+
```tsx
|
|
981
|
+
import * as React from 'react';
|
|
982
|
+
import { Button, Dialog, Field, Input } from '@mortenbrudvik/waveui';
|
|
983
|
+
import type { DialogOpenChangeDetails } from '@mortenbrudvik/waveui';
|
|
984
|
+
|
|
985
|
+
export function RenameDialog() {
|
|
986
|
+
const [open, setOpen] = React.useState(false);
|
|
987
|
+
const [name, setName] = React.useState('');
|
|
988
|
+
const handleOpenChange = (next: boolean, details?: DialogOpenChangeDetails) => {
|
|
989
|
+
// Unsaved changes: a backdrop press keeps the dialog open; Escape and the buttons close it.
|
|
990
|
+
if (!next && name !== '' && details?.reason === 'outside-press') return;
|
|
991
|
+
setOpen(next);
|
|
992
|
+
};
|
|
993
|
+
return (
|
|
994
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
995
|
+
<Dialog.Trigger>
|
|
996
|
+
<Button>Rename</Button>
|
|
997
|
+
</Dialog.Trigger>
|
|
998
|
+
<Dialog.Content title="Rename project">
|
|
999
|
+
<Field label="New name">
|
|
1000
|
+
<Input value={name} onValueChange={setName} />
|
|
1001
|
+
</Field>
|
|
1002
|
+
<Dialog.Footer>
|
|
1003
|
+
<Dialog.Close>
|
|
1004
|
+
<Button>Cancel</Button>
|
|
1005
|
+
</Dialog.Close>
|
|
1006
|
+
</Dialog.Footer>
|
|
1007
|
+
</Dialog.Content>
|
|
1008
|
+
</Dialog>
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### Popovers and tooltips
|
|
201
1014
|
|
|
202
|
-
|
|
1015
|
+
```tsx
|
|
1016
|
+
import { Button, Popover, Tooltip } from '@mortenbrudvik/waveui';
|
|
203
1017
|
|
|
204
|
-
|
|
1018
|
+
export function Author() {
|
|
1019
|
+
return (
|
|
1020
|
+
<Popover openOnHover>
|
|
1021
|
+
<Popover.Trigger>
|
|
1022
|
+
<Button appearance="transparent">Maria Lopez</Button>
|
|
1023
|
+
</Popover.Trigger>
|
|
1024
|
+
<Popover.Content title="Maria Lopez">
|
|
1025
|
+
Product designer, Oslo.
|
|
1026
|
+
<Tooltip content="Get her updates in your feed" openDelay={500}>
|
|
1027
|
+
<Button size="small">Follow</Button>
|
|
1028
|
+
</Tooltip>
|
|
1029
|
+
</Popover.Content>
|
|
1030
|
+
</Popover>
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
205
1034
|
|
|
206
|
-
|
|
1035
|
+
- **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.
|
|
1036
|
+
- **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`.
|
|
1037
|
+
- **`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.
|
|
1038
|
+
- **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).
|
|
207
1039
|
|
|
208
|
-
|
|
209
|
-
|---|---|
|
|
210
|
-
| `useControllable` | Manage controlled/uncontrolled component state |
|
|
211
|
-
| `useId` | Generate stable unique IDs for accessibility |
|
|
212
|
-
| `useEventCallback` | Stable callback reference without stale closures |
|
|
213
|
-
| `useRovingTabIndex` | Roving tab index for keyboard navigation in composite widgets |
|
|
1040
|
+
### Enter and exit motion
|
|
214
1041
|
|
|
215
|
-
|
|
1042
|
+
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.
|
|
216
1043
|
|
|
217
|
-
|
|
1044
|
+
`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.
|
|
218
1045
|
|
|
219
1046
|
```tsx
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
1047
|
+
import * as React from 'react';
|
|
1048
|
+
import { Button, Presence } from '@mortenbrudvik/waveui';
|
|
1049
|
+
|
|
1050
|
+
export function SavedNotice() {
|
|
1051
|
+
const [shown, setShown] = React.useState(false);
|
|
1052
|
+
return (
|
|
1053
|
+
<>
|
|
1054
|
+
<Button onClick={() => setShown((value) => !value)}>Toggle notice</Button>
|
|
1055
|
+
<Presence visible={shown}>
|
|
1056
|
+
<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">
|
|
1057
|
+
Saved
|
|
1058
|
+
</p>
|
|
1059
|
+
</Presence>
|
|
1060
|
+
</>
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
```css
|
|
1066
|
+
/* The fade without Tailwind: class="notice" on the child */
|
|
1067
|
+
.notice {
|
|
1068
|
+
transition: opacity var(--wave-duration-normal) var(--wave-curve-decelerate-mid);
|
|
1069
|
+
}
|
|
1070
|
+
@starting-style {
|
|
1071
|
+
.notice[data-presence='entering'] {
|
|
1072
|
+
opacity: 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
.notice[data-presence='exiting'] {
|
|
1076
|
+
opacity: 0;
|
|
1077
|
+
transition-duration: var(--wave-duration-fast);
|
|
1078
|
+
transition-timing-function: var(--wave-curve-accelerate-mid);
|
|
1079
|
+
}
|
|
1080
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1081
|
+
.notice {
|
|
1082
|
+
transition: none;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
224
1085
|
```
|
|
225
1086
|
|
|
226
|
-
|
|
1087
|
+
- **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.
|
|
1088
|
+
- **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.
|
|
1089
|
+
- **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.
|
|
1090
|
+
- **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.
|
|
1091
|
+
- **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.
|
|
1092
|
+
- `Menu.Popover` and the toasts in a `Toaster` mount through the presence core, so menus and toasts take exit classes too (see [Menus](#menus) and [Toasts](#toasts)). An enter class for them 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.
|
|
227
1093
|
|
|
228
|
-
|
|
1094
|
+
### Toasts
|
|
229
1095
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
1096
|
+
Wrap the app (or the part that shows toasts) in `<Toaster>`; `useToastController()` throws in development outside it. Dispatch a `<Toast>` with its parts:
|
|
1097
|
+
|
|
1098
|
+
```tsx
|
|
1099
|
+
import { Button, Toast, Toaster, useToastController } from '@mortenbrudvik/waveui';
|
|
1100
|
+
|
|
1101
|
+
function SendButton({ onUndo }: { onUndo: () => void }) {
|
|
1102
|
+
const { dispatchToast } = useToastController();
|
|
1103
|
+
return (
|
|
1104
|
+
<Button
|
|
1105
|
+
onClick={() =>
|
|
1106
|
+
dispatchToast(
|
|
1107
|
+
<Toast status="success">
|
|
1108
|
+
<Toast.Title
|
|
1109
|
+
action={
|
|
1110
|
+
<Toast.Close>
|
|
1111
|
+
<Button size="small" appearance="transparent" onClick={onUndo}>
|
|
1112
|
+
Undo
|
|
1113
|
+
</Button>
|
|
1114
|
+
</Toast.Close>
|
|
1115
|
+
}
|
|
1116
|
+
>
|
|
1117
|
+
Email sent
|
|
1118
|
+
</Toast.Title>
|
|
1119
|
+
<Toast.Body subtitle="To 3 people">Your message was delivered.</Toast.Body>
|
|
1120
|
+
</Toast>,
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
>
|
|
1124
|
+
Send
|
|
1125
|
+
</Button>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
233
1128
|
|
|
234
|
-
|
|
235
|
-
|
|
1129
|
+
export function App({ undoSend }: { undoSend: () => void }) {
|
|
1130
|
+
return (
|
|
1131
|
+
<Toaster
|
|
1132
|
+
position="bottom-end"
|
|
1133
|
+
shortcuts={{ focus: (event) => event.altKey && event.shiftKey && event.code === 'KeyN' }}
|
|
1134
|
+
>
|
|
1135
|
+
<SendButton onUndo={undoSend} />
|
|
1136
|
+
</Toaster>
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
`dispatchToast` returns the toast's id; `dismissToast(id)` removes the toast and `dismissAllToasts()` every toast. The 0.5 form builds a toast from text: `dispatchToast({ status: 'success', title: 'Saved', body: 'Your changes are saved.' })`. A plain object is read as options; anything else (an element, a string, an array) is content.
|
|
236
1142
|
|
|
237
|
-
|
|
238
|
-
|
|
1143
|
+
- **Parts.** `Toast.Title` holds the title text, which names the toast, and an `action` at the end of its row; `Toast.Body` holds the body text and a `subtitle`, which describe it; `Toast.Footer` is a row of actions below. Make them direct children of `Toast` (plain children are the body, and the `title` prop renders a title, as in 0.5). `icon` replaces the status icon: only `undefined` keeps it, and `null` shows none. Flat names for Server Components: `ToastTitle`, `ToastBody`, `ToastFooter`, `ToastClose`.
|
|
1144
|
+
- **`Toast.Close`** makes its single child dismiss the toast: it calls the Toast's `onDismiss` (when there is one), then dismisses the toast in the Toaster. Your `onClick` runs first, and `preventDefault()` keeps the toast. It also works in content without a `Toast`. Every Toast in a Toaster shows the Dismiss button as well.
|
|
1145
|
+
- **Severity.** The rendered Toast's `status` sets the announcement's politeness (`error` is assertive, the others polite) and its spoken prefix ("Error:", translated with `statusLabel`). For content without a `Toast`, pass `status` (and `statusLabel`) in the options: `dispatchToast(<UploadProgress />, { status: 'info' })`. A `status` in the options that differs from the Toast's own warns in development, and the Toast's wins.
|
|
1146
|
+
- **What is announced.** The Toaster reads each toast once, through permanent live regions that need no provider: the severity, the title, the body with its subtitle, then the labels of its actions ("Success: Email sent Your message was delivered. To 3 people Undo"), never the Dismiss button. Hidden text is skipped, and an image's `alt` is read. `announcement` in the options replaces everything but the severity, for a toast whose visible text is no good announcement (a progress toast). Dispatching the same `toastId` again replaces the toast and announces it again.
|
|
1147
|
+
- **Timing.** A toast times out after 5 seconds (`timeout` in the options). Every toast pauses while the pointer or focus is anywhere in the toast region, and while the page is in the background (the window has lost focus, or the tab is hidden, also when the Toaster mounted there). A toast with an action (a button, a link or a field besides its Dismiss button) stays until it is dismissed: people who use a screen reader, a switch or voice control need time to reach it. Pass `timeout` to change that deliberately.
|
|
1148
|
+
- **Keyboard.** `shortcuts.focus` returns `true` for the key press that moves focus to the newest toast; use a modifier (or let people change the shortcut), since a single-character shortcut fails WCAG 2.1.4. A focused toast is announced by its severity and title. Tab and Shift+Tab move through the toasts' controls; Escape dismisses every toast and returns focus to where it was (a tooltip or menu opened from a toast closes first); Delete or Backspace dismisses the focused toast (the keys are ignored in editable fields), and focus moves to the next toast, or the previous one, else back to where it was before the toasts. An Escape in the toasts never closes a Dialog below them: the next Escape does, once focus is back in the dialog.
|
|
1149
|
+
- Toasts stay reachable by Tab over an open Dialog and move out of an open Drawer's way. `statusLabel` and `dismissLabel` (props of `<Toast>`; `statusLabel` is also an option of both `dispatchToast` forms, `dismissLabel` of the text form) translate the hidden status text and the Dismiss button's name.
|
|
1150
|
+
- **`limit`** caps how many toasts show at once (`<Toaster limit={3}>`). Further toasts wait in a queue, in dispatch order, and appear as shown toasts go; a waiting toast is not announced and its timer does not run until it shows. A dismissed toast stops counting at once, also while an exit motion of yours runs. Lowering the limit never hides a shown toast. A value below 1 counts as 1 (with a development warning), and a fraction rounds down. Dispatching the id of a waiting toast again replaces it and keeps its place; `dismissToast(id)` removes a waiting toast silently.
|
|
1151
|
+
- **`dismissAllToasts()`** removes every toast, shown and waiting, and cancels their timers. When focus was in a toast, it returns to the element that had it before.
|
|
1152
|
+
- **Motion.** Each toast mounts through the presence core inside a container with the class `wave-toaster__toast`: style its `data-presence` phases for an enter and exit motion of yours (see [Enter and exit motion](#enter-and-exit-motion)). A dismissed toast stays mounted and `inert` until the exit ends; focus has already moved on. Wave ships no toast motion.
|
|
239
1153
|
|
|
240
|
-
|
|
241
|
-
npx tsc --noEmit
|
|
1154
|
+
**Toasts from code outside components.** A query cache, an event handler or a helper module cannot call a hook. Create a handle with `createToaster()` at module scope and bind a Toaster to it:
|
|
242
1155
|
|
|
243
|
-
|
|
244
|
-
|
|
1156
|
+
```tsx
|
|
1157
|
+
import * as React from 'react';
|
|
1158
|
+
import { Toast, Toaster, createToaster } from '@mortenbrudvik/waveui';
|
|
1159
|
+
|
|
1160
|
+
/** Module scope is safe: on the server a dispatch does nothing. */
|
|
1161
|
+
export const toaster = createToaster();
|
|
1162
|
+
|
|
1163
|
+
/** Callable from anywhere: a query cache's onError, an event handler, a helper. */
|
|
1164
|
+
export function notifyUploadFailed(message: string) {
|
|
1165
|
+
return toaster.dispatchToast(
|
|
1166
|
+
<Toast status="error">
|
|
1167
|
+
<Toast.Title>Upload failed</Toast.Title>
|
|
1168
|
+
<Toast.Body>{message}</Toast.Body>
|
|
1169
|
+
</Toast>,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function Notifications({ children }: { children: React.ReactNode }) {
|
|
1174
|
+
return <Toaster toaster={toaster}>{children}</Toaster>;
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
A handle's toasts get everything above (the announcements, the pausing, `limit`, the focus handling, the Drawer offset, reachability over modals), and their content reads React context from where the Toaster is mounted, so a router link works. Dispatches before a Toaster binds wait in the handle's queue and show once it binds; `dispatchToast` returns the id at once, so `dismissToast(id)` also removes a waiting toast. After the Toaster unmounts, the handle queues again until the next one binds. A handle binds one Toaster (a second one does not bind, and warns in development): give each Toaster its own handle to run several (Fluent's `toasterId`). `useToastController()` keeps working inside a bound Toaster. Keep notifications in the Toaster rather than in an area of your own: an open modal `Dialog` or `Drawer` makes the rest of the page `inert`, so your own area would take no clicks, drop out of the accessibility tree and sit under the dialog, while the Toaster's region stays usable over modals.
|
|
1179
|
+
|
|
1180
|
+
### Message bars
|
|
1181
|
+
|
|
1182
|
+
```tsx
|
|
1183
|
+
import { Button, MessageBar } from '@mortenbrudvik/waveui';
|
|
245
1184
|
|
|
246
|
-
|
|
247
|
-
|
|
1185
|
+
export function UpdateNotice({ onUpdate, onLater }: { onUpdate: () => void; onLater: () => void }) {
|
|
1186
|
+
return (
|
|
1187
|
+
<MessageBar status="warning" onDismiss={onLater}>
|
|
1188
|
+
<MessageBar.Title>Update available</MessageBar.Title>A new version is ready to install.
|
|
1189
|
+
<MessageBar.Actions>
|
|
1190
|
+
<Button size="small" onClick={onUpdate}>
|
|
1191
|
+
Update now
|
|
1192
|
+
</Button>
|
|
1193
|
+
<Button size="small" appearance="subtle" onClick={onLater}>
|
|
1194
|
+
Later
|
|
1195
|
+
</Button>
|
|
1196
|
+
</MessageBar.Actions>
|
|
1197
|
+
</MessageBar>
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
- **Parts.** `MessageBar.Title` is a semibold lead-in, read right after the severity; `MessageBar.Actions` holds buttons (Retry, Learn more) after the text. Both are direct children of the bar (Fragments count, and so do the flat `MessageBarTitle` and `MessageBarActions` written in a Server Component); one inside a component of yours renders where it is and warns in development. The built-in Dismiss button (`onDismiss`) stays the bar's own, after the actions.
|
|
1203
|
+
- **Layouts.** `layout="singleline"` keeps the actions on the text's row, `"multiline"` puts them on a row of their own under the text, and `"auto"` (the default) keeps them on the text's row while the text fits on one line and moves them below when it does not (measured after mount with a ResizeObserver; the server renders one row). No layout cuts text off: on one row it wraps inside its column. The DOM order is the same in every layout (icon, text, actions, Dismiss), so a reflow never moves the focused button, and the Tab order is the text's links, the actions, the Dismiss button. A bar without actions looks as in 0.7. The root carries `data-layout` with the layout in use (`singleline` or `multiline`).
|
|
1204
|
+
- **Names.** With its default role (`alert` for warnings and errors, `status` otherwise) the bar is a live region, and its title is read as the start of its content, once: "Warning: Update available A new version is ready to install.", then the actions' labels. A notice that stays on the page can take `role="region"`: then the title names the bar ("Warning: Update available"), which makes it a landmark; such a bar needs a title or an `aria-label` (a development warning says so). A consumer `aria-label` or `aria-labelledby` always wins.
|
|
1205
|
+
|
|
1206
|
+
### Badges
|
|
1207
|
+
|
|
1208
|
+
- `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.
|
|
1209
|
+
- **`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.
|
|
1210
|
+
- 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.
|
|
1211
|
+
- The root carries `data-color` and `data-appearance` with the resolved values (and `data-dot` on a dot), for your own selectors.
|
|
1212
|
+
|
|
1213
|
+
### Tags
|
|
1214
|
+
|
|
1215
|
+
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:
|
|
1216
|
+
|
|
1217
|
+
```tsx
|
|
1218
|
+
import * as React from 'react';
|
|
1219
|
+
import { Button, Tag } from '@mortenbrudvik/waveui';
|
|
1220
|
+
|
|
1221
|
+
const ALL_FILTERS = ['Red', 'Blue', 'Large'];
|
|
1222
|
+
|
|
1223
|
+
export function Filters() {
|
|
1224
|
+
const [filters, setFilters] = React.useState(ALL_FILTERS);
|
|
1225
|
+
const tags = React.useRef(new Map<string, HTMLElement>());
|
|
1226
|
+
const reset = React.useRef<HTMLButtonElement>(null);
|
|
1227
|
+
|
|
1228
|
+
const dismiss = (filter: string) => {
|
|
1229
|
+
const index = filters.indexOf(filter);
|
|
1230
|
+
const neighbour = filters[index + 1] ?? filters[index - 1];
|
|
1231
|
+
// The dismiss button is the tag's only button.
|
|
1232
|
+
const target =
|
|
1233
|
+
neighbour === undefined
|
|
1234
|
+
? reset.current
|
|
1235
|
+
: tags.current.get(neighbour)?.querySelector('button');
|
|
1236
|
+
target?.focus();
|
|
1237
|
+
setFilters((current) => current.filter((f) => f !== filter));
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
return (
|
|
1241
|
+
<div>
|
|
1242
|
+
<div role="group" aria-label="Active filters">
|
|
1243
|
+
{filters.map((filter) => (
|
|
1244
|
+
<Tag
|
|
1245
|
+
key={filter}
|
|
1246
|
+
dismissible
|
|
1247
|
+
dismissLabel="Remove"
|
|
1248
|
+
ref={(element) => {
|
|
1249
|
+
if (element) tags.current.set(filter, element);
|
|
1250
|
+
return () => {
|
|
1251
|
+
tags.current.delete(filter);
|
|
1252
|
+
};
|
|
1253
|
+
}}
|
|
1254
|
+
onDismiss={() => dismiss(filter)}
|
|
1255
|
+
>
|
|
1256
|
+
{filter}
|
|
1257
|
+
</Tag>
|
|
1258
|
+
))}
|
|
1259
|
+
</div>
|
|
1260
|
+
{/* Nothing to reset while every filter is set: unavailable, but still focusable. */}
|
|
1261
|
+
<Button
|
|
1262
|
+
ref={reset}
|
|
1263
|
+
disabledFocusable={filters.length === ALL_FILTERS.length}
|
|
1264
|
+
onClick={() => setFilters(ALL_FILTERS)}
|
|
1265
|
+
>
|
|
1266
|
+
Reset filters
|
|
1267
|
+
</Button>
|
|
1268
|
+
</div>
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
248
1271
|
```
|
|
249
1272
|
|
|
250
|
-
|
|
1273
|
+
### Sorting and selecting in a DataGrid
|
|
251
1274
|
|
|
252
|
-
|
|
1275
|
+
Sorting is controlled: the grid reports the requested sort and **you reorder the rows**.
|
|
1276
|
+
|
|
1277
|
+
```tsx
|
|
1278
|
+
import * as React from 'react';
|
|
1279
|
+
import { DataGrid } from '@mortenbrudvik/waveui';
|
|
1280
|
+
import type { DataGridColumn, DataGridSort } from '@mortenbrudvik/waveui';
|
|
1281
|
+
|
|
1282
|
+
interface Person {
|
|
1283
|
+
id: string;
|
|
1284
|
+
name: string;
|
|
1285
|
+
role: string;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const columns: DataGridColumn[] = [
|
|
1289
|
+
{ id: 'name', label: 'Name', sortable: true },
|
|
1290
|
+
{ id: 'role', label: 'Role' },
|
|
1291
|
+
];
|
|
1292
|
+
|
|
1293
|
+
export function People({ people }: { people: Person[] }) {
|
|
1294
|
+
const [sort, setSort] = React.useState<DataGridSort | null>(null);
|
|
1295
|
+
const rows = React.useMemo(() => {
|
|
1296
|
+
if (!sort) return people;
|
|
1297
|
+
const sorted = [...people].sort((a, b) => a.name.localeCompare(b.name));
|
|
1298
|
+
return sort.direction === 'ascending' ? sorted : sorted.reverse();
|
|
1299
|
+
}, [people, sort]);
|
|
1300
|
+
|
|
1301
|
+
return (
|
|
1302
|
+
<DataGrid
|
|
1303
|
+
aria-label="People"
|
|
1304
|
+
columns={columns}
|
|
1305
|
+
sort={sort}
|
|
1306
|
+
onSortChange={setSort}
|
|
1307
|
+
selectionMode="multiple"
|
|
1308
|
+
>
|
|
1309
|
+
<DataGrid.Body>
|
|
1310
|
+
{rows.map((person) => (
|
|
1311
|
+
<DataGrid.Row key={person.id} rowId={person.id}>
|
|
1312
|
+
<DataGrid.Cell>{person.name}</DataGrid.Cell>
|
|
1313
|
+
<DataGrid.Cell>{person.role}</DataGrid.Cell>
|
|
1314
|
+
</DataGrid.Row>
|
|
1315
|
+
))}
|
|
1316
|
+
</DataGrid.Body>
|
|
1317
|
+
</DataGrid>
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
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.
|
|
1323
|
+
|
|
1324
|
+
### Trees
|
|
1325
|
+
|
|
1326
|
+
```tsx
|
|
1327
|
+
import * as React from 'react';
|
|
1328
|
+
import { Button, Tree } from '@mortenbrudvik/waveui';
|
|
1329
|
+
|
|
1330
|
+
export function FileTree() {
|
|
1331
|
+
const [checked, setChecked] = React.useState<string[]>(['docs']);
|
|
1332
|
+
return (
|
|
1333
|
+
<Tree
|
|
1334
|
+
aria-label="Files"
|
|
1335
|
+
selectionMode="multiple"
|
|
1336
|
+
checkedItems={checked}
|
|
1337
|
+
onCheckedItemsChange={setChecked}
|
|
1338
|
+
>
|
|
1339
|
+
<Tree.Item value="docs" aside="3 files">
|
|
1340
|
+
Documents
|
|
1341
|
+
<Tree.Item value="cv">CV.pdf</Tree.Item>
|
|
1342
|
+
<Tree.Item value="letter">Letter.docx</Tree.Item>
|
|
1343
|
+
<Tree.Item value="notes">Notes.txt</Tree.Item>
|
|
1344
|
+
</Tree.Item>
|
|
1345
|
+
<Tree.Item
|
|
1346
|
+
value="photos"
|
|
1347
|
+
actions={
|
|
1348
|
+
<Button size="small" appearance="subtle">
|
|
1349
|
+
Share
|
|
1350
|
+
</Button>
|
|
1351
|
+
}
|
|
1352
|
+
>
|
|
1353
|
+
Photos
|
|
1354
|
+
<Tree.Item value="beach">Beach.jpg</Tree.Item>
|
|
1355
|
+
</Tree.Item>
|
|
1356
|
+
</Tree>
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
- **Selection modes.** `selectionMode` is `'none'` (the default: activation only, and `selected` highlights one item), `'single'` (a radio indicator on every item for `selected`/`onItemSelect`; pass `selected`, a development warning asks for it) or `'multiple'` (a checkbox on every item, bound to `checkedItems` or `defaultCheckedItems`; Fluent's `'multiselect'`). The root carries `data-selection-mode`.
|
|
1362
|
+
- **Checks cascade.** Checking an item checks it and all its descendants; unchecking does the reverse. A parent shows checked when all its descendants are checked, mixed when some are and unchecked when none are, and a listed parent also checks children that load later. `onCheckedItemsChange(checkedItems, details)` fires only on change, with a new array: every checked item in tree order (fully checked parents included, mixed ones not), then the listed values that match no item, unchanged, so values filtered out of the tree are not lost. `details` gives the toggled item's `value`, its new `checked` state and the DOM `event` (typed optional until 1.0).
|
|
1363
|
+
- **Lazy children.** A "Loading…" placeholder item is an item like any other: checking its parent lists its value too, and once the real children replace it, that value matches no item and stays in the list. Drop it from your state when the children arrive (a listed parent keeps checking them).
|
|
1364
|
+
- **Keys and clicks.** In a checkbox tree Space toggles the focused item and Enter activates it (`onItemSelect`); a click on the checkbox toggles, and a click elsewhere on the row activates. The checkbox is a real `role="checkbox"` named by the item, outside the Tab order, so touch screen readers and voice control reach it too. `*`, `current` and typeahead work as before (a Space typed within 500 ms of a letter continues the search).
|
|
1365
|
+
- **Row content.** `Tree.Item` takes `actions` (buttons at the end of the row, in a toolbar named by the item; shown while the row is hovered or has focus, and always in forced colors and on devices without hover), `aside` (a count or badge at the end of the row, part of the item's description; it gives way to the actions on hover and focus), `iconAfter` (decorative, after the label) and `expandIcon` (replaces the chevron: `null` or `undefined` keep it, `false` hides it, and `aria-expanded` still reports the state). Keys and clicks on the actions never activate, check or expand the item. Give an icon-only action an `aria-label`. Hidden actions keep their width, so a row does not shift when they show; prefer icon-only actions in a narrow tree.
|
|
1366
|
+
- **Dialogs opened from an action.** The actions also stay shown while a Menu, Popover or Dialog rendered inside them is open or has focus, so its focus return reaches the action. A Dialog rendered elsewhere and opened from an action through state (a "Delete…" menu item that opens a confirm Dialog outside the tree) closes after the actions have hidden again, on devices with hover and outside forced colors: a hidden action cannot take focus, so focus returns to the nearest element that can, normally the item. To return focus to the action, render the Dialog inside `actions`, with its `Dialog.Trigger` there or as a controlled `Dialog` next to the Menu that opens it (`finalFocusRef` cannot name the action, which is still hidden when the dialog closes).
|
|
1367
|
+
- **Navigation modes.** With `navigationMode="tree"` (the default), Tab from the focused item enters its actions (one stop; the arrow keys move inside them) and Shift+Tab returns. With `"treegrid"`, the "next" arrow (Right, Left in RTL) moves into the actions of an expanded parent or a leaf (a collapsed parent still expands first), "previous" returns, and Up and Down move between items from an action; the actions then take no Tab stop. This replaces APG's "next on an open item moves to its first child" (Down does that). The roles stay `tree` and `treeitem` in both modes, as in Fluent.
|
|
1368
|
+
- **Persona rows.** `Tree.ItemPersonaLayout` (flat `TreeItemPersonaLayout`, Fluent's name) lays out an item's label as a person: `media` (an Avatar of size 32 fits a row), the name as children and a `description` under it; the item is named by the name and described by the description.
|
|
1369
|
+
|
|
1370
|
+
### Slots
|
|
1371
|
+
|
|
1372
|
+
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:
|
|
1373
|
+
|
|
1374
|
+
```tsx
|
|
1375
|
+
import { Input } from '@mortenbrudvik/waveui';
|
|
1376
|
+
|
|
1377
|
+
export const Price = () => (
|
|
1378
|
+
<Input
|
|
1379
|
+
aria-label="Price"
|
|
1380
|
+
contentBefore="$"
|
|
1381
|
+
contentAfter={{ children: 'USD', className: 'text-muted-foreground' }}
|
|
1382
|
+
/>
|
|
1383
|
+
);
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
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>`).
|
|
1387
|
+
|
|
1388
|
+
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).
|
|
1389
|
+
|
|
1390
|
+
Slots that replace a built-in glyph follow one rule per kind:
|
|
1391
|
+
|
|
1392
|
+
- **Optional indicators** (`MenuButton.menuIcon`, the `expandIcon` of Combobox, TimePicker and `Tree.Item`): `null` or `undefined` keep the default glyph; `false` or any value that renders nothing hides it (for Combobox and TimePicker, the whole expand button).
|
|
1393
|
+
- **Required indicators** (`SplitButton.menuIcon`): the indicator always shows. A value that renders nothing keeps the default glyph, with a development warning.
|
|
1394
|
+
- **Status icons** (`MessageBar.icon`, `Toast.icon`, Field `validationMessageIcon`): only `undefined` keeps the default icon; `null` or a value that renders nothing shows none.
|
|
1395
|
+
- 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.
|
|
1396
|
+
|
|
1397
|
+
### Built-in text
|
|
1398
|
+
|
|
1399
|
+
The names and hidden texts that components render themselves are English by default. These props translate them:
|
|
1400
|
+
|
|
1401
|
+
| Component | Props |
|
|
1402
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1403
|
+
| `Dialog.Content`, `Drawer` | `closeLabel` (the Close button) |
|
|
1404
|
+
| `TeachingPopover` | `closeLabel` (the Close button), `labels` (`back`, `next`, `done`, `step(index, count)` with a zero-based index) |
|
|
1405
|
+
| `MessageBar`, `Toast` | `statusLabel` (the hidden severity text; also an option of both `dispatchToast` forms); `Toast` also `dismissLabel` (also an option of the text form) |
|
|
1406
|
+
| `Tag` | `dismissLabel` |
|
|
1407
|
+
| `Carousel` | `labels` (`previous`, `next`, `picker`, `slide(index, total)` with a zero-based index, `carouselRoleDescription`, `slideRoleDescription`), `autoPlayLabels` |
|
|
1408
|
+
| `Stepper` | `statusLabels` (`completed`, `error`; `''` omits the text) |
|
|
1409
|
+
| `Pagination` | `getItemAriaLabel(type, page, selected)` |
|
|
1410
|
+
| `ColorPicker` | `labels` (the 0.7 strings, plus `area` (the name of a bound area's group, "Saturation and brightness"), `saturation`, `brightness`, `roleDescription`, `hue`, `transparency`, `colorName(color)` and `valueText(color, channel)` for bound parts) |
|
|
1411
|
+
| `ColorArea` | `labels` (`saturation`, `brightness`, `roleDescription` ("2D slider"), `colorName(color)`, `valueText(color, channel)`) |
|
|
1412
|
+
| `ColorSlider` | `labels` (`hue`, `saturation`, `brightness` (the `value` channel), `colorName(color)`, `valueText(color, channel)`) |
|
|
1413
|
+
| `AlphaSlider` | `labels` (`opacity`, `transparency`, `valueText(color, transparency)`) |
|
|
1414
|
+
| `Combobox` | `labels` (`noMatches`, `clear` (the clear button), `expand` (the chevron button)) |
|
|
1415
|
+
| `Dropdown` | `labels` (`clear`, the clear button) |
|
|
1416
|
+
| `DatePicker` | `labels` (`clear`, `openCalendar`, `previousMonth`, `nextMonth`, and the error texts `invalidDate(pattern)`, `outOfRange(min, max)`, `unavailableDate`) |
|
|
1417
|
+
| `TimePicker` | `labels` (`clear`, `expand`, `list`, `noTimes`, `noMatches`, and the error texts `invalidTime(format)`, `outOfRange(min, max)`) |
|
|
1418
|
+
| `TagPicker` | `labels` (`remove(label)`, `selected`, `summary(labels)`, `added(label, count)`, `removed(label, count)`, `noMatches`) |
|
|
1419
|
+
| `Rating` | `labels` (`star(value, max)`) |
|
|
1420
|
+
| `RatingDisplay` | `labels` (`rating(value, max, formattedValue)`, `count(count, formattedCount)`); `locale` formats the value and the count (pass it when rendering on the server) |
|
|
1421
|
+
| `SpinButton` | `labels` (`increment`, `decrement`) |
|
|
1422
|
+
| `DataGrid` | `labels` (`selectAll`, `selectionHeader`); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
|
|
1423
|
+
| `AvatarGroup` | `overflowLabel`, `unnamedMemberLabel` |
|
|
1424
|
+
| `InfoLabel`, `SplitButton` | `infoButtonLabel`, `menuButtonLabel` |
|
|
1425
|
+
| `Spinner`, `Skeleton.Group` | `label` |
|
|
1426
|
+
|
|
1427
|
+
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`.
|
|
1428
|
+
|
|
1429
|
+
0.7 and 0.8 add no other 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, a toast's container by its severity and title, and a tree row's actions toolbar by its item's label.
|
|
1430
|
+
|
|
1431
|
+
## Keyboard support
|
|
1432
|
+
|
|
1433
|
+
As implemented in 0.8. 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.
|
|
1434
|
+
|
|
1435
|
+
| Component | Keys |
|
|
1436
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1437
|
+
| 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. |
|
|
1438
|
+
| 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. |
|
|
1439
|
+
| Rating | Right/Up one star more, Left/Down one fewer (never below 1), Home/End first/last; mirrored in RTL. |
|
|
1440
|
+
| 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. |
|
|
1441
|
+
| 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 activates; Space activates, or in a checkbox tree (`selectionMode="multiple"`) toggles the focused item's check; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). Row actions: Tab from the focused item enters them and Shift+Tab returns (`navigationMode="tree"`); with `navigationMode="treegrid"` the "next" arrow moves into them (on an expanded parent too, instead of to its first child), "previous" returns, and Up/Down move between items. |
|
|
1442
|
+
| 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. |
|
|
1443
|
+
| 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. |
|
|
1444
|
+
| 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. |
|
|
1445
|
+
| 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. |
|
|
1446
|
+
| 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. |
|
|
1447
|
+
| 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. |
|
|
1448
|
+
| 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. |
|
|
1449
|
+
| 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. |
|
|
1450
|
+
| 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. |
|
|
1451
|
+
| 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. |
|
|
1452
|
+
| Tooltip, InfoLabel | Open on keyboard focus (Tooltip after `openDelay`); Escape closes. InfoLabel: click pins it. |
|
|
1453
|
+
| Card (selectable) | With the default `selectionControl="card"`: Enter (key down) or Space (key up) selects. With `"checkbox"`: the built-in checkbox. |
|
|
1454
|
+
| Stepper | Each reachable step is a Tab stop; Enter/Space activate. |
|
|
1455
|
+
| Slider | Native range keys. |
|
|
1456
|
+
| Toaster | The app's `shortcuts.focus` key press focuses the newest toast; Tab and Shift+Tab move through the toasts' controls; Escape dismisses every toast and returns focus (a tooltip or menu of a toast closes first); Delete or Backspace dismisses the focused toast (the keys are ignored in editable fields), and focus moves to the next toast, or the previous one, else back to where it was before the toasts. |
|
|
1457
|
+
| ColorArea | One Tab stop. Left/Right change saturation (mirrored in RTL), Up/Down brightness; Shift with an arrow by 10; PageUp/PageDown brightness by 10; Home/End saturation to 0/100. |
|
|
1458
|
+
| ColorSlider, AlphaSlider | The arrows of their axis by 1 (mirrored in RTL when horizontal), Shift with an arrow by 10, PageUp/PageDown by 10, Home/End to the minimum/maximum. |
|
|
1459
|
+
|
|
1460
|
+
Accordion and Carousel have no arrow-key navigation: their triggers and controls are Tab stops.
|
|
1461
|
+
|
|
1462
|
+
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>`.
|
|
1463
|
+
|
|
1464
|
+
## Hooks and utilities
|
|
1465
|
+
|
|
1466
|
+
| Export | Purpose |
|
|
1467
|
+
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1468
|
+
| `useControllable(value, defaultValue, onChange)` | Controlled/uncontrolled state; returns `[value, setValue, isControlled]`; `setValue` accepts a value or an updater and skips no-op updates. |
|
|
1469
|
+
| `useRovingTabIndex(options)` | Roving tab stop and arrow-key navigation for composite widgets; items marked `data-disabled-focusable` stay reachable, a composite marked `data-roving-transparent` (a toolbar radio group) adds its items to the enclosing arrow order, and `tabStop: 'none'` leaves every item out of the Tab order (a composite reached by arrow keys only). |
|
|
1470
|
+
| `useId(prefix?)` | SSR-safe ids (`prefix-<react id>`; never parse the React part). |
|
|
1471
|
+
| `useEventCallback(fn)` | Stable callback that always calls the latest `fn`. |
|
|
1472
|
+
| `useMergedRefs(...refs)` | One stable callback ref for several refs. |
|
|
1473
|
+
| `useIsClient()` | `false` on the server and during hydration. |
|
|
1474
|
+
| `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)). |
|
|
1475
|
+
| `useFieldControl(props, options?)` | Merge a custom control's labelling props with the surrounding `Field` (its label, validation message, hint and state). |
|
|
1476
|
+
| `useAnnounce()`, `announce(message, politeness?)` | Screen-reader announcements through a shared live region. |
|
|
1477
|
+
| `useWaveTheme()` | Current theme, direction and portal container. |
|
|
1478
|
+
| `useToastController()` | `dispatchToast` (content or options) / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
|
|
1479
|
+
| `createToaster()` | A toast controller for code outside components; bind it with `<Toaster toaster={handle}>` (see [Toasts](#toasts)). Server-safe. |
|
|
1480
|
+
| `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
|
|
1481
|
+
| `hsvToHex(color)`, `hexToHsv(hex)` | Convert between an `HsvColor` and a hex string (`#rrggbb`, or `#rrggbbaa` below full opacity); `hexToHsv` returns `null` for other text. Server-safe. |
|
|
1482
|
+
| `buttonClassNames`, `toastTitleClassNames`, … | The stable class names of every component and part (see [Styling hooks](#styling-hooks)). Server-safe. |
|
|
1483
|
+
| `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. |
|
|
1484
|
+
| `composeEventHandlers(theirs, ours)` | Run a consumer handler, then yours unless it called `preventDefault()`. |
|
|
1485
|
+
| `mergeRefs(...refs)` | Merge refs outside components. |
|
|
1486
|
+
| `resolveSlot`, `renderSlot` | The slot helpers the components use. |
|
|
1487
|
+
| `getThemeClassName(theme)` | The theme classes of a theme. |
|
|
1488
|
+
|
|
1489
|
+
## Upgrading from 0.7
|
|
1490
|
+
|
|
1491
|
+
0.8 removes nothing public; these changes can still affect your tests and styles:
|
|
1492
|
+
|
|
1493
|
+
1. `Dialog.Content`'s 1rem gap to the viewport is now the surface's own margin: a margin class of yours replaces it on its sides, a percentage width or height resolves against the whole viewport, and a surface you position with `fixed` or `absolute` needs `m-0`.
|
|
1494
|
+
2. Muted `Dialog.Content` body text becomes the primary text colour, as in Drawer and Popover: put text that should stay secondary in an element with `text-muted-foreground`.
|
|
1495
|
+
3. Every component root and part carries a stable class (`wave-button`, `wave-button__icon`, …): assertions on a complete class string change.
|
|
1496
|
+
4. A toast in a Toaster renders inside a `role="group"` container named by its severity and title; the container now carries `data-wave-toast` and `data-presence`; the 0.5 `title` and body render as the `Toast.Title` and `Toast.Body` elements.
|
|
1497
|
+
5. Escape inside the toast region dismisses the toasts and no longer closes a Dialog or Drawer below them.
|
|
1498
|
+
6. Every toast pauses while the pointer or focus is anywhere in the toast region (0.7 paused the hovered or focused toast only).
|
|
1499
|
+
7. A toast dispatched with content that holds an action stays until it is dismissed, unless the dispatch passes `timeout`; the options form is unchanged.
|
|
1500
|
+
8. MessageBar renders `data-layout`, and Tree `data-selection-mode`.
|
|
1501
|
+
9. The default ColorPicker gains a color area and a hue slider, its opacity slider becomes an alpha slider (still named "Opacity"), and ColorPicker `children` now render a custom layout.
|
|
1502
|
+
10. `ToastController['dispatchToast']` is overloaded: a test double that implements it must accept content too.
|
|
1503
|
+
11. A `dispatchToast` options argument that is not a plain object (a class instance, `Object.create(defaults)`) is now read as React content and throws when it renders: pass a plain object (spread it: `{ ...defaults, title }`).
|
|
1504
|
+
|
|
1505
|
+
The CHANGELOG's [0.8.0 "Changed" section](CHANGELOG.md#changed) lists every behaviour, DOM and type change.
|
|
1506
|
+
|
|
1507
|
+
## Upgrading from 0.6
|
|
1508
|
+
|
|
1509
|
+
0.7 removes nothing public; these changes can still affect your setup, tests and styles:
|
|
1510
|
+
|
|
1511
|
+
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.
|
|
1512
|
+
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.
|
|
1513
|
+
3. Toolbar renders `data-size` and `data-orientation`, and `Toolbar` has static parts.
|
|
1514
|
+
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`.
|
|
1515
|
+
5. Tooltip `delay` is deprecated: rename it to `openDelay` (it still works and warns once in development).
|
|
1516
|
+
6. `Menu.Item` `persistOnClick` defaults to the Menu's new `persistOnItemClick` (`false`): no change unless you set it.
|
|
1517
|
+
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.
|
|
1518
|
+
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.
|
|
1519
|
+
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.
|
|
1520
|
+
|
|
1521
|
+
The CHANGELOG's [0.7.0 "Changed" section](CHANGELOG.md#changed-1) lists every behaviour, DOM and type change.
|
|
1522
|
+
|
|
1523
|
+
## Upgrading from 0.5
|
|
1524
|
+
|
|
1525
|
+
0.6 removes nothing public; these changes can still affect tests and styles:
|
|
1526
|
+
|
|
1527
|
+
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' })`).
|
|
1528
|
+
2. A Field keeps its hint visible below a validation message, and error messages show an icon (`validationMessageIcon={null}` removes it).
|
|
1529
|
+
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.
|
|
1530
|
+
4. TimePicker keeps rejected text on blur and flags it instead of reverting it; the list closes.
|
|
1531
|
+
5. Combobox and TimePicker show a chevron button (`expandIcon={false}` hides it), and Combobox and Dropdown wrap their control in a new `<div>`.
|
|
1532
|
+
6. Code that implements `ToastController` (a test double) adds `dismissAllToasts`.
|
|
1533
|
+
7. RatingDisplay formats its accessible name with the runtime locale when `locale` is omitted: pass `locale` when rendering on the server.
|
|
1534
|
+
8. Badge `color="important"` becomes a neutral color in 1.0: switch to `color="severe"` to keep the orange look.
|
|
1535
|
+
|
|
1536
|
+
The CHANGELOG's [0.6.0 "Changed" section](CHANGELOG.md#changed-2) lists every behaviour, DOM and type change.
|
|
1537
|
+
|
|
1538
|
+
## Upgrading from 0.4
|
|
1539
|
+
|
|
1540
|
+
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), [Upgrading from 0.6](#upgrading-from-06) and [Upgrading from 0.7](#upgrading-from-07).
|
|
1541
|
+
|
|
1542
|
+
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`.
|
|
1543
|
+
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.
|
|
1544
|
+
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.
|
|
1545
|
+
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.
|
|
1546
|
+
5. Replace deprecated props at your own pace; each warns once in development.
|
|
1547
|
+
|
|
1548
|
+
The [CHANGELOG](CHANGELOG.md) lists every change, including behaviour and DOM changes that can affect tests and styles.
|
|
1549
|
+
|
|
1550
|
+
## Development
|
|
1551
|
+
|
|
1552
|
+
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
1553
|
|
|
254
1554
|
```bash
|
|
255
|
-
|
|
1555
|
+
npm install
|
|
1556
|
+
npm run dev # Storybook on http://localhost:6006 (theme and direction toolbars)
|
|
1557
|
+
npm test # unit, integration, conventions and stories accessibility tests (Vitest)
|
|
1558
|
+
npm run typecheck # library, dev (tests + stories) and node TypeScript programs
|
|
1559
|
+
npm run lint # ESLint (fails on any warning)
|
|
1560
|
+
npm run build # type-check, library build, CSS build, dist verification
|
|
1561
|
+
npm run check:package # publint + are-the-types-wrong on a packed tarball
|
|
1562
|
+
npm run test:pack # pack the tarball and smoke-test it in plain and Tailwind fixtures
|
|
256
1563
|
```
|
|
257
1564
|
|
|
258
|
-
|
|
1565
|
+
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.
|
|
1566
|
+
|
|
1567
|
+
### Releasing
|
|
1568
|
+
|
|
1569
|
+
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
1570
|
|
|
260
1571
|
## License
|
|
261
1572
|
|