@jxsuite/ui 0.11.0 → 0.12.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/jx-checkbox.json +35 -2
- package/components/jx-dialog.json +3 -1
- package/components/jx-field.json +10 -1
- package/components/jx-number-field.json +39 -7
- package/components/jx-option.json +8 -0
- package/components/jx-select.json +13 -4
- package/components/jx-slider.json +16 -0
- package/components/jx-switch.json +18 -0
- package/components/jx-textfield.json +50 -8
- package/package.json +3 -3
- package/project.json +3 -5
- package/src/behaviors/combobox.ts +9 -0
- package/src/behaviors/number-field.ts +3 -1
- package/src/behaviors/select.ts +15 -0
- package/src/behaviors/textfield.ts +38 -0
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
"checked",
|
|
7
7
|
"indeterminate",
|
|
8
8
|
"disabled",
|
|
9
|
+
"required",
|
|
10
|
+
"invalid",
|
|
9
11
|
"label",
|
|
10
12
|
"labelledby",
|
|
11
13
|
"describedby",
|
|
12
14
|
"name",
|
|
15
|
+
"value",
|
|
13
16
|
"size"
|
|
14
17
|
],
|
|
15
18
|
"state": {
|
|
@@ -31,6 +34,18 @@
|
|
|
31
34
|
"attribute": "disabled",
|
|
32
35
|
"description": "Cannot be ticked or focused. Disabled, never absent."
|
|
33
36
|
},
|
|
37
|
+
"required": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"default": false,
|
|
40
|
+
"attribute": "required",
|
|
41
|
+
"description": "Forwarded to the control: a required box left unticked fails the form's own check."
|
|
42
|
+
},
|
|
43
|
+
"invalid": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"default": false,
|
|
46
|
+
"attribute": "invalid",
|
|
47
|
+
"description": "The answer is refused: the control says aria-invalid and the box is ringed in the danger colour."
|
|
48
|
+
},
|
|
34
49
|
"label": {
|
|
35
50
|
"type": "string",
|
|
36
51
|
"default": "",
|
|
@@ -55,6 +70,12 @@
|
|
|
55
70
|
"attribute": "name",
|
|
56
71
|
"description": "The form field name, forwarded to the control."
|
|
57
72
|
},
|
|
73
|
+
"value": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"default": "",
|
|
76
|
+
"attribute": "value",
|
|
77
|
+
"description": "What a ticked box submits under its name. Empty leaves the platform's own \"on\"."
|
|
78
|
+
},
|
|
58
79
|
"size": {
|
|
59
80
|
"type": "string",
|
|
60
81
|
"default": "md",
|
|
@@ -95,7 +116,8 @@
|
|
|
95
116
|
"data-size": "${state.size}",
|
|
96
117
|
"data-checked": "${state.checked ? '' : null}",
|
|
97
118
|
"data-indeterminate": "${state.indeterminate ? '' : null}",
|
|
98
|
-
"data-disabled": "${state.disabled ? '' : null}"
|
|
119
|
+
"data-disabled": "${state.disabled ? '' : null}",
|
|
120
|
+
"data-invalid": "${state.invalid ? '' : null}"
|
|
99
121
|
},
|
|
100
122
|
"style": {
|
|
101
123
|
"display": "inline-flex",
|
|
@@ -131,6 +153,12 @@
|
|
|
131
153
|
"borderRadius": "var(--jx-radius-xs)"
|
|
132
154
|
}
|
|
133
155
|
},
|
|
156
|
+
"&[data-invalid] [part=\"input\"]:not(:focus-visible), & [part=\"input\"]:user-invalid:not(:focus-visible)": {
|
|
157
|
+
"$description": "A refused box is ringed, by the host's word or by the control's own once the reader has been through it. A ring and not a fill, because the box's fill is the platform's accent and says ticked. Focus keeps its own ring, which is the one a keyboard reader is following.",
|
|
158
|
+
"outline": "1px solid var(--jx-danger)",
|
|
159
|
+
"outlineOffset": "1px",
|
|
160
|
+
"borderRadius": "var(--jx-radius-xs)"
|
|
161
|
+
},
|
|
134
162
|
"& [part=\"label\"]": {
|
|
135
163
|
"minWidth": "0",
|
|
136
164
|
"overflow": "hidden",
|
|
@@ -146,7 +174,7 @@
|
|
|
146
174
|
"color": "var(--jx-fg-dim)"
|
|
147
175
|
},
|
|
148
176
|
"&[data-size=\"sm\"] > [part=\"control\"]": {
|
|
149
|
-
"$description": "The control is the `<label>` wrapping the input, and the label is the pointer target: a click anywhere on it toggles the box. At `sm` it draws 4px under the kit's control height, the WCAG 2.2 SC 2.5.8 floor (ui.md
|
|
177
|
+
"$description": "The control is the `<label>` wrapping the input, and the label is the pointer target: a click anywhere on it toggles the box. At `sm` it draws 4px under the kit's control height, the WCAG 2.2 SC 2.5.8 floor (ui.md §4.3), so the `::before` extends the hit area 2px beyond the box on every side — jx-tab's close-button technique, the same shape jx-button carries — and the region that accepts the pointer is `--jx-control-h` tall while the row stays 20px. -2px is exact here where jx-button needs -3px: an absolutely positioned box is offset from the PADDING edge, and the label draws no border, so its padding edge is its drawn edge. The label needs `position: relative` here for the pseudo-element to measure against; the input inside is 14px and needs nothing of its own, because the label around it is the target. Held by target-size.test.ts and the conformance gate. `contain: layout` keeps the reach LOCAL, as it does on jx-button's and jx-action-button's small controls: an absolutely positioned box still counts toward the SCROLLABLE overflow of the nearest ancestor whose `overflow` isn't `visible`, invisible or not, so a small checkbox touching a scrolling host's edge would add 2px to that host's scrollable region and draw a scrollbar over content that fit. The price is the baseline: a layout-contained label exports none, so a row aligned on `baseline` puts the label's bottom edge on its text baseline, and a row that holds a small checkbox centres it (`align-items: center`) instead.",
|
|
150
178
|
"position": "relative",
|
|
151
179
|
"minHeight": "calc(var(--jx-control-h) - 4px)",
|
|
152
180
|
"fontSize": "var(--jx-text-sm)",
|
|
@@ -187,11 +215,16 @@
|
|
|
187
215
|
"aria-label": "${state.label || null}",
|
|
188
216
|
"aria-labelledby": "${state.labelledby || null}",
|
|
189
217
|
"aria-describedby": "${state.describedby || null}",
|
|
218
|
+
"aria-invalid": "${state.invalid ? 'true' : null}",
|
|
190
219
|
"tabindex": "${state.tabindex || null}",
|
|
191
220
|
"name": "${state.name || null}",
|
|
221
|
+
"value": "${state.value || null}",
|
|
192
222
|
"disabled": {
|
|
193
223
|
"$ref": "#/state/disabled"
|
|
194
224
|
},
|
|
225
|
+
"required": {
|
|
226
|
+
"$ref": "#/state/required"
|
|
227
|
+
},
|
|
195
228
|
"checked": {
|
|
196
229
|
"$ref": "#/state/checked"
|
|
197
230
|
}
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
"fontSize": "var(--jx-text-md)",
|
|
215
215
|
"lineHeight": "var(--jx-leading-md)",
|
|
216
216
|
"overflow": "auto",
|
|
217
|
+
"overscrollBehavior": "contain",
|
|
217
218
|
"&::backdrop": {
|
|
218
219
|
"background": "rgba(0, 0, 0, 0.4)"
|
|
219
220
|
},
|
|
@@ -234,7 +235,8 @@
|
|
|
234
235
|
"$description": "The one region that scrolls. Child combinators, not the descendant ones the parts below use, because a consumer's content is light DOM inside this same box and a surface may name a [part=\"body\"] of its own.",
|
|
235
236
|
"flex": "1 1 auto",
|
|
236
237
|
"minHeight": "0",
|
|
237
|
-
"overflowY": "auto"
|
|
238
|
+
"overflowY": "auto",
|
|
239
|
+
"overscrollBehavior": "contain"
|
|
238
240
|
},
|
|
239
241
|
"&[data-size=\"md\"] > [part=\"dialog\"]": {
|
|
240
242
|
"width": "min(90vw, 32rem)"
|
package/components/jx-field.json
CHANGED
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
"$src": "jx-ui:/behaviors/textfield.ts",
|
|
51
51
|
"$export": "nameFieldControl",
|
|
52
52
|
"description": "Mints the row's id stem and names the slotted control with it. A document cannot do either: the closed operator set has no counter, and no body can write a property on an element other than the one whose handler is running. It runs once, over what is slotted then: a control APPENDED to the row afterwards lands outside the control part, unplaced and unnamed. Keep the control in the document and switch its state rather than adding it later."
|
|
53
|
+
},
|
|
54
|
+
"onLabelClick": {
|
|
55
|
+
"$prototype": "Function",
|
|
56
|
+
"$src": "jx-ui:/behaviors/textfield.ts",
|
|
57
|
+
"$export": "focusFieldControl",
|
|
58
|
+
"description": "A click on the label moves focus to the control the row holds, as a label's for would. It only focuses: it never ticks a box or flips a switch, because a row's label sits a column away from its control. The control is found at the click, so one a kit element has since replaced is still reached."
|
|
53
59
|
}
|
|
54
60
|
},
|
|
55
61
|
"attributes": {
|
|
@@ -136,7 +142,10 @@
|
|
|
136
142
|
"part": "label",
|
|
137
143
|
"id": "${state.uid ? state.uid + '-label' : null}"
|
|
138
144
|
},
|
|
139
|
-
"textContent": "${state.label}"
|
|
145
|
+
"textContent": "${state.label}",
|
|
146
|
+
"onclick": {
|
|
147
|
+
"$ref": "#/state/onLabelClick"
|
|
148
|
+
}
|
|
140
149
|
},
|
|
141
150
|
{
|
|
142
151
|
"tagName": "div",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"labelledby",
|
|
18
18
|
"describedby",
|
|
19
19
|
"disabled",
|
|
20
|
+
"readonly",
|
|
21
|
+
"required",
|
|
22
|
+
"invalid",
|
|
20
23
|
"stepper",
|
|
21
24
|
"step-up-label",
|
|
22
25
|
"step-down-label",
|
|
@@ -78,6 +81,24 @@
|
|
|
78
81
|
"attribute": "disabled",
|
|
79
82
|
"description": "Cannot be edited, stepped or focused."
|
|
80
83
|
},
|
|
84
|
+
"readonly": {
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"default": false,
|
|
87
|
+
"attribute": "readonly",
|
|
88
|
+
"description": "The value can be read, selected and submitted and not changed: forwarded to the control, and the stepper buttons and the shifted arrows stand down with it, because the platform's own stepUp() ignores readonly."
|
|
89
|
+
},
|
|
90
|
+
"required": {
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"default": false,
|
|
93
|
+
"attribute": "required",
|
|
94
|
+
"description": "Forwarded to the control, which is the element that validates: an empty required field fails the form's own check."
|
|
95
|
+
},
|
|
96
|
+
"invalid": {
|
|
97
|
+
"type": "boolean",
|
|
98
|
+
"default": false,
|
|
99
|
+
"attribute": "invalid",
|
|
100
|
+
"description": "The value is refused: the control says aria-invalid and draws in the danger colour."
|
|
101
|
+
},
|
|
81
102
|
"stepper": {
|
|
82
103
|
"type": "boolean",
|
|
83
104
|
"default": false,
|
|
@@ -205,7 +226,8 @@
|
|
|
205
226
|
"data-size": "${state.size}",
|
|
206
227
|
"data-disabled": "${state.disabled ? '' : null}",
|
|
207
228
|
"data-stepper": "${state.stepper ? '' : null}",
|
|
208
|
-
"data-bad-input": "${state.badInput ? '' : null}"
|
|
229
|
+
"data-bad-input": "${state.badInput ? '' : null}",
|
|
230
|
+
"data-invalid": "${state.invalid ? '' : null}"
|
|
209
231
|
},
|
|
210
232
|
"style": {
|
|
211
233
|
"display": "inline-flex",
|
|
@@ -263,6 +285,13 @@
|
|
|
263
285
|
"cursor": "default"
|
|
264
286
|
}
|
|
265
287
|
},
|
|
288
|
+
"&[data-invalid] [part=\"input\"]": {
|
|
289
|
+
"borderColor": "var(--jx-danger)"
|
|
290
|
+
},
|
|
291
|
+
"& [part=\"input\"]:user-invalid:is(:out-of-range, :required)": {
|
|
292
|
+
"$description": "The control's own verdict, once the reader has finished with it: a number outside min and max, or a required field left empty. Not a bare :user-invalid, which is also true of a step mismatch — and a field with no step attribute has a step of one, so every fraction a reader typed into it would draw as an error.",
|
|
293
|
+
"borderColor": "var(--jx-danger)"
|
|
294
|
+
},
|
|
266
295
|
"& [part=\"stepper-slot\"]": {
|
|
267
296
|
"display": "contents"
|
|
268
297
|
},
|
|
@@ -332,6 +361,7 @@
|
|
|
332
361
|
"aria-label": "${state.label || null}",
|
|
333
362
|
"aria-labelledby": "${state.labelledby || null}",
|
|
334
363
|
"aria-describedby": "${state.describedby || null}",
|
|
364
|
+
"aria-invalid": "${state.invalid ? 'true' : null}",
|
|
335
365
|
"name": "${state.name || null}",
|
|
336
366
|
"min": "${state.min || null}",
|
|
337
367
|
"max": "${state.max || null}",
|
|
@@ -339,6 +369,12 @@
|
|
|
339
369
|
"disabled": {
|
|
340
370
|
"$ref": "#/state/disabled"
|
|
341
371
|
},
|
|
372
|
+
"readonly": {
|
|
373
|
+
"$ref": "#/state/readonly"
|
|
374
|
+
},
|
|
375
|
+
"required": {
|
|
376
|
+
"$ref": "#/state/required"
|
|
377
|
+
},
|
|
342
378
|
"value": "${state.value}"
|
|
343
379
|
},
|
|
344
380
|
"value": {
|
|
@@ -378,9 +414,7 @@
|
|
|
378
414
|
"type": "button",
|
|
379
415
|
"tabindex": "-1",
|
|
380
416
|
"aria-label": "${state.stepUpLabel}",
|
|
381
|
-
"disabled": {
|
|
382
|
-
"$ref": "#/state/disabled"
|
|
383
|
-
}
|
|
417
|
+
"disabled": "${state.disabled || state.readonly}"
|
|
384
418
|
},
|
|
385
419
|
"onclick": {
|
|
386
420
|
"$ref": "#/state/onStepUp"
|
|
@@ -408,9 +442,7 @@
|
|
|
408
442
|
"type": "button",
|
|
409
443
|
"tabindex": "-1",
|
|
410
444
|
"aria-label": "${state.stepDownLabel}",
|
|
411
|
-
"disabled": {
|
|
412
|
-
"$ref": "#/state/disabled"
|
|
413
|
-
}
|
|
445
|
+
"disabled": "${state.disabled || state.readonly}"
|
|
414
446
|
},
|
|
415
447
|
"onclick": {
|
|
416
448
|
"$ref": "#/state/onStepDown"
|
|
@@ -264,6 +264,14 @@
|
|
|
264
264
|
"&[aria-selected=\"true\"] [part=\"description\"], &[aria-selected=\"true\"] [part=\"end\"], &[aria-selected=\"true\"] [part=\"icon\"]": {
|
|
265
265
|
"$description": "A highlighted row draws on the accent, and a muted grey on an accent fails SC 1.4.3. Every channel takes the row's own ink instead of keeping its own.",
|
|
266
266
|
"color": "inherit"
|
|
267
|
+
},
|
|
268
|
+
"@(forced-colors: active)": {
|
|
269
|
+
"&[aria-selected=\"true\"]": {
|
|
270
|
+
"$description": "The highlighted row, redrawn. Forced colours replace the accent fill with the list's own Canvas, so the caret would move through a menu, a listbox or a combobox with nothing on screen following it. The system's selection pair says it instead, as jx-action-group does for its chosen segment; the description, end and icon already inherit the row's ink.",
|
|
271
|
+
"forcedColorAdjust": "none",
|
|
272
|
+
"background": "Highlight",
|
|
273
|
+
"color": "HighlightText"
|
|
274
|
+
}
|
|
267
275
|
}
|
|
268
276
|
},
|
|
269
277
|
"children": [
|
|
@@ -102,6 +102,11 @@
|
|
|
102
102
|
"default": "",
|
|
103
103
|
"description": "This element's own id stem, minted on mount. The error and help sentences take their ids from it, and the control names those ids in aria-describedby."
|
|
104
104
|
},
|
|
105
|
+
"plain": {
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"default": false,
|
|
108
|
+
"description": "Written at mount, never authored: the engine has no appearance: base-select, so the control is the platform's plain select. A plain select draws an option as its text and nothing else, which would run a row's label and its description together into one word, so the description is left out of the row while this is true. Faces, swatches and group headings are drawing only and simply do not appear."
|
|
109
|
+
},
|
|
105
110
|
"hasError": {
|
|
106
111
|
"$expression": {
|
|
107
112
|
"operator": "!==",
|
|
@@ -266,6 +271,10 @@
|
|
|
266
271
|
"&[data-invalid] select": {
|
|
267
272
|
"borderColor": "var(--jx-danger)"
|
|
268
273
|
},
|
|
274
|
+
"& select:user-invalid": {
|
|
275
|
+
"$description": "The control's own verdict after the reader has been through it, drawn as the host's is: a required select left on its empty row.",
|
|
276
|
+
"borderColor": "var(--jx-danger)"
|
|
277
|
+
},
|
|
269
278
|
"& [part=\"trigger\"]": {
|
|
270
279
|
"display": "flex",
|
|
271
280
|
"alignItems": "center",
|
|
@@ -500,9 +509,9 @@
|
|
|
500
509
|
"tagName": "span",
|
|
501
510
|
"attributes": {
|
|
502
511
|
"part": "description",
|
|
503
|
-
"hidden": "${!$map.item.description}"
|
|
512
|
+
"hidden": "${state.plain || !$map.item.description}"
|
|
504
513
|
},
|
|
505
|
-
"textContent": "${$map.item.description || ''}"
|
|
514
|
+
"textContent": "${state.plain ? '' : $map.item.description || ''}"
|
|
506
515
|
}
|
|
507
516
|
]
|
|
508
517
|
}
|
|
@@ -600,9 +609,9 @@
|
|
|
600
609
|
"tagName": "span",
|
|
601
610
|
"attributes": {
|
|
602
611
|
"part": "description",
|
|
603
|
-
"hidden": "${!$map.item.description}"
|
|
612
|
+
"hidden": "${state.plain || !$map.item.description}"
|
|
604
613
|
},
|
|
605
|
-
"textContent": "${$map.item.description || ''}"
|
|
614
|
+
"textContent": "${state.plain ? '' : $map.item.description || ''}"
|
|
606
615
|
}
|
|
607
616
|
]
|
|
608
617
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
"observedAttributes": [
|
|
6
6
|
"label",
|
|
7
7
|
"labelledby",
|
|
8
|
+
"describedby",
|
|
9
|
+
"name",
|
|
8
10
|
"value",
|
|
9
11
|
"min",
|
|
10
12
|
"max",
|
|
@@ -25,6 +27,18 @@
|
|
|
25
27
|
"attribute": "labelledby",
|
|
26
28
|
"description": "Forwarded to the inner range as aria-labelledby, for a visible label the element does not own."
|
|
27
29
|
},
|
|
30
|
+
"describedby": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"default": "",
|
|
33
|
+
"attribute": "describedby",
|
|
34
|
+
"description": "The id of the sentence that describes the control, forwarded as its aria-describedby. A jx-field row writes it for a slider slotted into one."
|
|
35
|
+
},
|
|
36
|
+
"name": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": "",
|
|
39
|
+
"attribute": "name",
|
|
40
|
+
"description": "The name the value is submitted under, forwarded to the control."
|
|
41
|
+
},
|
|
28
42
|
"value": {
|
|
29
43
|
"type": "number",
|
|
30
44
|
"default": 0,
|
|
@@ -188,6 +202,8 @@
|
|
|
188
202
|
"type": "range",
|
|
189
203
|
"aria-label": "${state.label || null}",
|
|
190
204
|
"aria-labelledby": "${state.labelledby || null}",
|
|
205
|
+
"aria-describedby": "${state.describedby || null}",
|
|
206
|
+
"name": "${state.name || null}",
|
|
191
207
|
"aria-valuetext": "${state.valuetext || null}",
|
|
192
208
|
"min": "${state.min}",
|
|
193
209
|
"max": "${state.max}",
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
"observedAttributes": [
|
|
6
6
|
"checked",
|
|
7
7
|
"disabled",
|
|
8
|
+
"required",
|
|
8
9
|
"hint",
|
|
9
10
|
"label",
|
|
10
11
|
"labelledby",
|
|
11
12
|
"describedby",
|
|
12
13
|
"name",
|
|
14
|
+
"value",
|
|
13
15
|
"size"
|
|
14
16
|
],
|
|
15
17
|
"$elements": [
|
|
@@ -30,6 +32,12 @@
|
|
|
30
32
|
"attribute": "disabled",
|
|
31
33
|
"description": "Cannot be flipped or focused. Disabled, never absent, and the hint moves from the tip to the row's title while it is: a disabled control cannot open a tip, and why the setting cannot be changed is exactly what the reader hovers to find out."
|
|
32
34
|
},
|
|
35
|
+
"required": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false,
|
|
38
|
+
"attribute": "required",
|
|
39
|
+
"description": "Forwarded to the control: a required switch left off fails the form's own check."
|
|
40
|
+
},
|
|
33
41
|
"hint": {
|
|
34
42
|
"type": "string",
|
|
35
43
|
"default": "",
|
|
@@ -60,6 +68,12 @@
|
|
|
60
68
|
"attribute": "name",
|
|
61
69
|
"description": "The form field name, forwarded to the control."
|
|
62
70
|
},
|
|
71
|
+
"value": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"default": "",
|
|
74
|
+
"attribute": "value",
|
|
75
|
+
"description": "What a switch that is on submits under its name. Empty leaves the platform's own \"on\"."
|
|
76
|
+
},
|
|
63
77
|
"size": {
|
|
64
78
|
"type": "string",
|
|
65
79
|
"default": "md",
|
|
@@ -272,9 +286,13 @@
|
|
|
272
286
|
"aria-describedby": "${[state.hasTip ? state.uid + '-tip' : null, state.describedby || null].filter(Boolean).join(' ') || null}",
|
|
273
287
|
"tabindex": "${state.tabindex || null}",
|
|
274
288
|
"name": "${state.name || null}",
|
|
289
|
+
"value": "${state.value || null}",
|
|
275
290
|
"disabled": {
|
|
276
291
|
"$ref": "#/state/disabled"
|
|
277
292
|
},
|
|
293
|
+
"required": {
|
|
294
|
+
"$ref": "#/state/required"
|
|
295
|
+
},
|
|
278
296
|
"checked": {
|
|
279
297
|
"$ref": "#/state/checked"
|
|
280
298
|
}
|
|
@@ -278,7 +278,26 @@
|
|
|
278
278
|
"Escape": [
|
|
279
279
|
{
|
|
280
280
|
"if": {
|
|
281
|
-
"
|
|
281
|
+
"operator": "&&",
|
|
282
|
+
"target": {
|
|
283
|
+
"operator": "&&",
|
|
284
|
+
"target": {
|
|
285
|
+
"operator": "!",
|
|
286
|
+
"target": {
|
|
287
|
+
"$ref": "event#/isComposing"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"value": {
|
|
291
|
+
"operator": "!==",
|
|
292
|
+
"target": {
|
|
293
|
+
"$ref": "event#/keyCode"
|
|
294
|
+
},
|
|
295
|
+
"value": 229
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"value": {
|
|
299
|
+
"$ref": "#/state/showClear"
|
|
300
|
+
}
|
|
282
301
|
},
|
|
283
302
|
"then": [
|
|
284
303
|
{
|
|
@@ -307,16 +326,35 @@
|
|
|
307
326
|
"if": {
|
|
308
327
|
"operator": "&&",
|
|
309
328
|
"target": {
|
|
310
|
-
"operator": "
|
|
329
|
+
"operator": "&&",
|
|
311
330
|
"target": {
|
|
312
|
-
"
|
|
331
|
+
"operator": "!",
|
|
332
|
+
"target": {
|
|
333
|
+
"$ref": "event#/isComposing"
|
|
334
|
+
}
|
|
313
335
|
},
|
|
314
|
-
"value":
|
|
336
|
+
"value": {
|
|
337
|
+
"operator": "!==",
|
|
338
|
+
"target": {
|
|
339
|
+
"$ref": "event#/keyCode"
|
|
340
|
+
},
|
|
341
|
+
"value": 229
|
|
342
|
+
}
|
|
315
343
|
},
|
|
316
344
|
"value": {
|
|
317
|
-
"operator": "
|
|
345
|
+
"operator": "&&",
|
|
318
346
|
"target": {
|
|
319
|
-
"
|
|
347
|
+
"operator": "===",
|
|
348
|
+
"target": {
|
|
349
|
+
"$ref": "#/state/type"
|
|
350
|
+
},
|
|
351
|
+
"value": "search"
|
|
352
|
+
},
|
|
353
|
+
"value": {
|
|
354
|
+
"operator": "!",
|
|
355
|
+
"target": {
|
|
356
|
+
"$ref": "#/state/multiline"
|
|
357
|
+
}
|
|
320
358
|
}
|
|
321
359
|
}
|
|
322
360
|
},
|
|
@@ -340,7 +378,7 @@
|
|
|
340
378
|
"description": "Escape finished emptying a clearable field; e.target is the field."
|
|
341
379
|
}
|
|
342
380
|
],
|
|
343
|
-
"description": "Two keys, and nothing else. Escape empties a clearable field that has something in it, and cancels so the key does not also close whatever the field sits in. Enter is cancelled on a single-line search field: a search input inside a form submits it, and a search field is the one kind that never means to — the element owns no form, so the guard lives here rather than at every call site. A textarea never submits on Enter, so a multiline field keeps the key and the reader keeps the newline."
|
|
381
|
+
"description": "Two keys, and nothing else. Escape empties a clearable field that has something in it, and cancels so the key does not also close whatever the field sits in. Enter is cancelled on a single-line search field: a search input inside a form submits it, and a search field is the one kind that never means to — the element owns no form, so the guard lives here rather than at every call site. A textarea never submits on Enter, so a multiline field keeps the key and the reader keeps the newline. A key pressed while an input method is composing belongs to the input method: the Enter that accepts a candidate and the Escape that dismisses the candidate list are neither a submit nor a clear, so both cases stand down while event.isComposing is true or the key code is 229, which is how the engines that have already ended the composition still mark its last key."
|
|
344
382
|
}
|
|
345
383
|
},
|
|
346
384
|
"onkeydown": {
|
|
@@ -435,6 +473,10 @@
|
|
|
435
473
|
"&[data-invalid] [part=\"input\"]": {
|
|
436
474
|
"borderColor": "var(--jx-danger)"
|
|
437
475
|
},
|
|
476
|
+
"& [part=\"input\"]:user-invalid": {
|
|
477
|
+
"$description": "The control's OWN verdict, drawn the way the host's is: a required field left empty or an address that is not one draws the danger border once the reader has finished with it. :user-invalid and never :invalid, which is true of an untouched required field before anyone has typed.",
|
|
478
|
+
"borderColor": "var(--jx-danger)"
|
|
479
|
+
},
|
|
438
480
|
"&[data-clearable] [part=\"input\"]": {
|
|
439
481
|
"::-webkit-search-cancel-button": {
|
|
440
482
|
"appearance": "none"
|
|
@@ -447,7 +489,7 @@
|
|
|
447
489
|
"fontSize": "var(--jx-text-lg)"
|
|
448
490
|
},
|
|
449
491
|
"& [part=\"clear\"]": {
|
|
450
|
-
"$description": "The clear button sits inside the field's border, so it draws 2px under the field's height: 22px in a 24px field, and 18px in an `sm` one. The `::before` is its hit area (WCAG 2.2 SC 2.5.8, ui.md
|
|
492
|
+
"$description": "The clear button sits inside the field's border, so it draws 2px under the field's height: 22px in a 24px field, and 18px in an `sm` one. The `::before` is its hit area (WCAG 2.2 SC 2.5.8, ui.md §4.3): an empty pseudo-element inset by half the difference between the drawn box and the kit's control height, which is -1px on a default field and -3px on a small one, so the region that accepts the pointer is `--jx-control-h` square at every size while the glyph and its hover box stay where they were. On a large field the difference is positive and the pseudo-element is an invisible box INSIDE a button that is already over the floor, which changes nothing. jx-tab's close button is the technique; the button is absolutely positioned already, so it is its own containing block. The extension lies over the input it clears, which was true of the drawn button too, and 2px past the field's edge at `sm`, where a neighbour at the kit's 4px gap extends 2px back and the two abut. `contain: layout` keeps the expansion LOCAL: an absolutely positioned box still counts toward the SCROLLABLE overflow of the nearest ancestor whose `overflow` is not `visible`, invisible or not, so a host that scrolls picks up a couple of phantom pixels from every `::before` like this one between it and the field, and draws a scrollbar over content that already fit.",
|
|
451
493
|
"position": "absolute",
|
|
452
494
|
"insetBlockStart": "1px",
|
|
453
495
|
"insetInlineEnd": "1px",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The Jx UI kit: interface elements authored as Jx documents, with their theme and icons",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"upgrade": "bun update --latest && bun install"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@jxsuite/runtime": "^
|
|
62
|
-
"@jxsuite/schema": "^
|
|
61
|
+
"@jxsuite/runtime": "^7.0.0",
|
|
62
|
+
"@jxsuite/schema": "^6.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@happy-dom/global-registrator": "^21.0.4",
|
package/project.json
CHANGED
|
@@ -129,17 +129,15 @@
|
|
|
129
129
|
"--jx-radius-md": "6px",
|
|
130
130
|
"--jx-radius-lg": "10px",
|
|
131
131
|
"--jx-focus-ring": "2px solid var(--jx-accent)",
|
|
132
|
-
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px rgb(0 0 0 / 0.6), 0 2px 6px rgb(0 0 0 / 0.3)",
|
|
133
|
-
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px rgb(0 0 0 / 0.7)",
|
|
132
|
+
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px light-dark(rgb(0 0 0 / 0.18), rgb(0 0 0 / 0.6)), 0 2px 6px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.3))",
|
|
133
|
+
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px light-dark(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.7))",
|
|
134
134
|
"--jx-dur-1": "150ms",
|
|
135
135
|
"--jx-dur-2": "200ms",
|
|
136
136
|
"--jx-ease-out": "cubic-bezier(0.2, 0, 0, 1)",
|
|
137
137
|
"--jx-ease-in": "cubic-bezier(0.4, 0, 1, 1)",
|
|
138
138
|
"colorScheme": "light dark",
|
|
139
139
|
"&[data-theme=\"light\"]": {
|
|
140
|
-
"colorScheme": "light"
|
|
141
|
-
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px rgb(0 0 0 / 0.18), 0 2px 6px rgb(0 0 0 / 0.1)",
|
|
142
|
-
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px rgb(0 0 0 / 0.25)"
|
|
140
|
+
"colorScheme": "light"
|
|
143
141
|
},
|
|
144
142
|
"&[data-theme=\"dark\"]": {
|
|
145
143
|
"colorScheme": "dark"
|
|
@@ -390,6 +390,15 @@ export function onComboboxKeydown(state: ComboboxState, event: KeyboardEvent): v
|
|
|
390
390
|
if (!host || event.ctrlKey || event.metaKey) {
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
|
+
/*
|
|
394
|
+
* An input method is composing: the arrows walk ITS candidate list and Enter accepts ITS
|
|
395
|
+
* candidate, so none of them is the reader's answer to this list yet. `keyCode` 229 is the same
|
|
396
|
+
* statement from the engines that fire the keydown before `compositionstart` sets the flag.
|
|
397
|
+
*/
|
|
398
|
+
// oxlint-disable-next-line typescript/no-deprecated -- the only signal Safari gives for the Enter that ends a composition.
|
|
399
|
+
if (event.isComposing || event.keyCode === 229) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
393
402
|
const rows = rowsOf(state);
|
|
394
403
|
const at = Number(state.activeIndex ?? -1);
|
|
395
404
|
switch (event.key) {
|
|
@@ -87,7 +87,9 @@ function announce(host: HTMLElement): void {
|
|
|
87
87
|
export function stepBy(state: NumberFieldState, event: Event, n: number): void {
|
|
88
88
|
const host = hostOf(event);
|
|
89
89
|
const control = host ? controlOf(host) : null;
|
|
90
|
-
|
|
90
|
+
/* `stepUp()` and `stepDown()` are script, and script may write a readonly control: the platform
|
|
91
|
+
only stops the reader's own keys. The element steps on the reader's behalf, so it stops too. */
|
|
92
|
+
if (!host || !control || control.readOnly) {
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
try {
|
package/src/behaviors/select.ts
CHANGED
|
@@ -55,6 +55,20 @@ export interface SelectState {
|
|
|
55
55
|
unlisted?: SelectRow[];
|
|
56
56
|
/** This element's own id stem, minted at mount. */
|
|
57
57
|
uid?: string;
|
|
58
|
+
/** Whether the engine draws a plain select: it has no `appearance: base-select`. */
|
|
59
|
+
plain?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether the engine can draw a select's own list, which is what every row's drawing depends on.
|
|
64
|
+
*
|
|
65
|
+
* Asked of the engine and never guessed from its name. An engine without the feature still gives a
|
|
66
|
+
* working control, with the platform's own list: it ignores the trigger button and the group
|
|
67
|
+
* headings, and draws each row as its text. That is why the one thing the fallback has to change is
|
|
68
|
+
* text: a row's label and its description are two spans, and read as text they are one word.
|
|
69
|
+
*/
|
|
70
|
+
export function drawsOwnList(): boolean {
|
|
71
|
+
return typeof CSS !== "undefined" && CSS.supports("appearance", "base-select");
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
/** How many stems have been minted, so no two instances share one. */
|
|
@@ -136,6 +150,7 @@ export function syncSelect(state: SelectState, sel: HTMLSelectElement): void {
|
|
|
136
150
|
export function mountSelect(state: SelectState, host: HTMLElement): void {
|
|
137
151
|
minted += 1;
|
|
138
152
|
state.uid = `jx-select-${minted}`;
|
|
153
|
+
state.plain = !drawsOwnList();
|
|
139
154
|
const sel = controlOf(host);
|
|
140
155
|
if (!sel) {
|
|
141
156
|
return;
|
|
@@ -109,6 +109,44 @@ export function mintFieldId(state: TextfieldState): void {
|
|
|
109
109
|
|
|
110
110
|
let namedRows = 0;
|
|
111
111
|
|
|
112
|
+
/** The native controls a reader can be IN, which is where a row's label leads. */
|
|
113
|
+
const FOCUSABLE_CONTROLS = "input:not([type=hidden]), select, textarea, button";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A click on a `jx-field`'s label moves focus to the control the row holds, which is what a `<label
|
|
117
|
+
* for>` does and what a reader aiming at the bigger target expects.
|
|
118
|
+
*
|
|
119
|
+
* It is done here rather than with a `for`, because `for` needs an id on the native control and the
|
|
120
|
+
* control a kit element draws is its own to replace: `jx-textfield` swaps an `<input>` for a
|
|
121
|
+
* `<textarea>` when `multiline` changes, and an id minted at mount would not survive that. So the
|
|
122
|
+
* control is looked up at the click: the slotted element itself when it is a native control,
|
|
123
|
+
* otherwise the first one it draws.
|
|
124
|
+
*
|
|
125
|
+
* The click moves focus and nothing else. It does not tick a box or flip a switch: the row's label
|
|
126
|
+
* sits a column away from its control and Studio's rows are dense, so a label that toggled would
|
|
127
|
+
* change a setting on a click that was aimed at reading it. A disabled control takes no focus, and
|
|
128
|
+
* the platform is what says so.
|
|
129
|
+
*
|
|
130
|
+
* @param _state The row's reactive scope, which this never reads.
|
|
131
|
+
* @param event The click on the row's label.
|
|
132
|
+
*/
|
|
133
|
+
export function focusFieldControl(_state: FieldRowState, event: Event): void {
|
|
134
|
+
const label = event.currentTarget;
|
|
135
|
+
if (!(label instanceof Element)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const control = label.nextElementSibling?.firstElementChild;
|
|
139
|
+
if (!control) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const target = control.matches(FOCUSABLE_CONTROLS)
|
|
143
|
+
? control
|
|
144
|
+
: control.querySelector(FOCUSABLE_CONTROLS);
|
|
145
|
+
if (target instanceof HTMLElement) {
|
|
146
|
+
target.focus();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
/** Whether an element's DEFINITION says it can be named — that it observes `labelledby`. */
|
|
113
151
|
function observesNaming(el: Element): boolean {
|
|
114
152
|
const def = customElements.get(el.localName);
|