@remix-run/ui 0.1.1 → 0.2.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 +33 -10
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/animation/spring.d.ts +6 -35
- package/dist/animation/spring.js +9 -8
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +7 -7
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +86 -4
- package/dist/components/button/button.js +63 -3
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +11 -11
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +4 -9
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +15 -24
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/popover/popover.d.ts +5 -5
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +11 -11
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +6 -6
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interactions/keydown/keydown.d.ts +1 -1
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
- package/dist/runtime/component.d.ts +18 -12
- package/dist/runtime/component.js +93 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +14 -0
- package/dist/runtime/core/attributes.js +103 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +170 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +20 -9
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +41 -47
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/jsx.js +2 -28
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/link-mixin.d.ts +5 -1
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +2 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
- package/dist/runtime/mixins/ref-mixin.js.map +1 -1
- package/dist/runtime/navigation.js +9 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +1 -1
- package/dist/runtime/reconcile.js +452 -197
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.d.ts +10 -10
- package/dist/runtime/scheduler.js +1 -1
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/svg-attributes.js +1 -0
- package/dist/runtime/svg-attributes.js.map +1 -1
- package/dist/runtime/to-vnode.js +7 -11
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +11 -5
- package/dist/runtime/vnode.js +6 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.d.ts +2 -0
- package/dist/server/stream.js +154 -114
- package/dist/server/stream.js.map +1 -1
- package/dist/style/css-mixin.d.ts +2 -1
- package/dist/style/css-mixin.js.map +1 -1
- package/dist/style/index.d.ts +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +13 -11
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/contract.d.ts +24 -222
- package/dist/theme/contract.js +2 -2
- package/dist/theme/contract.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -3
- package/dist/theme/glyph-contract.js.map +1 -1
- package/dist/theme/presets/rmx-01/index.d.ts +4 -2
- package/dist/theme/presets/rmx-01/index.js.map +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/dist/utils/scroll-lock.d.ts +3 -3
- package/dist/utils/scroll-lock.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +316 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/animation/spring.ts +21 -9
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +14 -14
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/anchor/anchor.ts +1 -1
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +118 -20
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/README.md +1 -1
- package/src/components/combobox/combobox.tsx +37 -21
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/glyph/glyph.tsx +2 -2
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +106 -110
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +61 -55
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/popover/popover.ts +90 -79
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +53 -36
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +2 -1
- package/src/runtime/component.ts +122 -88
- package/src/runtime/core/attributes.ts +118 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +187 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +20 -9
- package/src/runtime/frame.ts +71 -55
- package/src/runtime/jsx.ts +3 -30
- package/src/runtime/mixins/link-mixin.ts +88 -86
- package/src/runtime/mixins/mixin.ts +18 -5
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/mixins/ref-mixin.ts +4 -5
- package/src/runtime/navigation.ts +10 -1
- package/src/runtime/reconcile.ts +563 -223
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +20 -20
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/svg-attributes.ts +1 -0
- package/src/runtime/to-vnode.ts +7 -10
- package/src/runtime/vnode.ts +16 -9
- package/src/server/README.md +90 -0
- package/src/server/stream.ts +176 -124
- package/src/style/css-mixin.ts +6 -1
- package/src/style/index.ts +1 -2
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +88 -11
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/contract.ts +37 -5
- package/src/theme/glyph-contract.ts +3 -1
- package/src/theme/presets/rmx-01/index.ts +4 -2
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/utils/scroll-lock.ts +7 -3
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
css,
|
|
5
5
|
on,
|
|
6
6
|
type CSSMixinDescriptor,
|
|
7
|
+
type ElementProps,
|
|
7
8
|
type Handle,
|
|
9
|
+
type MixinFactory,
|
|
8
10
|
type RemixNode,
|
|
9
11
|
} from '@remix-run/ui'
|
|
10
12
|
import { theme } from '../../theme/theme.ts'
|
|
@@ -17,17 +19,8 @@ import { flashAttribute } from '../../utils/flash-attribute.ts'
|
|
|
17
19
|
|
|
18
20
|
export type ListboxValue = string | null
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Previous = 1,
|
|
23
|
-
First = 2,
|
|
24
|
-
Last = 3,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
enum State {
|
|
28
|
-
Idle = 'idle',
|
|
29
|
-
Selecting = 'selecting',
|
|
30
|
-
}
|
|
22
|
+
type NavigationStrategy = 'next' | 'previous' | 'first' | 'last'
|
|
23
|
+
type State = 'idle' | 'selecting'
|
|
31
24
|
|
|
32
25
|
interface Values {
|
|
33
26
|
value: ListboxValue
|
|
@@ -158,9 +151,9 @@ export const optionStyle = itemCss
|
|
|
158
151
|
export const glyphStyle = itemGlyphCss
|
|
159
152
|
export const labelStyle = itemLabelCss
|
|
160
153
|
|
|
161
|
-
function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
154
|
+
function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>): () => RemixNode {
|
|
162
155
|
let options: RegisteredOption[] = []
|
|
163
|
-
let state =
|
|
156
|
+
let state: State = 'idle'
|
|
164
157
|
|
|
165
158
|
function getOption(value: ListboxValue) {
|
|
166
159
|
return options.find((option) => option.value === value)
|
|
@@ -221,16 +214,16 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
221
214
|
return findSearchMatch(text, fromValue)
|
|
222
215
|
},
|
|
223
216
|
navigateFirst() {
|
|
224
|
-
context.navigate(
|
|
217
|
+
context.navigate('first')
|
|
225
218
|
},
|
|
226
219
|
navigateLast() {
|
|
227
|
-
context.navigate(
|
|
220
|
+
context.navigate('last')
|
|
228
221
|
},
|
|
229
222
|
navigateNext() {
|
|
230
|
-
context.navigate(
|
|
223
|
+
context.navigate('next')
|
|
231
224
|
},
|
|
232
225
|
navigatePrevious() {
|
|
233
|
-
context.navigate(
|
|
226
|
+
context.navigate('previous')
|
|
234
227
|
},
|
|
235
228
|
scrollActiveOptionIntoView() {
|
|
236
229
|
context.scrollActiveOptionIntoView()
|
|
@@ -261,13 +254,13 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
261
254
|
},
|
|
262
255
|
|
|
263
256
|
async select(value) {
|
|
264
|
-
if (state ===
|
|
257
|
+
if (state === 'selecting') {
|
|
265
258
|
return
|
|
266
259
|
}
|
|
267
|
-
state =
|
|
260
|
+
state = 'selecting'
|
|
268
261
|
let option = getOption(value)
|
|
269
262
|
if (!isInteractableOption(option)) {
|
|
270
|
-
state =
|
|
263
|
+
state = 'idle'
|
|
271
264
|
return
|
|
272
265
|
}
|
|
273
266
|
handle.props.onSelect(value, option)
|
|
@@ -279,17 +272,17 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
279
272
|
)
|
|
280
273
|
}
|
|
281
274
|
await handle.props.onSelectSettled?.(value, option)
|
|
282
|
-
state =
|
|
275
|
+
state = 'idle'
|
|
283
276
|
},
|
|
284
277
|
|
|
285
278
|
highlight(value) {
|
|
286
|
-
if (state ===
|
|
279
|
+
if (state === 'selecting') return
|
|
287
280
|
let option = getOption(value)
|
|
288
281
|
handle.props.onHighlight(value, option)
|
|
289
282
|
},
|
|
290
283
|
|
|
291
284
|
highlightSearchMatch(text) {
|
|
292
|
-
if (state ===
|
|
285
|
+
if (state === 'selecting') return
|
|
293
286
|
|
|
294
287
|
let option = findSearchMatch(text, handle.props.activeValue)
|
|
295
288
|
|
|
@@ -300,7 +293,7 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
300
293
|
},
|
|
301
294
|
|
|
302
295
|
navigate(strategy: NavigationStrategy) {
|
|
303
|
-
if (state ===
|
|
296
|
+
if (state === 'selecting') return
|
|
304
297
|
|
|
305
298
|
let option: RegisteredOption | undefined
|
|
306
299
|
let interactableOptions = getInteractableOptions()
|
|
@@ -309,19 +302,19 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
309
302
|
)
|
|
310
303
|
|
|
311
304
|
switch (strategy) {
|
|
312
|
-
case
|
|
305
|
+
case 'next':
|
|
313
306
|
option = interactableOptions[activeIndex + 1] ?? interactableOptions[0]
|
|
314
307
|
break
|
|
315
|
-
case
|
|
308
|
+
case 'previous':
|
|
316
309
|
option =
|
|
317
310
|
activeIndex === -1
|
|
318
311
|
? interactableOptions[interactableOptions.length - 1]
|
|
319
312
|
: interactableOptions[activeIndex - 1]
|
|
320
313
|
break
|
|
321
|
-
case
|
|
314
|
+
case 'first':
|
|
322
315
|
option = interactableOptions[0]
|
|
323
316
|
break
|
|
324
|
-
case
|
|
317
|
+
case 'last':
|
|
325
318
|
option = interactableOptions[interactableOptions.length - 1]
|
|
326
319
|
break
|
|
327
320
|
}
|
|
@@ -345,49 +338,51 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
|
|
|
345
338
|
}
|
|
346
339
|
}
|
|
347
340
|
|
|
348
|
-
const listMixin = createMixin<HTMLElement>(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
341
|
+
const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement>(
|
|
342
|
+
(handle) => (props) => {
|
|
343
|
+
let context = handle.context.get(ListboxProvider)
|
|
344
|
+
return [
|
|
345
|
+
attrs({
|
|
346
|
+
tabIndex: props.tabIndex ?? -1,
|
|
347
|
+
role: props.role ?? 'listbox',
|
|
348
|
+
}),
|
|
349
|
+
on('focus', () => {
|
|
350
|
+
context.scrollActiveOptionIntoView()
|
|
351
|
+
}),
|
|
352
|
+
on('keydown', (event) => {
|
|
353
|
+
switch (event.key) {
|
|
354
|
+
case 'ArrowDown':
|
|
355
|
+
event.preventDefault()
|
|
356
|
+
context.navigate('next')
|
|
357
|
+
break
|
|
358
|
+
case 'ArrowUp':
|
|
359
|
+
event.preventDefault()
|
|
360
|
+
context.navigate('previous')
|
|
361
|
+
break
|
|
362
|
+
case 'Tab':
|
|
363
|
+
event.preventDefault()
|
|
364
|
+
context.navigate('first')
|
|
365
|
+
break
|
|
366
|
+
case 'Enter':
|
|
367
|
+
case ' ':
|
|
368
|
+
event.preventDefault()
|
|
369
|
+
void context.select(context.activeValue)
|
|
370
|
+
break
|
|
371
|
+
case 'Home':
|
|
372
|
+
event.preventDefault()
|
|
373
|
+
context.navigate('first')
|
|
374
|
+
break
|
|
375
|
+
case 'End':
|
|
376
|
+
event.preventDefault()
|
|
377
|
+
context.navigate('last')
|
|
378
|
+
}
|
|
379
|
+
}),
|
|
380
|
+
hiddenTypeahead((text) => {
|
|
381
|
+
context.highlightSearchMatch(text)
|
|
382
|
+
}),
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
)
|
|
391
386
|
|
|
392
387
|
export interface ListboxOption {
|
|
393
388
|
id: string
|
|
@@ -397,50 +392,51 @@ export interface ListboxOption {
|
|
|
397
392
|
textValue?: SearchValue
|
|
398
393
|
}
|
|
399
394
|
|
|
400
|
-
const optionMixin
|
|
401
|
-
|
|
395
|
+
const optionMixin: MixinFactory<HTMLElement, [option: Omit<ListboxOption, 'id'>], ElementProps> =
|
|
396
|
+
createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
|
|
397
|
+
let optionRef: HTMLElement | undefined
|
|
402
398
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
return (option) => {
|
|
408
|
-
let context = handle.context.get(ListboxProvider)
|
|
409
|
-
context.registerOption({
|
|
410
|
-
...option,
|
|
411
|
-
id: handle.id,
|
|
412
|
-
get hidden() {
|
|
413
|
-
return optionRef?.hidden === true
|
|
414
|
-
},
|
|
415
|
-
get node() {
|
|
416
|
-
return optionRef as HTMLElement
|
|
417
|
-
},
|
|
399
|
+
handle.queueTask((node) => {
|
|
400
|
+
optionRef = node
|
|
418
401
|
})
|
|
419
402
|
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
|
|
403
|
+
return (option) => {
|
|
404
|
+
let context = handle.context.get(ListboxProvider)
|
|
405
|
+
context.registerOption({
|
|
406
|
+
...option,
|
|
423
407
|
id: handle.id,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
408
|
+
get hidden() {
|
|
409
|
+
return optionRef?.hidden === true
|
|
410
|
+
},
|
|
411
|
+
get node() {
|
|
412
|
+
return optionRef as HTMLElement
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
return [
|
|
417
|
+
attrs({
|
|
418
|
+
role: 'option',
|
|
419
|
+
id: handle.id,
|
|
420
|
+
'aria-selected': context.value === option.value ? 'true' : 'false',
|
|
421
|
+
'aria-disabled': option.disabled ? 'true' : 'false',
|
|
422
|
+
'data-highlighted': context.activeValue === option.value ? 'true' : 'false',
|
|
435
423
|
}),
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
})
|
|
424
|
+
!option.disabled && [
|
|
425
|
+
on('click', () => {
|
|
426
|
+
context.select(option.value)
|
|
427
|
+
}),
|
|
428
|
+
on('mousemove', () => {
|
|
429
|
+
if (context.activeValue === option.value) return
|
|
430
|
+
context.highlight(option.value)
|
|
431
|
+
}),
|
|
432
|
+
on('mouseleave', () => {
|
|
433
|
+
if (context.activeValue !== option.value) return
|
|
434
|
+
context.highlight(null)
|
|
435
|
+
}),
|
|
436
|
+
],
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
})
|
|
444
440
|
|
|
445
441
|
export const Context = ListboxProvider
|
|
446
442
|
export const list = listMixin
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# menu
|
|
2
|
+
|
|
3
|
+
`Menu` renders a button-triggered menu with keyboard navigation, checked items, selection events, and nested submenus. Use it for action menus and command groups.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import type { Handle } from 'remix/ui'
|
|
9
|
+
import { Menu, MenuItem, Submenu, onMenuSelect } from 'remix/ui/menu'
|
|
10
|
+
import { separatorStyle } from 'remix/ui/separator'
|
|
11
|
+
|
|
12
|
+
export function ViewMenu(handle: Handle) {
|
|
13
|
+
let wordWrap = false
|
|
14
|
+
let density = 'comfortable'
|
|
15
|
+
|
|
16
|
+
return () => (
|
|
17
|
+
<Menu
|
|
18
|
+
label="View"
|
|
19
|
+
mix={onMenuSelect((event) => {
|
|
20
|
+
if (event.item.name === 'wordWrap') {
|
|
21
|
+
wordWrap = event.item.checked ?? false
|
|
22
|
+
} else if (event.item.name === 'density' && event.item.value) {
|
|
23
|
+
density = event.item.value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void handle.update()
|
|
27
|
+
})}
|
|
28
|
+
>
|
|
29
|
+
<MenuItem checked={wordWrap} name="wordWrap" type="checkbox">
|
|
30
|
+
Word wrap
|
|
31
|
+
</MenuItem>
|
|
32
|
+
<MenuItem disabled name="minimap">
|
|
33
|
+
Minimap
|
|
34
|
+
</MenuItem>
|
|
35
|
+
<hr mix={separatorStyle} />
|
|
36
|
+
<MenuItem checked={density === 'compact'} name="density" type="radio" value="compact">
|
|
37
|
+
Compact
|
|
38
|
+
</MenuItem>
|
|
39
|
+
<MenuItem checked={density === 'comfortable'} name="density" type="radio" value="comfortable">
|
|
40
|
+
Comfortable
|
|
41
|
+
</MenuItem>
|
|
42
|
+
<Submenu label="Zoom">
|
|
43
|
+
<MenuItem name="zoomIn" value="zoom-in">
|
|
44
|
+
Zoom in
|
|
45
|
+
</MenuItem>
|
|
46
|
+
<MenuItem name="zoomOut" value="zoom-out">
|
|
47
|
+
Zoom out
|
|
48
|
+
</MenuItem>
|
|
49
|
+
</Submenu>
|
|
50
|
+
</Menu>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<MenuItem label="Open command palette" name="commandPalette" searchValue="palette">
|
|
59
|
+
Command palette
|
|
60
|
+
</MenuItem>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `menuLabel` when the menu surface needs a different accessible label from the visible trigger.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Menu label="..." menuLabel="Project actions">
|
|
67
|
+
<MenuItem name="rename">Rename project</MenuItem>
|
|
68
|
+
</Menu>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## `menu.*`
|
|
72
|
+
|
|
73
|
+
- `Menu`: composed trigger, popover, and list component for the common menu case.
|
|
74
|
+
- `MenuItem`: menu item wrapper. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
|
|
75
|
+
- `Submenu`: nested menu wrapper with its own trigger and child menu surface.
|
|
76
|
+
- `MenuList`: lower-level list wrapper for custom composition.
|
|
77
|
+
- `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
|
|
78
|
+
- `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
|
|
79
|
+
- `MenuSelectItem`: selected item shape with `checked`, `id`, `label`, `name`, `type`, and `value`.
|
|
80
|
+
- `menu.Context`, `menu.trigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
|
|
81
|
+
- `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemGlyphStyle`, and `triggerGlyphStyle`: flat style mixins used by the wrappers.
|
|
82
|
+
- `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
|
|
83
|
+
|
|
84
|
+
## Behavior Notes
|
|
85
|
+
|
|
86
|
+
- Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
|
|
87
|
+
- `ArrowDown` opens from the trigger at the first enabled item. `ArrowUp` opens at the last enabled item. Enter and Space open the menu with focus on the list.
|
|
88
|
+
- Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
|
|
89
|
+
- `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
|
|
90
|
+
- Printable keys use typeahead. Typeahead matches `searchValue` when provided and otherwise uses the item label.
|
|
91
|
+
- Submenus open with `ArrowRight`, close with `ArrowLeft`, and are anchored to the submenu trigger with `right-start` placement.
|
|
92
|
+
- Pointer movement highlights enabled items. Submenus open after a short focus/pointer delay and use hover aim so they stay open while moving toward the child surface.
|
|
93
|
+
- Selecting a regular item flashes that item. Selecting a checkbox or radio item flashes the committed checked state.
|
|
94
|
+
- Selection dispatches one bubbled `MenuSelectEvent`, closes the full menu tree, and restores focus to the root trigger.
|
|
95
|
+
- The composed `Menu` re-dispatches selection from its trigger so handlers on the `Menu` button and shared ancestors see the event once.
|
|
96
|
+
- `menuLabel`, `Submenu.menuLabel`, and `Submenu.listProps` let composed menus label and customize menu surfaces separately from trigger content.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { on } from '@remix-run/ui'
|
|
2
|
+
import { Menu, MenuItem, onMenuSelect } from '@remix-run/ui/menu'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @name Menu Event Bubbling
|
|
6
|
+
* @description Item-level handlers fire before the root handler, letting individual items intercept events while others bubble up.
|
|
7
|
+
*/
|
|
8
|
+
export default function Example() {
|
|
9
|
+
return () => (
|
|
10
|
+
<Menu
|
|
11
|
+
label="Project"
|
|
12
|
+
menuLabel="Project actions"
|
|
13
|
+
mix={onMenuSelect((event) => {
|
|
14
|
+
console.log('Menu root handler:', event.item)
|
|
15
|
+
})}
|
|
16
|
+
>
|
|
17
|
+
<MenuItem name="open" value="open-project">
|
|
18
|
+
Open project
|
|
19
|
+
</MenuItem>
|
|
20
|
+
<MenuItem
|
|
21
|
+
name="rename"
|
|
22
|
+
value="rename-project"
|
|
23
|
+
mix={onMenuSelect((event) => {
|
|
24
|
+
console.log('Menu item handler:', event.item)
|
|
25
|
+
})}
|
|
26
|
+
>
|
|
27
|
+
Rename project
|
|
28
|
+
</MenuItem>
|
|
29
|
+
<MenuItem name="duplicate" value="duplicate-project">
|
|
30
|
+
Duplicate project
|
|
31
|
+
</MenuItem>
|
|
32
|
+
</Menu>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Handle } from '@remix-run/ui'
|
|
2
|
+
import { Menu, MenuItem, onMenuSelect, Submenu } from '@remix-run/ui/menu'
|
|
3
|
+
import { separatorStyle } from '@remix-run/ui/separator'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @name Menu Overview
|
|
7
|
+
* @description A hierarchical menu with checkboxes, radio groups, submenus, and separators.
|
|
8
|
+
*/
|
|
9
|
+
export default function Example(handle: Handle) {
|
|
10
|
+
type Density = 'comfortable' | 'compact'
|
|
11
|
+
let wordWrap = true
|
|
12
|
+
let minimap = false
|
|
13
|
+
let showGutter = true
|
|
14
|
+
let density: Density = 'comfortable'
|
|
15
|
+
|
|
16
|
+
return () => (
|
|
17
|
+
<Menu
|
|
18
|
+
label="View"
|
|
19
|
+
menuLabel="View options"
|
|
20
|
+
mix={onMenuSelect((event) => {
|
|
21
|
+
switch (event.item.name) {
|
|
22
|
+
case 'wordWrap':
|
|
23
|
+
wordWrap = !wordWrap
|
|
24
|
+
break
|
|
25
|
+
case 'minimap':
|
|
26
|
+
minimap = !minimap
|
|
27
|
+
break
|
|
28
|
+
case 'showGutter':
|
|
29
|
+
showGutter = !showGutter
|
|
30
|
+
break
|
|
31
|
+
case 'density':
|
|
32
|
+
density = event.item.value as Density
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
handle.update()
|
|
36
|
+
})}
|
|
37
|
+
>
|
|
38
|
+
<MenuItem name="wordWrap" type="checkbox" checked={wordWrap}>
|
|
39
|
+
Word wrap
|
|
40
|
+
</MenuItem>
|
|
41
|
+
<MenuItem name="minimap" type="checkbox" checked={minimap}>
|
|
42
|
+
Minimap
|
|
43
|
+
</MenuItem>
|
|
44
|
+
<MenuItem name="showGutter" type="checkbox" checked={showGutter}>
|
|
45
|
+
Show gutter
|
|
46
|
+
</MenuItem>
|
|
47
|
+
|
|
48
|
+
<hr mix={separatorStyle} />
|
|
49
|
+
<Submenu label="Zoom">
|
|
50
|
+
<MenuItem name="zoomIn" value="zoom-in">
|
|
51
|
+
Zoom In
|
|
52
|
+
</MenuItem>
|
|
53
|
+
<MenuItem name="zoomOut" value="zoom-out">
|
|
54
|
+
Zoom Out
|
|
55
|
+
</MenuItem>
|
|
56
|
+
<MenuItem name="resetZoom" value="reset-zoom">
|
|
57
|
+
Reset Zoom
|
|
58
|
+
</MenuItem>
|
|
59
|
+
</Submenu>
|
|
60
|
+
|
|
61
|
+
<Submenu label="Density">
|
|
62
|
+
<MenuItem
|
|
63
|
+
name="density"
|
|
64
|
+
type="radio"
|
|
65
|
+
value="comfortable"
|
|
66
|
+
checked={density === 'comfortable'}
|
|
67
|
+
>
|
|
68
|
+
Comfortable
|
|
69
|
+
</MenuItem>
|
|
70
|
+
<MenuItem name="density" type="radio" value="compact" checked={density === 'compact'}>
|
|
71
|
+
Compact
|
|
72
|
+
</MenuItem>
|
|
73
|
+
</Submenu>
|
|
74
|
+
</Menu>
|
|
75
|
+
)
|
|
76
|
+
}
|