monochrome 0.2.0 → 0.4.0
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 +21 -97
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1 -1
- package/dist/react/accordion.d.ts +22 -0
- package/dist/react/collapsible.d.ts +13 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +1 -1
- package/dist/react/menu.d.ts +34 -0
- package/dist/react/shared.d.ts +6 -0
- package/dist/react/tabs.d.ts +24 -0
- package/dist/router.d.ts +1 -0
- package/dist/router.js +1 -0
- package/dist/vue/accordion.d.ts +62 -0
- package/dist/vue/collapsible.d.ts +23 -0
- package/dist/vue/index.d.ts +4 -0
- package/dist/vue/index.js +1 -0
- package/dist/vue/menu.d.ts +73 -0
- package/dist/vue/shared.d.ts +35 -0
- package/dist/vue/tabs.d.ts +87 -0
- package/package.json +32 -23
- package/src/index.ts +0 -561
- package/src/react/accordion.tsx +0 -85
- package/src/react/collapsible.tsx +0 -60
- package/src/react/index.ts +0 -4
- package/src/react/menu.tsx +0 -230
- package/src/react/shared.tsx +0 -16
- package/src/react/tabs.tsx +0 -116
package/src/index.ts
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
enum Focus {
|
|
2
|
-
Trigger,
|
|
3
|
-
First,
|
|
4
|
-
Last,
|
|
5
|
-
None,
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
enum Prefix {
|
|
9
|
-
Trigger = "mct:",
|
|
10
|
-
TriggerAccordion = "mct:a",
|
|
11
|
-
TriggerCollapsible = "mct:c",
|
|
12
|
-
TriggerMenu = "mct:m",
|
|
13
|
-
TriggerTabs = "mct:t",
|
|
14
|
-
Content = "mcc:",
|
|
15
|
-
ContentMenu = "mcc:m",
|
|
16
|
-
RootAccordion = "mcr:a",
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (typeof document !== "undefined") {
|
|
20
|
-
let shouldPreventDefault: boolean | null = null
|
|
21
|
-
let shouldMatchLetter: string | null = null
|
|
22
|
-
let shouldResetRadio: HTMLElement | null = null
|
|
23
|
-
let radioHeadDone: boolean | null = null
|
|
24
|
-
let radioTailChain: HTMLElement[] = []
|
|
25
|
-
const menuPopovers: HTMLElement[] = []
|
|
26
|
-
let rovingBoundary: Element | null = null
|
|
27
|
-
let safeGroup: HTMLElement | null = null
|
|
28
|
-
let safeRect: DOMRect | null = null
|
|
29
|
-
let safeDir = 0
|
|
30
|
-
|
|
31
|
-
type RovingNavigator = (origin: Element | null | undefined) => HTMLElement | null
|
|
32
|
-
type RovingFocusCallback = (
|
|
33
|
-
node: Element | null | undefined,
|
|
34
|
-
fallback: RovingNavigator,
|
|
35
|
-
) => HTMLElement | null
|
|
36
|
-
type Roving = (focus: RovingFocusCallback) => [RovingNavigator, RovingNavigator]
|
|
37
|
-
|
|
38
|
-
const isElement = (el: unknown): el is HTMLElement => el instanceof HTMLElement
|
|
39
|
-
|
|
40
|
-
const isTrigger = (el: unknown, prefix?: string): el is HTMLButtonElement =>
|
|
41
|
-
el instanceof HTMLButtonElement && (!prefix || el.id.startsWith(prefix))
|
|
42
|
-
|
|
43
|
-
const isMenuItem = (el: unknown): el is HTMLElement =>
|
|
44
|
-
isElement(el) && el.role?.startsWith("menuitem") === true && el.ariaDisabled !== "true"
|
|
45
|
-
|
|
46
|
-
const getContent = (el: HTMLElement): HTMLElement | null =>
|
|
47
|
-
document.getElementById(el.getAttribute("aria-controls") || "")
|
|
48
|
-
|
|
49
|
-
const findAncestor = (el: HTMLElement | null, prefix: string): HTMLElement | null => {
|
|
50
|
-
while (el) {
|
|
51
|
-
if (el.id.startsWith(prefix)) return el
|
|
52
|
-
el = el.parentElement
|
|
53
|
-
}
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const roving: Roving = (focus) => {
|
|
58
|
-
const next: RovingNavigator = (origin) =>
|
|
59
|
-
origin
|
|
60
|
-
? focus(origin.nextElementSibling || origin.parentElement?.firstElementChild, next)
|
|
61
|
-
: null
|
|
62
|
-
const previous: RovingNavigator = (origin) =>
|
|
63
|
-
origin
|
|
64
|
-
? focus(origin.previousElementSibling || origin.parentElement?.lastElementChild, previous)
|
|
65
|
-
: null
|
|
66
|
-
return [next, previous]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const menuRoving: RovingFocusCallback = (element, fallback) => {
|
|
70
|
-
if (isElement(element)) {
|
|
71
|
-
const menuitem = element.firstElementChild
|
|
72
|
-
if (shouldResetRadio) {
|
|
73
|
-
if (isElement(menuitem)) {
|
|
74
|
-
if (menuitem === shouldResetRadio) {
|
|
75
|
-
for (const item of radioTailChain) item.ariaChecked = "false"
|
|
76
|
-
return menuitem
|
|
77
|
-
}
|
|
78
|
-
if (menuitem.role === "menuitemradio") {
|
|
79
|
-
if (!radioHeadDone) {
|
|
80
|
-
menuitem.ariaChecked = "false"
|
|
81
|
-
} else {
|
|
82
|
-
radioTailChain.push(menuitem)
|
|
83
|
-
}
|
|
84
|
-
return fallback(element)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
radioHeadDone = true
|
|
88
|
-
radioTailChain = []
|
|
89
|
-
return fallback(element)
|
|
90
|
-
}
|
|
91
|
-
if (
|
|
92
|
-
isMenuItem(menuitem) &&
|
|
93
|
-
(!shouldMatchLetter || menuitem.textContent?.toLowerCase().startsWith(shouldMatchLetter))
|
|
94
|
-
) {
|
|
95
|
-
menuitem.focus()
|
|
96
|
-
shouldPreventDefault = true
|
|
97
|
-
return menuitem
|
|
98
|
-
} else if (rovingBoundary !== element) {
|
|
99
|
-
if (!rovingBoundary) rovingBoundary = element
|
|
100
|
-
return fallback(element)
|
|
101
|
-
} else {
|
|
102
|
-
rovingBoundary = null
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const accordionRoving: RovingFocusCallback = (node, fallback) => {
|
|
109
|
-
if (isElement(node)) {
|
|
110
|
-
if (rovingBoundary === node) return null
|
|
111
|
-
if (!rovingBoundary) rovingBoundary = node
|
|
112
|
-
const trigger = node.firstElementChild?.firstElementChild
|
|
113
|
-
if (isTrigger(trigger, Prefix.TriggerAccordion)) {
|
|
114
|
-
if (trigger.ariaDisabled === "true") return fallback(node)
|
|
115
|
-
shouldPreventDefault = true
|
|
116
|
-
trigger.focus()
|
|
117
|
-
return trigger
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return fallback(node)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const tabsRoving: RovingFocusCallback = (node, fallback) => {
|
|
124
|
-
if (isTrigger(node, Prefix.TriggerTabs)) {
|
|
125
|
-
if (rovingBoundary === node) return null
|
|
126
|
-
if (!rovingBoundary) rovingBoundary = node
|
|
127
|
-
if (node.ariaDisabled === "true") return fallback(node)
|
|
128
|
-
shouldPreventDefault = true
|
|
129
|
-
node.focus()
|
|
130
|
-
return node
|
|
131
|
-
} else {
|
|
132
|
-
return fallback(node)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const [menuNext, menuPrevious] = roving(menuRoving)
|
|
137
|
-
const [accordionNext, accordionPrevious] = roving(accordionRoving)
|
|
138
|
-
const [tabNext, tabPrevious] = roving(tabsRoving)
|
|
139
|
-
|
|
140
|
-
const collapsible = (trigger: HTMLElement) => {
|
|
141
|
-
const content = getContent(trigger)
|
|
142
|
-
if (content) {
|
|
143
|
-
const isOpen = trigger.ariaExpanded !== "true"
|
|
144
|
-
trigger.ariaExpanded = isOpen ? "true" : "false"
|
|
145
|
-
content.ariaHidden = isOpen ? "false" : "true"
|
|
146
|
-
isOpen ? content.removeAttribute("hidden") : content.setAttribute("hidden", "until-found")
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const accordion = (trigger: HTMLElement) => {
|
|
151
|
-
if (trigger.ariaDisabled === "true") return
|
|
152
|
-
if (trigger.ariaExpanded === "true") {
|
|
153
|
-
collapsible(trigger)
|
|
154
|
-
} else {
|
|
155
|
-
const root = findAncestor(trigger, Prefix.RootAccordion)
|
|
156
|
-
if (!root || root.getAttribute("data-mode") !== "single") {
|
|
157
|
-
collapsible(trigger)
|
|
158
|
-
} else {
|
|
159
|
-
let item = root.firstElementChild
|
|
160
|
-
while (item) {
|
|
161
|
-
const itemTrigger = item.firstElementChild?.firstElementChild
|
|
162
|
-
if (
|
|
163
|
-
isElement(itemTrigger) &&
|
|
164
|
-
(itemTrigger === trigger || itemTrigger.ariaExpanded === "true")
|
|
165
|
-
) {
|
|
166
|
-
collapsible(itemTrigger)
|
|
167
|
-
}
|
|
168
|
-
item = item.nextElementSibling
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const tabs = (trigger: HTMLElement) => {
|
|
175
|
-
if (trigger.ariaDisabled !== "true" && trigger.ariaSelected !== "true") {
|
|
176
|
-
let tab = trigger.parentElement?.firstElementChild
|
|
177
|
-
while (isElement(tab)) {
|
|
178
|
-
if (tab === trigger || tab.ariaSelected === "true") {
|
|
179
|
-
const content = getContent(tab)
|
|
180
|
-
if (content) {
|
|
181
|
-
const isSelected = tab.ariaSelected !== "true"
|
|
182
|
-
tab.ariaSelected = isSelected ? "true" : "false"
|
|
183
|
-
tab.tabIndex = isSelected ? 0 : -1
|
|
184
|
-
content.ariaHidden = isSelected ? "false" : "true"
|
|
185
|
-
if (content.hasAttribute("tabindex")) content.tabIndex = isSelected ? 0 : -1
|
|
186
|
-
isSelected
|
|
187
|
-
? content.removeAttribute("hidden")
|
|
188
|
-
: content.setAttribute("hidden", "until-found")
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
tab = tab.nextElementSibling
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const menu = (trigger: HTMLElement | undefined, mode = Focus.Trigger) => {
|
|
197
|
-
if (trigger?.id.startsWith(Prefix.TriggerMenu)) {
|
|
198
|
-
const content = getContent(trigger)
|
|
199
|
-
if (content) {
|
|
200
|
-
if (trigger.ariaExpanded === "true") {
|
|
201
|
-
if (safeGroup) safeGroup.removeAttribute("data-safe")
|
|
202
|
-
safeGroup = null
|
|
203
|
-
if (mode !== Focus.None) trigger.focus()
|
|
204
|
-
content.hidePopover()
|
|
205
|
-
trigger.ariaExpanded = "false"
|
|
206
|
-
content.ariaHidden = "true"
|
|
207
|
-
} else {
|
|
208
|
-
menuPopovers.push(trigger)
|
|
209
|
-
content.showPopover()
|
|
210
|
-
trigger.ariaExpanded = "true"
|
|
211
|
-
content.ariaHidden = "false"
|
|
212
|
-
const rect = trigger.getBoundingClientRect()
|
|
213
|
-
content.style.setProperty("--top", `${rect.top}px`)
|
|
214
|
-
content.style.setProperty("--right", `${rect.right}px`)
|
|
215
|
-
content.style.setProperty("--bottom", `${rect.bottom}px`)
|
|
216
|
-
content.style.setProperty("--left", `${rect.left}px`)
|
|
217
|
-
const group = trigger.parentElement
|
|
218
|
-
if (group) {
|
|
219
|
-
const cr = content.getBoundingClientRect()
|
|
220
|
-
const right = cr.left > rect.right
|
|
221
|
-
const sx = right ? cr.left : cr.right
|
|
222
|
-
safeGroup = group
|
|
223
|
-
safeRect = rect
|
|
224
|
-
safeDir = right ? -4 : 4
|
|
225
|
-
group.style.setProperty("--right", `${sx}px`)
|
|
226
|
-
group.style.setProperty("--top", `${cr.top}px`)
|
|
227
|
-
group.style.setProperty("--bottom", `${cr.bottom}px`)
|
|
228
|
-
}
|
|
229
|
-
if (mode === Focus.Trigger) {
|
|
230
|
-
trigger.focus()
|
|
231
|
-
} else if (mode === Focus.First) {
|
|
232
|
-
menuRoving(content.firstElementChild, menuNext)
|
|
233
|
-
} else if (mode === Focus.Last) {
|
|
234
|
-
menuRoving(content.lastElementChild, menuPrevious)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const menuHideAll = (keep = 0) => {
|
|
242
|
-
while (menuPopovers[keep]) menu(menuPopovers.pop(), Focus.None)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const menuItemAction = (el: HTMLElement) => {
|
|
246
|
-
if (el.role === "menuitemcheckbox") {
|
|
247
|
-
el.ariaChecked = el.ariaChecked === "true" ? "false" : "true"
|
|
248
|
-
} else if (el.role === "menuitemradio") {
|
|
249
|
-
shouldResetRadio = el
|
|
250
|
-
radioHeadDone = null
|
|
251
|
-
radioTailChain = []
|
|
252
|
-
menuNext(el.parentElement)
|
|
253
|
-
shouldResetRadio = null
|
|
254
|
-
el.ariaChecked = "true"
|
|
255
|
-
} else {
|
|
256
|
-
menuHideAll()
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
addEventListener("click", (event: MouseEvent) => {
|
|
261
|
-
shouldPreventDefault = null
|
|
262
|
-
const keyboard = event.detail === 0
|
|
263
|
-
|
|
264
|
-
const start: HTMLElement | null = isElement(event.target)
|
|
265
|
-
? event.target
|
|
266
|
-
: event.target instanceof Element
|
|
267
|
-
? event.target.parentElement
|
|
268
|
-
: null
|
|
269
|
-
|
|
270
|
-
if (start) {
|
|
271
|
-
let target: HTMLElement | null = start
|
|
272
|
-
|
|
273
|
-
while (target) {
|
|
274
|
-
const id = target.id
|
|
275
|
-
|
|
276
|
-
if (id.startsWith(Prefix.Trigger)) {
|
|
277
|
-
if (id.startsWith(Prefix.TriggerMenu)) {
|
|
278
|
-
const focusMode = keyboard ? Focus.First : Focus.None
|
|
279
|
-
const inPopover = findAncestor(target.parentElement, Prefix.ContentMenu)
|
|
280
|
-
if (inPopover) {
|
|
281
|
-
if (!menuPopovers.includes(target)) menu(target, focusMode)
|
|
282
|
-
} else {
|
|
283
|
-
if (menuPopovers[0]) {
|
|
284
|
-
const openTarget = target !== menuPopovers[0]
|
|
285
|
-
menuHideAll()
|
|
286
|
-
if (openTarget) menu(target, focusMode)
|
|
287
|
-
} else {
|
|
288
|
-
menu(target, focusMode)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
if (menuPopovers[0]) menuHideAll()
|
|
293
|
-
if (id.startsWith(Prefix.TriggerAccordion)) accordion(target)
|
|
294
|
-
else if (id.startsWith(Prefix.TriggerCollapsible)) collapsible(target)
|
|
295
|
-
else if (id.startsWith(Prefix.TriggerTabs)) tabs(target)
|
|
296
|
-
}
|
|
297
|
-
break
|
|
298
|
-
} else if (id.startsWith(Prefix.ContentMenu) && menuPopovers[0]) {
|
|
299
|
-
let el: HTMLElement | null = start
|
|
300
|
-
while (el && el !== target) {
|
|
301
|
-
if (isMenuItem(el) && !isTrigger(el, Prefix.TriggerMenu)) {
|
|
302
|
-
menuItemAction(el)
|
|
303
|
-
break
|
|
304
|
-
}
|
|
305
|
-
el = el.parentElement
|
|
306
|
-
}
|
|
307
|
-
break
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
target = target.parentElement
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!target && menuPopovers[0]) menuHideAll()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (shouldPreventDefault) event.preventDefault()
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
addEventListener("pointermove", (event: PointerEvent) => {
|
|
320
|
-
if (event.pointerType === "touch") return
|
|
321
|
-
if (menuPopovers[0]) {
|
|
322
|
-
if (menuPopovers[1] && safeGroup && safeRect) {
|
|
323
|
-
if (
|
|
324
|
-
event.clientX >= safeRect.left &&
|
|
325
|
-
event.clientX <= safeRect.right &&
|
|
326
|
-
event.clientY >= safeRect.top &&
|
|
327
|
-
event.clientY <= safeRect.bottom
|
|
328
|
-
) {
|
|
329
|
-
safeGroup.style.setProperty("--left", `${event.clientX + safeDir}px`)
|
|
330
|
-
safeGroup.style.setProperty("--center", `${event.clientY}px`)
|
|
331
|
-
if (!safeGroup.hasAttribute("data-safe")) safeGroup.setAttribute("data-safe", "")
|
|
332
|
-
} else if (
|
|
333
|
-
safeGroup.hasAttribute("data-safe") &&
|
|
334
|
-
(event.target !== safeGroup || safeDir * event.movementX > 0)
|
|
335
|
-
) {
|
|
336
|
-
safeGroup.removeAttribute("data-safe")
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const el = event.target
|
|
340
|
-
|
|
341
|
-
if (isElement(el)) {
|
|
342
|
-
const popoverTriggers: HTMLButtonElement[] = []
|
|
343
|
-
let target: HTMLElement | null = el
|
|
344
|
-
let bail = false
|
|
345
|
-
let foundItem = false
|
|
346
|
-
|
|
347
|
-
while (target) {
|
|
348
|
-
if (isMenuItem(target)) {
|
|
349
|
-
foundItem = true
|
|
350
|
-
}
|
|
351
|
-
if (!foundItem && target.id.startsWith(Prefix.Content)) {
|
|
352
|
-
bail = true
|
|
353
|
-
break
|
|
354
|
-
}
|
|
355
|
-
const firstChild = target.firstElementChild
|
|
356
|
-
if (isTrigger(firstChild, Prefix.TriggerMenu)) {
|
|
357
|
-
popoverTriggers.unshift(firstChild)
|
|
358
|
-
}
|
|
359
|
-
target = target.parentElement
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!bail && popoverTriggers[0]) {
|
|
363
|
-
let i = 0
|
|
364
|
-
while (menuPopovers[i] && menuPopovers[i] === popoverTriggers[i]) i++
|
|
365
|
-
if (i === 0 && popoverTriggers[0].role !== "menuitem") return
|
|
366
|
-
menuHideAll(i)
|
|
367
|
-
menu(popoverTriggers[i], Focus.None)
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
addEventListener("keydown", (event: KeyboardEvent) => {
|
|
374
|
-
shouldPreventDefault = null
|
|
375
|
-
shouldMatchLetter = null
|
|
376
|
-
rovingBoundary = null
|
|
377
|
-
|
|
378
|
-
const target = event.target
|
|
379
|
-
|
|
380
|
-
if (isTrigger(target, Prefix.TriggerAccordion)) {
|
|
381
|
-
const item = target.parentElement?.parentElement
|
|
382
|
-
if (item) {
|
|
383
|
-
switch (event.key) {
|
|
384
|
-
case "ArrowDown":
|
|
385
|
-
accordionNext(item)
|
|
386
|
-
break
|
|
387
|
-
case "ArrowUp":
|
|
388
|
-
accordionPrevious(item)
|
|
389
|
-
break
|
|
390
|
-
case "Home": {
|
|
391
|
-
const root = item.parentElement
|
|
392
|
-
if (root) accordionRoving(root.firstElementChild, accordionNext)
|
|
393
|
-
break
|
|
394
|
-
}
|
|
395
|
-
case "End": {
|
|
396
|
-
const root = item.parentElement
|
|
397
|
-
if (root) accordionRoving(root.lastElementChild, accordionPrevious)
|
|
398
|
-
break
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} else if (isTrigger(target, Prefix.TriggerTabs)) {
|
|
403
|
-
const vertical = target.parentElement?.ariaOrientation === "vertical"
|
|
404
|
-
switch (event.key) {
|
|
405
|
-
case "ArrowDown":
|
|
406
|
-
if (vertical) tabNext(target)
|
|
407
|
-
break
|
|
408
|
-
case "ArrowUp":
|
|
409
|
-
if (vertical) tabPrevious(target)
|
|
410
|
-
break
|
|
411
|
-
case "ArrowRight":
|
|
412
|
-
if (!vertical) tabNext(target)
|
|
413
|
-
break
|
|
414
|
-
case "ArrowLeft":
|
|
415
|
-
if (!vertical) tabPrevious(target)
|
|
416
|
-
break
|
|
417
|
-
case "Home":
|
|
418
|
-
tabsRoving(target.parentElement?.firstElementChild, tabNext)
|
|
419
|
-
break
|
|
420
|
-
case "End":
|
|
421
|
-
tabsRoving(target.parentElement?.lastElementChild, tabPrevious)
|
|
422
|
-
break
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
if (isTrigger(target, Prefix.TriggerMenu)) {
|
|
426
|
-
const isRootTrigger = findAncestor(target, Prefix.ContentMenu) === null
|
|
427
|
-
|
|
428
|
-
switch (event.key) {
|
|
429
|
-
case "ArrowDown":
|
|
430
|
-
if (isRootTrigger) {
|
|
431
|
-
if (target.ariaExpanded !== "true") {
|
|
432
|
-
menu(target, Focus.First)
|
|
433
|
-
} else {
|
|
434
|
-
const content = getContent(target)
|
|
435
|
-
if (content) menuRoving(content.firstElementChild, menuNext)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
break
|
|
439
|
-
case "ArrowUp":
|
|
440
|
-
if (isRootTrigger) {
|
|
441
|
-
if (target.ariaExpanded !== "true") {
|
|
442
|
-
menu(target, Focus.Last)
|
|
443
|
-
} else {
|
|
444
|
-
const content = getContent(target)
|
|
445
|
-
if (content) menuRoving(content.lastElementChild, menuPrevious)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
break
|
|
449
|
-
case "ArrowRight":
|
|
450
|
-
if (!isRootTrigger) menu(target, Focus.First)
|
|
451
|
-
break
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
!shouldPreventDefault &&
|
|
457
|
-
isElement(target) &&
|
|
458
|
-
target.role?.startsWith("menuitem") &&
|
|
459
|
-
target.parentElement
|
|
460
|
-
) {
|
|
461
|
-
const parent = target.parentElement
|
|
462
|
-
const menubarRoot = menuPopovers[0]?.parentElement || parent
|
|
463
|
-
|
|
464
|
-
const inPopover = findAncestor(target.parentElement, Prefix.ContentMenu)
|
|
465
|
-
|
|
466
|
-
switch (event.key) {
|
|
467
|
-
case "Tab":
|
|
468
|
-
if (menuPopovers[0]) menuPopovers[0].focus()
|
|
469
|
-
menuHideAll()
|
|
470
|
-
break
|
|
471
|
-
case "ArrowDown":
|
|
472
|
-
if (inPopover) menuNext(parent)
|
|
473
|
-
break
|
|
474
|
-
case "ArrowUp":
|
|
475
|
-
if (inPopover) menuPrevious(parent)
|
|
476
|
-
break
|
|
477
|
-
case "ArrowRight": {
|
|
478
|
-
const nextNode = menuNext(menubarRoot)
|
|
479
|
-
if (nextNode) {
|
|
480
|
-
const hadOpenMenu = menuPopovers[0]
|
|
481
|
-
menuHideAll()
|
|
482
|
-
if (hadOpenMenu && isTrigger(nextNode, Prefix.TriggerMenu)) {
|
|
483
|
-
menu(nextNode, Focus.None)
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
break
|
|
487
|
-
}
|
|
488
|
-
case "ArrowLeft":
|
|
489
|
-
if (menuPopovers[1]) {
|
|
490
|
-
menu(menuPopovers.pop(), Focus.Trigger)
|
|
491
|
-
} else {
|
|
492
|
-
const nextNode = menuPrevious(menubarRoot)
|
|
493
|
-
if (nextNode) {
|
|
494
|
-
const hadOpenMenu = menuPopovers[0]
|
|
495
|
-
menuHideAll()
|
|
496
|
-
if (hadOpenMenu && isTrigger(nextNode, Prefix.TriggerMenu)) {
|
|
497
|
-
menu(nextNode, Focus.None)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
break
|
|
502
|
-
case "Home":
|
|
503
|
-
menuRoving(parent.parentElement?.firstElementChild, menuNext)
|
|
504
|
-
break
|
|
505
|
-
case "End":
|
|
506
|
-
menuRoving(parent.parentElement?.lastElementChild, menuPrevious)
|
|
507
|
-
break
|
|
508
|
-
default:
|
|
509
|
-
if (/^[a-zA-Z]$/.test(event.key)) {
|
|
510
|
-
shouldMatchLetter = event.key.toLowerCase()
|
|
511
|
-
menuNext(parent)
|
|
512
|
-
}
|
|
513
|
-
break
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (event.key === "Escape" && menuPopovers[0]) {
|
|
519
|
-
menu(menuPopovers.pop(), Focus.Trigger)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (shouldPreventDefault) event.preventDefault()
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
addEventListener(
|
|
526
|
-
"scroll",
|
|
527
|
-
(event) => {
|
|
528
|
-
if (
|
|
529
|
-
menuPopovers[0] &&
|
|
530
|
-
(!isElement(event.target) || !event.target.id.startsWith(Prefix.ContentMenu))
|
|
531
|
-
) {
|
|
532
|
-
menuHideAll()
|
|
533
|
-
}
|
|
534
|
-
},
|
|
535
|
-
true,
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
addEventListener("resize", () => {
|
|
539
|
-
if (menuPopovers[0]) menuHideAll()
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
addEventListener("beforematch", (event) => {
|
|
543
|
-
if (isElement(event.target)) {
|
|
544
|
-
let target: HTMLElement | null = event.target
|
|
545
|
-
while (target) {
|
|
546
|
-
const triggerId = target.getAttribute("aria-labelledby")
|
|
547
|
-
if (triggerId) {
|
|
548
|
-
const trigger = document.getElementById(triggerId)
|
|
549
|
-
if (isTrigger(trigger, Prefix.TriggerAccordion)) {
|
|
550
|
-
if (trigger.ariaExpanded !== "true") accordion(trigger)
|
|
551
|
-
} else if (isTrigger(trigger, Prefix.TriggerCollapsible)) {
|
|
552
|
-
if (trigger.ariaExpanded !== "true") collapsible(trigger)
|
|
553
|
-
} else if (isTrigger(trigger, Prefix.TriggerTabs)) {
|
|
554
|
-
tabs(trigger)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
target = target.parentElement
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
})
|
|
561
|
-
}
|
package/src/react/accordion.tsx
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, useId } from "react"
|
|
4
|
-
import { type BaseProps, HiddenUntilFound } from "./shared.js"
|
|
5
|
-
|
|
6
|
-
type AccordionContextValue = { baseId: string; open: boolean; disabled: boolean }
|
|
7
|
-
const AccordionContext = createContext<AccordionContextValue | null>(null)
|
|
8
|
-
|
|
9
|
-
function useAccordionContext() {
|
|
10
|
-
const context = useContext(AccordionContext)
|
|
11
|
-
if (!context) throw new Error("Accordion components must be used within Accordion.Item")
|
|
12
|
-
return context
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function Root({ children, type, ...props }: BaseProps & { type?: "single" | "multiple" }) {
|
|
16
|
-
const id = useId()
|
|
17
|
-
return (
|
|
18
|
-
<div {...props} data-mode={type ?? "single"} id={`mcr:accordion:${id}`}>
|
|
19
|
-
{children}
|
|
20
|
-
</div>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function Item({
|
|
25
|
-
children,
|
|
26
|
-
open,
|
|
27
|
-
disabled,
|
|
28
|
-
...props
|
|
29
|
-
}: BaseProps & { open?: boolean; disabled?: boolean }) {
|
|
30
|
-
const baseId = useId()
|
|
31
|
-
return (
|
|
32
|
-
<AccordionContext.Provider value={{ baseId, open: open ?? false, disabled: disabled ?? false }}>
|
|
33
|
-
<div {...props}>{children}</div>
|
|
34
|
-
</AccordionContext.Provider>
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function Header({
|
|
39
|
-
children,
|
|
40
|
-
as,
|
|
41
|
-
...props
|
|
42
|
-
}: BaseProps & { as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" }) {
|
|
43
|
-
const Heading = as ?? "h3"
|
|
44
|
-
return <Heading {...props}>{children}</Heading>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function Trigger({ children, ...props }: BaseProps) {
|
|
48
|
-
const context = useAccordionContext()
|
|
49
|
-
const fullId = context.baseId
|
|
50
|
-
const isOpen = context.open
|
|
51
|
-
return (
|
|
52
|
-
<button
|
|
53
|
-
{...props}
|
|
54
|
-
type="button"
|
|
55
|
-
id={`mct:accordion:${fullId}`}
|
|
56
|
-
aria-expanded={isOpen}
|
|
57
|
-
aria-controls={`mcc:accordion:${fullId}`}
|
|
58
|
-
aria-disabled={context.disabled || undefined}
|
|
59
|
-
>
|
|
60
|
-
{children}
|
|
61
|
-
</button>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function Panel({ children, ...props }: BaseProps) {
|
|
66
|
-
const context = useAccordionContext()
|
|
67
|
-
const fullId = context.baseId
|
|
68
|
-
const isOpen = context.open
|
|
69
|
-
return (
|
|
70
|
-
// biome-ignore lint/a11y/useSemanticElements: WAI-ARIA Accordion Pattern
|
|
71
|
-
<div
|
|
72
|
-
{...props}
|
|
73
|
-
id={`mcc:accordion:${fullId}`}
|
|
74
|
-
role="region"
|
|
75
|
-
aria-labelledby={`mct:accordion:${fullId}`}
|
|
76
|
-
aria-hidden={!isOpen}
|
|
77
|
-
hidden={isOpen ? undefined : true}
|
|
78
|
-
>
|
|
79
|
-
{!isOpen && <HiddenUntilFound />}
|
|
80
|
-
{children}
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const Accordion = { Root, Item, Header, Trigger, Panel }
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, useId } from "react"
|
|
4
|
-
import { type BaseProps, HiddenUntilFound } from "./shared.js"
|
|
5
|
-
|
|
6
|
-
type CollapsibleContextValue = { baseId: string; open: boolean }
|
|
7
|
-
const CollapsibleContext = createContext<CollapsibleContextValue | null>(null)
|
|
8
|
-
|
|
9
|
-
function useCollapsibleContext() {
|
|
10
|
-
const context = useContext(CollapsibleContext)
|
|
11
|
-
if (!context) throw new Error("Collapsible components must be used within Collapsible.Root")
|
|
12
|
-
return context
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function Root({ children, open, ...props }: BaseProps & { open?: boolean }) {
|
|
16
|
-
const baseId = useId()
|
|
17
|
-
return (
|
|
18
|
-
<CollapsibleContext.Provider value={{ baseId, open: open ?? false }}>
|
|
19
|
-
<div {...props}>{children}</div>
|
|
20
|
-
</CollapsibleContext.Provider>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function Trigger({ children, ...props }: BaseProps) {
|
|
25
|
-
const context = useCollapsibleContext()
|
|
26
|
-
const fullId = context.baseId
|
|
27
|
-
const isOpen = context.open
|
|
28
|
-
return (
|
|
29
|
-
<button
|
|
30
|
-
{...props}
|
|
31
|
-
type="button"
|
|
32
|
-
id={`mct:collapsible:${fullId}`}
|
|
33
|
-
aria-expanded={isOpen}
|
|
34
|
-
aria-controls={`mcc:collapsible:${fullId}`}
|
|
35
|
-
>
|
|
36
|
-
{children}
|
|
37
|
-
</button>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function Panel({ children, ...props }: BaseProps) {
|
|
42
|
-
const context = useCollapsibleContext()
|
|
43
|
-
const fullId = context.baseId
|
|
44
|
-
const isOpen = context.open
|
|
45
|
-
return (
|
|
46
|
-
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: WAI-ARIA disclosure pattern
|
|
47
|
-
<div
|
|
48
|
-
{...props}
|
|
49
|
-
id={`mcc:collapsible:${fullId}`}
|
|
50
|
-
aria-labelledby={`mct:collapsible:${fullId}`}
|
|
51
|
-
aria-hidden={!isOpen}
|
|
52
|
-
hidden={isOpen ? undefined : true}
|
|
53
|
-
>
|
|
54
|
-
{!isOpen && <HiddenUntilFound />}
|
|
55
|
-
{children}
|
|
56
|
-
</div>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const Collapsible = { Root, Trigger, Panel }
|
package/src/react/index.ts
DELETED