@rio-m3e/core 0.2.0 → 0.3.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 +2 -0
- package/dist/index.d.ts +535 -21
- package/dist/index.js +1757 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ const buttonGroupSkeleton = {
|
|
|
32
32
|
tag: "div",
|
|
33
33
|
content: true
|
|
34
34
|
};
|
|
35
|
-
const valuesOf = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
|
|
35
|
+
const valuesOf$1 = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
|
|
36
36
|
function createButtonGroup(options) {
|
|
37
37
|
const props = () => ({
|
|
38
38
|
...buttonGroupDefaults,
|
|
@@ -40,7 +40,7 @@ function createButtonGroup(options) {
|
|
|
40
40
|
});
|
|
41
41
|
const isSelected = (value) => {
|
|
42
42
|
const p = props();
|
|
43
|
-
return p.selection !== "none" && valuesOf(p.selected).includes(value);
|
|
43
|
+
return p.selection !== "none" && valuesOf$1(p.selected).includes(value);
|
|
44
44
|
};
|
|
45
45
|
return {
|
|
46
46
|
parts: { root() {
|
|
@@ -60,7 +60,7 @@ function createButtonGroup(options) {
|
|
|
60
60
|
select(value, selected) {
|
|
61
61
|
const p = props();
|
|
62
62
|
if (p.selection === "none" || isSelected(value) === selected) return;
|
|
63
|
-
const current = valuesOf(p.selected);
|
|
63
|
+
const current = valuesOf$1(p.selected);
|
|
64
64
|
if (!selected && p.required && current.length === 1) return;
|
|
65
65
|
if (p.selection === "single") options.onChange(selected ? value : void 0);
|
|
66
66
|
else options.onChange(selected ? [...current, value] : current.filter((other) => other !== value));
|
|
@@ -199,7 +199,7 @@ function readSpringParams(element, token) {
|
|
|
199
199
|
const PRESSED_WIDTH_RATIO = .15;
|
|
200
200
|
function attachButtonGroup(root, options = {}) {
|
|
201
201
|
const ratio = options.pressedWidthRatio ?? PRESSED_WIDTH_RATIO;
|
|
202
|
-
const items = () => [...root.children].filter((el) => el instanceof HTMLElement);
|
|
202
|
+
const items = () => [...root.children].map((el) => el.classList.contains("rio-tooltip-trigger") ? el.firstElementChild : el).filter((el) => el instanceof HTMLElement);
|
|
203
203
|
const onKeydown = (event) => {
|
|
204
204
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
205
205
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
@@ -269,7 +269,8 @@ function attachButtonGroup(root, options = {}) {
|
|
|
269
269
|
}
|
|
270
270
|
};
|
|
271
271
|
const observer = new MutationObserver((mutations) => {
|
|
272
|
-
const
|
|
272
|
+
const current = items();
|
|
273
|
+
const own = mutations.filter((m) => current.includes(m.target));
|
|
273
274
|
if (own.length > 0) layout(own.some((m) => m.attributeName !== "data-pressed"));
|
|
274
275
|
});
|
|
275
276
|
observer.observe(root, {
|
|
@@ -384,6 +385,448 @@ function createButton(options) {
|
|
|
384
385
|
}
|
|
385
386
|
};
|
|
386
387
|
}
|
|
388
|
+
// src/dialog/dialog.ts
|
|
389
|
+
/**
|
|
390
|
+
* The dialog's ARIA role: `alertdialog` for a prompt that asks for a response, such as a
|
|
391
|
+
* confirmation, and `dialog` for one that holds a list or a form.
|
|
392
|
+
*/
|
|
393
|
+
const DIALOG_ROLES = ["alertdialog", "dialog"];
|
|
394
|
+
const dialogDefaults = {
|
|
395
|
+
open: false,
|
|
396
|
+
role: "alertdialog"
|
|
397
|
+
};
|
|
398
|
+
/**
|
|
399
|
+
* The markup of an open dialog. The parts inside render from opening until `attachDialog`'s
|
|
400
|
+
* `onClosed`; a closed dialog is the empty `<dialog>`.
|
|
401
|
+
*/
|
|
402
|
+
const dialogSkeleton = {
|
|
403
|
+
part: "root",
|
|
404
|
+
tag: "dialog",
|
|
405
|
+
children: [
|
|
406
|
+
{
|
|
407
|
+
part: "icon",
|
|
408
|
+
tag: "div",
|
|
409
|
+
when: "showsIcon",
|
|
410
|
+
content: true
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
part: "headline",
|
|
414
|
+
tag: "h2",
|
|
415
|
+
when: "showsHeadline",
|
|
416
|
+
content: true
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
part: "content",
|
|
420
|
+
tag: "div",
|
|
421
|
+
content: true
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
part: "actions",
|
|
425
|
+
tag: "div",
|
|
426
|
+
when: "showsActions",
|
|
427
|
+
content: true
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
const NO_CONTENT$2 = {
|
|
432
|
+
icon: false,
|
|
433
|
+
headline: false,
|
|
434
|
+
actions: false
|
|
435
|
+
};
|
|
436
|
+
function createDialog(options) {
|
|
437
|
+
const props = () => ({
|
|
438
|
+
...dialogDefaults,
|
|
439
|
+
...omitUndefined(options.props())
|
|
440
|
+
});
|
|
441
|
+
const content = () => options.content?.() ?? NO_CONTENT$2;
|
|
442
|
+
const showsIcon = () => content().icon;
|
|
443
|
+
const showsHeadline = () => content().headline;
|
|
444
|
+
const showsActions = () => content().actions;
|
|
445
|
+
const { ids } = options;
|
|
446
|
+
return {
|
|
447
|
+
parts: {
|
|
448
|
+
root() {
|
|
449
|
+
const alert = props().role === "alertdialog";
|
|
450
|
+
return {
|
|
451
|
+
class: "rio-dialog",
|
|
452
|
+
role: alert ? "alertdialog" : void 0,
|
|
453
|
+
"aria-labelledby": showsHeadline() ? ids.headline : void 0,
|
|
454
|
+
"aria-describedby": alert ? ids.content : void 0,
|
|
455
|
+
"data-icon": showsIcon() ? "" : void 0
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
icon: () => ({
|
|
459
|
+
class: "rio-dialog__icon",
|
|
460
|
+
"aria-hidden": "true"
|
|
461
|
+
}),
|
|
462
|
+
headline: () => ({
|
|
463
|
+
class: "rio-dialog__headline",
|
|
464
|
+
id: ids.headline
|
|
465
|
+
}),
|
|
466
|
+
content: () => ({
|
|
467
|
+
class: "rio-dialog__content rio-focus-ring",
|
|
468
|
+
id: ids.content,
|
|
469
|
+
"data-focus-ring": "inner"
|
|
470
|
+
}),
|
|
471
|
+
actions: () => ({ class: "rio-dialog__actions" })
|
|
472
|
+
},
|
|
473
|
+
showsIcon,
|
|
474
|
+
showsHeadline,
|
|
475
|
+
showsActions
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// src/primitives/overlay/overlay.ts
|
|
479
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
480
|
+
/** What a `contain`ed overlay keeps in */
|
|
481
|
+
const PRESSES = [
|
|
482
|
+
"click",
|
|
483
|
+
"auxclick",
|
|
484
|
+
"dblclick",
|
|
485
|
+
"pointerdown",
|
|
486
|
+
"pointerup",
|
|
487
|
+
"mousedown",
|
|
488
|
+
"mouseup",
|
|
489
|
+
"contextmenu"
|
|
490
|
+
];
|
|
491
|
+
const stopPropagation = (event) => event.stopPropagation();
|
|
492
|
+
const isOpen = ({ element, options }) => options.mode === "modal" ? element.open : element.matches(":popover-open");
|
|
493
|
+
/** Open on the record, but no longer in the top layer, after leaving the document */
|
|
494
|
+
const left = (entry) => entry.options.mode === "modal" ? !entry.element.matches(":modal") : !isOpen(entry);
|
|
495
|
+
const close = (entry) => {
|
|
496
|
+
if (!isOpen(entry)) return;
|
|
497
|
+
if (entry.options.mode === "modal") entry.element.close();
|
|
498
|
+
else entry.element.hidePopover();
|
|
499
|
+
};
|
|
500
|
+
const track = (registry) => {
|
|
501
|
+
if (registry.stack.length) registry.moves.observe(registry.document, {
|
|
502
|
+
childList: true,
|
|
503
|
+
subtree: true
|
|
504
|
+
});
|
|
505
|
+
else registry.moves.disconnect();
|
|
506
|
+
};
|
|
507
|
+
const opened = (entry) => {
|
|
508
|
+
const { stack } = entry.registry;
|
|
509
|
+
if (stack.includes(entry)) return;
|
|
510
|
+
stack.push(entry);
|
|
511
|
+
track(entry.registry);
|
|
512
|
+
for (const other of stack.filter((open) => open !== entry && open.options.transient)) {
|
|
513
|
+
other.reason = "replaced";
|
|
514
|
+
close(other);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
const closed = (entry, asked) => {
|
|
518
|
+
const { stack } = entry.registry;
|
|
519
|
+
const index = stack.indexOf(entry);
|
|
520
|
+
if (index < 0) return;
|
|
521
|
+
stack.splice(index, 1);
|
|
522
|
+
track(entry.registry);
|
|
523
|
+
const reason = entry.reason ?? "dismiss";
|
|
524
|
+
entry.reason = void 0;
|
|
525
|
+
if (!asked) entry.options.onClose(reason);
|
|
526
|
+
const inside = (node) => !!node && entry.element.contains(node);
|
|
527
|
+
for (const other of stack.filter((open) => inside(open.element) || inside(open.source))) {
|
|
528
|
+
other.reason = "parent";
|
|
529
|
+
close(other);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const registryOf = (document) => {
|
|
533
|
+
let registry = registries.get(document);
|
|
534
|
+
if (!registry) {
|
|
535
|
+
const stack = [];
|
|
536
|
+
const onKeydown = (event) => {
|
|
537
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
538
|
+
const top = stack.at(-1);
|
|
539
|
+
if (!top) return;
|
|
540
|
+
const target = event.target;
|
|
541
|
+
const host = typeof target?.closest === "function" ? target.closest("dialog:modal, :popover-open:not([popover=\"manual\"])") : null;
|
|
542
|
+
if (host && !stack.some((open) => open.element === host)) return;
|
|
543
|
+
event.preventDefault();
|
|
544
|
+
top.options.onClose("escape");
|
|
545
|
+
};
|
|
546
|
+
registry = {
|
|
547
|
+
stack,
|
|
548
|
+
attached: 0,
|
|
549
|
+
onKeydown,
|
|
550
|
+
moves: new MutationObserver(() => {
|
|
551
|
+
for (const entry of stack.filter(left)) {
|
|
552
|
+
if (entry.options.mode === "modal") entry.element.close();
|
|
553
|
+
closed(entry, false);
|
|
554
|
+
}
|
|
555
|
+
}),
|
|
556
|
+
document
|
|
557
|
+
};
|
|
558
|
+
registries.set(document, registry);
|
|
559
|
+
}
|
|
560
|
+
return registry;
|
|
561
|
+
};
|
|
562
|
+
function attachOverlay(element, options) {
|
|
563
|
+
const modal = options.mode === "modal";
|
|
564
|
+
if (modal && element.localName !== "dialog") throw new TypeError("A modal overlay must be a <dialog> element.");
|
|
565
|
+
const document = element.ownerDocument;
|
|
566
|
+
const registry = registryOf(document);
|
|
567
|
+
const entry = {
|
|
568
|
+
element,
|
|
569
|
+
options,
|
|
570
|
+
registry
|
|
571
|
+
};
|
|
572
|
+
const onToggle = () => isOpen(entry) ? opened(entry) : closed(entry, false);
|
|
573
|
+
const onDialogClose = () => {
|
|
574
|
+
if (!isOpen(entry)) closed(entry, false);
|
|
575
|
+
};
|
|
576
|
+
const onCancel = (event) => {
|
|
577
|
+
event.preventDefault();
|
|
578
|
+
options.onClose("escape");
|
|
579
|
+
};
|
|
580
|
+
if (modal) {
|
|
581
|
+
element.addEventListener("close", onDialogClose);
|
|
582
|
+
element.addEventListener("cancel", onCancel);
|
|
583
|
+
} else {
|
|
584
|
+
element.popover = options.mode;
|
|
585
|
+
element.addEventListener("toggle", onToggle);
|
|
586
|
+
}
|
|
587
|
+
if (options.contain) for (const type of PRESSES) element.addEventListener(type, stopPropagation);
|
|
588
|
+
if (registry.attached++ === 0) document.addEventListener("keydown", registry.onKeydown);
|
|
589
|
+
let detached = false;
|
|
590
|
+
return {
|
|
591
|
+
show(source) {
|
|
592
|
+
if (detached || isOpen(entry)) return;
|
|
593
|
+
const index = registry.stack.indexOf(entry);
|
|
594
|
+
if (index >= 0) registry.stack.splice(index, 1);
|
|
595
|
+
track(registry);
|
|
596
|
+
entry.reason = void 0;
|
|
597
|
+
entry.source = source;
|
|
598
|
+
if (modal) element.showModal();
|
|
599
|
+
else element.showPopover({ source });
|
|
600
|
+
opened(entry);
|
|
601
|
+
},
|
|
602
|
+
hide() {
|
|
603
|
+
if (!isOpen(entry)) return;
|
|
604
|
+
close(entry);
|
|
605
|
+
closed(entry, true);
|
|
606
|
+
},
|
|
607
|
+
covered() {
|
|
608
|
+
const index = registry.stack.indexOf(entry);
|
|
609
|
+
return index >= 0 && registry.stack.slice(index + 1).some((open) => !open.options.transient);
|
|
610
|
+
},
|
|
611
|
+
detach() {
|
|
612
|
+
if (detached) return;
|
|
613
|
+
detached = true;
|
|
614
|
+
element.removeEventListener("close", onDialogClose);
|
|
615
|
+
element.removeEventListener("cancel", onCancel);
|
|
616
|
+
element.removeEventListener("toggle", onToggle);
|
|
617
|
+
for (const type of PRESSES) element.removeEventListener(type, stopPropagation);
|
|
618
|
+
const index = registry.stack.indexOf(entry);
|
|
619
|
+
if (index >= 0) registry.stack.splice(index, 1);
|
|
620
|
+
track(registry);
|
|
621
|
+
close(entry);
|
|
622
|
+
if (!modal) element.removeAttribute("popover");
|
|
623
|
+
if (--registry.attached === 0) document.removeEventListener("keydown", registry.onKeydown);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
// src/dialog/interaction.ts
|
|
628
|
+
const TABBABLE = [
|
|
629
|
+
"a[href]",
|
|
630
|
+
"area[href]",
|
|
631
|
+
"button",
|
|
632
|
+
"input:not([type=\"hidden\"])",
|
|
633
|
+
"select",
|
|
634
|
+
"textarea",
|
|
635
|
+
"iframe",
|
|
636
|
+
"audio[controls]",
|
|
637
|
+
"video[controls]",
|
|
638
|
+
"summary",
|
|
639
|
+
"[contenteditable]:not([contenteditable=\"false\"])",
|
|
640
|
+
"[tabindex]"
|
|
641
|
+
].join(",");
|
|
642
|
+
const isRadio = (element) => element.localName === "input" && element.type === "radio";
|
|
643
|
+
const visible = (element) => typeof element.checkVisibility === "function" ? element.checkVisibility({ visibilityProperty: true }) : element.getClientRects().length > 0;
|
|
644
|
+
/** The elements Tab stops at in `root`, in order. Elements inside shadow roots aren't found. */
|
|
645
|
+
const tabbablesIn = (root) => {
|
|
646
|
+
const all = [...root.querySelectorAll(TABBABLE)].filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest("[inert]") && visible(element));
|
|
647
|
+
return all.filter((element) => {
|
|
648
|
+
if (!isRadio(element) || !element.name) return true;
|
|
649
|
+
const group = all.filter((other) => isRadio(other) && other.name === element.name && other.form === element.form);
|
|
650
|
+
return element === (group.find((radio) => radio.checked) ?? group[0]);
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
/** How far, in px, the content may scroll and still count as at its end */
|
|
654
|
+
const SCROLL_SLOP = 1;
|
|
655
|
+
const scrollLocks = /* @__PURE__ */ new WeakMap();
|
|
656
|
+
const lockScroll = (document) => {
|
|
657
|
+
const lock = scrollLocks.get(document);
|
|
658
|
+
if (lock) {
|
|
659
|
+
lock.count++;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const html = document.documentElement;
|
|
663
|
+
const view = document.defaultView;
|
|
664
|
+
const { overflow, scrollbarGutter } = html.style;
|
|
665
|
+
const scrollbar = view.innerWidth - html.clientWidth;
|
|
666
|
+
html.style.overflow = "hidden";
|
|
667
|
+
if (scrollbar > 0 && view.getComputedStyle(html).scrollbarGutter === "auto") html.style.scrollbarGutter = "stable";
|
|
668
|
+
scrollLocks.set(document, {
|
|
669
|
+
count: 1,
|
|
670
|
+
restore() {
|
|
671
|
+
html.style.overflow = overflow;
|
|
672
|
+
html.style.scrollbarGutter = scrollbarGutter;
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
};
|
|
676
|
+
const unlockScroll = (document) => {
|
|
677
|
+
const lock = scrollLocks.get(document);
|
|
678
|
+
if (!lock || --lock.count > 0) return;
|
|
679
|
+
lock.restore();
|
|
680
|
+
scrollLocks.delete(document);
|
|
681
|
+
};
|
|
682
|
+
function attachDialog(root, options) {
|
|
683
|
+
const document = root.ownerDocument;
|
|
684
|
+
const cancel = (reason) => {
|
|
685
|
+
let prevented = false;
|
|
686
|
+
options.onCancel?.({
|
|
687
|
+
reason,
|
|
688
|
+
get defaultPrevented() {
|
|
689
|
+
return prevented;
|
|
690
|
+
},
|
|
691
|
+
preventDefault() {
|
|
692
|
+
prevented = true;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
if (!prevented) options.onOpenChange(false);
|
|
696
|
+
};
|
|
697
|
+
const overlay = attachOverlay(root, {
|
|
698
|
+
mode: "modal",
|
|
699
|
+
contain: true,
|
|
700
|
+
onClose: (reason) => reason === "escape" ? cancel("escape") : options.onOpenChange(false)
|
|
701
|
+
});
|
|
702
|
+
const onKeydown = (event) => {
|
|
703
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
704
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
705
|
+
const tabbables = tabbablesIn(root);
|
|
706
|
+
const first = tabbables[0];
|
|
707
|
+
const last = tabbables.at(-1);
|
|
708
|
+
if (!first || !last) {
|
|
709
|
+
event.preventDefault();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const focused = document.activeElement;
|
|
713
|
+
if (!(event.shiftKey ? focused === first || focused === root : focused === last)) return;
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
(event.shiftKey ? last : first).focus();
|
|
716
|
+
};
|
|
717
|
+
const onScrim = (event) => {
|
|
718
|
+
if (event.target !== root) return false;
|
|
719
|
+
const box = root.getBoundingClientRect();
|
|
720
|
+
return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
|
|
721
|
+
};
|
|
722
|
+
let pressedScrim = false;
|
|
723
|
+
const onPointerDown = (event) => {
|
|
724
|
+
pressedScrim = onScrim(event) && !overlay.covered();
|
|
725
|
+
};
|
|
726
|
+
const onClick = (event) => {
|
|
727
|
+
const pressed = pressedScrim;
|
|
728
|
+
pressedScrim = false;
|
|
729
|
+
if (pressed && onScrim(event)) cancel("scrim");
|
|
730
|
+
};
|
|
731
|
+
const content = () => root.querySelector(":scope > .rio-dialog__content");
|
|
732
|
+
const updateDividers = () => {
|
|
733
|
+
const element = content();
|
|
734
|
+
if (!element) return;
|
|
735
|
+
const hidden = element.scrollHeight - element.clientHeight;
|
|
736
|
+
element.toggleAttribute("data-overflow-top", element.scrollTop >= SCROLL_SLOP);
|
|
737
|
+
element.toggleAttribute("data-overflow-bottom", hidden - element.scrollTop >= SCROLL_SLOP);
|
|
738
|
+
};
|
|
739
|
+
/** The content, while it takes the focus so the keyboard can scroll it */
|
|
740
|
+
let reachable = null;
|
|
741
|
+
const setReachable = (element) => {
|
|
742
|
+
if (element === reachable) return;
|
|
743
|
+
reachable?.removeAttribute("tabindex");
|
|
744
|
+
reachable = element;
|
|
745
|
+
reachable?.setAttribute("tabindex", "0");
|
|
746
|
+
};
|
|
747
|
+
const updateOverflow = () => {
|
|
748
|
+
updateDividers();
|
|
749
|
+
const element = content();
|
|
750
|
+
const scrolls = !!element && element.scrollHeight - element.clientHeight >= SCROLL_SLOP;
|
|
751
|
+
setReachable(scrolls && tabbablesIn(element).length === 0 ? element : null);
|
|
752
|
+
};
|
|
753
|
+
const resizes = new ResizeObserver(updateOverflow);
|
|
754
|
+
let watched = null;
|
|
755
|
+
const watch = () => {
|
|
756
|
+
resizes.disconnect();
|
|
757
|
+
watched?.removeEventListener("scroll", updateDividers);
|
|
758
|
+
watched = content();
|
|
759
|
+
if (!watched) return;
|
|
760
|
+
watched.addEventListener("scroll", updateDividers, { passive: true });
|
|
761
|
+
resizes.observe(watched);
|
|
762
|
+
for (const child of watched.children) resizes.observe(child);
|
|
763
|
+
};
|
|
764
|
+
const unwatch = () => {
|
|
765
|
+
resizes.disconnect();
|
|
766
|
+
watched?.removeEventListener("scroll", updateDividers);
|
|
767
|
+
watched = null;
|
|
768
|
+
setReachable(null);
|
|
769
|
+
};
|
|
770
|
+
const focusFirst = () => {
|
|
771
|
+
if (root.querySelector("[autofocus]")) return;
|
|
772
|
+
const tabbables = tabbablesIn(root);
|
|
773
|
+
(tabbables.find((element) => element !== reachable) ?? tabbables[0])?.focus();
|
|
774
|
+
};
|
|
775
|
+
/** Whether this dialog holds a lock on the page's scroll: from opening until it closes */
|
|
776
|
+
let locked = false;
|
|
777
|
+
/** Counts closings, so only the last one reports its end, and only if the dialog stayed closed */
|
|
778
|
+
let closings = 0;
|
|
779
|
+
const closed = () => {
|
|
780
|
+
if (!locked) return;
|
|
781
|
+
locked = false;
|
|
782
|
+
unlockScroll(document);
|
|
783
|
+
const closing = ++closings;
|
|
784
|
+
const motion = root.getAnimations({ subtree: true }).map((animation) => animation.finished.catch(() => void 0));
|
|
785
|
+
Promise.all(motion).then(() => {
|
|
786
|
+
if (closing === closings && !root.open) options.onClosed?.();
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
root.addEventListener("keydown", onKeydown);
|
|
790
|
+
root.addEventListener("pointerdown", onPointerDown);
|
|
791
|
+
root.addEventListener("click", onClick);
|
|
792
|
+
return {
|
|
793
|
+
sync(open) {
|
|
794
|
+
if (open && !root.open) {
|
|
795
|
+
overlay.show();
|
|
796
|
+
if (!locked) {
|
|
797
|
+
locked = true;
|
|
798
|
+
lockScroll(document);
|
|
799
|
+
}
|
|
800
|
+
const element = content();
|
|
801
|
+
if (element) element.scrollTop = 0;
|
|
802
|
+
watch();
|
|
803
|
+
updateOverflow();
|
|
804
|
+
focusFirst();
|
|
805
|
+
} else if (!open) {
|
|
806
|
+
if (root.open) overlay.hide();
|
|
807
|
+
unwatch();
|
|
808
|
+
closed();
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
update() {
|
|
812
|
+
if (!root.open) return;
|
|
813
|
+
watch();
|
|
814
|
+
updateOverflow();
|
|
815
|
+
},
|
|
816
|
+
detach() {
|
|
817
|
+
root.removeEventListener("keydown", onKeydown);
|
|
818
|
+
root.removeEventListener("pointerdown", onPointerDown);
|
|
819
|
+
root.removeEventListener("click", onClick);
|
|
820
|
+
unwatch();
|
|
821
|
+
overlay.detach();
|
|
822
|
+
closings++;
|
|
823
|
+
if (locked) {
|
|
824
|
+
locked = false;
|
|
825
|
+
unlockScroll(document);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
387
830
|
// src/icon-button/icon-button.ts
|
|
388
831
|
const ICON_BUTTON_VARIANTS = [
|
|
389
832
|
"filled",
|
|
@@ -456,6 +899,943 @@ function createIconButton(options) {
|
|
|
456
899
|
handleClick: button.handleClick
|
|
457
900
|
};
|
|
458
901
|
}
|
|
902
|
+
// src/primitives/anchored-position/anchored-position.ts
|
|
903
|
+
const OPPOSITE = {
|
|
904
|
+
top: "bottom",
|
|
905
|
+
bottom: "top",
|
|
906
|
+
start: "end",
|
|
907
|
+
end: "start"
|
|
908
|
+
};
|
|
909
|
+
const VARS = [
|
|
910
|
+
"--rio-anchored-available-width",
|
|
911
|
+
"--rio-anchored-available-height",
|
|
912
|
+
"--rio-anchored-anchor-width"
|
|
913
|
+
];
|
|
914
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
915
|
+
/** An anchor point, rather than an element. Checked by shape, so elements from iframes count. */
|
|
916
|
+
const isPoint = (target) => typeof target.getBoundingClientRect !== "function";
|
|
917
|
+
function attachAnchoredPosition(floating, options) {
|
|
918
|
+
const document = floating.ownerDocument;
|
|
919
|
+
const view = document.defaultView;
|
|
920
|
+
floating.dataset.anchored = "";
|
|
921
|
+
const offset = options.offset ?? 0;
|
|
922
|
+
const alignOffset = options.alignOffset ?? 0;
|
|
923
|
+
const padding = options.padding ?? 8;
|
|
924
|
+
const preferred = options.side ?? "bottom";
|
|
925
|
+
const align = options.align ?? "start";
|
|
926
|
+
const candidates = [.../* @__PURE__ */ new Set([preferred, ...options.fallbacks ?? [OPPOSITE[preferred]]])];
|
|
927
|
+
/** Rounds to the device's pixel grid, so text in the element stays sharp. */
|
|
928
|
+
const snap = (value) => Math.round(value * view.devicePixelRatio) / view.devicePixelRatio;
|
|
929
|
+
let frame;
|
|
930
|
+
const schedule = () => {
|
|
931
|
+
frame ??= view.requestAnimationFrame(() => {
|
|
932
|
+
frame = void 0;
|
|
933
|
+
place();
|
|
934
|
+
});
|
|
935
|
+
};
|
|
936
|
+
const onScroll = (event) => {
|
|
937
|
+
if (event.target !== document && floating.contains(event.target)) return;
|
|
938
|
+
schedule();
|
|
939
|
+
};
|
|
940
|
+
const resize = new view.ResizeObserver(schedule);
|
|
941
|
+
resize.observe(floating);
|
|
942
|
+
let observed;
|
|
943
|
+
const observeAnchor = (element) => {
|
|
944
|
+
if (element === observed) return;
|
|
945
|
+
if (observed) resize.unobserve(observed);
|
|
946
|
+
observed = element;
|
|
947
|
+
if (element) resize.observe(element);
|
|
948
|
+
};
|
|
949
|
+
const place = () => {
|
|
950
|
+
const target = options.anchor();
|
|
951
|
+
if (!target || floating.getClientRects().length === 0) return;
|
|
952
|
+
if (!isPoint(target) && !target.isConnected) return;
|
|
953
|
+
observeAnchor(isPoint(target) ? void 0 : target);
|
|
954
|
+
const anchor = isPoint(target) ? {
|
|
955
|
+
left: target.x,
|
|
956
|
+
right: target.x,
|
|
957
|
+
top: target.y,
|
|
958
|
+
bottom: target.y,
|
|
959
|
+
width: 0
|
|
960
|
+
} : target.getBoundingClientRect();
|
|
961
|
+
const rtl = view.getComputedStyle(isPoint(target) ? floating : target).direction === "rtl";
|
|
962
|
+
const viewport = {
|
|
963
|
+
width: document.documentElement.clientWidth,
|
|
964
|
+
height: document.documentElement.clientHeight
|
|
965
|
+
};
|
|
966
|
+
const physical = (side) => side === "start" ? rtl ? "right" : "left" : side === "end" ? rtl ? "left" : "right" : side;
|
|
967
|
+
const room = {
|
|
968
|
+
top: anchor.top - offset - padding,
|
|
969
|
+
bottom: viewport.height - anchor.bottom - offset - padding,
|
|
970
|
+
left: anchor.left - offset - padding,
|
|
971
|
+
right: viewport.width - anchor.right - offset - padding
|
|
972
|
+
};
|
|
973
|
+
floating.style.left = "0px";
|
|
974
|
+
floating.style.top = "0px";
|
|
975
|
+
const borders = {
|
|
976
|
+
width: floating.offsetWidth - floating.clientWidth,
|
|
977
|
+
height: floating.offsetHeight - floating.clientHeight
|
|
978
|
+
};
|
|
979
|
+
const natural = {
|
|
980
|
+
width: floating.scrollWidth + borders.width,
|
|
981
|
+
height: floating.scrollHeight + borders.height
|
|
982
|
+
};
|
|
983
|
+
const fits = (candidate) => {
|
|
984
|
+
const at = physical(candidate);
|
|
985
|
+
return at === "top" || at === "bottom" ? room[at] >= natural.height && natural.width <= viewport.width - 2 * padding : room[at] >= natural.width && natural.height <= viewport.height - 2 * padding;
|
|
986
|
+
};
|
|
987
|
+
const roomier = (a, b) => room[physical(b)] > room[physical(a)] ? b : a;
|
|
988
|
+
const side = candidates.find(fits) ?? roomier(preferred, OPPOSITE[preferred]);
|
|
989
|
+
const at = physical(side);
|
|
990
|
+
const vertical = at === "top" || at === "bottom";
|
|
991
|
+
const available = {
|
|
992
|
+
width: vertical ? viewport.width - 2 * padding : room[at],
|
|
993
|
+
height: vertical ? room[at] : viewport.height - 2 * padding
|
|
994
|
+
};
|
|
995
|
+
floating.style.setProperty(VARS[0], `${Math.max(0, available.width)}px`);
|
|
996
|
+
floating.style.setProperty(VARS[1], `${Math.max(0, available.height)}px`);
|
|
997
|
+
floating.style.setProperty(VARS[2], `${anchor.width}px`);
|
|
998
|
+
const size = {
|
|
999
|
+
width: floating.offsetWidth,
|
|
1000
|
+
height: floating.offsetHeight
|
|
1001
|
+
};
|
|
1002
|
+
let x;
|
|
1003
|
+
let y;
|
|
1004
|
+
if (vertical) {
|
|
1005
|
+
y = at === "bottom" ? anchor.bottom + offset : anchor.top - offset - size.height;
|
|
1006
|
+
const startEdge = rtl ? anchor.right - size.width : anchor.left;
|
|
1007
|
+
const endEdge = rtl ? anchor.left : anchor.right - size.width;
|
|
1008
|
+
x = (align === "center" ? anchor.left + (anchor.width - size.width) / 2 : align === "start" ? startEdge : endEdge) + (rtl ? -alignOffset : alignOffset);
|
|
1009
|
+
} else {
|
|
1010
|
+
x = at === "right" ? anchor.right + offset : anchor.left - offset - size.width;
|
|
1011
|
+
const height = anchor.bottom - anchor.top;
|
|
1012
|
+
y = (align === "center" ? anchor.top + (height - size.height) / 2 : align === "start" ? anchor.top : anchor.bottom - size.height) + alignOffset;
|
|
1013
|
+
}
|
|
1014
|
+
const fit = (value, length, extent, startAtEnd) => length > extent - 2 * padding ? startAtEnd ? extent - padding - length : padding : clamp(value, padding, extent - padding - length);
|
|
1015
|
+
x = fit(x, size.width, viewport.width, rtl);
|
|
1016
|
+
y = fit(y, size.height, viewport.height, false);
|
|
1017
|
+
floating.style.left = `${snap(x)}px`;
|
|
1018
|
+
floating.style.top = `${snap(y)}px`;
|
|
1019
|
+
floating.dataset.side = side;
|
|
1020
|
+
floating.dataset.align = align;
|
|
1021
|
+
if (anchor.bottom < 0 || anchor.top > viewport.height || anchor.right < 0 || anchor.left > viewport.width) floating.dataset.anchorHidden = "";
|
|
1022
|
+
else delete floating.dataset.anchorHidden;
|
|
1023
|
+
};
|
|
1024
|
+
document.addEventListener("scroll", onScroll, {
|
|
1025
|
+
capture: true,
|
|
1026
|
+
passive: true
|
|
1027
|
+
});
|
|
1028
|
+
view.addEventListener("resize", schedule, { passive: true });
|
|
1029
|
+
return {
|
|
1030
|
+
update: place,
|
|
1031
|
+
detach() {
|
|
1032
|
+
document.removeEventListener("scroll", onScroll, { capture: true });
|
|
1033
|
+
view.removeEventListener("resize", schedule);
|
|
1034
|
+
resize.disconnect();
|
|
1035
|
+
if (frame !== void 0) view.cancelAnimationFrame(frame);
|
|
1036
|
+
for (const name of [
|
|
1037
|
+
"left",
|
|
1038
|
+
"top",
|
|
1039
|
+
...VARS
|
|
1040
|
+
]) floating.style.removeProperty(name);
|
|
1041
|
+
for (const name of [
|
|
1042
|
+
"side",
|
|
1043
|
+
"align",
|
|
1044
|
+
"anchorHidden",
|
|
1045
|
+
"anchored"
|
|
1046
|
+
]) delete floating.dataset[name];
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
// src/menu/menu.ts
|
|
1051
|
+
/** M3E vertical menu color styles: surface-based, or tertiary-based for more emphasis. */
|
|
1052
|
+
const MENU_VARIANTS = ["standard", "vibrant"];
|
|
1053
|
+
/** Whether the menu's items can be selected: none, one at a time, or any number. */
|
|
1054
|
+
const MENU_SELECTIONS = [
|
|
1055
|
+
"none",
|
|
1056
|
+
"single",
|
|
1057
|
+
"multiple"
|
|
1058
|
+
];
|
|
1059
|
+
const menuDefaults = {
|
|
1060
|
+
open: false,
|
|
1061
|
+
variant: "standard",
|
|
1062
|
+
selection: "none",
|
|
1063
|
+
side: "bottom",
|
|
1064
|
+
align: "start",
|
|
1065
|
+
offset: 0,
|
|
1066
|
+
instant: false
|
|
1067
|
+
};
|
|
1068
|
+
const menuSkeleton = {
|
|
1069
|
+
part: "root",
|
|
1070
|
+
tag: "div",
|
|
1071
|
+
content: true
|
|
1072
|
+
};
|
|
1073
|
+
const valuesOf = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
|
|
1074
|
+
function createMenu(options) {
|
|
1075
|
+
const props = () => ({
|
|
1076
|
+
...menuDefaults,
|
|
1077
|
+
...omitUndefined(options.props())
|
|
1078
|
+
});
|
|
1079
|
+
const isSelected = (value) => {
|
|
1080
|
+
const p = props();
|
|
1081
|
+
return p.selection !== "none" && valuesOf(p.selected).includes(value);
|
|
1082
|
+
};
|
|
1083
|
+
return {
|
|
1084
|
+
parts: { root() {
|
|
1085
|
+
const p = props();
|
|
1086
|
+
return {
|
|
1087
|
+
class: "rio-menu",
|
|
1088
|
+
role: "menu",
|
|
1089
|
+
"aria-label": p.label,
|
|
1090
|
+
"data-variant": p.variant,
|
|
1091
|
+
"data-instant": p.instant ? "" : void 0
|
|
1092
|
+
};
|
|
1093
|
+
} },
|
|
1094
|
+
selection: () => props().selection,
|
|
1095
|
+
isSelected,
|
|
1096
|
+
choose(value) {
|
|
1097
|
+
const p = props();
|
|
1098
|
+
if (p.selection !== "none" && value !== void 0) {
|
|
1099
|
+
if (p.selection === "single") {
|
|
1100
|
+
if (!isSelected(value)) options.onChange(value);
|
|
1101
|
+
} else {
|
|
1102
|
+
const current = valuesOf(p.selected);
|
|
1103
|
+
options.onChange(isSelected(value) ? current.filter((other) => other !== value) : [...current, value]);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
options.onOpenChange(false);
|
|
1108
|
+
},
|
|
1109
|
+
close: () => options.onOpenChange(false),
|
|
1110
|
+
variant: () => props().variant,
|
|
1111
|
+
instant: () => props().instant
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const menuItemDefaults = { disabled: false };
|
|
1115
|
+
const menuItemSkeleton = {
|
|
1116
|
+
part: "root",
|
|
1117
|
+
tag: "button",
|
|
1118
|
+
children: [
|
|
1119
|
+
{
|
|
1120
|
+
part: "touch",
|
|
1121
|
+
tag: "span"
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
part: "icon",
|
|
1125
|
+
tag: "span",
|
|
1126
|
+
when: "showsIcon",
|
|
1127
|
+
content: true
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
part: "text",
|
|
1131
|
+
tag: "span",
|
|
1132
|
+
children: [{
|
|
1133
|
+
part: "label",
|
|
1134
|
+
tag: "span",
|
|
1135
|
+
content: true
|
|
1136
|
+
}, {
|
|
1137
|
+
part: "supportingText",
|
|
1138
|
+
tag: "span",
|
|
1139
|
+
when: "showsSupportingText",
|
|
1140
|
+
content: true
|
|
1141
|
+
}]
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
part: "trailingText",
|
|
1145
|
+
tag: "span",
|
|
1146
|
+
when: "showsTrailingText",
|
|
1147
|
+
content: true
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
part: "trailingIcon",
|
|
1151
|
+
tag: "span",
|
|
1152
|
+
when: "showsTrailingIcon",
|
|
1153
|
+
content: true
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
};
|
|
1157
|
+
const NO_CONTENT$1 = {
|
|
1158
|
+
icon: false,
|
|
1159
|
+
supportingText: false,
|
|
1160
|
+
trailingText: false,
|
|
1161
|
+
trailingIcon: false
|
|
1162
|
+
};
|
|
1163
|
+
function createMenuItem(options) {
|
|
1164
|
+
const props = () => ({
|
|
1165
|
+
...menuItemDefaults,
|
|
1166
|
+
...omitUndefined(options.props())
|
|
1167
|
+
});
|
|
1168
|
+
const content = () => options.content?.() ?? NO_CONTENT$1;
|
|
1169
|
+
const kind = () => {
|
|
1170
|
+
if (options.submenu) return void 0;
|
|
1171
|
+
const selection = options.menu?.()?.selection() ?? "none";
|
|
1172
|
+
if (selection === "none" || props().value === void 0) return void 0;
|
|
1173
|
+
return selection === "single" ? "radio" : "checkbox";
|
|
1174
|
+
};
|
|
1175
|
+
const selected = () => {
|
|
1176
|
+
const value = props().value;
|
|
1177
|
+
return kind() !== void 0 && !!options.menu?.()?.isSelected(value);
|
|
1178
|
+
};
|
|
1179
|
+
return {
|
|
1180
|
+
parts: {
|
|
1181
|
+
root() {
|
|
1182
|
+
const p = props();
|
|
1183
|
+
const k = kind();
|
|
1184
|
+
return {
|
|
1185
|
+
class: "rio-menu-item rio-state-layer rio-focus-ring",
|
|
1186
|
+
type: "button",
|
|
1187
|
+
role: k ? `menuitem${k}` : "menuitem",
|
|
1188
|
+
tabindex: "-1",
|
|
1189
|
+
"aria-checked": k ? String(selected()) : void 0,
|
|
1190
|
+
"aria-haspopup": options.submenu ? "menu" : void 0,
|
|
1191
|
+
"aria-expanded": options.submenu ? String(options.submenu.open()) : void 0,
|
|
1192
|
+
"aria-disabled": p.disabled ? "true" : void 0,
|
|
1193
|
+
"data-selected": k ? String(selected()) : void 0,
|
|
1194
|
+
"data-focus-ring": "inner"
|
|
1195
|
+
};
|
|
1196
|
+
},
|
|
1197
|
+
touch: () => ({
|
|
1198
|
+
class: "rio-menu-item__touch",
|
|
1199
|
+
"aria-hidden": "true"
|
|
1200
|
+
}),
|
|
1201
|
+
icon: () => ({
|
|
1202
|
+
class: "rio-menu-item__icon",
|
|
1203
|
+
"aria-hidden": "true"
|
|
1204
|
+
}),
|
|
1205
|
+
text: () => ({ class: "rio-menu-item__text" }),
|
|
1206
|
+
label: () => ({ class: "rio-menu-item__label" }),
|
|
1207
|
+
supportingText: () => ({ class: "rio-menu-item__supporting-text" }),
|
|
1208
|
+
trailingText: () => ({ class: "rio-menu-item__trailing-text" }),
|
|
1209
|
+
trailingIcon: () => ({
|
|
1210
|
+
class: "rio-menu-item__trailing-icon",
|
|
1211
|
+
"aria-hidden": "true"
|
|
1212
|
+
})
|
|
1213
|
+
},
|
|
1214
|
+
showsIcon: () => content().icon,
|
|
1215
|
+
showsSupportingText: () => content().supportingText,
|
|
1216
|
+
showsTrailingText: () => content().trailingText,
|
|
1217
|
+
showsTrailingIcon: () => content().trailingIcon,
|
|
1218
|
+
selected,
|
|
1219
|
+
handleClick(event) {
|
|
1220
|
+
if (props().disabled) {
|
|
1221
|
+
event.preventDefault();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
options.onClick(event);
|
|
1225
|
+
if (options.submenu) options.submenu.onOpen();
|
|
1226
|
+
else options.menu?.()?.choose(props().value);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* A submenu and the item that opens it. The two render side by side in the menu, the item where it
|
|
1232
|
+
* sits and the submenu after it; the submenu shows in the top layer next to the item.
|
|
1233
|
+
*/
|
|
1234
|
+
function createSubMenu(options) {
|
|
1235
|
+
const menu = createMenu({
|
|
1236
|
+
props: () => {
|
|
1237
|
+
const p = options.props();
|
|
1238
|
+
const parent = options.menu?.();
|
|
1239
|
+
return {
|
|
1240
|
+
open: options.open(),
|
|
1241
|
+
variant: parent?.variant(),
|
|
1242
|
+
instant: parent?.instant(),
|
|
1243
|
+
selection: p.selection,
|
|
1244
|
+
selected: p.selected,
|
|
1245
|
+
label: p.label
|
|
1246
|
+
};
|
|
1247
|
+
},
|
|
1248
|
+
onChange: options.onChange,
|
|
1249
|
+
onOpenChange: (open) => {
|
|
1250
|
+
const parent = options.menu?.();
|
|
1251
|
+
if (open) options.onOpenChange(true);
|
|
1252
|
+
else if (parent) parent.close();
|
|
1253
|
+
else options.onOpenChange(false);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
item: createMenuItem({
|
|
1258
|
+
props: () => ({ disabled: options.props().disabled }),
|
|
1259
|
+
content: options.content,
|
|
1260
|
+
onClick: () => {},
|
|
1261
|
+
submenu: {
|
|
1262
|
+
open: options.open,
|
|
1263
|
+
onOpen: () => options.onOpenChange(true)
|
|
1264
|
+
}
|
|
1265
|
+
}),
|
|
1266
|
+
menu
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
const menuGroupSkeleton = {
|
|
1270
|
+
part: "root",
|
|
1271
|
+
tag: "div",
|
|
1272
|
+
children: [{
|
|
1273
|
+
part: "label",
|
|
1274
|
+
tag: "div",
|
|
1275
|
+
when: "showsLabel",
|
|
1276
|
+
content: true
|
|
1277
|
+
}, {
|
|
1278
|
+
part: "items",
|
|
1279
|
+
tag: "div",
|
|
1280
|
+
content: true
|
|
1281
|
+
}]
|
|
1282
|
+
};
|
|
1283
|
+
function createMenuGroup(options) {
|
|
1284
|
+
return {
|
|
1285
|
+
parts: {
|
|
1286
|
+
root: () => ({
|
|
1287
|
+
class: "rio-menu-group",
|
|
1288
|
+
role: "group",
|
|
1289
|
+
"aria-label": options.props().label
|
|
1290
|
+
}),
|
|
1291
|
+
label: () => ({
|
|
1292
|
+
class: "rio-menu-group__label",
|
|
1293
|
+
"aria-hidden": "true"
|
|
1294
|
+
}),
|
|
1295
|
+
items: () => ({ class: "rio-menu-group__items" })
|
|
1296
|
+
},
|
|
1297
|
+
showsLabel: () => !!options.props().label
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
const menuDividerSkeleton = {
|
|
1301
|
+
part: "root",
|
|
1302
|
+
tag: "div"
|
|
1303
|
+
};
|
|
1304
|
+
function createMenuDivider() {
|
|
1305
|
+
return { parts: { root: () => ({
|
|
1306
|
+
class: "rio-menu-divider",
|
|
1307
|
+
role: "separator"
|
|
1308
|
+
}) } };
|
|
1309
|
+
}
|
|
1310
|
+
// src/menu/interaction.ts
|
|
1311
|
+
/** Where to try next when the preferred side has no room: across, then to the sides. */
|
|
1312
|
+
const FALLBACKS = {
|
|
1313
|
+
bottom: [
|
|
1314
|
+
"top",
|
|
1315
|
+
"end",
|
|
1316
|
+
"start"
|
|
1317
|
+
],
|
|
1318
|
+
top: [
|
|
1319
|
+
"bottom",
|
|
1320
|
+
"end",
|
|
1321
|
+
"start"
|
|
1322
|
+
],
|
|
1323
|
+
end: [
|
|
1324
|
+
"start",
|
|
1325
|
+
"bottom",
|
|
1326
|
+
"top"
|
|
1327
|
+
],
|
|
1328
|
+
start: [
|
|
1329
|
+
"end",
|
|
1330
|
+
"bottom",
|
|
1331
|
+
"top"
|
|
1332
|
+
]
|
|
1333
|
+
};
|
|
1334
|
+
/** How long typed letters add up to one search. */
|
|
1335
|
+
const TYPEAHEAD_MS = 500;
|
|
1336
|
+
/** How long the pointer rests on an item before its submenu opens */
|
|
1337
|
+
const HOVER_OPEN_MS = 100;
|
|
1338
|
+
/** How long the pointer may take to reach an open submenu across other items */
|
|
1339
|
+
const GRACE_MS = 300;
|
|
1340
|
+
/**
|
|
1341
|
+
* A submenu goes beside its item, 4px off the menu around it (the item is inset 4px), with its
|
|
1342
|
+
* first item lined up with the item (its own are inset 4px from its top).
|
|
1343
|
+
*/
|
|
1344
|
+
const SUBMENU_PLACEMENT = {
|
|
1345
|
+
side: "end",
|
|
1346
|
+
align: "start",
|
|
1347
|
+
offset: 8,
|
|
1348
|
+
alignOffset: -4
|
|
1349
|
+
};
|
|
1350
|
+
const isElement = (anchor) => !!anchor && typeof anchor.focus === "function";
|
|
1351
|
+
/** Points are compared by value: an inline `{ x, y }` is a new object on every render. */
|
|
1352
|
+
const samePoint = (a, b) => a.x === b.x && a.y === b.y && a.element === b.element;
|
|
1353
|
+
/** Whether a point is inside a polygon, by casting a ray to the right and counting crossings */
|
|
1354
|
+
const isInside = ({ x, y }, polygon) => {
|
|
1355
|
+
let inside = false;
|
|
1356
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
1357
|
+
const a = polygon[i];
|
|
1358
|
+
const b = polygon[j];
|
|
1359
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;
|
|
1360
|
+
}
|
|
1361
|
+
return inside;
|
|
1362
|
+
};
|
|
1363
|
+
const menuStates = /* @__PURE__ */ new WeakMap();
|
|
1364
|
+
const stateOf = (menu) => {
|
|
1365
|
+
let state = menuStates.get(menu);
|
|
1366
|
+
if (!state) {
|
|
1367
|
+
state = {
|
|
1368
|
+
openers: /* @__PURE__ */ new Map(),
|
|
1369
|
+
typing: () => false
|
|
1370
|
+
};
|
|
1371
|
+
menuStates.set(menu, state);
|
|
1372
|
+
}
|
|
1373
|
+
return state;
|
|
1374
|
+
};
|
|
1375
|
+
const endGrace = (state) => {
|
|
1376
|
+
if (state.grace) clearTimeout(state.grace.timer);
|
|
1377
|
+
state.grace = void 0;
|
|
1378
|
+
};
|
|
1379
|
+
const menuAround = (element) => element.parentElement?.closest(".rio-menu");
|
|
1380
|
+
const topOf = (menu) => {
|
|
1381
|
+
let top = menu;
|
|
1382
|
+
for (let up = menuAround(menu); up; up = menuAround(up)) top = up;
|
|
1383
|
+
return top;
|
|
1384
|
+
};
|
|
1385
|
+
const activeMenus = /* @__PURE__ */ new WeakMap();
|
|
1386
|
+
const shapeTree = (menu, active) => {
|
|
1387
|
+
const top = topOf(menu);
|
|
1388
|
+
if (active) activeMenus.set(top, active);
|
|
1389
|
+
let current = activeMenus.get(top) ?? top;
|
|
1390
|
+
while (current && !current.matches(":popover-open")) current = menuAround(current);
|
|
1391
|
+
const open = [top, ...top.querySelectorAll(".rio-menu")].filter((each) => each.matches(":popover-open"));
|
|
1392
|
+
for (const each of open) each.toggleAttribute("data-inactive", open.length > 1 && each !== current);
|
|
1393
|
+
};
|
|
1394
|
+
function attachMenu(root, options) {
|
|
1395
|
+
return setup(root, options);
|
|
1396
|
+
}
|
|
1397
|
+
function setup(root, options, nested) {
|
|
1398
|
+
const document = root.ownerDocument;
|
|
1399
|
+
const view = document.defaultView;
|
|
1400
|
+
let reanchoring = false;
|
|
1401
|
+
const overlay = attachOverlay(root, {
|
|
1402
|
+
mode: "auto",
|
|
1403
|
+
onClose: (reason) => {
|
|
1404
|
+
if (!(reanchoring && reason === "dismiss")) options.onClose();
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
const items = () => [...root.querySelectorAll("[role^=\"menuitem\"]")].filter((item) => item.closest("[role=\"menu\"]") === root);
|
|
1408
|
+
const focused = () => items().indexOf(document.activeElement);
|
|
1409
|
+
const focusAt = (index) => {
|
|
1410
|
+
const all = items();
|
|
1411
|
+
if (all.length) all[(index + all.length) % all.length].focus();
|
|
1412
|
+
};
|
|
1413
|
+
let anchored;
|
|
1414
|
+
let placement = "";
|
|
1415
|
+
/** What had the focus when the menu opened */
|
|
1416
|
+
let opener = null;
|
|
1417
|
+
let pinned;
|
|
1418
|
+
const pin = (fresh) => {
|
|
1419
|
+
const anchor = options.anchor();
|
|
1420
|
+
if (!anchor || isElement(anchor) || !anchor.element) {
|
|
1421
|
+
pinned = void 0;
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (!fresh && pinned && samePoint(pinned.point, anchor)) return;
|
|
1425
|
+
const rect = anchor.element.getBoundingClientRect();
|
|
1426
|
+
pinned = {
|
|
1427
|
+
point: { ...anchor },
|
|
1428
|
+
x: anchor.x - rect.left,
|
|
1429
|
+
y: anchor.y - rect.top
|
|
1430
|
+
};
|
|
1431
|
+
};
|
|
1432
|
+
/** The anchor for placing: for a point on an element, where that spot is now */
|
|
1433
|
+
const anchorNow = () => {
|
|
1434
|
+
const anchor = options.anchor();
|
|
1435
|
+
if (!anchor || isElement(anchor) || !anchor.element) return anchor;
|
|
1436
|
+
if (!anchor.element.isConnected) return void 0;
|
|
1437
|
+
if (!pinned || !samePoint(pinned.point, anchor)) pin(false);
|
|
1438
|
+
const rect = anchor.element.getBoundingClientRect();
|
|
1439
|
+
return {
|
|
1440
|
+
x: rect.left + pinned.x,
|
|
1441
|
+
y: rect.top + pinned.y
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
1444
|
+
const place = () => {
|
|
1445
|
+
const p = {
|
|
1446
|
+
...menuDefaults,
|
|
1447
|
+
alignOffset: 0,
|
|
1448
|
+
...omitUndefined(options.props())
|
|
1449
|
+
};
|
|
1450
|
+
const key = `${p.side} ${p.align} ${p.offset} ${p.alignOffset}`;
|
|
1451
|
+
if (anchored && key === placement) return anchored;
|
|
1452
|
+
anchored?.detach();
|
|
1453
|
+
placement = key;
|
|
1454
|
+
anchored = attachAnchoredPosition(root, {
|
|
1455
|
+
anchor: anchorNow,
|
|
1456
|
+
side: p.side,
|
|
1457
|
+
align: p.align,
|
|
1458
|
+
offset: p.offset,
|
|
1459
|
+
alignOffset: p.alignOffset,
|
|
1460
|
+
fallbacks: FALLBACKS[p.side]
|
|
1461
|
+
});
|
|
1462
|
+
return anchored;
|
|
1463
|
+
};
|
|
1464
|
+
const returnTarget = () => {
|
|
1465
|
+
const anchor = options.anchor();
|
|
1466
|
+
if (isElement(anchor) && anchor.isConnected) return anchor;
|
|
1467
|
+
return opener?.isConnected && !root.contains(opener) ? opener : null;
|
|
1468
|
+
};
|
|
1469
|
+
let typed = "";
|
|
1470
|
+
let typedAt = 0;
|
|
1471
|
+
const typeahead = (key) => {
|
|
1472
|
+
const now = performance.now();
|
|
1473
|
+
typed = now - typedAt > TYPEAHEAD_MS ? key : typed + key;
|
|
1474
|
+
typedAt = now;
|
|
1475
|
+
const all = items();
|
|
1476
|
+
const labelOf = (item) => (item.querySelector(".rio-menu-item__label")?.textContent ?? item.textContent ?? "").trim().toLowerCase();
|
|
1477
|
+
const search = [...typed].every((letter) => letter === typed[0]) ? typed[0] : typed;
|
|
1478
|
+
const start = focused() + (search.length === 1 ? 1 : 0);
|
|
1479
|
+
for (let step = 0; step < all.length; step++) {
|
|
1480
|
+
const item = all[(Math.max(start, 0) + step) % all.length];
|
|
1481
|
+
if (labelOf(item).startsWith(search)) {
|
|
1482
|
+
item.focus();
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
const state = stateOf(root);
|
|
1488
|
+
state.typing = () => typed !== "" && performance.now() - typedAt <= TYPEAHEAD_MS;
|
|
1489
|
+
const onKeydown = (event) => {
|
|
1490
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1491
|
+
if (event.key === "Tab") {
|
|
1492
|
+
if (nested) return;
|
|
1493
|
+
options.onClose();
|
|
1494
|
+
returnTarget()?.focus();
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (event.target.closest("[role=\"menu\"]") !== root) return;
|
|
1498
|
+
if (activeMenus.get(topOf(root)) !== root) shapeTree(root, root);
|
|
1499
|
+
const handled = () => event.preventDefault();
|
|
1500
|
+
if (nested && event.key === (isRtl() ? "ArrowRight" : "ArrowLeft")) {
|
|
1501
|
+
handled();
|
|
1502
|
+
options.onClose();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
switch (event.key) {
|
|
1506
|
+
case "ArrowDown":
|
|
1507
|
+
handled();
|
|
1508
|
+
focusAt(focused() + 1);
|
|
1509
|
+
return;
|
|
1510
|
+
case "ArrowUp":
|
|
1511
|
+
handled();
|
|
1512
|
+
focusAt(focused() < 0 ? -1 : focused() - 1);
|
|
1513
|
+
return;
|
|
1514
|
+
case "Home":
|
|
1515
|
+
handled();
|
|
1516
|
+
focusAt(0);
|
|
1517
|
+
return;
|
|
1518
|
+
case "End":
|
|
1519
|
+
handled();
|
|
1520
|
+
focusAt(-1);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (event.key.length === 1 && !(event.key === " " && !typed)) {
|
|
1524
|
+
handled();
|
|
1525
|
+
typeahead(event.key.toLowerCase());
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
root.addEventListener("keydown", onKeydown);
|
|
1529
|
+
const isRtl = () => view.getComputedStyle(root).direction === "rtl";
|
|
1530
|
+
let last;
|
|
1531
|
+
/** Which way the pointer last moved, across and down */
|
|
1532
|
+
const heading = {};
|
|
1533
|
+
const onPointerMove = (event) => {
|
|
1534
|
+
if (event.pointerType !== "mouse") return;
|
|
1535
|
+
const target = event.target;
|
|
1536
|
+
if (target.closest("[role=\"menu\"]") !== root) return;
|
|
1537
|
+
const point = {
|
|
1538
|
+
x: event.clientX,
|
|
1539
|
+
y: event.clientY
|
|
1540
|
+
};
|
|
1541
|
+
if (last && point.x !== last.x) heading.x = point.x > last.x ? "right" : "left";
|
|
1542
|
+
if (last && point.y !== last.y) heading.y = point.y > last.y ? "down" : "up";
|
|
1543
|
+
last = point;
|
|
1544
|
+
if (activeMenus.get(topOf(root)) !== root) shapeTree(root, root);
|
|
1545
|
+
const { grace } = state;
|
|
1546
|
+
if (grace) {
|
|
1547
|
+
if ((grace.toward === "left" || grace.toward === "right" ? heading.x : heading.y) === grace.toward && isInside(point, grace.area)) return;
|
|
1548
|
+
}
|
|
1549
|
+
endGrace(state);
|
|
1550
|
+
const item = target.closest("[role^=\"menuitem\"]");
|
|
1551
|
+
if (!item) return;
|
|
1552
|
+
const focusedMenu = document.activeElement?.closest("[role=\"menu\"]");
|
|
1553
|
+
if (focusedMenu && focusedMenu !== root && topOf(focusedMenu) === topOf(root)) item.focus({ preventScroll: true });
|
|
1554
|
+
for (const [opener, control] of state.openers) if (opener !== item) control.cancel();
|
|
1555
|
+
state.openers.get(item)?.hover();
|
|
1556
|
+
};
|
|
1557
|
+
root.addEventListener("pointermove", onPointerMove);
|
|
1558
|
+
const onFocusIn = (event) => {
|
|
1559
|
+
if (event.target.closest("[role=\"menu\"]") === root) shapeTree(root, root);
|
|
1560
|
+
};
|
|
1561
|
+
root.addEventListener("focusin", onFocusIn);
|
|
1562
|
+
const focusFirst = () => {
|
|
1563
|
+
const all = items();
|
|
1564
|
+
(all.find((item) => item.getAttribute("aria-checked") === "true") ?? all[0])?.focus();
|
|
1565
|
+
};
|
|
1566
|
+
/** What the menu last opened at: its anchor element or point, or null for none */
|
|
1567
|
+
let shownAt = null;
|
|
1568
|
+
const isNewAnchor = () => {
|
|
1569
|
+
const anchor = options.anchor();
|
|
1570
|
+
if (!anchor || !shownAt) return false;
|
|
1571
|
+
if (isElement(anchor) || isElement(shownAt)) return anchor !== shownAt;
|
|
1572
|
+
return !samePoint(anchor, shownAt);
|
|
1573
|
+
};
|
|
1574
|
+
const remember = () => {
|
|
1575
|
+
const anchor = options.anchor();
|
|
1576
|
+
shownAt = !anchor ? null : isElement(anchor) ? anchor : { ...anchor };
|
|
1577
|
+
};
|
|
1578
|
+
/**
|
|
1579
|
+
* Opens the menu. `afresh` opens it as if it had been closed: a menu still open, or still fading
|
|
1580
|
+
* out after a click outside, would only turn its closing back where it is, so that is cut short
|
|
1581
|
+
* and the opening motion plays from the start. By default, at a new anchor.
|
|
1582
|
+
*/
|
|
1583
|
+
const show = (afresh = isNewAnchor()) => {
|
|
1584
|
+
const active = document.activeElement;
|
|
1585
|
+
if (!root.contains(active)) opener = active instanceof document.defaultView.HTMLElement ? active : null;
|
|
1586
|
+
if (afresh) {
|
|
1587
|
+
root.style.setProperty("transition", "none");
|
|
1588
|
+
if (root.matches(":popover-open")) overlay.hide();
|
|
1589
|
+
root.offsetWidth;
|
|
1590
|
+
root.style.removeProperty("transition");
|
|
1591
|
+
}
|
|
1592
|
+
const anchored = place();
|
|
1593
|
+
const anchor = options.anchor();
|
|
1594
|
+
remember();
|
|
1595
|
+
const on = anchor && !isElement(anchor) ? anchor.element : void 0;
|
|
1596
|
+
overlay.show(isElement(anchor) ? anchor : on instanceof document.defaultView.HTMLElement ? on : void 0);
|
|
1597
|
+
anchored.update();
|
|
1598
|
+
const focusing = !nested || nested.focusOnShow();
|
|
1599
|
+
shapeTree(root, focusing ? root : void 0);
|
|
1600
|
+
if (focusing) focusFirst();
|
|
1601
|
+
};
|
|
1602
|
+
let pressing = false;
|
|
1603
|
+
let afterPress;
|
|
1604
|
+
let pressTimer;
|
|
1605
|
+
const release = () => {
|
|
1606
|
+
if (!pressing) return;
|
|
1607
|
+
pressing = false;
|
|
1608
|
+
clearTimeout(pressTimer);
|
|
1609
|
+
const run = afterPress;
|
|
1610
|
+
afterPress = void 0;
|
|
1611
|
+
if (run) setTimeout(run);
|
|
1612
|
+
};
|
|
1613
|
+
const onContextMenu = (event) => {
|
|
1614
|
+
if (event.buttons === 0) return;
|
|
1615
|
+
pressing = true;
|
|
1616
|
+
clearTimeout(pressTimer);
|
|
1617
|
+
pressTimer = setTimeout(release, 1e3);
|
|
1618
|
+
};
|
|
1619
|
+
const onContextMenuDone = (event) => {
|
|
1620
|
+
setTimeout(() => {
|
|
1621
|
+
if (!event.defaultPrevented) release();
|
|
1622
|
+
});
|
|
1623
|
+
};
|
|
1624
|
+
if (!nested) {
|
|
1625
|
+
document.addEventListener("contextmenu", onContextMenu, { capture: true });
|
|
1626
|
+
view.addEventListener("contextmenu", onContextMenuDone);
|
|
1627
|
+
for (const type of ["pointerup", "pointercancel"]) document.addEventListener(type, release, { capture: true });
|
|
1628
|
+
view.addEventListener("blur", release);
|
|
1629
|
+
}
|
|
1630
|
+
let wanted = false;
|
|
1631
|
+
return {
|
|
1632
|
+
sync(open) {
|
|
1633
|
+
wanted = open;
|
|
1634
|
+
const shown = root.matches(":popover-open");
|
|
1635
|
+
if (open && !shown) {
|
|
1636
|
+
pin(true);
|
|
1637
|
+
if (pressing) afterPress = () => wanted && !root.matches(":popover-open") && show();
|
|
1638
|
+
else show();
|
|
1639
|
+
} else if (!open) {
|
|
1640
|
+
const hadFocus = root.contains(document.activeElement);
|
|
1641
|
+
afterPress = void 0;
|
|
1642
|
+
reanchoring = false;
|
|
1643
|
+
overlay.hide();
|
|
1644
|
+
typed = "";
|
|
1645
|
+
shapeTree(root);
|
|
1646
|
+
if (hadFocus) returnTarget()?.focus();
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
update() {
|
|
1650
|
+
pin(false);
|
|
1651
|
+
if (!root.matches(":popover-open")) {
|
|
1652
|
+
if (wanted && !pressing && isNewAnchor()) show(true);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (pressing) {
|
|
1656
|
+
reanchoring = true;
|
|
1657
|
+
afterPress = () => {
|
|
1658
|
+
reanchoring = false;
|
|
1659
|
+
if (wanted) show(true);
|
|
1660
|
+
};
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (isNewAnchor()) show(true);
|
|
1664
|
+
else {
|
|
1665
|
+
remember();
|
|
1666
|
+
place().update();
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
focusFirst,
|
|
1670
|
+
detach() {
|
|
1671
|
+
root.removeEventListener("keydown", onKeydown);
|
|
1672
|
+
root.removeEventListener("pointermove", onPointerMove);
|
|
1673
|
+
root.removeEventListener("focusin", onFocusIn);
|
|
1674
|
+
endGrace(state);
|
|
1675
|
+
document.removeEventListener("contextmenu", onContextMenu, { capture: true });
|
|
1676
|
+
view.removeEventListener("contextmenu", onContextMenuDone);
|
|
1677
|
+
for (const type of ["pointerup", "pointercancel"]) document.removeEventListener(type, release, { capture: true });
|
|
1678
|
+
view.removeEventListener("blur", release);
|
|
1679
|
+
clearTimeout(pressTimer);
|
|
1680
|
+
pressing = false;
|
|
1681
|
+
afterPress = void 0;
|
|
1682
|
+
overlay.detach();
|
|
1683
|
+
anchored?.detach();
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
let submenuIds = 0;
|
|
1688
|
+
/**
|
|
1689
|
+
* A submenu's behavior in the DOM: a menu (see `attachMenu`) that `item` opens, shown beside it.
|
|
1690
|
+
* `item` sits in a menu that `attachMenu` or this function runs, and `root` follows it.
|
|
1691
|
+
*/
|
|
1692
|
+
function attachSubMenu(item, root, options) {
|
|
1693
|
+
const view = item.ownerDocument.defaultView;
|
|
1694
|
+
const parent = item.closest("[role=\"menu\"]");
|
|
1695
|
+
let open = false;
|
|
1696
|
+
/** Whether the opening asked for moves the focus in: from the keyboard, not the pointer */
|
|
1697
|
+
let focusIn = false;
|
|
1698
|
+
const request = (next, focus = false) => {
|
|
1699
|
+
focusIn = focus;
|
|
1700
|
+
options.onOpenChange(next);
|
|
1701
|
+
};
|
|
1702
|
+
const dom = setup(root, {
|
|
1703
|
+
anchor: () => item,
|
|
1704
|
+
props: () => SUBMENU_PLACEMENT,
|
|
1705
|
+
onClose: () => request(false)
|
|
1706
|
+
}, { focusOnShow: () => focusIn });
|
|
1707
|
+
const disabled = () => item.getAttribute("aria-disabled") === "true";
|
|
1708
|
+
root.id ||= `rio-submenu-${++submenuIds}`;
|
|
1709
|
+
item.setAttribute("popovertarget", root.id);
|
|
1710
|
+
item.setAttribute("popovertargetaction", "show");
|
|
1711
|
+
const onClickFirst = (event) => {
|
|
1712
|
+
focusIn = event.detail === 0;
|
|
1713
|
+
};
|
|
1714
|
+
const onClick = (event) => event.preventDefault();
|
|
1715
|
+
item.addEventListener("click", onClickFirst, { capture: true });
|
|
1716
|
+
item.addEventListener("click", onClick);
|
|
1717
|
+
let timer;
|
|
1718
|
+
const stopTimer = () => {
|
|
1719
|
+
clearTimeout(timer);
|
|
1720
|
+
timer = void 0;
|
|
1721
|
+
};
|
|
1722
|
+
const opener = {
|
|
1723
|
+
hover() {
|
|
1724
|
+
if (open || disabled() || timer !== void 0) return;
|
|
1725
|
+
timer = setTimeout(() => {
|
|
1726
|
+
timer = void 0;
|
|
1727
|
+
if (!parent || parent.matches(":popover-open")) request(true);
|
|
1728
|
+
}, HOVER_OPEN_MS);
|
|
1729
|
+
},
|
|
1730
|
+
cancel() {
|
|
1731
|
+
stopTimer();
|
|
1732
|
+
if (open) request(false);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
if (parent) stateOf(parent).openers.set(item, opener);
|
|
1736
|
+
const onPointerLeave = (event) => {
|
|
1737
|
+
if (event.pointerType !== "mouse") return;
|
|
1738
|
+
stopTimer();
|
|
1739
|
+
if (!open || !parent) return;
|
|
1740
|
+
const state = stateOf(parent);
|
|
1741
|
+
const box = root.getBoundingClientRect();
|
|
1742
|
+
const from = item.getBoundingClientRect();
|
|
1743
|
+
const toward = box.top >= from.bottom - 1 ? "down" : box.bottom <= from.top + 1 ? "up" : box.left >= from.left ? "right" : "left";
|
|
1744
|
+
const across = toward === "left" || toward === "right";
|
|
1745
|
+
const bleed = toward === "right" || toward === "down" ? -5 : 5;
|
|
1746
|
+
const exit = across ? {
|
|
1747
|
+
x: event.clientX + bleed,
|
|
1748
|
+
y: event.clientY
|
|
1749
|
+
} : {
|
|
1750
|
+
x: event.clientX,
|
|
1751
|
+
y: event.clientY + bleed
|
|
1752
|
+
};
|
|
1753
|
+
const near = {
|
|
1754
|
+
right: box.left,
|
|
1755
|
+
left: box.right,
|
|
1756
|
+
down: box.top,
|
|
1757
|
+
up: box.bottom
|
|
1758
|
+
}[toward];
|
|
1759
|
+
const far = {
|
|
1760
|
+
right: box.right,
|
|
1761
|
+
left: box.left,
|
|
1762
|
+
down: box.bottom,
|
|
1763
|
+
up: box.top
|
|
1764
|
+
}[toward];
|
|
1765
|
+
endGrace(state);
|
|
1766
|
+
state.grace = {
|
|
1767
|
+
area: across ? [
|
|
1768
|
+
exit,
|
|
1769
|
+
{
|
|
1770
|
+
x: near,
|
|
1771
|
+
y: box.top
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
x: far,
|
|
1775
|
+
y: box.top
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
x: far,
|
|
1779
|
+
y: box.bottom
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
x: near,
|
|
1783
|
+
y: box.bottom
|
|
1784
|
+
}
|
|
1785
|
+
] : [
|
|
1786
|
+
exit,
|
|
1787
|
+
{
|
|
1788
|
+
x: box.left,
|
|
1789
|
+
y: near
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
x: box.left,
|
|
1793
|
+
y: far
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
x: box.right,
|
|
1797
|
+
y: far
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
x: box.right,
|
|
1801
|
+
y: near
|
|
1802
|
+
}
|
|
1803
|
+
],
|
|
1804
|
+
toward,
|
|
1805
|
+
timer: setTimeout(() => endGrace(state), GRACE_MS)
|
|
1806
|
+
};
|
|
1807
|
+
};
|
|
1808
|
+
item.addEventListener("pointerleave", onPointerLeave);
|
|
1809
|
+
const onKeydown = (event) => {
|
|
1810
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1811
|
+
const forward = view.getComputedStyle(item).direction === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
1812
|
+
const space = event.key === " " && !(parent && stateOf(parent).typing());
|
|
1813
|
+
if (event.key !== forward && event.key !== "Enter" && !space) return;
|
|
1814
|
+
event.preventDefault();
|
|
1815
|
+
if (disabled()) return;
|
|
1816
|
+
if (open) dom.focusFirst();
|
|
1817
|
+
else request(true, true);
|
|
1818
|
+
};
|
|
1819
|
+
item.addEventListener("keydown", onKeydown);
|
|
1820
|
+
return {
|
|
1821
|
+
sync(next) {
|
|
1822
|
+
open = next;
|
|
1823
|
+
if (!next) stopTimer();
|
|
1824
|
+
dom.sync(next);
|
|
1825
|
+
},
|
|
1826
|
+
detach() {
|
|
1827
|
+
stopTimer();
|
|
1828
|
+
item.removeEventListener("click", onClickFirst, { capture: true });
|
|
1829
|
+
item.removeEventListener("click", onClick);
|
|
1830
|
+
item.removeEventListener("pointerleave", onPointerLeave);
|
|
1831
|
+
item.removeEventListener("keydown", onKeydown);
|
|
1832
|
+
item.removeAttribute("popovertarget");
|
|
1833
|
+
item.removeAttribute("popovertargetaction");
|
|
1834
|
+
if (parent && stateOf(parent).openers.get(item) === opener) stateOf(parent).openers.delete(item);
|
|
1835
|
+
dom.detach();
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
459
1839
|
// src/primitives/drag/drag.ts
|
|
460
1840
|
/** Velocity is measured over this window, so a pause before release reads as slow. */
|
|
461
1841
|
const VELOCITY_WINDOW_MS = 100;
|
|
@@ -1044,4 +2424,375 @@ function createSwitch(options) {
|
|
|
1044
2424
|
}
|
|
1045
2425
|
};
|
|
1046
2426
|
}
|
|
1047
|
-
|
|
2427
|
+
// src/tooltip/interaction.ts
|
|
2428
|
+
/** How long the pointer rests on the element before its tooltip shows */
|
|
2429
|
+
const SHOW_DELAY_MS = 500;
|
|
2430
|
+
/** How long a tooltip stays after the pointer and the focus leave */
|
|
2431
|
+
const HIDE_DELAY_MS = 1500;
|
|
2432
|
+
/** How soon after a tooltip closes the next one still shows at once */
|
|
2433
|
+
const WARM_MS = 300;
|
|
2434
|
+
/** How long a touch is held to show the tooltip */
|
|
2435
|
+
const LONG_PRESS_MS = 500;
|
|
2436
|
+
/** How long after a touch that showed the tooltip ends its click is swallowed */
|
|
2437
|
+
const SWALLOW_CLICK_MS = 600;
|
|
2438
|
+
/** How far a held touch may move, in px, and still count as held */
|
|
2439
|
+
const MOVE_SLOP = 10;
|
|
2440
|
+
const shown = /* @__PURE__ */ new Set();
|
|
2441
|
+
let lastClosedAt = -Infinity;
|
|
2442
|
+
const warm = () => shown.size > 0 || performance.now() - lastClosedAt < WARM_MS;
|
|
2443
|
+
const textOf = (element) => (element?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2444
|
+
/** The element's accessible name, near enough to tell whether a tooltip repeats it */
|
|
2445
|
+
const nameOf = (element) => {
|
|
2446
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2447
|
+
if (labelledBy) {
|
|
2448
|
+
const document = element.ownerDocument;
|
|
2449
|
+
return labelledBy.split(/\s+/).map((id) => textOf(document.getElementById(id))).join(" ").trim();
|
|
2450
|
+
}
|
|
2451
|
+
return element.getAttribute("aria-label")?.trim() || textOf(element);
|
|
2452
|
+
};
|
|
2453
|
+
/** Adds or removes one id in a space-separated attribute, keeping the others. */
|
|
2454
|
+
const setToken = (element, name, id, on) => {
|
|
2455
|
+
const ids = (element.getAttribute(name) ?? "").split(/\s+/).filter((each) => each && each !== id);
|
|
2456
|
+
if (on) ids.push(id);
|
|
2457
|
+
if (ids.length) element.setAttribute(name, ids.join(" "));
|
|
2458
|
+
else element.removeAttribute(name);
|
|
2459
|
+
};
|
|
2460
|
+
function attachTooltip(trigger, root, options) {
|
|
2461
|
+
const document = trigger.ownerDocument;
|
|
2462
|
+
const view = document.defaultView;
|
|
2463
|
+
/** The element the tooltip is for: the trigger's first child, ahead of the tooltip */
|
|
2464
|
+
const element = () => {
|
|
2465
|
+
const first = trigger.firstElementChild;
|
|
2466
|
+
return first instanceof view.HTMLElement && first !== root ? first : null;
|
|
2467
|
+
};
|
|
2468
|
+
const isOpen = () => root.matches(":popover-open");
|
|
2469
|
+
const request = (open) => options.onOpenChange(open);
|
|
2470
|
+
let overlay;
|
|
2471
|
+
let overlayPersistent;
|
|
2472
|
+
/** When the pointer last went down on the element, whether the tooltip was open */
|
|
2473
|
+
let openAtPress = false;
|
|
2474
|
+
const overlayNow = () => {
|
|
2475
|
+
const persistent = options.persistent();
|
|
2476
|
+
if (overlay && persistent === overlayPersistent) return overlay;
|
|
2477
|
+
overlay?.detach();
|
|
2478
|
+
overlayPersistent = persistent;
|
|
2479
|
+
const onClose = () => request(false);
|
|
2480
|
+
overlay = attachOverlay(root, persistent ? {
|
|
2481
|
+
mode: "auto",
|
|
2482
|
+
contain: true,
|
|
2483
|
+
onClose
|
|
2484
|
+
} : {
|
|
2485
|
+
mode: "manual",
|
|
2486
|
+
transient: true,
|
|
2487
|
+
contain: true,
|
|
2488
|
+
onClose
|
|
2489
|
+
});
|
|
2490
|
+
return overlay;
|
|
2491
|
+
};
|
|
2492
|
+
let anchored;
|
|
2493
|
+
let placement = "";
|
|
2494
|
+
const place = () => {
|
|
2495
|
+
const p = options.placement();
|
|
2496
|
+
const key = `${p.side} ${p.align} ${p.offset}`;
|
|
2497
|
+
if (anchored && key === placement) return anchored;
|
|
2498
|
+
anchored?.detach();
|
|
2499
|
+
placement = key;
|
|
2500
|
+
anchored = attachAnchoredPosition(root, {
|
|
2501
|
+
anchor: element,
|
|
2502
|
+
side: p.side,
|
|
2503
|
+
align: p.align,
|
|
2504
|
+
offset: p.offset
|
|
2505
|
+
});
|
|
2506
|
+
return anchored;
|
|
2507
|
+
};
|
|
2508
|
+
let described;
|
|
2509
|
+
let expanded;
|
|
2510
|
+
const updateAria = () => {
|
|
2511
|
+
const target = element();
|
|
2512
|
+
const text = options.interactive() ? root.querySelector(".rio-tooltip__supporting-text") : root;
|
|
2513
|
+
const describe = !!target && !!text?.id && nameOf(target) !== textOf(text);
|
|
2514
|
+
if (described && (!describe || described.element !== target || described.id !== text.id)) {
|
|
2515
|
+
setToken(described.element, "aria-describedby", described.id, false);
|
|
2516
|
+
described = void 0;
|
|
2517
|
+
}
|
|
2518
|
+
if (describe && !described) {
|
|
2519
|
+
setToken(target, "aria-describedby", text.id, true);
|
|
2520
|
+
described = {
|
|
2521
|
+
element: target,
|
|
2522
|
+
id: text.id
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
const persistent = options.persistent();
|
|
2526
|
+
if (expanded && (!persistent || expanded !== target)) {
|
|
2527
|
+
expanded.removeAttribute("aria-expanded");
|
|
2528
|
+
expanded = void 0;
|
|
2529
|
+
}
|
|
2530
|
+
if (persistent && target) {
|
|
2531
|
+
target.setAttribute("aria-expanded", String(isOpen()));
|
|
2532
|
+
expanded = target;
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
let showTimer;
|
|
2536
|
+
let hideTimer;
|
|
2537
|
+
const stopShow = () => {
|
|
2538
|
+
clearTimeout(showTimer);
|
|
2539
|
+
showTimer = void 0;
|
|
2540
|
+
};
|
|
2541
|
+
const stopHide = () => {
|
|
2542
|
+
clearTimeout(hideTimer);
|
|
2543
|
+
hideTimer = void 0;
|
|
2544
|
+
};
|
|
2545
|
+
const hideLater = () => {
|
|
2546
|
+
stopHide();
|
|
2547
|
+
hideTimer = setTimeout(() => {
|
|
2548
|
+
hideTimer = void 0;
|
|
2549
|
+
const focused = document.activeElement;
|
|
2550
|
+
const keyboard = !!focused && trigger.contains(focused) && focused.matches(":focus-visible");
|
|
2551
|
+
if (!trigger.matches(":hover") && !keyboard) request(false);
|
|
2552
|
+
}, HIDE_DELAY_MS);
|
|
2553
|
+
};
|
|
2554
|
+
const onPointerEnter = (event) => {
|
|
2555
|
+
if (event.pointerType !== "mouse" || options.persistent()) return;
|
|
2556
|
+
stopHide();
|
|
2557
|
+
if (isOpen() || showTimer !== void 0) return;
|
|
2558
|
+
if (warm()) {
|
|
2559
|
+
request(true);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
showTimer = setTimeout(() => {
|
|
2563
|
+
showTimer = void 0;
|
|
2564
|
+
request(true);
|
|
2565
|
+
}, SHOW_DELAY_MS);
|
|
2566
|
+
};
|
|
2567
|
+
const onPointerLeave = (event) => {
|
|
2568
|
+
if (event.pointerType !== "mouse" || options.persistent()) return;
|
|
2569
|
+
stopShow();
|
|
2570
|
+
if (isOpen()) hideLater();
|
|
2571
|
+
};
|
|
2572
|
+
const onFocusIn = (event) => {
|
|
2573
|
+
if (options.persistent()) return;
|
|
2574
|
+
const focused = event.target;
|
|
2575
|
+
if (!root.contains(focused) && !focused.matches(":focus-visible")) return;
|
|
2576
|
+
stopShow();
|
|
2577
|
+
stopHide();
|
|
2578
|
+
if (!isOpen()) request(true);
|
|
2579
|
+
};
|
|
2580
|
+
const onFocusOut = (event) => {
|
|
2581
|
+
const next = event.relatedTarget;
|
|
2582
|
+
if (next && trigger.contains(next)) return;
|
|
2583
|
+
if (options.persistent()) {
|
|
2584
|
+
if (next && isOpen()) request(false);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (isOpen()) hideLater();
|
|
2588
|
+
};
|
|
2589
|
+
let pressTimer;
|
|
2590
|
+
let pressedAt;
|
|
2591
|
+
let swallowUntil = -Infinity;
|
|
2592
|
+
const stopPress = () => {
|
|
2593
|
+
clearTimeout(pressTimer);
|
|
2594
|
+
pressTimer = void 0;
|
|
2595
|
+
pressedAt = void 0;
|
|
2596
|
+
};
|
|
2597
|
+
const onPointerDown = (event) => {
|
|
2598
|
+
if (!element()?.contains(event.target)) return;
|
|
2599
|
+
openAtPress = isOpen();
|
|
2600
|
+
if (options.persistent()) return;
|
|
2601
|
+
stopShow();
|
|
2602
|
+
if (event.pointerType === "touch") {
|
|
2603
|
+
stopPress();
|
|
2604
|
+
pressedAt = {
|
|
2605
|
+
x: event.clientX,
|
|
2606
|
+
y: event.clientY
|
|
2607
|
+
};
|
|
2608
|
+
pressTimer = setTimeout(() => {
|
|
2609
|
+
stopPress();
|
|
2610
|
+
swallowUntil = Infinity;
|
|
2611
|
+
stopHide();
|
|
2612
|
+
request(true);
|
|
2613
|
+
}, LONG_PRESS_MS);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
stopHide();
|
|
2617
|
+
if (isOpen()) request(false);
|
|
2618
|
+
};
|
|
2619
|
+
const onPointerMove = (event) => {
|
|
2620
|
+
if (!pressedAt) return;
|
|
2621
|
+
if (Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) > MOVE_SLOP) stopPress();
|
|
2622
|
+
};
|
|
2623
|
+
const onPointerUp = (event) => {
|
|
2624
|
+
if (event.pointerType !== "touch" || options.persistent()) return;
|
|
2625
|
+
stopPress();
|
|
2626
|
+
if (swallowUntil === Infinity) {
|
|
2627
|
+
swallowUntil = performance.now() + SWALLOW_CLICK_MS;
|
|
2628
|
+
if (isOpen()) hideLater();
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
const onPointerCancel = () => {
|
|
2632
|
+
stopPress();
|
|
2633
|
+
swallowUntil = -Infinity;
|
|
2634
|
+
};
|
|
2635
|
+
const onContextMenu = (event) => {
|
|
2636
|
+
if (pressTimer !== void 0 || performance.now() < swallowUntil) event.preventDefault();
|
|
2637
|
+
};
|
|
2638
|
+
const onClickFirst = (event) => {
|
|
2639
|
+
if (performance.now() >= swallowUntil || !element()?.contains(event.target)) return;
|
|
2640
|
+
swallowUntil = -Infinity;
|
|
2641
|
+
event.preventDefault();
|
|
2642
|
+
event.stopImmediatePropagation();
|
|
2643
|
+
};
|
|
2644
|
+
const onClick = (event) => {
|
|
2645
|
+
if (!options.persistent()) return;
|
|
2646
|
+
if (!element()?.contains(event.target)) return;
|
|
2647
|
+
request(!(event.detail === 0 ? isOpen() : openAtPress));
|
|
2648
|
+
};
|
|
2649
|
+
trigger.addEventListener("pointerenter", onPointerEnter);
|
|
2650
|
+
trigger.addEventListener("pointerleave", onPointerLeave);
|
|
2651
|
+
trigger.addEventListener("focusin", onFocusIn);
|
|
2652
|
+
trigger.addEventListener("focusout", onFocusOut);
|
|
2653
|
+
trigger.addEventListener("pointerdown", onPointerDown);
|
|
2654
|
+
trigger.addEventListener("pointermove", onPointerMove);
|
|
2655
|
+
trigger.addEventListener("pointerup", onPointerUp);
|
|
2656
|
+
trigger.addEventListener("pointercancel", onPointerCancel);
|
|
2657
|
+
trigger.addEventListener("contextmenu", onContextMenu);
|
|
2658
|
+
trigger.addEventListener("click", onClickFirst, { capture: true });
|
|
2659
|
+
trigger.addEventListener("click", onClick);
|
|
2660
|
+
overlayNow();
|
|
2661
|
+
updateAria();
|
|
2662
|
+
return {
|
|
2663
|
+
sync(open) {
|
|
2664
|
+
const showing = isOpen();
|
|
2665
|
+
if (open && !showing) {
|
|
2666
|
+
const target = element();
|
|
2667
|
+
if (!target) {
|
|
2668
|
+
request(false);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
overlayNow().show(target);
|
|
2672
|
+
place().update();
|
|
2673
|
+
shown.add(root);
|
|
2674
|
+
} else if (!open) {
|
|
2675
|
+
stopShow();
|
|
2676
|
+
stopHide();
|
|
2677
|
+
if (showing) overlay?.hide();
|
|
2678
|
+
if (shown.delete(root)) lastClosedAt = performance.now();
|
|
2679
|
+
}
|
|
2680
|
+
updateAria();
|
|
2681
|
+
},
|
|
2682
|
+
update: updateAria,
|
|
2683
|
+
detach() {
|
|
2684
|
+
trigger.removeEventListener("pointerenter", onPointerEnter);
|
|
2685
|
+
trigger.removeEventListener("pointerleave", onPointerLeave);
|
|
2686
|
+
trigger.removeEventListener("focusin", onFocusIn);
|
|
2687
|
+
trigger.removeEventListener("focusout", onFocusOut);
|
|
2688
|
+
trigger.removeEventListener("pointerdown", onPointerDown);
|
|
2689
|
+
trigger.removeEventListener("pointermove", onPointerMove);
|
|
2690
|
+
trigger.removeEventListener("pointerup", onPointerUp);
|
|
2691
|
+
trigger.removeEventListener("pointercancel", onPointerCancel);
|
|
2692
|
+
trigger.removeEventListener("contextmenu", onContextMenu);
|
|
2693
|
+
trigger.removeEventListener("click", onClickFirst, { capture: true });
|
|
2694
|
+
trigger.removeEventListener("click", onClick);
|
|
2695
|
+
stopShow();
|
|
2696
|
+
stopHide();
|
|
2697
|
+
stopPress();
|
|
2698
|
+
if (described) setToken(described.element, "aria-describedby", described.id, false);
|
|
2699
|
+
expanded?.removeAttribute("aria-expanded");
|
|
2700
|
+
if (shown.delete(root)) lastClosedAt = performance.now();
|
|
2701
|
+
overlay?.detach();
|
|
2702
|
+
anchored?.detach();
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
// src/tooltip/tooltip.ts
|
|
2707
|
+
/** M3 tooltip variants: a brief label, or a card with a subhead, text, and actions. */
|
|
2708
|
+
const TOOLTIP_VARIANTS = ["plain", "rich"];
|
|
2709
|
+
const tooltipDefaults = {
|
|
2710
|
+
variant: "plain",
|
|
2711
|
+
persistent: false,
|
|
2712
|
+
open: false,
|
|
2713
|
+
offset: 4
|
|
2714
|
+
};
|
|
2715
|
+
/** The trigger holds the app's element first, then the tooltip (`tooltipSkeleton`). */
|
|
2716
|
+
const tooltipTriggerSkeleton = {
|
|
2717
|
+
part: "trigger",
|
|
2718
|
+
tag: "span",
|
|
2719
|
+
content: true
|
|
2720
|
+
};
|
|
2721
|
+
/** Spans throughout, so a tooltip may sit inside a paragraph. */
|
|
2722
|
+
const tooltipSkeleton = {
|
|
2723
|
+
part: "root",
|
|
2724
|
+
tag: "span",
|
|
2725
|
+
children: [
|
|
2726
|
+
{
|
|
2727
|
+
part: "subhead",
|
|
2728
|
+
tag: "span",
|
|
2729
|
+
when: "showsSubhead",
|
|
2730
|
+
content: true
|
|
2731
|
+
},
|
|
2732
|
+
{
|
|
2733
|
+
part: "supportingText",
|
|
2734
|
+
tag: "span",
|
|
2735
|
+
content: true
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
part: "actions",
|
|
2739
|
+
tag: "span",
|
|
2740
|
+
when: "showsActions",
|
|
2741
|
+
content: true
|
|
2742
|
+
}
|
|
2743
|
+
]
|
|
2744
|
+
};
|
|
2745
|
+
const NO_CONTENT = {
|
|
2746
|
+
subhead: false,
|
|
2747
|
+
actions: false
|
|
2748
|
+
};
|
|
2749
|
+
function createTooltip(options) {
|
|
2750
|
+
const props = () => ({
|
|
2751
|
+
...tooltipDefaults,
|
|
2752
|
+
...omitUndefined(options.props())
|
|
2753
|
+
});
|
|
2754
|
+
const rich = () => props().variant === "rich";
|
|
2755
|
+
const content = () => options.content?.() ?? NO_CONTENT;
|
|
2756
|
+
const showsSubhead = () => rich() && content().subhead;
|
|
2757
|
+
const showsActions = () => rich() && content().actions;
|
|
2758
|
+
const interactive = showsActions;
|
|
2759
|
+
const { ids } = options;
|
|
2760
|
+
return {
|
|
2761
|
+
parts: {
|
|
2762
|
+
trigger: () => ({ class: "rio-tooltip-trigger" }),
|
|
2763
|
+
root() {
|
|
2764
|
+
const p = props();
|
|
2765
|
+
const dialog = interactive();
|
|
2766
|
+
return {
|
|
2767
|
+
class: "rio-tooltip",
|
|
2768
|
+
id: ids.root,
|
|
2769
|
+
role: dialog ? "dialog" : "tooltip",
|
|
2770
|
+
"aria-labelledby": dialog ? showsSubhead() ? ids.subhead : ids.supportingText : void 0,
|
|
2771
|
+
"data-variant": p.variant
|
|
2772
|
+
};
|
|
2773
|
+
},
|
|
2774
|
+
subhead: () => ({
|
|
2775
|
+
class: "rio-tooltip__subhead",
|
|
2776
|
+
id: ids.subhead
|
|
2777
|
+
}),
|
|
2778
|
+
supportingText: () => ({
|
|
2779
|
+
class: "rio-tooltip__supporting-text",
|
|
2780
|
+
id: ids.supportingText
|
|
2781
|
+
}),
|
|
2782
|
+
actions: () => ({ class: "rio-tooltip__actions" })
|
|
2783
|
+
},
|
|
2784
|
+
showsSubhead,
|
|
2785
|
+
showsActions,
|
|
2786
|
+
interactive,
|
|
2787
|
+
persistent: () => rich() && props().persistent,
|
|
2788
|
+
placement() {
|
|
2789
|
+
const p = props();
|
|
2790
|
+
return {
|
|
2791
|
+
side: p.side ?? (rich() ? "bottom" : "top"),
|
|
2792
|
+
align: p.align ?? (rich() ? "start" : "center"),
|
|
2793
|
+
offset: p.offset
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
export { BUTTON_GROUP_SELECTIONS, BUTTON_GROUP_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, DIALOG_ROLES, ICON_BUTTON_VARIANTS, ICON_BUTTON_WIDTHS, MENU_SELECTIONS, MENU_VARIANTS, SPLIT_BUTTON_VARIANTS, TOOLTIP_VARIANTS, attachAnchoredPosition, attachButtonGroup, attachDialog, attachDrag, attachMenu, attachOverlay, attachRipple, attachShapeMorph, attachSplitButtonMenuIcon, attachSpringProperty, attachSubMenu, attachSwitchHandle, attachTooltip, buttonDefaults, buttonGroupDefaults, buttonGroupSkeleton, buttonSkeleton, createButton, createButtonGroup, createDialog, createIconButton, createMenu, createMenuDivider, createMenuGroup, createMenuItem, createSplitButton, createSpring, createSubMenu, createSwitch, createTooltip, dialogDefaults, dialogSkeleton, iconButtonDefaults, iconButtonSkeleton, menuDefaults, menuDividerSkeleton, menuGroupSkeleton, menuItemDefaults, menuItemSkeleton, menuSkeleton, prefersReducedMotion, readSpringParams, solveSpring, splitButtonDefaults, splitButtonSkeleton, switchDefaults, switchSkeleton, tooltipDefaults, tooltipSkeleton, tooltipTriggerSkeleton };
|