phlex_kit 0.14.0 → 0.15.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/calendar/calendar.rb +9 -2
- data/app/components/phlex_kit/calendar/calendar_controller.js +39 -6
- data/app/components/phlex_kit/command/command.rb +16 -3
- data/app/components/phlex_kit/command/command_controller.js +6 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +15 -1
- data/app/components/phlex_kit/date_picker/date_picker.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +15 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +17 -1
- data/app/components/phlex_kit/popover/popover_content.rb +8 -1
- data/app/components/phlex_kit/popover/popover_controller.js +4 -0
- data/app/components/phlex_kit/radio_button/radio_button.css +16 -0
- data/app/components/phlex_kit/select/select.rb +1 -1
- data/app/components/phlex_kit/select/select_controller.js +32 -0
- data/app/components/phlex_kit/table/table_head.rb +6 -1
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +1 -0
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +6 -1
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e817130c54d3e6a8c547328c09b8e632e13adda0cafa7ed0f415359fd1c0ed9
|
|
4
|
+
data.tar.gz: bcb04540a46d3442ac21b8d54d10b1f903bbcf33af2ec65344e7ad6879ab8462
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad56db4fec9925c964ed13084658ddf29942d79d17d4cd141bdb3efa0bef5d3e0be0c6c9ff8f53b28da163dfb0f080c1a79c8330b806f01823e0e092b479a5f5
|
|
7
|
+
data.tar.gz: 3e90c49dc0cb097b2aee66a28d7537c7c834df03015338832e6f00c9cbe40c93374d7a1d034a3c9765b848dc12407afc44681d2b6ef4aa62176295fecb1f3ad2
|
|
@@ -16,7 +16,11 @@ module PhlexKit
|
|
|
16
16
|
# the year list). Pass `input_id:` a CSS selector (e.g. "#due-date") to
|
|
17
17
|
# push the picked date (or "start – end" range) into a
|
|
18
18
|
# phlex-kit--calendar-input outlet — that's how PhlexKit::DatePicker binds
|
|
19
|
-
# an Input.
|
|
19
|
+
# an Input. `locale:` (a BCP 47 tag) drives every Intl call the controller
|
|
20
|
+
# makes — caption, weekday headers, month dropdown, day accessible names and
|
|
21
|
+
# the EEEE/MMMM format tokens; unset follows the browser. The server-rendered
|
|
22
|
+
# month/weekday words stay English as the pre-JS baseline. Note the `do`/PPPP
|
|
23
|
+
# ordinal suffixes (st/nd/rd/th) are an English rule and stay English. Sizing rides --pk-cell-size/--pk-cell-radius (calendar.css).
|
|
20
24
|
class Calendar < BaseComponent
|
|
21
25
|
MODES = { single: "single", range: "range", multiple: "multiple" }.freeze
|
|
22
26
|
CAPTION_LAYOUTS = %i[label dropdown].freeze
|
|
@@ -25,7 +29,7 @@ module PhlexKit
|
|
|
25
29
|
def initialize(mode: :single, selected_date: nil, selected_dates: [], range_start: nil, range_end: nil,
|
|
26
30
|
min_date: nil, max_date: nil, disabled_dates: [], week_numbers: false,
|
|
27
31
|
caption_layout: :label, from_year: nil, to_year: nil,
|
|
28
|
-
input_id: nil, date_format: "yyyy-MM-dd", **attrs)
|
|
32
|
+
input_id: nil, date_format: "yyyy-MM-dd", locale: nil, **attrs)
|
|
29
33
|
@mode = MODES.fetch(mode.to_sym)
|
|
30
34
|
@selected_date = selected_date
|
|
31
35
|
@selected_dates = Array(selected_dates).map(&:to_s)
|
|
@@ -41,6 +45,7 @@ module PhlexKit
|
|
|
41
45
|
@to_year = to_year
|
|
42
46
|
@input_id = input_id
|
|
43
47
|
@date_format = date_format
|
|
48
|
+
@locale = locale
|
|
44
49
|
@attrs = attrs
|
|
45
50
|
end
|
|
46
51
|
|
|
@@ -112,6 +117,8 @@ module PhlexKit
|
|
|
112
117
|
data[:phlex_kit__calendar_selected_dates_value] = JSON.generate(@selected_dates) if @selected_dates.any?
|
|
113
118
|
data[:phlex_kit__calendar_disabled_dates_value] = JSON.generate(@disabled_dates) if @disabled_dates.any?
|
|
114
119
|
data[:phlex_kit__calendar_week_numbers_value] = "true" if @week_numbers
|
|
120
|
+
# Unset = the runtime's own locale (the controller passes undefined to Intl).
|
|
121
|
+
data[:phlex_kit__calendar_locale_value] = @locale if @locale
|
|
115
122
|
# Seed the view on the selection so the grid opens on the right month.
|
|
116
123
|
view_seed = @selected_date || @range_start || @selected_dates.first
|
|
117
124
|
data[:phlex_kit__calendar_view_date_value] = view_seed.to_s if view_seed
|
|
@@ -41,6 +41,10 @@ export default class extends Controller {
|
|
|
41
41
|
// toISOString() is UTC (wrong day for non-UTC users near midnight).
|
|
42
42
|
viewDate: { type: String, default: "" },
|
|
43
43
|
format: { type: String, default: "yyyy-MM-dd" },
|
|
44
|
+
// BCP 47 tag for every Intl call below. Unset = "" = undefined to Intl,
|
|
45
|
+
// i.e. the runtime's own locale (the server markup is English only as the
|
|
46
|
+
// pre-JS baseline, so the grid localizes itself on first render).
|
|
47
|
+
locale: { type: String, default: "" },
|
|
44
48
|
};
|
|
45
49
|
static outlets = ["phlex-kit--calendar-input"];
|
|
46
50
|
|
|
@@ -274,6 +278,34 @@ export default class extends Controller {
|
|
|
274
278
|
// `focusDay` overrides the day to refocus after re-render: on a keyboard
|
|
275
279
|
// month cross the target day lives in the NEW grid, not the pre-render
|
|
276
280
|
// activeElement, so onKeydown passes it explicitly.
|
|
281
|
+
// undefined (not "") is what Intl wants for "use the runtime default".
|
|
282
|
+
locale() {
|
|
283
|
+
return this.localeValue || undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The weekday <th>s are cloned verbatim from a server template and the month
|
|
287
|
+
// dropdown's <option>s are server-rendered — both English. Relabel them from
|
|
288
|
+
// Intl on every render so the whole chrome speaks one language.
|
|
289
|
+
localizeChrome() {
|
|
290
|
+
const heads = [...this.calendarTarget.querySelectorAll(".pk-calendar-weekday")].filter(
|
|
291
|
+
(th) => !th.classList.contains("pk-calendar-weeknumber-head"),
|
|
292
|
+
);
|
|
293
|
+
// The server row runs Monday..Sunday; 2024-01-01 was a Monday.
|
|
294
|
+
heads.forEach((th, index) => {
|
|
295
|
+
const day = new Date(2024, 0, 1 + index);
|
|
296
|
+
const name = day.toLocaleDateString(this.locale(), { weekday: "long" });
|
|
297
|
+
th.setAttribute("aria-label", name);
|
|
298
|
+
th.textContent = day.toLocaleDateString(this.locale(), { weekday: "short" }).slice(0, 2);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!this.hasMonthSelectTarget) return;
|
|
302
|
+
[...this.monthSelectTarget.options].forEach((option) => {
|
|
303
|
+
const month = Number(option.value);
|
|
304
|
+
if (!Number.isFinite(month)) return;
|
|
305
|
+
option.textContent = new Date(2024, month, 1).toLocaleDateString(this.locale(), { month: "long" });
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
277
309
|
updateCalendar(focusDay = null) {
|
|
278
310
|
if (this.hasTitleTarget) {
|
|
279
311
|
this.titleTarget.textContent = this.monthAndYear();
|
|
@@ -288,6 +320,7 @@ export default class extends Controller {
|
|
|
288
320
|
? document.activeElement.dataset?.day
|
|
289
321
|
: null);
|
|
290
322
|
this.calendarTarget.innerHTML = this.calendarHTML();
|
|
323
|
+
this.localizeChrome();
|
|
291
324
|
this.ensureGridTabStop(focusedDay);
|
|
292
325
|
}
|
|
293
326
|
|
|
@@ -419,7 +452,7 @@ export default class extends Controller {
|
|
|
419
452
|
dayDate: day.getDate(),
|
|
420
453
|
// full human-readable date for the button's accessible name — a bare
|
|
421
454
|
// day number ("14") is meaningless to a screen reader
|
|
422
|
-
dayLabel: day.toLocaleDateString(
|
|
455
|
+
dayLabel: day.toLocaleDateString(this.locale(), { weekday: "long", month: "long", day: "numeric", year: "numeric" }),
|
|
423
456
|
state: this.dayState(day),
|
|
424
457
|
};
|
|
425
458
|
|
|
@@ -499,10 +532,10 @@ export default class extends Controller {
|
|
|
499
532
|
.filter(Boolean);
|
|
500
533
|
}
|
|
501
534
|
|
|
535
|
+
// One Intl call rather than `${month} ${year}` so locales that order the
|
|
536
|
+
// parts differently (ja-JP → "2026年1月") come out right, not just translated.
|
|
502
537
|
monthAndYear() {
|
|
503
|
-
|
|
504
|
-
const year = this.viewDate().getFullYear();
|
|
505
|
-
return `${month} ${year}`;
|
|
538
|
+
return this.viewDate().toLocaleDateString(this.locale(), { month: "long", year: "numeric" });
|
|
506
539
|
}
|
|
507
540
|
|
|
508
541
|
selectedDate() {
|
|
@@ -592,8 +625,8 @@ export default class extends Controller {
|
|
|
592
625
|
const hours = date.getHours();
|
|
593
626
|
const minutes = date.getMinutes();
|
|
594
627
|
const seconds = date.getSeconds();
|
|
595
|
-
const dayOfWeek = date.toLocaleString(
|
|
596
|
-
const monthName = date.toLocaleString(
|
|
628
|
+
const dayOfWeek = date.toLocaleString(this.locale(), { weekday: "long" });
|
|
629
|
+
const monthName = date.toLocaleString(this.locale(), { month: "long" });
|
|
597
630
|
const daySuffix = this.getDaySuffix(day);
|
|
598
631
|
|
|
599
632
|
const map = {
|
|
@@ -7,7 +7,15 @@ module PhlexKit
|
|
|
7
7
|
# Upstream's fuse.js fuzzy search is replaced with a dependency-free substring
|
|
8
8
|
# match in the controller. Tailwind → vanilla `.pk-command*` (command.css).
|
|
9
9
|
class Command < BaseComponent
|
|
10
|
-
|
|
10
|
+
# Announcement templates for the live region — the only strings the
|
|
11
|
+
# controller writes itself (CommandEmpty is server-rendered, so it
|
|
12
|
+
# localizes with the rest of your markup). %{count} is interpolated.
|
|
13
|
+
DEFAULT_RESULTS_FORMAT = "%{count} result(s)"
|
|
14
|
+
DEFAULT_NO_RESULTS_TEXT = "No results"
|
|
15
|
+
|
|
16
|
+
def initialize(results_format: DEFAULT_RESULTS_FORMAT, no_results_text: DEFAULT_NO_RESULTS_TEXT, **attrs)
|
|
17
|
+
@results_format = results_format
|
|
18
|
+
@no_results_text = no_results_text
|
|
11
19
|
@attrs = attrs
|
|
12
20
|
end
|
|
13
21
|
|
|
@@ -21,12 +29,17 @@ module PhlexKit
|
|
|
21
29
|
private
|
|
22
30
|
|
|
23
31
|
# Screen-reader announcement of the filtered result count — the controller
|
|
24
|
-
# writes
|
|
32
|
+
# writes the results_format / no_results_text strings into it from
|
|
33
|
+
# filter(), interpolating %{count}.
|
|
25
34
|
def live_region
|
|
26
35
|
div(
|
|
27
36
|
class: "pk-sr-only",
|
|
28
37
|
aria: { live: "polite" },
|
|
29
|
-
data: {
|
|
38
|
+
data: {
|
|
39
|
+
phlex_kit__command_target: "liveRegion",
|
|
40
|
+
results_format: @results_format,
|
|
41
|
+
no_results_text: @no_results_text
|
|
42
|
+
}
|
|
30
43
|
)
|
|
31
44
|
end
|
|
32
45
|
end
|
|
@@ -262,8 +262,13 @@ export default class extends Controller {
|
|
|
262
262
|
if (count === null) {
|
|
263
263
|
this.liveRegionTarget.textContent = "";
|
|
264
264
|
} else {
|
|
265
|
+
// Templates come from the server (Command's results_format: /
|
|
266
|
+
// no_results_text: kwargs) — the defaults mirror command.rb's.
|
|
267
|
+
const data = this.liveRegionTarget.dataset;
|
|
265
268
|
this.liveRegionTarget.textContent =
|
|
266
|
-
count === 0
|
|
269
|
+
count === 0
|
|
270
|
+
? data.noResultsText || "No results"
|
|
271
|
+
: (data.resultsFormat || "%{count} result(s)").replace("%{count}", count);
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
|
|
@@ -164,7 +164,7 @@ export default class extends Controller {
|
|
|
164
164
|
// trigger still counts). Escape stays global while the menu is open (APG).
|
|
165
165
|
if (e.key !== "Escape" && !this.element.contains(document.activeElement)) return
|
|
166
166
|
|
|
167
|
-
const items = this.
|
|
167
|
+
const items = this.rovingItems()
|
|
168
168
|
const index = items.indexOf(document.activeElement)
|
|
169
169
|
switch (e.key) {
|
|
170
170
|
case "Escape":
|
|
@@ -254,4 +254,18 @@ export default class extends Controller {
|
|
|
254
254
|
(el) => !el.closest("[data-disabled]") && el.getClientRects().length > 0
|
|
255
255
|
)
|
|
256
256
|
}
|
|
257
|
+
|
|
258
|
+
// The list ArrowUp/Down/Home/End rove — level-aware. A submenu reveals on
|
|
259
|
+
// :focus-within, so focusing a sub-trigger makes its rows visible and
|
|
260
|
+
// items() would pick them up; roving would then dive into the submenu on
|
|
261
|
+
// ArrowDown instead of moving to the next PARENT item (APG reserves
|
|
262
|
+
// ArrowRight/enterKey for entering). When focus is inside a sub panel, rove
|
|
263
|
+
// that panel's own rows; otherwise rove the top-level rows, excluding any
|
|
264
|
+
// sub-content the reveal exposed. Mirrored in menubar/dropdown (shared model).
|
|
265
|
+
rovingItems() {
|
|
266
|
+
const rows = this.items()
|
|
267
|
+
const sub = document.activeElement?.closest(".pk-context-menu-sub-content")
|
|
268
|
+
if (sub) return rows.filter((el) => el.closest(".pk-context-menu-sub-content") === sub)
|
|
269
|
+
return rows.filter((el) => !el.closest(".pk-context-menu-sub-content"))
|
|
270
|
+
}
|
|
257
271
|
}
|
|
@@ -20,6 +20,7 @@ module PhlexKit
|
|
|
20
20
|
placeholder: "Select a date",
|
|
21
21
|
selected_date: value,
|
|
22
22
|
date_format: "yyyy-MM-dd",
|
|
23
|
+
locale: nil,
|
|
23
24
|
input_attrs: {},
|
|
24
25
|
calendar_attrs: {},
|
|
25
26
|
trigger_attrs: {},
|
|
@@ -31,6 +32,7 @@ module PhlexKit
|
|
|
31
32
|
@label = label
|
|
32
33
|
@selected_date = selected_date
|
|
33
34
|
@date_format = date_format
|
|
35
|
+
@locale = locale
|
|
34
36
|
# Seed the input with the same date_format the calendar controller
|
|
35
37
|
# writes — a bare `selected_date.to_s` (ISO) would mismatch the format
|
|
36
38
|
# until the first interaction.
|
|
@@ -65,7 +67,8 @@ module PhlexKit
|
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
render PopoverContent.new(**@content_attrs) do
|
|
68
|
-
render Calendar.new(input_id: input_selector, selected_date: @selected_date, date_format: @date_format,
|
|
70
|
+
render Calendar.new(input_id: input_selector, selected_date: @selected_date, date_format: @date_format,
|
|
71
|
+
locale: @locale, **@calendar_attrs)
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
end
|
|
@@ -164,7 +164,7 @@ export default class extends Controller {
|
|
|
164
164
|
// while the menu is open (APG).
|
|
165
165
|
if (e.key !== "Escape" && !this.element.contains(document.activeElement)) return;
|
|
166
166
|
|
|
167
|
-
const items = this.#
|
|
167
|
+
const items = this.#rovingItems();
|
|
168
168
|
if (items.length === 0) return;
|
|
169
169
|
const index = items.indexOf(document.activeElement);
|
|
170
170
|
|
|
@@ -245,6 +245,20 @@ export default class extends Controller {
|
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// The list ArrowUp/Down/Home/End rove — level-aware. A submenu reveals on
|
|
249
|
+
// :focus-within, so focusing a sub-trigger makes its rows visible and
|
|
250
|
+
// #items() would pick them up; roving would then dive into the submenu on
|
|
251
|
+
// ArrowDown instead of moving to the next PARENT item (APG reserves
|
|
252
|
+
// ArrowRight/enterKey for entering). When focus is inside a sub panel, rove
|
|
253
|
+
// that panel's own rows; otherwise rove the top-level rows, excluding any
|
|
254
|
+
// sub-content the reveal exposed. Mirrored in menubar/context (shared model).
|
|
255
|
+
#rovingItems() {
|
|
256
|
+
const rows = this.#items();
|
|
257
|
+
const sub = document.activeElement?.closest(".pk-dropdown-menu-sub-content");
|
|
258
|
+
if (sub) return rows.filter((el) => el.closest(".pk-dropdown-menu-sub-content") === sub);
|
|
259
|
+
return rows.filter((el) => !el.closest(".pk-dropdown-menu-sub-content"));
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
#addEventListeners() {
|
|
249
263
|
document.addEventListener("keydown", this.boundHandleKeydown);
|
|
250
264
|
}
|
|
@@ -210,7 +210,7 @@ export default class extends Controller {
|
|
|
210
210
|
}
|
|
211
211
|
return
|
|
212
212
|
}
|
|
213
|
-
const items = this.
|
|
213
|
+
const items = this.rovingItems()
|
|
214
214
|
const index = items.indexOf(document.activeElement)
|
|
215
215
|
switch (e.key) {
|
|
216
216
|
case "Escape":
|
|
@@ -333,6 +333,22 @@ export default class extends Controller {
|
|
|
333
333
|
return menu.querySelector("[role=\"menu\"], .pk-menubar-content, .pk-navigation-menu-content")
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
// The list ArrowUp/Down/Home/End rove — level-aware. A submenu reveals on
|
|
337
|
+
// :focus-within, so focusing a sub-trigger makes its rows visible and
|
|
338
|
+
// items() would pick them up; roving would then dive into the submenu on
|
|
339
|
+
// ArrowDown instead of moving to the next PARENT item (APG reserves
|
|
340
|
+
// ArrowRight/enterKey for entering). When focus is inside a sub panel, rove
|
|
341
|
+
// that panel's own rows; otherwise rove the top-level rows, excluding any
|
|
342
|
+
// sub-content the reveal exposed. Mirrored in dropdown/context (shared model).
|
|
343
|
+
rovingItems() {
|
|
344
|
+
const menu = this.openMenu
|
|
345
|
+
if (!menu) return []
|
|
346
|
+
const rows = this.items(menu)
|
|
347
|
+
const sub = document.activeElement?.closest(".pk-menubar-sub-content")
|
|
348
|
+
if (sub) return rows.filter((el) => el.closest(".pk-menubar-sub-content") === sub)
|
|
349
|
+
return rows.filter((el) => !el.closest(".pk-menubar-sub-content"))
|
|
350
|
+
}
|
|
351
|
+
|
|
336
352
|
items(menu) {
|
|
337
353
|
const panel = this.panel(menu)
|
|
338
354
|
if (!panel) return []
|
|
@@ -13,7 +13,14 @@ module PhlexKit
|
|
|
13
13
|
|
|
14
14
|
def view_template(&)
|
|
15
15
|
classes = [ "pk-popover-content", fetch_option(ALIGNS, @align, :align) ].compact.join(" ")
|
|
16
|
-
|
|
16
|
+
# `popover:` is a named default, not a merged attr: `mix` joins duplicate
|
|
17
|
+
# attrs (a caller's `popover: "manual"` would fuse into "auto manual",
|
|
18
|
+
# which the browser normalizes to manual — killing the native light
|
|
19
|
+
# dismiss/Escape this controller relies on). Skip the default when the
|
|
20
|
+
# caller sets it (mirrors MenubarContent).
|
|
21
|
+
base = { class: classes, data: { phlex_kit__popover_target: "content", state: "closed" } }
|
|
22
|
+
base[:popover] = "auto" unless attr_set?(:popover)
|
|
23
|
+
div(**mix(base, @attrs), &)
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
26
|
end
|
|
@@ -16,6 +16,10 @@ export default class extends Controller {
|
|
|
16
16
|
const invoker = this.triggerTarget.querySelector("button")
|
|
17
17
|
if (invoker) {
|
|
18
18
|
invoker.popoverTargetElement = this.contentTarget
|
|
19
|
+
// popoverTargetElement gives native aria-expanded but not haspopup —
|
|
20
|
+
// shadcn/Radix Popover.Trigger ships aria-haspopup="dialog", so add it
|
|
21
|
+
// (leave a caller-set value alone) to match the button-less path below.
|
|
22
|
+
if (!invoker.hasAttribute("aria-haspopup")) invoker.setAttribute("aria-haspopup", "dialog")
|
|
19
23
|
} else {
|
|
20
24
|
// Button-less fallback: keyboard toggling and popup semantics don't
|
|
21
25
|
// come for free like they do with popoverTargetElement — wire Enter
|
|
@@ -38,6 +38,22 @@
|
|
|
38
38
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
39
39
|
}
|
|
40
40
|
.pk-radio[aria-invalid="true"]:checked { border-color: var(--pk-brand); }
|
|
41
|
+
/* Invalid + keyboard focus: the always-on invalid ring above would otherwise
|
|
42
|
+
swallow the equal-specificity :focus-visible rule, leaving no visible focus
|
|
43
|
+
change (and the theme-scoped invalid arms below beat it outright in light).
|
|
44
|
+
Keep the red border (invalid stays legible) but switch the ring to the
|
|
45
|
+
standard focus color — mirrors checkbox.css. */
|
|
46
|
+
.pk-radio[aria-invalid="true"]:focus-visible,
|
|
47
|
+
:root[data-theme="light"] .pk-radio[aria-invalid="true"]:focus-visible {
|
|
48
|
+
border-color: var(--pk-red);
|
|
49
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
50
|
+
}
|
|
51
|
+
@media (prefers-color-scheme: light) {
|
|
52
|
+
:root[data-theme="system"] .pk-radio[aria-invalid="true"]:focus-visible {
|
|
53
|
+
border-color: var(--pk-red);
|
|
54
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
41
57
|
.pk-radio:disabled { cursor: not-allowed; opacity: .5; }
|
|
42
58
|
:root[data-theme="light"] .pk-radio:not(:checked) { background: transparent; }
|
|
43
59
|
:root[data-theme="light"] .pk-radio[aria-invalid="true"] {
|
|
@@ -39,7 +39,7 @@ module PhlexKit
|
|
|
39
39
|
# keydown.esc rides on the root (not only the items) so Escape closes
|
|
40
40
|
# the [popover=manual] panel with focus on the trigger too; handleEsc
|
|
41
41
|
# no-ops while closed.
|
|
42
|
-
action: "click@window->phlex-kit--select#clickOutside keydown.esc->phlex-kit--select#handleEsc focusout->phlex-kit--select#onFocusout"
|
|
42
|
+
action: "mousedown@window->phlex-kit--select#onMousedownOutside click@window->phlex-kit--select#clickOutside keydown.esc->phlex-kit--select#handleEsc focusout->phlex-kit--select#onFocusout"
|
|
43
43
|
}
|
|
44
44
|
}, @attrs), &block)
|
|
45
45
|
end
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
+
// One-shot capture listener swallowing the click an outside mousedown is about
|
|
4
|
+
// to produce (see onMousedownOutside; same helper in dropdown/context/menubar
|
|
5
|
+
// — duplicated per controller by design). Select is a MODAL menu: the
|
|
6
|
+
// dismissing outside click ONLY dismisses, never also acts on what sits under
|
|
7
|
+
// the pointer. Armed at MOUSEDOWN because for a focusable click target the same
|
|
8
|
+
// gesture's focusout closes the panel before the click fires (a click-time
|
|
9
|
+
// check would already see it closed and skip the swallow — audit round 8).
|
|
10
|
+
const swallowClick = (ev) => {
|
|
11
|
+
ev.preventDefault();
|
|
12
|
+
ev.stopPropagation();
|
|
13
|
+
};
|
|
14
|
+
function armSwallowClick() {
|
|
15
|
+
window.addEventListener("click", swallowClick, { once: true, capture: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
// Ported from ruby_ui's phlex-kit--select controller, minus the
|
|
4
19
|
// @floating-ui/dom dependency: the panel is a native [popover=manual] in the
|
|
5
20
|
// top layer, anchor-positioned with viewport-edge flipping by select.css
|
|
@@ -53,6 +68,12 @@ export default class extends Controller {
|
|
|
53
68
|
const newValue = item.dataset.value;
|
|
54
69
|
|
|
55
70
|
this.inputTarget.value = newValue;
|
|
71
|
+
// Also set the value ATTRIBUTE, not just the .value property: a Turbo
|
|
72
|
+
// snapshot restore clones the DOM (dropping the dirty property) and would
|
|
73
|
+
// otherwise revert the hidden input to its server value while the label
|
|
74
|
+
// and aria-selected still show the chosen item — submitting the wrong
|
|
75
|
+
// value behind a correct-looking UI. (re-derive-from-live-truth rule.)
|
|
76
|
+
this.inputTarget.setAttribute("value", newValue);
|
|
56
77
|
this.valueTarget.innerText = item.innerText;
|
|
57
78
|
|
|
58
79
|
this.dispatchOnChange(oldValue, newValue);
|
|
@@ -162,6 +183,17 @@ export default class extends Controller {
|
|
|
162
183
|
this.itemTargets.forEach((item) => item.removeAttribute("aria-current"));
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
// Modal dismiss: arm the swallow at mousedown so the outside click that
|
|
187
|
+
// dismisses an open select doesn't ALSO activate a focusable control under
|
|
188
|
+
// the pointer (the same gesture's focusout closes the panel before the
|
|
189
|
+
// click, so clickOutside below would already see it closed). Mirrors
|
|
190
|
+
// dropdown/context/menubar.
|
|
191
|
+
onMousedownOutside(event) {
|
|
192
|
+
if (!this.contentTarget.matches(":popover-open")) return;
|
|
193
|
+
if (this.element.contains(event.target)) return;
|
|
194
|
+
armSwallowClick();
|
|
195
|
+
}
|
|
196
|
+
|
|
165
197
|
clickOutside(event) {
|
|
166
198
|
if (!this.contentTarget.matches(":popover-open")) return;
|
|
167
199
|
if (this.element.contains(event.target)) return;
|
|
@@ -6,7 +6,12 @@ module PhlexKit
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def view_template(&block)
|
|
9
|
-
|
|
9
|
+
# scope="col" is the common case (header cells sit in a TableHeader row);
|
|
10
|
+
# a generated default, not a merged attr — `mix` would fuse a caller's
|
|
11
|
+
# `scope: "row"` into "col row" — so skip it when the caller sets scope.
|
|
12
|
+
base = { class: "pk-table-head" }
|
|
13
|
+
base[:scope] = "col" unless attr_set?(:scope)
|
|
14
|
+
th(**mix(base, @attrs), &block)
|
|
10
15
|
end
|
|
11
16
|
end
|
|
12
17
|
end
|
|
@@ -88,6 +88,7 @@ module PhlexKit
|
|
|
88
88
|
data: { controller: "phlex-kit--toggle-group",
|
|
89
89
|
phlex_kit__toggle_group_type_value: @type.to_s,
|
|
90
90
|
phlex_kit__toggle_group_name_value: @name.to_s,
|
|
91
|
+
phlex_kit__toggle_group_disabled_value: @disabled.to_s,
|
|
91
92
|
orientation: @orientation.to_s, spacing: @spacing.to_s } }
|
|
92
93
|
# spacing: N gaps the items by N × .25rem (Tailwind-style scale) via the
|
|
93
94
|
# custom property toggle_group.css reads. Trailing ";" so a caller
|
|
@@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--toggle-group"
|
|
4
4
|
export default class extends Controller {
|
|
5
5
|
static targets = ["item", "input"]
|
|
6
|
-
static values = { type: String, name: String }
|
|
6
|
+
static values = { type: String, name: String, disabled: Boolean }
|
|
7
7
|
|
|
8
8
|
connect() { this.reconcile() }
|
|
9
9
|
|
|
@@ -100,6 +100,11 @@ export default class extends Controller {
|
|
|
100
100
|
input.type = "hidden"
|
|
101
101
|
input.name = name
|
|
102
102
|
input.value = value
|
|
103
|
+
// A disabled group's hidden input is server-rendered `disabled` so it
|
|
104
|
+
// won't submit (toggle_group.rb). rebuildInputs() removes that server
|
|
105
|
+
// input and recreates it here on connect() — carry the disabled flag or
|
|
106
|
+
// the group would start submitting its value post-hydration.
|
|
107
|
+
if (this.disabledValue) input.disabled = true
|
|
103
108
|
input.setAttribute("data-phlex-kit--toggle-group-target", "input")
|
|
104
109
|
return input
|
|
105
110
|
}
|
data/lib/phlex_kit/version.rb
CHANGED