@acusti/dropdown 0.56.0 → 1.0.0-alpha.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 CHANGED
@@ -7,9 +7,10 @@
7
7
 
8
8
  `Dropdown` is a React component that renders a menu-like UI with a trigger
9
9
  that the user clicks to disclose a dropdown anchored to that trigger. The
10
- body of the dropdown can include any DOM, and many dropdowns can be
11
- combined to form a multi-item menu, like the system menu in the top toolbar
12
- of macOS.
10
+ body of the dropdown can include any DOM. Dropdowns also compose: nest a
11
+ `Dropdown` inside another dropdown’s body to create a submenu, or wrap
12
+ sibling dropdowns in the `Menubar` component to combine them into a
13
+ multi-menu bar, like the system menu in the top toolbar of macOS.
13
14
 
14
15
  The three primary design goals for the existence of this component:
15
16
 
@@ -26,7 +27,12 @@ The three primary design goals for the existence of this component:
26
27
  `data-ukt-value="foo"` to specify that an element is a dropdown item
27
28
  and the value of that item at the same time (otherwise, the value is
28
29
  the text content of the dropdown item element)
29
- 4. To style your dropdowns, use CSS; there are a
30
+ 4. To create a submenu, nest a `Dropdown` inside the dropdown body (or
31
+ author the equivalent [`data-ukt-submenu` markup](#submenus)
32
+ directly)
33
+ 5. To combine dropdowns into a menubar, wrap them in
34
+ [`<Menubar>`](#menubar)
35
+ 6. To style your dropdowns, use CSS; there are a
30
36
  [collection of CSS custom properties](https://github.com/acusti/uikit/blob/main/packages/dropdown/src/Dropdown.css)
31
37
  used internally to style them if that works best for you, or just
32
38
  override the minimal default CSS as appropriate
@@ -78,6 +84,36 @@ function CustomTrigger() {
78
84
  }
79
85
  ```
80
86
 
87
+ ```tsx
88
+ import Dropdown, { Menubar } from '@acusti/dropdown';
89
+
90
+ // Submenus (nested dropdowns) combined into a menubar
91
+ function AppMenu() {
92
+ return (
93
+ <Menubar>
94
+ <Dropdown label="File">
95
+ <ul>
96
+ <li data-ukt-item>New</li>
97
+ <li data-ukt-item>Open…</li>
98
+ <Dropdown label="Open Recent">
99
+ <ul>
100
+ <li data-ukt-item>project-a</li>
101
+ <li data-ukt-item>project-b</li>
102
+ </ul>
103
+ </Dropdown>
104
+ </ul>
105
+ </Dropdown>
106
+ <Dropdown label="Edit">
107
+ <ul>
108
+ <li data-ukt-item>Undo</li>
109
+ <li data-ukt-item>Redo</li>
110
+ </ul>
111
+ </Dropdown>
112
+ </Menubar>
113
+ );
114
+ }
115
+ ```
116
+
81
117
  ## Layout Model
82
118
 
83
119
  `Dropdown` uses CSS anchor positioning for placement and prefers a
@@ -125,12 +161,45 @@ For the most reliable anchor-positioning behavior:
125
161
  - prefer CSS variable overrides over custom `top`/`left`/`right` inset
126
162
  rules
127
163
 
164
+ The body renders in the top layer using the
165
+ [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API),
166
+ which is what makes the anchor positioning robust. A top-layer element’s
167
+ containing block is always the viewport, so an ancestor with a `transform`,
168
+ `scale`, `filter`, `backdrop-filter`, `perspective`,
169
+ `will-change: transform`, or `contain` can’t capture the `position: fixed`
170
+ body. Were the body a normal descendant instead, any such ancestor would
171
+ become its containing block and its placement math would be measured
172
+ against that box rather than the viewport — `position-try` fallbacks stop
173
+ firing, and the base `position-area` can even resolve to the wrong side.
174
+ Because the body is in the top layer, none of that applies and no
175
+ consumer-side de-transforming is needed. The body uses `popover="manual"`,
176
+ so this component keeps control of dismissal: outside-click, focus, and
177
+ iframe handling are unchanged, and a searchable trigger’s input stays open
178
+ while you interact with it.
179
+
128
180
  For placement recipes, see
129
181
  [Placement Customization](#placement-customization-with-css-variables)
130
182
  below. If your trigger sits near the right edge of the viewport, the
131
183
  [End-Aligned, Content-Sized Menu](#end-aligned-content-sized-menu) example
132
184
  is the one you want.
133
185
 
186
+ ## Browser Support
187
+
188
+ `Dropdown` relies on two web-platform features, both now
189
+ [Baseline](https://web.dev/baseline) across Chrome, Edge, Firefox, and
190
+ Safari:
191
+
192
+ - **CSS anchor positioning** (`anchor-name`, `position-area`,
193
+ `@position-try`) places and re-flows the body —
194
+ [Baseline 2026](https://developer.mozilla.org/en-US/docs/Web/CSS/anchor-name)
195
+ - the **Popover API** (`popover` plus `showPopover()`) renders the body in
196
+ the top layer —
197
+ [Baseline 2024](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
198
+
199
+ These are _newly available_ Baseline features: they work in up-to-date
200
+ browser versions but not in ones predating that support, which the
201
+ component requires.
202
+
134
203
  ## API Reference
135
204
 
136
205
  ### Props
@@ -143,8 +212,13 @@ type Props = {
143
212
  */
144
213
  allowCreate?: boolean;
145
214
  /**
146
- * Boolean indicating if the user can submit an empty value (i.e. clear
147
- * the value). Defaults to true.
215
+ * Boolean indicating if submitting with no item active emits an empty
216
+ * value (i.e. clears the value). Only has an effect when the dropdown has a
217
+ * text input to source that value from — a searchable dropdown’s search
218
+ * input, or a text input inside a custom trigger. With no such input there
219
+ * is no value to submit, so submitting with nothing selected is a no-op
220
+ * regardless; clear such a dropdown with an explicit empty-valued item
221
+ * instead. Defaults to true.
148
222
  */
149
223
  allowEmpty?: boolean;
150
224
  /**
@@ -155,11 +229,6 @@ type Props = {
155
229
  children: ReactNode | [ReactNode, ReactNode];
156
230
  className?: string;
157
231
  disabled?: boolean;
158
- /**
159
- * Group identifier string links dropdowns together into a menu
160
- * (like macOS top menubar).
161
- */
162
- group?: string;
163
232
  /**
164
233
  * Whether the dropdown contains items that can be selected.
165
234
  * Defaults to true if children contain elements with data-ukt-item or data-ukt-value.
@@ -180,20 +249,18 @@ type Props = {
180
249
  keepOpenOnSubmit?: boolean;
181
250
  /**
182
251
  * Label content for the trigger button (when using single child syntax).
252
+ * For a nested (submenu) Dropdown, this is the parent item’s content.
183
253
  */
184
254
  label?: ReactNode;
185
- /**
186
- * Minimum height for the dropdown body in pixels. Defaults to 30.
187
- */
188
- minHeightBody?: number;
189
- /**
190
- * Minimum width for the dropdown body in pixels.
191
- */
192
- minWidthBody?: number;
193
255
  /**
194
256
  * Name attribute for the search input (requires isSearchable: true).
195
257
  */
196
258
  name?: string;
259
+ /**
260
+ * Called when the active (highlighted) item changes. Receives the
261
+ * same payload as onSubmitItem.
262
+ */
263
+ onActiveItem?: (payload: Item) => void;
197
264
  onClick?: (event: React.MouseEvent<HTMLElement>) => unknown;
198
265
  onClose?: () => unknown;
199
266
  onMouseDown?: (event: React.MouseEvent<HTMLElement>) => unknown;
@@ -231,6 +298,11 @@ type Item = {
231
298
  element: HTMLElement | null;
232
299
  event: Event | React.SyntheticEvent<HTMLElement>;
233
300
  label: string;
301
+ /**
302
+ * Ancestor parent items from the root level down to the item’s
303
+ * immediate parent. Empty for top-level items.
304
+ */
305
+ path: Array<{ label: string; value: string }>;
234
306
  value: string;
235
307
  };
236
308
  ```
@@ -401,7 +473,7 @@ CSS custom properties for the most common low-level placement controls:
401
473
 
402
474
  - `--uktdd-body-position-area`
403
475
  - `--uktdd-body-position-try-fallbacks`
404
- - `--uktdd-body-translate`
476
+ - `--uktdd-body-gap` (space between the trigger and the body)
405
477
  - `--uktdd-body-min-height`
406
478
  - `--uktdd-body-min-width`
407
479
  - `--uktdd-body-max-height`
@@ -414,7 +486,7 @@ Example:
414
486
  --uktdd-body-position-area: bottom span-left;
415
487
  --uktdd-body-position-try-fallbacks:
416
488
  --uktdd-top-right, --uktdd-bottom-left, --uktdd-top-left;
417
- --uktdd-body-translate: -8px 0;
489
+ --uktdd-body-gap: 8px;
418
490
  }
419
491
 
420
492
  .settings-dropdown .uktdropdown-body {
@@ -425,6 +497,18 @@ Example:
425
497
  This approach keeps the public React API small while still allowing precise
426
498
  placement and sizing control when a product surface needs it.
427
499
 
500
+ Use `--uktdd-body-gap` for the space between the trigger and the body. It
501
+ is applied as a symmetric `margin-block`, so the gap lands on whichever
502
+ side the body attaches to and auto-reverses when `position-try` flips the
503
+ body above the trigger. A margin establishes no containing block, so
504
+ `--uktdd-body-gap` is safe on dropdowns with submenus.
505
+
506
+ For an offset a gap can’t express — a horizontal nudge, or deliberately
507
+ overlapping the trigger — set `translate` on `.uktdropdown-body` directly.
508
+ A `translate` makes the body a containing block for its fixed-position
509
+ submenus, which constrains and clips them, so reserve it for dropdowns
510
+ without submenus.
511
+
428
512
  ### End-Aligned, Content-Sized Menu
429
513
 
430
514
  For menus attached to controls near the right edge of the viewport, such as
@@ -448,15 +532,281 @@ This keeps the menu:
448
532
  Avoid hardcoding width for this pattern unless the product explicitly needs
449
533
  a fixed-size menu.
450
534
 
451
- ## Keyboard Navigation & Accessibility
535
+ ### Centered Menus
536
+
537
+ To center a menu over its trigger, use a `span-all` tile, not a `center`
538
+ tile. A `center` tile is only as wide as the trigger, so a body wider than
539
+ the trigger always overflows it — and `position-try` refuses to select a
540
+ fallback that overflows, so a `bottom center` → `top center` setup never
541
+ flips. `span-all` centers over the trigger identically but spans the full
542
+ width, so it flips cleanly and clamps to the viewport near an edge; the
543
+ package’s `position-try-order: most-height` then picks the taller side:
544
+
545
+ ```css
546
+ .centered-menu {
547
+ --uktdd-body-position-area: bottom span-all;
548
+ --uktdd-body-position-try-fallbacks: --centered-menu-top;
549
+ }
550
+
551
+ /* define the flipped fallback in your own CSS */
552
+ @position-try --centered-menu-top {
553
+ position-area: top span-all;
554
+ }
555
+ ```
556
+
557
+ ## Submenus
558
+
559
+ A `Dropdown` is a disclosure node: a trigger plus a disclosed list.
560
+ Disclosure nodes compose — nest them for depth (submenus), or wrap siblings
561
+ in [`Menubar`](#menubar) for breadth (a menu bar). A submenu is nothing
562
+ more than a `Dropdown` nested inside another dropdown’s body: the nested
563
+ dropdown renders as a **parent item**, an item that discloses a child menu
564
+ instead of submitting a value.
565
+
566
+ ```tsx
567
+ import Dropdown from '@acusti/dropdown';
568
+
569
+ function FormatMenu() {
570
+ return (
571
+ <Dropdown label="Format" onSubmitItem={applyFormat}>
572
+ <ul>
573
+ <li data-ukt-item>Bold</li>
574
+ <li data-ukt-item>Italic</li>
575
+ <Dropdown label="Align">
576
+ <ul>
577
+ <li data-ukt-value="left">Left</li>
578
+ <li data-ukt-value="center">Center</li>
579
+ <li data-ukt-value="right">Right</li>
580
+ </ul>
581
+ </Dropdown>
582
+ </ul>
583
+ </Dropdown>
584
+ );
585
+ }
586
+ ```
587
+
588
+ Submenu semantics:
589
+
590
+ - Parent items disclose; they never submit. `onSubmitItem` fires only for
591
+ leaf items, and the payload’s `path` reports the leaf’s ancestor parent
592
+ items (see the [`Item` type](#item-type) above), so leaves with the same
593
+ value in different submenus are distinguishable.
594
+ - Submenus nest to arbitrary depth — a submenu body can itself contain
595
+ nested dropdowns.
596
+ - A parent item highlights immediately when it becomes active — via pointer
597
+ or arrow keys — and discloses its submenu after a short intent delay,
598
+ with the highlight staying on the parent (macOS-style: moving quickly
599
+ past parent items never flashes their submenus open, but pausing on one
600
+ for even a moment shows its submenu). Activating a sibling closes it. →
601
+ opens the submenu immediately and moves the highlight into it — see
602
+ [Keyboard Navigation](#keyboard-navigation--accessibility).
603
+ - A parent item’s open state is carried by its `aria-expanded` attribute,
604
+ which is also the styling hook for submenu visibility.
605
+ - With the pointer, an open submenu tracks mouse intent: moving diagonally
606
+ from the parent item toward the submenu keeps it open even as the pointer
607
+ crosses sibling items along the way (the macOS “safe triangle” between
608
+ the pointer and the submenu’s near edge). Moving straight onto a sibling
609
+ — or pausing mid-diagonal — switches to that item as usual.
610
+
611
+ ### The `data-ukt-submenu` protocol
612
+
613
+ Like items, submenus are ultimately declared in the DOM. A nested
614
+ `Dropdown` renders this markup:
615
+
616
+ ```html
617
+ <li data-ukt-item aria-haspopup="menu" aria-expanded="false">
618
+ Align
619
+ <ul data-ukt-submenu role="menu">
620
+
621
+ </ul>
622
+ </li>
623
+ ```
624
+
625
+ You can author that markup directly instead of nesting a `Dropdown`
626
+ component; the two forms behave identically because the component form
627
+ compiles to the attribute form. Rules for direct authoring:
628
+
629
+ - the `data-ukt-submenu` element must be a descendant of its parent item
630
+ element
631
+ - in a level whose items include a submenu, mark all of that level’s items
632
+ explicitly with `data-ukt-item` or `data-ukt-value`; purely flat levels —
633
+ including flat submenu bodies — fall back to the automatic item-detection
634
+ heuristic
635
+ - a parent item’s label is its text content excluding the submenu subtree
636
+
637
+ ### Props on a nested Dropdown
638
+
639
+ Most props keep their meaning, scoped to the submenu:
640
+
641
+ - `label` (or the first of two children): the parent item’s content
642
+ - `onSubmitItem`: fires for submissions within that subtree only
643
+ - `onActiveItem`, `onOpen`, `onClose`: scoped to the submenu
644
+ - `disabled`: disables the parent item
645
+ - `className`/`style`: applied to the parent item element
646
+
647
+ Props that only make sense at the top level (`allowCreate`, `allowEmpty`,
648
+ `isOpenOnMount`, `isSearchable`, `keepOpenOnSubmit`, `name`, `placeholder`,
649
+ `tabIndex`, `value`) are ignored on a nested dropdown and warn
650
+ (unconditionally, matching the children-count misuse error).
651
+
652
+ ### Submenu placement and styling
653
+
654
+ Submenus reuse the dropdown’s anchor-positioning layout model: the expanded
655
+ parent item is the anchor, and the submenu opens at its inline-end,
656
+ top-aligned, falling back to the opposite side when out of bounds.
657
+ Placement custom properties follow the established naming scheme:
658
+
659
+ - `--uktdd-submenu-position-area` (default: `inline-end span-block-end`)
660
+ - `--uktdd-submenu-position-try-fallbacks`
661
+ - `--uktdd-submenu-gap` (space between the parent item and the submenu;
662
+ applied as a symmetric `margin-inline`, the inline-axis counterpart to
663
+ `--uktdd-body-gap`, so it auto-reverses when the submenu flips to the
664
+ opposite side)
665
+
666
+ Parent items render a macOS-style disclosure chevron, drawn in CSS with the
667
+ item’s `::after` pseudo-element and colored with `currentColor` so it
668
+ follows the item’s highlight color. Restyle or remove it by overriding
669
+ `[aria-haspopup='menu']::after`.
670
+
671
+ Submenu bodies set `color: var(--uktdd-body-color)` (default: `canvastext`)
672
+ explicitly — without it, an active parent item’s highlight text color would
673
+ inherit into its submenu’s items, because the submenu is the parent item’s
674
+ DOM child. Set `--uktdd-body-color` alongside `--uktdd-body-bg-color` when
675
+ theming menu colors.
676
+
677
+ The active path is fully styleable: `data-ukt-active` marks one item per
678
+ open level (a parent item stays active while you navigate its submenu), and
679
+ `aria-expanded="true"` marks open parent items. The default submenu
680
+ visibility rule, shown here for reference:
681
+
682
+ ```css
683
+ [data-ukt-submenu] {
684
+ display: none;
685
+ }
686
+
687
+ [aria-expanded='true'] > [data-ukt-submenu] {
688
+ display: block;
689
+ }
690
+ ```
691
+
692
+ Following macOS, the default styles distinguish the deepest active item —
693
+ the one keyboard input operates on — from its ancestors on the active path.
694
+ The deepest item gets the primary highlight
695
+ (`--uktdd-body-bg-color-hover`); an ancestor parent item — active with the
696
+ highlight deeper inside its submenu, or with the pointer anywhere inside
697
+ its open submenu, even over a separator or disabled item — gets a muted
698
+ highlight (`--uktdd-body-bg-color-path`, gray by default). Surfacing back
699
+ out with ← closes the submenu and restores the parent’s primary highlight.
700
+ The default rule, shown here for reference:
701
+
702
+ ```css
703
+ /* an active item is on the path — not deepest — while a deeper
704
+ highlight exists or the pointer is inside its submenu */
705
+ [data-ukt-active]:has([data-ukt-submenu] [data-ukt-active]),
706
+ [data-ukt-active]:has([data-ukt-submenu]:hover) {
707
+ background-color: var(--uktdd-body-bg-color-path);
708
+ color: var(--uktdd-body-color-path);
709
+ }
710
+ ```
711
+
712
+ ### Nested non-menu dropdowns
452
713
 
453
- The dropdown implements full keyboard navigation:
714
+ Nesting means submenu only for menus. A nested `Dropdown` with
715
+ `hasItems={false}` isn’t a menu, so it renders as an independent anchored
716
+ dropdown instead of a parent item — for example, an ℹ️ button next to an
717
+ input in a form dropdown that opens an info popover about the input:
454
718
 
455
- - **Enter/Space**: Open dropdown or select highlighted item
456
- - **Escape**: Close dropdown
457
- - **Arrow Up/Down**: Navigate between items
458
- - **Home/End**: Jump to first/last item
459
- - **Type characters**: Search for items (when searchable)
719
+ ```tsx
720
+ <Dropdown hasItems={false}>
721
+ Preferences
722
+ <div>
723
+ <label>
724
+ Width <input name="width" />
725
+ </label>
726
+ <Dropdown hasItems={false}>
727
+ <button aria-label="About width">ℹ️</button>
728
+ <p>Width accepts any CSS length.</p>
729
+ </Dropdown>
730
+ </div>
731
+ </Dropdown>
732
+ ```
733
+
734
+ The nested dropdown owns its own interactions: opening it, clicking inside
735
+ it, and navigating it never close or submit the outer dropdown, and Escape
736
+ closes the innermost open dropdown first — a second Escape closes the outer
737
+ one.
738
+
739
+ ## Menubar
740
+
741
+ `Menubar` combines sibling dropdowns into a single menu, like the system
742
+ menu in the top toolbar of macOS. Each dropdown’s trigger becomes a menubar
743
+ item. See the [Quick Start](#quick-start) for a complete example:
744
+
745
+ ```tsx
746
+ import Dropdown, { Menubar } from '@acusti/dropdown';
747
+
748
+ <Menubar>
749
+ <Dropdown label="File">…</Dropdown>
750
+ <Dropdown label="Edit">…</Dropdown>
751
+ <Dropdown label="View">…</Dropdown>
752
+ </Menubar>;
753
+ ```
754
+
755
+ Menubar behaviors:
756
+
757
+ - renders `role="menubar"`; each dropdown trigger is a menubar item
758
+ - at most one menu in the bar is open at a time
759
+ - once any menu is open, hovering another trigger switches to that menu
760
+ without a click (matching macOS menubar behavior)
761
+ - **←/→** move between menus, wrapping at the ends: with the bar focused
762
+ they move focus between triggers, and while a menu is open they slide the
763
+ open menu to the adjacent trigger — unless the active item is a parent
764
+ item, in which case **→** dives into its submenu instead
765
+ - **↓/Enter/Space** open the focused trigger’s menu
766
+ - **Escape** closes the open menu — including any open submenus — and
767
+ returns focus to its menubar item
768
+
769
+ The trigger of the open menu gets an active-state background so it reads as
770
+ selected, like the highlighted item in the macOS menu bar. The default
771
+ tints over whatever background the trigger already has (via
772
+ `--uktdd-menubar-trigger-bg-color-active`, applied to
773
+ `.uktmenubar .uktdropdown.is-open .uktdropdown-trigger`), so it works
774
+ regardless of how the trigger is styled; override the custom property, or
775
+ the rule itself, to restyle it.
776
+
777
+ `Menubar` supersedes the `group` prop that appeared in earlier versions of
778
+ the `Props` type: `group` was a string-ID way to associate dropdowns that
779
+ was never wired up, and composition expresses the relationship directly.
780
+
781
+ ## Keyboard Navigation & Accessibility
782
+
783
+ The dropdown implements full keyboard navigation. The model has one rule:
784
+ navigate along the current menu level’s axis, dive into a submenu
785
+ perpendicular to it, surface back out with the opposite key. Menus are
786
+ vertical, so ↑/↓ navigate and →/← dive/surface; a menubar is horizontal, so
787
+ ←/→ navigate and ↓ dives.
788
+
789
+ Keys operate on the **current level**: the level of the deepest highlighted
790
+ item. Note that this can be one level shallower than the deepest open
791
+ submenu — pausing on a parent item discloses its submenu after a short
792
+ intent delay while the highlight stays on the parent, and only → moves the
793
+ highlight into it.
794
+
795
+ - **Enter/Space**: Open the dropdown; with an item highlighted, select it
796
+ (leaf items) or open its submenu (parent items)
797
+ - **Escape**: Close the dropdown entirely — including any open submenus —
798
+ and return focus to the trigger (macOS-style; use ← to back out one level
799
+ at a time)
800
+ - **Arrow Up/Down**: Navigate between items in the current level
801
+ - **Arrow Right**: Open the highlighted parent item’s submenu (if not
802
+ already disclosed) and highlight its first item; in a
803
+ [`Menubar`](#menubar), if the highlighted item is a leaf, slide to the
804
+ next menu instead
805
+ - **Arrow Left**: Close the highlighted item’s level and return to its
806
+ parent item; in a `Menubar` with the highlight at the top level of a
807
+ menu, slide to the previous menu
808
+ - **Home/End**: Jump to first/last item in the current level
809
+ - **Type characters**: Jump to the best-matching item in the current level
460
810
 
461
811
  For accessibility, the component focuses on semantic HTML structure and
462
812
  keyboard navigation. It works best when you use appropriate HTML elements
@@ -477,3 +827,21 @@ The open body element also receives a matching `role` and an `id` so screen
477
827
  readers can associate the trigger with its popup. If your custom trigger
478
828
  already specifies any of these ARIA props, your values win — the component
479
829
  only fills in what you haven’t set.
830
+
831
+ Submenus and menubars extend the same pattern automatically:
832
+
833
+ - parent items receive `aria-haspopup="menu"` and `aria-expanded`, and
834
+ their submenu container receives `role="menu"` and a linked `id`
835
+ - the `Menubar` container receives `role="menubar"`
836
+
837
+ As with the trigger, any of these you specify yourself win.
838
+
839
+ One deliberate deviation from the
840
+ [APG menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/): APG
841
+ specifies that Escape closes one menu level per press, while this component
842
+ closes the whole menu at once (matching macOS native menus and Radix UI).
843
+ Level-by-level closing is already covered by ←, so a one-level Escape would
844
+ be redundant; closing everything preserves Escape’s universal “dismiss this
845
+ context” meaning. The part of the APG guidance that matters for assistive
846
+ technology is kept: closing the menu always returns focus to the element
847
+ that opened it.
@@ -1,7 +1,16 @@
1
- import { CSSProperties, MouseEvent as ReactMouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
1
+ import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement, type ReactNode, type SyntheticEvent } from 'react';
2
2
  export type Item = {
3
3
  element: MaybeHTMLElement;
4
4
  event: Event | SyntheticEvent<HTMLElement>;
5
+ label: string;
6
+ /**
7
+ * Ancestor parent items from the root level down to the item’s
8
+ * immediate parent. Empty for top-level items.
9
+ */
10
+ path: Array<ItemPathEntry>;
11
+ value: string;
12
+ };
13
+ export type ItemPathEntry = {
5
14
  label: string;
6
15
  value: string;
7
16
  };
@@ -12,8 +21,13 @@ export type Props = {
12
21
  */
13
22
  allowCreate?: boolean;
14
23
  /**
15
- * Boolean indicating if the user can submit an empty value (i.e. clear
16
- * the value). Defaults to true.
24
+ * Boolean indicating if submitting with no item active emits an empty
25
+ * value (i.e. clears the value). Only has an effect when the dropdown has a
26
+ * text input to source that value from — a searchable dropdown’s search
27
+ * input, or a text input inside a custom trigger. With no such input there
28
+ * is no value to submit, so submitting with nothing selected is a no-op
29
+ * regardless; clear such a dropdown with an explicit empty-valued item
30
+ * instead. Defaults to true.
17
31
  */
18
32
  allowEmpty?: boolean;
19
33
  /**
@@ -22,18 +36,15 @@ export type Props = {
22
36
  children: ChildrenTuple | ReactElement;
23
37
  className?: string;
24
38
  disabled?: boolean;
25
- /**
26
- * Group identifier string links dropdowns together into a menu
27
- * (like macOS top menubar).
28
- */
29
- group?: string;
30
39
  hasItems?: boolean;
31
40
  isOpenOnMount?: boolean;
32
41
  isSearchable?: boolean;
33
42
  keepOpenOnSubmit?: boolean;
43
+ /**
44
+ * Label content for the trigger button (when using single child syntax).
45
+ * For a nested (submenu) Dropdown, this is the parent item’s content.
46
+ */
34
47
  label?: ReactNode;
35
- minHeightBody?: number;
36
- minWidthBody?: number;
37
48
  /**
38
49
  * Only usable in conjunction with {isSearchable: true}.
39
50
  * Used as search input’s name.
@@ -51,7 +62,12 @@ export type Props = {
51
62
  * Used as search input’s placeholder.
52
63
  */
53
64
  placeholder?: string;
54
- style?: CSSProperties;
65
+ /**
66
+ * Applied to the dropdown root element. Also accepts the component’s CSS
67
+ * custom properties (e.g. `--uktdd-body-min-width`) for per-instance
68
+ * placement and sizing, which plain `CSSProperties` rejects.
69
+ */
70
+ style?: CSSProperties & Record<`--${string}`, string | number | undefined>;
55
71
  /**
56
72
  * Only usable in conjunction with {isSearchable: true}.
57
73
  * Used as search input’s tabIndex.
@@ -65,5 +81,5 @@ export type Props = {
65
81
  };
66
82
  type ChildrenTuple = [ReactNode, ReactNode] | readonly [ReactNode, ReactNode];
67
83
  type MaybeHTMLElement = HTMLElement | null;
68
- export default function Dropdown({ allowCreate, allowEmpty, children, className, disabled, hasItems, isOpenOnMount, isSearchable, keepOpenOnSubmit, label, minHeightBody, minWidthBody, name, onActiveItem, onClick, onClose, onMouseDown, onMouseUp, onOpen, onSubmitItem, placeholder, style: styleFromProps, tabIndex, value, }: Props): import("react/jsx-runtime").JSX.Element;
69
- export {};
84
+ export default function Dropdown(props: Props): import("react").JSX.Element;
85
+ export { default as Menubar, type MenubarProps } from './Menubar.js';