@juwel-development/design-system 3.8.0 → 3.9.1
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 +171 -0
- package/dist/design-system.js +669 -273
- package/dist/index.css +1 -1
- package/dist/types/Interaction/MultiSelect/MultiSelect.d.ts +102 -0
- package/dist/types/Interaction/MultiSelect/MultiSelectCompositionError.d.ts +3 -0
- package/dist/types/Interaction/MultiSelect/fitChips.d.ts +6 -0
- package/dist/types/Interaction/NumberInput/NumberInput.d.ts +34 -0
- package/dist/types/Interaction/Slider/Slider.d.ts +9 -4
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/Interaction/MultiSelect/MultiSelect.tsx +820 -0
- package/src/Interaction/MultiSelect/MultiSelectCompositionError.ts +6 -0
- package/src/Interaction/MultiSelect/fitChips.ts +27 -0
- package/src/Interaction/NumberInput/NumberInput.tsx +125 -0
- package/src/Interaction/Slider/Slider.tsx +34 -18
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -94,6 +94,177 @@ provided by Root, so callers do not compose another empty Option. Derive types w
|
|
|
94
94
|
`ComponentProps<typeof Select.Root>` or `ComponentProps<typeof Select.Option>` from React.
|
|
95
95
|
The consumer still awaits a published library release before changing its dependency.
|
|
96
96
|
|
|
97
|
+
## MultiSelect
|
|
98
|
+
|
|
99
|
+
`MultiSelect` is a compound namespace for selecting zero, one or several options
|
|
100
|
+
independently from a finite set: `MultiSelect.Root` renders a labelled one-line dropdown
|
|
101
|
+
trigger with the selected options as individually removable chips, a count for the chips
|
|
102
|
+
that do not fit, and a clear-all control; `MultiSelect.Option` renders one checkable row in
|
|
103
|
+
the dropdown. The consumer owns the options, the selection, every word and what the selected
|
|
104
|
+
set means; the library owns the control, the selection semantics, focus and the presentation.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { MultiSelect } from '@juwel-development/design-system';
|
|
108
|
+
import { BehaviorSubject, Subject } from 'rxjs';
|
|
109
|
+
|
|
110
|
+
const topics$ = new BehaviorSubject<readonly string[]>([]);
|
|
111
|
+
const topicChange$ = new Subject<readonly string[]>();
|
|
112
|
+
topicChange$.subscribe((next) => topics$.next(next));
|
|
113
|
+
|
|
114
|
+
<MultiSelect.Root
|
|
115
|
+
label={'Main topics'}
|
|
116
|
+
selected$={topics$}
|
|
117
|
+
onChange$={topicChange$}
|
|
118
|
+
emptyLabel={'No topics selected'}
|
|
119
|
+
removeLabel={'Remove {label}'}
|
|
120
|
+
clearLabel={'Clear topics'}
|
|
121
|
+
overflowLabel={'+{count}'}
|
|
122
|
+
hint={'Songs match any selected topic.'}
|
|
123
|
+
>
|
|
124
|
+
<MultiSelect.Option value={'family'}>{'Family'}</MultiSelect.Option>
|
|
125
|
+
<MultiSelect.Option value={'love'}>{'Love'}</MultiSelect.Option>
|
|
126
|
+
</MultiSelect.Root>;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The same control in German changes only the caller's wording:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<MultiSelect.Root
|
|
133
|
+
label={'Hauptthemen'}
|
|
134
|
+
selected$={topics$}
|
|
135
|
+
onChange$={topicChange$}
|
|
136
|
+
emptyLabel={'Keine Themen ausgewählt'}
|
|
137
|
+
removeLabel={'{label} entfernen'}
|
|
138
|
+
clearLabel={'Themen zurücksetzen'}
|
|
139
|
+
overflowLabel={'{count} weitere'}
|
|
140
|
+
>
|
|
141
|
+
<MultiSelect.Option value={'family'}>{'Familie'}</MultiSelect.Option>
|
|
142
|
+
<MultiSelect.Option value={'love'}>{'Liebe'}</MultiSelect.Option>
|
|
143
|
+
</MultiSelect.Root>;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`Root` requires `label`, `selected$`, `onChange$`, `emptyLabel`, `removeLabel`, `clearLabel`
|
|
147
|
+
and `overflowLabel`; `hint`, `disabled`, `testId` and `children` are optional. Each `Option`
|
|
148
|
+
requires a unique, stable string `value` and a text-only `children` label, and accepts a
|
|
149
|
+
`testId`. Options are direct children of `Root`; arrays and fragments are supported. The
|
|
150
|
+
wording contract is closed: `removeLabel` replaces `{label}` with the option's label to name a
|
|
151
|
+
chip's removal control, and `overflowLabel` replaces `{count}` with the number of selected
|
|
152
|
+
options hidden behind the count. There are no callback props and no built-in English; a
|
|
153
|
+
missing `hint` renders nothing.
|
|
154
|
+
|
|
155
|
+
**Streams.** `selected$` is a read-only `Observable<readonly string[]>` of the current
|
|
156
|
+
selection. Root renders the latest emission and nothing else: empty before the first
|
|
157
|
+
emission, empty again while a replaced source has not yet emitted, and updated silently on
|
|
158
|
+
every emission whether the dropdown is open, closed or disabled. Use a replaying source such
|
|
159
|
+
as a `BehaviorSubject` or `ReplaySubject(1)` so a remounted control shows the current state
|
|
160
|
+
at once. `onChange$` is a `Subject<readonly string[]>` that receives one fresh full proposed
|
|
161
|
+
selection per user edit - a toggle, a chip removal or clear-all - ordered by option order,
|
|
162
|
+
without duplicates and holding only supplied identities. Handed-in arrays are never mutated.
|
|
163
|
+
Rendering, opening, closing, option updates, source replacement and `selected$` emissions
|
|
164
|
+
never emit. Feed accepted proposals back into `selected$` immediately for ordinary
|
|
165
|
+
interaction; an unanswered proposal leaves the selection unchanged, and repeating the edit
|
|
166
|
+
repeats the proposal. Root subscribes only to `selected$`, unsubscribes on replacement and
|
|
167
|
+
unmount, and never completes either stream or assigns behaviour to their errors or completion.
|
|
168
|
+
|
|
169
|
+
**Caller obligations.** Keep option identities stable across reordering and translation, so
|
|
170
|
+
selection is preserved by identity; map options with the value as the React key. `selected$`
|
|
171
|
+
names supplied identities only: when an update removes options, remove their identities from
|
|
172
|
+
the selection in the same logical update. MultiSelect prunes nothing, invents nothing and
|
|
173
|
+
emits no synthetic change to reconcile invalid input.
|
|
174
|
+
|
|
175
|
+
**Behaviour.** The closed control stays on one line at every width: the leading chips that
|
|
176
|
+
fit are shown in option order, the rest are counted by `overflowLabel`, and at narrow widths
|
|
177
|
+
only the count remains. Overflow is recalculated on width, label and selection changes
|
|
178
|
+
without touching the selection; activating the count opens the dropdown, so every selection
|
|
179
|
+
stays reachable. Long chip labels truncate visually while the removal control keeps the full
|
|
180
|
+
name. The dropdown floats over the page on the shared `--elevation-floating` role, opens above
|
|
181
|
+
the control when the viewport below cannot hold it, is capped to the room it has and scrolls
|
|
182
|
+
its options. `disabled` keeps the selection visible, closes an open dropdown, disables every
|
|
183
|
+
control and emits nothing. With no options the dropdown opens empty.
|
|
184
|
+
|
|
185
|
+
**Keyboard and accessibility.** The visible label names the trigger and the option group; the
|
|
186
|
+
trigger exposes `aria-expanded` and, while open, `aria-controls`. Each option is a
|
|
187
|
+
`role="checkbox"` button with `aria-checked` and a tick that does not depend on colour. Enter,
|
|
188
|
+
Space or ArrowDown on the closed trigger opens the dropdown and focuses the first selected
|
|
189
|
+
option, or the first option; with no options focus stays on the trigger. Tab and Shift+Tab
|
|
190
|
+
traverse chips, clear-all and options normally with no focus trap; Space toggles a focused
|
|
191
|
+
option. Escape closes and returns focus to the trigger. Focus leaving the whole control, an
|
|
192
|
+
outside pointer interaction and the trigger itself close the dropdown without moving focus or
|
|
193
|
+
emitting. Chip removals and clear-all are named, non-submitting buttons outside the trigger:
|
|
194
|
+
a removal that takes its own focused control away moves focus to the next visible removal,
|
|
195
|
+
then the preceding one, then the trigger; clear-all returns focus to the trigger; a chip hidden
|
|
196
|
+
by overflow while focused hands focus to the trigger. This is a consumer-controlled selection
|
|
197
|
+
control, not a form field: it has no `name`, native submission, reset or validation.
|
|
198
|
+
|
|
199
|
+
## NumberInput
|
|
200
|
+
|
|
201
|
+
`NumberInput` is a labelled field for typed amounts and thresholds. It renders a text control
|
|
202
|
+
with a decimal keyboard hint (`inputmode="decimal"`) and keeps the entered text exactly as typed:
|
|
203
|
+
blank, `0`, `-`, `1.`, `1,5`, `1.5` and pasted content such as `12abc` stay distinct, untrimmed,
|
|
204
|
+
untruncated and unconverted. It is a separate roster entry with Input's field anatomy, not an
|
|
205
|
+
Input variant, and it does not extend Input's contract.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { NumberInput } from '@juwel-development/design-system';
|
|
209
|
+
import { Subject } from 'rxjs';
|
|
210
|
+
|
|
211
|
+
const maxPriceInput$ = new Subject<string>();
|
|
212
|
+
const maxPriceReset$ = new Subject<void>();
|
|
213
|
+
|
|
214
|
+
<NumberInput
|
|
215
|
+
label={'Maximum price'}
|
|
216
|
+
name={'maxPrice'}
|
|
217
|
+
placeholder={'No limit'}
|
|
218
|
+
hint={'Leave blank for no limit'}
|
|
219
|
+
defaultValue={savedMaxPrice}
|
|
220
|
+
invalid={maxPriceReading.kind === 'rejected'}
|
|
221
|
+
errorMessage={'Enter an amount such as 12.50'}
|
|
222
|
+
onInput$={maxPriceInput$}
|
|
223
|
+
reset$={maxPriceReset$}
|
|
224
|
+
/>;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`label` and `name` are required. Optional props are `required`, `optionalLabel`, `hint`,
|
|
228
|
+
`invalid`, `errorMessage`, `disabled`, `placeholder`, `defaultValue`, `onInput$`, `reset$` and
|
|
229
|
+
`testId`, with Input's meaning. Derive the props type with `ComponentProps<typeof NumberInput>`.
|
|
230
|
+
There are no `min`, `max`, `step`, `pattern` or length props and no steppers, formatting or key
|
|
231
|
+
filtering ([ADR 0009](docs/adr/0009-content-rules-stay-with-the-consumer.md)); the control has
|
|
232
|
+
textbox semantics, not spinbutton semantics.
|
|
233
|
+
|
|
234
|
+
**The consumer owns interpretation.** `onInput$` emits the current text as a string on every user
|
|
235
|
+
edit - typing, pasting and clearing by editing - and never a parsed number, `NaN` or `Infinity`.
|
|
236
|
+
The consumer decides whether the text is blank, unfinished, invalid or an accepted finite number,
|
|
237
|
+
which decimal and grouping conventions it accepts, and when to show an error; it drives `invalid`
|
|
238
|
+
and `errorMessage` in its own language. Validate the whole string before converting (a full-match
|
|
239
|
+
pattern, then `Number`), check the result with `Number.isFinite`, and never accept a numeric prefix
|
|
240
|
+
of invalid text. Blank is not zero: the field never converts one into the other. Integer-only and
|
|
241
|
+
whole-currency rules are consumer rules. The `EnglishParsing` and `GermanParsing` stories show one
|
|
242
|
+
such loop each; the library publishes no parser or locale service.
|
|
243
|
+
|
|
244
|
+
**Saved state and clearing.** `defaultValue` initialises the field on mount only; a later change
|
|
245
|
+
does not replace the current edit, so a mounted field keeps what the user typed across ordinary
|
|
246
|
+
rerenders. To restore saved text - returning to a tab, loading another record - remount the field
|
|
247
|
+
with the saved text as `defaultValue`. `reset$` empties the live node in place, so focus survives,
|
|
248
|
+
and emits nothing on `onInput$`; clear your own saved state alongside it if the field must stay
|
|
249
|
+
empty after a remount. Native form reset restores the form default (`defaultValue`), also silently.
|
|
250
|
+
Neither operation updates consumer-owned state. A disabled field accepts no edits and emits nothing,
|
|
251
|
+
but a `reset$` emission still clears it. Rendering, message changes and remount initialisation emit
|
|
252
|
+
no input events.
|
|
253
|
+
|
|
254
|
+
**Streams.** The component subscribes only to `reset$` and unsubscribes when the Subject is replaced
|
|
255
|
+
or the field unmounts. It only calls `.next()` on `onInput$` and never subscribes to, completes or
|
|
256
|
+
errors either stream; the consumer owns both lifetimes. There is no controlled `value` prop, no
|
|
257
|
+
live value stream and no React callback prop.
|
|
258
|
+
|
|
259
|
+
**Forms and accessibility.** The control works without JavaScript: the form submits the text by
|
|
260
|
+
`name`, and `required` checks presence only, so `1,5` and `twelve` both submit. Numeric validity is
|
|
261
|
+
not guaranteed by the form - a server must parse and validate on a no-JavaScript round-trip and
|
|
262
|
+
re-render the field `invalid` with its own error text, and a JavaScript consumer must itself prevent
|
|
263
|
+
an invalid submission. The label names the control, hint and error describe it, ids are unique per
|
|
264
|
+
instance, and the shared focus ring and field presentation apply in both themes. The device chooses
|
|
265
|
+
the actual keyboard: a decimal separator and digits are requested, but a particular layout, a minus
|
|
266
|
+
key or the exclusion of other characters is not promised.
|
|
267
|
+
|
|
97
268
|
## Collection
|
|
98
269
|
|
|
99
270
|
`Collection` is a vertical group of freely composed items with internal hairlines and
|