@acusti/dropdown 0.57.0 → 1.0.0-alpha.1

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
@@ -160,11 +229,6 @@ type Props = {
160
229
  children: ReactNode | [ReactNode, ReactNode];
161
230
  className?: string;
162
231
  disabled?: boolean;
163
- /**
164
- * Group identifier string links dropdowns together into a menu
165
- * (like macOS top menubar).
166
- */
167
- group?: string;
168
232
  /**
169
233
  * Whether the dropdown contains items that can be selected.
170
234
  * Defaults to true if children contain elements with data-ukt-item or data-ukt-value.
@@ -185,20 +249,18 @@ type Props = {
185
249
  keepOpenOnSubmit?: boolean;
186
250
  /**
187
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.
188
253
  */
189
254
  label?: ReactNode;
190
- /**
191
- * Minimum height for the dropdown body in pixels. Defaults to 30.
192
- */
193
- minHeightBody?: number;
194
- /**
195
- * Minimum width for the dropdown body in pixels.
196
- */
197
- minWidthBody?: number;
198
255
  /**
199
256
  * Name attribute for the search input (requires isSearchable: true).
200
257
  */
201
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;
202
264
  onClick?: (event: React.MouseEvent<HTMLElement>) => unknown;
203
265
  onClose?: () => unknown;
204
266
  onMouseDown?: (event: React.MouseEvent<HTMLElement>) => unknown;
@@ -236,6 +298,11 @@ type Item = {
236
298
  element: HTMLElement | null;
237
299
  event: Event | React.SyntheticEvent<HTMLElement>;
238
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 }>;
239
306
  value: string;
240
307
  };
241
308
  ```
@@ -406,7 +473,7 @@ CSS custom properties for the most common low-level placement controls:
406
473
 
407
474
  - `--uktdd-body-position-area`
408
475
  - `--uktdd-body-position-try-fallbacks`
409
- - `--uktdd-body-translate`
476
+ - `--uktdd-body-gap` (space between the trigger and the body)
410
477
  - `--uktdd-body-min-height`
411
478
  - `--uktdd-body-min-width`
412
479
  - `--uktdd-body-max-height`
@@ -419,7 +486,7 @@ Example:
419
486
  --uktdd-body-position-area: bottom span-left;
420
487
  --uktdd-body-position-try-fallbacks:
421
488
  --uktdd-top-right, --uktdd-bottom-left, --uktdd-top-left;
422
- --uktdd-body-translate: -8px 0;
489
+ --uktdd-body-gap: 8px;
423
490
  }
424
491
 
425
492
  .settings-dropdown .uktdropdown-body {
@@ -430,6 +497,18 @@ Example:
430
497
  This approach keeps the public React API small while still allowing precise
431
498
  placement and sizing control when a product surface needs it.
432
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
+
433
512
  ### End-Aligned, Content-Sized Menu
434
513
 
435
514
  For menus attached to controls near the right edge of the viewport, such as
@@ -453,15 +532,291 @@ This keeps the menu:
453
532
  Avoid hardcoding width for this pattern unless the product explicitly needs
454
533
  a fixed-size menu.
455
534
 
456
- ## 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
+ ```
457
624
 
458
- The dropdown implements full keyboard navigation:
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:
459
681
 
460
- - **Enter/Space**: Open dropdown or select highlighted item
461
- - **Escape**: Close dropdown
462
- - **Arrow Up/Down**: Navigate between items
463
- - **Home/End**: Jump to first/last item
464
- - **Type characters**: Search for items (when searchable)
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 independent dropdowns
713
+
714
+ Submenus are a menu concept: a `Dropdown` becomes a submenu only when it’s
715
+ a menu nested inside another menu. Anything else nests as an
716
+ **independent** anchored dropdown — opening on click and selecting items on
717
+ click (no hover-disclosed parent item), with its interactions never
718
+ touching the outer dropdown. Two cases qualify:
719
+
720
+ - a `hasItems={false}` `Dropdown` (a dialog/popover) nested anywhere — e.g.
721
+ an ℹ️ info popover next to an input in a form dropdown (below);
722
+ - any `Dropdown` nested inside a `hasItems={false}` dropdown — e.g. a
723
+ self-contained picker embedded in a larger dialog, which keeps full
724
+ click-to-select on its `data-ukt-value` items.
725
+
726
+ For example, an ℹ️ button next to an input in a form dropdown that opens an
727
+ info popover about the input:
728
+
729
+ ```tsx
730
+ <Dropdown hasItems={false}>
731
+ Preferences
732
+ <div>
733
+ <label>
734
+ Width <input name="width" />
735
+ </label>
736
+ <Dropdown hasItems={false}>
737
+ <button aria-label="About width">ℹ️</button>
738
+ <p>Width accepts any CSS length.</p>
739
+ </Dropdown>
740
+ </div>
741
+ </Dropdown>
742
+ ```
743
+
744
+ The nested dropdown owns its own interactions: opening it, clicking inside
745
+ it, and navigating it never close or submit the outer dropdown, and Escape
746
+ closes the innermost open dropdown first — a second Escape closes the outer
747
+ one.
748
+
749
+ ## Menubar
750
+
751
+ `Menubar` combines sibling dropdowns into a single menu, like the system
752
+ menu in the top toolbar of macOS. Each dropdown’s trigger becomes a menubar
753
+ item. See the [Quick Start](#quick-start) for a complete example:
754
+
755
+ ```tsx
756
+ import Dropdown, { Menubar } from '@acusti/dropdown';
757
+
758
+ <Menubar>
759
+ <Dropdown label="File">…</Dropdown>
760
+ <Dropdown label="Edit">…</Dropdown>
761
+ <Dropdown label="View">…</Dropdown>
762
+ </Menubar>;
763
+ ```
764
+
765
+ Menubar behaviors:
766
+
767
+ - renders `role="menubar"`; each dropdown trigger is a menubar item
768
+ - at most one menu in the bar is open at a time
769
+ - once any menu is open, hovering another trigger switches to that menu
770
+ without a click (matching macOS menubar behavior)
771
+ - **←/→** move between menus, wrapping at the ends: with the bar focused
772
+ they move focus between triggers, and while a menu is open they slide the
773
+ open menu to the adjacent trigger — unless the active item is a parent
774
+ item, in which case **→** dives into its submenu instead
775
+ - **↓/Enter/Space** open the focused trigger’s menu
776
+ - **Escape** closes the open menu — including any open submenus — and
777
+ returns focus to its menubar item
778
+
779
+ The trigger of the open menu gets an active-state background so it reads as
780
+ selected, like the highlighted item in the macOS menu bar. The default
781
+ tints over whatever background the trigger already has (via
782
+ `--uktdd-menubar-trigger-bg-color-active`, applied to
783
+ `.uktmenubar .uktdropdown.is-open .uktdropdown-trigger`), so it works
784
+ regardless of how the trigger is styled; override the custom property, or
785
+ the rule itself, to restyle it.
786
+
787
+ `Menubar` supersedes the `group` prop that appeared in earlier versions of
788
+ the `Props` type: `group` was a string-ID way to associate dropdowns that
789
+ was never wired up, and composition expresses the relationship directly.
790
+
791
+ ## Keyboard Navigation & Accessibility
792
+
793
+ The dropdown implements full keyboard navigation. The model has one rule:
794
+ navigate along the current menu level’s axis, dive into a submenu
795
+ perpendicular to it, surface back out with the opposite key. Menus are
796
+ vertical, so ↑/↓ navigate and →/← dive/surface; a menubar is horizontal, so
797
+ ←/→ navigate and ↓ dives.
798
+
799
+ Keys operate on the **current level**: the level of the deepest highlighted
800
+ item. Note that this can be one level shallower than the deepest open
801
+ submenu — pausing on a parent item discloses its submenu after a short
802
+ intent delay while the highlight stays on the parent, and only → moves the
803
+ highlight into it.
804
+
805
+ - **Enter/Space**: Open the dropdown; with an item highlighted, select it
806
+ (leaf items) or open its submenu (parent items)
807
+ - **Escape**: Close the dropdown entirely — including any open submenus —
808
+ and return focus to the trigger (macOS-style; use ← to back out one level
809
+ at a time)
810
+ - **Arrow Up/Down**: Navigate between items in the current level
811
+ - **Arrow Right**: Open the highlighted parent item’s submenu (if not
812
+ already disclosed) and highlight its first item; in a
813
+ [`Menubar`](#menubar), if the highlighted item is a leaf, slide to the
814
+ next menu instead
815
+ - **Arrow Left**: Close the highlighted item’s level and return to its
816
+ parent item; in a `Menubar` with the highlight at the top level of a
817
+ menu, slide to the previous menu
818
+ - **Home/End**: Jump to first/last item in the current level
819
+ - **Type characters**: Jump to the best-matching item in the current level
465
820
 
466
821
  For accessibility, the component focuses on semantic HTML structure and
467
822
  keyboard navigation. It works best when you use appropriate HTML elements
@@ -482,3 +837,21 @@ The open body element also receives a matching `role` and an `id` so screen
482
837
  readers can associate the trigger with its popup. If your custom trigger
483
838
  already specifies any of these ARIA props, your values win — the component
484
839
  only fills in what you haven’t set.
840
+
841
+ Submenus and menubars extend the same pattern automatically:
842
+
843
+ - parent items receive `aria-haspopup="menu"` and `aria-expanded`, and
844
+ their submenu container receives `role="menu"` and a linked `id`
845
+ - the `Menubar` container receives `role="menubar"`
846
+
847
+ As with the trigger, any of these you specify yourself win.
848
+
849
+ One deliberate deviation from the
850
+ [APG menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/): APG
851
+ specifies that Escape closes one menu level per press, while this component
852
+ closes the whole menu at once (matching macOS native menus and Radix UI).
853
+ Level-by-level closing is already covered by ←, so a one-level Escape would
854
+ be redundant; closing everything preserves Escape’s universal “dismiss this
855
+ context” meaning. The part of the APG guidance that matters for assistive
856
+ technology is kept: closing the menu always returns focus to the element
857
+ that opened it.
@@ -2,6 +2,15 @@ import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactEleme
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
  };
@@ -27,18 +36,15 @@ export type Props = {
27
36
  children: ChildrenTuple | ReactElement;
28
37
  className?: string;
29
38
  disabled?: boolean;
30
- /**
31
- * Group identifier string links dropdowns together into a menu
32
- * (like macOS top menubar).
33
- */
34
- group?: string;
35
39
  hasItems?: boolean;
36
40
  isOpenOnMount?: boolean;
37
41
  isSearchable?: boolean;
38
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
+ */
39
47
  label?: ReactNode;
40
- minHeightBody?: number;
41
- minWidthBody?: number;
42
48
  /**
43
49
  * Only usable in conjunction with {isSearchable: true}.
44
50
  * Used as search input’s name.
@@ -56,7 +62,12 @@ export type Props = {
56
62
  * Used as search input’s placeholder.
57
63
  */
58
64
  placeholder?: string;
59
- 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>;
60
71
  /**
61
72
  * Only usable in conjunction with {isSearchable: true}.
62
73
  * Used as search input’s tabIndex.
@@ -70,5 +81,5 @@ export type Props = {
70
81
  };
71
82
  type ChildrenTuple = [ReactNode, ReactNode] | readonly [ReactNode, ReactNode];
72
83
  type MaybeHTMLElement = HTMLElement | null;
73
- 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.Element;
74
- export {};
84
+ export default function Dropdown(props: Props): import("react").JSX.Element;
85
+ export { default as Menubar, type MenubarProps } from './Menubar.js';