phlex_kit 0.13.0 → 0.14.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/components/phlex_kit/accordion/accordion_content.rb +3 -1
- data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +1 -1
- data/app/components/phlex_kit/accordion/accordion_trigger.rb +1 -1
- data/app/components/phlex_kit/alert/alert.rb +5 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +16 -6
- data/app/components/phlex_kit/alert_dialog/alert_dialog_media.rb +4 -1
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +14 -4
- data/app/components/phlex_kit/attachment/attachment.css +7 -1
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +4 -3
- data/app/components/phlex_kit/avatar/avatar.css +4 -0
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +4 -0
- data/app/components/phlex_kit/breadcrumb/breadcrumb.rb +8 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb_page.rb +1 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb_separator.rb +1 -1
- data/app/components/phlex_kit/bubble/bubble_content.rb +5 -1
- data/app/components/phlex_kit/button_group/button_group_separator.rb +1 -1
- data/app/components/phlex_kit/calendar/calendar.css +5 -0
- data/app/components/phlex_kit/calendar/calendar_controller.js +26 -8
- data/app/components/phlex_kit/calendar/calendar_next.rb +5 -3
- data/app/components/phlex_kit/calendar/calendar_prev.rb +5 -3
- data/app/components/phlex_kit/carousel/carousel.css +7 -0
- data/app/components/phlex_kit/carousel/carousel.rb +8 -4
- data/app/components/phlex_kit/carousel/carousel_controller.js +64 -20
- data/app/components/phlex_kit/carousel/carousel_item.rb +5 -5
- data/app/components/phlex_kit/chart/chart_controller.js +5 -1
- data/app/components/phlex_kit/checkbox/checkbox.css +16 -0
- data/app/components/phlex_kit/checkbox/checkbox.rb +5 -1
- data/app/components/phlex_kit/clipboard/clipboard.rb +7 -0
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +12 -1
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +5 -3
- data/app/components/phlex_kit/combobox/combobox.rb +1 -1
- data/app/components/phlex_kit/combobox/combobox_clear_button.rb +5 -3
- data/app/components/phlex_kit/combobox/combobox_controller.js +56 -5
- data/app/components/phlex_kit/combobox/combobox_item.rb +7 -4
- data/app/components/phlex_kit/command/command_controller.js +9 -1
- data/app/components/phlex_kit/command/command_dialog_content.rb +10 -6
- data/app/components/phlex_kit/command/command_empty.rb +5 -3
- data/app/components/phlex_kit/command/command_separator.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +7 -5
- data/app/components/phlex_kit/context_menu/context_menu_content.rb +7 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +17 -2
- data/app/components/phlex_kit/context_menu/context_menu_group.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +5 -2
- data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +7 -5
- data/app/components/phlex_kit/context_menu/context_menu_separator.rb +6 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +12 -9
- data/app/components/phlex_kit/data_table/data_table.css +4 -0
- data/app/components/phlex_kit/data_table/data_table_search_controller.js +15 -1
- data/app/components/phlex_kit/data_table/data_table_select_all_checkbox.rb +8 -4
- data/app/components/phlex_kit/drawer/drawer.css +4 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +7 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +10 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_group.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +3 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_group.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +7 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_separator.rb +6 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_content.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +12 -9
- data/app/components/phlex_kit/empty/empty.rb +1 -0
- data/app/components/phlex_kit/field/field.rb +4 -1
- data/app/components/phlex_kit/field/field_error.rb +4 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +22 -5
- data/app/components/phlex_kit/icon/icon.rb +5 -1
- data/app/components/phlex_kit/input_group/input_group.css +6 -3
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +14 -6
- data/app/components/phlex_kit/input_otp/input_otp_slot.rb +7 -6
- data/app/components/phlex_kit/item/item.css +9 -1
- data/app/components/phlex_kit/item/item_separator.rb +1 -1
- data/app/components/phlex_kit/label/label.css +6 -1
- data/app/components/phlex_kit/menubar/menubar.rb +5 -3
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +6 -6
- data/app/components/phlex_kit/menubar/menubar_content.rb +5 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +11 -1
- data/app/components/phlex_kit/menubar/menubar_group.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_item.rb +4 -3
- data/app/components/phlex_kit/menubar/menubar_menu.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +6 -6
- data/app/components/phlex_kit/menubar/menubar_separator.rb +6 -1
- data/app/components/phlex_kit/menubar/menubar_sub_content.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +12 -11
- data/app/components/phlex_kit/menubar/menubar_trigger.rb +7 -4
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +5 -0
- data/app/components/phlex_kit/pagination/pagination.css +4 -0
- data/app/components/phlex_kit/radio_button/radio_button.rb +1 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +17 -4
- data/app/components/phlex_kit/resizable/resizable_handle.rb +9 -7
- data/app/components/phlex_kit/select/select.rb +1 -1
- data/app/components/phlex_kit/select/select_content.rb +7 -5
- data/app/components/phlex_kit/select/select_controller.js +10 -0
- data/app/components/phlex_kit/select/select_separator.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +12 -5
- data/app/components/phlex_kit/sidebar/sidebar_group_action.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_action.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +3 -2
- data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +2 -1
- data/app/components/phlex_kit/switch/switch.rb +4 -1
- data/app/components/phlex_kit/tabs/tabs_content.rb +6 -1
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +3 -1
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +24 -6
- data/app/components/phlex_kit/toast/toast_controller.js +46 -1
- data/app/components/phlex_kit/toast/toaster_controller.js +29 -1
- data/app/components/phlex_kit/toggle/toggle.rb +1 -1
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +18 -4
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +2 -2
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +5 -0
- data/app/components/phlex_kit/typography/typography.css +5 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -24,8 +24,12 @@ module PhlexKit
|
|
|
24
24
|
if @include_hidden && @attrs[:name] && !@attrs[:name].to_s.end_with?("[]")
|
|
25
25
|
# Disabled in lockstep with the checkbox (Rails' check_box idiom) —
|
|
26
26
|
# a disabled checkbox must not still post its unchecked value.
|
|
27
|
+
# form: rides along too (Rails' check_box slices name/disabled/form
|
|
28
|
+
# onto the hidden field) — a form-attributed checkbox outside its
|
|
29
|
+
# <form> must post the unchecked value to the same form.
|
|
27
30
|
input(type: "hidden", name: @attrs[:name], value: @unchecked_value,
|
|
28
|
-
disabled: @attrs[:disabled] ? true : nil
|
|
31
|
+
disabled: @attrs[:disabled] ? true : nil,
|
|
32
|
+
form: @attrs[:form] || @attrs["form"])
|
|
29
33
|
end
|
|
30
34
|
input(**mix({
|
|
31
35
|
type: :checkbox,
|
|
@@ -13,6 +13,13 @@ module PhlexKit
|
|
|
13
13
|
div(&block)
|
|
14
14
|
render PhlexKit::ClipboardPopover.new(type: :success) { @success }
|
|
15
15
|
render PhlexKit::ClipboardPopover.new(type: :error) { @error }
|
|
16
|
+
# A persistent (never display:none) sr-only live region: the visual
|
|
17
|
+
# popovers are shown by removing display:none, which re-inserts them
|
|
18
|
+
# into the a11y tree with pre-existing content — a change AT does not
|
|
19
|
+
# announce. This region stays in the tree; the controller writes the
|
|
20
|
+
# confirmation text into it on copy, and THAT mutation announces.
|
|
21
|
+
div(class: "pk-sr-only", role: "status", aria: { live: "polite" },
|
|
22
|
+
data: { phlex_kit__clipboard_target: "liveRegion" })
|
|
16
23
|
end
|
|
17
24
|
end
|
|
18
25
|
end
|
|
@@ -4,7 +4,7 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
4
4
|
// flashes a success/error popover (CSS-positioned, no @floating-ui). Ported from ruby_ui.
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
// No "trigger" target: the click->#copy action alone wires the trigger.
|
|
7
|
-
static targets = ["source", "successPopover", "errorPopover"]
|
|
7
|
+
static targets = ["source", "successPopover", "errorPopover", "liveRegion"]
|
|
8
8
|
|
|
9
9
|
connect() {
|
|
10
10
|
// Turbo snapshots at turbo:before-cache, BEFORE disconnect — hide the
|
|
@@ -49,6 +49,17 @@ export default class extends Controller {
|
|
|
49
49
|
show(target) {
|
|
50
50
|
this.hideAll()
|
|
51
51
|
target.classList.remove("pk-hidden")
|
|
52
|
+
// Announce via the persistent live region (not the visual popover, which
|
|
53
|
+
// display:none keeps out of the a11y tree). Clear then re-assert on the
|
|
54
|
+
// next frame so an identical consecutive message still reads as a content
|
|
55
|
+
// change — aria-live announces changes, not the current state.
|
|
56
|
+
if (this.hasLiveRegionTarget) {
|
|
57
|
+
const message = target.textContent.trim()
|
|
58
|
+
this.liveRegionTarget.textContent = ""
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
if (this.hasLiveRegionTarget) this.liveRegionTarget.textContent = message
|
|
61
|
+
})
|
|
62
|
+
}
|
|
52
63
|
clearTimeout(this.timer)
|
|
53
64
|
this.timer = setTimeout(() => target.classList.add("pk-hidden"), 1500)
|
|
54
65
|
}
|
|
@@ -6,9 +6,11 @@ module PhlexKit
|
|
|
6
6
|
@attrs = attrs
|
|
7
7
|
end
|
|
8
8
|
def view_template(&block)
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
9
|
+
# Visual-only now (aria-hidden): the announcement rides Clipboard's
|
|
10
|
+
# persistent sr-only role="status" live region — toggling display:none
|
|
11
|
+
# on this element never announced, since AT ignores content that was
|
|
12
|
+
# already present when a region re-enters the tree.
|
|
13
|
+
div(class: "pk-clipboard-popover pk-hidden", aria: { hidden: "true" }, data: { phlex_kit__clipboard_target: fetch_option(TARGETS, @type, :target) }) do
|
|
12
14
|
div(**mix({ class: "pk-clipboard-popover-inner" }, @attrs), &block)
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -28,7 +28,7 @@ module PhlexKit
|
|
|
28
28
|
controller: "phlex-kit--combobox",
|
|
29
29
|
phlex_kit__combobox_term_value: @term,
|
|
30
30
|
phlex_kit__combobox_auto_highlight_value: (@auto_highlight ? "true" : nil),
|
|
31
|
-
action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside"
|
|
31
|
+
action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside focusout->phlex-kit--combobox#onFocusout"
|
|
32
32
|
}
|
|
33
33
|
}, @attrs)) do
|
|
34
34
|
live_region
|
|
@@ -9,15 +9,17 @@ module PhlexKit
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def view_template
|
|
12
|
-
|
|
12
|
+
base = {
|
|
13
13
|
type: :button,
|
|
14
14
|
class: "pk-combobox-clear-button pk-hidden",
|
|
15
|
-
aria: { label: "Clear selection" },
|
|
16
15
|
data: {
|
|
17
16
|
phlex_kit__combobox_target: "clearButton",
|
|
18
17
|
action: "phlex-kit--combobox#clearAll"
|
|
19
18
|
}
|
|
20
|
-
}
|
|
19
|
+
}
|
|
20
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
21
|
+
base[:aria] = { label: "Clear selection" } unless aria_labelled?
|
|
22
|
+
button(**mix(base, @attrs)) do
|
|
21
23
|
render Icon.new(:x, size: 24)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -42,6 +42,10 @@ export default class extends Controller {
|
|
|
42
42
|
// itemTargetConnected uses must be initialized here.
|
|
43
43
|
this.itemIdCounter = 0
|
|
44
44
|
this.refocusing = false
|
|
45
|
+
// The query the list is currently filtered by. Initialized to "" so a
|
|
46
|
+
// caret/modifier keyup on an empty, unfiltered field reads as unchanged
|
|
47
|
+
// and filterItems skips re-running (which would wipe the highlight).
|
|
48
|
+
this._lastFilter = ""
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
connect() {
|
|
@@ -109,16 +113,20 @@ export default class extends Controller {
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
inputChanged(e) {
|
|
116
|
+
// Uncheck "select all" BEFORE updateTriggerContent: syncAriaSelected reads
|
|
117
|
+
// the toggle-all checkbox to set its row's aria-selected, so syncing first
|
|
118
|
+
// left the row marked aria-selected="true" while its checkbox showed
|
|
119
|
+
// unchecked.
|
|
120
|
+
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
121
|
+
this.toggleAllTarget.checked = false
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
this.updateTriggerContent()
|
|
113
125
|
|
|
114
126
|
if (e.target.type == "radio") {
|
|
115
127
|
this.closePopover()
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
119
|
-
this.toggleAllTarget.checked = false
|
|
120
|
-
}
|
|
121
|
-
|
|
122
130
|
// Selecting from a chip trigger consumes the query: clear it and reshow
|
|
123
131
|
// the full list for the next pick.
|
|
124
132
|
if (this.hasBadgeInputTarget && this.badgeInputTarget.value !== "") {
|
|
@@ -336,6 +344,15 @@ export default class extends Controller {
|
|
|
336
344
|
if (this.hasPopoverTarget && this.popoverTarget.matches(":popover-open")) this.popoverTarget.hidePopover()
|
|
337
345
|
this.updateTriggerContent() // reflect the selection into an input trigger
|
|
338
346
|
|
|
347
|
+
// Reset the filter so a REOPEN shows the full list, not the last query's
|
|
348
|
+
// leftover pk-hidden classes. The input trigger keeps showing the
|
|
349
|
+
// selection label (updateTriggerContent, above) but that's display-only —
|
|
350
|
+
// the active query is empty until the user types again; an in-popover or
|
|
351
|
+
// badge search field is cleared to match.
|
|
352
|
+
if (this.hasSearchInputTarget) this.searchInputTarget.value = ""
|
|
353
|
+
if (this.hasBadgeInputTarget) this.badgeInputTarget.value = ""
|
|
354
|
+
this.applyFilter("")
|
|
355
|
+
|
|
339
356
|
if (focusWasInside) {
|
|
340
357
|
const field = this.filterField()
|
|
341
358
|
// Input/badge layouts: back to the filter field (it lives outside the
|
|
@@ -350,6 +367,18 @@ export default class extends Controller {
|
|
|
350
367
|
}
|
|
351
368
|
}
|
|
352
369
|
|
|
370
|
+
// APG (combobox): Tab out of an open combobox closes the popup —
|
|
371
|
+
// dropdown_menu's onFocusout pattern. relatedTarget is null when the
|
|
372
|
+
// window itself blurs; leave the popup alone then. closePopover's
|
|
373
|
+
// focus-restore is a no-op here (focus has already left the popover, so
|
|
374
|
+
// focusWasInside is false — the user's Tab target keeps focus).
|
|
375
|
+
onFocusout(event) {
|
|
376
|
+
if (!this.isOpen()) return
|
|
377
|
+
const to = event.relatedTarget
|
|
378
|
+
if (!to || this.element.contains(to)) return
|
|
379
|
+
this.closePopover()
|
|
380
|
+
}
|
|
381
|
+
|
|
353
382
|
onClickOutside(event) {
|
|
354
383
|
if (!this.isOpen()) return
|
|
355
384
|
if (this.element.contains(event.target)) return
|
|
@@ -380,7 +409,25 @@ export default class extends Controller {
|
|
|
380
409
|
const field = (e.target instanceof HTMLInputElement) ? e.target : this.filterField()
|
|
381
410
|
if (!field) return
|
|
382
411
|
|
|
383
|
-
|
|
412
|
+
const term = field.value.toLowerCase()
|
|
413
|
+
// Only re-filter when the query text actually changed. filterItems runs on
|
|
414
|
+
// every keyup, so caret/modifier keys the exclusion list above misses
|
|
415
|
+
// (ArrowLeft/Right/Home/End, releasing Shift) would otherwise re-run
|
|
416
|
+
// applyFilter on the unchanged query and wipe the keyboard highlight
|
|
417
|
+
// (selectedItemIndex, aria-current, aria-activedescendant).
|
|
418
|
+
if (term === this._lastFilter) return
|
|
419
|
+
|
|
420
|
+
// Typing into a CLOSED input/badge trigger must reveal the results — the
|
|
421
|
+
// filtering is otherwise invisible (only click/focusin/Arrow keys opened
|
|
422
|
+
// it). Open without openPopover()'s field.select(), which would select the
|
|
423
|
+
// just-typed text so the next keystroke replaced it. The button layout's
|
|
424
|
+
// search field lives inside the popover, so it can't be typed while closed.
|
|
425
|
+
if (!this.isOpen() && term && (this.hasInputTriggerTarget || this.hasBadgeInputTarget)) {
|
|
426
|
+
this.setExpanded(true)
|
|
427
|
+
this.popoverTarget.showPopover()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.applyFilter(term)
|
|
384
431
|
}
|
|
385
432
|
|
|
386
433
|
// Strips combining diacritical marks after an NFD decomposition, so "é"
|
|
@@ -390,6 +437,10 @@ export default class extends Controller {
|
|
|
390
437
|
}
|
|
391
438
|
|
|
392
439
|
applyFilter(filterTerm) {
|
|
440
|
+
// The query the list is currently filtered by — filterItems reads this to
|
|
441
|
+
// skip re-filtering (and re-wiping the highlight) on caret/modifier keys.
|
|
442
|
+
this._lastFilter = filterTerm
|
|
443
|
+
|
|
393
444
|
if (this.hasToggleAllTarget) {
|
|
394
445
|
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("pk-hidden")
|
|
395
446
|
else this.toggleAllTarget.parentElement.classList.remove("pk-hidden")
|
|
@@ -11,12 +11,15 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&)
|
|
14
|
-
|
|
14
|
+
base = {
|
|
15
15
|
class: "pk-combobox-item",
|
|
16
|
-
role: "option",
|
|
17
|
-
aria: { selected: "false" },
|
|
18
16
|
data: { phlex_kit__combobox_target: "item" }
|
|
19
|
-
}
|
|
17
|
+
}
|
|
18
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses
|
|
19
|
+
# (a server-rendered initially-selected option sets its own selected).
|
|
20
|
+
base[:role] = "option" unless attr_set?(:role)
|
|
21
|
+
base[:aria] = { selected: "false" } unless aria_key_set?(:selected)
|
|
22
|
+
label(**mix(base, @attrs), &)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
@@ -75,7 +75,15 @@ export default class extends Controller {
|
|
|
75
75
|
// Only the cloned dialog overlay grabs focus on connect — an inline
|
|
76
76
|
// palette connecting at page load must not steal it.
|
|
77
77
|
if (this.isDialogClone()) this.inputTarget.focus();
|
|
78
|
-
|
|
78
|
+
// Re-derive item + empty-state visibility from the LIVE input value, not an
|
|
79
|
+
// unconditional hide: a Turbo cache restore of an inline palette can
|
|
80
|
+
// reconnect with an active no-match query still in the input — hiding the
|
|
81
|
+
// empty row then showed the query with neither items NOR a "No results".
|
|
82
|
+
if (this.inputTarget.value) {
|
|
83
|
+
this.filter({ target: this.inputTarget });
|
|
84
|
+
} else {
|
|
85
|
+
this.toggleVisibility(this.emptyTargets, false);
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// The command-dialog controller clones its <template> content into <body>;
|
|
@@ -28,12 +28,16 @@ module PhlexKit
|
|
|
28
28
|
action: "keydown->phlex-kit--command#trapFocus"
|
|
29
29
|
}) do
|
|
30
30
|
backdrop
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
panel_attrs = { class: panel_classes, data: { state: "open" } }
|
|
32
|
+
# Defaults only when the caller didn't supply their own — `mix`
|
|
33
|
+
# would fuse role="dialog x" / aria-modal="true false" (same guard
|
|
34
|
+
# as AlertDialogContent/SheetContent/DrawerContent, round 7).
|
|
35
|
+
panel_attrs[:role] = "dialog" unless attr_set?(:role)
|
|
36
|
+
aria = {}
|
|
37
|
+
aria[:modal] = "true" unless aria_key_set?(:modal)
|
|
38
|
+
aria[:label] = @aria_label unless aria_labelled?
|
|
39
|
+
panel_attrs[:aria] = aria unless aria.empty?
|
|
40
|
+
div(**mix(panel_attrs, @attrs), &block)
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# "No results" row —
|
|
3
|
-
#
|
|
2
|
+
# "No results" row — revealed by the controller only when a filter matches
|
|
3
|
+
# nothing. Server-rendered with pk-hidden so an inline palette doesn't flash
|
|
4
|
+
# "No results" under its items before Stimulus connects, and doesn't show it
|
|
5
|
+
# permanently with JS disabled. See command.rb.
|
|
4
6
|
class CommandEmpty < BaseComponent
|
|
5
7
|
def initialize(**attrs)
|
|
6
8
|
@attrs = attrs
|
|
@@ -8,7 +10,7 @@ module PhlexKit
|
|
|
8
10
|
|
|
9
11
|
def view_template(&)
|
|
10
12
|
div(**mix({
|
|
11
|
-
class: "pk-command-empty",
|
|
13
|
+
class: "pk-command-empty pk-hidden",
|
|
12
14
|
role: "presentation",
|
|
13
15
|
data: { phlex_kit__command_target: "empty" }
|
|
14
16
|
}, @attrs), &)
|
|
@@ -12,13 +12,15 @@ module PhlexKit
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def view_template(&block)
|
|
15
|
-
|
|
15
|
+
base = {
|
|
16
16
|
class: "pk-context-menu-item pk-context-menu-checkbox-item",
|
|
17
|
-
role: "menuitemcheckbox",
|
|
18
|
-
tabindex: "-1",
|
|
19
|
-
aria: { checked: @checked ? "true" : "false" },
|
|
20
17
|
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
21
|
-
}
|
|
18
|
+
}
|
|
19
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
20
|
+
base[:role] = "menuitemcheckbox" unless attr_set?(:role)
|
|
21
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
22
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
23
|
+
label(**mix(base, @attrs)) do
|
|
22
24
|
input(type: :checkbox, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
23
25
|
tabindex: "-1", data: { action: "change->phlex-kit--context-menu#syncChecked" })
|
|
24
26
|
span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
|
|
@@ -2,7 +2,13 @@ module PhlexKit
|
|
|
2
2
|
class ContextMenuContent < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template(&)
|
|
5
|
-
|
|
5
|
+
base = { class: "pk-context-menu-content", data: { phlex_kit__context_menu_target: "content", state: "closed" } }
|
|
6
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
7
|
+
base[:popover] = "manual" unless attr_set?(:popover)
|
|
8
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
9
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
10
|
+
base[:aria] = { orientation: "vertical" } unless aria_key_set?(:orientation)
|
|
11
|
+
div(**mix(base, @attrs), &)
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -15,7 +15,16 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
15
15
|
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
16
16
|
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
17
17
|
// one — rare enough to accept.
|
|
18
|
-
const swallowClick = (ev) =>
|
|
18
|
+
const swallowClick = (ev) => {
|
|
19
|
+
// preventDefault alone suppresses only native default actions; the
|
|
20
|
+
// dismissing outside click would still fire addEventListener handlers on
|
|
21
|
+
// whatever sits under the pointer. stopPropagation (this listener is
|
|
22
|
+
// capture-phase on window, the outermost node, so it runs before any deeper
|
|
23
|
+
// handler) starves those too — the modal-menu contract is that an outside
|
|
24
|
+
// click ONLY dismisses.
|
|
25
|
+
ev.preventDefault()
|
|
26
|
+
ev.stopPropagation()
|
|
27
|
+
}
|
|
19
28
|
function armSwallowClick() {
|
|
20
29
|
window.addEventListener("click", swallowClick, { once: true, capture: true })
|
|
21
30
|
}
|
|
@@ -25,7 +34,13 @@ export default class extends Controller {
|
|
|
25
34
|
|
|
26
35
|
connect() {
|
|
27
36
|
this.onDoc = (e) => {
|
|
28
|
-
if (this.contentTarget.contains(e.target))
|
|
37
|
+
if (this.contentTarget.contains(e.target)) {
|
|
38
|
+
// A right-click ON the open menu must not pop the NATIVE context menu
|
|
39
|
+
// on top of it — native menus swallow their own contextmenu. Suppress
|
|
40
|
+
// it and leave the kit menu open (no reposition for an inside click).
|
|
41
|
+
if (e.type === "contextmenu") e.preventDefault()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
29
44
|
// The opening right-click bubbles to document AFTER open() adds this
|
|
30
45
|
// listener — a contextmenu inside our own element must not close what
|
|
31
46
|
// it just opened (it repositions instead). A contextmenu anywhere
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class ContextMenuGroup < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-context-menu-group" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "group" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs), &)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -18,11 +18,14 @@ module PhlexKit
|
|
|
18
18
|
|
|
19
19
|
def view_template(&block)
|
|
20
20
|
classes = [ "pk-context-menu-item", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ")
|
|
21
|
-
base = { href: @href,
|
|
21
|
+
base = { href: @href, class: classes,
|
|
22
22
|
data: { action: ("click->phlex-kit--context-menu#close" unless @disabled),
|
|
23
23
|
phlex_kit__context_menu_target: "menuItem",
|
|
24
24
|
disabled: (@disabled ? "true" : nil) }.compact }
|
|
25
|
-
|
|
25
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
26
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
27
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
28
|
+
base[:"aria-disabled"] = "true" if @disabled && !aria_key_set?(:disabled)
|
|
26
29
|
a(**mix(base, @attrs)) do
|
|
27
30
|
if @checked
|
|
28
31
|
span(class: "pk-context-menu-check") do
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class ContextMenuRadioGroup < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-context-menu-group" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "radiogroup" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs), &)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -11,13 +11,15 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
|
-
|
|
14
|
+
base = {
|
|
15
15
|
class: "pk-context-menu-item pk-context-menu-radio-item",
|
|
16
|
-
role: "menuitemradio",
|
|
17
|
-
tabindex: "-1",
|
|
18
|
-
aria: { checked: @checked ? "true" : "false" },
|
|
19
16
|
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
20
|
-
}
|
|
17
|
+
}
|
|
18
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
19
|
+
base[:role] = "menuitemradio" unless attr_set?(:role)
|
|
20
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
21
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
22
|
+
label(**mix(base, @attrs)) do
|
|
21
23
|
input(type: :radio, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
22
24
|
tabindex: "-1", data: { action: "change->phlex-kit--context-menu#syncChecked" })
|
|
23
25
|
span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
|
|
@@ -2,7 +2,12 @@ module PhlexKit
|
|
|
2
2
|
class ContextMenuSeparator < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template
|
|
5
|
-
|
|
5
|
+
base = { class: "pk-context-menu-separator" }
|
|
6
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses,
|
|
7
|
+
# and aria_key_set? covers the aria: hash spelling the flat key missed.
|
|
8
|
+
base[:role] = "separator" unless attr_set?(:role)
|
|
9
|
+
base[:"aria-orientation"] = "horizontal" unless aria_key_set?(:orientation)
|
|
10
|
+
div(**mix(base, @attrs))
|
|
6
11
|
end
|
|
7
12
|
end
|
|
8
13
|
end
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class ContextMenuSubContent < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-context-menu-sub-content" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs)) do
|
|
8
11
|
div(class: "pk-context-menu-sub-viewport", &)
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -3,17 +3,20 @@ module PhlexKit
|
|
|
3
3
|
class ContextMenuSubTrigger < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&block)
|
|
6
|
-
|
|
6
|
+
base = {
|
|
7
7
|
class: "pk-context-menu-item pk-context-menu-sub-trigger",
|
|
8
|
-
role: "menuitem",
|
|
9
|
-
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
10
|
-
# tabindex 0 made it a stray tab stop inside the menu.
|
|
11
|
-
tabindex: "-1",
|
|
12
|
-
# expanded starts false and is mirrored live by the controller's
|
|
13
|
-
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
14
|
-
aria: { haspopup: "menu", expanded: "false" },
|
|
15
8
|
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
16
|
-
}
|
|
9
|
+
}
|
|
10
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
11
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
12
|
+
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
13
|
+
# tabindex 0 made it a stray tab stop inside the menu.
|
|
14
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
15
|
+
base[:"aria-haspopup"] = "menu" unless aria_key_set?(:haspopup)
|
|
16
|
+
# expanded starts false and is mirrored live by the controller's
|
|
17
|
+
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
18
|
+
base[:"aria-expanded"] = "false" unless aria_key_set?(:expanded)
|
|
19
|
+
div(**mix(base, @attrs)) do
|
|
17
20
|
block&.call
|
|
18
21
|
render Icon.new(:chevron_right, size: nil, class: "pk-context-menu-sub-chevron")
|
|
19
22
|
end
|
|
@@ -98,3 +98,7 @@
|
|
|
98
98
|
opacity: .5;
|
|
99
99
|
pointer-events: none;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/* RTL: the collapsed expand-toggle chevron points along the reading
|
|
103
|
+
direction; the expanded state's rotate(90deg) points down either way. */
|
|
104
|
+
[dir="rtl"] .pk-data-table-expand-toggle:not([aria-expanded="true"]) svg { transform: scaleX(-1); }
|
|
@@ -39,7 +39,13 @@ export default class extends Controller {
|
|
|
39
39
|
this.element.requestSubmit();
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
captureBeforeRender() {
|
|
42
|
+
captureBeforeRender(event) {
|
|
43
|
+
// Only capture for a render of the frame THIS form drives — the listener
|
|
44
|
+
// is document-wide, so an unrelated frame rendering elsewhere while the
|
|
45
|
+
// search input is focused would otherwise write a PENDING_FOCUS entry no
|
|
46
|
+
// matching swap ever consumes, stealing focus on a later restore.
|
|
47
|
+
const frame = event?.target;
|
|
48
|
+
if (!frame || !this.rendersInto(frame)) return;
|
|
43
49
|
const input = this.input();
|
|
44
50
|
if (!input || document.activeElement !== input) return;
|
|
45
51
|
PENDING_FOCUS.set(this.key(), {
|
|
@@ -48,6 +54,14 @@ export default class extends Controller {
|
|
|
48
54
|
});
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
// True when `frame` is the <turbo-frame> this form's submission renders: the
|
|
58
|
+
// frame named by the form's data-turbo-frame, else the frame the form lives
|
|
59
|
+
// inside (Turbo's default target).
|
|
60
|
+
rendersInto(frame) {
|
|
61
|
+
const targeted = this.element.dataset.turboFrame;
|
|
62
|
+
return targeted ? frame.id === targeted : frame.contains(this.element);
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
restoreIfPending() {
|
|
52
66
|
const state = PENDING_FOCUS.get(this.key());
|
|
53
67
|
if (!state) return;
|
|
@@ -2,18 +2,22 @@ module PhlexKit
|
|
|
2
2
|
# Header checkbox selecting every DataTableRowCheckbox on the page (the
|
|
3
3
|
# controller sets indeterminate for partial selections). See data_table.rb.
|
|
4
4
|
class DataTableSelectAllCheckbox < BaseComponent
|
|
5
|
-
def initialize(**attrs)
|
|
5
|
+
def initialize(label: "Select all", **attrs)
|
|
6
|
+
@label = label
|
|
6
7
|
@attrs = attrs
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def view_template
|
|
10
|
-
|
|
11
|
-
aria: { label: "Select all" },
|
|
11
|
+
base = {
|
|
12
12
|
data: {
|
|
13
13
|
phlex_kit__data_table_target: "selectAll",
|
|
14
14
|
action: "change->phlex-kit--data-table#toggleAll"
|
|
15
15
|
}
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
|
+
# label: kwarg (mirrors DataTableRowCheckbox); default only when the
|
|
18
|
+
# caller didn't supply their own accessible name — `mix` fuses.
|
|
19
|
+
base[:aria] = { label: @label } unless aria_labelled?
|
|
20
|
+
render Checkbox.new(**mix(base, @attrs))
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
background: var(--pk-surface);
|
|
21
21
|
color: var(--pk-text);
|
|
22
22
|
font-size: .875rem;
|
|
23
|
+
/* Content taller than the panel's max-height must scroll, not squash/spill
|
|
24
|
+
(sheet.css does the same on .pk-sheet-content); overflow also keeps the
|
|
25
|
+
rounded corners clipping. */
|
|
26
|
+
overflow: auto;
|
|
23
27
|
}
|
|
24
28
|
/* bottom (default) */
|
|
25
29
|
.pk-drawer.bottom {
|
|
@@ -12,13 +12,15 @@ module PhlexKit
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def view_template(&block)
|
|
15
|
-
|
|
15
|
+
base = {
|
|
16
16
|
class: "pk-dropdown-menu-item pk-dropdown-menu-checkbox-item",
|
|
17
|
-
role: "menuitemcheckbox",
|
|
18
|
-
tabindex: "-1",
|
|
19
|
-
aria: { checked: @checked ? "true" : "false" },
|
|
20
17
|
data: { phlex_kit__dropdown_menu_target: "menuItem" }
|
|
21
|
-
}
|
|
18
|
+
}
|
|
19
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
20
|
+
base[:role] = "menuitemcheckbox" unless attr_set?(:role)
|
|
21
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
22
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
23
|
+
label(**mix(base, @attrs)) do
|
|
22
24
|
input(type: :checkbox, class: "pk-dropdown-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
23
25
|
tabindex: "-1", data: { action: "change->phlex-kit--dropdown-menu#syncChecked" })
|
|
24
26
|
span(class: "pk-dropdown-menu-item-indicator", aria: { hidden: "true" }) { check_icon }
|
|
@@ -14,9 +14,12 @@ module PhlexKit
|
|
|
14
14
|
|
|
15
15
|
def view_template(&block)
|
|
16
16
|
div(class: wrapper_classes, popover: "manual", data: { phlex_kit__dropdown_menu_target: "content" }) do
|
|
17
|
+
base = { class: "pk-dropdown-menu-viewport" }
|
|
17
18
|
# role="menu" on the items' direct parent — the rows render
|
|
18
19
|
# role="menuitem", which is invalid ARIA without a menu ancestor.
|
|
19
|
-
|
|
20
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
21
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
22
|
+
div(**mix(base, @attrs), &block)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -17,7 +17,16 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
17
17
|
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
18
18
|
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
19
19
|
// one — rare enough to accept.
|
|
20
|
-
const swallowClick = (ev) =>
|
|
20
|
+
const swallowClick = (ev) => {
|
|
21
|
+
// preventDefault alone suppresses only native default actions; the
|
|
22
|
+
// dismissing outside click would still fire addEventListener handlers on
|
|
23
|
+
// whatever sits under the pointer. stopPropagation (this listener is
|
|
24
|
+
// capture-phase on window, the outermost node, so it runs before any deeper
|
|
25
|
+
// handler) starves those too — the modal-menu contract is that an outside
|
|
26
|
+
// click ONLY dismisses.
|
|
27
|
+
ev.preventDefault();
|
|
28
|
+
ev.stopPropagation();
|
|
29
|
+
};
|
|
21
30
|
function armSwallowClick() {
|
|
22
31
|
window.addEventListener("click", swallowClick, { once: true, capture: true });
|
|
23
32
|
}
|