phlex_kit 0.8.0 → 0.9.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/_tokens.css +11 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -2
- data/app/components/phlex_kit/accordion/accordion_content.rb +11 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +23 -4
- data/app/components/phlex_kit/alert/alert.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +11 -4
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +123 -6
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +10 -4
- data/app/components/phlex_kit/attachment/attachment.css +1 -1
- data/app/components/phlex_kit/attachment/attachment_action.rb +8 -3
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +3 -3
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +8 -0
- data/app/components/phlex_kit/avatar/avatar.css +16 -5
- data/app/components/phlex_kit/avatar/avatar_controller.js +10 -7
- data/app/components/phlex_kit/badge/badge.css +4 -4
- data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +3 -1
- data/app/components/phlex_kit/bubble/bubble.css +3 -3
- data/app/components/phlex_kit/bubble/bubble.rb +2 -0
- data/app/components/phlex_kit/bubble/bubble_content.rb +5 -1
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -0
- data/app/components/phlex_kit/button/button.css +6 -6
- data/app/components/phlex_kit/button_group/button_group.css +2 -2
- data/app/components/phlex_kit/button_group/button_group_separator.rb +3 -1
- data/app/components/phlex_kit/calendar/calendar.css +2 -2
- data/app/components/phlex_kit/calendar/calendar.rb +10 -3
- data/app/components/phlex_kit/calendar/calendar_body.rb +12 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +151 -12
- data/app/components/phlex_kit/calendar/calendar_days.rb +17 -15
- data/app/components/phlex_kit/calendar/calendar_input_controller.js +6 -0
- data/app/components/phlex_kit/calendar/calendar_next.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_prev.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_weekdays.rb +1 -1
- data/app/components/phlex_kit/carousel/carousel.rb +11 -1
- data/app/components/phlex_kit/carousel/carousel_controller.js +28 -1
- data/app/components/phlex_kit/chart/chart_controller.js +37 -10
- data/app/components/phlex_kit/checkbox/checkbox.css +2 -1
- data/app/components/phlex_kit/clipboard/clipboard.css +1 -1
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +7 -0
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +3 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +4 -1
- data/app/components/phlex_kit/collapsible/collapsible_content.rb +15 -2
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +11 -2
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +17 -2
- data/app/components/phlex_kit/combobox/combobox.css +10 -8
- data/app/components/phlex_kit/combobox/combobox_controller.js +67 -11
- data/app/components/phlex_kit/combobox/combobox_list_group.rb +13 -5
- data/app/components/phlex_kit/command/command.css +4 -4
- data/app/components/phlex_kit/command/command_controller.js +51 -10
- data/app/components/phlex_kit/command/command_dialog.rb +5 -6
- data/app/components/phlex_kit/command/command_dialog_controller.js +26 -8
- data/app/components/phlex_kit/command/command_input.rb +6 -2
- data/app/components/phlex_kit/command/command_item.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu.css +6 -6
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +69 -2
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/data_table/data_table.css +1 -1
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +11 -12
- data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +4 -1
- data/app/components/phlex_kit/data_table/data_table_pagy_adapter.rb +2 -1
- data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +19 -5
- data/app/components/phlex_kit/data_table/data_table_search_controller.js +13 -1
- data/app/components/phlex_kit/date_picker/date_picker.rb +50 -3
- data/app/components/phlex_kit/dialog/dialog.css +4 -4
- data/app/components/phlex_kit/dialog/dialog_controller.js +20 -4
- data/app/components/phlex_kit/drawer/drawer.css +3 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +6 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +5 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +99 -45
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/field/field.css +1 -1
- data/app/components/phlex_kit/field/field_error.rb +5 -6
- data/app/components/phlex_kit/form_field/form_field_controller.js +44 -12
- data/app/components/phlex_kit/form_field/form_field_error.rb +8 -1
- data/app/components/phlex_kit/hover_card/hover_card.css +1 -1
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +29 -5
- data/app/components/phlex_kit/input/input.css +1 -1
- data/app/components/phlex_kit/input_group/input_group.css +9 -7
- data/app/components/phlex_kit/input_group/input_group_addon.rb +5 -1
- data/app/components/phlex_kit/input_otp/input_otp.css +1 -1
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +43 -4
- data/app/components/phlex_kit/input_otp/input_otp_separator.rb +4 -2
- data/app/components/phlex_kit/item/item.css +1 -1
- data/app/components/phlex_kit/item/item_group.rb +4 -2
- data/app/components/phlex_kit/item/item_separator.rb +3 -1
- data/app/components/phlex_kit/marker/marker.css +1 -1
- data/app/components/phlex_kit/masked_input/masked_input.rb +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +34 -9
- data/app/components/phlex_kit/menubar/menubar.css +10 -9
- data/app/components/phlex_kit/menubar/menubar_content.rb +8 -2
- data/app/components/phlex_kit/menubar/menubar_controller.js +119 -12
- data/app/components/phlex_kit/menubar/menubar_menu.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_sub.rb +6 -1
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +7 -2
- data/app/components/phlex_kit/message_scroller/message_scroller.css +1 -1
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +46 -2
- data/app/components/phlex_kit/native_select/native_select.css +1 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +11 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +6 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -1
- data/app/components/phlex_kit/pagination/pagination.css +2 -2
- data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +3 -1
- data/app/components/phlex_kit/popover/popover.css +1 -1
- data/app/components/phlex_kit/resizable/resizable.css +3 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +78 -3
- data/app/components/phlex_kit/resizable/resizable_handle.rb +3 -1
- data/app/components/phlex_kit/resizable/resizable_panel.rb +3 -1
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +4 -2
- data/app/components/phlex_kit/select/select.css +3 -3
- data/app/components/phlex_kit/select/select.rb +11 -7
- data/app/components/phlex_kit/select/select_controller.js +35 -4
- data/app/components/phlex_kit/select/select_item.rb +5 -2
- data/app/components/phlex_kit/sheet/sheet.css +3 -3
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +36 -12
- data/app/components/phlex_kit/sidebar/sidebar.css +27 -15
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +98 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +24 -5
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +13 -4
- data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +14 -4
- data/app/components/phlex_kit/slider/slider.css +3 -3
- data/app/components/phlex_kit/slider/slider.rb +3 -1
- data/app/components/phlex_kit/switch/switch.rb +6 -1
- data/app/components/phlex_kit/table/table.css +5 -3
- data/app/components/phlex_kit/tabs/tabs.css +4 -4
- data/app/components/phlex_kit/tabs/tabs_content.rb +6 -3
- data/app/components/phlex_kit/tabs/tabs_controller.js +3 -1
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -0
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +49 -7
- data/app/components/phlex_kit/toast/toast.css +32 -14
- data/app/components/phlex_kit/toast/toast_controller.js +24 -4
- data/app/components/phlex_kit/toast/toast_region.rb +19 -5
- data/app/components/phlex_kit/toast/toaster_controller.js +20 -6
- data/app/components/phlex_kit/toggle/toggle.css +4 -4
- data/app/components/phlex_kit/toggle/toggle.rb +9 -3
- data/app/components/phlex_kit/tooltip/tooltip.css +28 -6
- data/app/components/phlex_kit/typography/heading.rb +9 -3
- data/app/components/phlex_kit/typography/typography.css +4 -3
- data/app/javascript/phlex_kit/controllers/index.js +0 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -2
- data/app/components/phlex_kit/select/select_item_controller.js +0 -14
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
/* Glyphs default to size-4; explicit width attrs (Icon size:) win. */
|
|
38
38
|
.pk-button svg { flex-shrink: 0; pointer-events: none; }
|
|
39
39
|
.pk-button svg:not([width]) { width: 1rem; height: 1rem; }
|
|
40
|
-
.pk-button:has(> [data-icon="inline-start"]) { padding-
|
|
41
|
-
.pk-button:has(> [data-icon="inline-end"]) { padding-
|
|
40
|
+
.pk-button:has(> [data-icon="inline-start"]) { padding-inline-start: .5rem; }
|
|
41
|
+
.pk-button:has(> [data-icon="inline-end"]) { padding-inline-end: .5rem; }
|
|
42
42
|
|
|
43
43
|
/* Sizes (md is the base above). */
|
|
44
44
|
.pk-button.xs {
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
border-radius: min(calc(var(--pk-radius) - 2px), 10px);
|
|
47
47
|
}
|
|
48
48
|
.pk-button.xs svg:not([width]) { width: .75rem; height: .75rem; }
|
|
49
|
-
.pk-button.xs:has(> [data-icon="inline-start"]) { padding-
|
|
50
|
-
.pk-button.xs:has(> [data-icon="inline-end"]) { padding-
|
|
49
|
+
.pk-button.xs:has(> [data-icon="inline-start"]) { padding-inline-start: .375rem; }
|
|
50
|
+
.pk-button.xs:has(> [data-icon="inline-end"]) { padding-inline-end: .375rem; }
|
|
51
51
|
.pk-button.sm {
|
|
52
52
|
height: 1.75rem; gap: .25rem; padding: .25rem .625rem; font-size: .8rem;
|
|
53
53
|
border-radius: min(calc(var(--pk-radius) - 2px), 12px);
|
|
54
54
|
}
|
|
55
55
|
.pk-button.sm svg:not([width]) { width: .875rem; height: .875rem; }
|
|
56
|
-
.pk-button.sm:has(> [data-icon="inline-start"]) { padding-
|
|
57
|
-
.pk-button.sm:has(> [data-icon="inline-end"]) { padding-
|
|
56
|
+
.pk-button.sm:has(> [data-icon="inline-start"]) { padding-inline-start: .375rem; }
|
|
57
|
+
.pk-button.sm:has(> [data-icon="inline-end"]) { padding-inline-end: .375rem; }
|
|
58
58
|
.pk-button.lg { height: 2.25rem; padding: .375rem .625rem; }
|
|
59
59
|
.pk-button.xl { height: 3rem; padding: .625rem 1.5rem; font-size: 1rem; }
|
|
60
60
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
.pk-button-group:not(.vertical) > :not(:first-child) {
|
|
15
15
|
border-top-left-radius: 0;
|
|
16
16
|
border-bottom-left-radius: 0;
|
|
17
|
-
border-
|
|
17
|
+
border-inline-start: 0;
|
|
18
18
|
}
|
|
19
19
|
.pk-button-group:not(.vertical) > :not(:last-child) {
|
|
20
20
|
border-top-right-radius: 0;
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
.pk-button-group:not(.vertical) > :not(:first-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
48
48
|
border-top-left-radius: 0;
|
|
49
49
|
border-bottom-left-radius: 0;
|
|
50
|
-
border-
|
|
50
|
+
border-inline-start: 0;
|
|
51
51
|
}
|
|
52
52
|
.pk-button-group:not(.vertical) > :not(:last-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
53
53
|
.pk-button-group:not(.vertical) > :not(:last-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
@@ -8,7 +8,9 @@ module PhlexKit
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def view_template
|
|
11
|
-
|
|
11
|
+
# Purely decorative — aria-hidden only (role="separator" would
|
|
12
|
+
# contradict aria-hidden and confuse AT).
|
|
13
|
+
div(**mix({ class: "pk-button-group-separator", aria: { hidden: true } }, @attrs))
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
opacity: .5;
|
|
54
54
|
transition: background-color .15s ease, opacity .15s ease;
|
|
55
55
|
}
|
|
56
|
-
.pk-calendar-prev {
|
|
57
|
-
.pk-calendar-next {
|
|
56
|
+
.pk-calendar-prev { inset-inline-start: .25rem; }
|
|
57
|
+
.pk-calendar-next { inset-inline-end: .25rem; }
|
|
58
58
|
.pk-calendar-prev:hover, .pk-calendar-next:hover { background: var(--pk-accent); opacity: 1; }
|
|
59
59
|
.pk-calendar-prev:focus-visible, .pk-calendar-next:focus-visible {
|
|
60
60
|
outline: none;
|
|
@@ -64,7 +64,9 @@ module PhlexKit
|
|
|
64
64
|
private
|
|
65
65
|
|
|
66
66
|
def render_dropdown_caption
|
|
67
|
-
|
|
67
|
+
view_seed = @selected_date || @range_start || @selected_dates.first
|
|
68
|
+
view_year = parse_year(view_seed) || Time.now.year
|
|
69
|
+
view_month = parse_month(view_seed) || Time.now.month # 1-12
|
|
68
70
|
first_year = @from_year || parse_year(@min_date) || (view_year - 10)
|
|
69
71
|
last_year = @to_year || parse_year(@max_date) || (view_year + 10)
|
|
70
72
|
|
|
@@ -74,14 +76,15 @@ module PhlexKit
|
|
|
74
76
|
aria: { label: "Month" },
|
|
75
77
|
data: { phlex_kit__calendar_target: "monthSelect", action: "change->phlex-kit--calendar#setMonth" }
|
|
76
78
|
) do
|
|
77
|
-
|
|
79
|
+
# Mark the view month selected so the caption is right pre-JS.
|
|
80
|
+
MONTHS.each_with_index { |name, index| option(value: index.to_s, selected: index == view_month - 1) { name } }
|
|
78
81
|
end
|
|
79
82
|
select(
|
|
80
83
|
class: "pk-native-select-field pk-calendar-dropdown",
|
|
81
84
|
aria: { label: "Year" },
|
|
82
85
|
data: { phlex_kit__calendar_target: "yearSelect", action: "change->phlex-kit--calendar#setYear" }
|
|
83
86
|
) do
|
|
84
|
-
(first_year..last_year).each { |year| option(value: year.to_s) { year.to_s } }
|
|
87
|
+
(first_year..last_year).each { |year| option(value: year.to_s, selected: year == view_year) { year.to_s } }
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
90
|
end
|
|
@@ -90,6 +93,10 @@ module PhlexKit
|
|
|
90
93
|
value.to_s[/\A(\d{4})/, 1]&.to_i
|
|
91
94
|
end
|
|
92
95
|
|
|
96
|
+
def parse_month(value)
|
|
97
|
+
value.to_s[/\A\d{4}-(\d{2})/, 1]&.to_i
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
def calendar_attrs
|
|
94
101
|
data = {
|
|
95
102
|
controller: "phlex-kit--calendar",
|
|
@@ -7,7 +7,18 @@ module PhlexKit
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def view_template
|
|
10
|
-
table(**mix({
|
|
10
|
+
table(**mix({
|
|
11
|
+
class: "pk-calendar-body",
|
|
12
|
+
# The day grid is a real ARIA grid: this table is role="grid", the
|
|
13
|
+
# controller-rendered rows are role="row", the <td>s gridcells.
|
|
14
|
+
role: "grid",
|
|
15
|
+
data: {
|
|
16
|
+
phlex_kit__calendar_target: "calendar",
|
|
17
|
+
# Delegated arrow-key/Home/End navigation for the day grid (the
|
|
18
|
+
# buttons are re-rendered wholesale, so the action lives here).
|
|
19
|
+
action: "keydown->phlex-kit--calendar#onKeydown"
|
|
20
|
+
}
|
|
21
|
+
}, @attrs))
|
|
11
22
|
end
|
|
12
23
|
end
|
|
13
24
|
end
|
|
@@ -17,6 +17,8 @@ export default class extends Controller {
|
|
|
17
17
|
"title",
|
|
18
18
|
"monthSelect",
|
|
19
19
|
"yearSelect",
|
|
20
|
+
"prevButton",
|
|
21
|
+
"nextButton",
|
|
20
22
|
"weekdaysTemplate",
|
|
21
23
|
"disabledDateTemplate",
|
|
22
24
|
"selectedDateTemplate",
|
|
@@ -42,18 +44,18 @@ export default class extends Controller {
|
|
|
42
44
|
};
|
|
43
45
|
static outlets = ["phlex-kit--calendar-input"];
|
|
44
46
|
|
|
45
|
-
initialize()
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// No initialize() render: Stimulus fires every value's `…ValueChanged`
|
|
48
|
+
// callback once during value initialization (before connect()), so
|
|
49
|
+
// viewDateValueChanged already performs the initial updateCalendar().
|
|
48
50
|
|
|
49
51
|
nextMonth(e) {
|
|
50
52
|
e.preventDefault();
|
|
51
|
-
this.viewDateValue = this.adjustMonth(1);
|
|
53
|
+
this.viewDateValue = this.clampViewIso(this.adjustMonth(1));
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
prevMonth(e) {
|
|
55
57
|
e.preventDefault();
|
|
56
|
-
this.viewDateValue = this.adjustMonth(-1);
|
|
58
|
+
this.viewDateValue = this.clampViewIso(this.adjustMonth(-1));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// month/year dropdown caption (native selects)
|
|
@@ -61,14 +63,62 @@ export default class extends Controller {
|
|
|
61
63
|
const date = this.viewDate();
|
|
62
64
|
date.setDate(2);
|
|
63
65
|
date.setMonth(Number(e.target.value));
|
|
64
|
-
this.viewDateValue = this.isoDate(date);
|
|
66
|
+
this.viewDateValue = this.clampViewIso(this.isoDate(date));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
setYear(e) {
|
|
68
70
|
const date = this.viewDate();
|
|
69
71
|
date.setDate(2);
|
|
70
72
|
date.setFullYear(Number(e.target.value));
|
|
71
|
-
this.viewDateValue = this.isoDate(date);
|
|
73
|
+
this.viewDateValue = this.clampViewIso(this.isoDate(date));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- month-navigation bounds (min/max dates + the year dropdown's range) ---
|
|
77
|
+
|
|
78
|
+
// Earliest/latest navigable months. min/max dates bound directly; a year
|
|
79
|
+
// dropdown additionally bounds the view to its option range (from_year /
|
|
80
|
+
// to_year) so navigation can't drive the select out of range (blank).
|
|
81
|
+
monthBounds() {
|
|
82
|
+
let min = this.minDate();
|
|
83
|
+
let max = this.maxDate();
|
|
84
|
+
if (this.hasYearSelectTarget) {
|
|
85
|
+
const years = [...this.yearSelectTarget.options].map((o) => Number(o.value)).filter(Number.isFinite);
|
|
86
|
+
if (years.length) {
|
|
87
|
+
const yearMin = new Date(Math.min(...years), 0, 1);
|
|
88
|
+
const yearMax = new Date(Math.max(...years), 11, 31);
|
|
89
|
+
if (!min || yearMin > min) min = yearMin;
|
|
90
|
+
if (!max || yearMax < max) max = yearMax;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { min, max };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
monthIndexOf(date) {
|
|
97
|
+
return date.getFullYear() * 12 + date.getMonth();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clampViewIso(iso) {
|
|
101
|
+
const date = this.parseDate(iso);
|
|
102
|
+
if (!date) return iso;
|
|
103
|
+
const { min, max } = this.monthBounds();
|
|
104
|
+
if (min && this.monthIndexOf(date) < this.monthIndexOf(min)) {
|
|
105
|
+
return this.isoDate(new Date(min.getFullYear(), min.getMonth(), 2));
|
|
106
|
+
}
|
|
107
|
+
if (max && this.monthIndexOf(date) > this.monthIndexOf(max)) {
|
|
108
|
+
return this.isoDate(new Date(max.getFullYear(), max.getMonth(), 2));
|
|
109
|
+
}
|
|
110
|
+
return iso;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
syncNavButtons() {
|
|
114
|
+
const { min, max } = this.monthBounds();
|
|
115
|
+
const view = this.monthIndexOf(this.viewDate());
|
|
116
|
+
if (this.hasPrevButtonTarget) {
|
|
117
|
+
this.prevButtonTarget.disabled = Boolean(min && view <= this.monthIndexOf(min));
|
|
118
|
+
}
|
|
119
|
+
if (this.hasNextButtonTarget) {
|
|
120
|
+
this.nextButtonTarget.disabled = Boolean(max && view >= this.monthIndexOf(max));
|
|
121
|
+
}
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
selectDay(e) {
|
|
@@ -119,6 +169,7 @@ export default class extends Controller {
|
|
|
119
169
|
}
|
|
120
170
|
this.selectedDatesValue = current;
|
|
121
171
|
this.updateCalendar();
|
|
172
|
+
this.pushToOutlets();
|
|
122
173
|
this.dispatchChange();
|
|
123
174
|
}
|
|
124
175
|
|
|
@@ -161,6 +212,9 @@ export default class extends Controller {
|
|
|
161
212
|
return end ? `${this.formatDate(start)} – ${this.formatDate(end)}` : this.formatDate(start);
|
|
162
213
|
}
|
|
163
214
|
if (this.modeValue === "multiple") {
|
|
215
|
+
// Deselecting the last date must clear the bound input: return ""
|
|
216
|
+
// (pushed to outlets), not null (skipped by pushToOutlets' guard).
|
|
217
|
+
if (!this.selectedDatesValue.length) return "";
|
|
164
218
|
return this.selectedDatesValue.map((d) => this.formatDate(this.parseDate(d))).join(", ");
|
|
165
219
|
}
|
|
166
220
|
const selected = this.selectedDate();
|
|
@@ -192,7 +246,75 @@ export default class extends Controller {
|
|
|
192
246
|
this.titleTarget.textContent = this.monthAndYear();
|
|
193
247
|
}
|
|
194
248
|
this.syncDropdowns();
|
|
249
|
+
this.syncNavButtons();
|
|
250
|
+
// innerHTML replacement drops focus — remember the focused day so
|
|
251
|
+
// keyboard selection/month hops don't strand focus on <body>.
|
|
252
|
+
const focusedDay = this.calendarTarget.contains(document.activeElement)
|
|
253
|
+
? document.activeElement.dataset?.day
|
|
254
|
+
: null;
|
|
195
255
|
this.calendarTarget.innerHTML = this.calendarHTML();
|
|
256
|
+
this.ensureGridTabStop(focusedDay);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Roving tabindex: exactly one day button is tabbable — the previously
|
|
260
|
+
// focused day (refocused across re-renders), else the selected day (its
|
|
261
|
+
// template ships tabindex="0"), else today, else the first enabled day of
|
|
262
|
+
// the current month. Without this, a calendar with no selection has no
|
|
263
|
+
// keyboard entry point at all.
|
|
264
|
+
ensureGridTabStop(focusedDay) {
|
|
265
|
+
const days = [...this.calendarTarget.querySelectorAll(".pk-calendar-day:not([disabled])")];
|
|
266
|
+
if (!days.length) return;
|
|
267
|
+
const focused = focusedDay ? days.find((d) => d.dataset.day === focusedDay) : null;
|
|
268
|
+
const stop =
|
|
269
|
+
focused ||
|
|
270
|
+
days.find((d) => d.getAttribute("tabindex") === "0") ||
|
|
271
|
+
days.find((d) => d.classList.contains("today")) ||
|
|
272
|
+
days.find((d) => !d.classList.contains("other")) ||
|
|
273
|
+
days[0];
|
|
274
|
+
days.forEach((d) => d.setAttribute("tabindex", d === stop ? "0" : "-1"));
|
|
275
|
+
if (focused) stop.focus({ preventScroll: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Arrow keys move by day/week, PageUp/PageDown by month (Shift: by year,
|
|
279
|
+
// per APG), Home/End jump within the rendered week row; crossing a month
|
|
280
|
+
// boundary re-renders the grid on the target month. Enter/Space activate
|
|
281
|
+
// natively (the days are real <button>s).
|
|
282
|
+
onKeydown(e) {
|
|
283
|
+
const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
284
|
+
const day = e.target.closest?.("[data-day]");
|
|
285
|
+
if (!day) return;
|
|
286
|
+
|
|
287
|
+
let target;
|
|
288
|
+
if (e.key in STEPS) {
|
|
289
|
+
target = this.parseDate(day.dataset.day);
|
|
290
|
+
target.setDate(target.getDate() + STEPS[e.key]);
|
|
291
|
+
} else if (e.key === "PageUp" || e.key === "PageDown") {
|
|
292
|
+
const base = this.parseDate(day.dataset.day);
|
|
293
|
+
const months = (e.key === "PageUp" ? -1 : 1) * (e.shiftKey ? 12 : 1);
|
|
294
|
+
target = new Date(base.getFullYear(), base.getMonth() + months, 1);
|
|
295
|
+
// land on the same day-of-month, clamped to the target month's length
|
|
296
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
297
|
+
target.setDate(Math.min(base.getDate(), lastDay));
|
|
298
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
299
|
+
const rowDays = [...day.closest("tr").querySelectorAll("[data-day]")];
|
|
300
|
+
target = this.parseDate(rowDays[e.key === "Home" ? 0 : rowDays.length - 1].dataset.day);
|
|
301
|
+
} else {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
|
|
306
|
+
const iso = this.isoDate(target);
|
|
307
|
+
if (this.isDateDisabled(iso)) return; // min/max/booked boundary: stay put
|
|
308
|
+
|
|
309
|
+
if (target.getMonth() !== this.viewDate().getMonth() || target.getFullYear() !== this.viewDate().getFullYear()) {
|
|
310
|
+
this.viewDateValue = iso; // re-renders the grid on the target month
|
|
311
|
+
}
|
|
312
|
+
const next = this.calendarTarget.querySelector(`[data-day="${iso}"]:not([disabled])`);
|
|
313
|
+
if (!next) return;
|
|
314
|
+
this.calendarTarget.querySelectorAll(".pk-calendar-day").forEach((d) => {
|
|
315
|
+
d.setAttribute("tabindex", d === next ? "0" : "-1");
|
|
316
|
+
});
|
|
317
|
+
next.focus({ preventScroll: true });
|
|
196
318
|
}
|
|
197
319
|
|
|
198
320
|
syncDropdowns() {
|
|
@@ -200,7 +322,15 @@ export default class extends Controller {
|
|
|
200
322
|
this.monthSelectTarget.value = String(this.viewDate().getMonth());
|
|
201
323
|
}
|
|
202
324
|
if (this.hasYearSelectTarget) {
|
|
203
|
-
|
|
325
|
+
const select = this.yearSelectTarget;
|
|
326
|
+
const year = this.viewDate().getFullYear();
|
|
327
|
+
select.value = String(year);
|
|
328
|
+
if (select.selectedIndex === -1 && select.options.length) {
|
|
329
|
+
// view year outside the option range — clamp to the nearest end
|
|
330
|
+
// instead of leaving the select blank
|
|
331
|
+
const years = [...select.options].map((o) => Number(o.value));
|
|
332
|
+
select.value = String(year < Math.min(...years) ? Math.min(...years) : Math.max(...years));
|
|
333
|
+
}
|
|
204
334
|
}
|
|
205
335
|
}
|
|
206
336
|
|
|
@@ -218,15 +348,22 @@ export default class extends Controller {
|
|
|
218
348
|
let cells = week.map((day) => this.renderDay(day)).join("");
|
|
219
349
|
if (this.weekNumbersValue) {
|
|
220
350
|
const weekNumber = this.isoWeekNumber(week[0]);
|
|
221
|
-
cells = `<td class="pk-calendar-weeknumber" role="
|
|
351
|
+
cells = `<td class="pk-calendar-weeknumber" role="rowheader">${weekNumber}</td>` + cells;
|
|
222
352
|
}
|
|
223
|
-
return `<tr class="pk-calendar-week">${cells}</tr>`;
|
|
353
|
+
return `<tr class="pk-calendar-week" role="row">${cells}</tr>`;
|
|
224
354
|
}
|
|
225
355
|
|
|
226
356
|
renderDay(day) {
|
|
227
357
|
const today = new Date();
|
|
228
358
|
const iso = this.isoDate(day);
|
|
229
|
-
const data = {
|
|
359
|
+
const data = {
|
|
360
|
+
day: iso,
|
|
361
|
+
dayDate: day.getDate(),
|
|
362
|
+
// full human-readable date for the button's accessible name — a bare
|
|
363
|
+
// day number ("14") is meaningless to a screen reader
|
|
364
|
+
dayLabel: day.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }),
|
|
365
|
+
state: this.dayState(day),
|
|
366
|
+
};
|
|
230
367
|
|
|
231
368
|
if (this.isDateDisabled(day)) {
|
|
232
369
|
return renderTemplate(this.disabledDateTemplateTarget.innerHTML, data);
|
|
@@ -404,8 +541,10 @@ export default class extends Controller {
|
|
|
404
541
|
PPPP: `${dayOfWeek}, ${monthName} ${day}${daySuffix}, ${year}`,
|
|
405
542
|
};
|
|
406
543
|
|
|
544
|
+
// Longest tokens first: MMMM must precede MM or the alternation eats
|
|
545
|
+
// "MMMM" as two "MM"s and renders the month number twice.
|
|
407
546
|
const formattedDate = format.replace(
|
|
408
|
-
/yyyy|MM|dd|HH|mm|ss|EEEE|
|
|
547
|
+
/yyyy|MMMM|MM|dd|HH|mm|ss|EEEE|PPPP|do/g,
|
|
409
548
|
(matched) => map[matched],
|
|
410
549
|
);
|
|
411
550
|
return formattedDate;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# <template>s for the five day states (disabled / selected / today / current
|
|
3
3
|
# month / other month). The controller picks one per day and interpolates
|
|
4
|
-
# {{day}} / {{dayDate}} / {{state}} — state carries the extra
|
|
5
|
-
# classes (range-start / range-end / in-range / booked)
|
|
4
|
+
# {{day}} / {{dayDate}} / {{dayLabel}} / {{state}} — state carries the extra
|
|
5
|
+
# range/booked classes (range-start / range-end / in-range / booked), and
|
|
6
|
+
# dayLabel is the full human-readable date used as the button's accessible
|
|
7
|
+
# name. Per the APG date-grid pattern the <td> is the gridcell (and carries
|
|
8
|
+
# aria-selected); the <button> inside is a plain button. See calendar.rb.
|
|
6
9
|
class CalendarDays < BaseComponent
|
|
7
10
|
def initialize(**attrs)
|
|
8
11
|
@attrs = attrs
|
|
@@ -25,25 +28,24 @@ module PhlexKit
|
|
|
25
28
|
name: "day",
|
|
26
29
|
class: "pk-calendar-day disabled {{state}}",
|
|
27
30
|
disabled: true,
|
|
28
|
-
role: "gridcell",
|
|
29
31
|
tabindex: "-1",
|
|
30
32
|
type: "button",
|
|
31
|
-
aria_disabled: "true"
|
|
33
|
+
aria_disabled: "true",
|
|
34
|
+
aria_label: "{{dayLabel}}"
|
|
32
35
|
) { "{{dayDate}}" }
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def render_selected_date_template
|
|
37
|
-
date_template("selectedDateTemplate") do
|
|
40
|
+
date_template("selectedDateTemplate", aria_selected: "true") do
|
|
38
41
|
button(
|
|
39
42
|
data_day: "{{day}}",
|
|
40
43
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
41
44
|
name: "day",
|
|
42
45
|
class: "pk-calendar-day selected {{state}}",
|
|
43
|
-
role: "gridcell",
|
|
44
46
|
tabindex: "0",
|
|
45
47
|
type: "button",
|
|
46
|
-
|
|
48
|
+
aria_label: "{{dayLabel}}"
|
|
47
49
|
) { "{{dayDate}}" }
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -55,9 +57,9 @@ module PhlexKit
|
|
|
55
57
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
56
58
|
name: "day",
|
|
57
59
|
class: "pk-calendar-day today {{state}}",
|
|
58
|
-
role: "gridcell",
|
|
59
60
|
tabindex: "-1",
|
|
60
|
-
type: "button"
|
|
61
|
+
type: "button",
|
|
62
|
+
aria_label: "{{dayLabel}}"
|
|
61
63
|
) { "{{dayDate}}" }
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -69,9 +71,9 @@ module PhlexKit
|
|
|
69
71
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
70
72
|
name: "day",
|
|
71
73
|
class: "pk-calendar-day {{state}}",
|
|
72
|
-
role: "gridcell",
|
|
73
74
|
tabindex: "-1",
|
|
74
|
-
type: "button"
|
|
75
|
+
type: "button",
|
|
76
|
+
aria_label: "{{dayLabel}}"
|
|
75
77
|
) { "{{dayDate}}" }
|
|
76
78
|
end
|
|
77
79
|
end
|
|
@@ -83,16 +85,16 @@ module PhlexKit
|
|
|
83
85
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
84
86
|
name: "day",
|
|
85
87
|
class: "pk-calendar-day other {{state}}",
|
|
86
|
-
role: "gridcell",
|
|
87
88
|
tabindex: "-1",
|
|
88
|
-
type: "button"
|
|
89
|
+
type: "button",
|
|
90
|
+
aria_label: "{{dayLabel}}"
|
|
89
91
|
) { "{{dayDate}}" }
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
93
|
-
def date_template(target, &block)
|
|
95
|
+
def date_template(target, **td_attrs, &block)
|
|
94
96
|
template(data: { phlex_kit__calendar_target: target }) do
|
|
95
|
-
td(class: "pk-calendar-cell", role: "
|
|
97
|
+
td(class: "pk-calendar-cell", role: "gridcell", **td_attrs, &block)
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
end
|
|
@@ -5,6 +5,12 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
5
5
|
// Connects to data-controller="phlex-kit--calendar-input"
|
|
6
6
|
export default class extends Controller {
|
|
7
7
|
setValue(value) {
|
|
8
|
+
if (this.element.value === value) return
|
|
8
9
|
this.element.value = value
|
|
10
|
+
// Programmatic .value assignment fires nothing — dispatch input/change so
|
|
11
|
+
// form frameworks, validators and Stimulus actions bound to the field see
|
|
12
|
+
// the calendar's write.
|
|
13
|
+
this.element.dispatchEvent(new Event("input", { bubbles: true }))
|
|
14
|
+
this.element.dispatchEvent(new Event("change", { bubbles: true }))
|
|
9
15
|
}
|
|
10
16
|
}
|
|
@@ -11,7 +11,11 @@ module PhlexKit
|
|
|
11
11
|
name: "next-month",
|
|
12
12
|
class: "pk-calendar-next",
|
|
13
13
|
aria: { label: "Go to next month" },
|
|
14
|
-
data: {
|
|
14
|
+
data: {
|
|
15
|
+
action: "click->phlex-kit--calendar#nextMonth",
|
|
16
|
+
# target so the controller can disable it at the max-date/to-year bound
|
|
17
|
+
phlex_kit__calendar_target: "nextButton"
|
|
18
|
+
}
|
|
15
19
|
}, @attrs)) do
|
|
16
20
|
icon
|
|
17
21
|
end
|
|
@@ -11,7 +11,11 @@ module PhlexKit
|
|
|
11
11
|
name: "previous-month",
|
|
12
12
|
class: "pk-calendar-prev",
|
|
13
13
|
aria: { label: "Go to previous month" },
|
|
14
|
-
data: {
|
|
14
|
+
data: {
|
|
15
|
+
action: "click->phlex-kit--calendar#prevMonth",
|
|
16
|
+
# target so the controller can disable it at the min-date/from-year bound
|
|
17
|
+
phlex_kit__calendar_target: "prevButton"
|
|
18
|
+
}
|
|
15
19
|
}, @attrs)) do
|
|
16
20
|
icon
|
|
17
21
|
end
|
|
@@ -12,7 +12,7 @@ module PhlexKit
|
|
|
12
12
|
def view_template
|
|
13
13
|
template(data: { phlex_kit__calendar_target: "weekdaysTemplate" }) do
|
|
14
14
|
thead(**@attrs) do
|
|
15
|
-
tr(class: "pk-calendar-weekdays-row") do
|
|
15
|
+
tr(class: "pk-calendar-weekdays-row", role: "row") do
|
|
16
16
|
th(scope: "col", class: "pk-calendar-weekday pk-calendar-weeknumber-head", aria: { label: "Week number" }) { "#" } if @week_numbers
|
|
17
17
|
DAYS.each { |day| render_day(day) }
|
|
18
18
|
end
|
|
@@ -23,7 +23,7 @@ module PhlexKit
|
|
|
23
23
|
data: {
|
|
24
24
|
controller: "phlex-kit--carousel",
|
|
25
25
|
phlex_kit__carousel_options_value: JSON.generate(default_options.merge(@options)),
|
|
26
|
-
action:
|
|
26
|
+
action: keyboard_action
|
|
27
27
|
}
|
|
28
28
|
}, @attrs), &)
|
|
29
29
|
end
|
|
@@ -33,5 +33,15 @@ module PhlexKit
|
|
|
33
33
|
def default_options
|
|
34
34
|
{ axis: (@orientation == :horizontal) ? "x" : "y" }
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
# Arrow keys follow the scroll axis: left/right for horizontal, up/down
|
|
38
|
+
# for vertical.
|
|
39
|
+
def keyboard_action
|
|
40
|
+
if @orientation == :vertical
|
|
41
|
+
"keydown.down->phlex-kit--carousel#scrollNext:prevent keydown.up->phlex-kit--carousel#scrollPrev:prevent"
|
|
42
|
+
else
|
|
43
|
+
"keydown.right->phlex-kit--carousel#scrollNext:prevent keydown.left->phlex-kit--carousel#scrollPrev:prevent"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
36
46
|
end
|
|
37
47
|
end
|
|
@@ -37,6 +37,12 @@ export default class extends Controller {
|
|
|
37
37
|
this._onPointerUp = this._onPointerUp.bind(this);
|
|
38
38
|
this._onClickCapture = this._onClickCapture.bind(this);
|
|
39
39
|
window.addEventListener("resize", this._onResize);
|
|
40
|
+
// window resize misses container-only resizes (flex/grid reflow,
|
|
41
|
+
// sidebar toggles) — observe the viewport itself.
|
|
42
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
43
|
+
this._resizeObserver = new ResizeObserver(() => this._applyTransform());
|
|
44
|
+
this._resizeObserver.observe(this.viewportTarget);
|
|
45
|
+
}
|
|
40
46
|
this.viewportTarget.addEventListener("pointerdown", this._onPointerDown);
|
|
41
47
|
this.viewportTarget.addEventListener("click", this._onClickCapture, true);
|
|
42
48
|
this._update();
|
|
@@ -44,8 +50,19 @@ export default class extends Controller {
|
|
|
44
50
|
|
|
45
51
|
disconnect() {
|
|
46
52
|
window.removeEventListener("resize", this._onResize);
|
|
53
|
+
this._resizeObserver?.disconnect();
|
|
54
|
+
this._resizeObserver = null;
|
|
47
55
|
this.viewportTarget.removeEventListener("pointerdown", this._onPointerDown);
|
|
48
56
|
this.viewportTarget.removeEventListener("click", this._onClickCapture, true);
|
|
57
|
+
// a disconnect mid-drag must drop the move/up listeners and drag state
|
|
58
|
+
this.viewportTarget.removeEventListener("pointermove", this._onPointerMove);
|
|
59
|
+
this.viewportTarget.removeEventListener("pointerup", this._onPointerUp);
|
|
60
|
+
this.viewportTarget.removeEventListener("pointercancel", this._onPointerUp);
|
|
61
|
+
if (this.drag) {
|
|
62
|
+
this.drag = null;
|
|
63
|
+
if (this.track) this.track.style.transition = "";
|
|
64
|
+
this.viewportTarget.classList.remove("dragging");
|
|
65
|
+
}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
scrollNext() {
|
|
@@ -170,9 +187,19 @@ export default class extends Controller {
|
|
|
170
187
|
const slide = this.slides[index];
|
|
171
188
|
const first = this.slides[0];
|
|
172
189
|
if (!slide || !first) return 0;
|
|
173
|
-
|
|
190
|
+
const raw = this.options.axis === "y"
|
|
174
191
|
? slide.offsetTop - first.offsetTop
|
|
175
192
|
: slide.offsetLeft - first.offsetLeft;
|
|
193
|
+
// Never scroll past the content edge: with multi-up layouts the last
|
|
194
|
+
// slides' offsets overshoot the max scrollable offset (track − viewport).
|
|
195
|
+
return Math.min(Math.max(0, raw), this._maxOffset());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_maxOffset() {
|
|
199
|
+
const vertical = this.options.axis === "y";
|
|
200
|
+
const trackSize = vertical ? this.track.scrollHeight : this.track.scrollWidth;
|
|
201
|
+
const viewportSize = vertical ? this.viewportTarget.clientHeight : this.viewportTarget.clientWidth;
|
|
202
|
+
return Math.max(0, trackSize - viewportSize);
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
_slideSize() {
|