@acusti/dropdown 1.0.0-rc.3 → 1.0.0-rc.4

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
@@ -33,9 +33,9 @@ The three primary design goals for the existence of this component:
33
33
  directly)
34
34
  5. To combine dropdowns into a menubar, wrap them in
35
35
  [`<Menubar>`](#menubar)
36
- 6. To style your dropdowns, use CSS; there are a
37
- [collection of CSS custom properties](https://github.com/acusti/uikit/blob/main/packages/dropdown/src/Dropdown.css)
38
- used internally to style them if that works best for you, or just
36
+ 6. To style your dropdowns, use CSS; there is a
37
+ [collection of CSS custom properties](#css-custom-properties) used
38
+ internally to style them if that works best for you, or just
39
39
  override the minimal default CSS as appropriate
40
40
  3. **Lightweight bundle size** with the bare minimum of dependencies (see
41
41
  minzipped size above)
@@ -144,8 +144,9 @@ Internally, the dropdown renders:
144
144
 
145
145
  Custom padding and overflow styling belongs on the content region, not the
146
146
  outer shell. Note that `.uktdropdown-content` already applies default
147
- padding (see the `--uktdd-body-pad-*` variables below), so your body
148
- element does **not** need its own padding:
147
+ padding (the `--uktdd-body-pad-*` custom properties, listed under
148
+ [CSS custom properties](#css-custom-properties)), so your body element does
149
+ **not** need its own padding:
149
150
 
150
151
  ```tsx
151
152
  // ✗ Don’t double-pad — the content region already has padding
@@ -155,7 +156,7 @@ element does **not** need its own padding:
155
156
  </Dropdown>
156
157
 
157
158
  // ✓ Override default padding via CSS variables if needed
158
- // .my-dropdown { --uktdd-body-pad-top: 16px; /* etc */ }
159
+ // .my-dropdown { --uktdd-body-pad-block-start: 16px; /* etc */ }
159
160
  ```
160
161
 
161
162
  For the most reliable anchor-positioning behavior:
@@ -262,11 +263,18 @@ type Props = {
262
263
  */
263
264
  allowEmpty?: boolean;
264
265
  /**
265
- * Can take a single React element or exactly two renderable children.
266
- * - Single child: The dropdown body (trigger will be auto-generated button)
267
- * - Two children: [trigger, body]
266
+ * Either a single React element (the dropdown body; the trigger is a
267
+ * generated button, or a generated search input when isSearchable) or
268
+ * exactly two renderable children: the trigger, then the body.
269
+ */
270
+ children:
271
+ | ReactElement
272
+ | [ReactNode, ReactNode]
273
+ | readonly [ReactNode, ReactNode];
274
+ /**
275
+ * Applied to the dropdown root element. For a nested (submenu) Dropdown,
276
+ * applied to its item element instead.
268
277
  */
269
- children: ReactNode | [ReactNode, ReactNode];
270
278
  className?: string;
271
279
  /**
272
280
  * Prevents the dropdown from opening via user interaction: pointer,
@@ -280,21 +288,38 @@ type Props = {
280
288
  */
281
289
  disabled?: boolean;
282
290
  /**
283
- * Whether the dropdown contains items that can be selected.
284
- * Defaults to true if children contain elements with data-ukt-item or data-ukt-value.
291
+ * Whether the body is a list of selectable items (the default) or
292
+ * arbitrary interactive content such as a form. hasItems={false} turns
293
+ * off item selection and item keyboard navigation, gives the popup
294
+ * role="dialog" (unless isSearchable), and keeps it open on clicks
295
+ * inside the body. Defaults to true and isn’t inferred from the children.
285
296
  */
286
297
  hasItems?: boolean;
287
298
  /**
288
- * Whether the dropdown should be open when first mounted.
299
+ * Renders the dropdown already open on mount. Uncontrolled: it sets only
300
+ * the initial state.
289
301
  */
290
302
  isOpenOnMount?: boolean;
291
303
  /**
292
- * Whether the dropdown should include a search input for filtering options.
304
+ * Renders the trigger as a text input (a combobox) that filters the items
305
+ * as the user types, and the popup as a listbox.
293
306
  */
294
307
  isSearchable?: boolean;
295
308
  /**
296
- * Whether the dropdown should remain open after selecting an item.
297
- * Useful for multi-select scenarios.
309
+ * For a nested (submenu) Dropdown, its item element. When unset, it is
310
+ * an <li> inside a <ul>, <ol>, or <menu> and a <div> anywhere else,
311
+ * decided by checking its container once it’s in the DOM. Setting this
312
+ * skips that check, so the item mounts once and server output matches
313
+ * the client; recommended for a body that isn’t a list, which otherwise
314
+ * pays for the check on every open. Only a submenu Dropdown (one nested
315
+ * in a dropdown where hasItems is true) reads it; anywhere else it is
316
+ * ignored, with a warning.
317
+ */
318
+ itemAs?: 'div' | 'li';
319
+ /**
320
+ * Whether the dropdown stays open after an item is submitted, e.g. for a
321
+ * multi-select. Defaults to !hasItems: a menu closes on submit, while a
322
+ * hasItems={false} dialog stays open.
298
323
  */
299
324
  keepOpenOnSubmit?: boolean;
300
325
  /**
@@ -303,19 +328,46 @@ type Props = {
303
328
  */
304
329
  label?: ReactNode;
305
330
  /**
306
- * Name attribute for the search input (requires isSearchable: true).
331
+ * The generated search input’s name. Only used when isSearchable is true.
307
332
  */
308
333
  name?: string;
309
334
  /**
310
- * Called when the active (highlighted) item changes. Receives the
311
- * same payload as onSubmitItem.
335
+ * Called whenever the highlighted item changes, with the same payload as
336
+ * onSubmitItem.
312
337
  */
313
338
  onActiveItem?: (payload: Item) => void;
339
+ /**
340
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
341
+ * Dropdown, which has no root element of its own.
342
+ */
314
343
  onClick?: (event: React.MouseEvent<HTMLElement>) => unknown;
344
+ /**
345
+ * Called after the dropdown closes. For a nested (submenu) Dropdown,
346
+ * called when its submenu closes.
347
+ */
315
348
  onClose?: () => unknown;
349
+ /**
350
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
351
+ * Dropdown, which has no root element of its own.
352
+ */
316
353
  onMouseDown?: (event: React.MouseEvent<HTMLElement>) => unknown;
354
+ /**
355
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
356
+ * Dropdown, which has no root element of its own.
357
+ */
317
358
  onMouseUp?: (event: React.MouseEvent<HTMLElement>) => unknown;
359
+ /**
360
+ * Called after the dropdown opens (on mount, when isOpenOnMount). For a
361
+ * nested (submenu) Dropdown, called when its submenu opens.
362
+ */
318
363
  onOpen?: () => unknown;
364
+ /**
365
+ * Called when an item is submitted (click, Enter, or Space). Parent items
366
+ * (submenus) disclose rather than submit, so this fires for leaf items
367
+ * only. For a nested (submenu) Dropdown, fires for submissions within its
368
+ * subtree only.
369
+ */
370
+ onSubmitItem?: (payload: Item) => void;
319
371
  /**
320
372
  * Opens the dropdown when the pointer hovers the trigger, and closes it a
321
373
  * short moment after the pointer leaves the trigger and body entirely (the
@@ -325,63 +377,158 @@ type Props = {
325
377
  */
326
378
  openOnHover?: boolean;
327
379
  /**
328
- * Called when an item is selected. The payload includes:
329
- * - element: The DOM element that was clicked
330
- * - event: The click or keyboard event
331
- * - label: The visible text of the item
332
- * - value: The value attribute or text content
380
+ * The generated search input’s placeholder. Only used when isSearchable is true.
333
381
  */
334
- onSubmitItem?: (payload: Item) => void;
382
+ placeholder?: string;
335
383
  /**
336
- * Placeholder text for the search input (requires isSearchable: true).
384
+ * Applied to the dropdown root element (for a nested (submenu) Dropdown,
385
+ * its item element). Also accepts the component’s CSS custom
386
+ * properties (e.g. `--uktdd-body-min-width`).
337
387
  */
338
- placeholder?: string;
339
- style?: React.CSSProperties;
388
+ style?: StyleWithCustomProperties;
340
389
  /**
341
- * Tab index for the search input (requires isSearchable: true).
390
+ * The generated search input’s tabIndex. Only used when isSearchable is true.
342
391
  */
343
392
  tabIndex?: number;
344
393
  /**
345
394
  * The dropdown’s controlled value. Pass a bare identifier when an item’s
346
395
  * stored value and its displayed label are the same, or a { label, value }
347
396
  * pair when they differ (e.g. a human-readable label shown for a stored
348
- * id) — the same { label, value } shape onSubmitItem reports back. Used for
349
- * change detection (skipping onSubmitItem when the already-selected item is
350
- * re-submitted); the label is shown as the search input’s value when
351
- * isSearchable is true. A bare identifier resolves to its label from the
352
- * matching child’s data-ukt-value in the body, so children whose value
353
- * and label differ need no explicit label.
397
+ * id) — the same { label, value } shape onSubmitItem reports back. The
398
+ * value is used for change detection (skipping onSubmitItem when the
399
+ * already-selected item is re-submitted); the label is shown as the search
400
+ * input’s value when isSearchable is true. A bare identifier is resolved
401
+ * to its label from the matching child’s data-ukt-value in the body — so
402
+ * children whose value and label differ need no explicit label; a
403
+ * { label, value } pair states it.
354
404
  */
355
405
  value?: ItemValue | string;
356
406
  };
357
407
  ```
358
408
 
409
+ `Props` is also exported as `DropdownProps`, the name that pairs with
410
+ `MenubarProps`. `StyleWithCustomProperties` is `React.CSSProperties`
411
+ extended to accept the component’s `--uktdd-*` custom properties, and is
412
+ the type of both components’ `style` prop.
413
+
359
414
  ### Item Types
360
415
 
361
416
  Both types are exported alongside the component:
362
417
 
363
418
  ```ts
364
419
  /**
365
- * A { label, value } pair naming an item: value is the stored value (the
366
- * item’s data-ukt-value) and label is its displayed text. Accepted by the
420
+ * A { label, value } pair naming an item: `value` is the stored value (the
421
+ * item’s data-ukt-value) and `label` is its displayed text. Accepted by the
367
422
  * value prop when an item’s value and label differ; also the shape of an
368
423
  * Item’s path entries.
369
424
  */
370
425
  type ItemValue = { label: string; value: string };
371
426
 
372
427
  type Item = {
428
+ /**
429
+ * The item element. Null when the submitted value came from a text
430
+ * input rather than an item (a created value with allowCreate, or an
431
+ * empty value with allowEmpty).
432
+ */
373
433
  element: HTMLElement | null;
434
+ /** The mouse, keyboard, or input event that activated or submitted the item. */
374
435
  event: Event | React.SyntheticEvent<HTMLElement>;
436
+ /** The item’s visible text (or the input’s text when element is null). */
375
437
  label: string;
376
438
  /**
377
439
  * Ancestor parent items from the root level down to the item’s
378
440
  * immediate parent. Empty for top-level items.
379
441
  */
380
442
  path: Array<ItemValue>;
443
+ /** The item’s data-ukt-value, or its label if it has none. */
381
444
  value: string;
382
445
  };
383
446
  ```
384
447
 
448
+ ### CSS custom properties
449
+
450
+ Every `--uktdd-*` custom property the component reads, with its default.
451
+ Set them on the dropdown’s root (a class, or the `style` prop, which
452
+ accepts them) or on an ancestor to theme a whole subtree. Anything not
453
+ listed here — the `--uktdd-anchor` and `--uktdd-submenu-anchor` anchor
454
+ names, and the `data-ukt-generated-id` attribute — is internal.
455
+
456
+ Colors (see [Color Scheme](#color-scheme)):
457
+
458
+ | Property | Default |
459
+ | ----------------------------------------- | ---------------------------------------------------- |
460
+ | `--uktdd-color-scheme` | `inherit` |
461
+ | `--uktdd-body-bg-color` | `Canvas` |
462
+ | `--uktdd-body-color` | `CanvasText` |
463
+ | `--uktdd-body-bg-color-hover` | `Highlight` |
464
+ | `--uktdd-body-color-hover` | `HighlightText` |
465
+ | `--uktdd-body-bg-color-selected` | `color-mix(in oklab, currentColor 6%, transparent)` |
466
+ | `--uktdd-body-bg-color-path` | 12% of the body color mixed into its background |
467
+ | `--uktdd-body-color-path` | `currentColor` |
468
+ | `--uktdd-body-border-color` | `color-mix(in oklab, currentColor 15%, transparent)` |
469
+ | `--uktdd-body-box-shadow` | `0 8px 18px rgba(0, 0, 0, 0.25)` |
470
+ | `--uktdd-menubar-trigger-bg-color-active` | `color-mix(in oklab, currentColor 12%, transparent)` |
471
+
472
+ Typography, sizing, and spacing:
473
+
474
+ | Property | Default |
475
+ | ------------------------------- | ----------------------------------------- |
476
+ | `--uktdd-font-family` | the system UI font stack |
477
+ | `--uktdd-body-buffer` | `10px` (viewport inset for the max sizes) |
478
+ | `--uktdd-body-min-height` | `30px` |
479
+ | `--uktdd-body-max-height` | `calc(100dvh - var(--uktdd-body-buffer))` |
480
+ | `--uktdd-body-min-width` | `min(50px, 100%)` |
481
+ | `--uktdd-body-max-width` | `calc(100dvw - var(--uktdd-body-buffer))` |
482
+ | `--uktdd-body-pad-block-start` | `0.5em` |
483
+ | `--uktdd-body-pad-block-end` | `0.5em` |
484
+ | `--uktdd-body-pad-inline-start` | `0.6875em` |
485
+ | `--uktdd-body-pad-inline-end` | `0.6875em` |
486
+ | `--uktdd-body-gap` | `0px` (trigger ↔ body) |
487
+ | `--uktdd-submenu-gap` | `0px` (parent item ↔ submenu) |
488
+ | `--uktdd-label-gap` | `0.625em` (`props.label` text ↔ trigger) |
489
+
490
+ Placement (see
491
+ [Placement Customization](#placement-customization-with-css-variables) and
492
+ [Submenu placement and styling](#submenu-placement-and-styling)):
493
+
494
+ | Property | Default |
495
+ | --------------------------------------- | -------------------------------------------------------- |
496
+ | `--uktdd-body-position-area` | `block-end span-inline-end` |
497
+ | `--uktdd-body-position-try-fallback-1` | `--uktdd-top-start` |
498
+ | `--uktdd-body-position-try-fallback-2` | `--uktdd-bottom-end` |
499
+ | `--uktdd-body-fill-fallbacks` | `--uktdd-fill-bottom, --uktdd-fill-top` |
500
+ | `--uktdd-submenu-position-area` | `inline-end span-block-end` |
501
+ | `--uktdd-submenu-position-try-fallback` | `--uktdd-submenu-inline-start` |
502
+ | `--uktdd-submenu-fill-fallbacks` | `--uktdd-submenu-fill, --uktdd-submenu-fill flip-inline` |
503
+
504
+ The `@position-try` placements the component ships, for use in the fallback
505
+ slots above: `--uktdd-top-start`, `--uktdd-top-end`,
506
+ `--uktdd-bottom-start`, `--uktdd-bottom-end`,
507
+ `--uktdd-submenu-inline-start`, the fills `--uktdd-fill-bottom`,
508
+ `--uktdd-fill-top`, `--uktdd-fill`, `--uktdd-fill-cover`, and
509
+ `--uktdd-submenu-fill`, and the no-op `--uktdd-noop`.
510
+
511
+ ### Class names and data attributes
512
+
513
+ The DOM the component renders is styleable by these class names, which are
514
+ part of the public API along with the `data-ukt-*` attributes:
515
+
516
+ - `.uktdropdown` — the root, with the state classes `.is-open`,
517
+ `.is-searchable`, and `.is-disabled`
518
+ - `.uktdropdown-trigger` — the generated trigger (a custom trigger keeps
519
+ its own classes)
520
+ - `.uktdropdown-label` and `.uktdropdown-label-text` — the wrapper and text
521
+ rendered for `props.label`
522
+ - `.uktdropdown-body` — the anchored popover shell, with `.has-items`
523
+ unless `hasItems={false}`
524
+ - `.uktdropdown-content` — the padded, scrollable region inside the body
525
+ - `.uktmenubar` — the [`Menubar`](#menubar) root
526
+ - `data-ukt-item` and `data-ukt-value` mark items, `data-ukt-submenu` marks
527
+ a [submenu](#the-data-ukt-submenu-protocol), and `data-ukt-active` marks
528
+ the highlighted item at each open level (the styling hook for the
529
+ highlight, alongside `aria-expanded` on open parent items and
530
+ `aria-selected` on the selected option)
531
+
385
532
  ## Usage Examples
386
533
 
387
534
  ### Basic List Dropdown
@@ -870,6 +1017,18 @@ Like items, submenus are ultimately declared in the DOM. A nested
870
1017
  </li>
871
1018
  ```
872
1019
 
1020
+ The parent item is an `<li>` when the nested `Dropdown` sits inside a
1021
+ `<ul>`, `<ol>`, or `<menu>`, and a `<div>` anywhere else (a body built from
1022
+ `<div data-ukt-item>` items, say), so once mounted it is valid HTML in
1023
+ either kind of body. The container is checked when the item enters the DOM,
1024
+ and for a non-list container the item then remounts as a `<div>` before
1025
+ paint — on every open, since the body unmounts on close. For a non-list
1026
+ body, pass `itemAs="div"` to name the element up front and skip the check:
1027
+ the item mounts once, the submenu body’s own mount effects run once, and
1028
+ server output is valid too (without `itemAs`, a server-rendered
1029
+ `isOpenOnMount` dropdown still ships the `<li>`). `className` and `style`
1030
+ on the nested `Dropdown` land on that element either way.
1031
+
873
1032
  You can author that markup directly instead of nesting a `Dropdown`
874
1033
  component; the two forms behave identically because the component form
875
1034
  compiles to the attribute form. Rules for direct authoring:
@@ -893,14 +1052,19 @@ Most props keep their meaning, scoped to the submenu:
893
1052
  - `onSubmitItem`: fires for submissions within that subtree only
894
1053
  - `onActiveItem`, `onOpen`, `onClose`: scoped to the submenu
895
1054
  - `disabled`: disables the parent item
896
- - `className`/`style`: applied to the parent item element
1055
+ - `className`/`style`: applied to the item element
1056
+ - `itemAs`: the item element (`'li'` or `'div'`), skipping the container
1057
+ check described above and recommended for a non-list body; submenu-only,
1058
+ and ignored (with a warning) anywhere else, including a nested
1059
+ `hasItems={false}` `Dropdown`
897
1060
 
898
1061
  Props that only make sense at the top level (`allowCreate`, `allowEmpty`,
899
1062
  `isOpenOnMount`, `isSearchable`, `keepOpenOnSubmit`, `name`, `openOnHover`,
900
- `placeholder`, `tabIndex`, `value`) are ignored on a nested dropdown and
901
- warn (unconditionally, matching the children-count misuse error). A submenu
902
- already discloses on hover intent — see [Submenus](#submenus) — so
903
- `openOnHover` has nothing to add there.
1063
+ `placeholder`, `tabIndex`, `value`, and the root-element event props
1064
+ `onClick`, `onMouseDown`, `onMouseUp`) are ignored on a nested dropdown and
1065
+ warn once per mount with `console.warn`, as the too-many-children message
1066
+ does. A submenu already discloses on hover intent — see
1067
+ [Submenus](#submenus) — so `openOnHover` has nothing to add there.
904
1068
 
905
1069
  ### Submenu placement and styling
906
1070
 
@@ -1172,9 +1336,9 @@ so pass `props.label` when you want the popup named as well.)
1172
1336
 
1173
1337
  Item roles are filled in the same way when the body opens: items receive
1174
1338
  `role="option"` in a searchable (listbox) dropdown or `role="menuitem"` in
1175
- a menu (always `menuitem` inside a submenu), and the `<ul>`/`<ol>` wrappers
1176
- around them receive `role="presentation"` so the listbox or menu owns its
1177
- items directly rather than through an intervening list. A natively
1339
+ a menu (always `menuitem` inside a submenu), and the `<ul>`/`<ol>`/`<menu>`
1340
+ wrappers around them receive `role="presentation"` so the listbox or menu
1341
+ owns its items directly rather than through an intervening list. A natively
1178
1342
  interactive item (a button, link, or input) or one with a role you set
1179
1343
  yourself keeps its own role.
1180
1344
 
@@ -1,13 +1,21 @@
1
1
  import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement, type ReactNode, type SyntheticEvent } from 'react';
2
2
  export type Item = {
3
+ /**
4
+ * The item element. Null when the submitted value came from a text
5
+ * input rather than an item (a created value with allowCreate, or an
6
+ * empty value with allowEmpty).
7
+ */
3
8
  element: MaybeHTMLElement;
9
+ /** The mouse, keyboard, or input event that activated or submitted the item. */
4
10
  event: Event | SyntheticEvent<HTMLElement>;
11
+ /** The item’s visible text (or the input’s text when element is null). */
5
12
  label: string;
6
13
  /**
7
14
  * Ancestor parent items from the root level down to the item’s
8
15
  * immediate parent. Empty for top-level items.
9
16
  */
10
17
  path: Array<ItemValue>;
18
+ /** The item’s data-ukt-value, or its label if it has none. */
11
19
  value: string;
12
20
  };
13
21
  /**
@@ -37,9 +45,15 @@ export type Props = {
37
45
  */
38
46
  allowEmpty?: boolean;
39
47
  /**
40
- * Can take a single React element or exactly two renderable children.
48
+ * Either a single React element (the dropdown body; the trigger is a
49
+ * generated button, or a generated search input when isSearchable) or
50
+ * exactly two renderable children: the trigger, then the body.
41
51
  */
42
52
  children: ChildrenTuple | ReactElement;
53
+ /**
54
+ * Applied to the dropdown root element. For a nested (submenu) Dropdown,
55
+ * applied to its item element instead.
56
+ */
43
57
  className?: string;
44
58
  /**
45
59
  * Prevents the dropdown from opening via user interaction: pointer,
@@ -52,9 +66,40 @@ export type Props = {
52
66
  * disabled doesn't override).
53
67
  */
54
68
  disabled?: boolean;
69
+ /**
70
+ * Whether the body is a list of selectable items (the default) or
71
+ * arbitrary interactive content such as a form. hasItems={false} turns
72
+ * off item selection and item keyboard navigation, gives the popup
73
+ * role="dialog" (unless isSearchable), and keeps it open on clicks
74
+ * inside the body. Defaults to true and isn’t inferred from the children.
75
+ */
55
76
  hasItems?: boolean;
77
+ /**
78
+ * Renders the dropdown already open on mount. Uncontrolled: it sets only
79
+ * the initial state.
80
+ */
56
81
  isOpenOnMount?: boolean;
82
+ /**
83
+ * Renders the trigger as a text input (a combobox) that filters the items
84
+ * as the user types, and the popup as a listbox.
85
+ */
57
86
  isSearchable?: boolean;
87
+ /**
88
+ * For a nested (submenu) Dropdown, its item element. When unset, it is
89
+ * an <li> inside a <ul>, <ol>, or <menu> and a <div> anywhere else,
90
+ * decided by checking its container once it’s in the DOM. Setting this
91
+ * skips that check, so the item mounts once and server output matches
92
+ * the client; recommended for a body that isn’t a list, which otherwise
93
+ * pays for the check on every open. Only a submenu Dropdown (one nested
94
+ * in a dropdown where hasItems is true) reads it; anywhere else it is
95
+ * ignored, with a warning.
96
+ */
97
+ itemAs?: 'div' | 'li';
98
+ /**
99
+ * Whether the dropdown stays open after an item is submitted, e.g. for a
100
+ * multi-select. Defaults to !hasItems: a menu closes on submit, while a
101
+ * hasItems={false} dialog stays open.
102
+ */
58
103
  keepOpenOnSubmit?: boolean;
59
104
  /**
60
105
  * Label content for the trigger button (when using single child syntax).
@@ -62,16 +107,45 @@ export type Props = {
62
107
  */
63
108
  label?: ReactNode;
64
109
  /**
65
- * Only usable in conjunction with {isSearchable: true}.
66
- * Used as search input’s name.
110
+ * The generated search input’s name. Only used when isSearchable is true.
67
111
  */
68
112
  name?: string;
113
+ /**
114
+ * Called whenever the highlighted item changes, with the same payload as
115
+ * onSubmitItem.
116
+ */
69
117
  onActiveItem?: (payload: Item) => void;
118
+ /**
119
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
120
+ * Dropdown, which has no root element of its own.
121
+ */
70
122
  onClick?: (event: ReactMouseEvent<HTMLElement>) => unknown;
123
+ /**
124
+ * Called after the dropdown closes. For a nested (submenu) Dropdown,
125
+ * called when its submenu closes.
126
+ */
71
127
  onClose?: () => unknown;
128
+ /**
129
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
130
+ * Dropdown, which has no root element of its own.
131
+ */
72
132
  onMouseDown?: (event: ReactMouseEvent<HTMLElement>) => unknown;
133
+ /**
134
+ * Applied to the dropdown root element. Ignored on a nested (submenu)
135
+ * Dropdown, which has no root element of its own.
136
+ */
73
137
  onMouseUp?: (event: ReactMouseEvent<HTMLElement>) => unknown;
138
+ /**
139
+ * Called after the dropdown opens (on mount, when isOpenOnMount). For a
140
+ * nested (submenu) Dropdown, called when its submenu opens.
141
+ */
74
142
  onOpen?: () => unknown;
143
+ /**
144
+ * Called when an item is submitted (click, Enter, or Space). Parent items
145
+ * (submenus) disclose rather than submit, so this fires for leaf items
146
+ * only. For a nested (submenu) Dropdown, fires for submissions within its
147
+ * subtree only.
148
+ */
75
149
  onSubmitItem?: (payload: Item) => void;
76
150
  /**
77
151
  * Opens the dropdown when the pointer hovers the trigger, and closes it a
@@ -82,19 +156,17 @@ export type Props = {
82
156
  */
83
157
  openOnHover?: boolean;
84
158
  /**
85
- * Only usable in conjunction with {isSearchable: true}.
86
- * Used as search input’s placeholder.
159
+ * The generated search input’s placeholder. Only used when isSearchable is true.
87
160
  */
88
161
  placeholder?: string;
89
162
  /**
90
- * Applied to the dropdown root element. Also accepts the component’s CSS
91
- * custom properties (e.g. `--uktdd-body-min-width`) for per-instance
92
- * placement and sizing, which plain `CSSProperties` rejects.
163
+ * Applied to the dropdown root element (for a nested (submenu) Dropdown,
164
+ * its item element). Also accepts the component’s CSS custom
165
+ * properties (e.g. `--uktdd-body-min-width`).
93
166
  */
94
- style?: CSSProperties & Record<`--${string}`, string | number | undefined>;
167
+ style?: StyleWithCustomProperties;
95
168
  /**
96
- * Only usable in conjunction with {isSearchable: true}.
97
- * Used as search input’s tabIndex.
169
+ * The generated search input’s tabIndex. Only used when isSearchable is true.
98
170
  */
99
171
  tabIndex?: number;
100
172
  /**
@@ -102,15 +174,22 @@ export type Props = {
102
174
  * stored value and its displayed label are the same, or a { label, value }
103
175
  * pair when they differ (e.g. a human-readable label shown for a stored
104
176
  * id) — the same { label, value } shape onSubmitItem reports back. The
105
- * value determines whether the value has changed, to avoid triggering
106
- * onSubmitItem when the already-selected item is re-submitted; the label is
107
- * used as the search input’s value when props.isSearchable === true. A bare
108
- * identifier is resolved to its label from the matching child’s
109
- * data-ukt-value in the body — so children whose value and label differ
110
- * need no explicit label; a { label, value } pair states it.
177
+ * value is used for change detection (skipping onSubmitItem when the
178
+ * already-selected item is re-submitted); the label is shown as the search
179
+ * input’s value when isSearchable is true. A bare identifier is resolved
180
+ * to its label from the matching child’s data-ukt-value in the body — so
181
+ * children whose value and label differ need no explicit label; a
182
+ * { label, value } pair states it.
111
183
  */
112
184
  value?: ItemValue | string;
113
185
  };
186
+ /** Alias of Props named to pair with MenubarProps. */
187
+ export type DropdownProps = Props;
188
+ /**
189
+ * A style object that also accepts the component’s CSS custom properties
190
+ * (e.g. `--uktdd-body-min-width`), which plain `CSSProperties` rejects.
191
+ */
192
+ export type StyleWithCustomProperties = CSSProperties & Record<`--${string}`, number | string | undefined>;
114
193
  type ChildrenTuple = [ReactNode, ReactNode] | readonly [ReactNode, ReactNode];
115
194
  type MaybeHTMLElement = HTMLElement | null;
116
195
  export default function Dropdown(props: Props): import("react").JSX.Element;