@threadlabs/looma 0.6.5 → 0.7.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.
- package/components/ui-button/ui-button.html +103 -81
- package/components/ui-checkbox/ui-checkbox.html +4 -1
- package/components/ui-combobox/ui-combobox.html +40 -3
- package/components/ui-combobox/ui-combobox.js +27 -5
- package/components/ui-select/ui-select.html +3 -5
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-button.css +111 -79
- package/styles/ui-checkbox.css +4 -1
- package/styles/ui-combobox.css +37 -0
- package/styles/ui-select.css +3 -4
- package/theme-dark.css +6 -30
- package/theme-high-contrast.css +30 -20
- package/theme-light.css +2 -16
- package/tokens.css +33 -24
- package/vanilla/UiButton.d.ts +1 -1
- package/vanilla/UiButton.js +1 -1
- package/vanilla/UiCombobox.js +1 -1
- package/vanilla/UiSelect.d.ts +0 -1
- package/vanilla/UiSelect.js +3 -6
- package/vue/UiButton.d.ts +2 -2
- package/vue/UiButton.vue +103 -96
- package/vue/UiCheckbox.vue +4 -1
- package/vue/UiCombobox.vue +46 -4
- package/vue/UiSelect.d.ts +0 -2
- package/vue/UiSelect.vue +2 -6
- package/vue/chunks/UiButton.js +3 -3
- package/vue/chunks/UiCheckbox.js +1 -1
- package/vue/chunks/UiCombobox.js +2 -2
- package/vue/chunks/UiSelect.js +1 -7
- package/vue/components.css +236 -189
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template component="ui-button">
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="variant" type="outline | solid | danger | ghost | link" default="outline"
|
|
4
|
-
>
|
|
4
|
+
>Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
|
|
5
|
+
hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it.</prop>
|
|
5
6
|
<prop name="size" type="sm | md | lg" default="md">Control size.</prop>
|
|
6
|
-
<prop name="tone" type="neutral |
|
|
7
|
-
>
|
|
8
|
-
|
|
9
|
-
rather than annotating every call site, and keep them as var() references so they follow a later change of accent.</prop>
|
|
7
|
+
<prop name="tone" type="accent | neutral | danger | success | warning | info" default="accent"
|
|
8
|
+
>Which colour the button speaks in. It applies to every variant, so an outline, a ghost, and a solid in the same tone are the same action at three volumes:
|
|
9
|
+
tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour.</prop>
|
|
10
10
|
<prop name="disabled" type="boolean" default="false">disabled token.</prop>
|
|
11
11
|
<prop name="align" type="center | start" default="center"
|
|
12
12
|
>Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
|
|
@@ -15,15 +15,23 @@
|
|
|
15
15
|
<button type="button" :disabled="disabled"><slot></slot></button>
|
|
16
16
|
<style>
|
|
17
17
|
:host {
|
|
18
|
+
--_hue: var(--ui-accent);
|
|
19
|
+
--_hue-hover: var(--ui-accent-hover);
|
|
20
|
+
--_hue-active: var(--ui-accent-active);
|
|
21
|
+
--_hue-text: var(--ui-accent-active);
|
|
22
|
+
--_on-hue: var(--ui-on-accent);
|
|
23
|
+
|
|
24
|
+
--_tone: var(--_hue);
|
|
25
|
+
--_tone-hover: var(--_hue-hover);
|
|
26
|
+
--_tone-active: var(--_hue-active);
|
|
27
|
+
--_tone-text: var(--_hue-text);
|
|
28
|
+
--_on-tone: var(--_on-hue);
|
|
29
|
+
|
|
18
30
|
display: inline-flex;
|
|
19
31
|
align-items: center;
|
|
20
32
|
justify-content: center;
|
|
21
33
|
vertical-align: middle;
|
|
22
|
-
color: var(--
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
:host-state([variant="solid"]) {
|
|
26
|
-
color: var(--ui-action-primary-text, var(--ui-text-on-accent));
|
|
34
|
+
color: var(--_tone-text);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
:host-state([align="start"]) {
|
|
@@ -39,23 +47,24 @@
|
|
|
39
47
|
:host {
|
|
40
48
|
appearance: none;
|
|
41
49
|
min-block-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
|
|
42
|
-
border:
|
|
50
|
+
border-style: solid;
|
|
51
|
+
border-width: 1px;
|
|
52
|
+
border-color: var(--ui-button-border, var(--_tone));
|
|
43
53
|
border-radius: var(--ui-button-radius, var(--ui-radius-md));
|
|
44
|
-
background: var(--ui-button-surface, var(--
|
|
45
|
-
color: var(--ui-button-text, var(--
|
|
54
|
+
background-color: var(--ui-button-surface, oklch(from var(--_tone) l c h / 5%));
|
|
55
|
+
color: var(--ui-button-text, var(--_tone-text));
|
|
46
56
|
font: inherit;
|
|
47
57
|
font-weight: var(--ui-font-medium);
|
|
48
58
|
line-height: var(--ui-line-height-tight);
|
|
49
59
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
50
60
|
background-image: var(--ui-raised-highlight);
|
|
51
|
-
box-shadow: var(--ui-button-shadow, var(--ui-raised
|
|
61
|
+
box-shadow: var(--ui-button-shadow, var(--ui-raised));
|
|
52
62
|
cursor: pointer;
|
|
53
63
|
transition:
|
|
54
64
|
background-color var(--ui-motion-fast) var(--ui-motion-ease),
|
|
55
65
|
border-color var(--ui-motion-fast) var(--ui-motion-ease),
|
|
56
66
|
color var(--ui-motion-fast) var(--ui-motion-ease),
|
|
57
|
-
box-shadow var(--ui-motion-fast) var(--ui-motion-ease)
|
|
58
|
-
transform var(--ui-motion-fast) var(--ui-motion-ease);
|
|
67
|
+
box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
|
|
59
68
|
margin: 0;
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -79,128 +88,141 @@
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
:host:hover:enabled {
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
border-color: var(--ui-button-hover-border, var(--_tone-hover));
|
|
92
|
+
background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
/* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
|
|
86
96
|
:host:active:enabled {
|
|
87
|
-
background: var(--ui-surface
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:host-state([variant="solid"]) {
|
|
91
|
-
border-color: var(--ui-action-primary-surface, var(--ui-accent-solid));
|
|
92
|
-
background: var(--ui-action-primary-surface, var(--ui-accent-solid));
|
|
93
|
-
color: var(--ui-action-primary-text, var(--ui-text-on-accent));
|
|
94
|
-
box-shadow: var(--ui-button-solid-shadow, inset 0 1px rgb(255 255 255 / 0.16), var(--ui-shadow-xs));
|
|
97
|
+
background-color: var(--ui-button-active-surface, oklch(from var(--_tone) l c h / 18%));
|
|
98
|
+
background-image: none;
|
|
99
|
+
box-shadow: var(--ui-pressed);
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
:host-state([variant="solid"]):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
:host-state([variant="solid"]), :host-state([variant="danger"]) {
|
|
103
|
+
border-color: var(--_tone);
|
|
104
|
+
background-color: var(--_tone);
|
|
105
|
+
color: var(--_on-tone);
|
|
106
|
+
box-shadow: var(--ui-button-solid-shadow, var(--ui-raised));
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
:host-state([variant="solid"]):
|
|
105
|
-
background: var(--
|
|
106
|
-
border-color: var(--
|
|
107
|
-
box-shadow: none;
|
|
108
|
-
transform: translateY(0);
|
|
109
|
+
:is(:host-state([variant="solid"]), :host-state([variant="danger"])):hover:enabled {
|
|
110
|
+
background-color: var(--_tone-hover);
|
|
111
|
+
border-color: var(--_tone-hover);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
:host-state([variant="danger"]) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
box-shadow: var(--ui-
|
|
114
|
+
:is(:host-state([variant="solid"]), :host-state([variant="danger"])):active:enabled {
|
|
115
|
+
background-color: var(--_tone-active);
|
|
116
|
+
border-color: var(--_tone-active);
|
|
117
|
+
background-image: none;
|
|
118
|
+
box-shadow: var(--ui-pressed);
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
/* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
|
|
122
|
+
destructive secondary action, not a special case. */
|
|
123
|
+
:host-state([tone="neutral"]) {
|
|
124
|
+
--_hue: var(--ui-text);
|
|
125
|
+
--_hue-hover: var(--ui-text);
|
|
126
|
+
--_hue-active: var(--ui-text);
|
|
127
|
+
--_hue-text: var(--ui-text);
|
|
128
|
+
--_on-hue: var(--ui-surface);
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
:host-state([variant="danger"])
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
:host-state([tone="danger"]), :host-state([variant="danger"]) {
|
|
132
|
+
--_hue: var(--ui-danger);
|
|
133
|
+
--_hue-hover: var(--ui-danger-hover);
|
|
134
|
+
--_hue-active: var(--ui-danger-hover);
|
|
135
|
+
--_hue-text: var(--ui-danger-hover);
|
|
136
|
+
--_on-hue: var(--ui-on-danger);
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
:host-state([tone="success"]) {
|
|
140
|
+
--_hue: var(--ui-success);
|
|
141
|
+
--_hue-hover: color-mix(in srgb, var(--ui-success) 85%, #000);
|
|
142
|
+
--_hue-active: color-mix(in srgb, var(--ui-success) 75%, #000);
|
|
143
|
+
--_hue-text: color-mix(in srgb, var(--ui-success) 85%, #000);
|
|
144
|
+
--_on-hue: var(--ui-surface);
|
|
136
145
|
}
|
|
137
146
|
|
|
138
|
-
:host-state([tone="
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
:host-state([tone="warning"]) {
|
|
148
|
+
--_hue: var(--ui-warning);
|
|
149
|
+
--_hue-hover: color-mix(in srgb, var(--ui-warning) 85%, #000);
|
|
150
|
+
--_hue-active: color-mix(in srgb, var(--ui-warning) 75%, #000);
|
|
151
|
+
--_hue-text: color-mix(in srgb, var(--ui-warning) 85%, #000);
|
|
152
|
+
--_on-hue: var(--ui-surface);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
:host-state([tone="
|
|
144
|
-
|
|
155
|
+
:host-state([tone="info"]) {
|
|
156
|
+
--_hue: var(--ui-info);
|
|
157
|
+
--_hue-hover: color-mix(in srgb, var(--ui-info) 85%, #000);
|
|
158
|
+
--_hue-active: color-mix(in srgb, var(--ui-info) 75%, #000);
|
|
159
|
+
--_hue-text: color-mix(in srgb, var(--ui-info) 85%, #000);
|
|
160
|
+
--_on-hue: var(--ui-surface);
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
:host-state([variant="ghost"]) {
|
|
148
164
|
border-color: transparent;
|
|
149
165
|
background: transparent;
|
|
150
166
|
box-shadow: none;
|
|
151
|
-
color: var(--ui-button-ghost-text, var(--
|
|
167
|
+
color: var(--ui-button-ghost-text, var(--_tone-text));
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
|
|
155
171
|
:host-state([variant="ghost"]):hover:enabled {
|
|
156
|
-
background: var(--ui-button-ghost-hover-surface,
|
|
172
|
+
background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
|
|
157
173
|
border-color: transparent;
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
:host-state([variant="ghost"]):active:enabled {
|
|
161
|
-
background: var(--ui-button-ghost-active-surface,
|
|
177
|
+
background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 16%));
|
|
162
178
|
border-color: transparent;
|
|
179
|
+
box-shadow: var(--ui-pressed);
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
/* A soft halo that transitions in with the other box-shadow changes, layered on the raised shadow. */
|
|
166
183
|
:host:focus-visible {
|
|
167
184
|
outline: none;
|
|
168
185
|
box-shadow:
|
|
169
|
-
var(--ui-button-shadow, var(--ui-raised
|
|
186
|
+
var(--ui-button-shadow, var(--ui-raised)),
|
|
170
187
|
var(--ui-focus-halo);
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
:host-state([variant="solid"]):focus-visible {
|
|
174
191
|
box-shadow:
|
|
175
|
-
var(--ui-button-solid-shadow,
|
|
192
|
+
var(--ui-button-solid-shadow, var(--ui-raised)),
|
|
176
193
|
var(--ui-focus-halo);
|
|
177
194
|
}
|
|
178
195
|
|
|
179
|
-
/* Disabled
|
|
196
|
+
/* Disabled drops the tone rather than the shape: every variant keeps its own structure, so a
|
|
197
|
+
disabled outline is still an outline and a disabled ghost is still text. Intent goes — an
|
|
198
|
+
unavailable action says "unavailable", not "unavailable, and destructive" — and so do the
|
|
199
|
+
highlight and shadow, because nothing that cannot be pressed should look raised. */
|
|
180
200
|
:host:disabled {
|
|
201
|
+
/* Derived from the tone itself, not from a separate disabled palette: faded toward the
|
|
202
|
+
page, which desaturates it at the same time. Toward the page rather than toward white,
|
|
203
|
+
so a dark theme dims where a light one lightens — the same rule, both schemes. */
|
|
204
|
+
--_tone: color-mix(in oklab, var(--_hue) 58%, var(--ui-surface));
|
|
205
|
+
--_tone-hover: var(--_tone);
|
|
206
|
+
--_tone-active: var(--_tone);
|
|
207
|
+
--_tone-text: color-mix(in oklab, var(--_hue-text) 72%, var(--ui-surface));
|
|
208
|
+
--_on-tone: var(--_tone-text);
|
|
209
|
+
|
|
181
210
|
opacity: var(--ui-button-disabled-opacity, 1);
|
|
182
211
|
cursor: not-allowed;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
color: var(--ui-button-disabled-text, var(--ui-disabled-text));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
:host-state([variant="solid"]):disabled {
|
|
189
|
-
border-color: var(--ui-button-disabled-border, var(--ui-border));
|
|
190
|
-
background-color: var(--ui-button-disabled-surface, var(--ui-disabled-surface));
|
|
191
|
-
color: var(--ui-button-disabled-text, var(--ui-disabled-text));
|
|
212
|
+
background-image: none;
|
|
213
|
+
box-shadow: none;
|
|
192
214
|
}
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
color: var(--ui-button-disabled-text, var(--ui-disabled-text));
|
|
216
|
+
/* A filled button keeps its fill, faded further so its text stays legible on it. */
|
|
217
|
+
:is(:host-state([variant="solid"]), :host-state([variant="danger"])):disabled {
|
|
218
|
+
--_tone: color-mix(in oklab, var(--_hue) 34%, var(--ui-surface));
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
/* Ghost has no hover to fall back on, so disabled is the one state it has to state outright:
|
|
222
|
+
it takes a surface, light and tinted, rather than reading as plain text. */
|
|
200
223
|
:host-state([variant="ghost"]):disabled {
|
|
201
|
-
background:
|
|
224
|
+
background-color: color-mix(in oklab, var(--_hue) 28%, var(--ui-surface));
|
|
202
225
|
border-color: transparent;
|
|
203
|
-
color: var(--ui-button-disabled-text, var(--ui-disabled-text));
|
|
204
226
|
}
|
|
205
227
|
|
|
206
228
|
@media (pointer: coarse) {
|
|
@@ -65,7 +65,10 @@
|
|
|
65
65
|
margin: auto;
|
|
66
66
|
inline-size: 0.32rem;
|
|
67
67
|
block-size: 0.58rem;
|
|
68
|
-
border: solid var(
|
|
68
|
+
/* Longhands, not `border: solid var(...)`: lowering drops a shorthand that omits a
|
|
69
|
+
component, which left the tick with no style and no colour. */
|
|
70
|
+
border-style: solid;
|
|
71
|
+
border-color: var(--ui-text-on-accent);
|
|
69
72
|
border-width: 0 0.125rem 0.125rem 0;
|
|
70
73
|
opacity: 1;
|
|
71
74
|
transform: translateY(-0.11rem) rotate(45deg);
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
<state name="display" :value="''"></state>
|
|
27
27
|
<state name="selected" type="string | null" :value="null"></state>
|
|
28
28
|
<state name="expanded" :value="false"></state>
|
|
29
|
-
<state
|
|
29
|
+
<state
|
|
30
|
+
name="rows"
|
|
31
|
+
type="list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean }))"
|
|
32
|
+
:value="[]"
|
|
33
|
+
></state>
|
|
30
34
|
<state name="active" :value="-1"></state>
|
|
31
35
|
<state name="loading" :value="false"></state>
|
|
32
36
|
<state name="helpOpen" :value="false"></state>
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
<state name="uid" :value="''"></state>
|
|
40
44
|
<state
|
|
41
45
|
name="groups"
|
|
42
|
-
type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, index: integer })) }))"
|
|
46
|
+
type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, index: integer })) }))"
|
|
43
47
|
:value="[]"
|
|
44
48
|
></state>
|
|
45
49
|
<state name="creatable" :value="false"></state>
|
|
@@ -170,7 +174,7 @@
|
|
|
170
174
|
class:active="row.index = active"
|
|
171
175
|
role="option"
|
|
172
176
|
:id="format('%s-option-%s', uid, row.index)"
|
|
173
|
-
:aria-selected="row.
|
|
177
|
+
:aria-selected="row.selected"
|
|
174
178
|
:aria-disabled="row.disabled"
|
|
175
179
|
:data-index="row.index"
|
|
176
180
|
>
|
|
@@ -407,6 +411,39 @@
|
|
|
407
411
|
}
|
|
408
412
|
}
|
|
409
413
|
|
|
414
|
+
/* Multiple mode: every option carries a checkbox, so a list that takes several answers says so
|
|
415
|
+
before anything is picked. aria-selected is the state; this draws it.
|
|
416
|
+
Longhands, not shorthands: lowering drops a shorthand that omits a component. */
|
|
417
|
+
:host-state([multiple]) .option {
|
|
418
|
+
display: flex;
|
|
419
|
+
gap: var(--ui-space-2);
|
|
420
|
+
align-items: center;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
:host-state([multiple]) .option::before {
|
|
424
|
+
content: "";
|
|
425
|
+
flex: none;
|
|
426
|
+
inline-size: 1.125rem;
|
|
427
|
+
block-size: 1.125rem;
|
|
428
|
+
border-style: solid;
|
|
429
|
+
border-width: 1px;
|
|
430
|
+
border-color: var(--ui-control-border-hover, var(--ui-border-strong));
|
|
431
|
+
border-radius: var(--ui-radius-sm);
|
|
432
|
+
background-color: var(--ui-control-surface, var(--ui-surface-default));
|
|
433
|
+
background-repeat: no-repeat;
|
|
434
|
+
background-position: center;
|
|
435
|
+
background-size: 0.7rem;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
:host-state([multiple]) .option[aria-selected="true"]::before {
|
|
439
|
+
border-color: var(--ui-accent-solid);
|
|
440
|
+
background-color: var(--ui-accent-solid);
|
|
441
|
+
background-image: var(
|
|
442
|
+
--ui-combobox-option-check,
|
|
443
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 5 5 9-9'/%3E%3C/svg%3E")
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
410
447
|
/* Multiple mode: chips wrap before the cursor on one flexible field line. */
|
|
411
448
|
:host-state([multiple]) .field {
|
|
412
449
|
flex-wrap: wrap;
|
|
@@ -113,13 +113,17 @@ export default function controller(host) {
|
|
|
113
113
|
}
|
|
114
114
|
const ids = new Set();
|
|
115
115
|
const selected = host.state.multiple ? selectedValues() : undefined;
|
|
116
|
+
// Multiple keeps its chosen options in the list, checked. Dropping them hid what was picked
|
|
117
|
+
// from the list and from assistive technology, which only ever heard aria-selected="false".
|
|
116
118
|
const filtered = options.filter((option) => {
|
|
117
119
|
if (ids.has(option.id)) return false;
|
|
118
120
|
ids.add(option.id);
|
|
119
121
|
if (fullSet) return true;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
return option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase());
|
|
123
|
+
}).map((option) => ({
|
|
124
|
+
...option,
|
|
125
|
+
selected: selected ? selected.has(option.value) : option.value === host.state.selected
|
|
126
|
+
}));
|
|
123
127
|
const groups = new Map();
|
|
124
128
|
for (const row of filtered) {
|
|
125
129
|
const group = row.group ?? "";
|
|
@@ -182,7 +186,16 @@ export default function controller(host) {
|
|
|
182
186
|
input.value = query;
|
|
183
187
|
host.dispatch("query-change", { query, display: query, trigger });
|
|
184
188
|
};
|
|
185
|
-
|
|
189
|
+
// A row carries a view-only `selected` flag; an item is the option itself, in its declared shape.
|
|
190
|
+
const asItem = ({ id, value, label, group, disabled }) => ({
|
|
191
|
+
id,
|
|
192
|
+
value,
|
|
193
|
+
label,
|
|
194
|
+
...(group === undefined ? {} : { group }),
|
|
195
|
+
...(disabled === undefined ? {} : { disabled })
|
|
196
|
+
});
|
|
197
|
+
const addSelectedItem = (row, trigger) => {
|
|
198
|
+
const option = asItem(row);
|
|
186
199
|
const current = items();
|
|
187
200
|
host.dispatch("add-item", { item: option, index: current.length, trigger });
|
|
188
201
|
emitItems([...current, option]);
|
|
@@ -192,10 +205,19 @@ export default function controller(host) {
|
|
|
192
205
|
const option = (host.state.rows ?? [])[index];
|
|
193
206
|
if (option?.disabled) return;
|
|
194
207
|
if (host.state.multiple) {
|
|
195
|
-
if (option)
|
|
208
|
+
if (option) {
|
|
209
|
+
// A checked row toggles off: the list is the selection, so it has to work both ways.
|
|
210
|
+
const position = items().findIndex((item) => item.value === option.value);
|
|
211
|
+
if (position >= 0) removeItemAt(position, trigger);
|
|
212
|
+
else addSelectedItem(option, trigger);
|
|
213
|
+
}
|
|
196
214
|
else if (canCreate() && index === host.state.rows.length) createSelectedItem(String(host.state.raw).trim(), trigger);
|
|
197
215
|
else return;
|
|
198
216
|
setMultiQuery("", trigger);
|
|
217
|
+
// The list stays open so several can be chosen without reopening it.
|
|
218
|
+
search("selection");
|
|
219
|
+
input.focus();
|
|
220
|
+
return;
|
|
199
221
|
} else if (option) commit(option.value, option.label, option, "selection", trigger);
|
|
200
222
|
else if (canCreate() && index === host.state.rows.length) commit(null, host.state.raw, null, "create", trigger);
|
|
201
223
|
else return;
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
<prop name="disabled" type="boolean" default="false">Prevents selection and form submission.</prop>
|
|
5
5
|
<prop name="invalid" type="boolean" default="false">Exposes invalid state to assistive technology and styling.</prop>
|
|
6
6
|
<prop name="required" type="boolean" default="false">Requires a non-empty option for form validation.</prop>
|
|
7
|
-
<prop name="multiple" type="boolean" default="false">Allows more than one option to be selected.</prop>
|
|
8
7
|
</defs>
|
|
9
|
-
<select :disabled="disabled" :required="required" :
|
|
8
|
+
<select :disabled="disabled" :required="required" :aria-invalid="invalid"><slot></slot></select>
|
|
10
9
|
<style>
|
|
11
10
|
:host {
|
|
12
11
|
box-sizing: border-box;
|
|
@@ -30,9 +29,9 @@
|
|
|
30
29
|
background-color var(--ui-motion-fast) var(--ui-motion-ease);
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
/*
|
|
32
|
+
/* The select draws its own chevron: the native caret cannot be inset or sized consistently.
|
|
34
33
|
* The root stays a native <select>, so the icon is a background image rather than an element. */
|
|
35
|
-
:host
|
|
34
|
+
:host {
|
|
36
35
|
appearance: none;
|
|
37
36
|
padding-inline-end: calc(var(--ui-space-3) + 1.5rem);
|
|
38
37
|
background-image: var(
|
|
@@ -73,7 +72,6 @@
|
|
|
73
72
|
cursor: not-allowed;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
:host[multiple],
|
|
77
75
|
:host[size]:not([size="1"]) {
|
|
78
76
|
min-block-size: 7rem;
|
|
79
77
|
padding: var(--ui-space-2);
|