phlex_kit 0.9.1 → 0.10.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 +8 -0
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +12 -0
- data/app/components/phlex_kit/attachment/attachment.css +6 -3
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +7 -2
- data/app/components/phlex_kit/avatar/avatar.css +2 -2
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +0 -4
- data/app/components/phlex_kit/bubble/bubble.css +1 -1
- data/app/components/phlex_kit/bubble/bubble_content.rb +4 -3
- data/app/components/phlex_kit/button_group/button_group.css +16 -16
- data/app/components/phlex_kit/calendar/calendar.css +10 -2
- data/app/components/phlex_kit/carousel/carousel.css +4 -4
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +18 -2
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +15 -6
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +5 -2
- data/app/components/phlex_kit/combobox/combobox.css +3 -3
- data/app/components/phlex_kit/combobox/combobox.rb +3 -3
- data/app/components/phlex_kit/combobox/combobox_controller.js +17 -9
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +3 -1
- data/app/components/phlex_kit/command/command.css +2 -2
- data/app/components/phlex_kit/command/command_controller.js +57 -34
- data/app/components/phlex_kit/command/command_dialog_controller.js +30 -1
- data/app/components/phlex_kit/command/command_group.rb +5 -2
- data/app/components/phlex_kit/dialog/dialog.css +2 -2
- data/app/components/phlex_kit/dialog/dialog_close.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +9 -0
- data/app/components/phlex_kit/drawer/drawer.css +4 -4
- data/app/components/phlex_kit/empty/empty.css +1 -1
- data/app/components/phlex_kit/field/field.css +2 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +7 -0
- data/app/components/phlex_kit/input_otp/input_otp.css +4 -4
- data/app/components/phlex_kit/marker/marker.rb +6 -0
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +17 -3
- data/app/components/phlex_kit/menubar/menubar.rb +3 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +22 -1
- data/app/components/phlex_kit/menubar/menubar_item.rb +4 -2
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +4 -2
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +9 -5
- data/app/components/phlex_kit/native_select/native_select.css +4 -2
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +4 -1
- data/app/components/phlex_kit/pagination/pagination_next.rb +9 -2
- data/app/components/phlex_kit/pagination/pagination_previous.rb +9 -2
- data/app/components/phlex_kit/popover/popover_controller.js +9 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +49 -5
- data/app/components/phlex_kit/resizable/resizable_panel.rb +13 -4
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +20 -4
- data/app/components/phlex_kit/select/select.css +0 -1
- data/app/components/phlex_kit/select/select_content.rb +9 -3
- data/app/components/phlex_kit/select/select_controller.js +32 -2
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/separator/separator.rb +7 -1
- data/app/components/phlex_kit/sheet/sheet_close.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar.css +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +10 -0
- data/app/components/phlex_kit/tabs/tabs.css +2 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +4 -1
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +4 -1
- data/app/components/phlex_kit/toast/toast.css +1 -1
- data/app/components/phlex_kit/toast/toast_controller.js +6 -1
- data/app/components/phlex_kit/toast/toaster_controller.js +19 -5
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +6 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +8 -0
- data/app/components/phlex_kit/tooltip/tooltip.css +2 -2
- data/app/components/phlex_kit/typography/heading.rb +6 -4
- 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: 9f20449d41b77f26b6a4272eb5f4ebd116a225af9bf2dcb6bfec5899a5b117a2
|
|
4
|
+
data.tar.gz: fd83a994ef1cc33452c19b8520934db31750babacb7508511dffb9c41b08a9dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f08a4051f646822945f9f1bf72b52cfce151509322032dccba9ec7166db4bf5a99bead12adeec4969edf0fc94df5b55dc902424322fa6a351abd18d92c04496b
|
|
7
|
+
data.tar.gz: 83b2a4ea332492c19ce059611e534fc57c79fa39e136e9bd00e2cf535fe09c791f75c0c81bc12056f2678f9a1bcf4bd18bd8246b79cd7377e0f990dbd4472c50
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/* Kit utilities used by the interactive controllers (Stimulus toggles these). */
|
|
2
2
|
.pk-hidden { display: none !important; }
|
|
3
3
|
|
|
4
|
+
/* Kit-wide screen-reader-only utility — used by many components
|
|
5
|
+
(breadcrumb, dialog, sheet, sidebar, …), so it lives here rather
|
|
6
|
+
than in any one component's stylesheet. */
|
|
7
|
+
.pk-sr-only {
|
|
8
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
9
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
/* Shimmer — shadcn's shimmer util: a highlight band sweeping across text, for
|
|
5
13
|
loading / "thinking…" states. Pure CSS, built on currentColor: the text keeps
|
|
6
14
|
its own color (any color) and the highlight derives from it via relative
|
|
@@ -15,7 +15,7 @@ module PhlexKit
|
|
|
15
15
|
# shadcn ships default/destructive; success/warning are kit extras kept from
|
|
16
16
|
# ruby_ui, restyled to the same text-tint grammar. `VARIANTS.fetch` fails loud.
|
|
17
17
|
class Alert < BaseComponent
|
|
18
|
-
# variant => extra class appended after the base `
|
|
18
|
+
# variant => extra class appended after the base `pk-alert` (nil = neutral).
|
|
19
19
|
VARIANTS = {
|
|
20
20
|
default: nil,
|
|
21
21
|
warning: "warning",
|
|
@@ -68,6 +68,10 @@ export default class extends Controller {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
keydown(event) {
|
|
71
|
+
// Every open clone has its own document-level listener; with stacked
|
|
72
|
+
// alert dialogs (a trigger inside a panel) only the topmost clone may
|
|
73
|
+
// handle the keyboard, or one Escape would close every layer at once.
|
|
74
|
+
if (!this.#topmost()) return;
|
|
71
75
|
if (event.key === "Escape") {
|
|
72
76
|
event.preventDefault();
|
|
73
77
|
this.dismiss();
|
|
@@ -97,6 +101,14 @@ export default class extends Controller {
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
// Clones are appended as direct <body> children whose direct child is the
|
|
105
|
+
// [role="alertdialog"] panel (source elements only hold a <template>, whose
|
|
106
|
+
// content never matches querySelector). The last such clone is the topmost.
|
|
107
|
+
#topmost() {
|
|
108
|
+
const clones = document.body.querySelectorAll(':scope > [data-controller~="phlex-kit--alert-dialog"] > [role="alertdialog"]');
|
|
109
|
+
return clones.length > 0 && clones[clones.length - 1].parentElement === this.element;
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
#panel() {
|
|
101
113
|
return this.element.querySelector('[role="alertdialog"]');
|
|
102
114
|
}
|
|
@@ -142,21 +142,24 @@
|
|
|
142
142
|
/* Scrollable, snapping row with an edge fade (horizontal scroll-fade). */
|
|
143
143
|
.pk-attachment-group {
|
|
144
144
|
--pk-scroll-fade-size: 2rem;
|
|
145
|
+
/* Gradient directions can't be logical — follow the inline axis by hand. */
|
|
146
|
+
--pk-scroll-fade-dir: to right;
|
|
145
147
|
display: flex;
|
|
146
148
|
gap: .5rem;
|
|
147
149
|
overflow-x: auto;
|
|
148
150
|
scroll-snap-type: x mandatory;
|
|
149
151
|
padding: 2px; /* room for card rings */
|
|
150
|
-
mask-image: linear-gradient(
|
|
152
|
+
mask-image: linear-gradient(var(--pk-scroll-fade-dir),
|
|
151
153
|
transparent 0, black var(--pk-scroll-fade-size),
|
|
152
154
|
black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
|
|
153
155
|
}
|
|
156
|
+
.pk-attachment-group:dir(rtl) { --pk-scroll-fade-dir: to left; }
|
|
154
157
|
.pk-attachment-group[data-at-start] {
|
|
155
|
-
mask-image: linear-gradient(
|
|
158
|
+
mask-image: linear-gradient(var(--pk-scroll-fade-dir),
|
|
156
159
|
black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
|
|
157
160
|
}
|
|
158
161
|
.pk-attachment-group[data-at-end] {
|
|
159
|
-
mask-image: linear-gradient(
|
|
162
|
+
mask-image: linear-gradient(var(--pk-scroll-fade-dir), transparent 0, black var(--pk-scroll-fade-size));
|
|
160
163
|
}
|
|
161
164
|
.pk-attachment-group[data-at-start][data-at-end] { mask-image: none; }
|
|
162
165
|
.pk-attachment-group > .pk-attachment { flex: none; scroll-snap-align: start; }
|
|
@@ -29,8 +29,13 @@ export default class extends Controller {
|
|
|
29
29
|
update() {
|
|
30
30
|
const el = this.element
|
|
31
31
|
if (this.axisValue === "x") {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Math.abs: in RTL scrollLeft runs 0..negative, so the raw value would
|
|
33
|
+
// read as permanently "at start" — abs tracks the logical inline start.
|
|
34
|
+
const scrolled = Math.abs(el.scrollLeft)
|
|
35
|
+
// <= 2, not <= 1: scroll-snap + the 2px ring padding leave the group
|
|
36
|
+
// resting at scrollLeft 2, which must still count as "at start".
|
|
37
|
+
el.toggleAttribute("data-at-start", scrolled <= 2)
|
|
38
|
+
el.toggleAttribute("data-at-end", scrolled + el.clientWidth >= el.scrollWidth - 1)
|
|
34
39
|
} else {
|
|
35
40
|
el.toggleAttribute("data-at-start", el.scrollTop <= 1)
|
|
36
41
|
el.toggleAttribute("data-at-end", el.scrollTop + el.clientHeight >= el.scrollHeight - 1)
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
modifier, glyph hidden when there's no room (shadcn's AvatarBadge). */
|
|
68
68
|
.pk-avatar-badge {
|
|
69
69
|
position: absolute;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
inset-inline-end: 0;
|
|
71
|
+
inset-block-end: 0;
|
|
72
72
|
z-index: 10;
|
|
73
73
|
display: inline-flex;
|
|
74
74
|
align-items: center;
|
|
@@ -12,7 +12,3 @@
|
|
|
12
12
|
.pk-breadcrumb-separator > svg { width: .875rem; height: .875rem; }
|
|
13
13
|
.pk-breadcrumb-ellipsis { display: flex; height: 1.25rem; width: 1.25rem; align-items: center; justify-content: center; }
|
|
14
14
|
.pk-breadcrumb-ellipsis > svg { width: 1rem; height: 1rem; }
|
|
15
|
-
.pk-sr-only {
|
|
16
|
-
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
17
|
-
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
18
|
-
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
.pk-message[data-align="end"] .pk-bubble { align-self: flex-end; }
|
|
15
15
|
.pk-bubble-content {
|
|
16
16
|
width: fit-content; max-width: 100%; min-width: 0; overflow: hidden;
|
|
17
|
-
overflow-wrap: break-word; border: 1px solid transparent; border-radius:
|
|
17
|
+
overflow-wrap: break-word; border: 1px solid transparent; border-radius: calc(var(--pk-radius) + 2px);
|
|
18
18
|
padding: .5rem .75rem; font-size: .875rem; line-height: 1.625;
|
|
19
19
|
}
|
|
20
20
|
.pk-bubble[data-align="end"] > .pk-bubble-content { align-self: flex-end; }
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class BubbleContent < BaseComponent
|
|
3
|
-
# `as:` is dispatched dynamically — whitelist the
|
|
4
|
-
#
|
|
5
|
-
|
|
3
|
+
# `as:` is dispatched dynamically — whitelist the documented containers
|
|
4
|
+
# (incl. the interactive button/a contract bubble.css styles) so it can't
|
|
5
|
+
# reach arbitrary (including private) methods.
|
|
6
|
+
AS_TAGS = %i[div p blockquote button a].freeze
|
|
6
7
|
def initialize(as: :div, **attrs)
|
|
7
8
|
@as = as.to_sym
|
|
8
9
|
raise ArgumentError, "unknown BubbleContent as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})" unless AS_TAGS.include?(@as)
|
|
@@ -12,25 +12,25 @@
|
|
|
12
12
|
|
|
13
13
|
/* horizontal (default) */
|
|
14
14
|
.pk-button-group:not(.vertical) > :not(:first-child) {
|
|
15
|
-
border-
|
|
16
|
-
border-
|
|
15
|
+
border-start-start-radius: 0;
|
|
16
|
+
border-end-start-radius: 0;
|
|
17
17
|
border-inline-start: 0;
|
|
18
18
|
}
|
|
19
19
|
.pk-button-group:not(.vertical) > :not(:last-child) {
|
|
20
|
-
border-
|
|
21
|
-
border-
|
|
20
|
+
border-start-end-radius: 0;
|
|
21
|
+
border-end-end-radius: 0;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/* vertical */
|
|
25
25
|
.pk-button-group.vertical { flex-direction: column; }
|
|
26
26
|
.pk-button-group.vertical > :not(:first-child) {
|
|
27
|
-
border-
|
|
28
|
-
border-
|
|
27
|
+
border-start-start-radius: 0;
|
|
28
|
+
border-start-end-radius: 0;
|
|
29
29
|
border-top: 0;
|
|
30
30
|
}
|
|
31
31
|
.pk-button-group.vertical > :not(:last-child) {
|
|
32
|
-
border-
|
|
33
|
-
border-
|
|
32
|
+
border-end-start-radius: 0;
|
|
33
|
+
border-end-end-radius: 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/* Nested groups keep their own corners — undo the squaring the parent applied. */
|
|
@@ -45,25 +45,25 @@
|
|
|
45
45
|
absolutely-positioned panels are untouched. */
|
|
46
46
|
.pk-button-group:not(.vertical) > :not(:first-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
47
47
|
.pk-button-group:not(.vertical) > :not(:first-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
48
|
-
border-
|
|
49
|
-
border-
|
|
48
|
+
border-start-start-radius: 0;
|
|
49
|
+
border-end-start-radius: 0;
|
|
50
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) {
|
|
54
|
-
border-
|
|
55
|
-
border-
|
|
54
|
+
border-start-end-radius: 0;
|
|
55
|
+
border-end-end-radius: 0;
|
|
56
56
|
}
|
|
57
57
|
.pk-button-group.vertical > :not(:first-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
58
58
|
.pk-button-group.vertical > :not(:first-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
59
|
-
border-
|
|
60
|
-
border-
|
|
59
|
+
border-start-start-radius: 0;
|
|
60
|
+
border-start-end-radius: 0;
|
|
61
61
|
border-top: 0;
|
|
62
62
|
}
|
|
63
63
|
.pk-button-group.vertical > :not(:last-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
64
64
|
.pk-button-group.vertical > :not(:last-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
65
|
-
border-
|
|
66
|
-
border-
|
|
65
|
+
border-end-start-radius: 0;
|
|
66
|
+
border-end-end-radius: 0;
|
|
67
67
|
}
|
|
68
68
|
/* .pk-select defaults to width:100%; as a group segment it hugs its trigger
|
|
69
69
|
and lets siblings (the input) take the leftover flex space — shadcn's
|
|
@@ -155,6 +155,14 @@
|
|
|
155
155
|
background: var(--pk-accent);
|
|
156
156
|
}
|
|
157
157
|
.pk-calendar-cell:has(> .range-cap) { background: var(--pk-accent); }
|
|
158
|
-
.pk-calendar-cell:has(> .range-start.range-cap) {
|
|
159
|
-
|
|
158
|
+
.pk-calendar-cell:has(> .range-start.range-cap) {
|
|
159
|
+
border-radius: 0;
|
|
160
|
+
border-start-start-radius: var(--pk-cell-radius);
|
|
161
|
+
border-end-start-radius: var(--pk-cell-radius);
|
|
162
|
+
}
|
|
163
|
+
.pk-calendar-cell:has(> .range-end.range-cap) {
|
|
164
|
+
border-radius: 0;
|
|
165
|
+
border-start-end-radius: var(--pk-cell-radius);
|
|
166
|
+
border-end-end-radius: var(--pk-cell-radius);
|
|
167
|
+
}
|
|
160
168
|
.pk-calendar-cell:has(> .range-start.range-end) { border-radius: var(--pk-cell-radius); }
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
transition: transform .3s ease;
|
|
17
17
|
will-change: transform;
|
|
18
18
|
}
|
|
19
|
-
.pk-carousel.is-horizontal .pk-carousel-track { margin-
|
|
19
|
+
.pk-carousel.is-horizontal .pk-carousel-track { margin-inline-start: -1rem; }
|
|
20
20
|
.pk-carousel.is-vertical .pk-carousel-track { flex-direction: column; margin-top: -1rem; }
|
|
21
21
|
|
|
22
22
|
.pk-carousel-item {
|
|
23
23
|
min-width: 0;
|
|
24
24
|
flex: 0 0 100%;
|
|
25
25
|
}
|
|
26
|
-
.pk-carousel.is-horizontal .pk-carousel-item { padding-
|
|
26
|
+
.pk-carousel.is-horizontal .pk-carousel-item { padding-inline-start: 1rem; }
|
|
27
27
|
.pk-carousel.is-vertical .pk-carousel-item { padding-top: 1rem; }
|
|
28
28
|
|
|
29
29
|
/* Edge buttons (an outline icon .pk-button; same specificity, later in the
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
height: 2rem;
|
|
35
35
|
border-radius: 9999px;
|
|
36
36
|
}
|
|
37
|
-
.pk-carousel.is-horizontal .pk-carousel-previous {
|
|
38
|
-
.pk-carousel.is-horizontal .pk-carousel-next {
|
|
37
|
+
.pk-carousel.is-horizontal .pk-carousel-previous { inset-inline-start: -3rem; top: 50%; transform: translateY(-50%); }
|
|
38
|
+
.pk-carousel.is-horizontal .pk-carousel-next { inset-inline-end: -3rem; top: 50%; transform: translateY(-50%); }
|
|
39
39
|
.pk-carousel.is-vertical .pk-carousel-previous { top: -3rem; left: 50%; transform: translateX(-50%) rotate(90deg); }
|
|
40
40
|
.pk-carousel.is-vertical .pk-carousel-next { bottom: -3rem; left: 50%; transform: translateX(-50%) rotate(90deg); }
|
|
41
41
|
.pk-carousel-previous svg, .pk-carousel-next svg { width: 1rem; height: 1rem; }
|
|
@@ -5,17 +5,33 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["trigger", "source", "successPopover", "errorPopover"]
|
|
7
7
|
|
|
8
|
+
connect() {
|
|
9
|
+
// Turbo snapshots at turbo:before-cache, BEFORE disconnect — hide the
|
|
10
|
+
// flash now or a restore shows a permanently stuck "Copied!" popover.
|
|
11
|
+
document.addEventListener("turbo:before-cache", this.beforeCache)
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
disconnect() {
|
|
9
15
|
// Don't leave the 1.5s hide timer running into a dead/recycled element
|
|
10
16
|
// (Turbo can detach and re-attach this subtree mid-flash).
|
|
17
|
+
clearTimeout(this.timer)
|
|
18
|
+
document.removeEventListener("turbo:before-cache", this.beforeCache)
|
|
19
|
+
this.hideAll()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeCache = () => {
|
|
11
23
|
clearTimeout(this.timer)
|
|
12
24
|
this.hideAll()
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
copy() {
|
|
28
|
+
// A source usually wraps one element (input/code/…), but bare text is
|
|
29
|
+
// legal too — fall back to the source's own text instead of erroring.
|
|
16
30
|
const el = this.sourceTarget.children[0]
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
const text = el
|
|
32
|
+
? (el.tagName === "INPUT" ? el.value : el.innerText)
|
|
33
|
+
: this.sourceTarget.textContent.trim()
|
|
34
|
+
if (!text) { this.show(this.errorPopoverTarget); return }
|
|
19
35
|
navigator.clipboard.writeText(text)
|
|
20
36
|
.then(() => this.show(this.successPopoverTarget))
|
|
21
37
|
.catch(() => this.show(this.errorPopoverTarget))
|
|
@@ -6,10 +6,19 @@ export default class extends Controller {
|
|
|
6
6
|
static values = { open: { type: Boolean, default: false } }
|
|
7
7
|
|
|
8
8
|
connect() {
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// aria-expanded must live on the element AT reads state from — the
|
|
10
|
+
// focused control, not the plain wrapper div. The trigger wrapper renders
|
|
11
|
+
// aria-expanded server-side (it can't reach into its block), so resolve
|
|
12
|
+
// the nested focusable control here, relocate the state onto it, and keep
|
|
13
|
+
// toggling there. Wrapper-only triggers keep the wrapper as carrier.
|
|
14
|
+
if (this.hasTriggerTarget) {
|
|
15
|
+
this.control = this.triggerTarget.querySelector("button, [role='button'], a[href], input") || this.triggerTarget
|
|
16
|
+
if (this.control !== this.triggerTarget) this.triggerTarget.removeAttribute("aria-expanded")
|
|
17
|
+
// The trigger can't know the content's id at render time — wire
|
|
18
|
+
// aria-controls here.
|
|
19
|
+
if (this.hasContentTarget && this.contentTarget.id) {
|
|
20
|
+
this.control.setAttribute("aria-controls", this.contentTarget.id)
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
this.openValue ? this.open() : this.close()
|
|
15
24
|
}
|
|
@@ -18,13 +27,13 @@ export default class extends Controller {
|
|
|
18
27
|
|
|
19
28
|
open() {
|
|
20
29
|
if (this.hasContentTarget) { this.contentTarget.classList.remove("pk-hidden"); this.openValue = true }
|
|
21
|
-
if (this.
|
|
30
|
+
if (this.control) this.control.setAttribute("aria-expanded", "true")
|
|
22
31
|
this.element.dataset.state = "open";
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
close() {
|
|
26
35
|
if (this.hasContentTarget) { this.contentTarget.classList.add("pk-hidden"); this.openValue = false }
|
|
27
|
-
if (this.
|
|
36
|
+
if (this.control) this.control.setAttribute("aria-expanded", "false")
|
|
28
37
|
this.element.dataset.state = "closed";
|
|
29
38
|
}
|
|
30
39
|
}
|
|
@@ -4,8 +4,11 @@ module PhlexKit
|
|
|
4
4
|
# control (a <button>, e.g. PhlexKit::Button) inside it for keyboard
|
|
5
5
|
# operability and semantics, the same composition pattern as
|
|
6
6
|
# AttachmentTrigger. Pass `open:` matching the parent Collapsible's so
|
|
7
|
-
# aria-expanded is correct before JS
|
|
8
|
-
#
|
|
7
|
+
# aria-expanded is correct before JS. The wrapper carries the server-rendered
|
|
8
|
+
# aria-expanded only because the component can't reach into its block; on
|
|
9
|
+
# connect the controller relocates it (plus aria-controls) onto the nested
|
|
10
|
+
# focusable control — AT reads state from the focused element, not the
|
|
11
|
+
# wrapper — falling back to the wrapper when no control is nested.
|
|
9
12
|
class CollapsibleTrigger < BaseComponent
|
|
10
13
|
def initialize(open: false, **attrs)
|
|
11
14
|
@open = open
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
margin: 0;
|
|
153
153
|
accent-color: var(--pk-brand);
|
|
154
154
|
}
|
|
155
|
-
.pk-combobox-checkbox { border-radius:
|
|
155
|
+
.pk-combobox-checkbox { border-radius: calc(var(--pk-radius) - 8px); }
|
|
156
156
|
.pk-combobox-checkbox:disabled, .pk-combobox-radio:disabled { cursor: not-allowed; opacity: .5; }
|
|
157
157
|
.pk-combobox-checkbox:focus-visible, .pk-combobox-radio:focus-visible {
|
|
158
158
|
outline: none;
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
width: .875rem;
|
|
297
297
|
height: .875rem;
|
|
298
298
|
border: 0;
|
|
299
|
-
border-radius:
|
|
299
|
+
border-radius: calc(var(--pk-radius) - 8px);
|
|
300
300
|
padding: 0;
|
|
301
301
|
background: transparent;
|
|
302
302
|
color: var(--pk-muted);
|
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
flex: none;
|
|
319
319
|
margin-inline-start: auto;
|
|
320
320
|
border: 0;
|
|
321
|
-
border-radius:
|
|
321
|
+
border-radius: calc(var(--pk-radius) - 8px);
|
|
322
322
|
padding: 0;
|
|
323
323
|
background: transparent;
|
|
324
324
|
color: var(--pk-muted);
|
|
@@ -5,9 +5,9 @@ module PhlexKit
|
|
|
5
5
|
# toggle-all live in the phlex-kit--combobox controller. Like PhlexKit::Select, the
|
|
6
6
|
# @floating-ui/dom + native-popover positioning is replaced with CSS (panel
|
|
7
7
|
# anchored below the trigger, click-outside via window action). Compose
|
|
8
|
-
# Combobox(term:) > trigger + Popover(SearchInput + List(
|
|
9
|
-
# Item(Checkbox|Radio + text +
|
|
10
|
-
#
|
|
8
|
+
# Combobox(term:) > trigger + Popover(SearchInput + List([Item(
|
|
9
|
+
# ToggleAllCheckbox) first +] ListGroup > Item(Checkbox|Radio + text +
|
|
10
|
+
# ItemIndicator) + EmptyState)). Three trigger flavours: ComboboxTrigger (button +
|
|
11
11
|
# popover search), ComboboxInputTrigger (the field is the filter), and
|
|
12
12
|
# ComboboxBadgeTrigger (multi-select chips + ComboboxClearButton) — the
|
|
13
13
|
# latter two shipped unfinished in ruby_ui (no controller support) and are
|
|
@@ -174,6 +174,11 @@ export default class extends Controller {
|
|
|
174
174
|
this.inputTargets.forEach((input) => {
|
|
175
175
|
input.parentElement.setAttribute("aria-selected", input.checked ? "true" : "false")
|
|
176
176
|
})
|
|
177
|
+
// The toggle-all row is an option too (first row inside the listbox) —
|
|
178
|
+
// keep its aria-selected in step with its checkbox.
|
|
179
|
+
if (this.hasToggleAllTarget && this.toggleAllTarget.parentElement.getAttribute("role") === "option") {
|
|
180
|
+
this.toggleAllTarget.parentElement.setAttribute("aria-selected", this.toggleAllTarget.checked ? "true" : "false")
|
|
181
|
+
}
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
renderBadges(checked) {
|
|
@@ -284,7 +289,7 @@ export default class extends Controller {
|
|
|
284
289
|
// aria-activedescendant holds an element id; on close it must be removed,
|
|
285
290
|
// not left pointing at a hidden option.
|
|
286
291
|
this.clearActiveDescendant()
|
|
287
|
-
if (this.popoverTarget.matches(":popover-open")) this.popoverTarget.hidePopover()
|
|
292
|
+
if (this.hasPopoverTarget && this.popoverTarget.matches(":popover-open")) this.popoverTarget.hidePopover()
|
|
288
293
|
this.updateTriggerContent() // reflect the selection into an input trigger
|
|
289
294
|
|
|
290
295
|
if (focusWasInside) {
|
|
@@ -398,20 +403,23 @@ export default class extends Controller {
|
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
focusSelectedInput() {
|
|
401
|
-
|
|
402
|
-
|
|
406
|
+
// Walk the option rows themselves, not the inner selection inputs: the
|
|
407
|
+
// toggle-all row is an option without an "input" target and must be
|
|
408
|
+
// arrow-reachable like any other option.
|
|
409
|
+
const visibleItems = this.itemTargets.filter(item => !item.classList.contains("pk-hidden"))
|
|
410
|
+
if (visibleItems.length === 0) return
|
|
403
411
|
|
|
404
|
-
this.wrapSelectedInputIndex(
|
|
412
|
+
this.wrapSelectedInputIndex(visibleItems.length)
|
|
405
413
|
|
|
406
|
-
|
|
414
|
+
visibleItems.forEach((item, index) => {
|
|
407
415
|
if (index == this.selectedItemIndex) {
|
|
408
|
-
|
|
416
|
+
item.ariaCurrent = "true"
|
|
409
417
|
// Focus stays in the filter field; the highlighted option is exposed
|
|
410
418
|
// via aria-activedescendant (option ids come from generateItemIds).
|
|
411
|
-
this.setActiveDescendant(
|
|
412
|
-
|
|
419
|
+
this.setActiveDescendant(item.id)
|
|
420
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
|
413
421
|
} else {
|
|
414
|
-
|
|
422
|
+
item.ariaCurrent = "false"
|
|
415
423
|
}
|
|
416
424
|
})
|
|
417
425
|
}
|
|
@@ -26,7 +26,9 @@ module PhlexKit
|
|
|
26
26
|
class: "pk-combobox-search-input",
|
|
27
27
|
data: {
|
|
28
28
|
phlex_kit__combobox_target: "searchInput",
|
|
29
|
-
|
|
29
|
+
# `input` covers context-menu paste and IME composition, which
|
|
30
|
+
# fire no keyup/search (mirrors ComboboxInputTrigger).
|
|
31
|
+
action: "keyup->phlex-kit--combobox#filterItems input->phlex-kit--combobox#filterItems search->phlex-kit--combobox#filterItems"
|
|
30
32
|
}
|
|
31
33
|
}, @attrs))
|
|
32
34
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# "Select all" checkbox for a multi-select combobox — wrap it in a
|
|
3
|
-
# PhlexKit::ComboboxItem
|
|
3
|
+
# PhlexKit::ComboboxItem as the FIRST row inside the ComboboxList (the item
|
|
4
|
+
# carries role="option", which is only valid inside the listbox, and arrow-key
|
|
5
|
+
# nav walks the list's rows). Hidden while a filter term is active.
|
|
4
6
|
# See combobox.rb.
|
|
5
7
|
class ComboboxToggleAllCheckbox < BaseComponent
|
|
6
8
|
def initialize(**attrs)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
.pk-command {
|
|
9
9
|
background: var(--pk-surface);
|
|
10
10
|
color: var(--pk-text);
|
|
11
|
-
border-radius:
|
|
11
|
+
border-radius: calc(var(--pk-radius) + 2px);
|
|
12
12
|
padding: .25rem;
|
|
13
13
|
}
|
|
14
14
|
.pk-command-dialog-trigger { display: inline-block; }
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
width: 100%;
|
|
35
35
|
max-width: 32rem;
|
|
36
36
|
border: 1px solid var(--pk-border);
|
|
37
|
-
border-radius:
|
|
37
|
+
border-radius: calc(var(--pk-radius) + 2px);
|
|
38
38
|
background: var(--pk-surface);
|
|
39
39
|
color: var(--pk-text);
|
|
40
40
|
box-shadow: 0 10px 30px color-mix(in srgb, var(--pk-shadow-color) 35%, transparent);
|