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
|
@@ -5,7 +5,9 @@ module PhlexKit
|
|
|
5
5
|
class ContextMenuSub < BaseComponent
|
|
6
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
7
7
|
def view_template(&)
|
|
8
|
-
|
|
8
|
+
# syncSub mirrors the CSS-driven reveal state onto the sub trigger's
|
|
9
|
+
# aria-expanded (same wiring as DropdownMenuSub / MenubarSub).
|
|
10
|
+
div(**mix({ class: "pk-context-menu-sub", data: { action: "mouseenter->phlex-kit--context-menu#syncSub mouseleave->phlex-kit--context-menu#syncSub focusin->phlex-kit--context-menu#syncSub focusout->phlex-kit--context-menu#syncSub" } }, @attrs), &)
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -9,7 +9,9 @@ module PhlexKit
|
|
|
9
9
|
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
10
10
|
# tabindex 0 made it a stray tab stop inside the menu.
|
|
11
11
|
tabindex: "-1",
|
|
12
|
-
|
|
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" },
|
|
13
15
|
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
14
16
|
}, @attrs)) do
|
|
15
17
|
block&.call
|
|
@@ -23,16 +23,6 @@
|
|
|
23
23
|
/* "Columns" dropdown rows. */
|
|
24
24
|
.pk-data-table-column-toggle { position: relative; }
|
|
25
25
|
.pk-data-table-column-toggle-icon { width: 1rem; height: 1rem; margin-inline-start: .25rem; }
|
|
26
|
-
.pk-data-table-column-option {
|
|
27
|
-
display: flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
gap: .5rem;
|
|
30
|
-
border-radius: calc(var(--pk-radius) - 4px);
|
|
31
|
-
padding: .375rem .5rem;
|
|
32
|
-
font-size: .875rem;
|
|
33
|
-
cursor: pointer;
|
|
34
|
-
}
|
|
35
|
-
.pk-data-table-column-option:hover { background: var(--pk-accent); }
|
|
36
26
|
|
|
37
27
|
/* Sortable header link. */
|
|
38
28
|
.pk-data-table-sort-head { color: var(--pk-text); white-space: nowrap; }
|
|
@@ -102,7 +92,7 @@
|
|
|
102
92
|
.pk-data-table-page-disabled {
|
|
103
93
|
display: inline-flex;
|
|
104
94
|
align-items: center;
|
|
105
|
-
height:
|
|
95
|
+
height: 2rem;
|
|
106
96
|
padding: 0 .75rem;
|
|
107
97
|
font-size: .875rem;
|
|
108
98
|
opacity: .5;
|
|
@@ -27,7 +27,10 @@ module PhlexKit
|
|
|
27
27
|
# invalid ARIA. The change event bubbles from the hidden checkbox
|
|
28
28
|
# to this row, where our toggle action picks it up.
|
|
29
29
|
render DropdownMenuCheckboxItem.new(
|
|
30
|
-
|
|
30
|
+
# visible: false starts the menu row unchecked for a column
|
|
31
|
+
# the host server-renders hidden (pk-hidden on its cells) —
|
|
32
|
+
# checked: true hardcoded meant the two could never agree.
|
|
33
|
+
checked: col.fetch(:visible, true),
|
|
31
34
|
data: {
|
|
32
35
|
column_key: col[:key].to_s,
|
|
33
36
|
action: "change->phlex-kit--data-table-column-visibility#toggle"
|
|
@@ -20,7 +20,11 @@ export default class extends Controller {
|
|
|
20
20
|
toggleAll(event) {
|
|
21
21
|
const checked = event.target.checked;
|
|
22
22
|
this.rowCheckboxTargets.forEach((cb) => {
|
|
23
|
+
if (cb.checked === checked) return;
|
|
23
24
|
cb.checked = checked;
|
|
25
|
+
// Programmatic .checked flips fire no event — hosts (and the
|
|
26
|
+
// form-field wiring on PhlexKit::Checkbox) must still hear select-all.
|
|
27
|
+
cb.dispatchEvent(new Event("change", { bubbles: true }));
|
|
24
28
|
});
|
|
25
29
|
this.updateState();
|
|
26
30
|
}
|
|
@@ -70,7 +70,10 @@ module PhlexKit
|
|
|
70
70
|
def edge_item(disabled:, href:, label:, name:)
|
|
71
71
|
if disabled
|
|
72
72
|
li do
|
|
73
|
-
|
|
73
|
+
# role-less span + aria-label is invalid ARIA (aria-label on a
|
|
74
|
+
# role-less element is not perceivable) — role + aria-disabled
|
|
75
|
+
# mirrors BreadcrumbPage's disabled-control pattern.
|
|
76
|
+
span(class: "pk-data-table-page-disabled", role: "link", aria: { disabled: "true", label: name }) { label }
|
|
74
77
|
end
|
|
75
78
|
else
|
|
76
79
|
render PaginationItem.new(href: href, aria: { label: name }) { label }
|
|
@@ -4,21 +4,26 @@ module PhlexKit
|
|
|
4
4
|
# (search/sort/direction/...) survive via `preserved_params:` hidden inputs,
|
|
5
5
|
# mirroring DataTableSearch. See data_table.rb.
|
|
6
6
|
class DataTablePerPageSelect < BaseComponent
|
|
7
|
-
|
|
7
|
+
# method:/action: are kit-owned (a GET form targeting `path:`) — named
|
|
8
|
+
# kwargs so a caller override lands in @method/@action directly instead
|
|
9
|
+
# of fusing with the generated defaults via `mix` ("get post").
|
|
10
|
+
def initialize(path:, name: "per_page", value: nil, frame_id: nil, options: [ 5, 10, 25, 50 ], preserved_params: {}, method: "get", action: nil, **attrs)
|
|
8
11
|
@path = path
|
|
9
12
|
@name = name
|
|
10
13
|
@value = value
|
|
11
14
|
@frame_id = frame_id
|
|
12
15
|
@options = options
|
|
13
16
|
@preserved_params = preserved_params
|
|
17
|
+
@method = method
|
|
18
|
+
@action = action || @path
|
|
14
19
|
@attrs = attrs
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def view_template
|
|
18
23
|
form_attrs = {
|
|
19
24
|
class: "pk-data-table-per-page",
|
|
20
|
-
action: @
|
|
21
|
-
method:
|
|
25
|
+
action: @action,
|
|
26
|
+
method: @method,
|
|
22
27
|
# Reuse the search controller for its CSP-safe #submitNow action.
|
|
23
28
|
data: { controller: "phlex-kit--data-table-search" }
|
|
24
29
|
}
|
|
@@ -3,7 +3,10 @@ module PhlexKit
|
|
|
3
3
|
# params survive via `preserved_params:` hidden inputs; focus/caret survive the
|
|
4
4
|
# frame swap (see the search controller). See data_table.rb.
|
|
5
5
|
class DataTableSearch < BaseComponent
|
|
6
|
-
|
|
6
|
+
# method:/action: are kit-owned (a GET form targeting `path:`) — named
|
|
7
|
+
# kwargs so a caller override lands in @method/@action directly instead
|
|
8
|
+
# of fusing with the generated defaults via `mix` ("get post").
|
|
9
|
+
def initialize(path:, name: "search", value: nil, frame_id: nil, placeholder: "Search...", debounce: 300, preserved_params: {}, method: "get", action: nil, **attrs)
|
|
7
10
|
@path = path
|
|
8
11
|
@name = name
|
|
9
12
|
@value = value
|
|
@@ -11,11 +14,13 @@ module PhlexKit
|
|
|
11
14
|
@placeholder = placeholder
|
|
12
15
|
@debounce = debounce
|
|
13
16
|
@preserved_params = preserved_params
|
|
17
|
+
@method = method
|
|
18
|
+
@action = action || @path
|
|
14
19
|
@attrs = attrs
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def view_template
|
|
18
|
-
form(**mix({ class: "pk-data-table-search", method:
|
|
23
|
+
form(**mix({ class: "pk-data-table-search", method: @method, action: @action, data: form_data }, @attrs)) do
|
|
19
24
|
render Input.new(
|
|
20
25
|
type: :search,
|
|
21
26
|
name: @name,
|
|
@@ -5,7 +5,12 @@ module PhlexKit
|
|
|
5
5
|
# controller and the calendar's outlet points at its id, so picking a day
|
|
6
6
|
# writes the formatted date into the field. Upstream's popover_options
|
|
7
7
|
# (hover trigger via @floating-ui) are gone — the kit popover is click-only,
|
|
8
|
-
# CSS-positioned.
|
|
8
|
+
# CSS-positioned. The phlex-kit--date-picker controller on the root div
|
|
9
|
+
# listens for the calendar's change event and closes the popover once a
|
|
10
|
+
# selection is COMPLETE (single: any pick; range: the second end only;
|
|
11
|
+
# multiple: never), returning focus to the input — see
|
|
12
|
+
# date_picker_controller.js and popover_controller.js#close.
|
|
13
|
+
# Tailwind → vanilla `.pk-date-picker*` (date_picker.css).
|
|
9
14
|
class DatePicker < BaseComponent
|
|
10
15
|
def initialize(
|
|
11
16
|
id: nil,
|
|
@@ -31,6 +36,12 @@ module PhlexKit
|
|
|
31
36
|
# until the first interaction.
|
|
32
37
|
@value = value || format_selected_date
|
|
33
38
|
@placeholder = placeholder
|
|
39
|
+
# An id smuggled through input_attrs would MERGE with the generated one
|
|
40
|
+
# (mix fuses duplicate string attrs), breaking label[for] and the
|
|
41
|
+
# calendar outlet selector — the top-level id: kwarg is the one path.
|
|
42
|
+
if input_attrs.key?(:id) || input_attrs.key?("id")
|
|
43
|
+
raise ArgumentError, "DatePicker: pass id: as the top-level kwarg, not input_attrs[:id]"
|
|
44
|
+
end
|
|
34
45
|
@input_attrs = input_attrs
|
|
35
46
|
@calendar_attrs = calendar_attrs
|
|
36
47
|
@trigger_attrs = trigger_attrs
|
|
@@ -39,7 +50,13 @@ module PhlexKit
|
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
def view_template
|
|
42
|
-
div(**mix({
|
|
53
|
+
div(**mix({
|
|
54
|
+
class: "pk-date-picker",
|
|
55
|
+
data: {
|
|
56
|
+
controller: "phlex-kit--date-picker",
|
|
57
|
+
action: "phlex-kit--calendar:change->phlex-kit--date-picker#onCalendarChange"
|
|
58
|
+
}
|
|
59
|
+
}, @attrs)) do
|
|
43
60
|
render Popover.new do
|
|
44
61
|
render PopoverTrigger.new(**trigger_attrs) do
|
|
45
62
|
div(class: "pk-date-picker-field") do
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Bridges the nested Calendar's change event to the wrapping Popover's
|
|
4
|
+
// close(), so picking a date collapses the panel instead of leaving the
|
|
5
|
+
// user to click outside/Escape after every pick (shadcn's DatePicker
|
|
6
|
+
// behavior). Only closes on a COMPLETE selection: single mode closes on
|
|
7
|
+
// any pick, range mode only once the second end (rangeEnd) is set — closing
|
|
8
|
+
// on the first end would strand the user before they can pick the second —
|
|
9
|
+
// and multiple mode never auto-closes since picking more than one date is
|
|
10
|
+
// the entire point. See date_picker.rb for the data-action wiring and
|
|
11
|
+
// popover_controller.js for close()/focusTrigger().
|
|
12
|
+
export default class extends Controller {
|
|
13
|
+
onCalendarChange(e) {
|
|
14
|
+
const { mode, rangeEnd } = e.detail
|
|
15
|
+
if (mode === "multiple") return
|
|
16
|
+
// calendar_controller.js clears rangeEnd via `this.rangeEndValue = null`,
|
|
17
|
+
// which goes through Element#setAttribute — the DOM stringifies a null
|
|
18
|
+
// argument to the literal "null", so a genuinely-unset rangeEnd reads
|
|
19
|
+
// back as that string, not JS null/undefined/"".
|
|
20
|
+
if (mode === "range" && (!rangeEnd || rangeEnd === "null")) return
|
|
21
|
+
this.popoverController()?.close()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
popoverController() {
|
|
25
|
+
const popoverEl = this.element.querySelector(".pk-popover")
|
|
26
|
+
return popoverEl && this.application.getControllerForElementAndIdentifier(popoverEl, "phlex-kit--popover")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
--pk-dialog-spacing: 1rem;
|
|
12
12
|
position: fixed; z-index: 50; width: 100%;
|
|
13
13
|
max-width: min(24rem, calc(100% - 2rem));
|
|
14
|
-
|
|
14
|
+
/* Breathing room like the horizontal inset — 100vh touched the screen
|
|
15
|
+
edges and overflowed behind mobile browser chrome (dvh tracks it). */
|
|
16
|
+
max-height: calc(100dvh - 2rem);
|
|
15
17
|
overflow-y: auto; gap: 1rem;
|
|
16
18
|
border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
|
|
17
19
|
border-radius: calc(var(--pk-radius) + 2px); background: var(--pk-surface); color: var(--pk-text);
|
|
@@ -21,7 +21,7 @@ module PhlexKit
|
|
|
21
21
|
aria = {}
|
|
22
22
|
aria[:labelledby] = @labelledby if @labelledby
|
|
23
23
|
aria[:describedby] = @describedby if @describedby
|
|
24
|
-
dialog(**mix({ class: cls, aria: aria, data: { phlex_kit__dialog_target: "dialog", action: "click->phlex-kit--dialog#backdropClick" } }, @attrs)) do
|
|
24
|
+
dialog(**mix({ class: cls, aria: aria, data: { phlex_kit__dialog_target: "dialog", action: "pointerdown->phlex-kit--dialog#backdropPointerdown click->phlex-kit--dialog#backdropClick" } }, @attrs)) do
|
|
25
25
|
yield
|
|
26
26
|
if @show_close_button
|
|
27
27
|
button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--dialog#dismiss" }) do
|
|
@@ -37,6 +37,9 @@ export default class extends Controller {
|
|
|
37
37
|
}
|
|
38
38
|
open(e) {
|
|
39
39
|
e?.preventDefault()
|
|
40
|
+
// open: is one-shot — the reflected value sits in the Turbo snapshot, so
|
|
41
|
+
// a cache-restored reconnect would re-open a dismissed dialog.
|
|
42
|
+
this.openValue = false
|
|
40
43
|
this.dialogTarget.showModal()
|
|
41
44
|
// Save/restore rather than removeProperty: a host page (or another
|
|
42
45
|
// component) may have set body overflow itself.
|
|
@@ -44,15 +47,26 @@ export default class extends Controller {
|
|
|
44
47
|
document.body.style.overflow = "hidden"
|
|
45
48
|
}
|
|
46
49
|
dismiss() { this.dialogTarget.close() }
|
|
50
|
+
// Only a press that STARTED on the backdrop may dismiss: a drag-select
|
|
51
|
+
// beginning inside the panel and released over the backdrop fires the
|
|
52
|
+
// click on <dialog> with outside coordinates — that must not close it
|
|
53
|
+
// (Radix guards on pointerdown origin the same way).
|
|
54
|
+
backdropPointerdown(e) {
|
|
55
|
+
this.pressStartedOutside = e.target === this.dialogTarget && !this.#insideBox(e)
|
|
56
|
+
}
|
|
47
57
|
// Only a click on the ::backdrop dismisses. e.target is the <dialog> for
|
|
48
58
|
// backdrop clicks, but ALSO for clicks on the dialog's own padding/flex
|
|
49
59
|
// gaps — disambiguate by testing the click point against the dialog's box.
|
|
50
60
|
backdropClick(e) {
|
|
51
61
|
if (e.target !== this.dialogTarget) return
|
|
52
62
|
if (e.detail === 0) return // synthetic/keyboard click — no coordinates
|
|
63
|
+
if (!this.pressStartedOutside) return
|
|
64
|
+
this.pressStartedOutside = false
|
|
65
|
+
if (!this.#insideBox(e)) this.dismiss()
|
|
66
|
+
}
|
|
67
|
+
#insideBox(e) {
|
|
53
68
|
const r = this.dialogTarget.getBoundingClientRect()
|
|
54
|
-
|
|
55
|
-
if (!inside) this.dismiss()
|
|
69
|
+
return e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom
|
|
56
70
|
}
|
|
57
71
|
handleClose = () => { document.body.style.overflow = this.previousOverflow ?? "" }
|
|
58
72
|
}
|
|
@@ -12,7 +12,7 @@ module PhlexKit
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def view_template(&)
|
|
15
|
-
div(**mix({ data: { controller: "phlex-kit--sheet", phlex_kit__sheet_open_value: @open } }, @attrs), &)
|
|
15
|
+
div(**mix({ data: { controller: "phlex-kit--sheet", phlex_kit__sheet_open_value: @open.to_s } }, @attrs), &)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -12,9 +12,16 @@ module PhlexKit
|
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
14
|
template(data: { phlex_kit__sheet_target: "content" }) do
|
|
15
|
-
div(data: { controller: "phlex-kit--sheet-content", action: "keydown->phlex-kit--sheet-content#keydown" }) do
|
|
16
|
-
div(class: "pk-drawer-overlay", data: { action: "click->phlex-kit--sheet-content#close" })
|
|
17
|
-
|
|
15
|
+
div(data: { controller: "phlex-kit--sheet-content", action: "keydown->phlex-kit--sheet-content#keydown", pk_overlay_clone: "" }) do
|
|
16
|
+
div(class: "pk-drawer-overlay", data: { action: "click->phlex-kit--sheet-content#close mousedown->phlex-kit--sheet-content#overlayMousedown" })
|
|
17
|
+
panel_attrs = { class: "pk-drawer #{fetch_option(SIDES, @side, :side)}", 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
|
|
18
25
|
div(class: "pk-drawer-handle", aria: { hidden: "true" })
|
|
19
26
|
yield if block
|
|
20
27
|
end
|
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
position-try-fallbacks: flip-block;
|
|
21
21
|
}
|
|
22
22
|
.pk-dropdown-menu-content-up { position-area: block-start span-inline-end; margin: 0 0 4px; }
|
|
23
|
+
/* Gap margins are logical to match the logical position-area — physical
|
|
24
|
+
margins landed on the wrong side in RTL (and flip-inline flips logical
|
|
25
|
+
margins along with the placement). */
|
|
23
26
|
.pk-dropdown-menu-content-right {
|
|
24
27
|
position-area: inline-end span-block-end;
|
|
25
|
-
margin: 0
|
|
28
|
+
margin: 0; margin-inline-start: 4px;
|
|
26
29
|
position-try-fallbacks: flip-inline;
|
|
27
30
|
}
|
|
28
31
|
.pk-dropdown-menu-content-left {
|
|
29
32
|
position-area: inline-start span-block-end;
|
|
30
|
-
margin: 0 4px
|
|
33
|
+
margin: 0; margin-inline-end: 4px;
|
|
31
34
|
position-try-fallbacks: flip-inline;
|
|
32
35
|
}
|
|
33
36
|
.pk-dropdown-menu-viewport {
|
|
@@ -113,6 +116,10 @@
|
|
|
113
116
|
the viewport right edge. */
|
|
114
117
|
.pk-dropdown-menu-sub { anchor-name: --pk-dropdown-menu-sub; anchor-scope: --pk-dropdown-menu-sub; }
|
|
115
118
|
.pk-dropdown-menu-sub-chevron { margin-inline-start: auto; width: 1rem; height: 1rem; }
|
|
119
|
+
/* The chevron points toward where the submenu opens (inline-end). In RTL that
|
|
120
|
+
is visually LEFT — mirror it. Ancestor [dir="rtl"] (not :dir()) so Chrome
|
|
121
|
+
re-styles it on a dynamic dir flip too. */
|
|
122
|
+
[dir="rtl"] .pk-dropdown-menu-sub-chevron { transform: scaleX(-1); }
|
|
116
123
|
.pk-dropdown-menu-sub-content {
|
|
117
124
|
position: fixed;
|
|
118
125
|
position-anchor: --pk-dropdown-menu-sub;
|
|
@@ -18,7 +18,9 @@ module PhlexKit
|
|
|
18
18
|
data: {
|
|
19
19
|
controller: "phlex-kit--dropdown-menu",
|
|
20
20
|
phlex_kit__dropdown_menu_open_value: @open,
|
|
21
|
-
|
|
21
|
+
# focusout: tabbing (or otherwise moving real focus) out of the
|
|
22
|
+
# menu closes the open [popover=manual] panel (menubar's pattern).
|
|
23
|
+
action: "click@window->phlex-kit--dropdown-menu#onClickOutside focusout->phlex-kit--dropdown-menu#onFocusout"
|
|
22
24
|
}
|
|
23
25
|
}, @attrs), &block)
|
|
24
26
|
end
|
|
@@ -16,20 +16,59 @@ export default class extends Controller {
|
|
|
16
16
|
|
|
17
17
|
connect() {
|
|
18
18
|
this.boundHandleKeydown = this.#handleKeydown.bind(this);
|
|
19
|
+
// Outstanding syncSub rAF handles — cancelled on disconnect so a queued
|
|
20
|
+
// frame never touches a torn-down menu (menubar's pattern).
|
|
21
|
+
this.subFrames = new Set();
|
|
19
22
|
// The visible control is the caller's button/link inside the trigger
|
|
20
23
|
// wrapper — that's what AT reads, so the popup wiring belongs on it.
|
|
21
24
|
this.invoker = this.triggerTarget.querySelector("button, a, [tabindex]") || this.triggerTarget;
|
|
25
|
+
// A Turbo snapshot (or a detach/reattach that mimics one) serializes
|
|
26
|
+
// aria-expanded="true" on sub triggers even though no submenu can be
|
|
27
|
+
// open at connect time — sweep every stale marker back to false
|
|
28
|
+
// (menubar's pattern). Runs before the invoker's own reset below so
|
|
29
|
+
// both are covered by one mental model.
|
|
30
|
+
this.element.querySelectorAll("[aria-expanded='true']").forEach((el) => el.setAttribute("aria-expanded", "false"));
|
|
31
|
+
// Same shape for checkbox/radio rows: their `checked` DOM property
|
|
32
|
+
// survives a reconnect but a hand-edited/replayed aria-checked can drift
|
|
33
|
+
// from it — resync every row from its live input.
|
|
34
|
+
this.element.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach((i) => {
|
|
35
|
+
i.closest('[role^="menuitem"]')?.setAttribute("aria-checked", i.checked);
|
|
36
|
+
});
|
|
22
37
|
this.invoker.setAttribute("aria-haspopup", "menu");
|
|
23
38
|
this.invoker.setAttribute("aria-expanded", "false");
|
|
39
|
+
// Associate trigger and menu for AT (the dialog controller wires its
|
|
40
|
+
// ids the same way).
|
|
41
|
+
if (!this.contentTarget.id) this.contentTarget.id = `pk-dropdown-menu-${Math.random().toString(36).slice(2, 8)}`;
|
|
42
|
+
this.invoker.setAttribute("aria-controls", this.contentTarget.id);
|
|
43
|
+
// Turbo snapshots BEFORE disconnect — an open menu (reflected in the
|
|
44
|
+
// open value) would be resurrected from the page cache on restore.
|
|
45
|
+
this.boundBeforeCache = () => this.close();
|
|
46
|
+
document.addEventListener("turbo:before-cache", this.boundBeforeCache);
|
|
24
47
|
// `open:` on the Ruby side renders the value — start open (same
|
|
25
48
|
// kwarg -> value -> connect flow as Collapsible).
|
|
26
49
|
if (this.openValue) this.#open();
|
|
27
50
|
}
|
|
28
51
|
|
|
29
52
|
disconnect() {
|
|
53
|
+
document.removeEventListener("turbo:before-cache", this.boundBeforeCache);
|
|
54
|
+
this.subFrames.forEach((id) => cancelAnimationFrame(id));
|
|
55
|
+
this.subFrames.clear();
|
|
30
56
|
this.#removeEventListeners();
|
|
31
57
|
}
|
|
32
58
|
|
|
59
|
+
// Mirrors the CSS-driven submenu state (:hover / :focus-within) onto the
|
|
60
|
+
// sub trigger's aria-expanded. rAF: on mouseleave/focusout the pseudo-class
|
|
61
|
+
// state isn't settled until the event finishes dispatching.
|
|
62
|
+
syncSub(e) {
|
|
63
|
+
const sub = e.currentTarget;
|
|
64
|
+
const id = requestAnimationFrame(() => {
|
|
65
|
+
this.subFrames.delete(id);
|
|
66
|
+
sub.querySelector(":scope > [aria-haspopup]")
|
|
67
|
+
?.setAttribute("aria-expanded", sub.matches(":hover, :focus-within"));
|
|
68
|
+
});
|
|
69
|
+
this.subFrames.add(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
33
72
|
onClickOutside(event) {
|
|
34
73
|
// Gate on the live :popover-open state, not the stored flag — a stale
|
|
35
74
|
// flag is how a close on an already-closed panel becomes an open.
|
|
@@ -47,6 +86,18 @@ export default class extends Controller {
|
|
|
47
86
|
this.contentTarget.matches(":popover-open") ? this.close() : this.#open();
|
|
48
87
|
}
|
|
49
88
|
|
|
89
|
+
// Tabbing (or otherwise moving real focus) out of the menu closes it —
|
|
90
|
+
// menubar's onFocusout pattern (menubar_controller.js). relatedTarget is
|
|
91
|
+
// null when the window itself blurs — leave the menu alone then. The
|
|
92
|
+
// trigger + content are both DOM children of this.element even though the
|
|
93
|
+
// content is a top-layer popover, so one containment check covers both.
|
|
94
|
+
onFocusout(e) {
|
|
95
|
+
if (!this.contentTarget.matches(":popover-open")) return;
|
|
96
|
+
const to = e.relatedTarget;
|
|
97
|
+
if (!to || this.element.contains(to)) return;
|
|
98
|
+
this.close();
|
|
99
|
+
}
|
|
100
|
+
|
|
50
101
|
#open() {
|
|
51
102
|
this.openValue = true;
|
|
52
103
|
this.#addEventListeners();
|
|
@@ -75,6 +126,16 @@ export default class extends Controller {
|
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
#handleKeydown(e) {
|
|
129
|
+
// This listener is document-level (see #addEventListeners) so it keeps
|
|
130
|
+
// receiving events even once real focus has left the menu (e.g. a click
|
|
131
|
+
// into an unrelated text field the onFocusout listener didn't catch) —
|
|
132
|
+
// scope the roving-nav keys to when focus is actually within the widget.
|
|
133
|
+
// this.element (not just contentTarget) so ArrowDown still works right
|
|
134
|
+
// after a mouse-driven open, when focus is still on the invoker button
|
|
135
|
+
// (#open() doesn't force focus into the first row). Escape stays global
|
|
136
|
+
// while the menu is open (APG).
|
|
137
|
+
if (e.key !== "Escape" && !this.element.contains(document.activeElement)) return;
|
|
138
|
+
|
|
78
139
|
const items = this.#items();
|
|
79
140
|
if (items.length === 0) return;
|
|
80
141
|
const index = items.indexOf(document.activeElement);
|
|
@@ -111,20 +172,27 @@ export default class extends Controller {
|
|
|
111
172
|
this.invoker.focus();
|
|
112
173
|
break;
|
|
113
174
|
case "ArrowRight":
|
|
114
|
-
// On a sub trigger, enter the submenu (focus reveals it via
|
|
115
|
-
// :focus-within, making its rows visible to the roving nav).
|
|
116
|
-
// Same keyboard grammar as context_menu.
|
|
117
|
-
if (document.activeElement?.matches(".pk-dropdown-menu-sub-trigger")) {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
this.#enterSub(document.activeElement);
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
175
|
case "ArrowLeft": {
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
// Submenus open inline-end — visually LEFT in RTL — so the enter key
|
|
177
|
+
// follows visual direction: ArrowLeft enters / ArrowRight exits in RTL
|
|
178
|
+
// (and the reverse in LTR). Same keyboard grammar as context_menu.
|
|
179
|
+
// Runtime dir check is reliable after a dynamic flip.
|
|
180
|
+
const rtl = getComputedStyle(this.element).direction === "rtl";
|
|
181
|
+
const enterKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
182
|
+
if (e.key === enterKey) {
|
|
183
|
+
// On a sub trigger, enter the submenu (focus reveals it via
|
|
184
|
+
// :focus-within, making its rows visible to the roving nav).
|
|
185
|
+
if (document.activeElement?.matches(".pk-dropdown-menu-sub-trigger")) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
this.#enterSub(document.activeElement);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Inside a submenu, step back to its trigger (closes it on focus-out).
|
|
191
|
+
const sub = document.activeElement?.closest(".pk-dropdown-menu-sub-content");
|
|
192
|
+
if (sub) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
sub.closest(".pk-dropdown-menu-sub")?.querySelector(".pk-dropdown-menu-sub-trigger")?.focus();
|
|
195
|
+
}
|
|
128
196
|
}
|
|
129
197
|
break;
|
|
130
198
|
}
|
|
@@ -5,7 +5,9 @@ module PhlexKit
|
|
|
5
5
|
class DropdownMenuSub < BaseComponent
|
|
6
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
7
7
|
def view_template(&)
|
|
8
|
-
|
|
8
|
+
# syncSub mirrors the CSS-driven reveal state onto the sub trigger's
|
|
9
|
+
# aria-expanded (same wiring as MenubarSub).
|
|
10
|
+
div(**mix({ class: "pk-dropdown-menu-sub", data: { action: "mouseenter->phlex-kit--dropdown-menu#syncSub mouseleave->phlex-kit--dropdown-menu#syncSub focusin->phlex-kit--dropdown-menu#syncSub focusout->phlex-kit--dropdown-menu#syncSub" } }, @attrs), &)
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -10,7 +10,9 @@ module PhlexKit
|
|
|
10
10
|
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
11
11
|
# tabindex 0 made it a stray tab stop inside the menu.
|
|
12
12
|
tabindex: "-1",
|
|
13
|
-
|
|
13
|
+
# expanded starts false and is mirrored live by the controller's
|
|
14
|
+
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
15
|
+
aria: { haspopup: "menu", expanded: "false" },
|
|
14
16
|
data: { phlex_kit__dropdown_menu_target: "menuItem" }
|
|
15
17
|
}, @attrs)) do
|
|
16
18
|
block&.call
|
|
@@ -6,7 +6,7 @@ module PhlexKit
|
|
|
6
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
7
7
|
|
|
8
8
|
def view_template(&block)
|
|
9
|
-
div(**mix({ class: "pk-field-separator"
|
|
9
|
+
div(**mix({ class: "pk-field-separator" }, @attrs)) do
|
|
10
10
|
render Separator.new(class: "pk-field-separator-line")
|
|
11
11
|
span(class: "pk-field-separator-content", &block) if block
|
|
12
12
|
end
|
|
@@ -9,10 +9,21 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
9
9
|
// back to the browser's validationMessage.
|
|
10
10
|
// Connects to data-controller="phlex-kit--form-field"
|
|
11
11
|
export default class extends Controller {
|
|
12
|
-
static targets = ["input", "error"];
|
|
12
|
+
static targets = ["input", "error", "hint"];
|
|
13
13
|
static values = { shouldValidate: false };
|
|
14
14
|
|
|
15
15
|
connect() {
|
|
16
|
+
// Hints are static description text — wire them onto every control's
|
|
17
|
+
// aria-describedby up front (errors join/leave dynamically via
|
|
18
|
+
// #describeBy, which preserves foreign tokens like these).
|
|
19
|
+
this.hintTargets.forEach((hint) => {
|
|
20
|
+
if (!hint.id) return;
|
|
21
|
+
this.inputTargets.forEach((input) => {
|
|
22
|
+
const tokens = (input.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean);
|
|
23
|
+
if (!tokens.includes(hint.id)) tokens.push(hint.id);
|
|
24
|
+
input.setAttribute("aria-describedby", tokens.join(" "));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
16
27
|
if (this.hasErrorTarget) {
|
|
17
28
|
if (this.errorTarget.textContent.trim()) {
|
|
18
29
|
this.shouldValidateValue = true;
|
|
@@ -37,43 +48,61 @@ export default class extends Controller {
|
|
|
37
48
|
if (!this.hasErrorTarget) return;
|
|
38
49
|
error.preventDefault();
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
// preventDefault also cancels the browser's focus-first-invalid — restore
|
|
52
|
+
// it for the first invalid control of the submit pass (all invalid events
|
|
53
|
+
// fire in the same task, so the microtask reset scopes "first" correctly).
|
|
54
|
+
if (!this._focusedInvalid) {
|
|
55
|
+
this._focusedInvalid = true;
|
|
56
|
+
queueMicrotask(() => { this._focusedInvalid = false; });
|
|
57
|
+
error.target.focus({ preventScroll: true });
|
|
58
|
+
error.target.scrollIntoView({ block: "nearest" });
|
|
59
|
+
}
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
this.#setErrorMessage(
|
|
61
|
+
this.shouldValidateValue = true;
|
|
62
|
+
this.#setErrorMessage();
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
this.#setErrorMessage(
|
|
65
|
+
onInput(_event) {
|
|
66
|
+
this.#setErrorMessage();
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// the control that fired, not always the first.
|
|
55
|
-
#inputFor(event) {
|
|
56
|
-
if (event && this.inputTargets.includes(event.target)) return event.target;
|
|
57
|
-
return this.inputTarget;
|
|
69
|
+
onChange(_event) {
|
|
70
|
+
this.#setErrorMessage();
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
// A FormField can carry several input targets (radio/checkbox groups, or
|
|
74
|
+
// independent controls like two required checkboxes sharing one error).
|
|
75
|
+
// Recompute across ALL of them on every validity event — deciding from the
|
|
76
|
+
// event's own target alone left a shared error stuck on/off based on
|
|
77
|
+
// whichever control happened to fire last, out of step with the others'
|
|
78
|
+
// own (correctly-updated) aria-invalid.
|
|
79
|
+
#setErrorMessage() {
|
|
61
80
|
if (!this.shouldValidateValue || !this.hasErrorTarget) return;
|
|
62
81
|
|
|
63
|
-
const input = this.#inputFor(event);
|
|
64
82
|
// aria-invalid drives the red ring for LIVE validation too (input.css
|
|
65
|
-
// only matched server-rendered attrs);
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
// only matched server-rendered attrs); each control reflects its own
|
|
84
|
+
// validity regardless of which one fired the event. Radio buttons
|
|
85
|
+
// sharing a `name` share native validity, so they naturally agree here.
|
|
86
|
+
let firstInvalid = null;
|
|
87
|
+
this.inputTargets.forEach((input) => {
|
|
88
|
+
if (input.validity.valid) {
|
|
89
|
+
input.removeAttribute("aria-invalid");
|
|
90
|
+
} else {
|
|
91
|
+
input.setAttribute("aria-invalid", "true");
|
|
92
|
+
firstInvalid ||= input;
|
|
93
|
+
}
|
|
94
|
+
// Clear the shared error's id everywhere first — it's re-added below,
|
|
95
|
+
// only on the one control whose message is currently shown.
|
|
69
96
|
this.#describeBy(input, false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (firstInvalid) {
|
|
100
|
+
this.#describeBy(firstInvalid, true);
|
|
101
|
+
this.errorTarget.textContent = this.#getValidationMessage(firstInvalid);
|
|
102
|
+
this.errorTarget.classList.remove("pk-hidden");
|
|
103
|
+
} else {
|
|
70
104
|
this.errorTarget.textContent = "";
|
|
71
105
|
this.errorTarget.classList.add("pk-hidden");
|
|
72
|
-
} else {
|
|
73
|
-
input.setAttribute("aria-invalid", "true");
|
|
74
|
-
this.#describeBy(input, true);
|
|
75
|
-
this.errorTarget.textContent = this.#getValidationMessage(input);
|
|
76
|
-
this.errorTarget.classList.remove("pk-hidden");
|
|
77
106
|
}
|
|
78
107
|
}
|
|
79
108
|
|