@unabridged/midwest 0.22.1 → 0.23.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 +1 -1
- package/app/assets/javascript/midwest/index.ts +8 -0
- package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
- package/app/assets/javascript/midwest.js +476 -37
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +13 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
- package/dist/javascript/midwest.js +476 -37
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml)
|
|
4
4
|
[](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml)
|
|
5
5
|
[](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
|
|
6
|
-
[](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
8
|
Midwest Component Library is an implementation of the Midwest Design System using [ViewComponent](https://github.com/github/view_component).
|
|
9
9
|
|
|
@@ -32,6 +32,10 @@ import PasswordRequirements from '../../../components/midwest/password_requireme
|
|
|
32
32
|
import CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller'
|
|
33
33
|
import DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller'
|
|
34
34
|
import TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller'
|
|
35
|
+
import IntersectionObserver from '../../../components/midwest/intersection_observer_component/intersection_observer_component_controller'
|
|
36
|
+
import FocusTrap from '../../../components/midwest/focus_trap_component/focus_trap_component_controller'
|
|
37
|
+
import Onboarding from '../../../components/midwest/onboarding_component/onboarding_component_controller'
|
|
38
|
+
import ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller'
|
|
35
39
|
/* IMPORTS */
|
|
36
40
|
|
|
37
41
|
export { Card, Banner, CountdownTimer, Chart }
|
|
@@ -71,5 +75,9 @@ export function registerMidwestControllers (application: any) {
|
|
|
71
75
|
application.register('midwest-code-block', CodeBlock)
|
|
72
76
|
application.register('midwest-date-picker', DatePicker)
|
|
73
77
|
application.register('midwest-time-picker', TimePicker)
|
|
78
|
+
application.register('midwest-intersection-observer', IntersectionObserver)
|
|
79
|
+
application.register('midwest-focus-trap', FocusTrap)
|
|
80
|
+
application.register('midwest-onboarding', Onboarding)
|
|
81
|
+
application.register('midwest-theme-switch', ThemeSwitch)
|
|
74
82
|
/* EXPORTS */
|
|
75
83
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Shared keyboard-navigation helpers for menu/list components.
|
|
2
|
+
//
|
|
3
|
+
// Implements the WAI-ARIA "roving focus" pattern: arrow keys move DOM focus
|
|
4
|
+
// between a flat list of items, Home/End jump to the ends, and Escape/Tab hand
|
|
5
|
+
// control back to the caller (typically to close a popover and restore focus).
|
|
6
|
+
// Components own *which* elements are navigable and *what* closing means; this
|
|
7
|
+
// module owns the key handling so the behavior stays consistent across the
|
|
8
|
+
// library.
|
|
9
|
+
|
|
10
|
+
const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape', 'Tab']
|
|
11
|
+
|
|
12
|
+
export interface RovingFocusOptions {
|
|
13
|
+
// Called on Escape. Typically closes the menu and returns focus to the trigger.
|
|
14
|
+
onEscape?: () => void
|
|
15
|
+
// Called on Tab. The default Tab behavior is left intact so focus still leaves
|
|
16
|
+
// the menu; use this to close the menu alongside it.
|
|
17
|
+
onTab?: () => void
|
|
18
|
+
// When true (default) arrow navigation wraps from the last item to the first
|
|
19
|
+
// and vice versa. When false it stops at the ends.
|
|
20
|
+
loop?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handles a keydown against a roving-focus list. Returns true when the event
|
|
24
|
+
// was one this helper is responsible for, so callers can `stopPropagation()`
|
|
25
|
+
// only for keys that were actually handled.
|
|
26
|
+
export function handleRovingFocusKeydown (
|
|
27
|
+
event: KeyboardEvent,
|
|
28
|
+
items: HTMLElement[],
|
|
29
|
+
options: RovingFocusOptions = {}
|
|
30
|
+
): boolean {
|
|
31
|
+
if (items.length === 0) return false
|
|
32
|
+
if (!NAVIGATION_KEYS.includes(event.key)) return false
|
|
33
|
+
|
|
34
|
+
const { onEscape, onTab, loop = true } = options
|
|
35
|
+
const current = items.findIndex(item => item === document.activeElement)
|
|
36
|
+
|
|
37
|
+
switch (event.key) {
|
|
38
|
+
case 'ArrowDown': {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
const next = current < items.length - 1 ? current + 1 : (loop ? 0 : items.length - 1)
|
|
41
|
+
items[next].focus()
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
case 'ArrowUp': {
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
const prev = current > 0 ? current - 1 : (loop ? items.length - 1 : 0)
|
|
47
|
+
items[prev].focus()
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
case 'Home':
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
items[0].focus()
|
|
53
|
+
break
|
|
54
|
+
case 'End':
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
items[items.length - 1].focus()
|
|
57
|
+
break
|
|
58
|
+
case 'Escape':
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
onEscape?.()
|
|
61
|
+
break
|
|
62
|
+
case 'Tab':
|
|
63
|
+
onTab?.()
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
@@ -2511,6 +2511,46 @@ const computePosition = (reference, floating, options) => {
|
|
|
2511
2511
|
});
|
|
2512
2512
|
};
|
|
2513
2513
|
|
|
2514
|
+
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"];
|
|
2515
|
+
function handleRovingFocusKeydown(event, items, options = {}) {
|
|
2516
|
+
if (items.length === 0)
|
|
2517
|
+
return false;
|
|
2518
|
+
if (!NAVIGATION_KEYS.includes(event.key))
|
|
2519
|
+
return false;
|
|
2520
|
+
const { onEscape, onTab, loop = true } = options;
|
|
2521
|
+
const current = items.findIndex((item) => item === document.activeElement);
|
|
2522
|
+
switch (event.key) {
|
|
2523
|
+
case "ArrowDown": {
|
|
2524
|
+
event.preventDefault();
|
|
2525
|
+
const next = current < items.length - 1 ? current + 1 : loop ? 0 : items.length - 1;
|
|
2526
|
+
items[next].focus();
|
|
2527
|
+
break;
|
|
2528
|
+
}
|
|
2529
|
+
case "ArrowUp": {
|
|
2530
|
+
event.preventDefault();
|
|
2531
|
+
const prev = current > 0 ? current - 1 : loop ? items.length - 1 : 0;
|
|
2532
|
+
items[prev].focus();
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2535
|
+
case "Home":
|
|
2536
|
+
event.preventDefault();
|
|
2537
|
+
items[0].focus();
|
|
2538
|
+
break;
|
|
2539
|
+
case "End":
|
|
2540
|
+
event.preventDefault();
|
|
2541
|
+
items[items.length - 1].focus();
|
|
2542
|
+
break;
|
|
2543
|
+
case "Escape":
|
|
2544
|
+
event.preventDefault();
|
|
2545
|
+
onEscape?.();
|
|
2546
|
+
break;
|
|
2547
|
+
case "Tab":
|
|
2548
|
+
onTab?.();
|
|
2549
|
+
break;
|
|
2550
|
+
}
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2514
2554
|
class Dropdown extends Controller {
|
|
2515
2555
|
static values = {
|
|
2516
2556
|
computed: { type: Boolean, default: false },
|
|
@@ -2564,45 +2604,15 @@ class Dropdown extends Controller {
|
|
|
2564
2604
|
}
|
|
2565
2605
|
};
|
|
2566
2606
|
handleKeydown = (event) => {
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
return;
|
|
2570
|
-
if (!["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"].includes(event.key))
|
|
2571
|
-
return;
|
|
2572
|
-
event.stopPropagation();
|
|
2573
|
-
const currentIndex = items.findIndex((item) => item === document.activeElement);
|
|
2574
|
-
switch (event.key) {
|
|
2575
|
-
case "ArrowDown": {
|
|
2576
|
-
event.preventDefault();
|
|
2577
|
-
const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
2578
|
-
items[next].focus();
|
|
2579
|
-
break;
|
|
2580
|
-
}
|
|
2581
|
-
case "ArrowUp": {
|
|
2582
|
-
event.preventDefault();
|
|
2583
|
-
const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
2584
|
-
items[prev].focus();
|
|
2585
|
-
break;
|
|
2586
|
-
}
|
|
2587
|
-
case "Home":
|
|
2588
|
-
event.preventDefault();
|
|
2589
|
-
items[0].focus();
|
|
2590
|
-
break;
|
|
2591
|
-
case "End":
|
|
2592
|
-
event.preventDefault();
|
|
2593
|
-
items[items.length - 1].focus();
|
|
2594
|
-
break;
|
|
2595
|
-
case "Escape":
|
|
2596
|
-
event.preventDefault();
|
|
2607
|
+
const handled = handleRovingFocusKeydown(event, this.menuItems, {
|
|
2608
|
+
onEscape: () => {
|
|
2597
2609
|
this.element.hidePopover();
|
|
2598
2610
|
requestAnimationFrame(() => this.buttonEl?.focus());
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
return;
|
|
2605
|
-
}
|
|
2611
|
+
},
|
|
2612
|
+
onTab: () => this.element.hidePopover()
|
|
2613
|
+
});
|
|
2614
|
+
if (handled)
|
|
2615
|
+
event.stopPropagation();
|
|
2606
2616
|
};
|
|
2607
2617
|
get menuItems() {
|
|
2608
2618
|
return Array.from(
|
|
@@ -6555,6 +6565,431 @@ class TimePicker extends Controller {
|
|
|
6555
6565
|
}
|
|
6556
6566
|
}
|
|
6557
6567
|
|
|
6568
|
+
class ViewportObserver extends Controller {
|
|
6569
|
+
static values = {
|
|
6570
|
+
threshold: { type: Number, default: 0 },
|
|
6571
|
+
rootMargin: { type: String, default: "0px" },
|
|
6572
|
+
once: { type: Boolean, default: false },
|
|
6573
|
+
visibleClass: { type: String, default: "" }
|
|
6574
|
+
};
|
|
6575
|
+
observer = null;
|
|
6576
|
+
connect() {
|
|
6577
|
+
this.observer = new IntersectionObserver(
|
|
6578
|
+
(entries) => {
|
|
6579
|
+
for (const entry of entries)
|
|
6580
|
+
this.handle(entry);
|
|
6581
|
+
},
|
|
6582
|
+
{ threshold: this.thresholdValue, rootMargin: this.rootMarginValue }
|
|
6583
|
+
);
|
|
6584
|
+
this.observer.observe(this.element);
|
|
6585
|
+
}
|
|
6586
|
+
disconnect() {
|
|
6587
|
+
this.observer?.disconnect();
|
|
6588
|
+
this.observer = null;
|
|
6589
|
+
}
|
|
6590
|
+
handle(entry) {
|
|
6591
|
+
if (entry.isIntersecting) {
|
|
6592
|
+
this.element.setAttribute("data-intersecting", "true");
|
|
6593
|
+
if (this.visibleClassValue)
|
|
6594
|
+
this.element.classList.add(this.visibleClassValue);
|
|
6595
|
+
this.dispatch("enter", { detail: { entry }, bubbles: true });
|
|
6596
|
+
if (this.onceValue)
|
|
6597
|
+
this.disconnect();
|
|
6598
|
+
} else {
|
|
6599
|
+
this.element.setAttribute("data-intersecting", "false");
|
|
6600
|
+
if (this.visibleClassValue)
|
|
6601
|
+
this.element.classList.remove(this.visibleClassValue);
|
|
6602
|
+
this.dispatch("leave", { detail: { entry }, bubbles: true });
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
const FOCUSABLE$1 = [
|
|
6608
|
+
"a[href]",
|
|
6609
|
+
"button:not([disabled])",
|
|
6610
|
+
"textarea:not([disabled])",
|
|
6611
|
+
"input:not([disabled])",
|
|
6612
|
+
"select:not([disabled])",
|
|
6613
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
6614
|
+
].join(", ");
|
|
6615
|
+
class FocusTrap extends Controller {
|
|
6616
|
+
static values = {
|
|
6617
|
+
active: { type: Boolean, default: true },
|
|
6618
|
+
returnFocus: { type: Boolean, default: true },
|
|
6619
|
+
initialFocus: { type: String, default: "" }
|
|
6620
|
+
};
|
|
6621
|
+
engaged = false;
|
|
6622
|
+
previousFocus = null;
|
|
6623
|
+
// Fires on connect for the initial value and on every subsequent change,
|
|
6624
|
+
// so it is the single entry point for engaging/releasing the trap.
|
|
6625
|
+
activeValueChanged(active) {
|
|
6626
|
+
if (active)
|
|
6627
|
+
this.engage();
|
|
6628
|
+
else
|
|
6629
|
+
this.release();
|
|
6630
|
+
}
|
|
6631
|
+
disconnect() {
|
|
6632
|
+
this.release();
|
|
6633
|
+
}
|
|
6634
|
+
engage() {
|
|
6635
|
+
if (this.engaged)
|
|
6636
|
+
return;
|
|
6637
|
+
this.engaged = true;
|
|
6638
|
+
this.previousFocus = document.activeElement;
|
|
6639
|
+
this.element.addEventListener("keydown", this.handleKeydown);
|
|
6640
|
+
requestAnimationFrame(() => {
|
|
6641
|
+
if (this.engaged)
|
|
6642
|
+
this.focusInitial();
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
6645
|
+
release() {
|
|
6646
|
+
if (!this.engaged)
|
|
6647
|
+
return;
|
|
6648
|
+
this.engaged = false;
|
|
6649
|
+
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
6650
|
+
if (this.returnFocusValue)
|
|
6651
|
+
this.previousFocus?.focus();
|
|
6652
|
+
this.previousFocus = null;
|
|
6653
|
+
}
|
|
6654
|
+
focusInitial() {
|
|
6655
|
+
const target = this.initialFocusValue ? this.element.querySelector(this.initialFocusValue) : this.focusable()[0];
|
|
6656
|
+
target?.focus();
|
|
6657
|
+
}
|
|
6658
|
+
focusable() {
|
|
6659
|
+
return Array.from(
|
|
6660
|
+
this.element.querySelectorAll(FOCUSABLE$1)
|
|
6661
|
+
).filter((el) => !el.closest("[inert]"));
|
|
6662
|
+
}
|
|
6663
|
+
handleKeydown = (event) => {
|
|
6664
|
+
if (event.key !== "Tab")
|
|
6665
|
+
return;
|
|
6666
|
+
const focusable = this.focusable();
|
|
6667
|
+
if (focusable.length === 0) {
|
|
6668
|
+
event.preventDefault();
|
|
6669
|
+
return;
|
|
6670
|
+
}
|
|
6671
|
+
const first = focusable[0];
|
|
6672
|
+
const last = focusable[focusable.length - 1];
|
|
6673
|
+
const active = document.activeElement;
|
|
6674
|
+
if (event.shiftKey) {
|
|
6675
|
+
if (active === first || !this.element.contains(active)) {
|
|
6676
|
+
event.preventDefault();
|
|
6677
|
+
last.focus();
|
|
6678
|
+
}
|
|
6679
|
+
} else if (active === last || !this.element.contains(active)) {
|
|
6680
|
+
event.preventDefault();
|
|
6681
|
+
first.focus();
|
|
6682
|
+
}
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6686
|
+
const FOCUSABLE = [
|
|
6687
|
+
"a[href]",
|
|
6688
|
+
"button:not([disabled])",
|
|
6689
|
+
"textarea:not([disabled])",
|
|
6690
|
+
"input:not([disabled])",
|
|
6691
|
+
"select:not([disabled])",
|
|
6692
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
6693
|
+
].join(", ");
|
|
6694
|
+
const GAP = 12;
|
|
6695
|
+
class Onboarding extends Controller {
|
|
6696
|
+
static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow"];
|
|
6697
|
+
static values = {
|
|
6698
|
+
autoOpen: { type: Boolean, default: false },
|
|
6699
|
+
confirm: { type: String, default: "" }
|
|
6700
|
+
};
|
|
6701
|
+
steps = [];
|
|
6702
|
+
index = 0;
|
|
6703
|
+
active = false;
|
|
6704
|
+
previousFocus = null;
|
|
6705
|
+
highlighted = null;
|
|
6706
|
+
connect() {
|
|
6707
|
+
this.steps = this.stepTargets.map((el) => ({
|
|
6708
|
+
el,
|
|
6709
|
+
name: el.dataset.name ?? "",
|
|
6710
|
+
selector: el.dataset.selector ?? "",
|
|
6711
|
+
position: el.dataset.position ?? "auto",
|
|
6712
|
+
title: el.dataset.title ?? "",
|
|
6713
|
+
nextText: el.dataset.nextText ?? "Next",
|
|
6714
|
+
backText: el.dataset.backText ?? "Back",
|
|
6715
|
+
completeText: el.dataset.completeText ?? "Done",
|
|
6716
|
+
canClickTarget: el.dataset.canClickTarget === "true"
|
|
6717
|
+
}));
|
|
6718
|
+
window.addEventListener("midwest-onboarding:open", this.handleOpen);
|
|
6719
|
+
if (this.autoOpenValue)
|
|
6720
|
+
this.start();
|
|
6721
|
+
}
|
|
6722
|
+
disconnect() {
|
|
6723
|
+
window.removeEventListener("midwest-onboarding:open", this.handleOpen);
|
|
6724
|
+
this.teardown();
|
|
6725
|
+
}
|
|
6726
|
+
start() {
|
|
6727
|
+
if (this.active || this.steps.length === 0)
|
|
6728
|
+
return;
|
|
6729
|
+
this.active = true;
|
|
6730
|
+
this.previousFocus = document.activeElement;
|
|
6731
|
+
this.overlayTarget.hidden = false;
|
|
6732
|
+
this.cardTarget.hidden = false;
|
|
6733
|
+
document.addEventListener("keydown", this.handleKeydown, true);
|
|
6734
|
+
window.addEventListener("resize", this.reposition);
|
|
6735
|
+
window.addEventListener("scroll", this.reposition, true);
|
|
6736
|
+
this.dispatch("start", { bubbles: true });
|
|
6737
|
+
this.render(0);
|
|
6738
|
+
}
|
|
6739
|
+
next() {
|
|
6740
|
+
if (this.index < this.steps.length - 1)
|
|
6741
|
+
this.render(this.index + 1);
|
|
6742
|
+
else
|
|
6743
|
+
this.complete();
|
|
6744
|
+
}
|
|
6745
|
+
back() {
|
|
6746
|
+
if (this.index > 0)
|
|
6747
|
+
this.render(this.index - 1);
|
|
6748
|
+
}
|
|
6749
|
+
// Jump to a step by its `name`.
|
|
6750
|
+
show(name) {
|
|
6751
|
+
const target = this.steps.findIndex((step) => step.name === name);
|
|
6752
|
+
if (target >= 0) {
|
|
6753
|
+
if (!this.active)
|
|
6754
|
+
this.start();
|
|
6755
|
+
this.render(target);
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
complete() {
|
|
6759
|
+
if (!this.active)
|
|
6760
|
+
return;
|
|
6761
|
+
this.teardown();
|
|
6762
|
+
this.dispatch("complete", { bubbles: true });
|
|
6763
|
+
}
|
|
6764
|
+
cancel() {
|
|
6765
|
+
if (!this.active)
|
|
6766
|
+
return;
|
|
6767
|
+
if (this.confirmValue && !window.confirm(this.confirmValue))
|
|
6768
|
+
return;
|
|
6769
|
+
this.teardown();
|
|
6770
|
+
this.dispatch("cancel", { bubbles: true });
|
|
6771
|
+
}
|
|
6772
|
+
render(index) {
|
|
6773
|
+
this.index = index;
|
|
6774
|
+
const step = this.steps[index];
|
|
6775
|
+
this.clearHighlight();
|
|
6776
|
+
this.titleTarget.textContent = step.title;
|
|
6777
|
+
this.renderBody(step);
|
|
6778
|
+
this.renderButtons(step, index);
|
|
6779
|
+
const target = step.selector ? document.querySelector(step.selector) : null;
|
|
6780
|
+
if (target) {
|
|
6781
|
+
this.highlight(target, step.canClickTarget);
|
|
6782
|
+
this.position(target, step.position);
|
|
6783
|
+
} else {
|
|
6784
|
+
this.center();
|
|
6785
|
+
}
|
|
6786
|
+
this.dispatch("show", { detail: { index, name: step.name }, bubbles: true });
|
|
6787
|
+
this.focusCard();
|
|
6788
|
+
}
|
|
6789
|
+
renderBody(step) {
|
|
6790
|
+
this.bodyTarget.replaceChildren();
|
|
6791
|
+
const template = step.el.querySelector("template");
|
|
6792
|
+
if (template)
|
|
6793
|
+
this.bodyTarget.appendChild(template.content.cloneNode(true));
|
|
6794
|
+
}
|
|
6795
|
+
renderButtons(step, index) {
|
|
6796
|
+
const isFirst = index === 0;
|
|
6797
|
+
const isLast = index === this.steps.length - 1;
|
|
6798
|
+
this.backTarget.textContent = step.backText;
|
|
6799
|
+
this.backTarget.style.visibility = isFirst ? "hidden" : "";
|
|
6800
|
+
this.nextTarget.textContent = step.nextText;
|
|
6801
|
+
this.nextTarget.style.display = isLast ? "none" : "";
|
|
6802
|
+
this.doneTarget.textContent = step.completeText;
|
|
6803
|
+
this.doneTarget.style.display = isLast ? "" : "none";
|
|
6804
|
+
}
|
|
6805
|
+
highlight(target, canClickTarget) {
|
|
6806
|
+
target.classList.add("midwest-onboarding-highlight");
|
|
6807
|
+
target.classList.toggle("is-locked", !canClickTarget);
|
|
6808
|
+
this.highlighted = target;
|
|
6809
|
+
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
6810
|
+
}
|
|
6811
|
+
clearHighlight() {
|
|
6812
|
+
this.highlighted?.classList.remove("midwest-onboarding-highlight", "is-locked");
|
|
6813
|
+
this.highlighted = null;
|
|
6814
|
+
}
|
|
6815
|
+
// Places the card against the target, choosing a side when `position` is auto.
|
|
6816
|
+
position(target, position) {
|
|
6817
|
+
const rect = target.getBoundingClientRect();
|
|
6818
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
6819
|
+
const vw = window.innerWidth;
|
|
6820
|
+
const vh = window.innerHeight;
|
|
6821
|
+
let placement = position;
|
|
6822
|
+
if (placement === "auto") {
|
|
6823
|
+
if (rect.bottom + card.height + GAP <= vh)
|
|
6824
|
+
placement = "bottom";
|
|
6825
|
+
else if (rect.top - card.height - GAP >= 0)
|
|
6826
|
+
placement = "top";
|
|
6827
|
+
else if (rect.right + card.width + GAP <= vw)
|
|
6828
|
+
placement = "right";
|
|
6829
|
+
else
|
|
6830
|
+
placement = "left";
|
|
6831
|
+
}
|
|
6832
|
+
let top;
|
|
6833
|
+
let left;
|
|
6834
|
+
switch (placement) {
|
|
6835
|
+
case "top":
|
|
6836
|
+
top = rect.top - card.height - GAP;
|
|
6837
|
+
left = rect.left + rect.width / 2 - card.width / 2;
|
|
6838
|
+
break;
|
|
6839
|
+
case "left":
|
|
6840
|
+
top = rect.top + rect.height / 2 - card.height / 2;
|
|
6841
|
+
left = rect.left - card.width - GAP;
|
|
6842
|
+
break;
|
|
6843
|
+
case "right":
|
|
6844
|
+
top = rect.top + rect.height / 2 - card.height / 2;
|
|
6845
|
+
left = rect.right + GAP;
|
|
6846
|
+
break;
|
|
6847
|
+
default:
|
|
6848
|
+
top = rect.bottom + GAP;
|
|
6849
|
+
left = rect.left + rect.width / 2 - card.width / 2;
|
|
6850
|
+
break;
|
|
6851
|
+
}
|
|
6852
|
+
left = Math.max(8, Math.min(left, vw - card.width - 8));
|
|
6853
|
+
top = Math.max(8, Math.min(top, vh - card.height - 8));
|
|
6854
|
+
this.cardTarget.style.top = `${top}px`;
|
|
6855
|
+
this.cardTarget.style.left = `${left}px`;
|
|
6856
|
+
this.cardTarget.dataset.placement = placement;
|
|
6857
|
+
this.positionArrow(placement, rect, top, left);
|
|
6858
|
+
}
|
|
6859
|
+
// Nudges the arrow so it points at the target's center, not the card's.
|
|
6860
|
+
positionArrow(placement, rect, top, left) {
|
|
6861
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
6862
|
+
const arrow = this.arrowTarget.style;
|
|
6863
|
+
if (placement === "top" || placement === "bottom") {
|
|
6864
|
+
const targetCenter = rect.left + rect.width / 2;
|
|
6865
|
+
const offset = Math.max(12, Math.min(targetCenter - left, card.width - 12));
|
|
6866
|
+
arrow.left = `${offset}px`;
|
|
6867
|
+
arrow.top = "";
|
|
6868
|
+
} else {
|
|
6869
|
+
const targetCenter = rect.top + rect.height / 2;
|
|
6870
|
+
const offset = Math.max(12, Math.min(targetCenter - top, card.height - 12));
|
|
6871
|
+
arrow.top = `${offset}px`;
|
|
6872
|
+
arrow.left = "";
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
center() {
|
|
6876
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
6877
|
+
this.cardTarget.style.top = `${Math.max(8, window.innerHeight / 2 - card.height / 2)}px`;
|
|
6878
|
+
this.cardTarget.style.left = `${Math.max(8, window.innerWidth / 2 - card.width / 2)}px`;
|
|
6879
|
+
this.cardTarget.dataset.placement = "center";
|
|
6880
|
+
}
|
|
6881
|
+
focusCard() {
|
|
6882
|
+
requestAnimationFrame(() => {
|
|
6883
|
+
if (this.active)
|
|
6884
|
+
this.cardTarget.focus();
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
teardown() {
|
|
6888
|
+
this.active = false;
|
|
6889
|
+
this.clearHighlight();
|
|
6890
|
+
this.overlayTarget.hidden = true;
|
|
6891
|
+
this.cardTarget.hidden = true;
|
|
6892
|
+
document.removeEventListener("keydown", this.handleKeydown, true);
|
|
6893
|
+
window.removeEventListener("resize", this.reposition);
|
|
6894
|
+
window.removeEventListener("scroll", this.reposition, true);
|
|
6895
|
+
this.previousFocus?.focus();
|
|
6896
|
+
this.previousFocus = null;
|
|
6897
|
+
}
|
|
6898
|
+
handleOpen = (event) => {
|
|
6899
|
+
const detail = event.detail ?? {};
|
|
6900
|
+
const id = String(detail.id ?? "").replace("#", "");
|
|
6901
|
+
if (id === this.element.id)
|
|
6902
|
+
this.start();
|
|
6903
|
+
};
|
|
6904
|
+
reposition = () => {
|
|
6905
|
+
if (!this.active)
|
|
6906
|
+
return;
|
|
6907
|
+
const step = this.steps[this.index];
|
|
6908
|
+
const target = step.selector ? document.querySelector(step.selector) : null;
|
|
6909
|
+
if (target)
|
|
6910
|
+
this.position(target, step.position);
|
|
6911
|
+
else
|
|
6912
|
+
this.center();
|
|
6913
|
+
};
|
|
6914
|
+
handleKeydown = (event) => {
|
|
6915
|
+
if (!this.active)
|
|
6916
|
+
return;
|
|
6917
|
+
switch (event.key) {
|
|
6918
|
+
case "Escape":
|
|
6919
|
+
event.preventDefault();
|
|
6920
|
+
this.cancel();
|
|
6921
|
+
break;
|
|
6922
|
+
case "ArrowRight":
|
|
6923
|
+
event.preventDefault();
|
|
6924
|
+
this.next();
|
|
6925
|
+
break;
|
|
6926
|
+
case "ArrowLeft":
|
|
6927
|
+
event.preventDefault();
|
|
6928
|
+
this.back();
|
|
6929
|
+
break;
|
|
6930
|
+
case "Tab":
|
|
6931
|
+
this.trapTab(event);
|
|
6932
|
+
break;
|
|
6933
|
+
}
|
|
6934
|
+
};
|
|
6935
|
+
// Keep Tab focus within the card while the tour is active.
|
|
6936
|
+
trapTab(event) {
|
|
6937
|
+
const focusable = Array.from(this.cardTarget.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && el.offsetParent !== null && getComputedStyle(el).visibility !== "hidden");
|
|
6938
|
+
if (focusable.length === 0)
|
|
6939
|
+
return;
|
|
6940
|
+
const first = focusable[0];
|
|
6941
|
+
const last = focusable[focusable.length - 1];
|
|
6942
|
+
const active = document.activeElement;
|
|
6943
|
+
if (event.shiftKey) {
|
|
6944
|
+
if (active === first || active === this.cardTarget || !this.cardTarget.contains(active)) {
|
|
6945
|
+
event.preventDefault();
|
|
6946
|
+
last.focus();
|
|
6947
|
+
}
|
|
6948
|
+
} else if (active === last || !this.cardTarget.contains(active)) {
|
|
6949
|
+
event.preventDefault();
|
|
6950
|
+
first.focus();
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
|
|
6955
|
+
class ThemeSwitch extends Controller {
|
|
6956
|
+
static values = {
|
|
6957
|
+
storageKey: { type: String, default: "midwest-theme" }
|
|
6958
|
+
};
|
|
6959
|
+
connect() {
|
|
6960
|
+
const isDark = this.resolveTheme() === "dark";
|
|
6961
|
+
this.apply(isDark);
|
|
6962
|
+
this.element.checked = isDark;
|
|
6963
|
+
}
|
|
6964
|
+
toggle() {
|
|
6965
|
+
const isDark = this.element.checked;
|
|
6966
|
+
this.apply(isDark);
|
|
6967
|
+
this.persist(isDark ? "dark" : "light");
|
|
6968
|
+
}
|
|
6969
|
+
resolveTheme() {
|
|
6970
|
+
const stored = this.read();
|
|
6971
|
+
if (stored === "dark" || stored === "light")
|
|
6972
|
+
return stored;
|
|
6973
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
6974
|
+
}
|
|
6975
|
+
apply(isDark) {
|
|
6976
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
6977
|
+
}
|
|
6978
|
+
read() {
|
|
6979
|
+
try {
|
|
6980
|
+
return window.localStorage.getItem(this.storageKeyValue);
|
|
6981
|
+
} catch {
|
|
6982
|
+
return null;
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
persist(theme) {
|
|
6986
|
+
try {
|
|
6987
|
+
window.localStorage.setItem(this.storageKeyValue, theme);
|
|
6988
|
+
} catch {
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
|
|
6558
6993
|
function registerMidwestControllers(application) {
|
|
6559
6994
|
application.register("midwest-card", Card);
|
|
6560
6995
|
application.register("midwest-banner", Banner);
|
|
@@ -6590,6 +7025,10 @@ function registerMidwestControllers(application) {
|
|
|
6590
7025
|
application.register("midwest-code-block", CodeBlock);
|
|
6591
7026
|
application.register("midwest-date-picker", DatePicker);
|
|
6592
7027
|
application.register("midwest-time-picker", TimePicker);
|
|
7028
|
+
application.register("midwest-intersection-observer", ViewportObserver);
|
|
7029
|
+
application.register("midwest-focus-trap", FocusTrap);
|
|
7030
|
+
application.register("midwest-onboarding", Onboarding);
|
|
7031
|
+
application.register("midwest-theme-switch", ThemeSwitch);
|
|
6593
7032
|
}
|
|
6594
7033
|
|
|
6595
7034
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|