@acusti/dropdown 1.0.0-beta.1 → 1.0.0-beta.2
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 +125 -6
- package/dist/Dropdown.d.ts +10 -0
- package/dist/Dropdown.js +683 -518
- package/dist/Dropdown.js.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/helpers.d.ts +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -267,6 +267,16 @@ type Props = {
|
|
|
267
267
|
*/
|
|
268
268
|
children: ReactNode | [ReactNode, ReactNode];
|
|
269
269
|
className?: string;
|
|
270
|
+
/**
|
|
271
|
+
* Prevents the dropdown from opening via user interaction: pointer,
|
|
272
|
+
* Enter/Space, hover (props.openOnHover), typing in a custom trigger’s
|
|
273
|
+
* text input, and Menubar navigation onto it alike. The generated trigger
|
|
274
|
+
* carries the native disabled attribute; a custom trigger can be any
|
|
275
|
+
* element, so it receives aria-disabled instead. Inside a Menubar the
|
|
276
|
+
* member is skipped. A dropdown disabled while already open still closes
|
|
277
|
+
* normally (as does one started open with props.isOpenOnMount, which
|
|
278
|
+
* disabled doesn't override).
|
|
279
|
+
*/
|
|
270
280
|
disabled?: boolean;
|
|
271
281
|
/**
|
|
272
282
|
* Whether the dropdown contains items that can be selected.
|
|
@@ -1016,7 +1026,23 @@ import Dropdown, { Menubar } from '@acusti/dropdown';
|
|
|
1016
1026
|
|
|
1017
1027
|
Menubar behaviors:
|
|
1018
1028
|
|
|
1019
|
-
- renders `role="menubar"
|
|
1029
|
+
- renders `role="menubar"`, and each member’s trigger is a real
|
|
1030
|
+
`role="menuitem"` that the bar owns directly — the wrapper elements
|
|
1031
|
+
between them are neutralized with `role="none"`, since a generic element
|
|
1032
|
+
in between would leave the menubar owning no menu items at all. A
|
|
1033
|
+
searchable member is exempt from the `menuitem` role: its trigger is a
|
|
1034
|
+
`combobox`, so it keeps its own semantics rather than being mislabelled.
|
|
1035
|
+
Its wrappers are still neutralized, but note that a `combobox` isn’t
|
|
1036
|
+
valid menubar content in the first place — no markup arrangement fixes
|
|
1037
|
+
that, so a search field belongs **outside** the `<Menubar>` (a container
|
|
1038
|
+
mixing menus and other controls is a `toolbar`, not a menubar)
|
|
1039
|
+
- the bar’s **menu items share a single tab stop**, as the APG menubar
|
|
1040
|
+
pattern expects: one `menuitem` trigger is tabbable and the rest are
|
|
1041
|
+
reached with ←/→. The tab stop follows the trigger you last focused, so
|
|
1042
|
+
tabbing away and back returns you where you were; it starts on (and falls
|
|
1043
|
+
back to) the first enabled member. A searchable member sits outside this
|
|
1044
|
+
— it’s a `combobox`, not a menu item, so it keeps its own native tab stop
|
|
1045
|
+
and never takes the bar’s
|
|
1020
1046
|
- at most one menu in the bar is open at a time
|
|
1021
1047
|
- opening a trigger’s menu (by click or keyboard) engages the bar
|
|
1022
1048
|
(menu-mode). While engaged, hovering or focusing another trigger switches
|
|
@@ -1028,6 +1054,10 @@ Menubar behaviors:
|
|
|
1028
1054
|
- a deliberate dismissal — **Escape**, a click outside the bar, or
|
|
1029
1055
|
selecting an item — leaves menu-mode, after which hovering a trigger no
|
|
1030
1056
|
longer opens its menu until a menu is opened again to re-engage the bar
|
|
1057
|
+
- a `disabled` member is passed over rather than landed on, the way macOS
|
|
1058
|
+
skips a disabled menu: ←/→ move to the next enabled trigger, hovering it
|
|
1059
|
+
leaves the open menu alone, and if no other member is enabled the open
|
|
1060
|
+
menu simply stays put
|
|
1031
1061
|
- **←/→** move between menus, wrapping at the ends: with the bar focused
|
|
1032
1062
|
they move focus between triggers, and while a menu is open they slide the
|
|
1033
1063
|
open menu to the adjacent trigger — unless the active item is a parent
|
|
@@ -1067,7 +1097,8 @@ highlight into it.
|
|
|
1067
1097
|
- **Escape**: Close the dropdown entirely — including any open submenus —
|
|
1068
1098
|
and return focus to the trigger (macOS-style; use ← to back out one level
|
|
1069
1099
|
at a time)
|
|
1070
|
-
- **Arrow Up/Down**: Navigate between items in the current level
|
|
1100
|
+
- **Arrow Up/Down**: Navigate between items in the current level; with ⌥ or
|
|
1101
|
+
⌘ held, jump to the first/last item of that level
|
|
1071
1102
|
- **Arrow Right**: Open the highlighted parent item’s submenu (if not
|
|
1072
1103
|
already disclosed) and highlight its first item; in a
|
|
1073
1104
|
[`Menubar`](#menubar), if the highlighted item is a leaf, slide to the
|
|
@@ -1075,7 +1106,9 @@ highlight into it.
|
|
|
1075
1106
|
- **Arrow Left**: Close the highlighted item’s level and return to its
|
|
1076
1107
|
parent item; in a `Menubar` with the highlight at the top level of a
|
|
1077
1108
|
menu, slide to the previous menu
|
|
1078
|
-
- **Home/End**: Jump to first/last item in the current level
|
|
1109
|
+
- **Home/End**: Jump to first/last item in the current level. Like ←/→,
|
|
1110
|
+
they stand down while a text input has focus (a searchable dropdown’s own
|
|
1111
|
+
input, or one inside a custom trigger), where they move the caret instead
|
|
1079
1112
|
- **Type characters**: Jump to the best-matching item in the current level
|
|
1080
1113
|
|
|
1081
1114
|
For accessibility, the component focuses on semantic HTML structure and
|
|
@@ -1083,13 +1116,22 @@ keyboard navigation. It works best when you use appropriate HTML elements
|
|
|
1083
1116
|
in your dropdown content (like `<ul>` and `<li>` for lists, `<button>`
|
|
1084
1117
|
elements for actions, etc.).
|
|
1085
1118
|
|
|
1119
|
+
The ARIA it adds is a best effort from what it can control: it annotates
|
|
1120
|
+
the DOM it renders and the shapes it recognizes, but never overwrites a
|
|
1121
|
+
value you pass. So when you provide a completely custom trigger, it is
|
|
1122
|
+
possible to end up with ARIA that doesn’t hold together. The safest thing
|
|
1123
|
+
if you go full custom is to take over the ARIA as well.
|
|
1124
|
+
|
|
1086
1125
|
### ARIA attributes
|
|
1087
1126
|
|
|
1088
1127
|
The trigger automatically receives `aria-haspopup`, `aria-expanded`, and
|
|
1089
|
-
`aria-controls` pointing to the open body. The
|
|
1090
|
-
|
|
1128
|
+
`aria-controls` pointing to the open body. The component does its best to
|
|
1129
|
+
also apply the correct roles and attributes with custom triggers, but if
|
|
1130
|
+
you are customizing in that way, consider also providing the appropriate
|
|
1131
|
+
ARIA. The popup role is chosen based on the dropdown’s mode:
|
|
1091
1132
|
|
|
1092
|
-
- `aria-haspopup="listbox"` when `isSearchable` is true (combobox pattern
|
|
1133
|
+
- `aria-haspopup="listbox"` when `isSearchable` is true (combobox pattern —
|
|
1134
|
+
see below)
|
|
1093
1135
|
- `aria-haspopup="menu"` when `hasItems` is true (the default)
|
|
1094
1136
|
- `aria-haspopup="dialog"` when `hasItems={false}` (interactive content)
|
|
1095
1137
|
|
|
@@ -1098,6 +1140,34 @@ readers can associate the trigger with its popup. If your custom trigger
|
|
|
1098
1140
|
already specifies any of these ARIA props, your values win — the component
|
|
1099
1141
|
only fills in what you haven’t set.
|
|
1100
1142
|
|
|
1143
|
+
The body is also named, via `aria-labelledby`, by whatever names the
|
|
1144
|
+
trigger — required for the `dialog` role and worth having on the others, so
|
|
1145
|
+
the popup is announced as “Alignment menu” rather than a bare “menu”. The
|
|
1146
|
+
name comes from `props.label` when you pass one (always text, and the same
|
|
1147
|
+
name the trigger takes), and otherwise from the trigger element itself. The
|
|
1148
|
+
component fills in the trigger’s `id` for this; a custom trigger with an
|
|
1149
|
+
`id` of its own keeps it, and the body points at that instead.
|
|
1150
|
+
|
|
1151
|
+
Two shapes end up with no name, both because the trigger has none to give:
|
|
1152
|
+
|
|
1153
|
+
- **A text input trigger.** `aria-labelledby` resolves a text input to its
|
|
1154
|
+
_value_, so pointing the popup at one would name it after whatever the
|
|
1155
|
+
user has typed. The component leaves the popup unnamed instead — for a
|
|
1156
|
+
searchable dropdown’s trigger (a text input by definition, generated or
|
|
1157
|
+
custom) and for a custom trigger that is itself an `<input>` or
|
|
1158
|
+
`<textarea>`. A text input _nested_ inside a custom trigger leaks its
|
|
1159
|
+
value the same way but isn’t detectable from the element alone, so it’s
|
|
1160
|
+
on you to pass `props.label` there.
|
|
1161
|
+
- **An empty generated trigger**: single-child syntax with no `props.label`
|
|
1162
|
+
renders a `<button>` with nothing in it, which has no accessible name of
|
|
1163
|
+
its own to lend. This matters most for `hasItems={false}`, where the
|
|
1164
|
+
popup is a `dialog` and an unnamed dialog is an ARIA violation.
|
|
1165
|
+
|
|
1166
|
+
In both cases the trigger is unnamed too, and `props.label` fixes both at
|
|
1167
|
+
once. (A custom trigger’s own `aria-label` names the trigger, but the popup
|
|
1168
|
+
can’t inherit it — `aria-labelledby` would resolve the input to its value —
|
|
1169
|
+
so pass `props.label` when you want the popup named as well.)
|
|
1170
|
+
|
|
1101
1171
|
Item roles are filled in the same way when the body opens: items receive
|
|
1102
1172
|
`role="option"` in a searchable (listbox) dropdown or `role="menuitem"` in
|
|
1103
1173
|
a menu (always `menuitem` inside a submenu), and the `<ul>`/`<ol>` wrappers
|
|
@@ -1115,6 +1185,55 @@ which case the component leaves selection ARIA entirely to you, as with any
|
|
|
1115
1185
|
ARIA you set. The persistent tint on the selected item is themeable via the
|
|
1116
1186
|
`--uktdd-body-bg-color-selected` custom property.
|
|
1117
1187
|
|
|
1188
|
+
### The searchable trigger is a combobox
|
|
1189
|
+
|
|
1190
|
+
A searchable dropdown’s trigger declares `role="combobox"` together with
|
|
1191
|
+
`aria-autocomplete="list"`, which describes what typing actually does here:
|
|
1192
|
+
suggestions appear in the popup, and nothing is completed inline in the
|
|
1193
|
+
input.
|
|
1194
|
+
|
|
1195
|
+
For a custom trigger, the role is filled in only when the trigger element
|
|
1196
|
+
is a single-line text input. A trigger that merely wraps an input is left
|
|
1197
|
+
alone. Same with a `<textarea>`, since a combobox is single-line. (A
|
|
1198
|
+
textarea trigger still counts as a text input for naming, so it can’t name
|
|
1199
|
+
the popup either.) As always, a `role` or `aria-autocomplete` you set
|
|
1200
|
+
yourself wins.
|
|
1201
|
+
|
|
1202
|
+
### The active item and `aria-activedescendant`
|
|
1203
|
+
|
|
1204
|
+
DOM focus stays on the trigger for as long as the dropdown is open — the
|
|
1205
|
+
highlight moves via a `data-ukt-active` attribute rather than by moving
|
|
1206
|
+
focus. On its own that would make navigation silent to a screen reader, so
|
|
1207
|
+
the trigger carries `aria-activedescendant` pointing at the highlighted
|
|
1208
|
+
item, and it is kept in sync however the highlight moves: arrow keys,
|
|
1209
|
+
Home/End, typeahead, hover, diving into and out of submenus, and the
|
|
1210
|
+
reveal-on-open above. It’s removed when nothing is highlighted and when the
|
|
1211
|
+
dropdown closes, so it never dangles at an item that has unmounted.
|
|
1212
|
+
|
|
1213
|
+
Pointing at an item means items need ids, and the component mints them from
|
|
1214
|
+
the same `useId` behind the trigger and body ids, suffixed with the item’s
|
|
1215
|
+
**index path** — its index at each level from the root level down. So
|
|
1216
|
+
`<bodyId>-2-1` is the second item of the submenu belonging to the third
|
|
1217
|
+
top-level item. An item with an `id` of your own keeps it and is pointed at
|
|
1218
|
+
as-is.
|
|
1219
|
+
|
|
1220
|
+
Ids are minted when an item becomes active rather than in the open-time
|
|
1221
|
+
annotation pass, which means items rendered into an already-open body —
|
|
1222
|
+
async-loaded, or filtered by your own search — get one too, even though
|
|
1223
|
+
that pass doesn’t reach them.
|
|
1224
|
+
|
|
1225
|
+
Because an index path is a snapshot of a position, a generated id is
|
|
1226
|
+
**re-derived every time its item becomes active**, so filtering or
|
|
1227
|
+
reordering an open body doesn’t leave items holding ids that describe where
|
|
1228
|
+
they used to be. Treat generated ids as internal: they’re minted and
|
|
1229
|
+
consumed within a single open, and they move as your list moves. Ids you
|
|
1230
|
+
set yourself are never touched or recomputed.
|
|
1231
|
+
|
|
1232
|
+
The indices come from the navigable items, so a disabled item shifts the
|
|
1233
|
+
ids after it. If the highlighted item leaves the DOM entirely — filtered
|
|
1234
|
+
away while the body is open — the reference is cleared rather than left
|
|
1235
|
+
dangling.
|
|
1236
|
+
|
|
1118
1237
|
Submenus and menubars extend the same pattern automatically:
|
|
1119
1238
|
|
|
1120
1239
|
- parent items receive `aria-haspopup="menu"` and `aria-expanded`, and
|
package/dist/Dropdown.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export type Props = {
|
|
|
41
41
|
*/
|
|
42
42
|
children: ChildrenTuple | ReactElement;
|
|
43
43
|
className?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Prevents the dropdown from opening via user interaction: pointer,
|
|
46
|
+
* Enter/Space, hover (props.openOnHover), typing in a custom trigger’s
|
|
47
|
+
* text input, and Menubar navigation onto it alike. The generated trigger
|
|
48
|
+
* carries the native disabled attribute; a custom trigger can be any
|
|
49
|
+
* element, so it receives aria-disabled instead. Inside a Menubar the
|
|
50
|
+
* member is skipped. A dropdown disabled while already open still closes
|
|
51
|
+
* normally (as does one started open with props.isOpenOnMount, which
|
|
52
|
+
* disabled doesn't override).
|
|
53
|
+
*/
|
|
44
54
|
disabled?: boolean;
|
|
45
55
|
hasItems?: boolean;
|
|
46
56
|
isOpenOnMount?: boolean;
|