@eifi1/ui-kit 0.22.0 → 0.24.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/README.md +42 -25
- package/dist/chart.d.ts +8 -7
- package/dist/components/amount-input.d.ts +34 -7
- package/dist/components/amount-input.js +14 -10
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +26 -0
- package/dist/components/autocomplete.js +164 -137
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -7
- package/dist/components/calculator.d.ts +8 -7
- package/dist/components/column-mapper.d.ts +235 -0
- package/dist/components/column-mapper.js +411 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +113 -3
- package/dist/components/combobox.js +456 -313
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -7
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -4
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -7
- package/dist/components/danger-confirm.js +64 -7
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +48 -2
- package/dist/components/entity-combobox.js +159 -114
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +8 -7
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +8 -7
- package/dist/components/file-dropzone.d.ts +8 -7
- package/dist/components/form-actions.d.ts +8 -7
- package/dist/components/form-actions.js +23 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -7
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -7
- package/dist/components/money-field.d.ts +11 -7
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +31 -2
- package/dist/components/multi-entity-combobox.js +152 -103
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -7
- package/dist/components/number-input.d.ts +8 -7
- package/dist/components/numpad-sheet.d.ts +8 -7
- package/dist/components/phone-input.d.ts +8 -7
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +8 -7
- package/dist/components/settings-fields.d.ts +8 -7
- package/dist/components/share-card.d.ts +8 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -7
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +5 -4
- package/dist/components/ui.js +60 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +185 -65
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -252
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +66 -287
- package/dist/feedback/feedback-thread.js +16 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
- package/dist/feedback.d.ts +65 -3
- package/dist/hooks/use-file-drop.d.ts +8 -7
- package/dist/i18n/defaults.d.ts +8 -7
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -7
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +8 -7
- package/dist/i18n/locales/de-CH.d.ts +8 -7
- package/dist/i18n/locales/en.d.ts +8 -7
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +8 -7
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -7
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -7
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -7
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -7
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -7
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +142 -13
- package/dist/rhf/fields.js +185 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -7
- package/dist/rhf.d.ts +9 -8
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +8 -7
- package/dist/shell/top-bar-brand.d.ts +8 -7
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +8 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +38 -9
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +8 -7
- package/package.json +1 -1
- package/src/components/amount-input.tsx +50 -14
- package/src/components/autocomplete.tsx +188 -116
- package/src/components/column-mapper.tsx +768 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +655 -335
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +274 -24
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +243 -129
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +52 -5
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +221 -121
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +114 -33
- package/src/feedback/feedback-attachment.tsx +372 -72
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +112 -9
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/strip-fade.ts +2 -2
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +421 -6
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
- package/src/components/field-anatomy.tsx +0 -190
|
@@ -51,6 +51,14 @@ import { Tooltip } from "./tooltip";
|
|
|
51
51
|
* where a tile would hide the text behind a bubble and repeat it.
|
|
52
52
|
*
|
|
53
53
|
* No strings of its own: every name is an item's `label`, the "none" tile's included.
|
|
54
|
+
*
|
|
55
|
+
* ## A tile that cannot be chosen
|
|
56
|
+
*
|
|
57
|
+
* `disabled` takes a tile out of the choice AND out of reach: the arrow keys skip it
|
|
58
|
+
* and Tab never lands on it, so a dimmed square says nothing about why. Give it a
|
|
59
|
+
* `disabledReason` instead and it stays reachable, `aria-disabled`, with the reason in
|
|
60
|
+
* its bubble and its description — and choosing it does nothing (0.23, the kit's later
|
|
61
|
+
* list after keksdose K3; {@link Button}'s `disabledReason`, one tile at a time).
|
|
54
62
|
*/
|
|
55
63
|
|
|
56
64
|
/** A tile's size: 28 / 32 / 44px — see {@link TILE_SIZE}. */
|
|
@@ -73,7 +81,19 @@ export interface TileItem<T extends string> {
|
|
|
73
81
|
/** Said after the name, and shown in the bubble under it. Marks the tile with a
|
|
74
82
|
* dot, so the note is visible without hovering every tile to find it. */
|
|
75
83
|
note?: string;
|
|
84
|
+
/** Out of the choice and out of reach: natively disabled, skipped by the arrow keys,
|
|
85
|
+
* never the tab stop. */
|
|
76
86
|
disabled?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Why this tile cannot be chosen right now — a colour another category already wears,
|
|
89
|
+
* a symbol only an owner may set. The tile LOOKS disabled but is `aria-disabled`, not
|
|
90
|
+
* `disabled`: Tab and the arrow keys still reach it, the reason shows in its bubble
|
|
91
|
+
* under its name (and its `note`) and describes it, and a click, Space / Enter or an
|
|
92
|
+
* arrow under automatic activation chooses nothing — the focus moves, the checked
|
|
93
|
+
* tile stays. Wins over `disabled`, as on {@link Button}; the whole group's
|
|
94
|
+
* `disabled` still wins over it.
|
|
95
|
+
*/
|
|
96
|
+
disabledReason?: ReactNode;
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
export interface TileRadioGroupProps<T extends string> {
|
|
@@ -119,6 +139,22 @@ const TILE_IDLE =
|
|
|
119
139
|
"border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]";
|
|
120
140
|
const TILE_SELECTED =
|
|
121
141
|
"border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]";
|
|
142
|
+
/** A locked tile: the disabled look without `disabled` — dimmed, a not-allowed cursor. */
|
|
143
|
+
const TILE_LOCKED = "cursor-not-allowed opacity-50";
|
|
144
|
+
/** …and no hover offer on an idle one, which cannot be chosen. Not on the selected
|
|
145
|
+
* tile, whose frame IS its state. */
|
|
146
|
+
const TILE_LOCKED_IDLE = "hover:border-[var(--border)] hover:bg-[var(--bg-surface)]";
|
|
147
|
+
|
|
148
|
+
/** `undefined`, `null`, `false` and `""` are no reason — what `cond && "…"` gives. */
|
|
149
|
+
function isLocked(item: TileItem<string>): boolean {
|
|
150
|
+
const r = item.disabledReason;
|
|
151
|
+
return r !== undefined && r !== null && r !== false && r !== "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Out of reach: natively disabled. A reason keeps a `disabled` tile reachable. */
|
|
155
|
+
function isOff(item: TileItem<string>): boolean {
|
|
156
|
+
return Boolean(item.disabled) && !isLocked(item);
|
|
157
|
+
}
|
|
122
158
|
|
|
123
159
|
/**
|
|
124
160
|
* The tiles of one radio group, without the group element — render them inside a
|
|
@@ -139,15 +175,17 @@ export function TileRadioGroup<T extends string>({
|
|
|
139
175
|
}: TileRadioGroupProps<T>) {
|
|
140
176
|
const refs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
141
177
|
const noteId = useId();
|
|
178
|
+
const reasonId = useId();
|
|
142
179
|
const s = TILE_SIZE[size];
|
|
143
180
|
const checkedIndex =
|
|
144
181
|
checked === undefined ? -1 : items.findIndex((it) => it.key === checked);
|
|
145
|
-
// The ONE tab stop: the checked tile, else the first that can be
|
|
146
|
-
// rule, so Tab into a group with nothing checked still lands somewhere.
|
|
182
|
+
// The ONE tab stop: the checked tile, else the first that can be reached — the APG
|
|
183
|
+
// rule, so Tab into a group with nothing checked still lands somewhere. A locked
|
|
184
|
+
// tile is reachable: landing on it is how its reason is read.
|
|
147
185
|
const tabStop =
|
|
148
|
-
checkedIndex >= 0 && !items[checkedIndex]
|
|
186
|
+
checkedIndex >= 0 && !isOff(items[checkedIndex])
|
|
149
187
|
? checkedIndex
|
|
150
|
-
: items.findIndex((it) => !it
|
|
188
|
+
: items.findIndex((it) => !isOff(it));
|
|
151
189
|
|
|
152
190
|
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
153
191
|
// Left and right are VISUAL directions: in a right-to-left page the row runs the
|
|
@@ -175,7 +213,8 @@ export function TileRadioGroup<T extends string>({
|
|
|
175
213
|
return;
|
|
176
214
|
}
|
|
177
215
|
e.preventDefault();
|
|
178
|
-
|
|
216
|
+
// Locked tiles are in the walk; natively disabled ones are not.
|
|
217
|
+
const enabled = items.map((it, i) => (isOff(it) ? -1 : i)).filter((i) => i >= 0);
|
|
179
218
|
if (!enabled.length) return;
|
|
180
219
|
const at = enabled.indexOf(index);
|
|
181
220
|
let target: number;
|
|
@@ -184,13 +223,17 @@ export function TileRadioGroup<T extends string>({
|
|
|
184
223
|
// Wrapping, as a native radio group does: the last tile's "next" is the first.
|
|
185
224
|
else target = enabled[(at + step + enabled.length) % enabled.length];
|
|
186
225
|
refs.current[target]?.focus();
|
|
187
|
-
|
|
226
|
+
// Onto a locked tile the focus moves and the choice does not: the checked tile
|
|
227
|
+
// stays checked, and the next arrow walks on from here.
|
|
228
|
+
if (activation === "automatic" && !isLocked(items[target])) onSelect(items[target].key);
|
|
188
229
|
};
|
|
189
230
|
|
|
190
231
|
return (
|
|
191
232
|
<>
|
|
192
233
|
{items.map((item, i) => {
|
|
193
234
|
const selected = i === checkedIndex;
|
|
235
|
+
const locked = isLocked(item);
|
|
236
|
+
const describedBy = [item.note && `${noteId}-${i}`, locked && `${reasonId}-${i}`].filter(Boolean).join(" ");
|
|
194
237
|
const button = (
|
|
195
238
|
<button
|
|
196
239
|
ref={(el) => {
|
|
@@ -201,18 +244,25 @@ export function TileRadioGroup<T extends string>({
|
|
|
201
244
|
aria-checked={selected}
|
|
202
245
|
aria-label={item.label}
|
|
203
246
|
// The note is a DESCRIPTION, read after the name and the state, not glued
|
|
204
|
-
// into the name — the punctuation between the two would be English's.
|
|
205
|
-
|
|
247
|
+
// into the name — the punctuation between the two would be English's. So
|
|
248
|
+
// is a lock's reason, after the note.
|
|
249
|
+
aria-describedby={describedBy || undefined}
|
|
206
250
|
tabIndex={i === tabStop ? 0 : -1}
|
|
207
|
-
disabled
|
|
251
|
+
// A reason wins over the tile's own `disabled` (as on Button): reachable,
|
|
252
|
+
// and refusing. The whole group's `disabled` still wins over both.
|
|
253
|
+
disabled={disabled || isOff(item)}
|
|
254
|
+
aria-disabled={(locked && !disabled) || undefined}
|
|
208
255
|
// Clicking the checked tile again does nothing: a radio is not a toggle.
|
|
209
|
-
// "None" is the way back to nothing, and it is a tile of its own.
|
|
210
|
-
|
|
256
|
+
// "None" is the way back to nothing, and it is a tile of its own. A locked
|
|
257
|
+
// tile is never chosen.
|
|
258
|
+
onClick={() => !selected && !locked && onSelect(item.key)}
|
|
211
259
|
onKeyDown={(e) => onKeyDown(e, i)}
|
|
212
260
|
className={cn(
|
|
213
261
|
TILE_BASE,
|
|
214
262
|
s.tile,
|
|
215
263
|
selected ? TILE_SELECTED : TILE_IDLE,
|
|
264
|
+
locked && TILE_LOCKED,
|
|
265
|
+
locked && !selected && TILE_LOCKED_IDLE,
|
|
216
266
|
tileClassName?.(item, selected),
|
|
217
267
|
)}
|
|
218
268
|
>
|
|
@@ -236,6 +286,14 @@ export function TileRadioGroup<T extends string>({
|
|
|
236
286
|
</span>
|
|
237
287
|
</>
|
|
238
288
|
)}
|
|
289
|
+
{/* The reason's description is a `hidden` copy, as on a locked Button: the
|
|
290
|
+
bubble exists only while hovered or focused once portalled, and a
|
|
291
|
+
description that comes and goes is read inconsistently. */}
|
|
292
|
+
{locked && (
|
|
293
|
+
<span id={`${reasonId}-${i}`} hidden>
|
|
294
|
+
{item.disabledReason}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
239
297
|
{selected && <SelectedTick />}
|
|
240
298
|
</button>
|
|
241
299
|
);
|
|
@@ -248,10 +306,13 @@ export function TileRadioGroup<T extends string>({
|
|
|
248
306
|
<Tooltip
|
|
249
307
|
key={item.key ?? "\u0000none"}
|
|
250
308
|
label={
|
|
251
|
-
item.note ? (
|
|
309
|
+
item.note || locked ? (
|
|
252
310
|
<>
|
|
253
311
|
<span className="block">{item.label}</span>
|
|
254
|
-
<span className="block font-normal text-[var(--text-muted)]">{item.note}</span>
|
|
312
|
+
{item.note && <span className="block font-normal text-[var(--text-muted)]">{item.note}</span>}
|
|
313
|
+
{/* The reason in the body colour: it is the news, the note is
|
|
314
|
+
standing context. */}
|
|
315
|
+
{locked && <span className="block font-normal">{item.disabledReason}</span>}
|
|
255
316
|
</>
|
|
256
317
|
) : (
|
|
257
318
|
item.label
|
|
@@ -5,7 +5,7 @@ import { horizontalStep } from "../lib/direction";
|
|
|
5
5
|
import { FIELD_INVALID, FloatingField, Label } from "./ui";
|
|
6
6
|
import { Tooltip } from "./tooltip";
|
|
7
7
|
import { useCommitReason } from "./write-lock";
|
|
8
|
-
import { hasContent, LabelStrip } from "./field-
|
|
8
|
+
import { hasContent, LabelStrip } from "./field-parts";
|
|
9
9
|
|
|
10
10
|
export interface ToggleOption<T extends string> {
|
|
11
11
|
value: T;
|
|
@@ -524,7 +524,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
524
524
|
}
|
|
525
525
|
if (strip) {
|
|
526
526
|
return (
|
|
527
|
-
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={dimmed} pad="
|
|
527
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={dimmed} pad="field" className={className}>
|
|
528
528
|
{group}
|
|
529
529
|
{captionEl}
|
|
530
530
|
{errorEl}
|
|
@@ -14,6 +14,9 @@ export interface TriggerAria {
|
|
|
14
14
|
"aria-labelledby"?: string;
|
|
15
15
|
"aria-describedby"?: string;
|
|
16
16
|
"aria-invalid"?: boolean | "true" | "false" | "grammar" | "spelling";
|
|
17
|
+
/** 0.23.0: a form library's `required` (RhfMonthPicker) is announced on the trigger,
|
|
18
|
+
* not lost on the wrapper. */
|
|
19
|
+
"aria-required"?: boolean | "true" | "false";
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** Split a field's props into what its trigger takes and what its wrapper takes. */
|
|
@@ -26,6 +29,7 @@ export function splitTriggerAria<T extends TriggerAria>(
|
|
|
26
29
|
"aria-labelledby": labelledBy,
|
|
27
30
|
"aria-describedby": describedBy,
|
|
28
31
|
"aria-invalid": invalid,
|
|
32
|
+
"aria-required": required,
|
|
29
33
|
...rest
|
|
30
34
|
} = props;
|
|
31
35
|
return [
|
|
@@ -35,6 +39,7 @@ export function splitTriggerAria<T extends TriggerAria>(
|
|
|
35
39
|
"aria-labelledby": labelledBy,
|
|
36
40
|
"aria-describedby": describedBy,
|
|
37
41
|
"aria-invalid": invalid,
|
|
42
|
+
"aria-required": required,
|
|
38
43
|
},
|
|
39
44
|
rest,
|
|
40
45
|
];
|
package/src/components/ui.tsx
CHANGED
|
@@ -17,9 +17,16 @@ import {
|
|
|
17
17
|
LockedReason,
|
|
18
18
|
useFieldHint,
|
|
19
19
|
useLockReason,
|
|
20
|
+
FLOATING_LABEL_STATIC,
|
|
21
|
+
STATIC_LABEL_TYPE,
|
|
20
22
|
type FieldHintParts,
|
|
21
23
|
} from "./field-parts";
|
|
22
24
|
|
|
25
|
+
// The static label lives in field-parts since 0.23 (it is what the field anatomy
|
|
26
|
+
// there draws, and field-parts must not import this file); public from here as before.
|
|
27
|
+
export { FieldLabel, FLOATING_LABEL_STATIC } from "./field-parts";
|
|
28
|
+
export type { FieldLabelProps } from "./field-parts";
|
|
29
|
+
|
|
23
30
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
|
|
24
31
|
|
|
25
32
|
// Shared base ring for every button-styled element. Kept as a named const so the
|
|
@@ -1483,22 +1490,6 @@ const FLOATING_ROW_CLASS = cn(
|
|
|
1483
1490
|
"peer-disabled:opacity-50",
|
|
1484
1491
|
);
|
|
1485
1492
|
|
|
1486
|
-
// The TYPE of the small static label, without any placement. Split out so the
|
|
1487
|
-
// label and anything sharing its line (see `hint` on {@link FloatingField}) are
|
|
1488
|
-
// laid out by one flex row instead of by two absolute offsets guessing at the
|
|
1489
|
-
// same baseline — which is what put dev#468's "?" three pixels above the word it
|
|
1490
|
-
// belongs to.
|
|
1491
|
-
const STATIC_LABEL_TYPE =
|
|
1492
|
-
"text-[11px] leading-tight text-[var(--text-muted)] peer-disabled:opacity-50";
|
|
1493
|
-
|
|
1494
|
-
// A field that always has a value (select / dropdown trigger) keeps the label
|
|
1495
|
-
// permanently in the floated position — small, in the top strip, value below.
|
|
1496
|
-
export const FLOATING_LABEL_STATIC = cn(
|
|
1497
|
-
"pointer-events-none absolute start-3 top-1",
|
|
1498
|
-
STATIC_LABEL_TYPE,
|
|
1499
|
-
"max-w-[calc(100%-1.5rem)] truncate",
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
1493
|
/**
|
|
1503
1494
|
* The one place that assembles a labelled field: a `relative` wrapper around the
|
|
1504
1495
|
* control (`children`) plus a floating label inside the top strip. Pass `staticLabel`
|
|
@@ -1650,23 +1641,6 @@ export function FieldHint({
|
|
|
1650
1641
|
);
|
|
1651
1642
|
}
|
|
1652
1643
|
|
|
1653
|
-
/**
|
|
1654
|
-
* The floating label for a custom-dropdown trigger (a `<span>`, since the trigger is a
|
|
1655
|
-
* button not a labelable input). Same placement as {@link FloatingField}'s static label,
|
|
1656
|
-
* so every labelled field lines up. Render inside a `relative` wrapper, before the trigger.
|
|
1657
|
-
*/
|
|
1658
|
-
export interface FieldLabelProps extends ComponentPropsWithoutRef<"span"> {
|
|
1659
|
-
children: ReactNode;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
export function FieldLabel({ children, className, ...rest }: FieldLabelProps) {
|
|
1663
|
-
return (
|
|
1664
|
-
<span {...rest} className={cn(FLOATING_LABEL_STATIC, "z-10", className)}>
|
|
1665
|
-
{children}
|
|
1666
|
-
</span>
|
|
1667
|
-
);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1644
|
export interface LabelProps extends ComponentPropsWithoutRef<"label"> {
|
|
1671
1645
|
/** Draw the required mark after the text. The mark is `aria-hidden`: what a
|
|
1672
1646
|
* screen reader hears is the control's own `required` / `aria-required`, which
|
|
@@ -2527,6 +2501,109 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
2527
2501
|
});
|
|
2528
2502
|
Select.displayName = "Select";
|
|
2529
2503
|
|
|
2504
|
+
/**
|
|
2505
|
+
* The backdrop of a labelled {@link Textarea}'s label strip (Kurvenschmiede, 0.24).
|
|
2506
|
+
*
|
|
2507
|
+
* The report: paste a long table into ColumnMapper, the field scrolls, and the floated
|
|
2508
|
+
* "Paste a table" / "Tabelle einfügen" sits ON the first visible line of text — on
|
|
2509
|
+
* desktop and on a phone. Not a ColumnMapper bug: every labelled textarea that scrolls
|
|
2510
|
+
* did it. An `<input>` has one line that never moves, so the strip FIELD_FLOATING_PAD
|
|
2511
|
+
* leaves for the label is always empty there; a textarea's top padding is part of its
|
|
2512
|
+
* SCROLLING area, and once the content scrolls the lines travel up through the strip
|
|
2513
|
+
* and under the label.
|
|
2514
|
+
*
|
|
2515
|
+
* The fix makes that padding stay put: this layer is a later sibling of the textarea
|
|
2516
|
+
* (so `peer-*` reaches it, and as a positioned box it paints above the textarea's
|
|
2517
|
+
* text), the label paints above it in turn, and it covers exactly the top padding —
|
|
2518
|
+
* `top-px` / `inset-x-px` inside the 1px border, `h-4` = `pt-4`. At rest it covers
|
|
2519
|
+
* padding and nothing else, so no pixel of an unscrolled line is ever hidden; scrolled
|
|
2520
|
+
* text slides out of sight under it instead of through the label.
|
|
2521
|
+
*
|
|
2522
|
+
* Why a layer and not "start the scrolling area below the strip" (a wrapper that draws
|
|
2523
|
+
* the border and background, the textarea transparent inside it): the box would stop
|
|
2524
|
+
* being the textarea. Its focus outline, its brand border on focus, the invalid ring,
|
|
2525
|
+
* the disabled and read-only greys and every caller's `[&_textarea]:…` would all have
|
|
2526
|
+
* to be rebuilt on the wrapper with `focus-within:` / `has-[…]:`, and a caller's
|
|
2527
|
+
* `style={{ height }}` would size the scroller rather than the field. The layer leaves
|
|
2528
|
+
* all of that exactly where it was.
|
|
2529
|
+
*
|
|
2530
|
+
* What it has to match, it matches from the same tokens the field uses: the surface is
|
|
2531
|
+
* FIELD_BASE's `--bg-surface`, and `--bg-surface-2` for `disabled` and `[readonly]` —
|
|
2532
|
+
* on the same ATTRIBUTE selector FIELD_BASE uses, for the reason given there. Focus and
|
|
2533
|
+
* `invalid` change only the border and the ring, which are outside the layer, so they
|
|
2534
|
+
* need nothing. Both themes come with the variables. The inner corners are the field's
|
|
2535
|
+
* radius (`rounded-md`, 6px) less the 1px border, so the rounded border is never cut
|
|
2536
|
+
* into — a literal 5px rather than `var(--radius-md)`, which Tailwind emits only where
|
|
2537
|
+
* a utility uses it and the kit's token check refuses (token-vars-declared).
|
|
2538
|
+
*
|
|
2539
|
+
* Hidden (`display: none`) until the label floats: an empty, unfocused field has
|
|
2540
|
+
* nothing that could scroll, and it renders exactly as it did before 0.24. Pointer
|
|
2541
|
+
* events pass through, so a click on the strip still lands in the field.
|
|
2542
|
+
*
|
|
2543
|
+
* The one thing CSS cannot know is a classic scrollbar's width, and a strip across the
|
|
2544
|
+
* whole inner width would hide the scrollbar's top arrow — see {@link TextareaLabelStrip}.
|
|
2545
|
+
*/
|
|
2546
|
+
const TEXTAREA_LABEL_STRIP = cn(
|
|
2547
|
+
"pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[5px] bg-[var(--bg-surface)]",
|
|
2548
|
+
"peer-focus:block peer-[:not(:placeholder-shown)]:block",
|
|
2549
|
+
"peer-disabled:bg-[var(--bg-surface-2)] peer-[[readonly]]:bg-[var(--bg-surface-2)]",
|
|
2550
|
+
);
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* {@link TEXTAREA_LABEL_STRIP}, stopped short of a classic scrollbar.
|
|
2554
|
+
*
|
|
2555
|
+
* A Windows / Linux desktop draws a textarea's vertical scrollbar INSIDE the border, in
|
|
2556
|
+
* a gutter it takes from the content box (15px in Chromium); a strip across the full
|
|
2557
|
+
* inner width would sit on top of the scrollbar's up arrow. So the gutter is measured —
|
|
2558
|
+
* border box less client width less the borders — and the strip ends where it begins.
|
|
2559
|
+
* On whichever side it is: `clientLeft` includes a gutter on the LEFT, which is where
|
|
2560
|
+
* Chromium and Firefox put it in a right-to-left field. The side is applied as an
|
|
2561
|
+
* inline-start / inline-end inset, so when `dir` flips afterwards and the scrollbar
|
|
2562
|
+
* follows it to the other edge, the strip does too without a re-measure. That corner
|
|
2563
|
+
* also loses its rounding, since it no longer meets the border.
|
|
2564
|
+
*
|
|
2565
|
+
* Overlay scrollbars (macOS, phones) take no gutter: the measurement is zero and the
|
|
2566
|
+
* strip spans the field. A ResizeObserver on the textarea re-measures when the
|
|
2567
|
+
* scrollbar comes or goes (it takes the content box with it) and when the field is
|
|
2568
|
+
* resized by hand. Not laid out — hidden in a closed panel, or jsdom — the CSS insets
|
|
2569
|
+
* stand until it is.
|
|
2570
|
+
*/
|
|
2571
|
+
function TextareaLabelStrip() {
|
|
2572
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
2573
|
+
useLayoutEffect(() => {
|
|
2574
|
+
const strip = ref.current;
|
|
2575
|
+
const field = strip?.previousElementSibling;
|
|
2576
|
+
if (!strip || !(field instanceof HTMLTextAreaElement)) return;
|
|
2577
|
+
const fit = () => {
|
|
2578
|
+
if (field.offsetWidth === 0) return;
|
|
2579
|
+
const style = getComputedStyle(field);
|
|
2580
|
+
const borderLeft = parseFloat(style.borderLeftWidth) || 0;
|
|
2581
|
+
const borderRight = parseFloat(style.borderRightWidth) || 0;
|
|
2582
|
+
const gutter = field.offsetWidth - field.clientWidth - borderLeft - borderRight;
|
|
2583
|
+
// offsetWidth and clientWidth are whole pixels, so a field with no scrollbar can
|
|
2584
|
+
// read ±1 here; no scrollbar is that thin.
|
|
2585
|
+
const bar = gutter > 1.5;
|
|
2586
|
+
const onLeft = field.clientLeft - borderLeft > 1.5;
|
|
2587
|
+
// Kept as a LOGICAL side: a `dir` flipped later (a language switch) moves the
|
|
2588
|
+
// scrollbar across without resizing anything, so nothing would re-measure —
|
|
2589
|
+
// but it stays at the inline end, and so does an inline-end inset.
|
|
2590
|
+
const rtl = style.direction === "rtl";
|
|
2591
|
+
const atEnd = onLeft === rtl;
|
|
2592
|
+
const border = (end: boolean) => (end === rtl ? borderLeft : borderRight);
|
|
2593
|
+
strip.style.insetInlineEnd = bar && atEnd ? `${border(true) + gutter}px` : "";
|
|
2594
|
+
strip.style.insetInlineStart = bar && !atEnd ? `${border(false) + gutter}px` : "";
|
|
2595
|
+
strip.style.borderStartEndRadius = bar && atEnd ? "0" : "";
|
|
2596
|
+
strip.style.borderStartStartRadius = bar && !atEnd ? "0" : "";
|
|
2597
|
+
};
|
|
2598
|
+
fit();
|
|
2599
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
2600
|
+
const observer = new ResizeObserver(fit);
|
|
2601
|
+
observer.observe(field);
|
|
2602
|
+
return () => observer.disconnect();
|
|
2603
|
+
}, []);
|
|
2604
|
+
return <span ref={ref} aria-hidden data-slot="label-strip" className={TEXTAREA_LABEL_STRIP} />;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2530
2607
|
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
2531
2608
|
label?: ReactNode;
|
|
2532
2609
|
/** See {@link Input}'s `invalid`. */
|
|
@@ -2617,6 +2694,10 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function
|
|
|
2617
2694
|
isInvalid && FIELD_INVALID,
|
|
2618
2695
|
)}
|
|
2619
2696
|
/>
|
|
2697
|
+
{/* Between the textarea and the label, on purpose: after the textarea so it
|
|
2698
|
+
is its `peer` and paints over its text, before the label so the label
|
|
2699
|
+
paints over it. See TEXTAREA_LABEL_STRIP. */}
|
|
2700
|
+
<TextareaLabelStrip />
|
|
2620
2701
|
</FloatingField>
|
|
2621
2702
|
</FieldGroup>
|
|
2622
2703
|
);
|