@primitiv-ui/web 0.1.56 → 0.1.57
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 +38 -4
- package/dist/cdn/chunks/chunk-P2K4DPA4.js +1 -0
- package/dist/cdn/dropdown.js +1 -0
- package/dist/cdn/popover.js +1 -1
- package/dist/cdn/tooltip.js +1 -1
- package/dist/custom-elements.json +909 -0
- package/dist/define/dropdown.d.ts +1 -0
- package/dist/define/dropdown.js +12 -0
- package/dist/define/index.d.ts +1 -0
- package/dist/define/index.js +1 -0
- package/dist/dropdown/DropdownController.d.ts +63 -0
- package/dist/dropdown/DropdownController.js +130 -0
- package/dist/dropdown/DropdownSubController.d.ts +34 -0
- package/dist/dropdown/DropdownSubController.js +78 -0
- package/dist/dropdown/elements.d.ts +174 -0
- package/dist/dropdown/elements.js +501 -0
- package/dist/dropdown/index.d.ts +3 -0
- package/dist/dropdown/index.js +3 -0
- package/dist/frameworks/events.d.ts +18 -0
- package/dist/frameworks/react.d.ts +46 -2
- package/dist/frameworks/svelte.d.ts +45 -1
- package/dist/frameworks/vue.d.ts +50 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/popover/PopoverController.d.ts +6 -0
- package/dist/popover/PopoverController.js +41 -1
- package/dist/primitiv-web.js +1 -1
- package/dist/tags.d.ts +15 -0
- package/dist/tooltip/TooltipController.d.ts +9 -1
- package/dist/tooltip/TooltipController.js +36 -2
- package/dist/tooltip/elements.js +1 -0
- package/package.json +10 -2
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { defineElements, ElementBase, reportOnce, write } from "../internal/element.js";
|
|
2
|
+
import { PrimitivRootElement } from "../internal/RootElement.js";
|
|
3
|
+
import { DropdownController } from "./DropdownController.js";
|
|
4
|
+
import { DropdownSubController } from "./DropdownSubController.js";
|
|
5
|
+
// Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineDropdown registry test instead.
|
|
6
|
+
const ROOT = "primitiv-dropdown";
|
|
7
|
+
const TRIGGER = "primitiv-dropdown-trigger";
|
|
8
|
+
const CONTENT = "primitiv-dropdown-content";
|
|
9
|
+
const ITEM = "primitiv-dropdown-item";
|
|
10
|
+
const CHECKBOX_ITEM = "primitiv-dropdown-checkbox-item";
|
|
11
|
+
const RADIO_GROUP = "primitiv-dropdown-radio-group";
|
|
12
|
+
const RADIO_ITEM = "primitiv-dropdown-radio-item";
|
|
13
|
+
const INDICATOR = "primitiv-dropdown-item-indicator";
|
|
14
|
+
const GROUP = "primitiv-dropdown-group";
|
|
15
|
+
const LABEL = "primitiv-dropdown-label";
|
|
16
|
+
const SEPARATOR = "primitiv-dropdown-separator";
|
|
17
|
+
const SUB = "primitiv-dropdown-sub";
|
|
18
|
+
const SUB_TRIGGER = "primitiv-dropdown-sub-trigger";
|
|
19
|
+
const SUB_CONTENT = "primitiv-dropdown-sub-content";
|
|
20
|
+
/** Every part that is a chosen item: the plain, checkbox and radio items. */
|
|
21
|
+
const ITEMS = `${ITEM},${CHECKBOX_ITEM},${RADIO_ITEM}`;
|
|
22
|
+
// Stryker restore StringLiteral
|
|
23
|
+
/** The one element child an interactive part decorates, or `null`, reported once, without exactly one. */
|
|
24
|
+
function decoratable(part, what) {
|
|
25
|
+
if (part.childElementCount === 1)
|
|
26
|
+
return part.firstElementChild;
|
|
27
|
+
reportOnce(part, `<${part.localName}> needs exactly one element child (${what}) to decorate.`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** Fire a bubbling, cancelable event on `target`; whether no listener vetoed it. */
|
|
31
|
+
function announceOn(target, type, detail) {
|
|
32
|
+
return target.dispatchEvent(new CustomEvent(type, { bubbles: true, cancelable: true, detail }));
|
|
33
|
+
}
|
|
34
|
+
/** A checkbox item's state, from its `checked` attribute: absent, present, or `"indeterminate"`. */
|
|
35
|
+
function checkedState(part) {
|
|
36
|
+
const value = part.getAttribute("checked");
|
|
37
|
+
return value === "indeterminate" ? value : value !== null;
|
|
38
|
+
}
|
|
39
|
+
/** A radio item's state: whether its value is its group's. */
|
|
40
|
+
function radioChecked(part) {
|
|
41
|
+
return part.closest(RADIO_GROUP)?.getAttribute("value") === part.getAttribute("value");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `<primitiv-dropdown>` — the state owner of a dropdown menu (WAI-ARIA APG
|
|
45
|
+
* Menu Button): a trigger that opens a menu of items. The menu is a
|
|
46
|
+
* `popover="auto"` in the top layer, shown with the trigger as its source,
|
|
47
|
+
* which makes the trigger its implicit anchor for CSS anchor positioning.
|
|
48
|
+
* Choosing an item — any item, a submenu's too — closes the menu and hands
|
|
49
|
+
* focus back to the trigger.
|
|
50
|
+
*
|
|
51
|
+
* The root finds its own parts and reconciles their ARIA from its reflected
|
|
52
|
+
* state whenever a part arrives, leaves, or changes an attribute the root
|
|
53
|
+
* reads (the lifecycle is `PrimitivRootElement`'s).
|
|
54
|
+
*
|
|
55
|
+
* @customElement primitiv-dropdown
|
|
56
|
+
* @summary The state owner of a dropdown menu.
|
|
57
|
+
* @attr {boolean} open - Whether the menu is open. Reflected: it changes as the user opens and dismisses it. Set it (or the `open` property) to open or close it from script.
|
|
58
|
+
* @fires {CustomEvent<{ open: boolean }>} primitiv-dropdown-change - Bubbles, cancelable, fired before a user-driven change is applied — the trigger, Escape, a press outside or a chosen item — with the open state afterwards. Call `preventDefault()` to veto it.
|
|
59
|
+
* @fires {CustomEvent<null>} primitiv-dropdown-select - Bubbles, cancelable, fired on the item part when an item is chosen, after a checkbox or radio item has changed. Call `preventDefault()` to keep the menu open.
|
|
60
|
+
* @fires {CustomEvent<{ checked: boolean }>} primitiv-dropdown-checked-change - Bubbles, cancelable, fired on a checkbox item before it changes, with its state afterwards. Call `preventDefault()` to veto it.
|
|
61
|
+
* @fires {CustomEvent<{ value: string }>} primitiv-dropdown-value-change - Bubbles, cancelable, fired on a radio group before its value changes, with the value afterwards. Call `preventDefault()` to veto it.
|
|
62
|
+
*/
|
|
63
|
+
export class PrimitivDropdownElement extends PrimitivRootElement {
|
|
64
|
+
/** The menu the root currently decorates, and the controller over it. */
|
|
65
|
+
#content = null;
|
|
66
|
+
#menu = null;
|
|
67
|
+
/** The decorated trigger, as of the last reconcile. */
|
|
68
|
+
#trigger = null;
|
|
69
|
+
/** The root's own listeners, while connected. */
|
|
70
|
+
#listening = null;
|
|
71
|
+
controller = {
|
|
72
|
+
connect: () => this.#connect(),
|
|
73
|
+
disconnect: () => this.#disconnect(),
|
|
74
|
+
};
|
|
75
|
+
reconcileOn = ["open", "disabled", "checked", "value"];
|
|
76
|
+
/** Whether the menu is open, reflected as the boolean `open` attribute. */
|
|
77
|
+
get open() {
|
|
78
|
+
return this.hasAttribute("open");
|
|
79
|
+
}
|
|
80
|
+
set open(next) {
|
|
81
|
+
this.toggleAttribute("open", next);
|
|
82
|
+
}
|
|
83
|
+
/** The controller's view of this element: state lives in the reflected attribute. */
|
|
84
|
+
#host() {
|
|
85
|
+
return {
|
|
86
|
+
state: () => ({ open: this.open }),
|
|
87
|
+
change: (open) => this.#change(open),
|
|
88
|
+
trigger: () => this.#trigger,
|
|
89
|
+
source: () => this.#trigger,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** A change the user asked for: announced first, so a listener can veto it. */
|
|
93
|
+
#change(open) {
|
|
94
|
+
if (open === this.open)
|
|
95
|
+
return;
|
|
96
|
+
if (this.announce("primitiv-dropdown-change", { open }))
|
|
97
|
+
this.open = open;
|
|
98
|
+
// A veto can leave the menu out of step — the browser is light-dismissing
|
|
99
|
+
// it — so put it back where the state says, once that dismissal is done.
|
|
100
|
+
else
|
|
101
|
+
queueMicrotask(() => this.#menu?.sync());
|
|
102
|
+
}
|
|
103
|
+
#connect() {
|
|
104
|
+
this.#listening = new AbortController();
|
|
105
|
+
const options = { signal: this.#listening.signal };
|
|
106
|
+
this.addEventListener("click", (event) => this.#click(event), options);
|
|
107
|
+
this.addEventListener("pointerover", (event) => this.#highlight(event, true), options);
|
|
108
|
+
this.addEventListener("pointerout", (event) => this.#highlight(event, false), options);
|
|
109
|
+
this.#menu?.connect();
|
|
110
|
+
}
|
|
111
|
+
#disconnect() {
|
|
112
|
+
// A root disconnects only after it connected, so it is listening.
|
|
113
|
+
this.#listening.abort();
|
|
114
|
+
this.#listening = null;
|
|
115
|
+
this.#menu?.disconnect();
|
|
116
|
+
}
|
|
117
|
+
/** The item part (of this root) an event landed in, or `null`. */
|
|
118
|
+
#itemOf(target) {
|
|
119
|
+
const part = target?.closest(ITEMS) ?? null;
|
|
120
|
+
return part?.closest(ROOT) === this ? part : null;
|
|
121
|
+
}
|
|
122
|
+
/** Mark the item the pointer is over, as React's `data-highlighted`. */
|
|
123
|
+
#highlight(event, over) {
|
|
124
|
+
const part = this.#itemOf(event.target);
|
|
125
|
+
// The relatedTarget test keeps the highlight steady as the pointer crosses
|
|
126
|
+
// the row's own children. user-event sets no relatedTarget, so only a real
|
|
127
|
+
// browser can tell: the interop lab's dropdown spec counts the flips.
|
|
128
|
+
// Stryker disable next-line ConditionalExpression,LogicalOperator: see above — unobservable in jsdom, proven in Chromium.
|
|
129
|
+
if (!part || part === this.#itemOf(event.relatedTarget))
|
|
130
|
+
return;
|
|
131
|
+
const child = part.firstElementChild;
|
|
132
|
+
write(child, "data-highlighted", over ? "" : null);
|
|
133
|
+
}
|
|
134
|
+
#click(event) {
|
|
135
|
+
const target = event.target;
|
|
136
|
+
if (this.#trigger?.contains(target)) {
|
|
137
|
+
this.#change(!this.open);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const part = this.#itemOf(target);
|
|
141
|
+
if (!part || part.hasAttribute("disabled"))
|
|
142
|
+
return;
|
|
143
|
+
if (part.localName === CHECKBOX_ITEM) {
|
|
144
|
+
const checked = checkedState(part) !== true;
|
|
145
|
+
// `write`, not `toggleAttribute`: a mixed item's "indeterminate" must become plain checked.
|
|
146
|
+
if (announceOn(part, "primitiv-dropdown-checked-change", { checked }))
|
|
147
|
+
write(part, "checked", checked ? "" : null);
|
|
148
|
+
}
|
|
149
|
+
else if (part.localName === RADIO_ITEM && !radioChecked(part)) {
|
|
150
|
+
const group = part.closest(RADIO_GROUP);
|
|
151
|
+
const value = part.getAttribute("value");
|
|
152
|
+
if (announceOn(group, "primitiv-dropdown-value-change", { value }))
|
|
153
|
+
group.setAttribute("value", value);
|
|
154
|
+
}
|
|
155
|
+
if (!announceOn(part, "primitiv-dropdown-select", null))
|
|
156
|
+
return;
|
|
157
|
+
this.#change(false);
|
|
158
|
+
this.#trigger?.focus();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Decorate `content` from now on: the controller follows it. Only a
|
|
162
|
+
* reconcile calls this, and a root reconciles only while connected, so the
|
|
163
|
+
* new controller listens at once.
|
|
164
|
+
*/
|
|
165
|
+
#use(content) {
|
|
166
|
+
// Stryker disable next-line ConditionalExpression: equivalent — rebuilding the controller over the same menu swaps one listener set for an identical one.
|
|
167
|
+
if (content === this.#content)
|
|
168
|
+
return;
|
|
169
|
+
this.#menu?.disconnect();
|
|
170
|
+
this.#content = content;
|
|
171
|
+
this.#menu = content ? new DropdownController(content, this.#host()) : null;
|
|
172
|
+
this.#menu?.connect();
|
|
173
|
+
}
|
|
174
|
+
reconcile() {
|
|
175
|
+
const open = this.open;
|
|
176
|
+
const rootId = this.rootId();
|
|
177
|
+
const [content = null] = this.parts(CONTENT);
|
|
178
|
+
const [triggerPart] = this.parts(TRIGGER);
|
|
179
|
+
this.#trigger = triggerPart ? decoratable(triggerPart, "a <button>") : null;
|
|
180
|
+
if (content) {
|
|
181
|
+
if (!content.id)
|
|
182
|
+
content.id = `${rootId}-content`;
|
|
183
|
+
write(content, "role", "menu");
|
|
184
|
+
if (!content.hasAttribute("popover"))
|
|
185
|
+
content.setAttribute("popover", "auto");
|
|
186
|
+
}
|
|
187
|
+
if (this.#trigger) {
|
|
188
|
+
write(this.#trigger, "aria-haspopup", "menu");
|
|
189
|
+
write(this.#trigger, "aria-expanded", String(open));
|
|
190
|
+
write(this.#trigger, "aria-controls", content ? content.id : null);
|
|
191
|
+
}
|
|
192
|
+
for (const part of this.parts(ITEMS)) {
|
|
193
|
+
const child = decoratable(part, "an <li>");
|
|
194
|
+
if (!child)
|
|
195
|
+
continue;
|
|
196
|
+
const checkbox = part.localName === CHECKBOX_ITEM;
|
|
197
|
+
const radio = part.localName === RADIO_ITEM;
|
|
198
|
+
const checked = checkbox ? checkedState(part) : radio && radioChecked(part);
|
|
199
|
+
write(child, "role", checkbox ? "menuitemcheckbox" : radio ? "menuitemradio" : "menuitem");
|
|
200
|
+
write(child, "tabindex", "-1");
|
|
201
|
+
write(child, "aria-disabled", part.hasAttribute("disabled") ? "true" : null);
|
|
202
|
+
write(child, "aria-checked", checkbox || radio ? (checked === "indeterminate" ? "mixed" : String(checked)) : null);
|
|
203
|
+
}
|
|
204
|
+
for (const indicator of this.parts(INDICATOR)) {
|
|
205
|
+
const item = indicator.closest(`${CHECKBOX_ITEM},${RADIO_ITEM}`);
|
|
206
|
+
const checked = item?.localName === CHECKBOX_ITEM ? checkedState(item) : item ? radioChecked(item) : false;
|
|
207
|
+
write(indicator, "data-state", checked === "indeterminate" ? checked : checked ? "checked" : "unchecked");
|
|
208
|
+
write(indicator, "hidden", checked || indicator.hasAttribute("force-mount") ? null : "");
|
|
209
|
+
}
|
|
210
|
+
let labels = 0;
|
|
211
|
+
for (const label of this.parts(LABEL)) {
|
|
212
|
+
if (!label.id)
|
|
213
|
+
label.id = `${rootId}-label-${++labels}`;
|
|
214
|
+
}
|
|
215
|
+
for (const group of this.parts(GROUP)) {
|
|
216
|
+
const label = [...group.querySelectorAll(LABEL)].find((candidate) => candidate.closest(GROUP) === group);
|
|
217
|
+
write(group, "role", "group");
|
|
218
|
+
write(group, "aria-labelledby", label ? label.id : null);
|
|
219
|
+
}
|
|
220
|
+
for (const group of this.parts(RADIO_GROUP))
|
|
221
|
+
write(group, "role", "group");
|
|
222
|
+
for (const separator of this.parts(SEPARATOR))
|
|
223
|
+
write(separator, "role", "separator");
|
|
224
|
+
this.#use(content);
|
|
225
|
+
this.#menu?.sync();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* `<primitiv-dropdown-sub>` — the state owner of a submenu, inside a dropdown's
|
|
230
|
+
* menu: a sub-trigger row that opens a nested menu on a click, a hover, or the
|
|
231
|
+
* arrow pointing away from its parent (ArrowRight, or ArrowLeft right to
|
|
232
|
+
* left), and closes it on the arrow back, or when the pointer moves to another
|
|
233
|
+
* row of the parent menu.
|
|
234
|
+
*
|
|
235
|
+
* @customElement primitiv-dropdown-sub
|
|
236
|
+
* @summary The state owner of a submenu.
|
|
237
|
+
* @attr {boolean} open - Whether the submenu is open. Reflected.
|
|
238
|
+
* @fires {CustomEvent<{ open: boolean }>} primitiv-dropdown-sub-change - Bubbles, cancelable, fired before a user-driven change is applied, with the open state afterwards. Call `preventDefault()` to veto it.
|
|
239
|
+
*/
|
|
240
|
+
export class PrimitivDropdownSubElement extends PrimitivRootElement {
|
|
241
|
+
#content = null;
|
|
242
|
+
#menu = null;
|
|
243
|
+
#trigger = null;
|
|
244
|
+
/** The sub-trigger part (whose `disabled` the sub reads), as of the last reconcile. */
|
|
245
|
+
#triggerPart = null;
|
|
246
|
+
/** Whether the pointer is over the sub-trigger. */
|
|
247
|
+
#hovered = false;
|
|
248
|
+
#listening = null;
|
|
249
|
+
controller = {
|
|
250
|
+
connect: () => this.#connect(),
|
|
251
|
+
disconnect: () => this.#disconnect(),
|
|
252
|
+
};
|
|
253
|
+
reconcileOn = ["open", "disabled"];
|
|
254
|
+
/** Whether the submenu is open, reflected as the boolean `open` attribute. */
|
|
255
|
+
get open() {
|
|
256
|
+
return this.hasAttribute("open");
|
|
257
|
+
}
|
|
258
|
+
set open(next) {
|
|
259
|
+
this.toggleAttribute("open", next);
|
|
260
|
+
}
|
|
261
|
+
#host() {
|
|
262
|
+
return {
|
|
263
|
+
state: () => ({ open: this.open }),
|
|
264
|
+
change: (open) => this.#change(open),
|
|
265
|
+
trigger: () => this.#trigger,
|
|
266
|
+
source: () => this.#trigger,
|
|
267
|
+
closeKey: () => (this.direction() === "rtl" ? "ArrowRight" : "ArrowLeft"),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
#change(open) {
|
|
271
|
+
if (open === this.open)
|
|
272
|
+
return;
|
|
273
|
+
if (this.announce("primitiv-dropdown-sub-change", { open }))
|
|
274
|
+
this.open = open;
|
|
275
|
+
else
|
|
276
|
+
queueMicrotask(() => this.#menu?.sync());
|
|
277
|
+
}
|
|
278
|
+
/** Whether the sub-trigger is disabled. Only asked once a decorated sub-trigger exists. */
|
|
279
|
+
#disabled() {
|
|
280
|
+
return this.#triggerPart.hasAttribute("disabled");
|
|
281
|
+
}
|
|
282
|
+
#connect() {
|
|
283
|
+
this.#listening = new AbortController();
|
|
284
|
+
const options = { signal: this.#listening.signal };
|
|
285
|
+
this.addEventListener("click", (event) => this.#onTrigger(event) && this.#openIfEnabled(), options);
|
|
286
|
+
this.addEventListener("keydown", (event) => this.#keydown(event), options);
|
|
287
|
+
this.addEventListener("pointerover", (event) => this.#pointer(event, true), options);
|
|
288
|
+
this.addEventListener("pointerout", (event) => this.#pointer(event, false), options);
|
|
289
|
+
document.addEventListener("pointerover", (event) => this.#sibling(event), options);
|
|
290
|
+
this.#menu?.connect();
|
|
291
|
+
}
|
|
292
|
+
#disconnect() {
|
|
293
|
+
this.#listening.abort();
|
|
294
|
+
this.#listening = null;
|
|
295
|
+
this.#menu?.disconnect();
|
|
296
|
+
}
|
|
297
|
+
/** Whether an event landed on the decorated sub-trigger. */
|
|
298
|
+
#onTrigger(event) {
|
|
299
|
+
return this.#trigger?.contains(event.target) ?? false;
|
|
300
|
+
}
|
|
301
|
+
#openIfEnabled() {
|
|
302
|
+
if (!this.#disabled())
|
|
303
|
+
this.#change(true);
|
|
304
|
+
}
|
|
305
|
+
#keydown(event) {
|
|
306
|
+
if (!this.#onTrigger(event) || this.#disabled())
|
|
307
|
+
return;
|
|
308
|
+
if (event.key !== (this.direction() === "rtl" ? "ArrowLeft" : "ArrowRight"))
|
|
309
|
+
return;
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
event.stopPropagation();
|
|
312
|
+
this.#change(true);
|
|
313
|
+
}
|
|
314
|
+
#pointer(event, over) {
|
|
315
|
+
// Past the first test there is a decorated trigger. The relatedTarget test
|
|
316
|
+
// keeps the hover steady across the row's own children, which only a real
|
|
317
|
+
// browser can show (see #highlight).
|
|
318
|
+
// Stryker disable next-line ConditionalExpression,LogicalOperator: unobservable in jsdom, proven in Chromium by the interop lab.
|
|
319
|
+
if (!this.#onTrigger(event) || this.#trigger.contains(event.relatedTarget))
|
|
320
|
+
return;
|
|
321
|
+
this.#hovered = over;
|
|
322
|
+
this.reconcile();
|
|
323
|
+
if (over)
|
|
324
|
+
this.#openIfEnabled();
|
|
325
|
+
}
|
|
326
|
+
/** The pointer reached another row of the parent menu: close, as moving the keyboard there would. */
|
|
327
|
+
#sibling(event) {
|
|
328
|
+
if (!this.open || this.contains(event.target))
|
|
329
|
+
return;
|
|
330
|
+
const row = event.target.closest('[role^="menuitem"]');
|
|
331
|
+
// A connected sub always has a parent; only a row of its parent's menu closes it.
|
|
332
|
+
if (row && row.closest('[role="menu"]') === this.parentElement.closest('[role="menu"]'))
|
|
333
|
+
this.#change(false);
|
|
334
|
+
}
|
|
335
|
+
#use(content) {
|
|
336
|
+
// Stryker disable next-line ConditionalExpression: equivalent — rebuilding the controller over the same menu swaps one listener set for an identical one.
|
|
337
|
+
if (content === this.#content)
|
|
338
|
+
return;
|
|
339
|
+
this.#menu?.disconnect();
|
|
340
|
+
this.#content = content;
|
|
341
|
+
this.#menu = content ? new DropdownSubController(content, this.#host()) : null;
|
|
342
|
+
this.#menu?.connect();
|
|
343
|
+
}
|
|
344
|
+
reconcile() {
|
|
345
|
+
const open = this.open;
|
|
346
|
+
const [content = null] = this.parts(SUB_CONTENT);
|
|
347
|
+
const [triggerPart = null] = this.parts(SUB_TRIGGER);
|
|
348
|
+
this.#triggerPart = triggerPart;
|
|
349
|
+
this.#trigger = triggerPart ? decoratable(triggerPart, "an <li>") : null;
|
|
350
|
+
if (content) {
|
|
351
|
+
if (!content.id)
|
|
352
|
+
content.id = `${this.rootId()}-content`;
|
|
353
|
+
write(content, "role", "menu");
|
|
354
|
+
if (!content.hasAttribute("popover"))
|
|
355
|
+
content.setAttribute("popover", "auto");
|
|
356
|
+
}
|
|
357
|
+
if (this.#trigger) {
|
|
358
|
+
write(this.#trigger, "role", "menuitem");
|
|
359
|
+
write(this.#trigger, "tabindex", "-1");
|
|
360
|
+
write(this.#trigger, "aria-haspopup", "menu");
|
|
361
|
+
write(this.#trigger, "aria-expanded", String(open));
|
|
362
|
+
write(this.#trigger, "aria-controls", content ? content.id : null);
|
|
363
|
+
write(this.#trigger, "aria-disabled", this.#disabled() ? "true" : null);
|
|
364
|
+
write(this.#trigger, "data-highlighted", this.#hovered || open ? "" : null);
|
|
365
|
+
}
|
|
366
|
+
this.#use(content);
|
|
367
|
+
this.#menu?.sync();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* `<primitiv-dropdown-trigger>` — an interactive part. It decorates exactly
|
|
372
|
+
* one child the consumer writes, a `<button>`, which toggles the menu and is
|
|
373
|
+
* its menu button (`aria-haspopup`, `aria-expanded`, `aria-controls`).
|
|
374
|
+
*
|
|
375
|
+
* @customElement primitiv-dropdown-trigger
|
|
376
|
+
*/
|
|
377
|
+
export class PrimitivDropdownTriggerElement extends ElementBase {
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* `<primitiv-dropdown-content>` — the menu itself. Structural: the root gives
|
|
381
|
+
* it an id, `role="menu"` and `popover="auto"` (write it in your markup too, so
|
|
382
|
+
* it is hidden before the script loads).
|
|
383
|
+
*
|
|
384
|
+
* @customElement primitiv-dropdown-content
|
|
385
|
+
*/
|
|
386
|
+
export class PrimitivDropdownContentElement extends ElementBase {
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* `<primitiv-dropdown-item>` — an interactive part: one item. It decorates
|
|
390
|
+
* exactly one child the consumer writes, an `<li>` (or a link), which gets
|
|
391
|
+
* `role="menuitem"`, `tabindex="-1"` and `data-highlighted` while the pointer
|
|
392
|
+
* is over it.
|
|
393
|
+
*
|
|
394
|
+
* @customElement primitiv-dropdown-item
|
|
395
|
+
* @attr {boolean} disabled - Skipped by the keyboard, and does nothing when clicked (`aria-disabled` on the child).
|
|
396
|
+
*/
|
|
397
|
+
export class PrimitivDropdownItemElement extends ElementBase {
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* `<primitiv-dropdown-checkbox-item>` — an item that toggles. Its child gets
|
|
401
|
+
* `role="menuitemcheckbox"` and `aria-checked`.
|
|
402
|
+
*
|
|
403
|
+
* @customElement primitiv-dropdown-checkbox-item
|
|
404
|
+
* @attr {boolean} checked - Whether it is checked; `checked="indeterminate"` for mixed. Reflected.
|
|
405
|
+
* @attr {boolean} disabled - As on an item.
|
|
406
|
+
*/
|
|
407
|
+
export class PrimitivDropdownCheckboxItemElement extends ElementBase {
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* `<primitiv-dropdown-radio-group>` — a group of radio items, one of which is
|
|
411
|
+
* checked. Structural: `role="group"`.
|
|
412
|
+
*
|
|
413
|
+
* @customElement primitiv-dropdown-radio-group
|
|
414
|
+
* @attr {string} value - The checked item's value. Reflected.
|
|
415
|
+
*/
|
|
416
|
+
export class PrimitivDropdownRadioGroupElement extends ElementBase {
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* `<primitiv-dropdown-radio-item>` — an item checked when its value is its
|
|
420
|
+
* group's. Its child gets `role="menuitemradio"` and `aria-checked`.
|
|
421
|
+
*
|
|
422
|
+
* @customElement primitiv-dropdown-radio-item
|
|
423
|
+
* @attr {string} value - The value it sets on its group.
|
|
424
|
+
* @attr {boolean} disabled - As on an item.
|
|
425
|
+
*/
|
|
426
|
+
export class PrimitivDropdownRadioItemElement extends ElementBase {
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* `<primitiv-dropdown-item-indicator>` — the check or dot inside a checkbox or
|
|
430
|
+
* radio item, `hidden` while unchecked. Structural: it gets `data-state`.
|
|
431
|
+
*
|
|
432
|
+
* @customElement primitiv-dropdown-item-indicator
|
|
433
|
+
* @attr {boolean} force-mount - Keep it shown while unchecked, to animate it.
|
|
434
|
+
*/
|
|
435
|
+
export class PrimitivDropdownItemIndicatorElement extends ElementBase {
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* `<primitiv-dropdown-group>` — a labelled group of items. Structural:
|
|
439
|
+
* `role="group"`, named by the label inside it.
|
|
440
|
+
*
|
|
441
|
+
* @customElement primitiv-dropdown-group
|
|
442
|
+
*/
|
|
443
|
+
export class PrimitivDropdownGroupElement extends ElementBase {
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* `<primitiv-dropdown-label>` — a group's label, not an item. Structural: the
|
|
447
|
+
* root gives it an id.
|
|
448
|
+
*
|
|
449
|
+
* @customElement primitiv-dropdown-label
|
|
450
|
+
*/
|
|
451
|
+
export class PrimitivDropdownLabelElement extends ElementBase {
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* `<primitiv-dropdown-separator>` — a rule between items. Structural:
|
|
455
|
+
* `role="separator"`.
|
|
456
|
+
*
|
|
457
|
+
* @customElement primitiv-dropdown-separator
|
|
458
|
+
*/
|
|
459
|
+
export class PrimitivDropdownSeparatorElement extends ElementBase {
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* `<primitiv-dropdown-sub-trigger>` — the row that opens a submenu. It
|
|
463
|
+
* decorates exactly one child, an `<li>`, which gets `role="menuitem"` and the
|
|
464
|
+
* submenu's `aria-haspopup`, `aria-expanded` and `aria-controls`.
|
|
465
|
+
*
|
|
466
|
+
* @customElement primitiv-dropdown-sub-trigger
|
|
467
|
+
* @attr {boolean} disabled - It does not open the submenu.
|
|
468
|
+
*/
|
|
469
|
+
export class PrimitivDropdownSubTriggerElement extends ElementBase {
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* `<primitiv-dropdown-sub-content>` — the submenu itself, inside the parent
|
|
473
|
+
* menu. Structural, as the content: an id, `role="menu"`, `popover="auto"`.
|
|
474
|
+
*
|
|
475
|
+
* @customElement primitiv-dropdown-sub-content
|
|
476
|
+
*/
|
|
477
|
+
export class PrimitivDropdownSubContentElement extends ElementBase {
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Define the fourteen Dropdown elements on `registry` (the global
|
|
481
|
+
* `customElements` by default). Safe to call more than once, and a no-op where
|
|
482
|
+
* there is no registry (a server render).
|
|
483
|
+
*/
|
|
484
|
+
export function defineDropdown(registry = globalThis.customElements) {
|
|
485
|
+
defineElements(registry, [
|
|
486
|
+
[ROOT, PrimitivDropdownElement],
|
|
487
|
+
[TRIGGER, PrimitivDropdownTriggerElement],
|
|
488
|
+
[CONTENT, PrimitivDropdownContentElement],
|
|
489
|
+
[ITEM, PrimitivDropdownItemElement],
|
|
490
|
+
[CHECKBOX_ITEM, PrimitivDropdownCheckboxItemElement],
|
|
491
|
+
[RADIO_GROUP, PrimitivDropdownRadioGroupElement],
|
|
492
|
+
[RADIO_ITEM, PrimitivDropdownRadioItemElement],
|
|
493
|
+
[INDICATOR, PrimitivDropdownItemIndicatorElement],
|
|
494
|
+
[GROUP, PrimitivDropdownGroupElement],
|
|
495
|
+
[LABEL, PrimitivDropdownLabelElement],
|
|
496
|
+
[SEPARATOR, PrimitivDropdownSeparatorElement],
|
|
497
|
+
[SUB, PrimitivDropdownSubElement],
|
|
498
|
+
[SUB_TRIGGER, PrimitivDropdownSubTriggerElement],
|
|
499
|
+
[SUB_CONTENT, PrimitivDropdownSubContentElement],
|
|
500
|
+
]);
|
|
501
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DropdownController, type DropdownControllerHost } from "./DropdownController.js";
|
|
2
|
+
export { DropdownSubController, type DropdownSubControllerHost } from "./DropdownSubController.js";
|
|
3
|
+
export { defineDropdown, PrimitivDropdownCheckboxItemElement, PrimitivDropdownContentElement, PrimitivDropdownElement, PrimitivDropdownGroupElement, PrimitivDropdownItemElement, PrimitivDropdownItemIndicatorElement, PrimitivDropdownLabelElement, PrimitivDropdownRadioGroupElement, PrimitivDropdownRadioItemElement, PrimitivDropdownSeparatorElement, PrimitivDropdownSubContentElement, PrimitivDropdownSubElement, PrimitivDropdownSubTriggerElement, PrimitivDropdownTriggerElement, } from "./elements.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DropdownController } from "./DropdownController.js";
|
|
2
|
+
export { DropdownSubController } from "./DropdownSubController.js";
|
|
3
|
+
export { defineDropdown, PrimitivDropdownCheckboxItemElement, PrimitivDropdownContentElement, PrimitivDropdownElement, PrimitivDropdownGroupElement, PrimitivDropdownItemElement, PrimitivDropdownItemIndicatorElement, PrimitivDropdownLabelElement, PrimitivDropdownRadioGroupElement, PrimitivDropdownRadioItemElement, PrimitivDropdownSeparatorElement, PrimitivDropdownSubContentElement, PrimitivDropdownSubElement, PrimitivDropdownSubTriggerElement, PrimitivDropdownTriggerElement, } from "./elements.js";
|
|
@@ -27,3 +27,21 @@ export type PrimitivPopoverChangeEvent = CustomEvent<{
|
|
|
27
27
|
export type PrimitivTooltipChangeEvent = CustomEvent<{
|
|
28
28
|
open: boolean;
|
|
29
29
|
}>;
|
|
30
|
+
/** The `primitiv-dropdown-change` event: bubbles, cancelable, fired before the change applies, with the open state afterwards. */
|
|
31
|
+
export type PrimitivDropdownChangeEvent = CustomEvent<{
|
|
32
|
+
open: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
/** The `primitiv-dropdown-sub-change` event: bubbles, cancelable, fired before a submenu's change applies, with its open state afterwards. */
|
|
35
|
+
export type PrimitivDropdownSubChangeEvent = CustomEvent<{
|
|
36
|
+
open: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
/** The `primitiv-dropdown-select` event: bubbles, cancelable, fired on an item as it is chosen; cancel it to keep the menu open. */
|
|
39
|
+
export type PrimitivDropdownSelectEvent = CustomEvent<null>;
|
|
40
|
+
/** The `primitiv-dropdown-checked-change` event: bubbles, cancelable, fired on a checkbox item before it changes, with its state afterwards. */
|
|
41
|
+
export type PrimitivDropdownCheckedChangeEvent = CustomEvent<{
|
|
42
|
+
checked: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/** The `primitiv-dropdown-value-change` event: bubbles, cancelable, fired on a radio group before its value changes, with the value afterwards. */
|
|
45
|
+
export type PrimitivDropdownValueChangeEvent = CustomEvent<{
|
|
46
|
+
value: string;
|
|
47
|
+
}>;
|
|
@@ -20,16 +20,17 @@
|
|
|
20
20
|
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
21
21
|
import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "../accordion/elements.js";
|
|
22
22
|
import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "../collapsible/elements.js";
|
|
23
|
+
import type { PrimitivDropdownCheckboxItemElement, PrimitivDropdownContentElement, PrimitivDropdownElement, PrimitivDropdownGroupElement, PrimitivDropdownItemElement, PrimitivDropdownItemIndicatorElement, PrimitivDropdownLabelElement, PrimitivDropdownRadioGroupElement, PrimitivDropdownRadioItemElement, PrimitivDropdownSeparatorElement, PrimitivDropdownSubContentElement, PrimitivDropdownSubElement, PrimitivDropdownSubTriggerElement, PrimitivDropdownTriggerElement } from "../dropdown/elements.js";
|
|
23
24
|
import type { PrimitivModalCloseElement, PrimitivModalContentElement, PrimitivModalDescriptionElement, PrimitivModalElement, PrimitivModalTitleElement, PrimitivModalTriggerElement } from "../modal/elements.js";
|
|
24
25
|
import type { PrimitivPopoverAnchorElement, PrimitivPopoverCloseElement, PrimitivPopoverContentElement, PrimitivPopoverDescriptionElement, PrimitivPopoverElement, PrimitivPopoverTitleElement, PrimitivPopoverTriggerElement } from "../popover/elements.js";
|
|
25
26
|
import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "../tabs/elements.js";
|
|
26
27
|
import type { PrimitivTooltipArrowElement, PrimitivTooltipContentElement, PrimitivTooltipElement, PrimitivTooltipProviderElement, PrimitivTooltipTriggerElement } from "../tooltip/elements.js";
|
|
27
|
-
import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivPopoverChangeEvent, PrimitivTabsChangeEvent, PrimitivTooltipChangeEvent } from "./events.js";
|
|
28
|
+
import type { PrimitivAccordionChangeEvent, PrimitivDropdownChangeEvent, PrimitivDropdownCheckedChangeEvent, PrimitivDropdownSelectEvent, PrimitivDropdownSubChangeEvent, PrimitivDropdownValueChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivPopoverChangeEvent, PrimitivTabsChangeEvent, PrimitivTooltipChangeEvent } from "./events.js";
|
|
28
29
|
/** React's own props for an element, plus `class`, which React 19 passes to custom elements. */
|
|
29
30
|
type ElementProps<E extends HTMLElement> = DetailedHTMLProps<HTMLAttributes<E>, E> & {
|
|
30
31
|
class?: string;
|
|
31
32
|
};
|
|
32
|
-
export type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivPopoverChangeEvent, PrimitivTabsChangeEvent, PrimitivTooltipChangeEvent, };
|
|
33
|
+
export type { PrimitivAccordionChangeEvent, PrimitivDropdownChangeEvent, PrimitivDropdownCheckedChangeEvent, PrimitivDropdownSelectEvent, PrimitivDropdownSubChangeEvent, PrimitivDropdownValueChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivPopoverChangeEvent, PrimitivTabsChangeEvent, PrimitivTooltipChangeEvent, };
|
|
33
34
|
declare module "react" {
|
|
34
35
|
namespace JSX {
|
|
35
36
|
interface IntrinsicElements {
|
|
@@ -62,6 +63,49 @@ declare module "react" {
|
|
|
62
63
|
"onprimitiv-collapsible-show"?: (event: CustomEvent) => void;
|
|
63
64
|
};
|
|
64
65
|
"primitiv-collapsible-trigger-icon": ElementProps<PrimitivCollapsibleTriggerIconElement>;
|
|
66
|
+
"primitiv-dropdown": ElementProps<PrimitivDropdownElement> & {
|
|
67
|
+
open?: boolean;
|
|
68
|
+
"onprimitiv-dropdown-change"?: (event: PrimitivDropdownChangeEvent) => void;
|
|
69
|
+
};
|
|
70
|
+
"primitiv-dropdown-trigger": ElementProps<PrimitivDropdownTriggerElement>;
|
|
71
|
+
"primitiv-dropdown-content": ElementProps<PrimitivDropdownContentElement> & {
|
|
72
|
+
popover?: "auto" | "manual" | "";
|
|
73
|
+
};
|
|
74
|
+
"primitiv-dropdown-item": ElementProps<PrimitivDropdownItemElement> & {
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
"onprimitiv-dropdown-select"?: (event: PrimitivDropdownSelectEvent) => void;
|
|
77
|
+
};
|
|
78
|
+
"primitiv-dropdown-checkbox-item": ElementProps<PrimitivDropdownCheckboxItemElement> & {
|
|
79
|
+
checked?: boolean | "indeterminate";
|
|
80
|
+
disabled?: boolean;
|
|
81
|
+
"onprimitiv-dropdown-checked-change"?: (event: PrimitivDropdownCheckedChangeEvent) => void;
|
|
82
|
+
"onprimitiv-dropdown-select"?: (event: PrimitivDropdownSelectEvent) => void;
|
|
83
|
+
};
|
|
84
|
+
"primitiv-dropdown-radio-group": ElementProps<PrimitivDropdownRadioGroupElement> & {
|
|
85
|
+
value?: string;
|
|
86
|
+
"onprimitiv-dropdown-value-change"?: (event: PrimitivDropdownValueChangeEvent) => void;
|
|
87
|
+
};
|
|
88
|
+
"primitiv-dropdown-radio-item": ElementProps<PrimitivDropdownRadioItemElement> & {
|
|
89
|
+
value: string;
|
|
90
|
+
disabled?: boolean;
|
|
91
|
+
"onprimitiv-dropdown-select"?: (event: PrimitivDropdownSelectEvent) => void;
|
|
92
|
+
};
|
|
93
|
+
"primitiv-dropdown-item-indicator": ElementProps<PrimitivDropdownItemIndicatorElement> & {
|
|
94
|
+
"force-mount"?: boolean;
|
|
95
|
+
};
|
|
96
|
+
"primitiv-dropdown-group": ElementProps<PrimitivDropdownGroupElement>;
|
|
97
|
+
"primitiv-dropdown-label": ElementProps<PrimitivDropdownLabelElement>;
|
|
98
|
+
"primitiv-dropdown-separator": ElementProps<PrimitivDropdownSeparatorElement>;
|
|
99
|
+
"primitiv-dropdown-sub": ElementProps<PrimitivDropdownSubElement> & {
|
|
100
|
+
open?: boolean;
|
|
101
|
+
"onprimitiv-dropdown-sub-change"?: (event: PrimitivDropdownSubChangeEvent) => void;
|
|
102
|
+
};
|
|
103
|
+
"primitiv-dropdown-sub-trigger": ElementProps<PrimitivDropdownSubTriggerElement> & {
|
|
104
|
+
disabled?: boolean;
|
|
105
|
+
};
|
|
106
|
+
"primitiv-dropdown-sub-content": ElementProps<PrimitivDropdownSubContentElement> & {
|
|
107
|
+
popover?: "auto" | "manual" | "";
|
|
108
|
+
};
|
|
65
109
|
"primitiv-modal": ElementProps<PrimitivModalElement> & {
|
|
66
110
|
open?: boolean;
|
|
67
111
|
"onprimitiv-modal-change"?: (event: PrimitivModalChangeEvent) => void;
|