@acusti/dropdown 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +129 -30
- package/dist/Dropdown.d.ts +19 -5
- package/dist/Dropdown.js +523 -426
- package/dist/Dropdown.js.map +1 -1
- package/dist/helpers.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,13 +123,17 @@ CSS-first sizing model:
|
|
|
123
123
|
- The dropdown body is an anchored shell
|
|
124
124
|
- The inner content region becomes scrollable only when the content exceeds
|
|
125
125
|
the available space
|
|
126
|
-
-
|
|
126
|
+
- The body opens in its authored direction whenever it fits there, falling
|
|
127
|
+
back through the other placements in order and only squeezing-to-scroll
|
|
128
|
+
when it fits nowhere at its natural size (see
|
|
129
|
+
[Changing the Default Direction](#changing-the-default-direction))
|
|
127
130
|
|
|
128
131
|
This means the dropdown tends to:
|
|
129
132
|
|
|
130
133
|
- stay content-sized when the contents are small
|
|
131
|
-
-
|
|
132
|
-
-
|
|
134
|
+
- open in the direction you asked for, as long as that side can hold it
|
|
135
|
+
- expand to the available viewport space, and become scrollable, only when
|
|
136
|
+
the contents exceed the room the chosen side has
|
|
133
137
|
|
|
134
138
|
Internally, the dropdown renders:
|
|
135
139
|
|
|
@@ -292,16 +296,33 @@ type Props = {
|
|
|
292
296
|
*/
|
|
293
297
|
tabIndex?: number;
|
|
294
298
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
299
|
+
* The dropdown’s controlled value. Pass a bare identifier when an item’s
|
|
300
|
+
* stored value and its displayed label are the same, or a { label, value }
|
|
301
|
+
* pair when they differ (e.g. a human-readable label shown for a stored
|
|
302
|
+
* id) — the same { label, value } shape onSubmitItem reports back. Used for
|
|
303
|
+
* change detection (skipping onSubmitItem when the already-selected item is
|
|
304
|
+
* re-submitted); the label is shown as the search input’s value when
|
|
305
|
+
* isSearchable is true. A bare identifier resolves to its label from the
|
|
306
|
+
* matching child’s data-ukt-value in the body, so children whose value
|
|
307
|
+
* and label differ need no explicit label.
|
|
297
308
|
*/
|
|
298
|
-
value?: string;
|
|
309
|
+
value?: ItemValue | string;
|
|
299
310
|
};
|
|
300
311
|
```
|
|
301
312
|
|
|
302
|
-
### Item
|
|
313
|
+
### Item Types
|
|
314
|
+
|
|
315
|
+
Both types are exported alongside the component:
|
|
303
316
|
|
|
304
317
|
```ts
|
|
318
|
+
/**
|
|
319
|
+
* A { label, value } pair naming an item: value is the stored value (the
|
|
320
|
+
* item’s data-ukt-value) and label is its displayed text. Accepted by the
|
|
321
|
+
* value prop when an item’s value and label differ; also the shape of an
|
|
322
|
+
* Item’s path entries.
|
|
323
|
+
*/
|
|
324
|
+
type ItemValue = { label: string; value: string };
|
|
325
|
+
|
|
305
326
|
type Item = {
|
|
306
327
|
element: HTMLElement | null;
|
|
307
328
|
event: Event | React.SyntheticEvent<HTMLElement>;
|
|
@@ -310,7 +331,7 @@ type Item = {
|
|
|
310
331
|
* Ancestor parent items from the root level down to the item’s
|
|
311
332
|
* immediate parent. Empty for top-level items.
|
|
312
333
|
*/
|
|
313
|
-
path: Array<
|
|
334
|
+
path: Array<ItemValue>;
|
|
314
335
|
value: string;
|
|
315
336
|
};
|
|
316
337
|
```
|
|
@@ -514,9 +535,9 @@ Example:
|
|
|
514
535
|
|
|
515
536
|
```css
|
|
516
537
|
.settings-dropdown {
|
|
517
|
-
--uktdd-body-position-area:
|
|
538
|
+
--uktdd-body-position-area: block-end span-inline-start;
|
|
518
539
|
--uktdd-body-position-try-fallbacks:
|
|
519
|
-
--uktdd-top-
|
|
540
|
+
--uktdd-top-end, --uktdd-bottom-start, --uktdd-top-start;
|
|
520
541
|
--uktdd-body-gap: 8px;
|
|
521
542
|
}
|
|
522
543
|
|
|
@@ -528,6 +549,65 @@ Example:
|
|
|
528
549
|
This approach keeps the public React API small while still allowing precise
|
|
529
550
|
placement and sizing control when a product surface needs it.
|
|
530
551
|
|
|
552
|
+
### Changing the Default Direction
|
|
553
|
+
|
|
554
|
+
`--uktdd-body-position-area` and `--uktdd-body-position-try-fallbacks`
|
|
555
|
+
always change together. `--uktdd-body-position-area` is the primary
|
|
556
|
+
placement, used whenever it fits in the viewport — that’s what actually
|
|
557
|
+
determines the direction a dropdown opens by default. The fallbacks list is
|
|
558
|
+
only consulted when the primary placement doesn’t fit, so overriding it
|
|
559
|
+
alone changes nothing in the common case where the primary already fits.
|
|
560
|
+
Overriding the primary alone works, but leaves behind a fallback cascade
|
|
561
|
+
tuned for the old primary — in a cramped viewport, the dropdown can fall
|
|
562
|
+
back toward the direction you just moved away from.
|
|
563
|
+
|
|
564
|
+
The four `@position-try` blocks the component ships (`--uktdd-top-start`,
|
|
565
|
+
`--uktdd-top-end`, `--uktdd-bottom-start`, `--uktdd-bottom-end`) are named
|
|
566
|
+
for the aligned edge, not the direction the body extends toward: `start`
|
|
567
|
+
opens with the body’s inline-start edge flush with the trigger’s
|
|
568
|
+
inline-start edge (extending toward inline-end), and `end` is the mirror
|
|
569
|
+
image. This also means they’re RTL-safe — `start`/`end` follow the
|
|
570
|
+
document’s writing direction the way `left`/`right` wouldn’t. The values
|
|
571
|
+
pair `block-start`/`block-end` with `span-inline-start`/`span-inline-end`
|
|
572
|
+
rather than `top`/`bottom` — `position-area` doesn’t allow mixing a
|
|
573
|
+
physical keyword on one axis with a logical one on the other — though
|
|
574
|
+
`block-start`/`block-end` read as `top`/`bottom` in the near-universal
|
|
575
|
+
horizontal-tb writing mode.
|
|
576
|
+
|
|
577
|
+
Pick the row matching the direction you want, and set both variables to its
|
|
578
|
+
pair:
|
|
579
|
+
|
|
580
|
+
| Direction | `--uktdd-body-position-area` | `--uktdd-body-position-try-fallbacks` |
|
|
581
|
+
| ----------------------------------- | ------------------------------- | ------------------------------------------------------------- |
|
|
582
|
+
| Bottom, start-aligned (the default) | `block-end span-inline-end` | `--uktdd-top-start, --uktdd-bottom-end, --uktdd-top-end` |
|
|
583
|
+
| Bottom, end-aligned | `block-end span-inline-start` | `--uktdd-top-end, --uktdd-bottom-start, --uktdd-top-start` |
|
|
584
|
+
| Top, start-aligned | `block-start span-inline-end` | `--uktdd-bottom-start, --uktdd-top-end, --uktdd-bottom-end` |
|
|
585
|
+
| Top, end-aligned | `block-start span-inline-start` | `--uktdd-bottom-end, --uktdd-top-start, --uktdd-bottom-start` |
|
|
586
|
+
|
|
587
|
+
For example, to make a dropdown open upward and end-aligned (useful for a
|
|
588
|
+
trigger pinned to the bottom of the viewport, near the inline-end edge):
|
|
589
|
+
|
|
590
|
+
```css
|
|
591
|
+
.bottom-toolbar-dropdown {
|
|
592
|
+
--uktdd-body-position-area: block-start span-inline-start;
|
|
593
|
+
--uktdd-body-position-try-fallbacks:
|
|
594
|
+
--uktdd-bottom-end, --uktdd-top-start, --uktdd-bottom-start;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
See the [`DirectionRecipes` story][] for a live demo of all four.
|
|
599
|
+
|
|
600
|
+
[`DirectionRecipes` story]:
|
|
601
|
+
https://uikit.acusti.ca/?path=/docs/uikit-controls-Dropdown--docs#direction-recipes
|
|
602
|
+
|
|
603
|
+
The primary placement wins whenever the body fits there at its natural
|
|
604
|
+
size, then the fallbacks are tried in order — the body opens in the
|
|
605
|
+
direction you asked for as long as that side can hold it, and never flips
|
|
606
|
+
to the opposite side just because the viewport happens to have more empty
|
|
607
|
+
space there. Only when the body fits in none of those placements does it
|
|
608
|
+
size itself to the block-axis space its preferred side has and let the
|
|
609
|
+
content scroll, so it never overflows the viewport or covers the trigger.
|
|
610
|
+
|
|
531
611
|
Use `--uktdd-body-gap` for the space between the trigger and the body. It
|
|
532
612
|
is applied as a symmetric `margin-block`, so the gap lands on whichever
|
|
533
613
|
side the body attaches to and auto-reverses when `position-try` flips the
|
|
@@ -548,9 +628,9 @@ the menu size itself from its contents.
|
|
|
548
628
|
|
|
549
629
|
```css
|
|
550
630
|
.avatar-menu {
|
|
551
|
-
--uktdd-body-position-area:
|
|
631
|
+
--uktdd-body-position-area: block-end span-inline-start;
|
|
552
632
|
--uktdd-body-position-try-fallbacks:
|
|
553
|
-
--uktdd-top-
|
|
633
|
+
--uktdd-top-end, --uktdd-top-start, --uktdd-bottom-end;
|
|
554
634
|
}
|
|
555
635
|
```
|
|
556
636
|
|
|
@@ -570,8 +650,9 @@ tile. A `center` tile is only as wide as the trigger, so a body wider than
|
|
|
570
650
|
the trigger always overflows it — and `position-try` refuses to select a
|
|
571
651
|
fallback that overflows, so a `bottom center` → `top center` setup never
|
|
572
652
|
flips. `span-all` centers over the trigger identically but spans the full
|
|
573
|
-
width, so it flips cleanly and clamps to the viewport near an edge
|
|
574
|
-
|
|
653
|
+
width, so it flips cleanly and clamps to the viewport near an edge. The
|
|
654
|
+
menu opens below by default and flips above only when it doesn’t fit below;
|
|
655
|
+
list the flipped fallback so that flip is available:
|
|
575
656
|
|
|
576
657
|
```css
|
|
577
658
|
.centered-menu {
|
|
@@ -620,7 +701,7 @@ Submenu semantics:
|
|
|
620
701
|
|
|
621
702
|
- Parent items disclose; they never submit. `onSubmitItem` fires only for
|
|
622
703
|
leaf items, and the payload’s `path` reports the leaf’s ancestor parent
|
|
623
|
-
items (see the [`Item` type](#item-
|
|
704
|
+
items (see the [`Item` type](#item-types) above), so leaves with the same
|
|
624
705
|
value in different submenus are distinguishable.
|
|
625
706
|
- Submenus nest to arbitrary depth — a submenu body can itself contain
|
|
626
707
|
nested dropdowns.
|
|
@@ -647,7 +728,7 @@ Like items, submenus are ultimately declared in the DOM. A nested
|
|
|
647
728
|
```html
|
|
648
729
|
<li data-ukt-item aria-haspopup="menu" aria-expanded="false">
|
|
649
730
|
Align
|
|
650
|
-
<ul data-ukt-submenu role="menu">
|
|
731
|
+
<ul data-ukt-submenu role="menu" popover="manual">
|
|
651
732
|
…
|
|
652
733
|
</ul>
|
|
653
734
|
</li>
|
|
@@ -659,6 +740,9 @@ compiles to the attribute form. Rules for direct authoring:
|
|
|
659
740
|
|
|
660
741
|
- the `data-ukt-submenu` element must be a descendant of its parent item
|
|
661
742
|
element
|
|
743
|
+
- the submenu discloses in the top layer as a popover; the component sets
|
|
744
|
+
`popover="manual"`, and the engine adds it if your markup omits it, so
|
|
745
|
+
you don’t manage `display` (visibility follows the popover open state)
|
|
662
746
|
- in a level whose items include a submenu, mark all of that level’s items
|
|
663
747
|
explicitly with `data-ukt-item` or `data-ukt-value`; purely flat levels —
|
|
664
748
|
including flat submenu bodies — fall back to the automatic item-detection
|
|
@@ -686,8 +770,12 @@ already discloses on hover intent — see [Submenus](#submenus) — so
|
|
|
686
770
|
|
|
687
771
|
Submenus reuse the dropdown’s anchor-positioning layout model: the expanded
|
|
688
772
|
parent item is the anchor, and the submenu opens at its inline-end,
|
|
689
|
-
top-aligned, falling back to the opposite side when out of bounds.
|
|
690
|
-
|
|
773
|
+
top-aligned, falling back to the opposite side when out of bounds. Like the
|
|
774
|
+
body, a submenu renders in the top layer (`popover="manual"`), so its
|
|
775
|
+
containing block is the viewport — no ancestor (including the body’s own
|
|
776
|
+
top-layer box) can capture it and clip or shift its placement, which keeps
|
|
777
|
+
it flush to the parent item’s edge across browsers. Placement custom
|
|
778
|
+
properties follow the established naming scheme:
|
|
691
779
|
|
|
692
780
|
- `--uktdd-submenu-position-area` (default: `inline-end span-block-end`)
|
|
693
781
|
- `--uktdd-submenu-position-try-fallbacks`
|
|
@@ -709,18 +797,12 @@ theming menu colors.
|
|
|
709
797
|
|
|
710
798
|
The active path is fully styleable: `data-ukt-active` marks one item per
|
|
711
799
|
open level (a parent item stays active while you navigate its submenu), and
|
|
712
|
-
`aria-expanded="true"` marks open parent items.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
[aria-expanded='true'] > [data-ukt-submenu] {
|
|
721
|
-
display: block;
|
|
722
|
-
}
|
|
723
|
-
```
|
|
800
|
+
`aria-expanded="true"` marks open parent items. Submenu visibility is not a
|
|
801
|
+
`display` rule you own: each submenu is a `popover="manual"` element the
|
|
802
|
+
component shows and hides (via `showPopover`/`hidePopover`) as its parent
|
|
803
|
+
item expands and collapses, so it discloses in the top layer and the UA’s
|
|
804
|
+
`:popover-open` state controls whether it renders. Don’t set `display` on
|
|
805
|
+
`[data-ukt-submenu]` — it would fight that state.
|
|
724
806
|
|
|
725
807
|
Following macOS, the default styles distinguish the deepest active item —
|
|
726
808
|
the one keyboard input operates on — from its ancestors on the active path.
|
|
@@ -879,6 +961,23 @@ readers can associate the trigger with its popup. If your custom trigger
|
|
|
879
961
|
already specifies any of these ARIA props, your values win — the component
|
|
880
962
|
only fills in what you haven’t set.
|
|
881
963
|
|
|
964
|
+
Item roles are filled in the same way when the body opens: items receive
|
|
965
|
+
`role="option"` in a searchable (listbox) dropdown or `role="menuitem"` in
|
|
966
|
+
a menu (always `menuitem` inside a submenu), and the `<ul>`/`<ol>` wrappers
|
|
967
|
+
around them receive `role="presentation"` so the listbox or menu owns its
|
|
968
|
+
items directly rather than through an intervening list. A natively
|
|
969
|
+
interactive item (a button, link, or input) or one with a role you set
|
|
970
|
+
yourself keeps its own role.
|
|
971
|
+
|
|
972
|
+
Opening also reveals the current selection: the item matching `props.value`
|
|
973
|
+
becomes the active item (so keyboard navigation starts from the current
|
|
974
|
+
selection) and is scrolled into view. In a searchable (listbox) dropdown it
|
|
975
|
+
additionally receives `aria-selected="true"` (`aria-selected` isn’t valid
|
|
976
|
+
on a `menuitem`) — unless you author `aria-selected` on items yourself, in
|
|
977
|
+
which case the component leaves selection ARIA entirely to you, as with any
|
|
978
|
+
ARIA you set. The persistent tint on the selected item is themeable via the
|
|
979
|
+
`--uktdd-body-bg-color-selected` custom property.
|
|
980
|
+
|
|
882
981
|
Submenus and menubars extend the same pattern automatically:
|
|
883
982
|
|
|
884
983
|
- parent items receive `aria-haspopup="menu"` and `aria-expanded`, and
|
package/dist/Dropdown.d.ts
CHANGED
|
@@ -7,10 +7,16 @@ export type Item = {
|
|
|
7
7
|
* Ancestor parent items from the root level down to the item’s
|
|
8
8
|
* immediate parent. Empty for top-level items.
|
|
9
9
|
*/
|
|
10
|
-
path: Array<
|
|
10
|
+
path: Array<ItemValue>;
|
|
11
11
|
value: string;
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A { label, value } pair naming an item: `value` is the stored value (the
|
|
15
|
+
* item’s data-ukt-value) and `label` is its displayed text. Accepted by the
|
|
16
|
+
* value prop when an item’s value and label differ; also the shape of an
|
|
17
|
+
* Item’s path entries.
|
|
18
|
+
*/
|
|
19
|
+
export type ItemValue = {
|
|
14
20
|
label: string;
|
|
15
21
|
value: string;
|
|
16
22
|
};
|
|
@@ -82,10 +88,18 @@ export type Props = {
|
|
|
82
88
|
*/
|
|
83
89
|
tabIndex?: number;
|
|
84
90
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
91
|
+
* The dropdown’s controlled value. Pass a bare identifier when an item’s
|
|
92
|
+
* stored value and its displayed label are the same, or a { label, value }
|
|
93
|
+
* pair when they differ (e.g. a human-readable label shown for a stored
|
|
94
|
+
* id) — the same { label, value } shape onSubmitItem reports back. The
|
|
95
|
+
* value determines whether the value has changed, to avoid triggering
|
|
96
|
+
* onSubmitItem when the already-selected item is re-submitted; the label is
|
|
97
|
+
* used as the search input’s value when props.isSearchable === true. A bare
|
|
98
|
+
* identifier is resolved to its label from the matching child’s
|
|
99
|
+
* data-ukt-value in the body — so children whose value and label differ
|
|
100
|
+
* need no explicit label; a { label, value } pair states it.
|
|
87
101
|
*/
|
|
88
|
-
value?: string;
|
|
102
|
+
value?: ItemValue | string;
|
|
89
103
|
};
|
|
90
104
|
type ChildrenTuple = [ReactNode, ReactNode] | readonly [ReactNode, ReactNode];
|
|
91
105
|
type MaybeHTMLElement = HTMLElement | null;
|