@santi020k/lumen-astro 3.0.0 → 4.0.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 +403 -7
- package/components/AmountField.astro +21 -0
- package/components/AnimatedNumber.astro +1 -1
- package/components/ApprovalCard.astro +30 -0
- package/components/AttachmentList.astro +15 -0
- package/components/AttachmentPreview.astro +51 -0
- package/components/Avatar.astro +2 -0
- package/components/BarChart.astro +95 -31
- package/components/BoxPlot.astro +77 -0
- package/components/Breadcrumb.astro +2 -1
- package/components/BulletChart.astro +64 -0
- package/components/Button.astro +1 -1
- package/components/Calendar.astro +54 -65
- package/components/CalendarHeatmap.astro +76 -0
- package/components/Card.astro +4 -0
- package/components/ChangeSummary.astro +36 -0
- package/components/ChartDatumActions.astro +24 -0
- package/components/ChartMotion.astro +11 -0
- package/components/Code.astro +18 -3
- package/components/CodeTabs.astro +12 -0
- package/components/ComboChart.astro +93 -11
- package/components/CountryFlag.astro +23 -0
- package/components/DataTable.astro +15 -7
- package/components/DatePicker.astro +24 -17
- package/components/DescriptionDetail.astro +18 -0
- package/components/DescriptionItem.astro +18 -0
- package/components/DescriptionTerm.astro +18 -0
- package/components/DeviceFrame.astro +79 -0
- package/components/DialogBody.astro +20 -0
- package/components/DialogClose.astro +21 -0
- package/components/DialogFooter.astro +20 -0
- package/components/DialogHeader.astro +20 -0
- package/components/DialogTitle.astro +21 -0
- package/components/DumbbellChart.astro +8 -0
- package/components/FileUpload.astro +3 -1
- package/components/FilterBar.astro +25 -0
- package/components/FunnelChart.astro +50 -0
- package/components/Grid.astro +2 -2
- package/components/Heatmap.astro +61 -12
- package/components/Histogram.astro +30 -0
- package/components/Icon.astro +7 -3
- package/components/ImageComparison.astro +74 -0
- package/components/Input.astro +2 -2
- package/components/LineChart.astro +127 -67
- package/components/LollipopChart.astro +8 -0
- package/components/MediaFilmstrip.astro +19 -0
- package/components/MediaThumbnail.astro +29 -0
- package/components/MediaViewport.astro +38 -0
- package/components/Mentions.astro +2 -1
- package/components/MessageScroller.astro +6 -0
- package/components/MotionGroup.astro +19 -0
- package/components/NativeSelect.astro +2 -2
- package/components/Pagination.astro +2 -1
- package/components/PasswordField.astro +1 -1
- package/components/PhoneInput.astro +60 -9
- package/components/PhoneNumber.astro +19 -0
- package/components/PieChart.astro +26 -3
- package/components/PromptComposer.astro +36 -0
- package/components/RangeChart.astro +38 -8
- package/components/ScatterChart.astro +62 -14
- package/components/Segmented.astro +5 -5
- package/components/Select.astro +8 -8
- package/components/SourceCitation.astro +18 -0
- package/components/Sparkline.astro +9 -10
- package/components/Stack.astro +2 -2
- package/components/StreamMessage.astro +26 -0
- package/components/Tabs.astro +3 -0
- package/components/ToolActivity.astro +17 -0
- package/components/TreeSelect.astro +1 -0
- package/components/VirtualList.astro +5 -2
- package/components/VisualEffect.astro +21 -0
- package/components/WaterfallChart.astro +27 -0
- package/components/WorldMap.astro +213 -0
- package/forms.ts +17 -4
- package/index.ts +37 -0
- package/internal/ChartInspection.astro +27 -0
- package/internal/ComparisonChart.astro +69 -0
- package/internal/IntervalChart.astro +66 -0
- package/package.json +6 -4
- package/runtime/UIPrimitives.astro +670 -647
- package/runtime/chart-actions.ts +19 -0
- package/runtime/controllers/amount-field.ts +12 -0
- package/runtime/controllers/attachments.ts +13 -0
- package/runtime/controllers/chart-activation.ts +33 -0
- package/runtime/controllers/charts.ts +33 -0
- package/runtime/controllers/combobox.ts +39 -0
- package/runtime/controllers/comparison-reset.ts +20 -0
- package/runtime/controllers/data-table.ts +142 -0
- package/runtime/controllers/dialogs.ts +32 -32
- package/runtime/controllers/document-navigation.ts +54 -22
- package/runtime/controllers/file-upload.ts +89 -0
- package/runtime/controllers/image-comparison.ts +53 -0
- package/runtime/controllers/media-selection.ts +20 -0
- package/runtime/controllers/media-viewport.ts +54 -0
- package/runtime/controllers/media-workspace.ts +8 -0
- package/runtime/controllers/message-scroller.ts +13 -0
- package/runtime/controllers/motion.ts +15 -16
- package/runtime/controllers/optional-media.ts +29 -0
- package/runtime/controllers/phone-input.ts +128 -39
- package/runtime/controllers/visual-interactions.ts +75 -0
- package/runtime/controllers/world-map.ts +154 -0
- package/styles/critical.css +634 -61
- package/styles/lumen.css +101 -0
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
<script>
|
|
12
|
+
import { isLumenDateBoundsValid, parseLumenDate as parseCalendarDate, resolveLumenDateLabels, resolveLumenDateLocale } from '@santi020k/lumen-core'
|
|
12
13
|
import {
|
|
14
|
+
coerceThemePreset,
|
|
13
15
|
createLumenKanbanMoveDetail,
|
|
14
|
-
|
|
16
|
+
createLumenVirtualListController,
|
|
17
|
+
createThemeBuilderTokens,
|
|
18
|
+
executeLumenRichTextCommand,
|
|
15
19
|
exportThemeCss,
|
|
16
20
|
exportThemeDesignTokens,
|
|
17
21
|
exportThemeFigmaVariables,
|
|
18
22
|
formatLumenLanguageLabel,
|
|
23
|
+
getLumenDirectionalKey,
|
|
19
24
|
getLumenLocalePair,
|
|
20
25
|
getLumenRichTextShortcut,
|
|
21
26
|
isLumenRichTextToggleCommand,
|
|
@@ -28,6 +33,12 @@ import {
|
|
|
28
33
|
normalizeLumenLocales,
|
|
29
34
|
scrollLumenTabIntoView } from '@santi020k/lumen-core'
|
|
30
35
|
|
|
36
|
+
import { initAmountFields } from './controllers/amount-field'
|
|
37
|
+
import { clearComboboxes, closeCombobox, initComboboxes } from './controllers/combobox'
|
|
38
|
+
import { initDataTableSorting } from './controllers/data-table'
|
|
39
|
+
import { initMessageFeeds } from './controllers/message-scroller'
|
|
40
|
+
import { initOptionalMediaControllers } from './controllers/optional-media'
|
|
41
|
+
|
|
31
42
|
/* eslint-disable complexity */
|
|
32
43
|
/* Stateful DOM handlers stay local so Astro consumers do not need a framework runtime. */
|
|
33
44
|
type ThemeBuilderExportFormat = 'css' | 'figma' | 'tokens'
|
|
@@ -67,12 +78,6 @@ const fieldControlSelector = [
|
|
|
67
78
|
const fieldDescriptionSelector = '[data-ui-field-hint], [data-ui-field-error], .ui-field__hint, .ui-field__error'
|
|
68
79
|
const fieldErrorSelector = '[data-ui-field-error], .ui-field__error'
|
|
69
80
|
|
|
70
|
-
const formControlSelector = [
|
|
71
|
-
'input:not([type="hidden"])',
|
|
72
|
-
'select',
|
|
73
|
-
'textarea'
|
|
74
|
-
].join(',')
|
|
75
|
-
|
|
76
81
|
const validityMessageAttributes = [
|
|
77
82
|
['valueMissing', 'data-error-required'],
|
|
78
83
|
['typeMismatch', 'data-error-type'],
|
|
@@ -99,16 +104,36 @@ const supportsAnchoredPopovers =
|
|
|
99
104
|
CSS.supports('anchor-name: --ui-anchor') &&
|
|
100
105
|
CSS.supports('position-area: block-end')
|
|
101
106
|
|
|
107
|
+
let uiGeneratedId = 0
|
|
108
|
+
|
|
109
|
+
const nextRuntimeId = (prefix: string, ownerDocument: Document): string => {
|
|
110
|
+
let id: string
|
|
111
|
+
|
|
112
|
+
do {
|
|
113
|
+
id = `${prefix}-${++uiGeneratedId}`
|
|
114
|
+
} while (ownerDocument.getElementById(id))
|
|
115
|
+
|
|
116
|
+
return id
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
let uiAnchorCount = 0
|
|
103
120
|
let uiTabsCount = 0
|
|
104
121
|
const nextAnchorName = (): string => `--ui-anchor-${++uiAnchorCount}`
|
|
105
122
|
|
|
106
|
-
const isElementVisible = (element: HTMLElement): boolean =>
|
|
107
|
-
element.checkVisibility
|
|
108
|
-
|
|
123
|
+
const isElementVisible = (element: HTMLElement): boolean => {
|
|
124
|
+
if (typeof element.checkVisibility === 'function') {
|
|
125
|
+
return element.checkVisibility({ visibilityProperty: true })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const visibility = getComputedStyle(element).visibility
|
|
129
|
+
|
|
130
|
+
if (visibility === 'hidden' || visibility === 'collapse') return false
|
|
131
|
+
|
|
132
|
+
return element.offsetParent !== null || element.getClientRects().length > 0
|
|
133
|
+
}
|
|
109
134
|
|
|
110
135
|
const getFocusable = (root: ParentNode): HTMLElement[] => [...root.querySelectorAll<HTMLElement>(focusableSelector)]
|
|
111
|
-
.filter(element => !element.
|
|
136
|
+
.filter(element => !element.matches(':disabled') && !element.closest('[hidden], [inert]') && isElementVisible(element))
|
|
112
137
|
|
|
113
138
|
const getPanel = (trigger: HTMLElement): HTMLElement | null => {
|
|
114
139
|
const controls = trigger.getAttribute('aria-controls')
|
|
@@ -206,6 +231,16 @@ const initDisclosures = (scope: ParentNode): void => {
|
|
|
206
231
|
})
|
|
207
232
|
|
|
208
233
|
trigger.addEventListener('keydown', event => {
|
|
234
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
235
|
+
|
|
236
|
+
if (event.key === 'Escape' && trigger.getAttribute('aria-expanded') === 'true') {
|
|
237
|
+
event.preventDefault()
|
|
238
|
+
|
|
239
|
+
setOpen(trigger, panel, false)
|
|
240
|
+
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
209
244
|
if (event.key !== 'ArrowDown' && event.key !== 'Enter' && event.key !== ' ') return
|
|
210
245
|
|
|
211
246
|
event.preventDefault()
|
|
@@ -216,6 +251,8 @@ const initDisclosures = (scope: ParentNode): void => {
|
|
|
216
251
|
})
|
|
217
252
|
|
|
218
253
|
panel.addEventListener('keydown', event => {
|
|
254
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
255
|
+
|
|
219
256
|
if (event.key === 'Escape') {
|
|
220
257
|
event.preventDefault()
|
|
221
258
|
|
|
@@ -226,6 +263,8 @@ const initDisclosures = (scope: ParentNode): void => {
|
|
|
226
263
|
return
|
|
227
264
|
}
|
|
228
265
|
|
|
266
|
+
if (event.target instanceof Element && event.target.matches('input, textarea, [contenteditable="true"]')) return
|
|
267
|
+
|
|
229
268
|
if (root.hasAttribute('data-ui-cascader')) return
|
|
230
269
|
|
|
231
270
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Home' && event.key !== 'End') return
|
|
@@ -255,9 +294,31 @@ const initDisclosures = (scope: ParentNode): void => {
|
|
|
255
294
|
}
|
|
256
295
|
|
|
257
296
|
if (root.hasAttribute('data-ui-hover-card')) {
|
|
297
|
+
const containsFocusTarget = (target: EventTarget | null): boolean => {
|
|
298
|
+
try {
|
|
299
|
+
return Reflect.apply(root.contains, root, [target]) === true
|
|
300
|
+
} catch {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
258
305
|
root.addEventListener('mouseenter', () => setOpen(trigger, panel, true))
|
|
259
306
|
|
|
260
|
-
root.addEventListener('mouseleave', () =>
|
|
307
|
+
root.addEventListener('mouseleave', () => {
|
|
308
|
+
if (!root.contains(root.ownerDocument.activeElement)) setOpen(trigger, panel, false)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
root.addEventListener('focusin', event => {
|
|
312
|
+
if (containsFocusTarget(event.relatedTarget)) return
|
|
313
|
+
|
|
314
|
+
setOpen(trigger, panel, true)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
root.addEventListener('focusout', event => {
|
|
318
|
+
if (containsFocusTarget(event.relatedTarget)) return
|
|
319
|
+
|
|
320
|
+
setOpen(trigger, panel, false)
|
|
321
|
+
})
|
|
261
322
|
}
|
|
262
323
|
}
|
|
263
324
|
}
|
|
@@ -287,7 +348,7 @@ const initTooltips = (scope: ParentNode): void => {
|
|
|
287
348
|
|
|
288
349
|
if (trigger && !trigger.closest('[role="tooltip"]')) {
|
|
289
350
|
if (!tip.id) {
|
|
290
|
-
tip.id =
|
|
351
|
+
tip.id = nextRuntimeId('ui-tooltip', tip.ownerDocument)
|
|
291
352
|
}
|
|
292
353
|
|
|
293
354
|
const describedBy = new Set((trigger.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean))
|
|
@@ -339,12 +400,22 @@ const initTooltips = (scope: ParentNode): void => {
|
|
|
339
400
|
root.addEventListener('focusout', hide)
|
|
340
401
|
|
|
341
402
|
root.addEventListener('keydown', event => {
|
|
342
|
-
if (event.
|
|
403
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
404
|
+
|
|
405
|
+
if (event.key === 'Escape' && tip.matches(':popover-open')) {
|
|
406
|
+
event.preventDefault()
|
|
407
|
+
|
|
408
|
+
hide()
|
|
409
|
+
}
|
|
343
410
|
})
|
|
344
411
|
} else {
|
|
345
412
|
/* WCAG 1.4.13: hover/focus tooltips must be dismissible with Escape. */
|
|
346
413
|
root.addEventListener('keydown', event => {
|
|
347
|
-
if (event.
|
|
414
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
415
|
+
|
|
416
|
+
if (event.key !== 'Escape' || getComputedStyle(tip).visibility === 'hidden') return
|
|
417
|
+
|
|
418
|
+
event.preventDefault()
|
|
348
419
|
|
|
349
420
|
tip.style.visibility = 'hidden'
|
|
350
421
|
})
|
|
@@ -386,13 +457,25 @@ const initGlassTracking = (scope: ParentNode): void => {
|
|
|
386
457
|
}
|
|
387
458
|
}
|
|
388
459
|
|
|
460
|
+
const isFormElement = (element: EventTarget | null): element is Element => {
|
|
461
|
+
try {
|
|
462
|
+
return element !== null && Element.prototype.matches.call(element, '*')
|
|
463
|
+
} catch {
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const isFormHtmlElement = (element: EventTarget | null): element is HTMLElement => (
|
|
469
|
+
isFormElement(element) && element.namespaceURI === 'http://www.w3.org/1999/xhtml'
|
|
470
|
+
)
|
|
471
|
+
|
|
389
472
|
const getFieldControl = (root: HTMLElement): HTMLElement | null => {
|
|
390
473
|
const controlId = root.dataset.uiFieldControl
|
|
391
474
|
|
|
392
475
|
if (controlId) {
|
|
393
|
-
const control =
|
|
476
|
+
const control = root.ownerDocument.getElementById(controlId)
|
|
394
477
|
|
|
395
|
-
if (control
|
|
478
|
+
if (isFormHtmlElement(control)) return control
|
|
396
479
|
}
|
|
397
480
|
|
|
398
481
|
return root.querySelector<HTMLElement>(fieldControlSelector)
|
|
@@ -400,7 +483,7 @@ const getFieldControl = (root: HTMLElement): HTMLElement | null => {
|
|
|
400
483
|
|
|
401
484
|
const getFieldDescriptionId = (element: HTMLElement): string => {
|
|
402
485
|
if (!element.id) {
|
|
403
|
-
element.id =
|
|
486
|
+
element.id = nextRuntimeId('ui-field-description', element.ownerDocument)
|
|
404
487
|
}
|
|
405
488
|
|
|
406
489
|
return element.id
|
|
@@ -413,9 +496,7 @@ const getFieldRoot = (control: HTMLElement): HTMLElement | null => (
|
|
|
413
496
|
type NativeFormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
414
497
|
|
|
415
498
|
const isNativeFormControl = (element: EventTarget | null): element is NativeFormControl => (
|
|
416
|
-
element
|
|
417
|
-
element instanceof HTMLSelectElement ||
|
|
418
|
-
element instanceof HTMLTextAreaElement
|
|
499
|
+
isFormHtmlElement(element) && element.matches('input, select, textarea')
|
|
419
500
|
)
|
|
420
501
|
|
|
421
502
|
const getValidationMessage = (
|
|
@@ -483,8 +564,9 @@ const validateControl = (
|
|
|
483
564
|
}
|
|
484
565
|
|
|
485
566
|
const getFormControls = (form: HTMLFormElement): NativeFormControl[] => [
|
|
486
|
-
...form.
|
|
487
|
-
].filter(control => control.
|
|
567
|
+
...form.elements
|
|
568
|
+
].filter(isNativeFormControl).filter(control => !control.disabled &&
|
|
569
|
+
control.type !== 'hidden')
|
|
488
570
|
|
|
489
571
|
const validateForm = (form: HTMLFormElement): Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> => {
|
|
490
572
|
const invalidControls: Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> = []
|
|
@@ -522,8 +604,35 @@ const initFields = (scope: ParentNode): void => {
|
|
|
522
604
|
}
|
|
523
605
|
}
|
|
524
606
|
|
|
607
|
+
const formValidationDocuments = new WeakSet<Document>()
|
|
608
|
+
|
|
609
|
+
const observeFormValidation = (ownerDocument: Document): void => {
|
|
610
|
+
if (formValidationDocuments.has(ownerDocument)) return
|
|
611
|
+
|
|
612
|
+
formValidationDocuments.add(ownerDocument)
|
|
613
|
+
|
|
614
|
+
ownerDocument.addEventListener('focusout', event => {
|
|
615
|
+
const control = event.target
|
|
616
|
+
|
|
617
|
+
if (!isNativeFormControl(control)) return
|
|
618
|
+
|
|
619
|
+
const form = control.form
|
|
620
|
+
|
|
621
|
+
if (!form || form.dataset.uiFormBound !== 'true') return
|
|
622
|
+
|
|
623
|
+
const valid = validateControl(control, form)
|
|
624
|
+
|
|
625
|
+
form.dispatchEvent(new CustomEvent(valid ? 'ui:valid' : 'ui:invalid', {
|
|
626
|
+
bubbles: true,
|
|
627
|
+
detail: valid ? { control, form } : { control, controls: [control], form }
|
|
628
|
+
}))
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
525
632
|
const initForms = (scope: ParentNode): void => {
|
|
526
633
|
for (const form of scope.querySelectorAll<HTMLFormElement>('form[data-ui-form]')) {
|
|
634
|
+
observeFormValidation(form.ownerDocument)
|
|
635
|
+
|
|
527
636
|
if (form.dataset.uiFormBound === 'true') continue
|
|
528
637
|
|
|
529
638
|
form.dataset.uiFormBound = 'true'
|
|
@@ -540,13 +649,14 @@ const initForms = (scope: ParentNode): void => {
|
|
|
540
649
|
form.addEventListener('click', event => {
|
|
541
650
|
const target = event.target
|
|
542
651
|
|
|
543
|
-
if (!(target
|
|
652
|
+
if (!isFormElement(target)) return
|
|
544
653
|
|
|
545
654
|
const link = target.closest<HTMLAnchorElement>('[data-ui-error-summary] a[href^="#"]')
|
|
546
655
|
const controlId = link?.hash.slice(1)
|
|
547
|
-
const control = controlId ?
|
|
656
|
+
const control = controlId ? form.ownerDocument.getElementById(controlId) : null
|
|
548
657
|
|
|
549
|
-
if (!link || !(control
|
|
658
|
+
if (!link || !isFormHtmlElement(control) ||
|
|
659
|
+
!(isNativeFormControl(control) ? control.form === form : form.contains(control))) return
|
|
550
660
|
|
|
551
661
|
event.preventDefault()
|
|
552
662
|
|
|
@@ -555,19 +665,6 @@ const initForms = (scope: ParentNode): void => {
|
|
|
555
665
|
control.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
556
666
|
})
|
|
557
667
|
|
|
558
|
-
form.addEventListener('focusout', event => {
|
|
559
|
-
if (!isNativeFormControl(event.target) || event.target.form !== form) return
|
|
560
|
-
|
|
561
|
-
const valid = validateControl(event.target, form)
|
|
562
|
-
|
|
563
|
-
form.dispatchEvent(new CustomEvent(valid ? 'ui:valid' : 'ui:invalid', {
|
|
564
|
-
bubbles: true,
|
|
565
|
-
detail: valid ?
|
|
566
|
-
{ control: event.target, form } :
|
|
567
|
-
{ control: event.target, controls: [event.target], form }
|
|
568
|
-
}))
|
|
569
|
-
})
|
|
570
|
-
|
|
571
668
|
form.addEventListener('submit', event => {
|
|
572
669
|
if (form.dataset.status === 'submitting') {
|
|
573
670
|
event.preventDefault()
|
|
@@ -641,7 +738,7 @@ const initPasswordFields = (scope: ParentNode): void => {
|
|
|
641
738
|
|
|
642
739
|
toggle.setAttribute('aria-pressed', String(visible))
|
|
643
740
|
|
|
644
|
-
if (visibleLabel) visibleLabel.textContent = visible ?
|
|
741
|
+
if (visibleLabel) visibleLabel.textContent = visible ? hideLabel : showLabel
|
|
645
742
|
}
|
|
646
743
|
|
|
647
744
|
toggle.addEventListener('click', () => {
|
|
@@ -969,7 +1066,7 @@ const initTabs = (scope: ParentNode): void => {
|
|
|
969
1066
|
|
|
970
1067
|
if (initiallySelected) activate(initiallySelected, false)
|
|
971
1068
|
|
|
972
|
-
for (const
|
|
1069
|
+
for (const tab of tabs) {
|
|
973
1070
|
tab.addEventListener('click', () => activate(tab))
|
|
974
1071
|
|
|
975
1072
|
tab.addEventListener('keydown', event => {
|
|
@@ -981,12 +1078,14 @@ const initTabs = (scope: ParentNode): void => {
|
|
|
981
1078
|
|
|
982
1079
|
event.preventDefault()
|
|
983
1080
|
|
|
984
|
-
const
|
|
1081
|
+
const enabledTabs = tabs.filter(candidate => !candidate.matches(':disabled, [aria-disabled="true"]'))
|
|
1082
|
+
|
|
1083
|
+
const nextTab = enabledTabs[
|
|
985
1084
|
getLoopedIndex(
|
|
986
1085
|
event.key,
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
orientation === 'vertical' ? ['ArrowDown'] : ['ArrowRight']
|
|
1086
|
+
Math.max(0, enabledTabs.indexOf(tab)),
|
|
1087
|
+
enabledTabs.length,
|
|
1088
|
+
orientation === 'vertical' ? ['ArrowDown'] : [getLumenDirectionalKey(tab, 'ArrowRight')]
|
|
990
1089
|
)
|
|
991
1090
|
]
|
|
992
1091
|
|
|
@@ -1003,7 +1102,6 @@ const initTabs = (scope: ParentNode): void => {
|
|
|
1003
1102
|
const initRovingGroups = (scope: ParentNode): void => {
|
|
1004
1103
|
const groupSelector = [
|
|
1005
1104
|
'[data-ui-menubar]',
|
|
1006
|
-
'[data-ui-navigation-menu]',
|
|
1007
1105
|
'[data-ui-radio-group]',
|
|
1008
1106
|
'[data-ui-toggle-group]',
|
|
1009
1107
|
'[data-ui-toolbar]'
|
|
@@ -1035,7 +1133,7 @@ const initRovingGroups = (scope: ParentNode): void => {
|
|
|
1035
1133
|
nextItem.tabIndex = -1
|
|
1036
1134
|
}
|
|
1037
1135
|
|
|
1038
|
-
const nextItem = items[getLoopedIndex(event.key, index, items.length, ['ArrowRight', 'ArrowDown'])]
|
|
1136
|
+
const nextItem = items[getLoopedIndex(getLumenDirectionalKey(root, event.key), index, items.length, ['ArrowRight', 'ArrowDown'])]
|
|
1039
1137
|
|
|
1040
1138
|
if (nextItem) {
|
|
1041
1139
|
nextItem.tabIndex = 0
|
|
@@ -1201,6 +1299,12 @@ const initThemeToggles = (scope: ParentNode): void => {
|
|
|
1201
1299
|
|
|
1202
1300
|
control.dataset.uiBound = 'true'
|
|
1203
1301
|
|
|
1302
|
+
if (control.hasAttribute('data-ui-controlled')) {
|
|
1303
|
+
sync(root.dataset.theme ?? control.dataset.lightTheme ?? 'light')
|
|
1304
|
+
|
|
1305
|
+
continue
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1204
1308
|
const storageKey = control.dataset.storageKey ?? 'lumen-theme'
|
|
1205
1309
|
let storedTheme: string | null = null
|
|
1206
1310
|
|
|
@@ -1333,41 +1437,10 @@ const initLanguageToggles = (scope: ParentNode): void => {
|
|
|
1333
1437
|
}
|
|
1334
1438
|
}
|
|
1335
1439
|
|
|
1336
|
-
const getVisibleItems = (items: HTMLElement[]): HTMLElement[] => items.filter(item => !item.hidden &&
|
|
1337
|
-
item.offsetParent !== null &&
|
|
1338
|
-
!item.hasAttribute('disabled') &&
|
|
1339
|
-
item.getAttribute('aria-disabled') !== 'true')
|
|
1340
|
-
|
|
1341
|
-
const closeCombobox = (root: HTMLElement): void => {
|
|
1342
|
-
const input = root.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
1343
|
-
const listbox = root.querySelector<HTMLElement>('[role="listbox"]')
|
|
1344
|
-
|
|
1345
|
-
if (!input || !listbox) return
|
|
1346
|
-
|
|
1347
|
-
input.setAttribute('aria-expanded', 'false')
|
|
1348
|
-
|
|
1349
|
-
listbox.hidden = true
|
|
1350
|
-
|
|
1351
|
-
listbox.dataset.state = 'closed'
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const openCombobox = (root: HTMLElement): void => {
|
|
1355
|
-
const input = root.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
1356
|
-
const listbox = root.querySelector<HTMLElement>('[role="listbox"]')
|
|
1357
|
-
|
|
1358
|
-
if (!input || !listbox) return
|
|
1359
|
-
|
|
1360
|
-
input.setAttribute('aria-expanded', 'true')
|
|
1361
|
-
|
|
1362
|
-
listbox.hidden = false
|
|
1363
|
-
|
|
1364
|
-
listbox.dataset.state = 'open'
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
1440
|
let codeCopyLiveRegion: HTMLElement | undefined
|
|
1368
1441
|
|
|
1369
1442
|
const getCodeCopyLiveRegion = (): HTMLElement => {
|
|
1370
|
-
if (codeCopyLiveRegion) return codeCopyLiveRegion
|
|
1443
|
+
if (codeCopyLiveRegion?.isConnected) return codeCopyLiveRegion
|
|
1371
1444
|
|
|
1372
1445
|
codeCopyLiveRegion = document.createElement('div')
|
|
1373
1446
|
|
|
@@ -1392,39 +1465,19 @@ const getCodeCopyLiveRegion = (): HTMLElement => {
|
|
|
1392
1465
|
return codeCopyLiveRegion
|
|
1393
1466
|
}
|
|
1394
1467
|
|
|
1395
|
-
const
|
|
1396
|
-
for (const
|
|
1397
|
-
if (
|
|
1468
|
+
const initCodeRegions = (scope: ParentNode): void => {
|
|
1469
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-code]')) {
|
|
1470
|
+
if (root.classList.contains('ui-code--wrap')) continue
|
|
1398
1471
|
|
|
1399
|
-
|
|
1472
|
+
for (const pre of root.querySelectorAll<HTMLPreElement>('pre')) {
|
|
1473
|
+
if (!pre.hasAttribute('tabindex')) pre.tabIndex = 0
|
|
1400
1474
|
|
|
1401
|
-
|
|
1402
|
-
const root = button.closest<HTMLElement>('[data-ui-code]')
|
|
1403
|
-
const code = root?.querySelector<HTMLElement>('pre code, code')
|
|
1404
|
-
const text = code?.innerText ?? code?.textContent
|
|
1475
|
+
if (!pre.hasAttribute('role')) pre.setAttribute('role', 'region')
|
|
1405
1476
|
|
|
1406
|
-
if (!
|
|
1407
|
-
|
|
1408
|
-
try {
|
|
1409
|
-
await navigator.clipboard.writeText(text)
|
|
1410
|
-
} catch {
|
|
1411
|
-
return
|
|
1477
|
+
if (!pre.hasAttribute('aria-label') && !pre.hasAttribute('aria-labelledby')) {
|
|
1478
|
+
pre.setAttribute('aria-label', root.dataset.codeLabel ?? 'Code example')
|
|
1412
1479
|
}
|
|
1413
|
-
|
|
1414
|
-
button.classList.add('ui-code__copy--copied')
|
|
1415
|
-
|
|
1416
|
-
button.setAttribute('aria-label', 'Code copied!')
|
|
1417
|
-
|
|
1418
|
-
getCodeCopyLiveRegion().textContent = 'Code copied to clipboard'
|
|
1419
|
-
|
|
1420
|
-
window.setTimeout(() => {
|
|
1421
|
-
button.classList.remove('ui-code__copy--copied')
|
|
1422
|
-
|
|
1423
|
-
button.setAttribute('aria-label', 'Copy code to clipboard')
|
|
1424
|
-
|
|
1425
|
-
getCodeCopyLiveRegion().textContent = ''
|
|
1426
|
-
}, 2000)
|
|
1427
|
-
})
|
|
1480
|
+
}
|
|
1428
1481
|
}
|
|
1429
1482
|
}
|
|
1430
1483
|
|
|
@@ -1433,6 +1486,12 @@ const getCopyButtonValue = (button: HTMLElement): string | undefined => {
|
|
|
1433
1486
|
return button.dataset.copyValue
|
|
1434
1487
|
}
|
|
1435
1488
|
|
|
1489
|
+
if (button.hasAttribute('data-ui-code-copy')) {
|
|
1490
|
+
const code = button.closest('[data-ui-code]')?.querySelector<HTMLElement>('pre code, code')
|
|
1491
|
+
|
|
1492
|
+
return code?.innerText ?? code?.textContent ?? undefined
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1436
1495
|
const selector = button.dataset.copyTarget
|
|
1437
1496
|
|
|
1438
1497
|
if (!selector) return undefined
|
|
@@ -1448,17 +1507,20 @@ const getCopyButtonValue = (button: HTMLElement): string | undefined => {
|
|
|
1448
1507
|
|
|
1449
1508
|
const initCopyButtons = (scope: ParentNode): void => {
|
|
1450
1509
|
for (const button of scope.querySelectorAll<HTMLButtonElement>(
|
|
1451
|
-
'[data-ui-copy-button]'
|
|
1510
|
+
'[data-ui-copy-button], [data-ui-code-copy]'
|
|
1452
1511
|
)) {
|
|
1453
1512
|
if (button.dataset.uiCopyButtonBound === 'true') continue
|
|
1454
1513
|
|
|
1455
1514
|
button.dataset.uiCopyButtonBound = 'true'
|
|
1456
1515
|
|
|
1457
1516
|
let resetTimer = 0
|
|
1517
|
+
let operation = 0
|
|
1458
1518
|
|
|
1459
1519
|
const setCopyButtonState = (state: 'copied' | 'error' | 'idle'): void => {
|
|
1460
1520
|
button.dataset.state = state
|
|
1461
1521
|
|
|
1522
|
+
button.classList.toggle('ui-code__copy--copied', state === 'copied' && button.hasAttribute('data-ui-code-copy'))
|
|
1523
|
+
|
|
1462
1524
|
for (const part of button.querySelectorAll<HTMLElement>('[data-slot^="copy-"]')) {
|
|
1463
1525
|
const isActive = part.dataset.slot === `copy-${state}`
|
|
1464
1526
|
|
|
@@ -1471,6 +1533,10 @@ const initCopyButtons = (scope: ParentNode): void => {
|
|
|
1471
1533
|
setCopyButtonState('idle')
|
|
1472
1534
|
|
|
1473
1535
|
button.addEventListener('click', async () => {
|
|
1536
|
+
const currentOperation = ++operation
|
|
1537
|
+
|
|
1538
|
+
window.clearTimeout(resetTimer)
|
|
1539
|
+
|
|
1474
1540
|
const label = button.dataset.copyLabel ?? 'Copy to clipboard'
|
|
1475
1541
|
|
|
1476
1542
|
const copiedLabel = button.dataset.copyCopiedLabel ??
|
|
@@ -1488,10 +1554,14 @@ const initCopyButtons = (scope: ParentNode): void => {
|
|
|
1488
1554
|
|
|
1489
1555
|
await navigator.clipboard.writeText(value)
|
|
1490
1556
|
|
|
1557
|
+
if (operation !== currentOperation || !button.isConnected) return
|
|
1558
|
+
|
|
1491
1559
|
setCopyButtonState('copied')
|
|
1492
1560
|
|
|
1493
1561
|
button.setAttribute('aria-label', copiedLabel)
|
|
1494
1562
|
|
|
1563
|
+
if (button.hasAttribute('data-ui-code-copy')) button.title = copiedLabel
|
|
1564
|
+
|
|
1495
1565
|
button.dispatchEvent(new CustomEvent('ui:copy-success', {
|
|
1496
1566
|
bubbles: true,
|
|
1497
1567
|
detail: { value }
|
|
@@ -1505,10 +1575,14 @@ const initCopyButtons = (scope: ParentNode): void => {
|
|
|
1505
1575
|
}))
|
|
1506
1576
|
}
|
|
1507
1577
|
} catch (error) {
|
|
1578
|
+
if (operation !== currentOperation || !button.isConnected) return
|
|
1579
|
+
|
|
1508
1580
|
setCopyButtonState('error')
|
|
1509
1581
|
|
|
1510
1582
|
button.setAttribute('aria-label', errorLabel)
|
|
1511
1583
|
|
|
1584
|
+
if (button.hasAttribute('data-ui-code-copy')) button.title = errorLabel
|
|
1585
|
+
|
|
1512
1586
|
button.dispatchEvent(new CustomEvent('ui:copy-error', {
|
|
1513
1587
|
bubbles: true,
|
|
1514
1588
|
detail: { error }
|
|
@@ -1536,6 +1610,8 @@ const initCopyButtons = (scope: ParentNode): void => {
|
|
|
1536
1610
|
|
|
1537
1611
|
button.setAttribute('aria-label', label)
|
|
1538
1612
|
|
|
1613
|
+
if (button.hasAttribute('data-ui-code-copy')) button.title = label
|
|
1614
|
+
|
|
1539
1615
|
getCodeCopyLiveRegion().textContent = ''
|
|
1540
1616
|
}, resetAfter)
|
|
1541
1617
|
})
|
|
@@ -1596,22 +1672,6 @@ const filterItems = (input: HTMLInputElement, items: HTMLElement[]): void => {
|
|
|
1596
1672
|
}
|
|
1597
1673
|
}
|
|
1598
1674
|
|
|
1599
|
-
const selectComboboxOption = (root: HTMLElement, option: HTMLElement): void => {
|
|
1600
|
-
const input = root.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
1601
|
-
|
|
1602
|
-
if (!input) return
|
|
1603
|
-
|
|
1604
|
-
input.value = option.dataset.value ?? option.textContent?.trim() ?? ''
|
|
1605
|
-
|
|
1606
|
-
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1607
|
-
|
|
1608
|
-
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1609
|
-
|
|
1610
|
-
closeCombobox(root)
|
|
1611
|
-
|
|
1612
|
-
input.focus({ preventScroll: true })
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
1675
|
const closeSelect = (root: HTMLElement): void => {
|
|
1616
1676
|
const trigger = root.querySelector<HTMLElement>('[data-ui-select-trigger]')
|
|
1617
1677
|
const listbox = root.querySelector<HTMLElement>('[data-ui-select-list]')
|
|
@@ -1741,21 +1801,6 @@ const focusSelectOption = (
|
|
|
1741
1801
|
nextItem?.focus()
|
|
1742
1802
|
}
|
|
1743
1803
|
|
|
1744
|
-
const focusLoopedOption = (
|
|
1745
|
-
items: HTMLElement[],
|
|
1746
|
-
currentItem: HTMLElement,
|
|
1747
|
-
key: string
|
|
1748
|
-
): void => {
|
|
1749
|
-
const visibleItems = getVisibleItems(items)
|
|
1750
|
-
|
|
1751
|
-
if (!visibleItems.length) return
|
|
1752
|
-
|
|
1753
|
-
const currentIndex = visibleItems.indexOf(currentItem)
|
|
1754
|
-
const nextItem = visibleItems[getLoopedIndex(key, Math.max(0, currentIndex), visibleItems.length, ['ArrowDown'])]
|
|
1755
|
-
|
|
1756
|
-
nextItem?.focus()
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
1804
|
const initCommand = (scope: ParentNode): void => {
|
|
1760
1805
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-command]')) {
|
|
1761
1806
|
if (root.dataset.uiBound === 'true') continue
|
|
@@ -1774,88 +1819,7 @@ const initCommand = (scope: ParentNode): void => {
|
|
|
1774
1819
|
}
|
|
1775
1820
|
}
|
|
1776
1821
|
|
|
1777
|
-
|
|
1778
|
-
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-combobox]')) {
|
|
1779
|
-
if (root.dataset.uiBound === 'true') continue
|
|
1780
|
-
|
|
1781
|
-
const input = root.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
1782
|
-
const listbox = root.querySelector<HTMLElement>('[role="listbox"]')
|
|
1783
|
-
|
|
1784
|
-
if (!input || !listbox) continue
|
|
1785
|
-
|
|
1786
|
-
root.dataset.uiBound = 'true'
|
|
1787
|
-
|
|
1788
|
-
const items = [...listbox.querySelectorAll<HTMLElement>('[role="option"]')]
|
|
1789
|
-
|
|
1790
|
-
closeCombobox(root)
|
|
1791
|
-
|
|
1792
|
-
input.addEventListener('focus', () => openCombobox(root))
|
|
1793
|
-
|
|
1794
|
-
input.addEventListener('input', () => {
|
|
1795
|
-
filterItems(input, items)
|
|
1796
|
-
|
|
1797
|
-
openCombobox(root)
|
|
1798
|
-
})
|
|
1799
|
-
|
|
1800
|
-
input.addEventListener('keydown', event => {
|
|
1801
|
-
if (event.key === 'Escape') {
|
|
1802
|
-
closeCombobox(root)
|
|
1803
|
-
|
|
1804
|
-
return
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter') return
|
|
1808
|
-
|
|
1809
|
-
const visibleItems = getVisibleItems(items)
|
|
1810
|
-
|
|
1811
|
-
if (!visibleItems.length) return
|
|
1812
|
-
|
|
1813
|
-
event.preventDefault()
|
|
1814
|
-
|
|
1815
|
-
openCombobox(root)
|
|
1816
|
-
|
|
1817
|
-
if (event.key === 'Enter') {
|
|
1818
|
-
selectComboboxOption(root, visibleItems[0])
|
|
1819
|
-
|
|
1820
|
-
return
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
const targetIndex = event.key === 'ArrowUp' ? visibleItems.length - 1 : 0
|
|
1824
|
-
|
|
1825
|
-
visibleItems[targetIndex]?.focus()
|
|
1826
|
-
})
|
|
1827
|
-
|
|
1828
|
-
for (const item of items) {
|
|
1829
|
-
item.tabIndex = -1
|
|
1830
|
-
|
|
1831
|
-
item.addEventListener('click', () => selectComboboxOption(root, item))
|
|
1832
|
-
|
|
1833
|
-
item.addEventListener('keydown', event => {
|
|
1834
|
-
if (event.key === 'Escape') {
|
|
1835
|
-
closeCombobox(root)
|
|
1836
|
-
|
|
1837
|
-
input.focus({ preventScroll: true })
|
|
1838
|
-
|
|
1839
|
-
return
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
1843
|
-
event.preventDefault()
|
|
1844
|
-
|
|
1845
|
-
selectComboboxOption(root, item)
|
|
1846
|
-
|
|
1847
|
-
return
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Home' && event.key !== 'End') return
|
|
1851
|
-
|
|
1852
|
-
event.preventDefault()
|
|
1853
|
-
|
|
1854
|
-
focusLoopedOption(items, item, event.key)
|
|
1855
|
-
})
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1822
|
+
document.addEventListener('astro:before-swap', clearComboboxes)
|
|
1859
1823
|
|
|
1860
1824
|
const initSelects = (scope: ParentNode): void => {
|
|
1861
1825
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-select]')) {
|
|
@@ -1904,6 +1868,12 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
1904
1868
|
}
|
|
1905
1869
|
}
|
|
1906
1870
|
|
|
1871
|
+
for (const attribute of ['aria-label', 'aria-labelledby']) {
|
|
1872
|
+
const value = trigger.getAttribute(attribute)
|
|
1873
|
+
|
|
1874
|
+
if (value) listbox.setAttribute(attribute, value)
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1907
1877
|
for (const label of select.labels ?? []) {
|
|
1908
1878
|
label.addEventListener('click', event => {
|
|
1909
1879
|
event.preventDefault()
|
|
@@ -1930,7 +1900,9 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
1930
1900
|
}
|
|
1931
1901
|
|
|
1932
1902
|
const handleTypeahead = (event: KeyboardEvent, currentItem?: HTMLElement): boolean => {
|
|
1933
|
-
if (!isPrintableKey(event)
|
|
1903
|
+
if (!isPrintableKey(event) || (
|
|
1904
|
+
event.key === ' ' && (!typeahead || trigger.getAttribute('aria-expanded') !== 'true')
|
|
1905
|
+
)) return false
|
|
1934
1906
|
|
|
1935
1907
|
event.preventDefault()
|
|
1936
1908
|
|
|
@@ -1953,9 +1925,23 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
1953
1925
|
|
|
1954
1926
|
closeSelect(root)
|
|
1955
1927
|
|
|
1928
|
+
root.addEventListener('focusout', event => {
|
|
1929
|
+
if (event.relatedTarget instanceof Node && root.contains(event.relatedTarget)) return
|
|
1930
|
+
|
|
1931
|
+
typeahead = ''
|
|
1932
|
+
|
|
1933
|
+
window.clearTimeout(typeaheadTimer)
|
|
1934
|
+
|
|
1935
|
+
closeSelect(root)
|
|
1936
|
+
})
|
|
1937
|
+
|
|
1956
1938
|
trigger.addEventListener('click', () => {
|
|
1957
1939
|
const isOpen = trigger.getAttribute('aria-expanded') === 'true'
|
|
1958
1940
|
|
|
1941
|
+
typeahead = ''
|
|
1942
|
+
|
|
1943
|
+
window.clearTimeout(typeaheadTimer)
|
|
1944
|
+
|
|
1959
1945
|
if (isOpen) {
|
|
1960
1946
|
closeSelect(root)
|
|
1961
1947
|
} else {
|
|
@@ -1964,9 +1950,13 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
1964
1950
|
})
|
|
1965
1951
|
|
|
1966
1952
|
trigger.addEventListener('keydown', event => {
|
|
1953
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
1954
|
+
|
|
1967
1955
|
if (handleTypeahead(event)) return
|
|
1968
1956
|
|
|
1969
|
-
if (event.key === 'Escape') {
|
|
1957
|
+
if (event.key === 'Escape' && !listbox.hidden) {
|
|
1958
|
+
event.preventDefault()
|
|
1959
|
+
|
|
1970
1960
|
closeSelect(root)
|
|
1971
1961
|
|
|
1972
1962
|
return
|
|
@@ -1978,6 +1968,10 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
1978
1968
|
|
|
1979
1969
|
event.preventDefault()
|
|
1980
1970
|
|
|
1971
|
+
typeahead = ''
|
|
1972
|
+
|
|
1973
|
+
window.clearTimeout(typeaheadTimer)
|
|
1974
|
+
|
|
1981
1975
|
openSelect(root)
|
|
1982
1976
|
|
|
1983
1977
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -2003,9 +1997,13 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
2003
1997
|
item.addEventListener('click', () => selectSelectOption(root, item))
|
|
2004
1998
|
|
|
2005
1999
|
item.addEventListener('keydown', event => {
|
|
2000
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
2001
|
+
|
|
2006
2002
|
if (handleTypeahead(event, item)) return
|
|
2007
2003
|
|
|
2008
|
-
if (event.key === 'Escape') {
|
|
2004
|
+
if (event.key === 'Escape' && !listbox.hidden) {
|
|
2005
|
+
event.preventDefault()
|
|
2006
|
+
|
|
2009
2007
|
closeSelect(root)
|
|
2010
2008
|
|
|
2011
2009
|
trigger.focus({ preventScroll: true })
|
|
@@ -2025,6 +2023,10 @@ const initSelects = (scope: ParentNode): void => {
|
|
|
2025
2023
|
|
|
2026
2024
|
event.preventDefault()
|
|
2027
2025
|
|
|
2026
|
+
typeahead = ''
|
|
2027
|
+
|
|
2028
|
+
window.clearTimeout(typeaheadTimer)
|
|
2029
|
+
|
|
2028
2030
|
focusSelectOption(root, event.key, item)
|
|
2029
2031
|
})
|
|
2030
2032
|
}
|
|
@@ -2173,19 +2175,26 @@ const initResizableGroups = (scope: ParentNode): void => {
|
|
|
2173
2175
|
if (handle.dataset.active !== 'true') return
|
|
2174
2176
|
|
|
2175
2177
|
const delta = ((event[axis] - startPosition) / containerSize) * 100
|
|
2178
|
+
const multiplier = direction === 'horizontal' && getLumenDirectionalKey(handle, 'ArrowRight') === 'ArrowLeft' ? -1 : 1
|
|
2176
2179
|
|
|
2177
|
-
resizePair(index, startSize + delta)
|
|
2180
|
+
resizePair(index, startSize + delta * multiplier)
|
|
2178
2181
|
})
|
|
2179
2182
|
|
|
2180
|
-
|
|
2183
|
+
const finishResize = (event: PointerEvent): void => {
|
|
2181
2184
|
if (handle.dataset.active !== 'true') return
|
|
2182
2185
|
|
|
2183
2186
|
delete handle.dataset.active
|
|
2184
2187
|
|
|
2185
2188
|
delete root.dataset.resizing
|
|
2186
2189
|
|
|
2187
|
-
handle.releasePointerCapture(event.pointerId)
|
|
2188
|
-
}
|
|
2190
|
+
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
handle.addEventListener('pointerup', finishResize)
|
|
2194
|
+
|
|
2195
|
+
handle.addEventListener('pointercancel', finishResize)
|
|
2196
|
+
|
|
2197
|
+
handle.addEventListener('lostpointercapture', finishResize)
|
|
2189
2198
|
|
|
2190
2199
|
handle.addEventListener('dblclick', () => {
|
|
2191
2200
|
if (root.dataset.uiResizableReset !== 'true') return
|
|
@@ -2218,7 +2227,7 @@ const initResizableGroups = (scope: ParentNode): void => {
|
|
|
2218
2227
|
return
|
|
2219
2228
|
}
|
|
2220
2229
|
|
|
2221
|
-
const delta = keyDeltas[event.key]
|
|
2230
|
+
const delta = keyDeltas[getLumenDirectionalKey(handle, event.key)]
|
|
2222
2231
|
|
|
2223
2232
|
if (delta === undefined) return
|
|
2224
2233
|
|
|
@@ -2375,132 +2384,6 @@ const initInputOtpFields = (scope: ParentNode): void => {
|
|
|
2375
2384
|
}
|
|
2376
2385
|
|
|
2377
2386
|
/* cspell:ignore Datatable datatable */
|
|
2378
|
-
/* eslint-disable @stylistic/padding-line-between-statements, no-nested-ternary */
|
|
2379
|
-
const getDataTableSortValue = (
|
|
2380
|
-
cell: HTMLTableCellElement | undefined
|
|
2381
|
-
): string => cell?.dataset.sortValue ?? cell?.textContent?.trim() ?? ''
|
|
2382
|
-
|
|
2383
|
-
const compareDataTableValues = (value: string, other: string, sortType: string | undefined): number => {
|
|
2384
|
-
const numericValue = Number(value.replace(/,/g, ''))
|
|
2385
|
-
const numericOther = Number(other.replace(/,/g, ''))
|
|
2386
|
-
const useNumeric = sortType === 'number' || (
|
|
2387
|
-
sortType !== 'string' &&
|
|
2388
|
-
value.trim() !== '' &&
|
|
2389
|
-
other.trim() !== '' &&
|
|
2390
|
-
Number.isFinite(numericValue) &&
|
|
2391
|
-
Number.isFinite(numericOther)
|
|
2392
|
-
)
|
|
2393
|
-
|
|
2394
|
-
if (useNumeric) {
|
|
2395
|
-
return numericValue - numericOther
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
return value.localeCompare(other, undefined, { numeric: true, sensitivity: 'base' })
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
const sortDataTableRows = (
|
|
2402
|
-
table: HTMLTableElement,
|
|
2403
|
-
header: HTMLTableCellElement,
|
|
2404
|
-
columnIndex: number,
|
|
2405
|
-
direction: 'ascending' | 'descending' | 'none'
|
|
2406
|
-
): void => {
|
|
2407
|
-
const body = table.tBodies[0]
|
|
2408
|
-
|
|
2409
|
-
if (!body) return
|
|
2410
|
-
|
|
2411
|
-
const rows = [...body.rows]
|
|
2412
|
-
const sortType = header.dataset.uiDatatableSortType
|
|
2413
|
-
|
|
2414
|
-
rows.sort((row, other) => {
|
|
2415
|
-
if (direction === 'none') {
|
|
2416
|
-
return Number(row.dataset.uiDatatableIndex ?? '0') - Number(other.dataset.uiDatatableIndex ?? '0')
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
const result = compareDataTableValues(
|
|
2420
|
-
getDataTableSortValue(row.cells[columnIndex]), getDataTableSortValue(other.cells[columnIndex]), sortType
|
|
2421
|
-
)
|
|
2422
|
-
|
|
2423
|
-
return (direction === 'ascending' ? result : -result) ||
|
|
2424
|
-
Number(row.dataset.uiDatatableIndex ?? '0') - Number(other.dataset.uiDatatableIndex ?? '0')
|
|
2425
|
-
})
|
|
2426
|
-
|
|
2427
|
-
rows.forEach(row => body.append(row))
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
const updateDataTableSort = (
|
|
2431
|
-
root: HTMLElement,
|
|
2432
|
-
table: HTMLTableElement,
|
|
2433
|
-
header: HTMLTableCellElement,
|
|
2434
|
-
direction: 'ascending' | 'descending' | 'none'
|
|
2435
|
-
): void => {
|
|
2436
|
-
for (const item of table.querySelectorAll<HTMLTableCellElement>('thead th[aria-sort]')) {
|
|
2437
|
-
item.setAttribute('aria-sort', item === header ? direction : 'none')
|
|
2438
|
-
item.dataset.sort = item === header ? direction : 'none'
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
root.dataset.uiDatatableSortDirection = direction
|
|
2442
|
-
|
|
2443
|
-
if (direction === 'none') {
|
|
2444
|
-
delete root.dataset.uiDatatableSortColumn
|
|
2445
|
-
} else {
|
|
2446
|
-
root.dataset.uiDatatableSortColumn = header.dataset.uiDatatableColumn ?? ''
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
const initDataTableSorting = (root: HTMLElement, table: HTMLTableElement): void => {
|
|
2451
|
-
const headers = [...table.querySelectorAll<HTMLTableCellElement>('thead th')].filter(
|
|
2452
|
-
header => header.dataset.uiDatatableSortable === 'true' ||
|
|
2453
|
-
header.dataset.sortable === 'true' ||
|
|
2454
|
-
header.hasAttribute('data-sortable')
|
|
2455
|
-
)
|
|
2456
|
-
|
|
2457
|
-
for (const header of headers) {
|
|
2458
|
-
if (header.dataset.uiDatatableSortBound === 'true') continue
|
|
2459
|
-
|
|
2460
|
-
const row = header.parentElement
|
|
2461
|
-
const columnIndex = row ? [...row.children].indexOf(header) : -1
|
|
2462
|
-
|
|
2463
|
-
if (columnIndex < 0) continue
|
|
2464
|
-
|
|
2465
|
-
header.dataset.uiDatatableSortBound = 'true'
|
|
2466
|
-
header.dataset.uiDatatableColumn = String(columnIndex)
|
|
2467
|
-
header.setAttribute('aria-sort', 'none')
|
|
2468
|
-
|
|
2469
|
-
const button = document.createElement('button')
|
|
2470
|
-
|
|
2471
|
-
button.type = 'button'
|
|
2472
|
-
button.className = 'ui-data-table__sort'
|
|
2473
|
-
button.dataset.uiDatatableSort = ''
|
|
2474
|
-
|
|
2475
|
-
while (header.firstChild) {
|
|
2476
|
-
button.append(header.firstChild)
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
header.append(button)
|
|
2480
|
-
|
|
2481
|
-
button.addEventListener('click', () => {
|
|
2482
|
-
const currentColumn = root.dataset.uiDatatableSortColumn
|
|
2483
|
-
const currentDirection = header.getAttribute('aria-sort')
|
|
2484
|
-
const nextDirection = currentColumn !== String(columnIndex) || currentDirection === 'none' ?
|
|
2485
|
-
'ascending' :
|
|
2486
|
-
currentDirection === 'ascending' ?
|
|
2487
|
-
'descending' :
|
|
2488
|
-
'none'
|
|
2489
|
-
|
|
2490
|
-
updateDataTableSort(root, table, header, nextDirection)
|
|
2491
|
-
sortDataTableRows(table, header, columnIndex, nextDirection)
|
|
2492
|
-
})
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
const body = table.tBodies[0]
|
|
2496
|
-
|
|
2497
|
-
if (!body) return
|
|
2498
|
-
|
|
2499
|
-
;[...body.rows].forEach((row, index) => {
|
|
2500
|
-
row.dataset.uiDatatableIndex = row.dataset.uiDatatableIndex ?? String(index)
|
|
2501
|
-
})
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
2387
|
const getDataTableRows = (table: HTMLTableElement): HTMLTableRowElement[] => [
|
|
2505
2388
|
...(table.tBodies[0]?.rows ?? [])
|
|
2506
2389
|
]
|
|
@@ -2527,6 +2410,7 @@ const syncDataTableSelection = (
|
|
|
2527
2410
|
|
|
2528
2411
|
if (selected) {
|
|
2529
2412
|
row.dataset.state = 'selected'
|
|
2413
|
+
|
|
2530
2414
|
selectedValues.push(getDataTableRowValue(row, index))
|
|
2531
2415
|
} else {
|
|
2532
2416
|
delete row.dataset.state
|
|
@@ -2534,6 +2418,7 @@ const syncDataTableSelection = (
|
|
|
2534
2418
|
})
|
|
2535
2419
|
|
|
2536
2420
|
selectAll.checked = rows.length > 0 && selectedValues.length === rows.length
|
|
2421
|
+
|
|
2537
2422
|
selectAll.indeterminate = selectedValues.length > 0 && selectedValues.length < rows.length
|
|
2538
2423
|
|
|
2539
2424
|
const name = root.dataset.uiDatatableName
|
|
@@ -2542,8 +2427,11 @@ const syncDataTableSelection = (
|
|
|
2542
2427
|
if (name) {
|
|
2543
2428
|
if (!inputs) {
|
|
2544
2429
|
inputs = document.createElement('div')
|
|
2430
|
+
|
|
2545
2431
|
inputs.hidden = true
|
|
2432
|
+
|
|
2546
2433
|
inputs.dataset.uiDatatableInputs = ''
|
|
2434
|
+
|
|
2547
2435
|
root.append(inputs)
|
|
2548
2436
|
}
|
|
2549
2437
|
|
|
@@ -2551,7 +2439,9 @@ const syncDataTableSelection = (
|
|
|
2551
2439
|
const input = document.createElement('input')
|
|
2552
2440
|
|
|
2553
2441
|
input.type = 'hidden'
|
|
2442
|
+
|
|
2554
2443
|
input.name = name
|
|
2444
|
+
|
|
2555
2445
|
input.value = value
|
|
2556
2446
|
|
|
2557
2447
|
return input
|
|
@@ -2581,16 +2471,24 @@ const initDataTableSelection = (root: HTMLElement, table: HTMLTableElement): voi
|
|
|
2581
2471
|
const selectAll = document.createElement('input')
|
|
2582
2472
|
|
|
2583
2473
|
headerCell.className = 'ui-data-table__selection'
|
|
2474
|
+
|
|
2584
2475
|
headerCell.scope = 'col'
|
|
2476
|
+
|
|
2585
2477
|
selectAll.type = 'checkbox'
|
|
2478
|
+
|
|
2586
2479
|
selectAll.className = 'ui-data-table__checkbox'
|
|
2480
|
+
|
|
2587
2481
|
selectAll.dataset.uiDatatableSelectAll = ''
|
|
2482
|
+
|
|
2588
2483
|
selectAll.setAttribute('aria-label', 'Select all rows')
|
|
2484
|
+
|
|
2589
2485
|
headerCell.append(selectAll)
|
|
2486
|
+
|
|
2590
2487
|
headRow.prepend(headerCell)
|
|
2591
2488
|
|
|
2592
2489
|
getDataTableRows(table).forEach((row, index) => {
|
|
2593
2490
|
row.dataset.uiDatatableRow = ''
|
|
2491
|
+
|
|
2594
2492
|
row.dataset.value = getDataTableRowValue(row, index)
|
|
2595
2493
|
|
|
2596
2494
|
const cell = document.createElement('td')
|
|
@@ -2598,15 +2496,23 @@ const initDataTableSelection = (root: HTMLElement, table: HTMLTableElement): voi
|
|
|
2598
2496
|
const selected = row.dataset.state === 'selected' || row.getAttribute('aria-selected') === 'true'
|
|
2599
2497
|
|
|
2600
2498
|
cell.className = 'ui-data-table__selection'
|
|
2499
|
+
|
|
2601
2500
|
checkbox.type = 'checkbox'
|
|
2501
|
+
|
|
2602
2502
|
checkbox.className = 'ui-data-table__checkbox'
|
|
2503
|
+
|
|
2603
2504
|
checkbox.dataset.uiDatatableRowSelect = ''
|
|
2505
|
+
|
|
2604
2506
|
checkbox.checked = selected
|
|
2507
|
+
|
|
2605
2508
|
checkbox.defaultChecked = selected
|
|
2509
|
+
|
|
2606
2510
|
checkbox.setAttribute('aria-label', `Select row ${index + 1}`)
|
|
2511
|
+
|
|
2607
2512
|
checkbox.addEventListener('change', () => syncDataTableSelection(root, table, selectAll, true))
|
|
2608
2513
|
|
|
2609
2514
|
cell.append(checkbox)
|
|
2515
|
+
|
|
2610
2516
|
row.prepend(cell)
|
|
2611
2517
|
})
|
|
2612
2518
|
|
|
@@ -2636,71 +2542,63 @@ const initDataTables = (scope: ParentNode): void => {
|
|
|
2636
2542
|
root.dataset.uiBound = 'true'
|
|
2637
2543
|
|
|
2638
2544
|
initDataTableSelection(root, table)
|
|
2545
|
+
|
|
2639
2546
|
initDataTableSorting(root, table)
|
|
2640
2547
|
}
|
|
2641
2548
|
}
|
|
2642
|
-
/* eslint-enable @stylistic/padding-line-between-statements, no-nested-ternary */
|
|
2643
|
-
|
|
2644
|
-
const calendarDatePattern = /^\d{4}-\d{2}-\d{2}$/
|
|
2645
|
-
const calendarMonthPattern = /^\d{4}-\d{2}$/
|
|
2646
|
-
|
|
2647
|
-
const parseCalendarDate = (value: string | null | undefined): Date | null => {
|
|
2648
|
-
if (!value || !calendarDatePattern.test(value)) return null
|
|
2649
|
-
|
|
2650
|
-
const [year, month, day] = value.split('-').map(Number)
|
|
2651
|
-
const date = new Date(Date.UTC(year, month - 1, day))
|
|
2652
|
-
|
|
2653
|
-
return date.getUTCFullYear() === year &&
|
|
2654
|
-
date.getUTCMonth() === month - 1 &&
|
|
2655
|
-
date.getUTCDate() === day ?
|
|
2656
|
-
date :
|
|
2657
|
-
null
|
|
2658
|
-
}
|
|
2659
2549
|
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2550
|
+
const createCalendarDate = (year: number, month: number, day: number): Date => {
|
|
2551
|
+
const date = new Date(0)
|
|
2662
2552
|
|
|
2663
|
-
|
|
2664
|
-
const date = new Date(Date.UTC(year, month - 1, 1))
|
|
2553
|
+
date.setUTCFullYear(year, month, day)
|
|
2665
2554
|
|
|
2666
|
-
return date
|
|
2555
|
+
return date
|
|
2667
2556
|
}
|
|
2668
2557
|
|
|
2669
|
-
const
|
|
2670
|
-
const
|
|
2558
|
+
const parseCalendarMonth = (value: string | null | undefined): Date | null => value ? parseCalendarDate(`${value}-01`) : null
|
|
2559
|
+
const formatCalendarDate = (date: Date): string => date.toISOString().split('T')[0] ?? ''
|
|
2560
|
+
const formatCalendarMonth = (date: Date): string => formatCalendarDate(date).slice(0, -3)
|
|
2671
2561
|
|
|
2672
|
-
const addCalendarDays = (date: Date, days: number): Date =>
|
|
2673
|
-
|
|
2562
|
+
const addCalendarDays = (date: Date, days: number): Date => createCalendarDate(
|
|
2563
|
+
date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days
|
|
2674
2564
|
)
|
|
2675
2565
|
|
|
2676
|
-
const getCalendarDaysInMonth = (date: Date): number =>
|
|
2677
|
-
|
|
2566
|
+
const getCalendarDaysInMonth = (date: Date): number => createCalendarDate(
|
|
2567
|
+
date.getUTCFullYear(), date.getUTCMonth() + 1, 0
|
|
2678
2568
|
).getUTCDate()
|
|
2679
2569
|
|
|
2680
2570
|
const addCalendarMonths = (date: Date, months: number): Date => {
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
return new Date(Date.UTC(
|
|
2688
|
-
targetMonth.getUTCFullYear(), targetMonth.getUTCMonth(), Math.min(day, daysInTargetMonth)
|
|
2689
|
-
))
|
|
2571
|
+
const target = createCalendarDate(date.getUTCFullYear(), date.getUTCMonth() + months, 1)
|
|
2572
|
+
|
|
2573
|
+
return createCalendarDate(
|
|
2574
|
+
target.getUTCFullYear(), target.getUTCMonth(), Math.min(date.getUTCDate(), getCalendarDaysInMonth(target))
|
|
2575
|
+
)
|
|
2690
2576
|
}
|
|
2691
2577
|
|
|
2692
|
-
const compareCalendarDates = (date: Date, other: Date | null): number =>
|
|
2693
|
-
|
|
2578
|
+
const compareCalendarDates = (date: Date, other: Date | null): number => (
|
|
2579
|
+
other ? Math.sign(date.getTime() - other.getTime()) : 0
|
|
2580
|
+
)
|
|
2581
|
+
|
|
2582
|
+
const getCalendarLocale = (root: HTMLElement): string => resolveLumenDateLocale(root.getAttribute('locale') || root.dataset.locale || root.closest('[lang]')?.getAttribute('lang') || undefined)
|
|
2583
|
+
const getCalendarMin = (root: HTMLElement): Date | null => parseCalendarDate(root.getAttribute('min') ?? root.dataset.uiCalendarMin ?? '0001-01-01')
|
|
2584
|
+
const getCalendarMax = (root: HTMLElement): Date | null => parseCalendarDate(root.getAttribute('max') ?? root.dataset.uiCalendarMax ?? '9999-12-31')
|
|
2585
|
+
const isCalendarDisabled = (root: HTMLElement): boolean => root.hasAttribute('disabled') || root.dataset.disabled === 'true' || !isLumenDateBoundsValid(root.getAttribute('min') ?? root.dataset.uiCalendarMin, root.getAttribute('max') ?? root.dataset.uiCalendarMax)
|
|
2586
|
+
const isCalendarReadOnly = (root: HTMLElement): boolean => root.hasAttribute('readonly') || root.dataset.readonly === 'true'
|
|
2587
|
+
|
|
2588
|
+
const getCalendarToday = (): Date => {
|
|
2589
|
+
const today = new Date()
|
|
2694
2590
|
|
|
2695
|
-
return
|
|
2591
|
+
return createCalendarDate(today.getFullYear(), today.getMonth(), today.getDate())
|
|
2696
2592
|
}
|
|
2697
2593
|
|
|
2594
|
+
const getCalendarGridStart = (month: Date): Date => addCalendarDays(month, -((month.getUTCDay() + 6) % 7))
|
|
2595
|
+
|
|
2698
2596
|
const isCalendarDateDisabled = (
|
|
2699
2597
|
root: HTMLElement,
|
|
2700
2598
|
date: Date,
|
|
2701
2599
|
min: Date | null,
|
|
2702
2600
|
max: Date | null
|
|
2703
|
-
): boolean => root
|
|
2601
|
+
): boolean => isCalendarDisabled(root) ||
|
|
2704
2602
|
compareCalendarDates(date, min) < 0 ||
|
|
2705
2603
|
compareCalendarDates(date, max) > 0
|
|
2706
2604
|
|
|
@@ -2712,16 +2610,6 @@ const clampCalendarDate = (date: Date, min: Date | null, max: Date | null): Date
|
|
|
2712
2610
|
return date
|
|
2713
2611
|
}
|
|
2714
2612
|
|
|
2715
|
-
const getCalendarLocale = (): string => document.documentElement.lang || navigator.language || 'en'
|
|
2716
|
-
|
|
2717
|
-
const getCalendarToday = (): Date => {
|
|
2718
|
-
const today = new Date()
|
|
2719
|
-
|
|
2720
|
-
return new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()))
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
const getCalendarGridStart = (month: Date): Date => addCalendarDays(month, -((month.getUTCDay() + 6) % 7))
|
|
2724
|
-
|
|
2725
2613
|
const getCalendarFocusDate = (
|
|
2726
2614
|
root: HTMLElement,
|
|
2727
2615
|
month: Date,
|
|
@@ -2760,7 +2648,7 @@ const syncCalendarNavigation = (
|
|
|
2760
2648
|
): void => {
|
|
2761
2649
|
const previous = root.querySelector<HTMLButtonElement>('[data-ui-calendar-prev]')
|
|
2762
2650
|
const next = root.querySelector<HTMLButtonElement>('[data-ui-calendar-next]')
|
|
2763
|
-
const disabled = root
|
|
2651
|
+
const disabled = isCalendarDisabled(root)
|
|
2764
2652
|
const previousMonthLastDay = addCalendarDays(month, -1)
|
|
2765
2653
|
const nextMonthFirstDay = addCalendarMonths(month, 1)
|
|
2766
2654
|
|
|
@@ -2781,19 +2669,32 @@ const renderCalendar = (
|
|
|
2781
2669
|
const label = root.querySelector<HTMLElement>('[data-ui-calendar-label]')
|
|
2782
2670
|
const grid = root.querySelector<HTMLElement>('[data-ui-calendar-grid]')
|
|
2783
2671
|
|
|
2784
|
-
if (!label || !grid) return
|
|
2672
|
+
if (!label || !grid) return
|
|
2673
|
+
|
|
2674
|
+
const locale = getCalendarLocale(root)
|
|
2675
|
+
const labels = resolveLumenDateLabels(locale)
|
|
2676
|
+
const input = root.querySelector<HTMLInputElement>('[data-ui-calendar-input]')
|
|
2677
|
+
|
|
2678
|
+
if (input) input.disabled = isCalendarDisabled(root)
|
|
2679
|
+
|
|
2680
|
+
root.setAttribute('aria-disabled', String(isCalendarDisabled(root)))
|
|
2681
|
+
|
|
2682
|
+
grid.setAttribute('aria-readonly', String(isCalendarReadOnly(root)))
|
|
2785
2683
|
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2684
|
+
root.querySelector('[data-ui-calendar-prev]')?.setAttribute('aria-label', root.dataset.previousMonthLabel || labels.previousMonth)
|
|
2685
|
+
|
|
2686
|
+
root.querySelector('[data-ui-calendar-next]')?.setAttribute('aria-label', root.dataset.nextMonthLabel || labels.nextMonth)
|
|
2687
|
+
|
|
2688
|
+
const min = getCalendarMin(root)
|
|
2689
|
+
const max = getCalendarMax(root)
|
|
2789
2690
|
const selectedDate = parseCalendarDate(root.dataset.uiCalendarValue)
|
|
2790
2691
|
const todayIso = formatCalendarDate(getCalendarToday())
|
|
2791
2692
|
|
|
2792
2693
|
const month = parseCalendarMonth(root.dataset.uiCalendarMonth) ??
|
|
2793
|
-
(selectedDate ?
|
|
2694
|
+
(selectedDate ? createCalendarDate(selectedDate.getUTCFullYear(), selectedDate.getUTCMonth(), 1) : null) ??
|
|
2794
2695
|
getCalendarToday()
|
|
2795
2696
|
|
|
2796
|
-
const visibleMonth =
|
|
2697
|
+
const visibleMonth = createCalendarDate(month.getUTCFullYear(), month.getUTCMonth(), 1)
|
|
2797
2698
|
const focusDate = getCalendarFocusDate(root, visibleMonth, min, max, requestedFocusDate)
|
|
2798
2699
|
const focusIso = formatCalendarDate(focusDate)
|
|
2799
2700
|
const selectedIso = selectedDate ? formatCalendarDate(selectedDate) : ''
|
|
@@ -2837,7 +2738,7 @@ const renderCalendar = (
|
|
|
2837
2738
|
|
|
2838
2739
|
cell.role = 'columnheader'
|
|
2839
2740
|
|
|
2840
|
-
cell.textContent = weekdayLabel.format(
|
|
2741
|
+
cell.textContent = weekdayLabel.format(createCalendarDate(2026, 0, 5 + index))
|
|
2841
2742
|
|
|
2842
2743
|
headerRow.append(cell)
|
|
2843
2744
|
}
|
|
@@ -2900,7 +2801,7 @@ const renderCalendar = (
|
|
|
2900
2801
|
grid.replaceChildren(header, body)
|
|
2901
2802
|
}
|
|
2902
2803
|
|
|
2903
|
-
if (shouldFocus) {
|
|
2804
|
+
if (shouldFocus && !isCalendarDisabled(root)) {
|
|
2904
2805
|
root.querySelector<HTMLElement>(`[data-ui-calendar-day][data-date="${focusIso}"]`)?.focus({ preventScroll: true })
|
|
2905
2806
|
}
|
|
2906
2807
|
}
|
|
@@ -2908,11 +2809,11 @@ const renderCalendar = (
|
|
|
2908
2809
|
const selectCalendarDate = (root: HTMLElement, date: Date): void => {
|
|
2909
2810
|
const input = root.querySelector<HTMLInputElement>('[data-ui-calendar-input]')
|
|
2910
2811
|
|
|
2911
|
-
if (!input || root
|
|
2812
|
+
if (!input || isCalendarDisabled(root) || isCalendarReadOnly(root)) return
|
|
2912
2813
|
|
|
2913
|
-
const min =
|
|
2914
|
-
const max =
|
|
2915
|
-
const nextDate =
|
|
2814
|
+
const min = getCalendarMin(root)
|
|
2815
|
+
const max = getCalendarMax(root)
|
|
2816
|
+
const nextDate = date
|
|
2916
2817
|
|
|
2917
2818
|
if (isCalendarDateDisabled(root, nextDate, min, max)) return
|
|
2918
2819
|
|
|
@@ -2922,16 +2823,16 @@ const selectCalendarDate = (root: HTMLElement, date: Date): void => {
|
|
|
2922
2823
|
|
|
2923
2824
|
root.dataset.uiCalendarMonth = formatCalendarMonth(nextDate)
|
|
2924
2825
|
|
|
2826
|
+
renderCalendar(root, nextDate, true)
|
|
2827
|
+
|
|
2925
2828
|
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2926
2829
|
|
|
2927
2830
|
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2928
|
-
|
|
2929
|
-
renderCalendar(root, nextDate, true)
|
|
2930
2831
|
}
|
|
2931
2832
|
|
|
2932
2833
|
const focusCalendarDate = (root: HTMLElement, date: Date): void => {
|
|
2933
|
-
const min =
|
|
2934
|
-
const max =
|
|
2834
|
+
const min = getCalendarMin(root)
|
|
2835
|
+
const max = getCalendarMax(root)
|
|
2935
2836
|
const nextDate = clampCalendarDate(date, min, max)
|
|
2936
2837
|
|
|
2937
2838
|
root.dataset.uiCalendarMonth = formatCalendarMonth(nextDate)
|
|
@@ -2957,7 +2858,7 @@ const moveCalendarFocus = (root: HTMLElement, currentDate: Date, key: string): v
|
|
|
2957
2858
|
return
|
|
2958
2859
|
}
|
|
2959
2860
|
|
|
2960
|
-
const offset = keyOffsets[key]
|
|
2861
|
+
const offset = keyOffsets[getLumenDirectionalKey(root, key)]
|
|
2961
2862
|
|
|
2962
2863
|
if (offset === undefined) return
|
|
2963
2864
|
|
|
@@ -2972,6 +2873,8 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
2972
2873
|
|
|
2973
2874
|
if (!input) continue
|
|
2974
2875
|
|
|
2876
|
+
const initialValue = input.value
|
|
2877
|
+
|
|
2975
2878
|
root.dataset.uiBound = 'true'
|
|
2976
2879
|
|
|
2977
2880
|
renderCalendar(root)
|
|
@@ -2979,10 +2882,12 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
2979
2882
|
root.querySelector<HTMLButtonElement>('[data-ui-calendar-prev]')?.addEventListener('click', () => {
|
|
2980
2883
|
const month = parseCalendarMonth(root.dataset.uiCalendarMonth)
|
|
2981
2884
|
|
|
2982
|
-
if (!month) return
|
|
2885
|
+
if (!month || isCalendarDisabled(root)) return
|
|
2983
2886
|
|
|
2984
2887
|
const nextMonth = addCalendarMonths(month, -1)
|
|
2985
2888
|
|
|
2889
|
+
if (compareCalendarDates(addCalendarDays(month, -1), getCalendarMin(root)) < 0) return
|
|
2890
|
+
|
|
2986
2891
|
root.dataset.uiCalendarMonth = formatCalendarMonth(nextMonth)
|
|
2987
2892
|
|
|
2988
2893
|
renderCalendar(root, nextMonth, true)
|
|
@@ -2991,10 +2896,12 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
2991
2896
|
root.querySelector<HTMLButtonElement>('[data-ui-calendar-next]')?.addEventListener('click', () => {
|
|
2992
2897
|
const month = parseCalendarMonth(root.dataset.uiCalendarMonth)
|
|
2993
2898
|
|
|
2994
|
-
if (!month) return
|
|
2899
|
+
if (!month || isCalendarDisabled(root)) return
|
|
2995
2900
|
|
|
2996
2901
|
const nextMonth = addCalendarMonths(month, 1)
|
|
2997
2902
|
|
|
2903
|
+
if (compareCalendarDates(nextMonth, getCalendarMax(root)) > 0) return
|
|
2904
|
+
|
|
2998
2905
|
root.dataset.uiCalendarMonth = formatCalendarMonth(nextMonth)
|
|
2999
2906
|
|
|
3000
2907
|
renderCalendar(root, nextMonth, true)
|
|
@@ -3023,7 +2930,7 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
3023
2930
|
|
|
3024
2931
|
const date = parseCalendarDate(target.dataset.date)
|
|
3025
2932
|
|
|
3026
|
-
if (!date) return
|
|
2933
|
+
if (!date || isCalendarDisabled(root)) return
|
|
3027
2934
|
|
|
3028
2935
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
3029
2936
|
event.preventDefault()
|
|
@@ -3051,9 +2958,37 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
3051
2958
|
moveCalendarFocus(root, date, event.key)
|
|
3052
2959
|
})
|
|
3053
2960
|
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
2961
|
+
const observer = new MutationObserver(mutations => {
|
|
2962
|
+
for (const mutation of mutations) {
|
|
2963
|
+
if (mutation.attributeName === 'value') {
|
|
2964
|
+
root.dataset.uiCalendarValue = parseCalendarDate(root.getAttribute('value')) ? root.getAttribute('value') ?? '' : ''
|
|
2965
|
+
|
|
2966
|
+
input.value = root.dataset.uiCalendarValue
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if (mutation.attributeName === 'month') {
|
|
2970
|
+
root.dataset.uiCalendarMonth = root.getAttribute('month') ?? root.dataset.uiCalendarInitialMonth ?? ''
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
const activeElement = root.ownerDocument.activeElement
|
|
2975
|
+
|
|
2976
|
+
const focusedDay = activeElement && root.contains(activeElement) ?
|
|
2977
|
+
parseCalendarDate(activeElement.getAttribute('data-date')) :
|
|
2978
|
+
null
|
|
2979
|
+
|
|
2980
|
+
renderCalendar(root, focusedDay, focusedDay !== null)
|
|
2981
|
+
})
|
|
2982
|
+
|
|
2983
|
+
observer.observe(root, { attributes: true, attributeFilter: ['disabled', 'readonly', 'min', 'max', 'value', 'month', 'locale', 'lang', 'data-disabled', 'data-readonly', 'data-locale', 'data-ui-calendar-min', 'data-ui-calendar-max', 'data-previous-month-label', 'data-next-month-label'] })
|
|
2984
|
+
|
|
2985
|
+
input.form?.addEventListener('reset', event => {
|
|
2986
|
+
globalThis.setTimeout(() => {
|
|
2987
|
+
if (event.defaultPrevented || !root.isConnected) return
|
|
2988
|
+
|
|
2989
|
+
root.dataset.uiCalendarValue = initialValue
|
|
2990
|
+
|
|
2991
|
+
input.value = initialValue
|
|
3057
2992
|
|
|
3058
2993
|
root.dataset.uiCalendarMonth = root.dataset.uiCalendarInitialMonth ?? root.dataset.uiCalendarMonth
|
|
3059
2994
|
|
|
@@ -3063,12 +2998,12 @@ const initCalendars = (scope: ParentNode): void => {
|
|
|
3063
2998
|
}
|
|
3064
2999
|
}
|
|
3065
3000
|
|
|
3066
|
-
const formatDatePickerValue = (value: string, placeholder: string): string => {
|
|
3001
|
+
const formatDatePickerValue = (value: string, placeholder: string, locale: string): string => {
|
|
3067
3002
|
const date = parseCalendarDate(value)
|
|
3068
3003
|
|
|
3069
3004
|
if (!date) return placeholder
|
|
3070
3005
|
|
|
3071
|
-
return new Intl.DateTimeFormat(
|
|
3006
|
+
return new Intl.DateTimeFormat(locale, {
|
|
3072
3007
|
day: 'numeric',
|
|
3073
3008
|
month: 'short',
|
|
3074
3009
|
timeZone: 'UTC',
|
|
@@ -3080,7 +3015,7 @@ const closeDatePicker = (root: HTMLElement, restoreFocus = false): void => {
|
|
|
3080
3015
|
const trigger = root.querySelector<HTMLButtonElement>('[data-ui-date-picker-trigger]')
|
|
3081
3016
|
const popover = root.querySelector<HTMLElement>('[data-ui-date-picker-popover]')
|
|
3082
3017
|
|
|
3083
|
-
if (!trigger || !popover
|
|
3018
|
+
if (!trigger || !popover) return
|
|
3084
3019
|
|
|
3085
3020
|
popover.hidden = true
|
|
3086
3021
|
|
|
@@ -3097,9 +3032,10 @@ const syncDatePickerValue = (root: HTMLElement): void => {
|
|
|
3097
3032
|
|
|
3098
3033
|
if (!native || !value) return
|
|
3099
3034
|
|
|
3100
|
-
const
|
|
3035
|
+
const defaultPlaceholder = root.dataset.chooseDateLabel || resolveLumenDateLabels(getCalendarLocale(root)).chooseDate
|
|
3036
|
+
const placeholder = root.dataset.placeholderDefault === 'true' ? defaultPlaceholder : native.getAttribute('placeholder') || defaultPlaceholder
|
|
3101
3037
|
|
|
3102
|
-
value.textContent = formatDatePickerValue(native.value, placeholder)
|
|
3038
|
+
value.textContent = formatDatePickerValue(native.value, placeholder, getCalendarLocale(root))
|
|
3103
3039
|
|
|
3104
3040
|
if (native.value) {
|
|
3105
3041
|
delete root.dataset.placeholder
|
|
@@ -3118,13 +3054,16 @@ const openDatePicker = (root: HTMLElement): void => {
|
|
|
3118
3054
|
const popover = root.querySelector<HTMLElement>('[data-ui-date-picker-popover]')
|
|
3119
3055
|
const calendar = root.querySelector<HTMLElement>('[data-ui-calendar]')
|
|
3120
3056
|
|
|
3121
|
-
if (!native || !trigger || !popover || native.disabled
|
|
3057
|
+
if (!native || !trigger || !popover || native.disabled || native.readOnly ||
|
|
3058
|
+
!isLumenDateBoundsValid(native.min || undefined, native.max || undefined)) return
|
|
3122
3059
|
|
|
3123
3060
|
for (const other of document.querySelectorAll<HTMLElement>('[data-ui-date-picker]')) {
|
|
3124
3061
|
if (other !== root) closeDatePicker(other)
|
|
3125
3062
|
}
|
|
3126
3063
|
|
|
3127
3064
|
if (calendar) {
|
|
3065
|
+
calendar.dataset.locale = getCalendarLocale(root)
|
|
3066
|
+
|
|
3128
3067
|
calendar.dataset.uiCalendarValue = native.value
|
|
3129
3068
|
|
|
3130
3069
|
if (native.value) calendar.dataset.uiCalendarMonth = native.value.slice(0, 7)
|
|
@@ -3145,7 +3084,12 @@ const openDatePicker = (root: HTMLElement): void => {
|
|
|
3145
3084
|
trigger.setAttribute('aria-expanded', 'true')
|
|
3146
3085
|
|
|
3147
3086
|
window.requestAnimationFrame(() => {
|
|
3148
|
-
|
|
3087
|
+
if (popover.hidden || native.disabled || native.readOnly) return
|
|
3088
|
+
|
|
3089
|
+
const focusTarget = calendar?.querySelector<HTMLElement>('[data-ui-calendar-day][tabindex="0"]') ??
|
|
3090
|
+
calendar?.querySelector<HTMLButtonElement>('button:not(:disabled)')
|
|
3091
|
+
|
|
3092
|
+
focusTarget?.focus({ preventScroll: true })
|
|
3149
3093
|
})
|
|
3150
3094
|
}
|
|
3151
3095
|
|
|
@@ -3169,6 +3113,14 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3169
3113
|
|
|
3170
3114
|
native.tabIndex = -1
|
|
3171
3115
|
|
|
3116
|
+
if (native.id) {
|
|
3117
|
+
const controlId = native.id
|
|
3118
|
+
|
|
3119
|
+
native.id = `${controlId}-native`
|
|
3120
|
+
|
|
3121
|
+
trigger.id = controlId
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3172
3124
|
control.hidden = false
|
|
3173
3125
|
|
|
3174
3126
|
const accessibleName = native.getAttribute('aria-label')
|
|
@@ -3181,7 +3133,41 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3181
3133
|
|
|
3182
3134
|
if (describedBy) trigger.setAttribute('aria-describedby', describedBy)
|
|
3183
3135
|
|
|
3184
|
-
|
|
3136
|
+
const syncState = (): void => {
|
|
3137
|
+
trigger.disabled = native.disabled
|
|
3138
|
+
|
|
3139
|
+
trigger.setAttribute('aria-disabled', String(native.disabled || native.readOnly))
|
|
3140
|
+
|
|
3141
|
+
trigger.setAttribute('aria-required', String(native.required))
|
|
3142
|
+
|
|
3143
|
+
popover.setAttribute('aria-label', root.dataset.chooseDateLabel || resolveLumenDateLabels(getCalendarLocale(root)).chooseDate)
|
|
3144
|
+
|
|
3145
|
+
syncDatePickerValue(root)
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
syncState()
|
|
3149
|
+
|
|
3150
|
+
const observer = new MutationObserver(() => {
|
|
3151
|
+
closeDatePicker(root)
|
|
3152
|
+
|
|
3153
|
+
syncState()
|
|
3154
|
+
})
|
|
3155
|
+
|
|
3156
|
+
observer.observe(native, { attributes: true, attributeFilter: ['disabled', 'readonly', 'required', 'value', 'min', 'max', 'placeholder'] })
|
|
3157
|
+
|
|
3158
|
+
observer.observe(root, { attributes: true, attributeFilter: ['locale', 'lang', 'data-locale', 'data-choose-date-label'] })
|
|
3159
|
+
|
|
3160
|
+
root.addEventListener('keydown', event => {
|
|
3161
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
3162
|
+
|
|
3163
|
+
if (event.key !== 'Escape' || popover.hidden) return
|
|
3164
|
+
|
|
3165
|
+
event.preventDefault()
|
|
3166
|
+
|
|
3167
|
+
event.stopPropagation()
|
|
3168
|
+
|
|
3169
|
+
closeDatePicker(root, true)
|
|
3170
|
+
})
|
|
3185
3171
|
|
|
3186
3172
|
trigger.addEventListener('click', () => {
|
|
3187
3173
|
if (popover.hidden) openDatePicker(root)
|
|
@@ -3197,6 +3183,10 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3197
3183
|
})
|
|
3198
3184
|
|
|
3199
3185
|
calendarInput.addEventListener('change', () => {
|
|
3186
|
+
if (native.disabled || native.readOnly || !parseCalendarDate(calendarInput.value) ||
|
|
3187
|
+
!isLumenDateBoundsValid(native.min || undefined, native.max || undefined) ||
|
|
3188
|
+
(native.min && calendarInput.value < native.min) || (native.max && calendarInput.value > native.max)) return
|
|
3189
|
+
|
|
3200
3190
|
native.value = calendarInput.value
|
|
3201
3191
|
|
|
3202
3192
|
syncDatePickerValue(root)
|
|
@@ -3208,7 +3198,11 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3208
3198
|
closeDatePicker(root, true)
|
|
3209
3199
|
})
|
|
3210
3200
|
|
|
3211
|
-
native.addEventListener('change', () =>
|
|
3201
|
+
native.addEventListener('change', () => {
|
|
3202
|
+
closeDatePicker(root)
|
|
3203
|
+
|
|
3204
|
+
syncState()
|
|
3205
|
+
})
|
|
3212
3206
|
|
|
3213
3207
|
native.addEventListener('invalid', event => {
|
|
3214
3208
|
event.preventDefault()
|
|
@@ -3218,8 +3212,10 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3218
3212
|
trigger.focus()
|
|
3219
3213
|
})
|
|
3220
3214
|
|
|
3221
|
-
native.form?.addEventListener('reset',
|
|
3222
|
-
|
|
3215
|
+
native.form?.addEventListener('reset', event => {
|
|
3216
|
+
globalThis.setTimeout(() => {
|
|
3217
|
+
if (event.defaultPrevented || !root.isConnected) return
|
|
3218
|
+
|
|
3223
3219
|
syncDatePickerValue(root)
|
|
3224
3220
|
|
|
3225
3221
|
closeDatePicker(root)
|
|
@@ -3254,16 +3250,22 @@ const initSchedules = (scope: ParentNode): void => {
|
|
|
3254
3250
|
|
|
3255
3251
|
root.dataset.uiBound = 'true'
|
|
3256
3252
|
|
|
3253
|
+
let activeEvent: HTMLElement | undefined
|
|
3254
|
+
|
|
3257
3255
|
for (const event of root.querySelectorAll<HTMLElement>('[data-ui-schedule-event]')) {
|
|
3258
3256
|
event.draggable = event.draggable || event.dataset.uiDraggable === 'true'
|
|
3259
3257
|
|
|
3260
3258
|
event.addEventListener('dragstart', dragEvent => {
|
|
3261
3259
|
dragEvent.dataTransfer?.setData('text/plain', event.id || event.textContent?.trim() || '')
|
|
3262
3260
|
|
|
3261
|
+
activeEvent = event
|
|
3262
|
+
|
|
3263
3263
|
root.dataset.uiDragging = 'true'
|
|
3264
3264
|
})
|
|
3265
3265
|
|
|
3266
3266
|
event.addEventListener('dragend', () => {
|
|
3267
|
+
activeEvent = undefined
|
|
3268
|
+
|
|
3267
3269
|
delete root.dataset.uiDragging
|
|
3268
3270
|
})
|
|
3269
3271
|
}
|
|
@@ -3285,11 +3287,12 @@ const initSchedules = (scope: ParentNode): void => {
|
|
|
3285
3287
|
delete dropzone.dataset.state
|
|
3286
3288
|
|
|
3287
3289
|
const draggedId = event.dataTransfer?.getData('text/plain')
|
|
3288
|
-
const dragged =
|
|
3290
|
+
const dragged = activeEvent
|
|
3289
3291
|
|
|
3290
|
-
if (dragged instanceof HTMLElement)
|
|
3291
|
-
|
|
3292
|
-
|
|
3292
|
+
if (!(dragged instanceof HTMLElement) ||
|
|
3293
|
+
dragged.closest('[data-ui-schedule]') !== root) return
|
|
3294
|
+
|
|
3295
|
+
dropzone.append(dragged)
|
|
3293
3296
|
|
|
3294
3297
|
root.dispatchEvent(new CustomEvent('ui:schedule-change', {
|
|
3295
3298
|
bubbles: true,
|
|
@@ -3598,43 +3601,30 @@ const initTreeCollections = (scope: ParentNode): void => {
|
|
|
3598
3601
|
}
|
|
3599
3602
|
}
|
|
3600
3603
|
|
|
3601
|
-
const
|
|
3602
|
-
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-virtual-list]')) {
|
|
3603
|
-
if (root.dataset.uiBound === 'true') continue
|
|
3604
|
-
|
|
3605
|
-
const items = [...root.children].filter((child): child is HTMLElement => child instanceof HTMLElement)
|
|
3604
|
+
const virtualListControllers = new Map<HTMLElement, { destroy: () => void, ownerDocument: Document }>()
|
|
3606
3605
|
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
root.
|
|
3610
|
-
|
|
3611
|
-
const overscan = Number(root.dataset.uiOverscan ?? 4)
|
|
3612
|
-
const itemSize = Number(root.dataset.uiItemSize ?? 44)
|
|
3613
|
-
|
|
3614
|
-
const update = (): void => {
|
|
3615
|
-
const visibleCount = Math.ceil(root.clientHeight / itemSize) + overscan
|
|
3616
|
-
const startIndex = Math.max(0, Math.floor(root.scrollTop / itemSize) - overscan)
|
|
3617
|
-
const endIndex = startIndex + visibleCount
|
|
3618
|
-
|
|
3619
|
-
for (const [index, item] of items.entries()) {
|
|
3620
|
-
item.hidden = index < startIndex || index > endIndex
|
|
3621
|
-
}
|
|
3606
|
+
const initVirtualLists = (scope: ParentNode): void => {
|
|
3607
|
+
for (const [root, controller] of virtualListControllers) {
|
|
3608
|
+
if (!root.isConnected || root.ownerDocument !== controller.ownerDocument) {
|
|
3609
|
+
controller.destroy()
|
|
3622
3610
|
|
|
3623
|
-
|
|
3624
|
-
bubbles: true,
|
|
3625
|
-
detail: {
|
|
3626
|
-
endIndex: Math.min(items.length - 1, endIndex),
|
|
3627
|
-
startIndex
|
|
3628
|
-
}
|
|
3629
|
-
}))
|
|
3611
|
+
virtualListControllers.delete(root)
|
|
3630
3612
|
}
|
|
3613
|
+
}
|
|
3631
3614
|
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3615
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-virtual-list]')) {
|
|
3616
|
+
if (!virtualListControllers.has(root)) virtualListControllers.set(root, {
|
|
3617
|
+
...createLumenVirtualListController(root), ownerDocument: root.ownerDocument
|
|
3618
|
+
})
|
|
3635
3619
|
}
|
|
3636
3620
|
}
|
|
3637
3621
|
|
|
3622
|
+
document.addEventListener('astro:before-swap', () => {
|
|
3623
|
+
for (const controller of virtualListControllers.values()) controller.destroy()
|
|
3624
|
+
|
|
3625
|
+
virtualListControllers.clear()
|
|
3626
|
+
})
|
|
3627
|
+
|
|
3638
3628
|
const initTagGroups = (scope: ParentNode): void => {
|
|
3639
3629
|
for (const root of scope.querySelectorAll<HTMLElement>('.ui-tag-group, [data-ui-tag-group]')) {
|
|
3640
3630
|
if (root.dataset.uiBound === 'true') continue
|
|
@@ -3673,7 +3663,9 @@ const getRichTextCommandValue = (control: HTMLElement): string | undefined => {
|
|
|
3673
3663
|
}
|
|
3674
3664
|
|
|
3675
3665
|
const syncRichTextCommandStates = (root: HTMLElement): void => {
|
|
3676
|
-
|
|
3666
|
+
if (root.dataset.uiEditorNativeState === 'false') return
|
|
3667
|
+
|
|
3668
|
+
const commandDocument: RichTextCommandDocument = document
|
|
3677
3669
|
|
|
3678
3670
|
for (const control of root.querySelectorAll<HTMLElement>('[data-ui-editor-command]')) {
|
|
3679
3671
|
const command = control.dataset.uiEditorCommand ?? ''
|
|
@@ -3719,14 +3711,15 @@ const executeRichTextCommand = (
|
|
|
3719
3711
|
): boolean => {
|
|
3720
3712
|
if (!command) return false
|
|
3721
3713
|
|
|
3722
|
-
const commandDocument = document
|
|
3723
|
-
|
|
3714
|
+
const commandDocument: RichTextCommandDocument = document
|
|
3715
|
+
|
|
3716
|
+
const executed = executeLumenRichTextCommand(root, { command, ...(value === undefined ? {} : { value }) }, () => {
|
|
3717
|
+
if (typeof commandDocument.execCommand !== 'function') return false
|
|
3724
3718
|
|
|
3725
|
-
|
|
3726
|
-
executed = value === undefined ?
|
|
3719
|
+
return value === undefined ?
|
|
3727
3720
|
commandDocument.execCommand(command) :
|
|
3728
3721
|
commandDocument.execCommand(command, false, value)
|
|
3729
|
-
}
|
|
3722
|
+
})
|
|
3730
3723
|
|
|
3731
3724
|
const detail: LumenRichTextCommandDetail = {
|
|
3732
3725
|
command,
|
|
@@ -3842,41 +3835,6 @@ const normalizeThemeBuilderHex = (value: string): string | null => {
|
|
|
3842
3835
|
return /^[\da-f]{6}$/i.test(hex) ? `#${hex.toLowerCase()}` : null
|
|
3843
3836
|
}
|
|
3844
3837
|
|
|
3845
|
-
const hexToThemeBuilderHsl = (value: string): { hue: number, value: string } | null => {
|
|
3846
|
-
const normalized = normalizeThemeBuilderHex(value)
|
|
3847
|
-
|
|
3848
|
-
if (!normalized) return null
|
|
3849
|
-
|
|
3850
|
-
const red = Number.parseInt(normalized.slice(1, 3), 16) / 255
|
|
3851
|
-
const green = Number.parseInt(normalized.slice(3, 5), 16) / 255
|
|
3852
|
-
const blue = Number.parseInt(normalized.slice(5, 7), 16) / 255
|
|
3853
|
-
const max = Math.max(red, green, blue)
|
|
3854
|
-
const min = Math.min(red, green, blue)
|
|
3855
|
-
const lightness = (max + min) / 2
|
|
3856
|
-
const delta = max - min
|
|
3857
|
-
let hue = 0
|
|
3858
|
-
let saturation = 0
|
|
3859
|
-
|
|
3860
|
-
if (delta !== 0) {
|
|
3861
|
-
saturation = delta / (1 - Math.abs(2 * lightness - 1))
|
|
3862
|
-
|
|
3863
|
-
if (max === red) {
|
|
3864
|
-
hue = 60 * (((green - blue) / delta) % 6)
|
|
3865
|
-
} else if (max === green) {
|
|
3866
|
-
hue = 60 * ((blue - red) / delta + 2)
|
|
3867
|
-
} else {
|
|
3868
|
-
hue = 60 * ((red - green) / delta + 4)
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
|
-
|
|
3872
|
-
const normalizedHue = normalizeThemeBuilderHue(hue)
|
|
3873
|
-
|
|
3874
|
-
return {
|
|
3875
|
-
hue: normalizedHue,
|
|
3876
|
-
value: `${normalizedHue} ${Math.round(saturation * 100)}% ${Math.round(lightness * 100)}%`
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
3838
|
const setThemeBuilderHexValidity = (input: HTMLInputElement, isValid: boolean): void => {
|
|
3881
3839
|
if (isValid) {
|
|
3882
3840
|
input.removeAttribute('aria-invalid')
|
|
@@ -4067,8 +4025,19 @@ const initThemeBuilders = (scope: ParentNode): void => {
|
|
|
4067
4025
|
const exportFormatButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-ui-theme-export-format]')]
|
|
4068
4026
|
const modeButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-ui-theme-mode]')]
|
|
4069
4027
|
const schemeButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-ui-theme-scheme]')]
|
|
4028
|
+
const presetButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-ui-theme-preset]')]
|
|
4029
|
+
const initialPreset = root.getAttribute('data-ui-theme-preset')
|
|
4030
|
+
let currentPreset = initialPreset === 'custom' ? null : initialPreset
|
|
4031
|
+
|
|
4032
|
+
for (const button of presetButtons) {
|
|
4033
|
+
button.setAttribute('aria-pressed', String(button.getAttribute('data-ui-theme-preset') === (currentPreset ?? 'custom')))
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
setThemeBuilderPressedState(presetButtons, 'data-ui-theme-preset', currentPreset ?? 'custom')
|
|
4070
4037
|
|
|
4071
4038
|
const hasTokenControls = Boolean(
|
|
4039
|
+
presetButtons.length || currentPreset ||
|
|
4040
|
+
root.querySelector('[data-ui-theme-radius-scale], [data-ui-theme-spacing-scale], [data-ui-theme-border-width]') ||
|
|
4072
4041
|
brandHue ||
|
|
4073
4042
|
brandHueNumber ||
|
|
4074
4043
|
accentHue ||
|
|
@@ -4144,29 +4113,20 @@ const initThemeBuilders = (scope: ParentNode): void => {
|
|
|
4144
4113
|
)
|
|
4145
4114
|
)
|
|
4146
4115
|
|
|
4147
|
-
const
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4116
|
+
const result = createThemeBuilderTokens({
|
|
4117
|
+
hue,
|
|
4118
|
+
accentHue: nextAccentHue,
|
|
4119
|
+
mode,
|
|
4120
|
+
scheme: currentScheme,
|
|
4121
|
+
primaryColor: primaryHex?.value ?? primaryColor?.value ?? null,
|
|
4122
|
+
secondaryColor: secondaryHex?.value ?? secondaryColor?.value ?? null,
|
|
4123
|
+
preset: currentPreset,
|
|
4124
|
+
radiusScale: root.querySelector<HTMLInputElement>('[data-ui-theme-radius-scale]')?.value ?? null,
|
|
4125
|
+
spacingScale: root.querySelector<HTMLInputElement>('[data-ui-theme-spacing-scale]')?.value ?? null,
|
|
4126
|
+
borderWidth: root.querySelector<HTMLInputElement>('[data-ui-theme-border-width]')?.value ?? null
|
|
4157
4127
|
})
|
|
4158
4128
|
|
|
4159
|
-
|
|
4160
|
-
if (primary) {
|
|
4161
|
-
tokens.brand = primary.value
|
|
4162
|
-
|
|
4163
|
-
tokens['brand-solid'] = primary.value
|
|
4164
|
-
}
|
|
4165
|
-
|
|
4166
|
-
if (secondary) {
|
|
4167
|
-
tokens.accent = secondary.value
|
|
4168
|
-
}
|
|
4169
|
-
}
|
|
4129
|
+
const tokens = result.tokens
|
|
4170
4130
|
|
|
4171
4131
|
currentTokens = tokens
|
|
4172
4132
|
|
|
@@ -4182,16 +4142,14 @@ const initThemeBuilders = (scope: ParentNode): void => {
|
|
|
4182
4142
|
|
|
4183
4143
|
root.dispatchEvent(new CustomEvent('ui:theme-change', {
|
|
4184
4144
|
bubbles: true,
|
|
4185
|
-
detail:
|
|
4186
|
-
accentHue: baseAccentHue,
|
|
4187
|
-
hue: baseHue,
|
|
4188
|
-
mode,
|
|
4189
|
-
scheme: currentScheme,
|
|
4190
|
-
tokens
|
|
4191
|
-
}
|
|
4145
|
+
detail: result
|
|
4192
4146
|
}))
|
|
4193
4147
|
}
|
|
4194
4148
|
|
|
4149
|
+
for (const input of root.querySelectorAll<HTMLInputElement>('[data-ui-theme-radius-scale], [data-ui-theme-spacing-scale], [data-ui-theme-border-width]')) {
|
|
4150
|
+
input.addEventListener('input', () => update())
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4195
4153
|
bindSyncedThemeBuilderHueInputs(brandHue, brandHueNumber, update)
|
|
4196
4154
|
|
|
4197
4155
|
bindSyncedThemeBuilderHueInputs(accentHue, accentHueNumber, update)
|
|
@@ -4232,6 +4190,16 @@ const initThemeBuilders = (scope: ParentNode): void => {
|
|
|
4232
4190
|
})
|
|
4233
4191
|
}
|
|
4234
4192
|
|
|
4193
|
+
for (const button of presetButtons) {
|
|
4194
|
+
button.addEventListener('click', () => {
|
|
4195
|
+
currentPreset = button.getAttribute('data-ui-theme-preset') === 'custom' ? null : coerceThemePreset(button.getAttribute('data-ui-theme-preset'))
|
|
4196
|
+
|
|
4197
|
+
setThemeBuilderPressedState(presetButtons, 'data-ui-theme-preset', currentPreset ?? 'custom')
|
|
4198
|
+
|
|
4199
|
+
update()
|
|
4200
|
+
})
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4235
4203
|
for (const button of schemeButtons) {
|
|
4236
4204
|
button.addEventListener('click', () => {
|
|
4237
4205
|
currentScheme = coerceThemeBuilderScheme(button.getAttribute('data-ui-theme-scheme'))
|
|
@@ -4344,6 +4312,8 @@ const initContextMenus = (scope: ParentNode): void => {
|
|
|
4344
4312
|
})
|
|
4345
4313
|
|
|
4346
4314
|
menu.addEventListener('keydown', event => {
|
|
4315
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
4316
|
+
|
|
4347
4317
|
if (event.key === 'Escape') {
|
|
4348
4318
|
closeContextMenu(menu)
|
|
4349
4319
|
|
|
@@ -4360,9 +4330,13 @@ const initContextMenus = (scope: ParentNode): void => {
|
|
|
4360
4330
|
|
|
4361
4331
|
event.preventDefault()
|
|
4362
4332
|
|
|
4363
|
-
const currentIndex = items.
|
|
4333
|
+
const currentIndex = items.findIndex(item => item === menu.ownerDocument.activeElement)
|
|
4364
4334
|
|
|
4365
|
-
|
|
4335
|
+
const nextIndex = currentIndex < 0 && event.key === 'ArrowUp' ?
|
|
4336
|
+
items.length - 1 :
|
|
4337
|
+
getLoopedIndex(event.key, currentIndex, items.length, ['ArrowDown'])
|
|
4338
|
+
|
|
4339
|
+
items[nextIndex]?.focus()
|
|
4366
4340
|
})
|
|
4367
4341
|
|
|
4368
4342
|
menu.addEventListener('click', event => {
|
|
@@ -4478,6 +4452,9 @@ type ToastApi = {
|
|
|
4478
4452
|
const defaultToastDuration = 5000
|
|
4479
4453
|
const defaultToastMax = 5
|
|
4480
4454
|
const noop = (): void => undefined
|
|
4455
|
+
// Tracks the Document the toast API is bound to, since Astro's HTML swap resets any
|
|
4456
|
+
// documentElement dataset flag on navigation even though these document listeners persist.
|
|
4457
|
+
let toastDocumentApiDocument: Document | null = null
|
|
4481
4458
|
|
|
4482
4459
|
const toastTimers = new WeakMap<HTMLElement, {
|
|
4483
4460
|
cleanup: () => void
|
|
@@ -4567,39 +4544,68 @@ const scheduleToastDismiss = (toast: HTMLElement, duration: number): void => {
|
|
|
4567
4544
|
timer: 0
|
|
4568
4545
|
}
|
|
4569
4546
|
|
|
4547
|
+
let hovered = false
|
|
4548
|
+
let paused = false
|
|
4549
|
+
|
|
4570
4550
|
const start = (): void => {
|
|
4551
|
+
paused = false
|
|
4552
|
+
|
|
4571
4553
|
timerState.startedAt = Date.now()
|
|
4572
4554
|
|
|
4573
4555
|
timerState.timer = window.setTimeout(() => dismissToast(toast), timerState.remaining)
|
|
4574
4556
|
}
|
|
4575
4557
|
|
|
4576
4558
|
const pause = (): void => {
|
|
4559
|
+
if (paused) return
|
|
4560
|
+
|
|
4561
|
+
paused = true
|
|
4562
|
+
|
|
4577
4563
|
window.clearTimeout(timerState.timer)
|
|
4578
4564
|
|
|
4579
4565
|
timerState.remaining = Math.max(0, timerState.remaining - (Date.now() - timerState.startedAt))
|
|
4580
4566
|
}
|
|
4581
4567
|
|
|
4582
4568
|
const resume = (): void => {
|
|
4583
|
-
if (
|
|
4569
|
+
if (!paused || hovered || toast.contains(document.activeElement)) return
|
|
4570
|
+
|
|
4571
|
+
start()
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
const enter = (): void => {
|
|
4575
|
+
hovered = true
|
|
4576
|
+
|
|
4577
|
+
pause()
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
const leave = (): void => {
|
|
4581
|
+
hovered = false
|
|
4582
|
+
|
|
4583
|
+
resume()
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
const blur = (event: FocusEvent): void => {
|
|
4587
|
+
if (event.relatedTarget instanceof Node && toast.contains(event.relatedTarget)) return
|
|
4588
|
+
|
|
4589
|
+
resume()
|
|
4584
4590
|
}
|
|
4585
4591
|
|
|
4586
4592
|
timerState.cleanup = () => {
|
|
4587
|
-
toast.removeEventListener('mouseenter',
|
|
4593
|
+
toast.removeEventListener('mouseenter', enter)
|
|
4588
4594
|
|
|
4589
|
-
toast.removeEventListener('mouseleave',
|
|
4595
|
+
toast.removeEventListener('mouseleave', leave)
|
|
4590
4596
|
|
|
4591
4597
|
toast.removeEventListener('focusin', pause)
|
|
4592
4598
|
|
|
4593
|
-
toast.removeEventListener('focusout',
|
|
4599
|
+
toast.removeEventListener('focusout', blur)
|
|
4594
4600
|
}
|
|
4595
4601
|
|
|
4596
|
-
toast.addEventListener('mouseenter',
|
|
4602
|
+
toast.addEventListener('mouseenter', enter)
|
|
4597
4603
|
|
|
4598
|
-
toast.addEventListener('mouseleave',
|
|
4604
|
+
toast.addEventListener('mouseleave', leave)
|
|
4599
4605
|
|
|
4600
4606
|
toast.addEventListener('focusin', pause)
|
|
4601
4607
|
|
|
4602
|
-
toast.addEventListener('focusout',
|
|
4608
|
+
toast.addEventListener('focusout', blur)
|
|
4603
4609
|
|
|
4604
4610
|
toastTimers.set(toast, timerState)
|
|
4605
4611
|
|
|
@@ -4683,14 +4689,10 @@ const setToastContent = (toast: HTMLElement, detail: ToastDetail): void => {
|
|
|
4683
4689
|
toast.append(dismiss)
|
|
4684
4690
|
}
|
|
4685
4691
|
|
|
4686
|
-
const
|
|
4687
|
-
if (document.documentElement.dataset.uiToastsBound === 'true') return
|
|
4688
|
-
|
|
4689
|
-
document.documentElement.dataset.uiToastsBound = 'true'
|
|
4690
|
-
|
|
4692
|
+
const bindToastDocumentApi = (): void => {
|
|
4691
4693
|
const createToast = (detail: ToastDetail): string => {
|
|
4692
4694
|
const viewport = getToastViewport(detail.placement)
|
|
4693
|
-
const id = detail.id ??
|
|
4695
|
+
const id = detail.id ?? nextRuntimeId('ui-toast', document)
|
|
4694
4696
|
const duration = detail.duration ?? defaultToastDuration
|
|
4695
4697
|
const max = detail.max ?? Number(viewport.dataset.uiToastMax ?? defaultToastMax)
|
|
4696
4698
|
const existingToast = document.getElementById(id)
|
|
@@ -4713,9 +4715,13 @@ const initToasts = (): void => {
|
|
|
4713
4715
|
|
|
4714
4716
|
toast.dataset.uiToast = ''
|
|
4715
4717
|
|
|
4718
|
+
toast.dataset.uiBound = 'true'
|
|
4719
|
+
|
|
4716
4720
|
toast.dataset.state = 'open'
|
|
4717
4721
|
|
|
4718
4722
|
toast.addEventListener('keydown', event => {
|
|
4723
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
4724
|
+
|
|
4719
4725
|
if (event.key !== 'Escape') return
|
|
4720
4726
|
|
|
4721
4727
|
event.preventDefault()
|
|
@@ -4768,24 +4774,6 @@ const initToasts = (): void => {
|
|
|
4768
4774
|
|
|
4769
4775
|
;(window as Window & { LumenToast?: ToastApi }).LumenToast = api
|
|
4770
4776
|
|
|
4771
|
-
for (const viewport of document.querySelectorAll<HTMLElement>('[data-ui-toast-viewport]')) {
|
|
4772
|
-
viewport.dataset.placement = getToastPlacement(viewport.dataset.placement)
|
|
4773
|
-
|
|
4774
|
-
viewport.setAttribute('aria-live', viewport.getAttribute('aria-live') ?? 'polite')
|
|
4775
|
-
|
|
4776
|
-
viewport.setAttribute('aria-atomic', viewport.getAttribute('aria-atomic') ?? 'false')
|
|
4777
|
-
}
|
|
4778
|
-
|
|
4779
|
-
for (const toast of document.querySelectorAll<HTMLElement>('[data-ui-toast], .ui-toast')) {
|
|
4780
|
-
toast.addEventListener('keydown', event => {
|
|
4781
|
-
if (event.key !== 'Escape') return
|
|
4782
|
-
|
|
4783
|
-
event.preventDefault()
|
|
4784
|
-
|
|
4785
|
-
dismissToast(toast)
|
|
4786
|
-
})
|
|
4787
|
-
}
|
|
4788
|
-
|
|
4789
4777
|
document.addEventListener('ui:toast', event => {
|
|
4790
4778
|
createToast(event instanceof CustomEvent ? event.detail as ToastDetail : {})
|
|
4791
4779
|
})
|
|
@@ -4803,63 +4791,35 @@ const initToasts = (): void => {
|
|
|
4803
4791
|
})
|
|
4804
4792
|
}
|
|
4805
4793
|
|
|
4806
|
-
const
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
const input = root.querySelector<HTMLInputElement>('[data-ui-file-upload-input]')
|
|
4811
|
-
|
|
4812
|
-
if (!input) continue
|
|
4813
|
-
|
|
4814
|
-
root.dataset.uiBound = 'true'
|
|
4815
|
-
|
|
4816
|
-
const files = root.querySelector<HTMLElement>('[data-ui-file-upload-files]')
|
|
4817
|
-
|
|
4818
|
-
const renderFiles = (): void => {
|
|
4819
|
-
const selectedFiles = input.files ? [...input.files] : []
|
|
4820
|
-
|
|
4821
|
-
root.dataset.state = selectedFiles.length > 0 ? 'selected' : 'idle'
|
|
4794
|
+
const initToasts = (scope: ParentNode): void => {
|
|
4795
|
+
if (toastDocumentApiDocument !== document) {
|
|
4796
|
+
toastDocumentApiDocument = document
|
|
4822
4797
|
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
if (selectedFiles.length === 1) {
|
|
4826
|
-
files.textContent = selectedFiles[0]?.name ?? ''
|
|
4827
|
-
} else {
|
|
4828
|
-
files.textContent = selectedFiles.length > 1 ?
|
|
4829
|
-
`${selectedFiles.length} files selected` :
|
|
4830
|
-
''
|
|
4831
|
-
}
|
|
4832
|
-
}
|
|
4798
|
+
bindToastDocumentApi()
|
|
4799
|
+
}
|
|
4833
4800
|
|
|
4834
|
-
|
|
4801
|
+
for (const viewport of scope.querySelectorAll<HTMLElement>('[data-ui-toast-viewport]')) {
|
|
4802
|
+
viewport.dataset.placement = getToastPlacement(viewport.dataset.placement)
|
|
4835
4803
|
|
|
4836
|
-
|
|
4837
|
-
if (input.disabled) return
|
|
4804
|
+
viewport.setAttribute('aria-live', viewport.getAttribute('aria-live') ?? 'polite')
|
|
4838
4805
|
|
|
4839
|
-
|
|
4806
|
+
viewport.setAttribute('aria-atomic', viewport.getAttribute('aria-atomic') ?? 'false')
|
|
4807
|
+
}
|
|
4840
4808
|
|
|
4841
|
-
|
|
4842
|
-
|
|
4809
|
+
for (const toast of scope.querySelectorAll<HTMLElement>('[data-ui-toast], .ui-toast')) {
|
|
4810
|
+
if (toast.dataset.uiBound === 'true') continue
|
|
4843
4811
|
|
|
4844
|
-
|
|
4845
|
-
if (event.relatedTarget instanceof Node && root.contains(event.relatedTarget)) return
|
|
4812
|
+
toast.dataset.uiBound = 'true'
|
|
4846
4813
|
|
|
4847
|
-
|
|
4848
|
-
|
|
4814
|
+
toast.addEventListener('keydown', event => {
|
|
4815
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
4849
4816
|
|
|
4850
|
-
|
|
4851
|
-
if (input.disabled) return
|
|
4817
|
+
if (event.key !== 'Escape') return
|
|
4852
4818
|
|
|
4853
4819
|
event.preventDefault()
|
|
4854
4820
|
|
|
4855
|
-
|
|
4856
|
-
input.files = event.dataTransfer.files
|
|
4857
|
-
|
|
4858
|
-
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
4859
|
-
}
|
|
4821
|
+
dismissToast(toast)
|
|
4860
4822
|
})
|
|
4861
|
-
|
|
4862
|
-
renderFiles()
|
|
4863
4823
|
}
|
|
4864
4824
|
}
|
|
4865
4825
|
|
|
@@ -5021,6 +4981,10 @@ const initMentions = (scope: ParentNode): void => {
|
|
|
5021
4981
|
const options = [...list.querySelectorAll<HTMLButtonElement>('[data-ui-mentions-option]')]
|
|
5022
4982
|
let activeIndex = -1
|
|
5023
4983
|
|
|
4984
|
+
for (const option of options) {
|
|
4985
|
+
option.tabIndex = -1
|
|
4986
|
+
}
|
|
4987
|
+
|
|
5024
4988
|
const setActive = (index: number): void => {
|
|
5025
4989
|
const visibleOptions = options.filter(option => !option.hidden)
|
|
5026
4990
|
|
|
@@ -5094,6 +5058,8 @@ const initMentions = (scope: ParentNode): void => {
|
|
|
5094
5058
|
input.addEventListener('keydown', event => {
|
|
5095
5059
|
const visibleOptions = options.filter(option => !option.hidden)
|
|
5096
5060
|
|
|
5061
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
5062
|
+
|
|
5097
5063
|
if (event.key === 'Escape' && !list.hidden) {
|
|
5098
5064
|
event.preventDefault()
|
|
5099
5065
|
|
|
@@ -5113,7 +5079,9 @@ const initMentions = (scope: ParentNode): void => {
|
|
|
5113
5079
|
}
|
|
5114
5080
|
})
|
|
5115
5081
|
|
|
5116
|
-
input.addEventListener('blur', () => window.setTimeout(
|
|
5082
|
+
input.addEventListener('blur', () => window.setTimeout(() => {
|
|
5083
|
+
if (!root.contains(root.ownerDocument.activeElement)) close()
|
|
5084
|
+
}, 120))
|
|
5117
5085
|
|
|
5118
5086
|
for (const option of options) {
|
|
5119
5087
|
option.addEventListener('mousedown', event => {
|
|
@@ -5173,7 +5141,15 @@ const initSpeedDials = (scope: ParentNode): void => {
|
|
|
5173
5141
|
})
|
|
5174
5142
|
|
|
5175
5143
|
trigger.addEventListener('keydown', event => {
|
|
5176
|
-
if (event.
|
|
5144
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
5145
|
+
|
|
5146
|
+
if (event.key === 'Escape' && root.dataset.state === 'open') {
|
|
5147
|
+
event.preventDefault()
|
|
5148
|
+
|
|
5149
|
+
closeSpeedDial(root)
|
|
5150
|
+
|
|
5151
|
+
return
|
|
5152
|
+
}
|
|
5177
5153
|
|
|
5178
5154
|
if (['ArrowDown', 'ArrowRight'].includes(event.key)) {
|
|
5179
5155
|
event.preventDefault()
|
|
@@ -5197,6 +5173,8 @@ const initSpeedDials = (scope: ParentNode): void => {
|
|
|
5197
5173
|
})
|
|
5198
5174
|
|
|
5199
5175
|
actions.addEventListener('keydown', event => {
|
|
5176
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
5177
|
+
|
|
5200
5178
|
if (event.key === 'Escape') {
|
|
5201
5179
|
event.preventDefault()
|
|
5202
5180
|
|
|
@@ -5382,6 +5360,18 @@ const initOptionalControllers = (scope: ParentNode): void => {
|
|
|
5382
5360
|
})
|
|
5383
5361
|
}
|
|
5384
5362
|
|
|
5363
|
+
initOptionalMediaControllers(scope).catch(reportError)
|
|
5364
|
+
|
|
5365
|
+
if (scope.querySelector('[data-ui-tab-indicator], [data-ui-chart-motion], [data-ui-motion-group], [data-ui-visual-effect="spotlight"], [data-ui-prompt-composer], [data-ui-approval-card]')) {
|
|
5366
|
+
import('./controllers/visual-interactions.js')
|
|
5367
|
+
.then(({ initVisualInteractionControllers }) => initVisualInteractionControllers(scope))
|
|
5368
|
+
.catch(reportError)
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
if (scope.querySelector('[data-ui-chart-interactive]')) {
|
|
5372
|
+
import('./controllers/charts.js').then(({ initChartControllers }) => initChartControllers(scope)).catch(reportError)
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5385
5375
|
if (scope.querySelector(
|
|
5386
5376
|
'[data-ui-back-to-top], [data-ui-scroll-reveal], [data-ui-reveal-group], [data-ui-animated-number]'
|
|
5387
5377
|
)) {
|
|
@@ -5390,6 +5380,18 @@ const initOptionalControllers = (scope: ParentNode): void => {
|
|
|
5390
5380
|
.catch(reportError)
|
|
5391
5381
|
}
|
|
5392
5382
|
|
|
5383
|
+
if (scope.querySelector('[data-ui-attachment-preview]')) {
|
|
5384
|
+
import('./controllers/attachments.js')
|
|
5385
|
+
.then(({ initAttachmentPreviewControllers }) => initAttachmentPreviewControllers(scope))
|
|
5386
|
+
.catch(reportError)
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
if (scope.querySelector('[data-ui-chart-activation]:not([data-ui-chart-adapter="react"]):not([data-ui-chart-adapter="elements"])')) {
|
|
5390
|
+
import('./controllers/chart-activation.js')
|
|
5391
|
+
.then(({ initChartActivationControllers }) => initChartActivationControllers(scope))
|
|
5392
|
+
.catch(reportError)
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5393
5395
|
if (scope.querySelector('[data-ui-phone-input]')) {
|
|
5394
5396
|
import('./controllers/phone-input.js')
|
|
5395
5397
|
.then(({ initPhoneInputControllers }) => initPhoneInputControllers(scope))
|
|
@@ -5450,7 +5452,11 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
|
|
|
5450
5452
|
|
|
5451
5453
|
initDatePickers(scope)
|
|
5452
5454
|
|
|
5453
|
-
|
|
5455
|
+
initAmountFields(scope)
|
|
5456
|
+
|
|
5457
|
+
initMessageFeeds(scope)
|
|
5458
|
+
|
|
5459
|
+
initCodeRegions(scope)
|
|
5454
5460
|
|
|
5455
5461
|
initCopyButtons(scope)
|
|
5456
5462
|
|
|
@@ -5480,8 +5486,6 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
|
|
|
5480
5486
|
|
|
5481
5487
|
initGlassTracking(scope)
|
|
5482
5488
|
|
|
5483
|
-
initFileUploads(scope)
|
|
5484
|
-
|
|
5485
5489
|
initTours(scope)
|
|
5486
5490
|
|
|
5487
5491
|
initTransfers(scope)
|
|
@@ -5497,7 +5501,7 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
|
|
|
5497
5501
|
|
|
5498
5502
|
initOptionalControllers(scope)
|
|
5499
5503
|
|
|
5500
|
-
initToasts()
|
|
5504
|
+
initToasts(scope)
|
|
5501
5505
|
}
|
|
5502
5506
|
|
|
5503
5507
|
const lumenWindow = window as Window & { LumenInitUiPrimitives?: (scope?: ParentNode) => void }
|
|
@@ -5550,30 +5554,49 @@ if (document.documentElement.dataset.uiPrimitivesGlobalBound !== 'true') {
|
|
|
5550
5554
|
})
|
|
5551
5555
|
|
|
5552
5556
|
document.addEventListener('keydown', event => {
|
|
5553
|
-
if (event.key !== 'Escape') return
|
|
5557
|
+
if (event.defaultPrevented || event.isComposing || event.key !== 'Escape') return
|
|
5554
5558
|
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5559
|
+
// The first open owner in the event path is the innermost active popup.
|
|
5560
|
+
for (const target of event.composedPath()) {
|
|
5561
|
+
if (!(target instanceof HTMLElement)) continue
|
|
5558
5562
|
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
}
|
|
5563
|
+
if (target.matches(disclosureSelector)) {
|
|
5564
|
+
const trigger = target.querySelector<HTMLElement>('[data-ui-trigger]')
|
|
5562
5565
|
|
|
5563
|
-
|
|
5564
|
-
closeSelect(root)
|
|
5565
|
-
}
|
|
5566
|
+
if (trigger?.getAttribute('aria-expanded') !== 'true') continue
|
|
5566
5567
|
|
|
5567
|
-
|
|
5568
|
-
closeDatePicker(root, root.contains(document.activeElement))
|
|
5569
|
-
}
|
|
5568
|
+
event.preventDefault()
|
|
5570
5569
|
|
|
5571
|
-
|
|
5572
|
-
closeContextMenu(menu)
|
|
5573
|
-
}
|
|
5570
|
+
closeDisclosure(target)
|
|
5574
5571
|
|
|
5575
|
-
|
|
5576
|
-
|
|
5572
|
+
trigger.focus({ preventScroll: true })
|
|
5573
|
+
|
|
5574
|
+
return
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
if (target.matches('[data-ui-date-picker]') && target.querySelector('[aria-expanded="true"]')) {
|
|
5578
|
+
event.preventDefault()
|
|
5579
|
+
|
|
5580
|
+
closeDatePicker(target, true)
|
|
5581
|
+
|
|
5582
|
+
return
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
if (target.matches('[data-ui-context-menu][data-state="open"]')) {
|
|
5586
|
+
event.preventDefault()
|
|
5587
|
+
|
|
5588
|
+
closeContextMenu(target)
|
|
5589
|
+
|
|
5590
|
+
return
|
|
5591
|
+
}
|
|
5592
|
+
|
|
5593
|
+
if (target.matches('[data-ui-speed-dial][data-state="open"]')) {
|
|
5594
|
+
event.preventDefault()
|
|
5595
|
+
|
|
5596
|
+
closeSpeedDial(target, true)
|
|
5597
|
+
|
|
5598
|
+
return
|
|
5599
|
+
}
|
|
5577
5600
|
}
|
|
5578
5601
|
})
|
|
5579
5602
|
|