@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,1014 @@
|
|
|
1
|
+
// @jsxRuntime classic
|
|
2
|
+
// @jsx createElement
|
|
3
|
+
import {
|
|
4
|
+
attrs,
|
|
5
|
+
css,
|
|
6
|
+
createElement,
|
|
7
|
+
createMixin,
|
|
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
|
+
|
|
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 { type SearchValue } from '../../interactions/typeahead/typeahead-mixin.ts'
|
|
23
|
+
import { waitForCssTransition } from '../../utils/wait-for-css-transition.ts'
|
|
24
|
+
import { wait } from '../../utils/wait.ts'
|
|
25
|
+
|
|
26
|
+
const COMBOBOX_CHANGE_EVENT = 'rmx:combobox-change' as const
|
|
27
|
+
const INPUT_COMMIT_DELAY_MS = 50
|
|
28
|
+
|
|
29
|
+
type ComboboxChangeHandler = (
|
|
30
|
+
event: ComboboxChangeEvent,
|
|
31
|
+
signal: AbortSignal,
|
|
32
|
+
) => void | Promise<void>
|
|
33
|
+
|
|
34
|
+
type ShowReason = 'hint' | 'nav'
|
|
35
|
+
|
|
36
|
+
const comboboxPopoverCss: CSSMixinDescriptor = css({
|
|
37
|
+
opacity: 0,
|
|
38
|
+
'&:popover-open': {
|
|
39
|
+
opacity: 1,
|
|
40
|
+
},
|
|
41
|
+
'&:not(:popover-open)': {
|
|
42
|
+
pointerEvents: 'none',
|
|
43
|
+
},
|
|
44
|
+
'&[data-show-reason="nav"]:not(:popover-open)': {
|
|
45
|
+
transition: 'opacity 180ms ease-in, overlay 180ms ease-in, display 180ms ease-in',
|
|
46
|
+
transitionBehavior: 'allow-discrete',
|
|
47
|
+
},
|
|
48
|
+
'&[data-show-reason="hint"]:not(:popover-open)': {
|
|
49
|
+
transition: 'none',
|
|
50
|
+
transitionBehavior: 'normal',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const comboboxInputCss: CSSMixinDescriptor = css({
|
|
55
|
+
minHeight: theme.control.height.sm,
|
|
56
|
+
width: '100%',
|
|
57
|
+
paddingInline: theme.space.sm,
|
|
58
|
+
border: `0.5px solid ${theme.colors.border.default}`,
|
|
59
|
+
borderRadius: theme.radius.md,
|
|
60
|
+
backgroundColor: theme.surface.lvl0,
|
|
61
|
+
color: theme.colors.text.primary,
|
|
62
|
+
fontFamily: theme.fontFamily.sans,
|
|
63
|
+
fontSize: theme.fontSize.sm,
|
|
64
|
+
lineHeight: theme.lineHeight.normal,
|
|
65
|
+
boxShadow: 'inset 0 1px 0 rgb(255 255 255 / 0.7)',
|
|
66
|
+
'&:focus-visible': {
|
|
67
|
+
outline: `2px solid ${theme.colors.focus.ring}`,
|
|
68
|
+
outlineOffset: theme.space.none,
|
|
69
|
+
},
|
|
70
|
+
'&[data-surface-visible="true"][aria-activedescendant]:focus-visible': {
|
|
71
|
+
outline: 'none',
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const inputStyle = comboboxInputCss
|
|
76
|
+
export const popoverStyle = comboboxPopoverCss
|
|
77
|
+
|
|
78
|
+
declare global {
|
|
79
|
+
interface HTMLElementEventMap {
|
|
80
|
+
[COMBOBOX_CHANGE_EVENT]: ComboboxChangeEvent
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ComboboxChangeEvent extends Event {
|
|
85
|
+
readonly label: string | null
|
|
86
|
+
readonly optionId: string | null
|
|
87
|
+
readonly value: string | null
|
|
88
|
+
|
|
89
|
+
constructor({
|
|
90
|
+
label,
|
|
91
|
+
optionId,
|
|
92
|
+
value,
|
|
93
|
+
}: {
|
|
94
|
+
label: string | null
|
|
95
|
+
optionId: string | null
|
|
96
|
+
value: string | null
|
|
97
|
+
}) {
|
|
98
|
+
super(COMBOBOX_CHANGE_EVENT, { bubbles: true })
|
|
99
|
+
this.label = label
|
|
100
|
+
this.optionId = optionId
|
|
101
|
+
this.value = value
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type ComboboxOpenStrategy = 'selected' | 'selected-or-none' | 'first' | 'last'
|
|
106
|
+
|
|
107
|
+
export interface ComboboxHandle {
|
|
108
|
+
readonly activeOptionId: string | null
|
|
109
|
+
readonly id: string
|
|
110
|
+
readonly inputText: string
|
|
111
|
+
readonly isOpen: boolean
|
|
112
|
+
readonly label: string | null
|
|
113
|
+
readonly value: string | null
|
|
114
|
+
close: () => void
|
|
115
|
+
open: (strategy?: ComboboxOpenStrategy) => Promise<void>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ComboboxContextProps {
|
|
119
|
+
children?: RemixNode
|
|
120
|
+
defaultValue?: string | null
|
|
121
|
+
disabled?: boolean
|
|
122
|
+
name?: string
|
|
123
|
+
ref?: (handle: ComboboxHandle) => void
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ComboboxProps extends Omit<Props<'div'>, 'children'> {
|
|
127
|
+
children?: RemixNode
|
|
128
|
+
defaultValue?: string | null
|
|
129
|
+
disabled?: boolean
|
|
130
|
+
inputId?: string
|
|
131
|
+
name?: string
|
|
132
|
+
placeholder?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ComboboxOptionOptions {
|
|
136
|
+
disabled?: boolean
|
|
137
|
+
label: string
|
|
138
|
+
searchValue?: SearchValue
|
|
139
|
+
value: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ComboboxOptionProps extends Omit<Props<'div'>, 'children'> {
|
|
143
|
+
children?: RemixNode
|
|
144
|
+
disabled?: boolean
|
|
145
|
+
label: string
|
|
146
|
+
searchValue?: SearchValue
|
|
147
|
+
value: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ComboboxContextValue {
|
|
151
|
+
readonly activeId: string | undefined
|
|
152
|
+
readonly disabled: boolean
|
|
153
|
+
readonly filterText: string
|
|
154
|
+
readonly inputText: string
|
|
155
|
+
readonly isOpen: boolean
|
|
156
|
+
readonly listId: string
|
|
157
|
+
readonly name: string | undefined
|
|
158
|
+
readonly showReason: ShowReason
|
|
159
|
+
readonly surfaceVisible: boolean
|
|
160
|
+
readonly value: listbox.ListboxValue
|
|
161
|
+
clearInputSelection: () => void
|
|
162
|
+
close: () => void
|
|
163
|
+
handleBlur: () => void
|
|
164
|
+
handleEscape: () => void
|
|
165
|
+
navigateNext: () => void
|
|
166
|
+
navigatePrevious: () => void
|
|
167
|
+
open: (strategy?: ComboboxOpenStrategy) => Promise<void>
|
|
168
|
+
openFromArrow: (direction: 'first' | 'last') => Promise<void>
|
|
169
|
+
openFromInputActivation: () => Promise<void>
|
|
170
|
+
registerInput: (node: HTMLInputElement) => void
|
|
171
|
+
registerSurface: (node: HTMLElement) => void
|
|
172
|
+
setInputText: (text: string) => Promise<void>
|
|
173
|
+
syncPopoverMinWidth: () => void
|
|
174
|
+
selectActive: () => Promise<void>
|
|
175
|
+
unregisterInput: (node: HTMLInputElement) => void
|
|
176
|
+
unregisterSurface: (node: HTMLElement) => void
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type PendingChange = {
|
|
180
|
+
label: string
|
|
181
|
+
optionId: string
|
|
182
|
+
value: string
|
|
183
|
+
} | null
|
|
184
|
+
|
|
185
|
+
function getSearchValues(value: SearchValue) {
|
|
186
|
+
return Array.isArray(value) ? value : [value]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesSearchValue(value: SearchValue, text: string) {
|
|
190
|
+
if (text === '') {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let normalizedText = text.toLowerCase()
|
|
195
|
+
return getSearchValues(value).some((candidate) =>
|
|
196
|
+
candidate.toLowerCase().startsWith(normalizedText),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function matchesExactSearchValue(value: SearchValue, text: string) {
|
|
201
|
+
if (text === '') {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let normalizedText = text.toLowerCase()
|
|
206
|
+
return getSearchValues(value).some((candidate) => candidate.toLowerCase() === normalizedText)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getOptionSearchValue(option: {
|
|
210
|
+
label: string
|
|
211
|
+
searchValue?: SearchValue
|
|
212
|
+
textValue?: SearchValue
|
|
213
|
+
}) {
|
|
214
|
+
return option.searchValue ?? option.textValue ?? option.label
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ComboboxProvider(handle: Handle<ComboboxContextProps, ComboboxContextValue>) {
|
|
218
|
+
let inputRef: HTMLInputElement | undefined
|
|
219
|
+
let listboxRef: listbox.ListboxRef | undefined
|
|
220
|
+
let surfaceRef: HTMLElement | undefined
|
|
221
|
+
|
|
222
|
+
let hasInitialized = false
|
|
223
|
+
let hasProvidedRef = false
|
|
224
|
+
|
|
225
|
+
let activeValue: listbox.ListboxValue = null
|
|
226
|
+
let activeId: string | undefined = undefined
|
|
227
|
+
let value: listbox.ListboxValue = null
|
|
228
|
+
let selectedId: string | undefined = undefined
|
|
229
|
+
let selectedLabel = ''
|
|
230
|
+
let inputText = ''
|
|
231
|
+
let filterText = ''
|
|
232
|
+
let open = false
|
|
233
|
+
let surfaceVisible = false
|
|
234
|
+
let showReason: ShowReason = 'nav'
|
|
235
|
+
let pendingInputValue: string | null = null
|
|
236
|
+
let pendingChange: PendingChange = null
|
|
237
|
+
let selectInputAfterClose = false
|
|
238
|
+
let closeSequenceId = 0
|
|
239
|
+
|
|
240
|
+
let listId = `${handle.id}-combobox-list`
|
|
241
|
+
|
|
242
|
+
function getOptions() {
|
|
243
|
+
return listboxRef?.options ?? []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getSelectedOption() {
|
|
247
|
+
return getOptions().find((option) => option.value === value)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getMatchingOptions(text: string) {
|
|
251
|
+
if (text === '') {
|
|
252
|
+
return getOptions()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return getOptions().filter((option) =>
|
|
256
|
+
matchesSearchValue(option.textValue ?? option.label, text),
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getEnabledMatchingOptions(text: string) {
|
|
261
|
+
return getMatchingOptions(text).filter((option) => !option.disabled)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getExactInputMatch(text = inputText) {
|
|
265
|
+
return (
|
|
266
|
+
getOptions().find(
|
|
267
|
+
(option) =>
|
|
268
|
+
!option.disabled && matchesExactSearchValue(option.textValue ?? option.label, text),
|
|
269
|
+
) ?? null
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getOpenFilterText() {
|
|
274
|
+
if (inputText === '') {
|
|
275
|
+
return ''
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (getExactInputMatch(inputText)) {
|
|
279
|
+
return ''
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (getMatchingOptions(inputText).length === 0) {
|
|
283
|
+
return ''
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return inputText
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function resolveOpenOption(strategy: ComboboxOpenStrategy, nextFilterText: string) {
|
|
290
|
+
let enabledMatchingOptions = getEnabledMatchingOptions(nextFilterText)
|
|
291
|
+
if (enabledMatchingOptions.length === 0) {
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (strategy === 'selected-or-none') {
|
|
296
|
+
let selectedOption = getSelectedOption()
|
|
297
|
+
if (
|
|
298
|
+
selectedOption &&
|
|
299
|
+
!selectedOption.disabled &&
|
|
300
|
+
matchesSearchValue(selectedOption.textValue ?? selectedOption.label, nextFilterText)
|
|
301
|
+
) {
|
|
302
|
+
return selectedOption
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (strategy === 'selected') {
|
|
309
|
+
let exactInputMatch = getExactInputMatch(inputText)
|
|
310
|
+
if (
|
|
311
|
+
exactInputMatch &&
|
|
312
|
+
matchesSearchValue(exactInputMatch.textValue ?? exactInputMatch.label, nextFilterText)
|
|
313
|
+
) {
|
|
314
|
+
return exactInputMatch
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let selectedOption = getSelectedOption()
|
|
318
|
+
if (
|
|
319
|
+
selectedOption &&
|
|
320
|
+
!selectedOption.disabled &&
|
|
321
|
+
matchesSearchValue(selectedOption.textValue ?? selectedOption.label, nextFilterText)
|
|
322
|
+
) {
|
|
323
|
+
return selectedOption
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return strategy === 'last'
|
|
328
|
+
? enabledMatchingOptions[enabledMatchingOptions.length - 1]
|
|
329
|
+
: enabledMatchingOptions[0]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function setInputValue(nextValue: string) {
|
|
333
|
+
if (!inputRef || inputRef.value === nextValue) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
inputRef.value = nextValue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function clearInputSelection() {
|
|
341
|
+
if (!inputRef || inputRef.selectionStart === null || inputRef.selectionEnd === null) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let cursor = inputRef.value.length
|
|
346
|
+
if (inputRef.selectionStart === cursor && inputRef.selectionEnd === cursor) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
inputRef.setSelectionRange(cursor, cursor)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function syncPopoverMinWidth() {
|
|
354
|
+
if (!surfaceRef || !inputRef) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let width = inputRef.offsetWidth
|
|
359
|
+
if (width <= 0) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
surfaceRef.style.minWidth = `${width}px`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function dispatchChange(selection: {
|
|
367
|
+
label: string | null
|
|
368
|
+
optionId: string | null
|
|
369
|
+
value: string | null
|
|
370
|
+
}) {
|
|
371
|
+
let target = inputRef ?? surfaceRef
|
|
372
|
+
target?.dispatchEvent(
|
|
373
|
+
new ComboboxChangeEvent({
|
|
374
|
+
label: selection.label,
|
|
375
|
+
optionId: selection.optionId,
|
|
376
|
+
value: selection.value,
|
|
377
|
+
}),
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function clearCommittedSelection() {
|
|
382
|
+
let selectionChanged = value !== null || selectedId !== undefined
|
|
383
|
+
value = null
|
|
384
|
+
selectedId = undefined
|
|
385
|
+
selectedLabel = ''
|
|
386
|
+
return selectionChanged
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function clearInputAndSelection() {
|
|
390
|
+
closeSequenceId++
|
|
391
|
+
pendingInputValue = null
|
|
392
|
+
pendingChange = null
|
|
393
|
+
selectInputAfterClose = false
|
|
394
|
+
|
|
395
|
+
inputText = ''
|
|
396
|
+
activeValue = null
|
|
397
|
+
activeId = undefined
|
|
398
|
+
if (!open) {
|
|
399
|
+
filterText = ''
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let selectionChanged = clearCommittedSelection()
|
|
403
|
+
setInputValue('')
|
|
404
|
+
|
|
405
|
+
if (selectionChanged) {
|
|
406
|
+
dispatchChange({ label: null, optionId: null, value: null })
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function closePopover() {
|
|
411
|
+
if (!open) {
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let closeId = ++closeSequenceId
|
|
416
|
+
let shouldWaitForTransition = showReason === 'nav'
|
|
417
|
+
open = false
|
|
418
|
+
|
|
419
|
+
if (!shouldWaitForTransition) {
|
|
420
|
+
surfaceVisible = false
|
|
421
|
+
filterText = ''
|
|
422
|
+
activeValue = null
|
|
423
|
+
activeId = undefined
|
|
424
|
+
await handle.update()
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let signal = await handle.update()
|
|
429
|
+
if (signal.aborted || closeId !== closeSequenceId || open) {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (surfaceRef?.isConnected) {
|
|
434
|
+
await waitForCssTransition(surfaceRef, signal)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (signal.aborted || closeId !== closeSequenceId || open) {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
surfaceVisible = false
|
|
442
|
+
filterText = ''
|
|
443
|
+
activeValue = null
|
|
444
|
+
activeId = undefined
|
|
445
|
+
|
|
446
|
+
signal = await handle.update()
|
|
447
|
+
if (signal.aborted || closeId !== closeSequenceId || open) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (pendingInputValue !== null) {
|
|
452
|
+
let nextValue = pendingInputValue
|
|
453
|
+
await wait(INPUT_COMMIT_DELAY_MS)
|
|
454
|
+
if (
|
|
455
|
+
signal.aborted ||
|
|
456
|
+
closeId !== closeSequenceId ||
|
|
457
|
+
open ||
|
|
458
|
+
pendingInputValue !== nextValue
|
|
459
|
+
) {
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
setInputValue(nextValue)
|
|
464
|
+
pendingInputValue = null
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (selectInputAfterClose && inputRef?.isConnected && document.activeElement === inputRef) {
|
|
468
|
+
inputRef.select()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
selectInputAfterClose = false
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function openPopover(
|
|
475
|
+
strategy: ComboboxOpenStrategy = 'selected',
|
|
476
|
+
nextShowReason: ShowReason = 'nav',
|
|
477
|
+
) {
|
|
478
|
+
if (handle.props.disabled) {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (pendingInputValue !== null) {
|
|
483
|
+
setInputValue(pendingInputValue)
|
|
484
|
+
pendingInputValue = null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
closeSequenceId++
|
|
488
|
+
selectInputAfterClose = false
|
|
489
|
+
|
|
490
|
+
let nextFilterText = getOpenFilterText()
|
|
491
|
+
let matchingOptions = getMatchingOptions(nextFilterText)
|
|
492
|
+
if (matchingOptions.length === 0) {
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let activeOption = resolveOpenOption(strategy, nextFilterText)
|
|
497
|
+
|
|
498
|
+
filterText = nextFilterText
|
|
499
|
+
showReason = nextShowReason
|
|
500
|
+
open = true
|
|
501
|
+
surfaceVisible = true
|
|
502
|
+
activeValue = activeOption?.value ?? null
|
|
503
|
+
activeId = activeOption?.id
|
|
504
|
+
|
|
505
|
+
let signal = await handle.update()
|
|
506
|
+
if (signal.aborted) {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
listboxRef?.scrollActiveOptionIntoView()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function beginSelection(
|
|
514
|
+
nextValue: listbox.ListboxValue,
|
|
515
|
+
option: listbox.ListboxRegisteredOption | undefined,
|
|
516
|
+
) {
|
|
517
|
+
if (!option) {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let selectionChanged = value !== nextValue
|
|
522
|
+
value = nextValue
|
|
523
|
+
selectedId = option.id
|
|
524
|
+
selectedLabel = option.label
|
|
525
|
+
activeValue = nextValue
|
|
526
|
+
activeId = option.id
|
|
527
|
+
inputText = option.label
|
|
528
|
+
showReason = 'nav'
|
|
529
|
+
pendingInputValue = inputRef?.value === option.label ? null : option.label
|
|
530
|
+
pendingChange = selectionChanged
|
|
531
|
+
? {
|
|
532
|
+
label: option.label,
|
|
533
|
+
optionId: option.id,
|
|
534
|
+
value: option.value,
|
|
535
|
+
}
|
|
536
|
+
: null
|
|
537
|
+
selectInputAfterClose = true
|
|
538
|
+
void handle.update()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function finishSelection() {
|
|
542
|
+
let change = pendingChange
|
|
543
|
+
pendingChange = null
|
|
544
|
+
|
|
545
|
+
if (change) {
|
|
546
|
+
dispatchChange(change)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
await closePopover()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function setInputText(nextText: string) {
|
|
553
|
+
closeSequenceId++
|
|
554
|
+
pendingInputValue = null
|
|
555
|
+
pendingChange = null
|
|
556
|
+
selectInputAfterClose = false
|
|
557
|
+
|
|
558
|
+
let selectionChanged = clearCommittedSelection()
|
|
559
|
+
|
|
560
|
+
inputText = nextText
|
|
561
|
+
showReason = 'hint'
|
|
562
|
+
activeValue = null
|
|
563
|
+
activeId = undefined
|
|
564
|
+
|
|
565
|
+
let nextFilterText = nextText !== '' || !open ? nextText : filterText
|
|
566
|
+
let matchingOptions = getMatchingOptions(nextText)
|
|
567
|
+
filterText = nextFilterText
|
|
568
|
+
|
|
569
|
+
if (selectionChanged) {
|
|
570
|
+
dispatchChange({ label: null, optionId: null, value: null })
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (nextText === '' || matchingOptions.length === 0) {
|
|
574
|
+
if (open) {
|
|
575
|
+
open = false
|
|
576
|
+
surfaceVisible = false
|
|
577
|
+
} else {
|
|
578
|
+
filterText = ''
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
await handle.update()
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
open = true
|
|
586
|
+
surfaceVisible = true
|
|
587
|
+
await handle.update()
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function handleBlur() {
|
|
591
|
+
let exactMatch = getExactInputMatch(inputText)
|
|
592
|
+
if (!exactMatch) {
|
|
593
|
+
clearInputAndSelection()
|
|
594
|
+
} else {
|
|
595
|
+
let selectionChanged = value !== exactMatch.value
|
|
596
|
+
value = exactMatch.value
|
|
597
|
+
selectedId = exactMatch.id
|
|
598
|
+
selectedLabel = exactMatch.label
|
|
599
|
+
activeValue = exactMatch.value
|
|
600
|
+
activeId = exactMatch.id
|
|
601
|
+
|
|
602
|
+
if (selectionChanged) {
|
|
603
|
+
dispatchChange({
|
|
604
|
+
label: exactMatch.label,
|
|
605
|
+
optionId: exactMatch.id,
|
|
606
|
+
value: exactMatch.value,
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
void closePopover()
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function handleEscape() {
|
|
615
|
+
if (!getExactInputMatch(inputText)) {
|
|
616
|
+
clearInputAndSelection()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (open) {
|
|
620
|
+
void closePopover()
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function openFromArrow(direction: 'first' | 'last') {
|
|
625
|
+
let strategy: ComboboxOpenStrategy = getExactInputMatch(inputText) ? 'selected' : direction
|
|
626
|
+
clearInputSelection()
|
|
627
|
+
return openPopover(strategy)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function openFromInputActivation() {
|
|
631
|
+
clearInputSelection()
|
|
632
|
+
return openPopover('selected-or-none')
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function navigateNext() {
|
|
636
|
+
clearInputSelection()
|
|
637
|
+
listboxRef?.navigateNext()
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function navigatePrevious() {
|
|
641
|
+
clearInputSelection()
|
|
642
|
+
listboxRef?.navigatePrevious()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function selectActive() {
|
|
646
|
+
return listboxRef?.selectActive() ?? Promise.resolve()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function registerInput(node: HTMLInputElement) {
|
|
650
|
+
inputRef = node
|
|
651
|
+
setInputValue(inputText)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function unregisterInput(node: HTMLInputElement) {
|
|
655
|
+
if (inputRef === node) {
|
|
656
|
+
inputRef = undefined
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function registerSurface(node: HTMLElement) {
|
|
661
|
+
surfaceRef = node
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function unregisterSurface(node: HTMLElement) {
|
|
665
|
+
if (surfaceRef === node) {
|
|
666
|
+
surfaceRef = undefined
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
let publicHandle: ComboboxHandle = {
|
|
671
|
+
get activeOptionId() {
|
|
672
|
+
return activeId ?? null
|
|
673
|
+
},
|
|
674
|
+
get id() {
|
|
675
|
+
return listId
|
|
676
|
+
},
|
|
677
|
+
get inputText() {
|
|
678
|
+
return inputText
|
|
679
|
+
},
|
|
680
|
+
get isOpen() {
|
|
681
|
+
return open
|
|
682
|
+
},
|
|
683
|
+
get label() {
|
|
684
|
+
return selectedLabel || null
|
|
685
|
+
},
|
|
686
|
+
get value() {
|
|
687
|
+
return value
|
|
688
|
+
},
|
|
689
|
+
close() {
|
|
690
|
+
void closePopover()
|
|
691
|
+
},
|
|
692
|
+
open(strategy = 'selected') {
|
|
693
|
+
return openPopover(strategy)
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
handle.context.set({
|
|
698
|
+
get activeId() {
|
|
699
|
+
return activeId
|
|
700
|
+
},
|
|
701
|
+
get disabled() {
|
|
702
|
+
return handle.props.disabled === true
|
|
703
|
+
},
|
|
704
|
+
get filterText() {
|
|
705
|
+
return filterText
|
|
706
|
+
},
|
|
707
|
+
get inputText() {
|
|
708
|
+
return inputText
|
|
709
|
+
},
|
|
710
|
+
get isOpen() {
|
|
711
|
+
return open
|
|
712
|
+
},
|
|
713
|
+
get listId() {
|
|
714
|
+
return listId
|
|
715
|
+
},
|
|
716
|
+
get name() {
|
|
717
|
+
return handle.props.name
|
|
718
|
+
},
|
|
719
|
+
get showReason() {
|
|
720
|
+
return showReason
|
|
721
|
+
},
|
|
722
|
+
get surfaceVisible() {
|
|
723
|
+
return surfaceVisible
|
|
724
|
+
},
|
|
725
|
+
get value() {
|
|
726
|
+
return value
|
|
727
|
+
},
|
|
728
|
+
clearInputSelection,
|
|
729
|
+
close() {
|
|
730
|
+
void closePopover()
|
|
731
|
+
},
|
|
732
|
+
handleBlur,
|
|
733
|
+
handleEscape,
|
|
734
|
+
navigateNext,
|
|
735
|
+
navigatePrevious,
|
|
736
|
+
open(strategy = 'selected') {
|
|
737
|
+
return openPopover(strategy)
|
|
738
|
+
},
|
|
739
|
+
openFromArrow,
|
|
740
|
+
openFromInputActivation,
|
|
741
|
+
registerInput,
|
|
742
|
+
registerSurface,
|
|
743
|
+
setInputText,
|
|
744
|
+
selectActive,
|
|
745
|
+
syncPopoverMinWidth,
|
|
746
|
+
unregisterInput,
|
|
747
|
+
unregisterSurface,
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
return () => {
|
|
751
|
+
if (!hasInitialized) {
|
|
752
|
+
value = handle.props.defaultValue ?? null
|
|
753
|
+
inputText = handle.props.defaultValue ?? ''
|
|
754
|
+
hasInitialized = true
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
handle.queueTask(() => {
|
|
758
|
+
let selectedOption = getSelectedOption()
|
|
759
|
+
if (selectedOption) {
|
|
760
|
+
selectedId = selectedOption.id
|
|
761
|
+
selectedLabel = selectedOption.label
|
|
762
|
+
} else if (value === null) {
|
|
763
|
+
selectedId = undefined
|
|
764
|
+
selectedLabel = ''
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
if (!hasProvidedRef) {
|
|
769
|
+
handle.queueTask(() => {
|
|
770
|
+
handle.props.ref?.(publicHandle)
|
|
771
|
+
})
|
|
772
|
+
hasProvidedRef = true
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<popover.Context>
|
|
777
|
+
<listbox.Context
|
|
778
|
+
activeValue={activeValue}
|
|
779
|
+
flashSelection
|
|
780
|
+
onHighlight={(nextActiveValue, option) => {
|
|
781
|
+
if (!open) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
activeValue = nextActiveValue
|
|
786
|
+
activeId = option?.id
|
|
787
|
+
if (option) {
|
|
788
|
+
showReason = 'nav'
|
|
789
|
+
}
|
|
790
|
+
void handle.update()
|
|
791
|
+
}}
|
|
792
|
+
onSelect={beginSelection}
|
|
793
|
+
onSelectSettled={finishSelection}
|
|
794
|
+
ref={(ref) => {
|
|
795
|
+
listboxRef = ref
|
|
796
|
+
}}
|
|
797
|
+
selectionFlashAttribute="data-combobox-flash"
|
|
798
|
+
value={value}
|
|
799
|
+
>
|
|
800
|
+
{handle.props.children}
|
|
801
|
+
</listbox.Context>
|
|
802
|
+
</popover.Context>
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function getComboboxContext(handle: Handle | MixinHandle) {
|
|
808
|
+
return handle.context.get(ComboboxProvider)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const inputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
|
|
812
|
+
let context = getComboboxContext(handle)
|
|
813
|
+
|
|
814
|
+
return (props) => [
|
|
815
|
+
attrs({
|
|
816
|
+
'aria-activedescendant': context.activeId,
|
|
817
|
+
'aria-autocomplete': 'list',
|
|
818
|
+
'aria-controls': context.listId,
|
|
819
|
+
'aria-expanded': context.isOpen ? 'true' : 'false',
|
|
820
|
+
'aria-haspopup': 'listbox',
|
|
821
|
+
autocomplete: props.autocomplete ?? 'off',
|
|
822
|
+
'data-surface-visible': context.surfaceVisible ? 'true' : undefined,
|
|
823
|
+
disabled: context.disabled ? true : props.disabled,
|
|
824
|
+
role: 'combobox',
|
|
825
|
+
type: props.type ?? 'text',
|
|
826
|
+
}),
|
|
827
|
+
ref((node: HTMLInputElement, signal) => {
|
|
828
|
+
context.registerInput(node)
|
|
829
|
+
signal.addEventListener('abort', () => {
|
|
830
|
+
context.unregisterInput(node)
|
|
831
|
+
})
|
|
832
|
+
}),
|
|
833
|
+
popover.anchor({ placement: 'bottom-start' }),
|
|
834
|
+
on('click', () => {
|
|
835
|
+
if (context.isOpen) {
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
void context.openFromInputActivation()
|
|
840
|
+
}),
|
|
841
|
+
on('input', (event) => {
|
|
842
|
+
void context.setInputText(event.currentTarget.value)
|
|
843
|
+
}),
|
|
844
|
+
on('keydown', (event) => {
|
|
845
|
+
switch (event.key) {
|
|
846
|
+
case 'ArrowDown':
|
|
847
|
+
event.preventDefault()
|
|
848
|
+
if (context.isOpen) {
|
|
849
|
+
context.navigateNext()
|
|
850
|
+
} else {
|
|
851
|
+
void context.openFromArrow('first')
|
|
852
|
+
}
|
|
853
|
+
return
|
|
854
|
+
case 'ArrowUp':
|
|
855
|
+
event.preventDefault()
|
|
856
|
+
if (context.isOpen) {
|
|
857
|
+
context.navigatePrevious()
|
|
858
|
+
} else {
|
|
859
|
+
void context.openFromArrow('last')
|
|
860
|
+
}
|
|
861
|
+
return
|
|
862
|
+
case 'Enter':
|
|
863
|
+
if (!context.isOpen) {
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
event.preventDefault()
|
|
868
|
+
void context.selectActive()
|
|
869
|
+
return
|
|
870
|
+
case 'Escape':
|
|
871
|
+
if (!context.isOpen && context.inputText === '') {
|
|
872
|
+
return
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
event.preventDefault()
|
|
876
|
+
context.handleEscape()
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
}),
|
|
880
|
+
on('blur', () => {
|
|
881
|
+
context.handleBlur()
|
|
882
|
+
}),
|
|
883
|
+
]
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
|
|
887
|
+
let context = getComboboxContext(handle)
|
|
888
|
+
|
|
889
|
+
return () => [
|
|
890
|
+
attrs({ 'data-show-reason': context.showReason }),
|
|
891
|
+
ref((node: HTMLElement, signal) => {
|
|
892
|
+
context.registerSurface(node)
|
|
893
|
+
signal.addEventListener('abort', () => {
|
|
894
|
+
context.unregisterSurface(node)
|
|
895
|
+
})
|
|
896
|
+
}),
|
|
897
|
+
popover.surface({
|
|
898
|
+
open: context.isOpen,
|
|
899
|
+
onHide() {
|
|
900
|
+
context.close()
|
|
901
|
+
},
|
|
902
|
+
closeOnAnchorClick: false,
|
|
903
|
+
}),
|
|
904
|
+
on('beforetoggle', (event) => {
|
|
905
|
+
if (event.newState === 'open') {
|
|
906
|
+
context.syncPopoverMinWidth()
|
|
907
|
+
}
|
|
908
|
+
}),
|
|
909
|
+
]
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
|
|
913
|
+
let context = getComboboxContext(handle)
|
|
914
|
+
return () => [attrs({ id: context.listId }), listbox.list()]
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
|
|
918
|
+
let context = getComboboxContext(handle)
|
|
919
|
+
return () =>
|
|
920
|
+
attrs({
|
|
921
|
+
disabled: context.disabled ? true : undefined,
|
|
922
|
+
name: context.name,
|
|
923
|
+
type: 'hidden',
|
|
924
|
+
value: context.value ?? '',
|
|
925
|
+
})
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
const optionMixin = createMixin<HTMLElement, [options: ComboboxOptionOptions], ElementProps>(
|
|
929
|
+
(handle) => {
|
|
930
|
+
let context = getComboboxContext(handle)
|
|
931
|
+
|
|
932
|
+
return (options) => {
|
|
933
|
+
let hidden = !matchesSearchValue(getOptionSearchValue(options), context.filterText)
|
|
934
|
+
|
|
935
|
+
return [
|
|
936
|
+
attrs({ hidden: hidden ? true : undefined }),
|
|
937
|
+
on('pointerdown', (event) => {
|
|
938
|
+
if (event.button === 0) {
|
|
939
|
+
event.preventDefault()
|
|
940
|
+
}
|
|
941
|
+
}),
|
|
942
|
+
listbox.option({
|
|
943
|
+
disabled: options.disabled,
|
|
944
|
+
label: options.label,
|
|
945
|
+
textValue: options.searchValue,
|
|
946
|
+
value: options.value,
|
|
947
|
+
}),
|
|
948
|
+
]
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
export const Context = ComboboxProvider
|
|
954
|
+
export const hiddenInput = hiddenInputMixin
|
|
955
|
+
export const input = inputMixin
|
|
956
|
+
export const list = listMixin
|
|
957
|
+
export const option = optionMixin
|
|
958
|
+
export { popoverMixin as popover }
|
|
959
|
+
|
|
960
|
+
const combobox = {
|
|
961
|
+
Context,
|
|
962
|
+
hiddenInput,
|
|
963
|
+
input,
|
|
964
|
+
list,
|
|
965
|
+
option,
|
|
966
|
+
popover: popoverMixin,
|
|
967
|
+
} as const
|
|
968
|
+
|
|
969
|
+
export function onComboboxChange(handler: ComboboxChangeHandler, captureBoolean?: boolean) {
|
|
970
|
+
return on<HTMLElement, typeof COMBOBOX_CHANGE_EVENT>(
|
|
971
|
+
COMBOBOX_CHANGE_EVENT,
|
|
972
|
+
handler,
|
|
973
|
+
captureBoolean,
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function Combobox(handle: Handle<ComboboxProps>) {
|
|
978
|
+
return () => {
|
|
979
|
+
let { children, defaultValue, disabled, inputId, name, placeholder, ...divProps } = handle.props
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<combobox.Context defaultValue={defaultValue} disabled={disabled} name={name}>
|
|
983
|
+
<div {...divProps}>
|
|
984
|
+
<input
|
|
985
|
+
defaultValue={defaultValue ?? undefined}
|
|
986
|
+
id={inputId}
|
|
987
|
+
mix={[inputStyle, combobox.input()]}
|
|
988
|
+
placeholder={placeholder}
|
|
989
|
+
/>
|
|
990
|
+
<div mix={[popover.surfaceStyle, popoverStyle, combobox.popover()]}>
|
|
991
|
+
<div mix={[popover.contentStyle, listbox.listStyle, combobox.list()]}>{children}</div>
|
|
992
|
+
</div>
|
|
993
|
+
{name && <input mix={combobox.hiddenInput()} />}
|
|
994
|
+
</div>
|
|
995
|
+
</combobox.Context>
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function ComboboxOption(handle: Handle<ComboboxOptionProps>) {
|
|
1001
|
+
return () => {
|
|
1002
|
+
let { children, disabled, label, mix, searchValue, value, ...divProps } = handle.props
|
|
1003
|
+
|
|
1004
|
+
return (
|
|
1005
|
+
<div
|
|
1006
|
+
{...divProps}
|
|
1007
|
+
mix={[listbox.optionStyle, combobox.option({ disabled, label, searchValue, value }), mix]}
|
|
1008
|
+
>
|
|
1009
|
+
<Glyph mix={listbox.glyphStyle} name="check" />
|
|
1010
|
+
<span mix={listbox.labelStyle}>{children ?? label}</span>
|
|
1011
|
+
</div>
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
}
|