@remix-run/ui 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- package/src/utils/wait.ts +5 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// @jsxRuntime classic
|
|
2
|
+
// @jsx createElement
|
|
3
|
+
import {
|
|
4
|
+
attrs,
|
|
5
|
+
createElement,
|
|
6
|
+
createMixin,
|
|
7
|
+
css,
|
|
8
|
+
on,
|
|
9
|
+
ref,
|
|
10
|
+
type CSSMixinDescriptor,
|
|
11
|
+
type ElementProps,
|
|
12
|
+
type Handle,
|
|
13
|
+
type MixinHandle,
|
|
14
|
+
type Props,
|
|
15
|
+
type RemixNode,
|
|
16
|
+
} from '@remix-run/ui'
|
|
17
|
+
import * as button from '../button/button.tsx'
|
|
18
|
+
import { Glyph } from '../glyph/glyph.tsx'
|
|
19
|
+
import * as listbox from '../listbox/listbox.ts'
|
|
20
|
+
import * as popover from '../popover/popover.ts'
|
|
21
|
+
import { theme } from '../../theme/theme.ts'
|
|
22
|
+
import { hiddenTypeahead } from '../../interactions/typeahead/typeahead-mixin.ts'
|
|
23
|
+
import { onKeyDown } from '../../interactions/keydown/keydown.ts'
|
|
24
|
+
import { waitForCssTransition } from '../../utils/wait-for-css-transition.ts'
|
|
25
|
+
import { wait } from '../../utils/wait.ts'
|
|
26
|
+
import type { AnchorOptions } from '../anchor/anchor.ts'
|
|
27
|
+
|
|
28
|
+
const SELECT_CHANGE_EVENT = 'rmx:select-change' as const
|
|
29
|
+
const LABEL_SWAP_DELAY_MS = 75
|
|
30
|
+
|
|
31
|
+
type SelectChangeHandler = (event: SelectChangeEvent, signal: AbortSignal) => void | Promise<void>
|
|
32
|
+
|
|
33
|
+
declare global {
|
|
34
|
+
interface HTMLElementEventMap {
|
|
35
|
+
[SELECT_CHANGE_EVENT]: SelectChangeEvent
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SelectChangeEvent extends Event {
|
|
40
|
+
readonly label: string | null
|
|
41
|
+
readonly optionId: string | null
|
|
42
|
+
readonly value: string | null
|
|
43
|
+
|
|
44
|
+
constructor({
|
|
45
|
+
label,
|
|
46
|
+
optionId,
|
|
47
|
+
value,
|
|
48
|
+
}: {
|
|
49
|
+
label: string | null
|
|
50
|
+
optionId: string | null
|
|
51
|
+
value: string | null
|
|
52
|
+
}) {
|
|
53
|
+
super(SELECT_CHANGE_EVENT, { bubbles: true })
|
|
54
|
+
this.label = label
|
|
55
|
+
this.optionId = optionId
|
|
56
|
+
this.value = value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SelectContextProps {
|
|
61
|
+
children?: RemixNode
|
|
62
|
+
defaultLabel: string
|
|
63
|
+
defaultValue?: string | null
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
name?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SelectProps extends Omit<Props<'button'>, 'children' | 'name'> {
|
|
69
|
+
children?: RemixNode
|
|
70
|
+
defaultLabel: string
|
|
71
|
+
defaultValue?: string | null
|
|
72
|
+
name?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type SelectOptionProps = Props<'div'> & Omit<listbox.ListboxOption, 'id'>
|
|
76
|
+
|
|
77
|
+
enum State {
|
|
78
|
+
Initializing = 'initializing',
|
|
79
|
+
Closed = 'closed',
|
|
80
|
+
Open = 'open',
|
|
81
|
+
Selecting = 'selecting',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type PendingChange = {
|
|
85
|
+
label: string | null
|
|
86
|
+
optionId: string | null
|
|
87
|
+
value: string | null
|
|
88
|
+
} | null
|
|
89
|
+
|
|
90
|
+
interface SelectContextValue {
|
|
91
|
+
readonly activeId: string | undefined
|
|
92
|
+
readonly disabled: boolean
|
|
93
|
+
readonly displayedLabel: string
|
|
94
|
+
readonly isExpanded: boolean
|
|
95
|
+
readonly isOpen: boolean
|
|
96
|
+
readonly listId: string
|
|
97
|
+
readonly name: string | undefined
|
|
98
|
+
readonly selectedId: string | undefined
|
|
99
|
+
readonly value: listbox.ListboxValue
|
|
100
|
+
close: () => void
|
|
101
|
+
open: () => void
|
|
102
|
+
registerPopoverContext: (context: popover.PopoverContext) => void
|
|
103
|
+
registerSurface: (node: HTMLElement) => void
|
|
104
|
+
registerTrigger: (node: HTMLButtonElement) => void
|
|
105
|
+
selectTypeaheadMatch: (text: string) => void
|
|
106
|
+
syncPopoverMinWidth: () => void
|
|
107
|
+
unregisterPopoverContext: (context: popover.PopoverContext) => void
|
|
108
|
+
unregisterSurface: (node: HTMLElement) => void
|
|
109
|
+
unregisterTrigger: (node: HTMLButtonElement) => void
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>) {
|
|
113
|
+
let triggerRef: HTMLButtonElement | undefined
|
|
114
|
+
let listboxRef: listbox.ListboxRef | undefined
|
|
115
|
+
let surfaceRef: HTMLElement | undefined
|
|
116
|
+
let popoverContextRef: popover.PopoverContext | undefined
|
|
117
|
+
|
|
118
|
+
let state: State = State.Initializing
|
|
119
|
+
|
|
120
|
+
let value: listbox.ListboxValue = null
|
|
121
|
+
let activeValue: listbox.ListboxValue = null
|
|
122
|
+
let activeId: string | undefined = undefined
|
|
123
|
+
let selectedId: string | undefined = undefined
|
|
124
|
+
let selectedLabel = ''
|
|
125
|
+
let displayedLabel = ''
|
|
126
|
+
let pendingChange: PendingChange = null
|
|
127
|
+
|
|
128
|
+
let listId = `${handle.id}-list`
|
|
129
|
+
|
|
130
|
+
function open() {
|
|
131
|
+
if (state !== State.Closed || handle.props.disabled) return
|
|
132
|
+
state = State.Open
|
|
133
|
+
activeValue = value
|
|
134
|
+
handle.update()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function syncPopoverMinWidth() {
|
|
138
|
+
if (state !== State.Open || !surfaceRef || !triggerRef) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
surfaceRef.style.minWidth = `${triggerRef.offsetWidth}px`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getPopoverAnchorOptions(): AnchorOptions {
|
|
146
|
+
return {
|
|
147
|
+
placement: 'left',
|
|
148
|
+
inset: true,
|
|
149
|
+
relativeTo: selectedId ? `#${selectedId}` : '[role="option"]',
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function syncPopoverContext() {
|
|
154
|
+
if (!popoverContextRef) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
popoverContextRef.hideFocusTarget = triggerRef ?? null
|
|
159
|
+
popoverContextRef.anchor = triggerRef
|
|
160
|
+
? {
|
|
161
|
+
node: triggerRef,
|
|
162
|
+
options: getPopoverAnchorOptions(),
|
|
163
|
+
}
|
|
164
|
+
: null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function close() {
|
|
168
|
+
if (state !== State.Open) return
|
|
169
|
+
state = State.Closed
|
|
170
|
+
handle.update()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function setSelectedOption(
|
|
174
|
+
nextValue: listbox.ListboxValue,
|
|
175
|
+
option: listbox.ListboxOption | undefined,
|
|
176
|
+
syncDisplayedLabel = false,
|
|
177
|
+
) {
|
|
178
|
+
value = nextValue
|
|
179
|
+
activeValue = nextValue
|
|
180
|
+
activeId = option?.id
|
|
181
|
+
selectedId = option?.id
|
|
182
|
+
selectedLabel = option ? option.label : handle.props.defaultLabel
|
|
183
|
+
syncPopoverContext()
|
|
184
|
+
|
|
185
|
+
if (syncDisplayedLabel) {
|
|
186
|
+
displayedLabel = selectedLabel
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getPendingChange(
|
|
191
|
+
nextValue: listbox.ListboxValue,
|
|
192
|
+
option: listbox.ListboxOption | undefined,
|
|
193
|
+
): PendingChange {
|
|
194
|
+
if (!option || value === nextValue) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
label: option.label,
|
|
200
|
+
optionId: option.id,
|
|
201
|
+
value: option.value,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function dispatchChange(change: PendingChange) {
|
|
206
|
+
if (!change) {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let target = triggerRef ?? surfaceRef
|
|
211
|
+
target?.dispatchEvent(new SelectChangeEvent(change))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function selectOption(
|
|
215
|
+
nextValue: listbox.ListboxValue,
|
|
216
|
+
option: listbox.ListboxOption | undefined,
|
|
217
|
+
) {
|
|
218
|
+
if (state !== State.Open) return
|
|
219
|
+
pendingChange = getPendingChange(nextValue, option)
|
|
220
|
+
setSelectedOption(nextValue, option)
|
|
221
|
+
handle.update()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function settleSelectedOption() {
|
|
225
|
+
if (state !== State.Open) return
|
|
226
|
+
|
|
227
|
+
let change = pendingChange
|
|
228
|
+
pendingChange = null
|
|
229
|
+
state = State.Selecting
|
|
230
|
+
|
|
231
|
+
if (!surfaceRef) {
|
|
232
|
+
displayedLabel = selectedLabel
|
|
233
|
+
state = State.Closed
|
|
234
|
+
let signal = await handle.update()
|
|
235
|
+
if (signal.aborted) return
|
|
236
|
+
dispatchChange(change)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await Promise.all([handle.update(), waitForCssTransition(surfaceRef, handle.signal)])
|
|
241
|
+
await wait(LABEL_SWAP_DELAY_MS) // UX delay label swap for clear value change
|
|
242
|
+
if (handle.signal.aborted) return
|
|
243
|
+
displayedLabel = selectedLabel
|
|
244
|
+
state = State.Closed
|
|
245
|
+
let signal = await handle.update()
|
|
246
|
+
if (signal.aborted) return
|
|
247
|
+
dispatchChange(change)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function selectTypeaheadMatch(text: string) {
|
|
251
|
+
if (state !== State.Closed || handle.props.disabled) return
|
|
252
|
+
|
|
253
|
+
let option = listboxRef?.matchSearchText(text, value)
|
|
254
|
+
if (!option) return
|
|
255
|
+
|
|
256
|
+
let change = getPendingChange(option.value, option)
|
|
257
|
+
pendingChange = null
|
|
258
|
+
setSelectedOption(option.value, option, true)
|
|
259
|
+
void handle.update().then((signal) => {
|
|
260
|
+
if (signal.aborted) return
|
|
261
|
+
dispatchChange(change)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function highlightOption(
|
|
266
|
+
nextActiveValue: listbox.ListboxValue,
|
|
267
|
+
option: listbox.ListboxOption | undefined,
|
|
268
|
+
) {
|
|
269
|
+
if (state !== State.Open) return
|
|
270
|
+
activeValue = nextActiveValue
|
|
271
|
+
activeId = option?.id
|
|
272
|
+
handle.update()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
handle.context.set({
|
|
276
|
+
get activeId() {
|
|
277
|
+
return activeId
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
get disabled() {
|
|
281
|
+
return !!handle.props.disabled
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
get displayedLabel() {
|
|
285
|
+
return displayedLabel
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
get isExpanded() {
|
|
289
|
+
return state === State.Open || state === State.Selecting
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
get isOpen() {
|
|
293
|
+
return state === State.Open
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
get listId() {
|
|
297
|
+
return listId
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
get name() {
|
|
301
|
+
return handle.props.name
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
get selectedId() {
|
|
305
|
+
return selectedId
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
get value() {
|
|
309
|
+
return value
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
close,
|
|
313
|
+
|
|
314
|
+
open,
|
|
315
|
+
|
|
316
|
+
registerSurface(node) {
|
|
317
|
+
surfaceRef = node
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
registerTrigger(node) {
|
|
321
|
+
triggerRef = node
|
|
322
|
+
syncPopoverContext()
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
registerPopoverContext(context) {
|
|
326
|
+
popoverContextRef = context
|
|
327
|
+
syncPopoverContext()
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
selectTypeaheadMatch,
|
|
331
|
+
|
|
332
|
+
syncPopoverMinWidth,
|
|
333
|
+
|
|
334
|
+
unregisterSurface(node) {
|
|
335
|
+
if (surfaceRef === node) {
|
|
336
|
+
surfaceRef = undefined
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
unregisterPopoverContext(context) {
|
|
341
|
+
if (popoverContextRef !== context) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
popoverContextRef.anchor = null
|
|
346
|
+
popoverContextRef.hideFocusTarget = null
|
|
347
|
+
popoverContextRef = undefined
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
unregisterTrigger(node) {
|
|
351
|
+
if (triggerRef === node) {
|
|
352
|
+
triggerRef = undefined
|
|
353
|
+
syncPopoverContext()
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
return () => {
|
|
359
|
+
if (state === State.Initializing) {
|
|
360
|
+
selectedLabel = displayedLabel = handle.props.defaultLabel
|
|
361
|
+
value = handle.props.defaultValue ?? null
|
|
362
|
+
activeValue = value
|
|
363
|
+
state = State.Closed
|
|
364
|
+
|
|
365
|
+
handle.queueTask(() => {
|
|
366
|
+
if (selectedId || !surfaceRef) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let selected = surfaceRef.querySelector(`[aria-selected="true"]`)
|
|
371
|
+
if (selected && !selectedId) {
|
|
372
|
+
selectedId = selected.id
|
|
373
|
+
syncPopoverContext()
|
|
374
|
+
handle.update()
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<listbox.Context
|
|
381
|
+
flashSelection
|
|
382
|
+
ref={(nextListboxRef: listbox.ListboxRef) => {
|
|
383
|
+
listboxRef = nextListboxRef
|
|
384
|
+
}}
|
|
385
|
+
value={value}
|
|
386
|
+
activeValue={activeValue}
|
|
387
|
+
onSelectSettled={settleSelectedOption}
|
|
388
|
+
onSelect={selectOption}
|
|
389
|
+
onHighlight={highlightOption}
|
|
390
|
+
selectionFlashAttribute="data-select-flash"
|
|
391
|
+
>
|
|
392
|
+
{handle.props.children}
|
|
393
|
+
</listbox.Context>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function getSelectContext(handle: Handle | MixinHandle) {
|
|
399
|
+
return handle.context.get(SelectProvider)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) => {
|
|
403
|
+
let context = getSelectContext(handle)
|
|
404
|
+
|
|
405
|
+
return (props) => [
|
|
406
|
+
attrs({
|
|
407
|
+
'aria-haspopup': 'listbox',
|
|
408
|
+
'aria-expanded': context.isExpanded ? 'true' : 'false',
|
|
409
|
+
'aria-controls': context.listId,
|
|
410
|
+
'aria-describedby': context.selectedId,
|
|
411
|
+
disabled: context.disabled ? true : props.disabled,
|
|
412
|
+
}),
|
|
413
|
+
ref((node: HTMLButtonElement, signal) => {
|
|
414
|
+
context.registerTrigger(node)
|
|
415
|
+
signal.addEventListener('abort', () => {
|
|
416
|
+
context.unregisterTrigger(node)
|
|
417
|
+
})
|
|
418
|
+
}),
|
|
419
|
+
hiddenTypeahead((text) => {
|
|
420
|
+
context.selectTypeaheadMatch(text)
|
|
421
|
+
}),
|
|
422
|
+
on('click', () => {
|
|
423
|
+
context.open()
|
|
424
|
+
}),
|
|
425
|
+
onKeyDown('ArrowDown', () => {
|
|
426
|
+
context.open()
|
|
427
|
+
}),
|
|
428
|
+
onKeyDown('ArrowUp', () => {
|
|
429
|
+
context.open()
|
|
430
|
+
}),
|
|
431
|
+
]
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
|
|
435
|
+
let context = getSelectContext(handle)
|
|
436
|
+
let popoverState = handle.context.get(popover.Context)
|
|
437
|
+
|
|
438
|
+
return () => [
|
|
439
|
+
ref((node: HTMLElement, signal) => {
|
|
440
|
+
context.registerSurface(node)
|
|
441
|
+
context.registerPopoverContext(popoverState)
|
|
442
|
+
signal.addEventListener('abort', () => {
|
|
443
|
+
context.unregisterSurface(node)
|
|
444
|
+
context.unregisterPopoverContext(popoverState)
|
|
445
|
+
})
|
|
446
|
+
}),
|
|
447
|
+
popover.surface({
|
|
448
|
+
open: context.isOpen,
|
|
449
|
+
onHide: context.close,
|
|
450
|
+
}),
|
|
451
|
+
on('beforetoggle', (event) => {
|
|
452
|
+
if (event.newState === 'open') {
|
|
453
|
+
context.syncPopoverMinWidth()
|
|
454
|
+
}
|
|
455
|
+
}),
|
|
456
|
+
]
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
|
|
460
|
+
let context = getSelectContext(handle)
|
|
461
|
+
|
|
462
|
+
return () => [
|
|
463
|
+
attrs({
|
|
464
|
+
id: context.listId,
|
|
465
|
+
'aria-activedescendant': context.activeId,
|
|
466
|
+
}),
|
|
467
|
+
popover.focusOnShow(),
|
|
468
|
+
listbox.list(),
|
|
469
|
+
]
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
|
|
473
|
+
let context = getSelectContext(handle)
|
|
474
|
+
|
|
475
|
+
return () =>
|
|
476
|
+
attrs({
|
|
477
|
+
disabled: context.disabled ? true : undefined,
|
|
478
|
+
name: context.name,
|
|
479
|
+
type: 'hidden',
|
|
480
|
+
value: context.value ?? '',
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const selectTriggerCss: CSSMixinDescriptor = css({
|
|
485
|
+
minHeight: theme.control.height.sm,
|
|
486
|
+
width: '100%',
|
|
487
|
+
paddingInline: theme.space.md,
|
|
488
|
+
paddingInlineEnd: theme.space.sm,
|
|
489
|
+
display: 'grid',
|
|
490
|
+
gridTemplateColumns: 'minmax(0, 1fr) auto',
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
gap: theme.space.sm,
|
|
493
|
+
borderRadius: theme.radius.md,
|
|
494
|
+
backgroundImage: 'none',
|
|
495
|
+
border: '0.5px solid transparent',
|
|
496
|
+
boxShadow: 'none',
|
|
497
|
+
fontSize: theme.fontSize.xs,
|
|
498
|
+
textAlign: 'left',
|
|
499
|
+
backgroundColor: theme.surface.lvl3,
|
|
500
|
+
color: theme.colors.text.secondary,
|
|
501
|
+
'&:hover, &:focus-visible, &[aria-expanded="true"], &[aria-expanded="true"]:hover, &[aria-expanded="true"]:focus-visible':
|
|
502
|
+
{
|
|
503
|
+
backgroundColor: theme.surface.lvl4,
|
|
504
|
+
color: theme.colors.text.primary,
|
|
505
|
+
},
|
|
506
|
+
'&:active': {
|
|
507
|
+
backgroundColor: theme.surface.lvl3,
|
|
508
|
+
},
|
|
509
|
+
'&:focus-visible': {
|
|
510
|
+
outline: `2px solid ${theme.colors.focus.ring}`,
|
|
511
|
+
outlineOffset: '2px',
|
|
512
|
+
},
|
|
513
|
+
'&:disabled': {
|
|
514
|
+
opacity: 0.6,
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
export const triggerStyle = selectTriggerCss
|
|
519
|
+
export const Context = SelectProvider
|
|
520
|
+
export const hiddenInput = hiddenInputMixin
|
|
521
|
+
export const list = listMixin
|
|
522
|
+
export const option = listbox.option
|
|
523
|
+
export { popoverMixin as popover }
|
|
524
|
+
export const trigger = triggerMixin
|
|
525
|
+
|
|
526
|
+
const select = {
|
|
527
|
+
Context,
|
|
528
|
+
hiddenInput,
|
|
529
|
+
list,
|
|
530
|
+
option,
|
|
531
|
+
popover: popoverMixin,
|
|
532
|
+
trigger,
|
|
533
|
+
} as const
|
|
534
|
+
|
|
535
|
+
export function onSelectChange(handler: SelectChangeHandler, captureBoolean?: boolean) {
|
|
536
|
+
return on<HTMLElement, typeof SELECT_CHANGE_EVENT>(SELECT_CHANGE_EVENT, handler, captureBoolean)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function SelectLabel(handle: Handle) {
|
|
540
|
+
let context = getSelectContext(handle)
|
|
541
|
+
|
|
542
|
+
return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function Select(handle: Handle<SelectProps>) {
|
|
546
|
+
return () => {
|
|
547
|
+
let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<select.Context
|
|
551
|
+
defaultLabel={defaultLabel}
|
|
552
|
+
defaultValue={defaultValue}
|
|
553
|
+
disabled={disabled}
|
|
554
|
+
name={name}
|
|
555
|
+
>
|
|
556
|
+
<button {...buttonProps} mix={[button.baseStyle, triggerStyle, select.trigger(), mix]}>
|
|
557
|
+
<SelectLabel />
|
|
558
|
+
<Glyph mix={button.iconStyle} name="chevronVertical" />
|
|
559
|
+
</button>
|
|
560
|
+
<popover.Context>
|
|
561
|
+
<div mix={[popover.surfaceStyle, select.popover()]}>
|
|
562
|
+
<div mix={[popover.contentStyle, listbox.listStyle, select.list()]}>{children}</div>
|
|
563
|
+
</div>
|
|
564
|
+
</popover.Context>
|
|
565
|
+
{name && <input mix={select.hiddenInput()} />}
|
|
566
|
+
</select.Context>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function Option(handle: Handle<SelectOptionProps>) {
|
|
572
|
+
return () => {
|
|
573
|
+
let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div
|
|
577
|
+
{...divProps}
|
|
578
|
+
mix={[listbox.optionStyle, select.option({ value, label, disabled, textValue }), mix]}
|
|
579
|
+
>
|
|
580
|
+
<Glyph mix={listbox.glyphStyle} name="check" />
|
|
581
|
+
<span mix={listbox.labelStyle}>{children ?? handle.props.label}</span>
|
|
582
|
+
</div>
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { css, type CSSMixinDescriptor } from '@remix-run/ui'
|
|
2
|
+
|
|
3
|
+
import { theme } from '../../theme/theme.ts'
|
|
4
|
+
|
|
5
|
+
const itemInset = `var(--rmx-ui-item-inset, ${theme.space.sm})`
|
|
6
|
+
const itemIndicatorHalfInset =
|
|
7
|
+
'calc((var(--rmx-ui-item-indicator-width, 0px) + var(--rmx-ui-item-indicator-gap, 0px)) / 2)'
|
|
8
|
+
|
|
9
|
+
const separatorCss: CSSMixinDescriptor = css({
|
|
10
|
+
flexShrink: 0,
|
|
11
|
+
margin: 0,
|
|
12
|
+
marginBlock: theme.space.xs,
|
|
13
|
+
marginInlineStart: `calc(${itemInset} + ${itemIndicatorHalfInset})`,
|
|
14
|
+
marginInlineEnd: itemInset,
|
|
15
|
+
border: 0,
|
|
16
|
+
borderTop: `1px solid ${theme.colors.border.subtle}`,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const separatorStyle = separatorCss
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
`tabs` is a primitive for building accessible tabs with automatic activation and simple default styling.
|
|
4
|
+
|
|
5
|
+
Use it when you need a tablist, tab triggers, and tab panels. `tabs.listStyle` and `tabs.triggerStyle` style the tab row and triggers, but panel presentation stays app-owned.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { css, type Handle } from 'remix/ui'
|
|
11
|
+
import { Tab, Tabs, TabsList, TabsPanel } from 'remix/ui/tabs'
|
|
12
|
+
|
|
13
|
+
let panel = css({
|
|
14
|
+
padding: '16px',
|
|
15
|
+
border: '1px solid #d6d6d6',
|
|
16
|
+
borderRadius: '16px',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export function ProjectTabs(_handle: Handle) {
|
|
20
|
+
return () => (
|
|
21
|
+
<Tabs defaultValue="overview">
|
|
22
|
+
<TabsList aria-label="Project sections">
|
|
23
|
+
<Tab value="overview">Overview</Tab>
|
|
24
|
+
<Tab value="analytics">Analytics</Tab>
|
|
25
|
+
<Tab value="reports">Reports</Tab>
|
|
26
|
+
<Tab value="settings">Settings</Tab>
|
|
27
|
+
</TabsList>
|
|
28
|
+
|
|
29
|
+
<TabsPanel mix={panel} value="overview">
|
|
30
|
+
View your key metrics and recent project activity.
|
|
31
|
+
</TabsPanel>
|
|
32
|
+
<TabsPanel mix={panel} value="analytics">
|
|
33
|
+
Track trends and compare performance over time.
|
|
34
|
+
</TabsPanel>
|
|
35
|
+
<TabsPanel mix={panel} value="reports">
|
|
36
|
+
Export and review your latest reports.
|
|
37
|
+
</TabsPanel>
|
|
38
|
+
<TabsPanel mix={panel} value="settings">
|
|
39
|
+
Manage project preferences and defaults.
|
|
40
|
+
</TabsPanel>
|
|
41
|
+
</Tabs>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `tabs.*`
|
|
47
|
+
|
|
48
|
+
### `tabs.Context`
|
|
49
|
+
|
|
50
|
+
Provides the shared coordinator for one tabs instance.
|
|
51
|
+
|
|
52
|
+
- Owns the selected tab value for uncontrolled usage.
|
|
53
|
+
- Accepts controlled `value` plus `onValueChange` when the app owns selection state.
|
|
54
|
+
- Accepts `orientation="vertical"` when the tablist should use `ArrowUp` and `ArrowDown`.
|
|
55
|
+
|
|
56
|
+
### `tabs.list()`
|
|
57
|
+
|
|
58
|
+
Turns the host into the tablist.
|
|
59
|
+
|
|
60
|
+
- Wires `role="tablist"`.
|
|
61
|
+
- Adds `aria-orientation="vertical"` when needed.
|
|
62
|
+
|
|
63
|
+
### `tabs.trigger({ value, disabled })`
|
|
64
|
+
|
|
65
|
+
Registers one tab trigger.
|
|
66
|
+
|
|
67
|
+
- Wires `role="tab"`, `aria-selected`, `aria-controls`, and roving `tabIndex`.
|
|
68
|
+
- Selects on press.
|
|
69
|
+
- Supports `ArrowLeft` and `ArrowRight` for horizontal lists.
|
|
70
|
+
- Supports `ArrowUp` and `ArrowDown` for vertical lists.
|
|
71
|
+
- Supports `Home` and `End`.
|
|
72
|
+
|
|
73
|
+
### `tabs.panel({ value })`
|
|
74
|
+
|
|
75
|
+
Turns the host into the panel for one tab value.
|
|
76
|
+
|
|
77
|
+
- Wires `role="tabpanel"` and `aria-labelledby`.
|
|
78
|
+
- Hides inactive panels with `hidden`.
|
|
79
|
+
|
|
80
|
+
### `onTabsChange(...)`
|
|
81
|
+
|
|
82
|
+
The listener mixin for bubbled tab selection changes.
|
|
83
|
+
|
|
84
|
+
- `event.value` is the newly selected tab value.
|
|
85
|
+
- `event.previousValue` is the previously selected value, or `null`.
|
|
86
|
+
|
|
87
|
+
## Convenience Components
|
|
88
|
+
|
|
89
|
+
- `Tabs` is a thin wrapper around `tabs.Context`.
|
|
90
|
+
- `TabsList` applies `tabs.list()` with `tabs.listStyle`.
|
|
91
|
+
- `Tab` applies `tabs.trigger(...)` with `button.baseStyle` and `tabs.triggerStyle`.
|
|
92
|
+
- `TabsPanel` applies `tabs.panel(...)` without adding panel styling.
|
|
93
|
+
|
|
94
|
+
## Behavior Notes
|
|
95
|
+
|
|
96
|
+
- When you do not provide `defaultValue` or controlled `value`, the first enabled tab becomes selected automatically.
|
|
97
|
+
- Arrow-key navigation wraps and skips disabled tabs.
|
|
98
|
+
- Selection follows focus during arrow-key navigation.
|
|
99
|
+
- The default panel contract is behavioral only. Apps own spacing, borders, backgrounds, and layout for tab panels.
|
|
100
|
+
|
|
101
|
+
## When To Use Something Else
|
|
102
|
+
|
|
103
|
+
- Use `accordion` when sections should expand and collapse independently instead of switching a single active panel.
|
|
104
|
+
- Use `select` or `listbox` when the control chooses a form value instead of changing visible page sections.
|
|
105
|
+
- Use `menu` for action lists and command surfaces.
|