phlex_kit 0.12.0 → 0.13.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/alert_dialog/alert_dialog_content.rb +2 -2
- data/app/components/phlex_kit/calendar/calendar_controller.js +14 -3
- data/app/components/phlex_kit/chart/chart.rb +1 -1
- data/app/components/phlex_kit/checkbox/checkbox.rb +15 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +2 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +4 -0
- data/app/components/phlex_kit/command/command_controller.js +60 -1
- data/app/components/phlex_kit/command/command_dialog_content.rb +5 -0
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +26 -0
- data/app/components/phlex_kit/data_table/data_table_controller.js +7 -1
- data/app/components/phlex_kit/data_table/data_table_selection_summary.rb +12 -3
- data/app/components/phlex_kit/date_picker/date_picker_controller.js +3 -5
- data/app/components/phlex_kit/drawer/drawer_content.rb +2 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +24 -5
- data/app/components/phlex_kit/icon/icon.rb +1 -1
- data/app/components/phlex_kit/link/link.rb +10 -1
- data/app/components/phlex_kit/menubar/menubar.rb +3 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +26 -0
- data/app/components/phlex_kit/progress/progress.css +19 -0
- data/app/components/phlex_kit/progress/progress.rb +29 -13
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content.rb +2 -2
- data/app/components/phlex_kit/switch/switch.rb +1 -1
- data/app/components/phlex_kit/toast/toast_controller.js +6 -1
- data/app/components/phlex_kit/toast/toast_item.rb +2 -2
- data/app/components/phlex_kit/toast/toaster_controller.js +34 -6
- data/lib/phlex_kit/base_component.rb +9 -0
- 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: 4e3d6272f0ef818c51cc952c6a2021a1f2b6a036afdab70b617f05c64ada4144
|
|
4
|
+
data.tar.gz: aa43b4e8826c3dfe52d96440987195815e836de43a795f9628dfce1cbc3bcbd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b655fda7c0a0b013e006b2586b5b2eec5714106a7877f4b012ed6567af45fedd0320cb8cec8360fc05288924af71e809ce56ce911d2dbb72d7fceb52adf938c2
|
|
7
|
+
data.tar.gz: 90070ed31d24e9f46f311345a19eaa2ef79b760a9de08cb28c174ee6126e68aa6cd4122fbe8f915d61aaf800801d1dd781b81e86fc7e4aa90595e54be8631435
|
|
@@ -30,9 +30,9 @@ module PhlexKit
|
|
|
30
30
|
# Defaults only when the caller didn't supply their own — `mix`
|
|
31
31
|
# would fuse role="alertdialog dialog" / aria-modal="true false" /
|
|
32
32
|
# tabindex="-1 0" instead of overriding.
|
|
33
|
-
panel_attrs[:role] = "alertdialog" unless
|
|
33
|
+
panel_attrs[:role] = "alertdialog" unless attr_set?(:role)
|
|
34
34
|
panel_attrs[:"aria-modal"] = "true" unless aria_key_set?(:modal)
|
|
35
|
-
panel_attrs[:tabindex] = "-1" unless
|
|
35
|
+
panel_attrs[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
36
36
|
div(**mix(panel_attrs, @attrs), &block)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -144,9 +144,11 @@ export default class extends Controller {
|
|
|
144
144
|
const end = this.parseDate(this.rangeEndValue);
|
|
145
145
|
|
|
146
146
|
if (!start || (start && end)) {
|
|
147
|
-
// begin a fresh range
|
|
147
|
+
// begin a fresh range. undefined (not null) clears the value: Stimulus
|
|
148
|
+
// removes the attribute for undefined, while null goes through
|
|
149
|
+
// setAttribute and serializes as the literal string "null".
|
|
148
150
|
this.rangeStartValue = this.isoDate(candidate);
|
|
149
|
-
this.rangeEndValue =
|
|
151
|
+
this.rangeEndValue = undefined;
|
|
150
152
|
} else if (candidate < start) {
|
|
151
153
|
this.rangeStartValue = this.isoDate(candidate);
|
|
152
154
|
} else {
|
|
@@ -182,9 +184,10 @@ export default class extends Controller {
|
|
|
182
184
|
return;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
// update the viewDateValue to the selected date's month
|
|
187
|
+
// update the viewDateValue to the selected date's month
|
|
186
188
|
const newViewDate = new Date(selectedDate);
|
|
187
189
|
newViewDate.setDate(2); // avoid month-length/timezone edges
|
|
190
|
+
this._viewRendered = true;
|
|
188
191
|
this.viewDateValue = this.isoDate(newViewDate);
|
|
189
192
|
|
|
190
193
|
this.updateCalendar();
|
|
@@ -197,6 +200,13 @@ export default class extends Controller {
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
viewDateValueChanged(value, prevValue) {
|
|
203
|
+
// Month-cross paths render synchronously right after writing
|
|
204
|
+
// viewDateValue (focus must land in the NEW grid); this async echo
|
|
205
|
+
// (MutationObserver) would render the same view a second time.
|
|
206
|
+
if (this._viewRendered) {
|
|
207
|
+
this._viewRendered = false;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
200
210
|
this.updateCalendar();
|
|
201
211
|
}
|
|
202
212
|
|
|
@@ -334,6 +344,7 @@ export default class extends Controller {
|
|
|
334
344
|
// below would hit the OLD month, and the target day (outside it) would be
|
|
335
345
|
// missing → early return, stranding focus on <body>. Drive the re-render
|
|
336
346
|
// ourselves and hand it the target day so ensureGridTabStop focuses it.
|
|
347
|
+
this._viewRendered = true;
|
|
337
348
|
this.viewDateValue = iso;
|
|
338
349
|
this.updateCalendar(iso);
|
|
339
350
|
return;
|
|
@@ -31,7 +31,7 @@ module PhlexKit
|
|
|
31
31
|
# Canvas content is invisible to AT — announce it as an image with at
|
|
32
32
|
# least a generic name; pass aria: { label: "…" } (or block fallback
|
|
33
33
|
# content) to describe the actual data.
|
|
34
|
-
base[:role] = "img" unless
|
|
34
|
+
base[:role] = "img" unless attr_set?(:role)
|
|
35
35
|
base[:aria] = { label: "Chart" } unless aria_labelled?
|
|
36
36
|
canvas(**mix(base, @attrs), &)
|
|
37
37
|
end
|
|
@@ -8,11 +8,25 @@ module PhlexKit
|
|
|
8
8
|
# `name:`/`value:`/`checked:`/`**on(...)` pass through via `mix`. Styled by
|
|
9
9
|
# `.pk-checkbox` (checkbox.css).
|
|
10
10
|
class Checkbox < BaseComponent
|
|
11
|
-
|
|
11
|
+
# include_hidden mirrors Rails' check_box (and PhlexKit::Switch): an
|
|
12
|
+
# unchecked box posts nothing, so a paired hidden field carries the
|
|
13
|
+
# unchecked value. Emitted only when a `name:` is present — and never for
|
|
14
|
+
# array-style names ("ids[]"), where an unchecked "0" would inject a
|
|
15
|
+
# bogus element into the collection param (use a single blank hidden for
|
|
16
|
+
# the whole collection instead, as Rails' collection helpers do).
|
|
17
|
+
def initialize(include_hidden: true, unchecked_value: "0", **attrs)
|
|
18
|
+
@include_hidden = include_hidden
|
|
19
|
+
@unchecked_value = unchecked_value
|
|
12
20
|
@attrs = attrs
|
|
13
21
|
end
|
|
14
22
|
|
|
15
23
|
def view_template
|
|
24
|
+
if @include_hidden && @attrs[:name] && !@attrs[:name].to_s.end_with?("[]")
|
|
25
|
+
# Disabled in lockstep with the checkbox (Rails' check_box idiom) —
|
|
26
|
+
# a disabled checkbox must not still post its unchecked value.
|
|
27
|
+
input(type: "hidden", name: @attrs[:name], value: @unchecked_value,
|
|
28
|
+
disabled: @attrs[:disabled] ? true : nil)
|
|
29
|
+
end
|
|
16
30
|
input(**mix({
|
|
17
31
|
type: :checkbox,
|
|
18
32
|
class: "pk-checkbox",
|
|
@@ -15,8 +15,8 @@ module PhlexKit
|
|
|
15
15
|
# Defaults only when the caller didn't supply their own — `mix` would
|
|
16
16
|
# fuse tabindex="0 -1" / role="region article" instead of overriding.
|
|
17
17
|
base = { class: "pk-codeblock", data: { syntax: @syntax } }
|
|
18
|
-
base[:tabindex] = "0" unless
|
|
19
|
-
base[:role] = "region" unless
|
|
18
|
+
base[:tabindex] = "0" unless attr_set?(:tabindex)
|
|
19
|
+
base[:role] = "region" unless attr_set?(:role)
|
|
20
20
|
# A region landmark without a name is an axe violation — default one
|
|
21
21
|
# from the syntax; a caller aria: { label: } replaces it (never fused).
|
|
22
22
|
unless aria_labelled?
|
|
@@ -354,6 +354,10 @@ export default class extends Controller {
|
|
|
354
354
|
if (!this.isOpen()) return
|
|
355
355
|
if (this.element.contains(event.target)) return
|
|
356
356
|
|
|
357
|
+
// Kit-wide dismiss contract: the combobox is POPOVER-family (shadcn
|
|
358
|
+
// builds it on Popover light-dismiss), so the dismissing outside click
|
|
359
|
+
// deliberately clicks through — no preventDefault. Only the MENU
|
|
360
|
+
// overlays (dropdown/context/menubar/select) swallow theirs.
|
|
357
361
|
this.closePopover()
|
|
358
362
|
}
|
|
359
363
|
|
|
@@ -49,6 +49,7 @@ export default class extends Controller {
|
|
|
49
49
|
// itemTargetConnected uses must be initialized here (mirrors
|
|
50
50
|
// combobox_controller.js).
|
|
51
51
|
this.itemIdCounter = 0;
|
|
52
|
+
this.orderCounter = 0;
|
|
52
53
|
this.searchEntries = [];
|
|
53
54
|
this.selectedIndex = -1;
|
|
54
55
|
}
|
|
@@ -83,6 +84,13 @@ export default class extends Controller {
|
|
|
83
84
|
return this.element.hasAttribute("data-phlex-kit--command-dialog-instance");
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Clones are appended as direct <body> children; the last stamped clone is
|
|
88
|
+
// the visually topmost overlay across all clone-based overlay families.
|
|
89
|
+
isTopmostClone() {
|
|
90
|
+
const clones = document.body.querySelectorAll(":scope > [data-pk-overlay-clone]");
|
|
91
|
+
return clones.length > 0 && clones[clones.length - 1] === this.element;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
// ARIA plumbing: aria-controls is wired to the listbox (mirrors
|
|
87
95
|
// select_controller.js); the per-result ids come from itemTargetConnected.
|
|
88
96
|
generateItemIds() {
|
|
@@ -103,6 +111,10 @@ export default class extends Controller {
|
|
|
103
111
|
// arrival (mirrors combobox_controller.js).
|
|
104
112
|
itemTargetConnected(item) {
|
|
105
113
|
if (!item.id) item.id = `${this.listId()}-${this.itemIdCounter++}`;
|
|
114
|
+
// Original position, stamped once — filter() reorders rows by fuzzy
|
|
115
|
+
// score and resetVisibility() restores this order. Survives the
|
|
116
|
+
// disconnect/reconnect a DOM move itself triggers.
|
|
117
|
+
if (!item.dataset.pkOrder) item.dataset.pkOrder = String(this.orderCounter++);
|
|
106
118
|
this.searchEntries.push({
|
|
107
119
|
value: (item.dataset.value || "").toLowerCase(),
|
|
108
120
|
text: (item.dataset.text || "").toLowerCase(),
|
|
@@ -119,7 +131,22 @@ export default class extends Controller {
|
|
|
119
131
|
this.selectedIndex = -1;
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
dismiss() {
|
|
134
|
+
dismiss(e) {
|
|
135
|
+
if (e?.type === "keydown" && this.isDialogClone()) {
|
|
136
|
+
// The backdrop's Escape binding is window-scoped, so it fires even
|
|
137
|
+
// with another overlay (alert dialog, sheet) stacked ON TOP of the
|
|
138
|
+
// palette — those clones are later <body> siblings carrying the same
|
|
139
|
+
// [data-pk-overlay-clone] marker. Only the topmost clone may act on
|
|
140
|
+
// Escape (mirrors alert_dialog_controller.js#topmost).
|
|
141
|
+
if (!this.isTopmostClone()) return;
|
|
142
|
+
// The input's Escape binding is element-scoped and fires BEFORE lower
|
|
143
|
+
// overlays' document/window listeners — stop the keydown here, or by
|
|
144
|
+
// the time they run this clone (and its marker) is already removed and
|
|
145
|
+
// their own topmost check would wrongly elect them (mirrors
|
|
146
|
+
// sheet_content_controller.js's Escape guard).
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
}
|
|
149
|
+
|
|
123
150
|
// Cloned dialog overlay: tear the clone down and hand focus back.
|
|
124
151
|
if (this.isDialogClone()) {
|
|
125
152
|
// Dispatched BEFORE teardown: the command-dialog controller listens on
|
|
@@ -210,6 +237,9 @@ export default class extends Controller {
|
|
|
210
237
|
results.forEach((result) =>
|
|
211
238
|
this.toggleVisibility([result.item.element], true),
|
|
212
239
|
);
|
|
240
|
+
// cmdk parity: the best match lists first. Within each parent only —
|
|
241
|
+
// groups keep their identity, matches never jump between them.
|
|
242
|
+
this.reorderWithinParents(results.map((r) => r.item.element));
|
|
213
243
|
|
|
214
244
|
this.announceResultCount(results.length);
|
|
215
245
|
this.toggleVisibility(this.emptyTargets, results.length === 0);
|
|
@@ -248,6 +278,35 @@ export default class extends Controller {
|
|
|
248
278
|
this.toggleVisibility(this.groupTargets, true);
|
|
249
279
|
this.toggleVisibility(this.separatorTargets, true);
|
|
250
280
|
this.toggleVisibility(this.emptyTargets, false);
|
|
281
|
+
// Undo any fuzzy-score reordering: back to server-rendered order.
|
|
282
|
+
this.reorderWithinParents(
|
|
283
|
+
[...this.itemTargets].sort((a, b) => Number(a.dataset.pkOrder) - Number(b.dataset.pkOrder)),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Re-inserts `orderedItems` in the given sequence, each within its own
|
|
288
|
+
// parent, at the position of that parent's first item row — headings,
|
|
289
|
+
// separators and empty-state siblings keep their places. (Items are
|
|
290
|
+
// assumed contiguous within a parent; a separator BETWEEN items of one
|
|
291
|
+
// parent would drift below them.) The moves retrigger item target
|
|
292
|
+
// disconnect/connect, which is safe: ids and pkOrder are stamped once,
|
|
293
|
+
// and filter()/dismiss() have already reset the highlight.
|
|
294
|
+
reorderWithinParents(orderedItems) {
|
|
295
|
+
const itemSet = new Set(this.itemTargets);
|
|
296
|
+
const byParent = new Map();
|
|
297
|
+
orderedItems.forEach((el) => {
|
|
298
|
+
const parent = el.parentElement;
|
|
299
|
+
if (!parent) return;
|
|
300
|
+
if (!byParent.has(parent)) byParent.set(parent, []);
|
|
301
|
+
byParent.get(parent).push(el);
|
|
302
|
+
});
|
|
303
|
+
byParent.forEach((els, parent) => {
|
|
304
|
+
const firstItem = [...parent.children].find((c) => itemSet.has(c));
|
|
305
|
+
const placeholder = document.createComment("");
|
|
306
|
+
parent.insertBefore(placeholder, firstItem || null);
|
|
307
|
+
els.forEach((el) => parent.insertBefore(el, placeholder));
|
|
308
|
+
placeholder.remove();
|
|
309
|
+
});
|
|
251
310
|
}
|
|
252
311
|
|
|
253
312
|
// Upstream builds a Fuse index here; this keeps the same result shape —
|
|
@@ -17,9 +17,14 @@ module PhlexKit
|
|
|
17
17
|
template(data: { phlex_kit__command_dialog_target: "content" }) do
|
|
18
18
|
# The keydown action is the focus trap: Tab cycles within the cloned
|
|
19
19
|
# overlay instead of escaping to the page underneath.
|
|
20
|
+
# data-pk-overlay-clone is the common marker stamped by every
|
|
21
|
+
# clone-based overlay family (see alert_dialog_controller.js#topmost):
|
|
22
|
+
# it lets stacked overlays of DIFFERENT types resolve which one is
|
|
23
|
+
# topmost, so one Escape never closes both layers at once.
|
|
20
24
|
div(data: {
|
|
21
25
|
controller: "phlex-kit--command",
|
|
22
26
|
phlex_kit__command_dialog_instance: true,
|
|
27
|
+
pk_overlay_clone: "",
|
|
23
28
|
action: "keydown->phlex-kit--command#trapFocus"
|
|
24
29
|
}) do
|
|
25
30
|
backdrop
|
|
@@ -8,6 +8,18 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
8
8
|
// native [popover=manual] in the top layer (context_menu.css); it is placed
|
|
9
9
|
// in viewport coordinates after showPopover() — a hidden popover has no
|
|
10
10
|
// size — and clamped so it never overflows the viewport (no floating-ui).
|
|
11
|
+
|
|
12
|
+
// One-shot capture listener swallowing the click an outside mousedown is
|
|
13
|
+
// about to produce (see onDocMousedown; same helper in dropdown/menubar —
|
|
14
|
+
// duplicated per controller by design). {once} self-removes; the DOM
|
|
15
|
+
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
16
|
+
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
17
|
+
// one — rare enough to accept.
|
|
18
|
+
const swallowClick = (ev) => ev.preventDefault()
|
|
19
|
+
function armSwallowClick() {
|
|
20
|
+
window.addEventListener("click", swallowClick, { once: true, capture: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export default class extends Controller {
|
|
12
24
|
static targets = ["content", "trigger", "menuItem"]
|
|
13
25
|
|
|
@@ -22,6 +34,17 @@ export default class extends Controller {
|
|
|
22
34
|
if (e.type === "contextmenu" && this.element.contains(e.target)) return
|
|
23
35
|
this.close()
|
|
24
36
|
}
|
|
37
|
+
// Kit-wide dismiss contract (Radix modal-menu parity): a plain click
|
|
38
|
+
// outside only dismisses — swallowed, like dropdown/menubar/select.
|
|
39
|
+
// Armed at MOUSEDOWN: the gesture's focusout close() removes onDoc
|
|
40
|
+
// before the click even fires. Right-button mousedowns are ignored so a
|
|
41
|
+
// contextmenu elsewhere stays click-through (that's how a second kit —
|
|
42
|
+
// or the native — context menu opens in the same gesture).
|
|
43
|
+
this.onDocMousedown = (e) => {
|
|
44
|
+
if (this.contentTarget.contains(e.target)) return
|
|
45
|
+
if (e.button !== 0) return
|
|
46
|
+
armSwallowClick()
|
|
47
|
+
}
|
|
25
48
|
this.onKey = (e) => this.keydown(e)
|
|
26
49
|
// Turbo snapshots BEFORE disconnect — close now or the snapshot keeps a
|
|
27
50
|
// stale data-state="open" (the panel itself can't resurrect, but host
|
|
@@ -47,6 +70,7 @@ export default class extends Controller {
|
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
disconnect() {
|
|
73
|
+
document.removeEventListener("mousedown", this.onDocMousedown)
|
|
50
74
|
document.removeEventListener("click", this.onDoc)
|
|
51
75
|
document.removeEventListener("contextmenu", this.onDoc)
|
|
52
76
|
document.removeEventListener("keydown", this.onKey)
|
|
@@ -95,6 +119,7 @@ export default class extends Controller {
|
|
|
95
119
|
c.style.left = `${x}px`
|
|
96
120
|
c.style.top = `${y}px`
|
|
97
121
|
c.dataset.state = "open"
|
|
122
|
+
document.addEventListener("mousedown", this.onDocMousedown)
|
|
98
123
|
document.addEventListener("click", this.onDoc)
|
|
99
124
|
document.addEventListener("contextmenu", this.onDoc)
|
|
100
125
|
document.addEventListener("keydown", this.onKey)
|
|
@@ -109,6 +134,7 @@ export default class extends Controller {
|
|
|
109
134
|
if (opts?.target?.closest?.('a[href="#"]')) opts.preventDefault()
|
|
110
135
|
if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover()
|
|
111
136
|
this.contentTarget.dataset.state = "closed"
|
|
137
|
+
document.removeEventListener("mousedown", this.onDocMousedown)
|
|
112
138
|
document.removeEventListener("click", this.onDoc)
|
|
113
139
|
document.removeEventListener("contextmenu", this.onDoc)
|
|
114
140
|
document.removeEventListener("keydown", this.onKey)
|
|
@@ -66,7 +66,13 @@ export default class extends Controller {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (this.hasSelectionSummaryTarget) {
|
|
69
|
-
|
|
69
|
+
// The component stamps its (possibly localized) %{selected}/%{total}
|
|
70
|
+
// template as data-format; live updates interpolate the same string
|
|
71
|
+
// the server rendered.
|
|
72
|
+
const format = this.selectionSummaryTarget.dataset.format || "%{selected} of %{total} row(s) selected.";
|
|
73
|
+
this.selectionSummaryTarget.textContent = format
|
|
74
|
+
.replace("%{selected}", String(selected))
|
|
75
|
+
.replace("%{total}", String(total));
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
if (this.hasBulkActionsTarget) {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# "N of M row(s) selected." line, kept current by the controller.
|
|
3
|
+
# `format:` localizes it — a template with %{selected} and %{total}
|
|
4
|
+
# placeholders, used for the server render AND stamped on the element so
|
|
5
|
+
# the controller's live updates interpolate the same string.
|
|
3
6
|
# See data_table.rb.
|
|
4
7
|
class DataTableSelectionSummary < BaseComponent
|
|
5
|
-
|
|
8
|
+
DEFAULT_FORMAT = "%{selected} of %{total} row(s) selected."
|
|
9
|
+
|
|
10
|
+
def initialize(total_on_page: 0, format: DEFAULT_FORMAT, **attrs)
|
|
6
11
|
@total_on_page = total_on_page
|
|
12
|
+
@format = format
|
|
7
13
|
@attrs = attrs
|
|
8
14
|
end
|
|
9
15
|
|
|
@@ -11,9 +17,12 @@ module PhlexKit
|
|
|
11
17
|
div(**mix({
|
|
12
18
|
class: "pk-data-table-selection-summary",
|
|
13
19
|
role: "status", # live region — selection changes are announced to AT
|
|
14
|
-
data: {
|
|
20
|
+
data: {
|
|
21
|
+
phlex_kit__data_table_target: "selectionSummary",
|
|
22
|
+
format: @format
|
|
23
|
+
}
|
|
15
24
|
}, @attrs)) do
|
|
16
|
-
plain
|
|
25
|
+
plain Kernel.format(@format, selected: 0, total: @total_on_page)
|
|
17
26
|
end
|
|
18
27
|
end
|
|
19
28
|
end
|
|
@@ -13,11 +13,9 @@ export default class extends Controller {
|
|
|
13
13
|
onCalendarChange(e) {
|
|
14
14
|
const { mode, rangeEnd } = e.detail
|
|
15
15
|
if (mode === "multiple") return
|
|
16
|
-
// calendar_controller.js clears rangeEnd
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
// back as that string, not JS null/undefined/"".
|
|
20
|
-
if (mode === "range" && (!rangeEnd || rangeEnd === "null")) return
|
|
16
|
+
// calendar_controller.js clears rangeEnd by assigning undefined (Stimulus
|
|
17
|
+
// removes the attribute), so an unset rangeEnd reads back as null here.
|
|
18
|
+
if (mode === "range" && !rangeEnd) return
|
|
21
19
|
this.popoverController()?.close()
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -18,9 +18,9 @@ module PhlexKit
|
|
|
18
18
|
# Defaults only when the caller didn't supply their own — `mix`
|
|
19
19
|
# would fuse role="dialog region" / aria-modal="true false" /
|
|
20
20
|
# tabindex="-1 0" instead of overriding.
|
|
21
|
-
panel_attrs[:role] = "dialog" unless
|
|
21
|
+
panel_attrs[:role] = "dialog" unless attr_set?(:role)
|
|
22
22
|
panel_attrs[:aria] = { modal: "true" } unless aria_key_set?(:modal)
|
|
23
|
-
panel_attrs[:tabindex] = "-1" unless
|
|
23
|
+
panel_attrs[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
24
24
|
div(**mix(panel_attrs, @attrs)) do
|
|
25
25
|
div(class: "pk-drawer-handle", aria: { hidden: "true" })
|
|
26
26
|
yield if block
|
|
@@ -20,7 +20,9 @@ module PhlexKit
|
|
|
20
20
|
phlex_kit__dropdown_menu_open_value: @open,
|
|
21
21
|
# focusout: tabbing (or otherwise moving real focus) out of the
|
|
22
22
|
# menu closes the open [popover=manual] panel (menubar's pattern).
|
|
23
|
-
|
|
23
|
+
# mousedown@window arms the modal-menu click swallow before the
|
|
24
|
+
# gesture's focusout closes the panel (see the controller).
|
|
25
|
+
action: "mousedown@window->phlex-kit--dropdown-menu#onMousedownOutside click@window->phlex-kit--dropdown-menu#onClickOutside focusout->phlex-kit--dropdown-menu#onFocusout"
|
|
24
26
|
}
|
|
25
27
|
}, @attrs), &block)
|
|
26
28
|
end
|
|
@@ -10,6 +10,18 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
10
10
|
// sub-rows are skipped via a visibility filter. Escape returns focus to the
|
|
11
11
|
// trigger; Enter/Space activate the focused row (links, checkbox/radio
|
|
12
12
|
// labels, and sub triggers alike).
|
|
13
|
+
|
|
14
|
+
// One-shot capture listener swallowing the click an outside mousedown is
|
|
15
|
+
// about to produce (see onMousedownOutside; same helper in menubar/context
|
|
16
|
+
// menu — duplicated per controller by design). {once} self-removes; the DOM
|
|
17
|
+
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
18
|
+
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
19
|
+
// one — rare enough to accept.
|
|
20
|
+
const swallowClick = (ev) => ev.preventDefault();
|
|
21
|
+
function armSwallowClick() {
|
|
22
|
+
window.addEventListener("click", swallowClick, { once: true, capture: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export default class extends Controller {
|
|
14
26
|
static targets = ["trigger", "content", "menuItem"];
|
|
15
27
|
static values = { open: { type: Boolean, default: false } };
|
|
@@ -69,16 +81,23 @@ export default class extends Controller {
|
|
|
69
81
|
this.subFrames.add(id);
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
// Deliberate (matches Radix's modal dismiss): the dismissing outside
|
|
85
|
+
// click ONLY dismisses the menu — swallowed rather than also acting on
|
|
86
|
+
// whatever was under the pointer (e.g. navigating a link). Armed at
|
|
87
|
+
// MOUSEDOWN: for a focusable click target the same gesture's focusout
|
|
88
|
+
// closes the panel before the click fires, so a click-time swallow
|
|
89
|
+
// silently skipped those targets (audit round 8).
|
|
90
|
+
onMousedownOutside(event) {
|
|
91
|
+
if (!this.contentTarget.matches(":popover-open")) return;
|
|
92
|
+
if (this.element.contains(event.target)) return;
|
|
93
|
+
armSwallowClick();
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
onClickOutside(event) {
|
|
73
97
|
// Gate on the live :popover-open state, not the stored flag — a stale
|
|
74
98
|
// flag is how a close on an already-closed panel becomes an open.
|
|
75
99
|
if (!this.contentTarget.matches(":popover-open")) return;
|
|
76
100
|
if (this.element.contains(event.target)) return;
|
|
77
|
-
|
|
78
|
-
// Deliberate (matches Radix's modal dismiss): the outside click ONLY
|
|
79
|
-
// dismisses the menu — it is swallowed rather than also acting on
|
|
80
|
-
// whatever was under the pointer (e.g. navigating a link).
|
|
81
|
-
event.preventDefault();
|
|
82
101
|
this.close();
|
|
83
102
|
}
|
|
84
103
|
|
|
@@ -24,7 +24,7 @@ module PhlexKit
|
|
|
24
24
|
# `mix` merges duplicate string attrs ("16 24") — drop a generated attr
|
|
25
25
|
# whenever the caller supplies their own copy, so theirs wins.
|
|
26
26
|
%i[width height viewbox aria-hidden].each do |key|
|
|
27
|
-
base.delete(key) if
|
|
27
|
+
base.delete(key) if attr_set?(key)
|
|
28
28
|
end
|
|
29
29
|
svg(**mix(base, @attrs)) do |s|
|
|
30
30
|
icon[:elements].each { |tag, attrs| s.public_send(tag, **attrs) }
|
|
@@ -36,11 +36,20 @@ module PhlexKit
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def view_template(&block)
|
|
39
|
-
|
|
39
|
+
base = { href: @href, class: classes }
|
|
40
|
+
# target="_blank" hands the new page a window.opener reference in older
|
|
41
|
+
# browsers (modern ones imply noopener) — default the safe rel unless
|
|
42
|
+
# the caller supplied their own (`mix` would fuse "noopener external").
|
|
43
|
+
base[:rel] = "noopener" if blank_target? && !attr_set?(:rel)
|
|
44
|
+
a(**mix(base, @attrs), &block)
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
private
|
|
43
48
|
|
|
49
|
+
def blank_target?
|
|
50
|
+
(@attrs[:target] || @attrs["target"]).to_s == "_blank"
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
def classes
|
|
45
54
|
[ "pk-button", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size), ("icon" if @icon) ].compact.join(" ")
|
|
46
55
|
end
|
|
@@ -18,7 +18,9 @@ module PhlexKit
|
|
|
18
18
|
controller: "phlex-kit--menubar",
|
|
19
19
|
# focusout: tabbing out of the bar closes the open [popover=manual]
|
|
20
20
|
# panel (which no focus trap holds open).
|
|
21
|
-
|
|
21
|
+
# mousedown@window arms the modal-menu click swallow (deliberately
|
|
22
|
+
# NOT bound by navigation_menu.rb — nav menus are non-modal).
|
|
23
|
+
action: "mousedown@window->phlex-kit--menubar#onMousedownOutside click@window->phlex-kit--menubar#onClickOutside keydown->phlex-kit--menubar#onKeydown focusout->phlex-kit--menubar#onFocusout"
|
|
22
24
|
}
|
|
23
25
|
}, @attrs), &)
|
|
24
26
|
end
|
|
@@ -13,6 +13,17 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
13
13
|
// Connects to data-controller="phlex-kit--menubar"
|
|
14
14
|
let uid = 0
|
|
15
15
|
|
|
16
|
+
// One-shot capture listener swallowing the click an outside mousedown is
|
|
17
|
+
// about to produce (see onMousedownOutside; same helper in dropdown/context
|
|
18
|
+
// menu — duplicated per controller by design). {once} self-removes; the DOM
|
|
19
|
+
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
20
|
+
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
21
|
+
// one — rare enough to accept.
|
|
22
|
+
const swallowClick = (ev) => ev.preventDefault()
|
|
23
|
+
function armSwallowClick() {
|
|
24
|
+
window.addEventListener("click", swallowClick, { once: true, capture: true })
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export default class extends Controller {
|
|
17
28
|
static targets = ["menu"]
|
|
18
29
|
|
|
@@ -131,6 +142,21 @@ export default class extends Controller {
|
|
|
131
142
|
if (opts.refocus === true) trigger?.focus()
|
|
132
143
|
}
|
|
133
144
|
|
|
145
|
+
// Kit-wide dismiss contract (Radix parity): MENU overlays (dropdown,
|
|
146
|
+
// context, menubar, select) are modal — the dismissing outside click is
|
|
147
|
+
// swallowed rather than also acting on what was under the pointer. The
|
|
148
|
+
// decision happens at MOUSEDOWN: the same gesture's focusout closes the
|
|
149
|
+
// panel before the click event fires, so a click-time openMenu check
|
|
150
|
+
// would already be null (that race made the old swallow silently skip
|
|
151
|
+
// focusable click targets). Only menubar.rb binds this action — the
|
|
152
|
+
// navigation menu (hover-open mode) is non-modal like Radix's and its
|
|
153
|
+
// dismissing click clicks through.
|
|
154
|
+
onMousedownOutside(e) {
|
|
155
|
+
if (this.element.contains(e.target)) return
|
|
156
|
+
if (!this.openMenu) return
|
|
157
|
+
armSwallowClick()
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
onClickOutside(e) {
|
|
135
161
|
if (this.element.contains(e.target)) return
|
|
136
162
|
this.close()
|
|
@@ -23,3 +23,22 @@
|
|
|
23
23
|
.pk-progress:dir(rtl) .pk-progress-indicator {
|
|
24
24
|
transform: translateX(calc((100 - var(--pk-progress-value, 0)) * 1%));
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/* Indeterminate (value: nil): a 40%-wide segment sweeps the track.
|
|
28
|
+
translateX % is relative to the segment's own width: -100% parks it just
|
|
29
|
+
off the track's start edge, +250% (track = 250% of segment) just past the
|
|
30
|
+
end. Direction-agnostic — the sweep reads fine mirrored, no RTL arm. */
|
|
31
|
+
.pk-progress.indeterminate .pk-progress-indicator {
|
|
32
|
+
width: 40%;
|
|
33
|
+
transition: none;
|
|
34
|
+
animation: pk-progress-indeterminate 1.5s ease-in-out infinite;
|
|
35
|
+
}
|
|
36
|
+
@keyframes pk-progress-indeterminate {
|
|
37
|
+
from { transform: translateX(-100%); }
|
|
38
|
+
to { transform: translateX(250%); }
|
|
39
|
+
}
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
.pk-progress.indeterminate .pk-progress-indicator {
|
|
42
|
+
animation-duration: 3s;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -6,29 +6,45 @@ module PhlexKit
|
|
|
6
6
|
class Progress < BaseComponent
|
|
7
7
|
# value_text: is a named kwarg (not read from **attrs) because `mix`
|
|
8
8
|
# would *fuse* a caller "aria-valuetext" with the generated default.
|
|
9
|
+
# `value: nil` renders the INDETERMINATE state (Radix parity): a sliding
|
|
10
|
+
# animation, data-state="indeterminate", and no aria-valuenow — which is
|
|
11
|
+
# exactly how ARIA marks a progressbar of unknown value.
|
|
9
12
|
def initialize(value: 0, value_text: nil, **attrs)
|
|
10
|
-
@value = value.to_f.clamp(0.0, 100.0)
|
|
13
|
+
@value = value.nil? ? nil : value.to_f.clamp(0.0, 100.0)
|
|
11
14
|
@value_text = value_text
|
|
12
15
|
@attrs = attrs
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def view_template
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
div(**mix(root_attrs, @attrs)) do
|
|
20
|
+
div(class: "pk-progress-indicator")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def root_attrs
|
|
27
|
+
base = {
|
|
22
28
|
role: "progressbar",
|
|
23
|
-
"aria-valuenow": v,
|
|
24
29
|
"aria-valuemin": 0,
|
|
25
30
|
"aria-valuemax": 100,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
class: "pk-progress"
|
|
32
|
+
}
|
|
33
|
+
if @value.nil?
|
|
34
|
+
base[:class] = "pk-progress indeterminate"
|
|
35
|
+
base[:data] = { state: "indeterminate" }
|
|
36
|
+
base[:"aria-valuetext"] = @value_text if @value_text
|
|
37
|
+
else
|
|
38
|
+
# %g drops float artifacts ("40.0" would be announced as "forty point
|
|
39
|
+
# zero percent"). The value travels as a custom property so the fill
|
|
40
|
+
# transform lives in progress.css, where a :dir(rtl) arm can flip it —
|
|
41
|
+
# an inline translateX always filled from the physical left.
|
|
42
|
+
v = Kernel.format("%g", @value)
|
|
43
|
+
base[:"aria-valuenow"] = v
|
|
44
|
+
base[:"aria-valuetext"] = @value_text || "#{v}%"
|
|
45
|
+
base[:style] = "--pk-progress-value: #{v};"
|
|
31
46
|
end
|
|
47
|
+
base
|
|
32
48
|
end
|
|
33
49
|
end
|
|
34
50
|
end
|
|
@@ -15,7 +15,7 @@ module PhlexKit
|
|
|
15
15
|
# Defaults only when the caller didn't supply their own — `mix` would
|
|
16
16
|
# fuse role="region log" / tabindex="0 -1" instead of overriding.
|
|
17
17
|
base = { class: "pk-scroll-area" }
|
|
18
|
-
base[:tabindex] = "0" unless
|
|
18
|
+
base[:tabindex] = "0" unless attr_set?(:tabindex)
|
|
19
19
|
base[:role] = "region" if aria_labelled? && !@attrs.key?(:role) && !@attrs.key?("role")
|
|
20
20
|
div(**mix(base, @attrs), &)
|
|
21
21
|
end
|
|
@@ -18,9 +18,9 @@ module PhlexKit
|
|
|
18
18
|
# Defaults only when the caller didn't supply their own — `mix`
|
|
19
19
|
# would fuse role="dialog region" / aria-modal="true false" /
|
|
20
20
|
# tabindex="-1 0" instead of overriding.
|
|
21
|
-
panel_attrs[:role] = "dialog" unless
|
|
21
|
+
panel_attrs[:role] = "dialog" unless attr_set?(:role)
|
|
22
22
|
panel_attrs[:aria] = { modal: "true" } unless aria_key_set?(:modal)
|
|
23
|
-
panel_attrs[:tabindex] = "-1" unless
|
|
23
|
+
panel_attrs[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
24
24
|
div(**mix(panel_attrs, @attrs)) do
|
|
25
25
|
block&.call
|
|
26
26
|
if @show_close_button
|
|
@@ -9,7 +9,7 @@ module PhlexKit
|
|
|
9
9
|
# value: is silently clobbered below (the checkbox's real value comes
|
|
10
10
|
# from checked_value:) — fail loud rather than let a caller's value:
|
|
11
11
|
# vanish (mirrors ToggleGroupItem's unsupported-kwargs guard).
|
|
12
|
-
if attrs.key?(:value)
|
|
12
|
+
if attrs.key?(:value) || attrs.key?("value")
|
|
13
13
|
raise ArgumentError, "Switch does not support value: — the checkbox's value comes from checked_value:, pass that instead"
|
|
14
14
|
end
|
|
15
15
|
@include_hidden = include_hidden
|
|
@@ -208,7 +208,12 @@ export default class extends Controller {
|
|
|
208
208
|
this.element.dataset.swipe = "cancel"
|
|
209
209
|
this.element.style.removeProperty("--pk-toast-swipe-x")
|
|
210
210
|
this.element.style.removeProperty("--pk-toast-swipe-y")
|
|
211
|
-
|
|
211
|
+
// pointerleave is suppressed while a swipe is active, so release the
|
|
212
|
+
// hover hold here — but only if the pointer actually left the toast; a
|
|
213
|
+
// cancelled swipe ending under the cursor must stay hover-paused (the
|
|
214
|
+
// eventual pointerleave resumes it, including the touch case where
|
|
215
|
+
// pointerleave fires right after pointerup).
|
|
216
|
+
if (!this.element.matches(":hover")) this._resume("hover")
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
}
|
|
@@ -60,10 +60,10 @@ module PhlexKit
|
|
|
60
60
|
# Defaults only when the caller didn't supply their own — `mix` would
|
|
61
61
|
# fuse role="status log" / tabindex="0 -1" / aria-atomic="true false"
|
|
62
62
|
# instead of overriding.
|
|
63
|
-
unless
|
|
63
|
+
unless attr_set?(:role)
|
|
64
64
|
attrs[:role] = ALERT_VARIANTS.include?(@variant) ? "alert" : "status"
|
|
65
65
|
end
|
|
66
|
-
attrs[:tabindex] = "0" unless
|
|
66
|
+
attrs[:tabindex] = "0" unless attr_set?(:tabindex)
|
|
67
67
|
unless aria_key_set?(:atomic)
|
|
68
68
|
attrs[:aria] = { atomic: "true" }
|
|
69
69
|
end
|
|
@@ -63,12 +63,26 @@ export default class extends Controller {
|
|
|
63
63
|
// _setExpanded's state change left auto-dismiss running under the cursor.
|
|
64
64
|
this._onPointerEnter = () => { this._setPaused(true); this._setExpanded(true) }
|
|
65
65
|
this._onPointerLeave = () => { this._setPaused(false); if (!this.expandValue) this._setExpanded(false) }
|
|
66
|
-
this._onWindowToast = (e) =>
|
|
66
|
+
this._onWindowToast = (e) => {
|
|
67
|
+
const detail = e.detail || {}
|
|
68
|
+
// Routing: toast(msg, { region: "<id>" }) targets the region whose
|
|
69
|
+
// wrapper or list carries that id. Without region only the page's
|
|
70
|
+
// FIRST region spawns — the event is a window broadcast, so every
|
|
71
|
+
// region hears it and an unguarded spawn would duplicate the toast
|
|
72
|
+
// once per region.
|
|
73
|
+
if (!this._handlesRegion(detail.region)) return
|
|
74
|
+
this._spawn(detail)
|
|
75
|
+
}
|
|
67
76
|
this._onWindowDismissAll = () => this._dismissById(null)
|
|
77
|
+
// Dismiss-by-id is a broadcast too: _dismissById only acts on a toast in
|
|
78
|
+
// its OWN list, so whichever region owns the id handles it — the api
|
|
79
|
+
// registration (last region wins) no longer has to be the owner.
|
|
80
|
+
this._onWindowDismissId = (e) => this._dismissById(e.detail?.id)
|
|
68
81
|
this._onKey = this._onKey.bind(this)
|
|
69
82
|
|
|
70
83
|
window.addEventListener("phlex-kit:toast", this._onWindowToast)
|
|
71
84
|
window.addEventListener("phlex-kit:toast:dismiss-all", this._onWindowDismissAll)
|
|
85
|
+
window.addEventListener("phlex-kit:toast:dismiss", this._onWindowDismissId)
|
|
72
86
|
this._listEl.addEventListener("pointerenter", this._onPointerEnter)
|
|
73
87
|
this._listEl.addEventListener("pointerleave", this._onPointerLeave)
|
|
74
88
|
document.addEventListener("keydown", this._onKey)
|
|
@@ -81,6 +95,7 @@ export default class extends Controller {
|
|
|
81
95
|
disconnect() {
|
|
82
96
|
window.removeEventListener("phlex-kit:toast", this._onWindowToast)
|
|
83
97
|
window.removeEventListener("phlex-kit:toast:dismiss-all", this._onWindowDismissAll)
|
|
98
|
+
window.removeEventListener("phlex-kit:toast:dismiss", this._onWindowDismissId)
|
|
84
99
|
this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
|
|
85
100
|
this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
|
|
86
101
|
document.removeEventListener("keydown", this._onKey)
|
|
@@ -165,6 +180,15 @@ export default class extends Controller {
|
|
|
165
180
|
return node.id
|
|
166
181
|
}
|
|
167
182
|
|
|
183
|
+
// True when this region should act on a toast event carrying `region`
|
|
184
|
+
// (the list's base id or the wrapper's "<id>-region" both match). Without
|
|
185
|
+
// an explicit region, only the page's first region in DOM order handles
|
|
186
|
+
// the broadcast — single-region pages are unaffected.
|
|
187
|
+
_handlesRegion(region) {
|
|
188
|
+
if (region) return region === this.element.id || region === this._listEl?.id
|
|
189
|
+
return this.element === document.querySelector("[data-controller~='phlex-kit--toaster']")
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
_dismissById(id) {
|
|
169
193
|
if (!id) {
|
|
170
194
|
this.toastTargets.forEach((el) =>
|
|
@@ -311,12 +335,14 @@ export default class extends Controller {
|
|
|
311
335
|
api.info = (m, o) => fire("info", m, o)
|
|
312
336
|
api.loading = (m, o = {}) => fire("loading", m, { ...o, duration: o.duration ?? 0 })
|
|
313
337
|
api.dismiss = (id) => {
|
|
314
|
-
|
|
338
|
+
// Both broadcasts: whichever region owns the id dismisses it — the
|
|
339
|
+
// api may be registered by a different region than the owner.
|
|
340
|
+
if (id) window.dispatchEvent(new CustomEvent("phlex-kit:toast:dismiss", { detail: { id } }))
|
|
315
341
|
else window.dispatchEvent(new CustomEvent("phlex-kit:toast:dismiss-all"))
|
|
316
342
|
}
|
|
317
343
|
api.promise = (p, msgs = {}) => {
|
|
318
344
|
const id = `toast-${this._uuid()}`
|
|
319
|
-
fire("loading", typeof msgs.loading === "function" ? msgs.loading() : (msgs.loading || "Loading..."), { id, duration: 0 })
|
|
345
|
+
fire("loading", typeof msgs.loading === "function" ? msgs.loading() : (msgs.loading || "Loading..."), { id, duration: 0, region: msgs.region })
|
|
320
346
|
Promise.resolve(p).then(
|
|
321
347
|
(val) => this._mutate(id, "success", typeof msgs.success === "function" ? msgs.success(val) : msgs.success),
|
|
322
348
|
(err) => this._mutate(id, "error", typeof msgs.error === "function" ? msgs.error(err) : msgs.error)
|
|
@@ -330,9 +356,11 @@ export default class extends Controller {
|
|
|
330
356
|
}
|
|
331
357
|
|
|
332
358
|
_mutate(id, variant, text) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
// Document-scoped: promise()'s loading toast spawns in whichever region
|
|
360
|
+
// the broadcast routed it to, which need not be the region that
|
|
361
|
+
// registered the api this closure belongs to.
|
|
362
|
+
const el = document.getElementById(id)
|
|
363
|
+
if (!el || !el.closest(".pk-toast-list")) return
|
|
336
364
|
el.dataset.variant = variant
|
|
337
365
|
el.setAttribute("role", variant === "error" ? "alert" : "status")
|
|
338
366
|
this._swapIcon(el, variant)
|
|
@@ -31,6 +31,15 @@ module PhlexKit
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# True when the caller supplied their own value for a plain attribute in
|
|
35
|
+
# @attrs — symbol or string spelling. A component with a generated default
|
|
36
|
+
# for that attribute must skip it then: `mix` merges duplicate attrs
|
|
37
|
+
# (role="dialog region") instead of overriding. Aria attributes have their
|
|
38
|
+
# own check (aria_key_set?) covering the aria: hash spelling too.
|
|
39
|
+
def attr_set?(key)
|
|
40
|
+
@attrs.key?(key) || @attrs.key?(key.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
# True when the caller supplied an accessible name in @attrs — via the
|
|
35
44
|
# aria: hash or flat aria_label/aria-label(ledby) keys. Components with a
|
|
36
45
|
# generated default label must skip it then: `mix` merges duplicate string
|
data/lib/phlex_kit/version.rb
CHANGED