phlex_kit 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +7 -0
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +13 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +38 -7
- data/app/components/phlex_kit/attachment/attachment.css +15 -0
- data/app/components/phlex_kit/attachment/attachment_group.rb +6 -1
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +8 -0
- data/app/components/phlex_kit/avatar/avatar_badge.rb +9 -2
- data/app/components/phlex_kit/badge/badge.css +1 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +1 -0
- data/app/components/phlex_kit/bubble/bubble.rb +4 -6
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -6
- data/app/components/phlex_kit/calendar/calendar_controller.js +56 -15
- data/app/components/phlex_kit/carousel/carousel.rb +3 -2
- data/app/components/phlex_kit/carousel/carousel_controller.js +59 -4
- data/app/components/phlex_kit/chart/chart.rb +10 -2
- data/app/components/phlex_kit/chart/chart_controller.js +37 -8
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +9 -2
- data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +1 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +11 -1
- data/app/components/phlex_kit/collapsible/collapsible.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox.css +1 -1
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +72 -3
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +4 -0
- data/app/components/phlex_kit/command/command_controller.js +23 -2
- data/app/components/phlex_kit/command/command_input.rb +3 -2
- data/app/components/phlex_kit/command/command_item.rb +4 -0
- data/app/components/phlex_kit/command/command_list.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu.css +3 -0
- data/app/components/phlex_kit/context_menu/context_menu.rb +9 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +74 -12
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +3 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table.css +1 -11
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +4 -0
- data/app/components/phlex_kit/data_table/data_table_pagination.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +8 -3
- data/app/components/phlex_kit/data_table/data_table_search.rb +7 -2
- data/app/components/phlex_kit/date_picker/date_picker.rb +19 -2
- data/app/components/phlex_kit/date_picker/date_picker_controller.js +28 -0
- data/app/components/phlex_kit/dialog/dialog.css +3 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +16 -2
- data/app/components/phlex_kit/drawer/drawer.rb +1 -1
- data/app/components/phlex_kit/drawer/drawer_content.rb +10 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +9 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +81 -13
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/field/field_separator.rb +1 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +54 -25
- data/app/components/phlex_kit/form_field/form_field_hint.rb +6 -2
- data/app/components/phlex_kit/hover_card/hover_card.css +10 -2
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +24 -0
- data/app/components/phlex_kit/icon/icon.css +9 -0
- data/app/components/phlex_kit/icon/icon.rb +7 -1
- data/app/components/phlex_kit/input/input.css +0 -1
- data/app/components/phlex_kit/input_group/input_group.css +0 -1
- data/app/components/phlex_kit/input_otp/input_otp.rb +6 -3
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +24 -2
- data/app/components/phlex_kit/item/item.css +2 -1
- data/app/components/phlex_kit/kbd/kbd.css +2 -1
- data/app/components/phlex_kit/label/label.css +1 -1
- data/app/components/phlex_kit/link/link.rb +1 -0
- data/app/components/phlex_kit/marker/marker.css +2 -1
- data/app/components/phlex_kit/marker/marker.rb +9 -2
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +20 -14
- data/app/components/phlex_kit/menubar/menubar.css +3 -0
- data/app/components/phlex_kit/menubar/menubar_controller.js +46 -16
- data/app/components/phlex_kit/message/message.rb +5 -0
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +3 -0
- data/app/components/phlex_kit/native_select/native_select.css +14 -0
- data/app/components/phlex_kit/native_select/native_select.rb +12 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +4 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_link.rb +8 -1
- data/app/components/phlex_kit/pagination/pagination.rb +10 -2
- data/app/components/phlex_kit/pagination/pagination_next.rb +3 -1
- data/app/components/phlex_kit/pagination/pagination_previous.rb +3 -1
- data/app/components/phlex_kit/popover/popover_controller.js +42 -1
- data/app/components/phlex_kit/progress/progress.css +8 -0
- data/app/components/phlex_kit/progress/progress.rb +14 -5
- data/app/components/phlex_kit/resizable/resizable_controller.js +3 -0
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +5 -15
- data/app/components/phlex_kit/select/select.css +5 -2
- data/app/components/phlex_kit/select/select_controller.js +49 -4
- data/app/components/phlex_kit/select/select_trigger.rb +4 -1
- data/app/components/phlex_kit/separator/separator.rb +5 -1
- data/app/components/phlex_kit/sheet/sheet.css +4 -2
- data/app/components/phlex_kit/sheet/sheet_content.rb +13 -3
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +40 -0
- data/app/components/phlex_kit/sheet/sheet_controller.js +31 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +9 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +8 -1
- data/app/components/phlex_kit/slider/slider.css +15 -0
- data/app/components/phlex_kit/spinner/spinner.rb +6 -3
- data/app/components/phlex_kit/switch/switch.css +5 -0
- data/app/components/phlex_kit/switch/switch.rb +6 -0
- data/app/components/phlex_kit/table/table.css +2 -1
- data/app/components/phlex_kit/table/table.rb +5 -2
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +8 -2
- data/app/components/phlex_kit/tabs/tabs_list.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +11 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +14 -0
- data/app/components/phlex_kit/toast/toast.css +2 -1
- data/app/components/phlex_kit/toast/toast_action.rb +4 -1
- data/app/components/phlex_kit/toast/toast_close.rb +6 -3
- data/app/components/phlex_kit/toast/toast_controller.js +40 -21
- data/app/components/phlex_kit/toast/toast_item.rb +11 -7
- data/app/components/phlex_kit/toast/toast_region.rb +29 -10
- data/app/components/phlex_kit/toast/toaster_controller.js +17 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +17 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +16 -9
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +20 -2
- data/app/components/phlex_kit/tooltip/tooltip.css +5 -1
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +7 -1
- data/app/components/phlex_kit/typography/heading.rb +7 -1
- data/app/components/phlex_kit/typography/list.rb +22 -0
- data/app/components/phlex_kit/typography/text.rb +7 -1
- data/app/javascript/phlex_kit/controllers/index.js +2 -0
- data/lib/phlex_kit/base_component.rb +23 -0
- data/lib/phlex_kit/version.rb +1 -1
- data/lib/phlex_kit.rb +1 -0
- metadata +4 -1
|
@@ -17,14 +17,23 @@ export default class extends Controller {
|
|
|
17
17
|
if (invoker) {
|
|
18
18
|
invoker.popoverTargetElement = this.contentTarget
|
|
19
19
|
} else {
|
|
20
|
+
// Button-less fallback: keyboard toggling and popup semantics don't
|
|
21
|
+
// come for free like they do with popoverTargetElement — wire Enter
|
|
22
|
+
// (and Space, off text-entry controls) plus aria-haspopup/-expanded
|
|
23
|
+
// onto the focusable child so AT users can discover and open it.
|
|
24
|
+
this.fallbackInvoker = this.triggerTarget.querySelector("input, a[href], select, textarea, [tabindex]") || this.triggerTarget
|
|
25
|
+
this.fallbackInvoker.setAttribute("aria-haspopup", "dialog")
|
|
26
|
+
this.fallbackInvoker.setAttribute("aria-expanded", this.contentTarget.matches(":popover-open") ? "true" : "false")
|
|
20
27
|
this.triggerTarget.addEventListener("pointerdown", this.armToggle)
|
|
21
28
|
this.triggerTarget.addEventListener("click", this.toggle)
|
|
29
|
+
this.triggerTarget.addEventListener("keydown", this.keyToggle)
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
disconnect() {
|
|
26
34
|
this.triggerTarget.removeEventListener("pointerdown", this.armToggle)
|
|
27
35
|
this.triggerTarget.removeEventListener("click", this.toggle)
|
|
36
|
+
this.triggerTarget.removeEventListener("keydown", this.keyToggle)
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
armToggle = () => { this.wasOpen = this.contentTarget.matches(":popover-open") }
|
|
@@ -32,12 +41,44 @@ export default class extends Controller {
|
|
|
32
41
|
if (this.wasOpen) { this.wasOpen = false; return }
|
|
33
42
|
this.contentTarget.togglePopover()
|
|
34
43
|
}
|
|
44
|
+
keyToggle = (e) => {
|
|
45
|
+
const textEntry = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)
|
|
46
|
+
if (e.key !== "Enter" && !(e.key === " " && !textEntry)) return
|
|
47
|
+
e.preventDefault()
|
|
48
|
+
this.contentTarget.togglePopover()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Public close, for consumers that need to dismiss the panel from outside
|
|
52
|
+
// a trigger interaction (e.g. the date picker closing on a complete
|
|
53
|
+
// calendar selection). Returns focus to the trigger's focusable child —
|
|
54
|
+
// dismissing without it would strand focus on the day button that's
|
|
55
|
+
// about to disappear from the DOM.
|
|
56
|
+
close() {
|
|
57
|
+
if (this.contentTarget.matches(":popover-open")) {
|
|
58
|
+
this.contentTarget.hidePopover()
|
|
59
|
+
}
|
|
60
|
+
this.focusTrigger()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
focusTrigger() {
|
|
64
|
+
const focusable = this.fallbackInvoker
|
|
65
|
+
|| this.triggerTarget.querySelector("button, input, a[href], select, textarea, [tabindex]")
|
|
66
|
+
|| this.triggerTarget
|
|
67
|
+
focusable.focus()
|
|
68
|
+
}
|
|
35
69
|
|
|
36
70
|
// One stable handler added/removed symmetrically — an anonymous listener
|
|
37
71
|
// here would pile up a duplicate on every target reconnect.
|
|
38
|
-
syncState = (e) => {
|
|
72
|
+
syncState = (e) => {
|
|
73
|
+
e.target.dataset.state = e.newState === "open" ? "open" : "closed"
|
|
74
|
+
this.fallbackInvoker?.setAttribute("aria-expanded", e.newState === "open" ? "true" : "false")
|
|
75
|
+
}
|
|
39
76
|
|
|
40
77
|
contentTargetConnected(el) {
|
|
78
|
+
// A Turbo snapshot serializes data-state="open" but :popover-open does
|
|
79
|
+
// not survive the restore — resync before listening so host CSS keyed
|
|
80
|
+
// on the state hook doesn't misfire.
|
|
81
|
+
el.dataset.state = el.matches(":popover-open") ? "open" : "closed"
|
|
41
82
|
el.addEventListener("toggle", this.syncState)
|
|
42
83
|
}
|
|
43
84
|
|
|
@@ -14,4 +14,12 @@
|
|
|
14
14
|
width: 100%;
|
|
15
15
|
background: var(--pk-brand);
|
|
16
16
|
transition: transform .3s ease;
|
|
17
|
+
/* value% − 100%: slides the full-width indicator into view from the
|
|
18
|
+
inline-start edge. --pk-progress-value is a unitless 0–100 set inline
|
|
19
|
+
by progress.rb. */
|
|
20
|
+
transform: translateX(calc((var(--pk-progress-value, 0) - 100) * 1%));
|
|
21
|
+
}
|
|
22
|
+
/* RTL: the bar fills from the right, so travel flips. */
|
|
23
|
+
.pk-progress:dir(rtl) .pk-progress-indicator {
|
|
24
|
+
transform: translateX(calc((100 - var(--pk-progress-value, 0)) * 1%));
|
|
17
25
|
}
|
|
@@ -4,21 +4,30 @@ module PhlexKit
|
|
|
4
4
|
# JS; width comes from a caller `class:`/`style:`. Tailwind → vanilla
|
|
5
5
|
# `.pk-progress*` (progress.css).
|
|
6
6
|
class Progress < BaseComponent
|
|
7
|
-
|
|
7
|
+
# value_text: is a named kwarg (not read from **attrs) because `mix`
|
|
8
|
+
# would *fuse* a caller "aria-valuetext" with the generated default.
|
|
9
|
+
def initialize(value: 0, value_text: nil, **attrs)
|
|
8
10
|
@value = value.to_f.clamp(0.0, 100.0)
|
|
11
|
+
@value_text = value_text
|
|
9
12
|
@attrs = attrs
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def view_template
|
|
16
|
+
# %g drops float artifacts ("40.0" would be announced as "forty point
|
|
17
|
+
# zero percent"). The value travels as a custom property so the fill
|
|
18
|
+
# transform lives in progress.css, where a :dir(rtl) arm can flip it —
|
|
19
|
+
# an inline translateX always filled from the physical left.
|
|
20
|
+
v = Kernel.format("%g", @value)
|
|
13
21
|
div(**mix({
|
|
14
22
|
role: "progressbar",
|
|
15
|
-
"aria-valuenow":
|
|
23
|
+
"aria-valuenow": v,
|
|
16
24
|
"aria-valuemin": 0,
|
|
17
25
|
"aria-valuemax": 100,
|
|
18
|
-
"aria-valuetext": "#{
|
|
19
|
-
class: "pk-progress"
|
|
26
|
+
"aria-valuetext": @value_text || "#{v}%",
|
|
27
|
+
class: "pk-progress",
|
|
28
|
+
style: "--pk-progress-value: #{v};"
|
|
20
29
|
}, @attrs)) do
|
|
21
|
-
div(class: "pk-progress-indicator"
|
|
30
|
+
div(class: "pk-progress-indicator")
|
|
22
31
|
end
|
|
23
32
|
end
|
|
24
33
|
end
|
|
@@ -36,6 +36,9 @@ export default class extends Controller {
|
|
|
36
36
|
const next = handle.nextElementSibling
|
|
37
37
|
if (!prev || !next) return
|
|
38
38
|
e.preventDefault()
|
|
39
|
+
// preventDefault suppresses mouse-driven focus — hand it to the handle
|
|
40
|
+
// so a drag can be fine-tuned with the arrow keys immediately.
|
|
41
|
+
handle.focus({ preventScroll: true })
|
|
39
42
|
|
|
40
43
|
const horizontal = this.directionValue === "horizontal"
|
|
41
44
|
const sizeOf = (el) => horizontal ? el.getBoundingClientRect().width : el.getBoundingClientRect().height
|
|
@@ -12,22 +12,12 @@ module PhlexKit
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def view_template(&)
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Defaults only when the caller didn't supply their own — `mix` would
|
|
16
|
+
# fuse role="region log" / tabindex="0 -1" instead of overriding.
|
|
17
|
+
base = { class: "pk-scroll-area" }
|
|
18
|
+
base[:tabindex] = "0" unless @attrs.key?(:tabindex) || @attrs.key?("tabindex")
|
|
19
|
+
base[:role] = "region" if aria_labelled? && !@attrs.key?(:role) && !@attrs.key?("role")
|
|
17
20
|
div(**mix(base, @attrs), &)
|
|
18
21
|
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
# True when the caller supplies an accessible name, via the aria: hash
|
|
23
|
-
# (aria: { label:/labelledby: }) or flat aria_label/aria-labelledby attrs.
|
|
24
|
-
def labelled?
|
|
25
|
-
aria = @attrs[:aria] || @attrs["aria"]
|
|
26
|
-
if aria.is_a?(Hash)
|
|
27
|
-
return true if %w[label labelledby].any? { |k| aria[k.to_sym] || aria[k] }
|
|
28
|
-
end
|
|
29
|
-
[ :aria_label, "aria_label", "aria-label",
|
|
30
|
-
:aria_labelledby, "aria_labelledby", "aria-labelledby" ].any? { |k| @attrs[k] }
|
|
31
|
-
end
|
|
32
22
|
end
|
|
33
23
|
end
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
border-radius: var(--pk-radius);
|
|
24
24
|
border: 1px solid var(--pk-input);
|
|
25
25
|
color: var(--pk-text);
|
|
26
|
-
padding: .25rem
|
|
26
|
+
padding-block: .25rem;
|
|
27
|
+
padding-inline: .625rem .5rem;
|
|
27
28
|
gap: .375rem;
|
|
28
29
|
font: inherit;
|
|
29
30
|
font-size: .875rem;
|
|
@@ -87,7 +88,9 @@
|
|
|
87
88
|
user-select: none;
|
|
88
89
|
gap: .375rem;
|
|
89
90
|
border-radius: calc(var(--pk-radius) - 2px);
|
|
90
|
-
|
|
91
|
+
/* 2rem gutter on the inline-end — where .pk-select-item-check sits. */
|
|
92
|
+
padding-block: .25rem;
|
|
93
|
+
padding-inline: .375rem 2rem;
|
|
91
94
|
font-size: .875rem;
|
|
92
95
|
color: var(--pk-text);
|
|
93
96
|
outline: none;
|
|
@@ -9,7 +9,28 @@ export default class extends Controller {
|
|
|
9
9
|
static targets = ["trigger", "content", "input", "value", "item"];
|
|
10
10
|
static values = { open: Boolean };
|
|
11
11
|
|
|
12
|
+
initialize() {
|
|
13
|
+
// Stimulus fires [target]Connected before connect(): state used by
|
|
14
|
+
// itemTargetConnected must be initialized here.
|
|
15
|
+
this.itemIdCounter = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Per-item, not a connect()-time sweep: options injected later (Turbo
|
|
19
|
+
// stream/frame) must still get ids or aria-activedescendant would point
|
|
20
|
+
// at nothing — and a caller-provided id is left alone, not clobbered.
|
|
21
|
+
itemTargetConnected(item) {
|
|
22
|
+
if (!item.id) item.id = `${this.contentTarget.id}-${this.itemIdCounter++}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
connect() {
|
|
26
|
+
// A Turbo snapshot serializes aria-expanded / aria-activedescendant /
|
|
27
|
+
// aria-current even though :popover-open does not survive the restore —
|
|
28
|
+
// normalize here so a restored page doesn't announce an open listbox
|
|
29
|
+
// over a closed select (same mitigation as dropdown_menu's connect).
|
|
30
|
+
this.openValue = false;
|
|
31
|
+
this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
32
|
+
this.triggerTarget.removeAttribute("aria-activedescendant");
|
|
33
|
+
this.itemTargets.forEach((item) => item.removeAttribute("aria-current"));
|
|
13
34
|
this.generateItemsIds();
|
|
14
35
|
}
|
|
15
36
|
|
|
@@ -38,6 +59,28 @@ export default class extends Controller {
|
|
|
38
59
|
this.closeContent();
|
|
39
60
|
}
|
|
40
61
|
|
|
62
|
+
// APG select pattern: ArrowDown/ArrowUp on the CLOSED trigger opens the
|
|
63
|
+
// listbox and highlights the first/last option, instead of only working
|
|
64
|
+
// once already open (combobox_controller.js's keyDownPressed/keyUpPressed
|
|
65
|
+
// does the equivalent for its own open/closed model).
|
|
66
|
+
handleTriggerArrowDown(event) {
|
|
67
|
+
if (this.openValue) return
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
this.#show()
|
|
70
|
+
if (this.itemTargets.length === 0) return
|
|
71
|
+
this.resetCurrent()
|
|
72
|
+
this.setAriaCurrentAndActiveDescendant(0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
handleTriggerArrowUp(event) {
|
|
76
|
+
if (this.openValue) return
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
this.#show()
|
|
79
|
+
if (this.itemTargets.length === 0) return
|
|
80
|
+
this.resetCurrent()
|
|
81
|
+
this.setAriaCurrentAndActiveDescendant(this.itemTargets.length - 1)
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
onClick() {
|
|
42
85
|
this.toggleContent();
|
|
43
86
|
|
|
@@ -176,10 +219,12 @@ export default class extends Controller {
|
|
|
176
219
|
generateItemsIds() {
|
|
177
220
|
const contentId = this.contentTarget.getAttribute("id");
|
|
178
221
|
this.triggerTarget.setAttribute("aria-controls", contentId);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
// Name the listbox from the trigger (Radix wires it the same way) —
|
|
223
|
+
// an unnamed role=listbox is announced as an anonymous list.
|
|
224
|
+
if (!this.triggerTarget.id) this.triggerTarget.id = `${contentId}-trigger`;
|
|
225
|
+
if (!this.contentTarget.hasAttribute("aria-labelledby")) {
|
|
226
|
+
this.contentTarget.setAttribute("aria-labelledby", this.triggerTarget.id);
|
|
227
|
+
}
|
|
183
228
|
}
|
|
184
229
|
|
|
185
230
|
setAriaCurrentAndActiveDescendant(currentIndex) {
|
|
@@ -16,7 +16,10 @@ module PhlexKit
|
|
|
16
16
|
role: "combobox",
|
|
17
17
|
class: [ "pk-select-trigger", fetch_option(SIZES, @size, :size) ].compact.join(" "),
|
|
18
18
|
aria: { expanded: "false", haspopup: "listbox", autocomplete: "none" },
|
|
19
|
-
data: { action: "phlex-kit--select#onClick
|
|
19
|
+
data: { action: "phlex-kit--select#onClick " \
|
|
20
|
+
"keydown.down->phlex-kit--select#handleTriggerArrowDown " \
|
|
21
|
+
"keydown.up->phlex-kit--select#handleTriggerArrowUp",
|
|
22
|
+
phlex_kit__select_target: "trigger" }
|
|
20
23
|
}, @attrs)) do
|
|
21
24
|
block&.call
|
|
22
25
|
icon
|
|
@@ -24,7 +24,11 @@ module PhlexKit
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def view_template(&block)
|
|
27
|
-
|
|
27
|
+
base = { role: (@decorative ? "none" : "separator"), class: classes }
|
|
28
|
+
# role="separator" is implicitly horizontal — a semantic vertical one
|
|
29
|
+
# must say so or AT announces the wrong axis.
|
|
30
|
+
base[:"aria-orientation"] = "vertical" if !@decorative && @orientation == :vertical
|
|
31
|
+
send(@as, **mix(base, @attrs), &block)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
private
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
}
|
|
17
17
|
.pk-sheet-content.right { inset-block: 0; right: 0; height: 100%; width: 75%; max-width: 24rem; border-left: 1px solid var(--pk-border); }
|
|
18
18
|
.pk-sheet-content.left { inset-block: 0; left: 0; height: 100%; width: 75%; max-width: 24rem; border-right: 1px solid var(--pk-border); }
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/* max-height: without it a tall top/bottom panel grows past the viewport and
|
|
20
|
+
overflow:auto never engages (footer/close unreachable). */
|
|
21
|
+
.pk-sheet-content.top { inset-inline: 0; top: 0; max-height: 100dvh; border-bottom: 1px solid var(--pk-border); }
|
|
22
|
+
.pk-sheet-content.bottom { inset-inline: 0; bottom: 0; max-height: 100dvh; border-top: 1px solid var(--pk-border); }
|
|
21
23
|
.pk-sheet-header { display: flex; flex-direction: column; gap: .125rem; padding: 1rem; }
|
|
22
24
|
.pk-sheet-footer { display: flex; flex-direction: column-reverse; gap: .5rem; padding: 1rem; margin-top: auto; }
|
|
23
25
|
.pk-sheet-title { font-size: 1rem; font-weight: 500; line-height: 1; color: var(--pk-text); }
|
|
@@ -9,9 +9,19 @@ module PhlexKit
|
|
|
9
9
|
end
|
|
10
10
|
def view_template(&block)
|
|
11
11
|
template(data: { phlex_kit__sheet_target: "content" }) do
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# data-pk-overlay-clone is a common marker read by other clone-based
|
|
13
|
+
# overlays (see alert_dialog_controller.js#topmost) to determine
|
|
14
|
+
# z-stacking across overlay families when they nest.
|
|
15
|
+
div(data: { controller: "phlex-kit--sheet-content", action: "keydown->phlex-kit--sheet-content#keydown", pk_overlay_clone: "" }) do
|
|
16
|
+
div(class: "pk-sheet-backdrop", data: { action: "click->phlex-kit--sheet-content#close mousedown->phlex-kit--sheet-content#overlayMousedown" })
|
|
17
|
+
panel_attrs = { class: [ "pk-sheet-content", fetch_option(SIDES, @side, :side) ].join(" "), data: { phlex_kit__sheet_content_target: "panel" } }
|
|
18
|
+
# Defaults only when the caller didn't supply their own — `mix`
|
|
19
|
+
# would fuse role="dialog region" / aria-modal="true false" /
|
|
20
|
+
# tabindex="-1 0" instead of overriding.
|
|
21
|
+
panel_attrs[:role] = "dialog" unless @attrs.key?(:role) || @attrs.key?("role")
|
|
22
|
+
panel_attrs[:aria] = { modal: "true" } unless aria_key_set?(:modal)
|
|
23
|
+
panel_attrs[:tabindex] = "-1" unless @attrs.key?(:tabindex) || @attrs.key?("tabindex")
|
|
24
|
+
div(**mix(panel_attrs, @attrs)) do
|
|
15
25
|
block&.call
|
|
16
26
|
if @show_close_button
|
|
17
27
|
button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--sheet-content#close" }) do
|
|
@@ -15,6 +15,7 @@ export default class extends Controller {
|
|
|
15
15
|
static targets = ["panel"]
|
|
16
16
|
|
|
17
17
|
connect() {
|
|
18
|
+
this.restored = false
|
|
18
19
|
this.opener = document.activeElement
|
|
19
20
|
this.previousOverflow = document.body.style.overflow
|
|
20
21
|
document.body.style.overflow = "hidden"
|
|
@@ -26,18 +27,57 @@ export default class extends Controller {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
disconnect() {
|
|
30
|
+
this.restoreImmediate()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Synchronously undoes everything connect() did: background inert,
|
|
34
|
+
// scroll lock, and the closed-notification to the source sheet
|
|
35
|
+
// controller. Called from disconnect() (the normal removal path) AND
|
|
36
|
+
// from the source phlex-kit--sheet controller's turbo:before-cache
|
|
37
|
+
// handler, which must restore state before Turbo's synchronous snapshot
|
|
38
|
+
// — disconnect() fires via MutationObserver, too late for that snapshot.
|
|
39
|
+
// Idempotent (guarded by `this.restored`) so whichever path runs second
|
|
40
|
+
// is a harmless no-op.
|
|
41
|
+
restoreImmediate() {
|
|
42
|
+
if (this.restored) return
|
|
43
|
+
this.restored = true
|
|
29
44
|
this.#restoreInert()
|
|
30
45
|
document.body.style.overflow = this.previousOverflow
|
|
31
46
|
if (this.opener?.isConnected) this.opener.focus()
|
|
47
|
+
// Tell the source phlex-kit--sheet controller the clone is gone so it
|
|
48
|
+
// can flip the trigger's aria-expanded back (removal happens by many
|
|
49
|
+
// paths: close button, Escape, backdrop, before-cache).
|
|
50
|
+
document.dispatchEvent(new CustomEvent("phlex-kit:sheet-content:closed"))
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
close() {
|
|
35
54
|
this.element.remove()
|
|
36
55
|
}
|
|
37
56
|
|
|
57
|
+
// Backdrop mousedown would move focus to <body>, killing this
|
|
58
|
+
// element-scoped keydown listener (Escape after a drag-off-backdrop would
|
|
59
|
+
// stop working) — prevented here so focus stays inside the panel. The
|
|
60
|
+
// backdrop's click->close action still fires normally afterward.
|
|
61
|
+
overlayMousedown(event) {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
keydown(event) {
|
|
66
|
+
// A native <dialog> nested inside this panel (e.g. a Dialog opened from
|
|
67
|
+
// within SheetContent) owns its own Escape handling; the keydown still
|
|
68
|
+
// bubbles through this element-scoped listener since dialog is a DOM
|
|
69
|
+
// descendant — ignore it so one Escape doesn't also close the sheet
|
|
70
|
+
// underneath.
|
|
71
|
+
if (event.key === "Escape" && event.target.closest("dialog[open]")) return
|
|
39
72
|
if (event.key === "Escape") {
|
|
40
73
|
event.preventDefault()
|
|
74
|
+
// Stop the keydown from reaching a lower/outer overlay's own
|
|
75
|
+
// document-level Escape listener (e.g. an alert_dialog this sheet is
|
|
76
|
+
// stacked on top of) — this element-scoped listener runs first during
|
|
77
|
+
// bubbling, and by the time a document-level listener would see it,
|
|
78
|
+
// this.close() has already removed the [data-pk-overlay-clone] marker
|
|
79
|
+
// the other overlay's #topmost() check relies on.
|
|
80
|
+
event.stopPropagation()
|
|
41
81
|
this.close()
|
|
42
82
|
return
|
|
43
83
|
}
|
|
@@ -11,25 +11,55 @@ export default class extends Controller {
|
|
|
11
11
|
|
|
12
12
|
initialize() {
|
|
13
13
|
this.overlay = null
|
|
14
|
+
// Turbo snapshots synchronously right after dispatching this event —
|
|
15
|
+
// BEFORE Stimulus disconnect() runs (it fires via MutationObserver, too
|
|
16
|
+
// late to keep the snapshot from capturing an inerted, scroll-locked
|
|
17
|
+
// page). Reach into the clone's own controller and restore its state
|
|
18
|
+
// synchronously before removing it; that method is idempotent, so the
|
|
19
|
+
// content controller's own disconnect() (fired later, once the removal
|
|
20
|
+
// is observed) is a harmless no-op.
|
|
14
21
|
this.clearOverlay = () => {
|
|
15
|
-
if (this.overlay?.isConnected)
|
|
22
|
+
if (this.overlay?.isConnected) {
|
|
23
|
+
const content = this.application.getControllerForElementAndIdentifier(this.overlay, "phlex-kit--sheet-content")
|
|
24
|
+
content?.restoreImmediate()
|
|
25
|
+
this.overlay.remove()
|
|
26
|
+
}
|
|
16
27
|
this.overlay = null
|
|
17
28
|
}
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
connect() {
|
|
21
32
|
document.addEventListener("turbo:before-cache", this.clearOverlay)
|
|
33
|
+
// Popup semantics for AT: the trigger's real control announces that it
|
|
34
|
+
// opens a dialog, and expanded state tracks the clone's lifetime (the
|
|
35
|
+
// content controller signals removal via phlex-kit:sheet-content:closed).
|
|
36
|
+
// Drawer rides this same controller (see drawer.rb) — its trigger is
|
|
37
|
+
// marked .pk-drawer-trigger rather than .pk-sheet-trigger.
|
|
38
|
+
this.invoker = this.element.querySelector(
|
|
39
|
+
".pk-sheet-trigger button, .pk-sheet-trigger a, .pk-sheet-trigger [tabindex], .pk-drawer-trigger button, .pk-drawer-trigger a, .pk-drawer-trigger [tabindex]"
|
|
40
|
+
)
|
|
41
|
+
this.invoker?.setAttribute("aria-haspopup", "dialog")
|
|
42
|
+
this.invoker?.setAttribute("aria-expanded", "false")
|
|
43
|
+
this.onOverlayClosed = () => {
|
|
44
|
+
if (!this.overlay?.isConnected) this.invoker?.setAttribute("aria-expanded", "false")
|
|
45
|
+
}
|
|
46
|
+
document.addEventListener("phlex-kit:sheet-content:closed", this.onOverlayClosed)
|
|
22
47
|
if (this.openValue) this.open()
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
disconnect() {
|
|
26
51
|
document.removeEventListener("turbo:before-cache", this.clearOverlay)
|
|
52
|
+
document.removeEventListener("phlex-kit:sheet-content:closed", this.onOverlayClosed)
|
|
27
53
|
this.clearOverlay()
|
|
28
54
|
}
|
|
29
55
|
|
|
30
56
|
open() {
|
|
31
57
|
if (this.overlay?.isConnected) return
|
|
58
|
+
// open: is one-shot — the reflected value sits in the Turbo snapshot, so
|
|
59
|
+
// a cache-restored reconnect would re-open a dismissed sheet.
|
|
60
|
+
this.openValue = false
|
|
32
61
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
|
|
33
62
|
this.overlay = document.body.lastElementChild
|
|
63
|
+
this.invoker?.setAttribute("aria-expanded", "true")
|
|
34
64
|
}
|
|
35
65
|
}
|
|
@@ -11,8 +11,16 @@ module PhlexKit
|
|
|
11
11
|
# untouched — keep the label in a real element there. Attrs pass through via
|
|
12
12
|
# mix. See sidebar.rb.
|
|
13
13
|
class SidebarMenuButton < BaseComponent
|
|
14
|
+
# `as:` is dispatched dynamically (send) — whitelist so it can't reach
|
|
15
|
+
# arbitrary (including private) methods; to_sym so a String "a" still
|
|
16
|
+
# earns aria-current when active.
|
|
17
|
+
AS_TAGS = %i[button a].freeze
|
|
18
|
+
|
|
14
19
|
def initialize(as: :button, active: false, tooltip: nil, **attrs)
|
|
15
|
-
@as = as
|
|
20
|
+
@as = as.to_sym
|
|
21
|
+
unless AS_TAGS.include?(@as)
|
|
22
|
+
raise ArgumentError, "unknown SidebarMenuButton as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
23
|
+
end
|
|
16
24
|
@active = active
|
|
17
25
|
@tooltip = tooltip
|
|
18
26
|
@attrs = attrs
|
|
@@ -5,8 +5,15 @@ module PhlexKit
|
|
|
5
5
|
# primary). Kwarg rather than attr for the same mix-merge reason as
|
|
6
6
|
# SidebarMenuButton. See sidebar.rb.
|
|
7
7
|
class SidebarMenuSubButton < BaseComponent
|
|
8
|
+
# `as:` is dispatched dynamically (send) — whitelist, matching
|
|
9
|
+
# SidebarMenuButton.
|
|
10
|
+
AS_TAGS = %i[a button].freeze
|
|
11
|
+
|
|
8
12
|
def initialize(as: :a, active: false, **attrs)
|
|
9
|
-
@as = as
|
|
13
|
+
@as = as.to_sym
|
|
14
|
+
unless AS_TAGS.include?(@as)
|
|
15
|
+
raise ArgumentError, "unknown SidebarMenuSubButton as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
16
|
+
end
|
|
10
17
|
@active = active
|
|
11
18
|
@attrs = attrs
|
|
12
19
|
end
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
var(--pk-brand) var(--pk-slider-progress, 50%),
|
|
19
19
|
var(--pk-surface-2) var(--pk-slider-progress, 50%));
|
|
20
20
|
}
|
|
21
|
+
/* Chrome mirrors the native range input in RTL but the gradient stays
|
|
22
|
+
physical — repaint the fill from the right. (:dir() reads document
|
|
23
|
+
directionality, so .vertical's internal `direction: rtl` flip below
|
|
24
|
+
doesn't match; its own `to top` gradient also comes later and wins.) */
|
|
25
|
+
.pk-slider:dir(rtl)::-webkit-slider-runnable-track {
|
|
26
|
+
background: linear-gradient(to left,
|
|
27
|
+
var(--pk-brand) var(--pk-slider-progress, 50%),
|
|
28
|
+
var(--pk-surface-2) var(--pk-slider-progress, 50%));
|
|
29
|
+
}
|
|
21
30
|
.pk-slider::-webkit-slider-thumb {
|
|
22
31
|
-webkit-appearance: none;
|
|
23
32
|
width: .75rem;
|
|
@@ -57,6 +66,12 @@
|
|
|
57
66
|
.pk-slider:active::-moz-range-thumb {
|
|
58
67
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
59
68
|
}
|
|
69
|
+
/* line 13 strips the native outline in every browser — without this arm the
|
|
70
|
+
replacement ring only existed on the -webkit thumb and Firefox keyboard
|
|
71
|
+
focus was invisible. */
|
|
72
|
+
.pk-slider:focus-visible::-moz-range-thumb {
|
|
73
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
74
|
+
}
|
|
60
75
|
.pk-slider:disabled { opacity: .5; cursor: not-allowed; }
|
|
61
76
|
/* Vertical: modern writing-mode flip (min-h-40 like theirs). */
|
|
62
77
|
.pk-slider.vertical {
|
|
@@ -11,12 +11,15 @@ module PhlexKit
|
|
|
11
11
|
|
|
12
12
|
def view_template
|
|
13
13
|
# A visible loader, not decoration: role=status + label, aria-hidden off.
|
|
14
|
-
|
|
14
|
+
base = {
|
|
15
15
|
class: [ "pk-spinner", fetch_option(SIZES, @size, :size) ].compact.join(" "),
|
|
16
16
|
role: "status",
|
|
17
|
-
aria: { label: "Loading" },
|
|
18
17
|
"aria-hidden": false
|
|
19
|
-
}
|
|
18
|
+
}
|
|
19
|
+
# Default label only when the caller didn't supply their own — `mix`
|
|
20
|
+
# would fuse aria-label="Loading Saving" instead of overriding.
|
|
21
|
+
base[:aria] = { label: "Loading" } unless aria_labelled?
|
|
22
|
+
render Icon.new(:loader, size: nil, **mix(base, @attrs))
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
transform: translateX(calc(100% - 2px));
|
|
50
50
|
background: var(--pk-brand-ink);
|
|
51
51
|
}
|
|
52
|
+
/* RTL: the resting thumb sits at the physical right — checked travel must
|
|
53
|
+
slide it left or it leaves the track. */
|
|
54
|
+
.pk-switch:dir(rtl):has(.pk-switch-input:checked) .pk-switch-thumb {
|
|
55
|
+
transform: translateX(calc(-100% + 2px));
|
|
56
|
+
}
|
|
52
57
|
:root[data-theme="light"] .pk-switch { background: var(--pk-input); }
|
|
53
58
|
:root[data-theme="light"] .pk-switch:has(.pk-switch-input:checked) { background: var(--pk-brand); }
|
|
54
59
|
:root[data-theme="light"] .pk-switch-thumb { background: var(--pk-bg) !important; }
|
|
@@ -6,6 +6,12 @@ module PhlexKit
|
|
|
6
6
|
SIZES = { md: nil, sm: "sm" }.freeze
|
|
7
7
|
|
|
8
8
|
def initialize(include_hidden: true, checked_value: "1", unchecked_value: "0", size: :md, wrapper: {}, **attrs)
|
|
9
|
+
# value: is silently clobbered below (the checkbox's real value comes
|
|
10
|
+
# from checked_value:) — fail loud rather than let a caller's value:
|
|
11
|
+
# vanish (mirrors ToggleGroupItem's unsupported-kwargs guard).
|
|
12
|
+
if attrs.key?(:value)
|
|
13
|
+
raise ArgumentError, "Switch does not support value: — the checkbox's value comes from checked_value:, pass that instead"
|
|
14
|
+
end
|
|
9
15
|
@include_hidden = include_hidden
|
|
10
16
|
@checked_value = checked_value
|
|
11
17
|
@unchecked_value = unchecked_value
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
.pk-table-wrapper { position: relative; width: 100%; overflow: auto; }
|
|
6
6
|
.pk-table { width: 100%; border-collapse: collapse; caption-side: bottom; font-size: .875rem; }
|
|
7
7
|
|
|
8
|
-
.pk-table-header .pk-table-row { border-bottom: 1px solid var(--pk-border); }
|
|
9
8
|
.pk-table-body .pk-table-row:last-child { border-bottom: 0; }
|
|
10
9
|
.pk-table-footer { border-top: 1px solid var(--pk-border); font-weight: 500; background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
|
|
10
|
+
/* shadcn parity: the totals row doesn't draw a stray line under the table. */
|
|
11
|
+
.pk-table-footer .pk-table-row:last-child { border-bottom: 0; }
|
|
11
12
|
|
|
12
13
|
.pk-table-row { border-bottom: 1px solid var(--pk-border); transition: background-color .12s ease; }
|
|
13
14
|
.pk-table-row:hover { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
|
|
@@ -22,12 +22,15 @@ module PhlexKit
|
|
|
22
22
|
#
|
|
23
23
|
# Tailwind → vanilla `.pk-table-*` (table.css), palette from the global tokens.
|
|
24
24
|
class Table < BaseComponent
|
|
25
|
-
def initialize(**attrs)
|
|
25
|
+
def initialize(wrapper: {}, **attrs)
|
|
26
|
+
@wrapper = wrapper
|
|
26
27
|
@attrs = attrs
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def view_template(&block)
|
|
30
|
-
|
|
31
|
+
# Caller attrs land on the <table>; wrapper: reaches the scroll
|
|
32
|
+
# container (e.g. to cap its height) — same escape hatch as Switch.
|
|
33
|
+
div(**mix({ class: "pk-table-wrapper" }, @wrapper)) do
|
|
31
34
|
table(**mix({ class: "pk-table" }, @attrs), &block)
|
|
32
35
|
end
|
|
33
36
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class TabsContent < BaseComponent
|
|
3
|
-
|
|
3
|
+
# id: is a named kwarg (not left in **attrs) — see TabsTrigger.
|
|
4
|
+
def initialize(value:, active: false, id: nil, **attrs)
|
|
4
5
|
@value = value
|
|
5
6
|
@active = active
|
|
7
|
+
@id = id || "pk-tabs-panel-#{value}"
|
|
6
8
|
@attrs = attrs
|
|
7
9
|
end
|
|
8
10
|
def view_template(&)
|
|
@@ -11,7 +13,7 @@ module PhlexKit
|
|
|
11
13
|
# focusable, per the APG tabs pattern. `active:` (mirroring TabsTrigger's)
|
|
12
14
|
# renders the panel visible pre-JS/no-JS; the controller's sync()
|
|
13
15
|
# reconciles visibility from the active value after connect.
|
|
14
|
-
div(**mix({ class: @active ? "pk-tabs-content" : "pk-tabs-content pk-hidden", role: "tabpanel", id:
|
|
16
|
+
div(**mix({ class: @active ? "pk-tabs-content" : "pk-tabs-content pk-hidden", role: "tabpanel", id: @id, aria_labelledby: "pk-tabs-trigger-#{@value}", tabindex: "0", data: { phlex_kit__tabs_target: "content", value: @value } }, @attrs), &)
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -29,8 +29,14 @@ export default class extends Controller {
|
|
|
29
29
|
// tab sets also answer to ArrowUp/ArrowDown.
|
|
30
30
|
keydown(event) {
|
|
31
31
|
const vertical = this.element.classList.contains("vertical")
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// In RTL the tablist mirrors: the physical LEFT arrow moves to the NEXT
|
|
33
|
+
// trigger. The vertical Up/Down pair is unaffected (runtime dir check is
|
|
34
|
+
// reliable even after a dynamic flip — carousel's idiom).
|
|
35
|
+
const rtl = getComputedStyle(this.element).direction === "rtl"
|
|
36
|
+
const fwd = rtl ? "ArrowLeft" : "ArrowRight"
|
|
37
|
+
const back = rtl ? "ArrowRight" : "ArrowLeft"
|
|
38
|
+
const nextKeys = vertical ? [fwd, "ArrowDown"] : [fwd]
|
|
39
|
+
const prevKeys = vertical ? [back, "ArrowUp"] : [back]
|
|
34
40
|
const triggers = this.triggerTargets.filter((el) => !el.disabled)
|
|
35
41
|
if (triggers.length === 0) return
|
|
36
42
|
const index = triggers.indexOf(document.activeElement)
|