@torpor/ui 1.0.2 → 1.1.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/dist/ColorPicker/ColorPicker.torp +36 -75
- package/dist/ColorPicker/ColorPickerInput.torp +122 -0
- package/dist/ColorPicker/ColorPickerTypes.ts +12 -0
- package/dist/ColorPicker/index.d.ts +40 -1
- package/dist/ColorPicker/index.js +2 -1
- package/dist/CommandPalette/CommandPalette.torp +49 -47
- package/dist/CommandPalette/CommandPaletteInput.torp +51 -0
- package/dist/CommandPalette/CommandPaletteItem.torp +62 -0
- package/dist/CommandPalette/CommandPaletteList.torp +55 -0
- package/dist/CommandPalette/CommandPaletteTypes.ts +32 -0
- package/dist/CommandPalette/index.d.ts +88 -2
- package/dist/CommandPalette/index.js +4 -1
- package/dist/DataGrid/DataGrid.torp +22 -22
- package/dist/DataGrid/DataGridCell.torp +50 -0
- package/dist/DataGrid/DataGridColumnHeader.torp +54 -0
- package/dist/DataGrid/DataGridTypes.ts +17 -0
- package/dist/DataGrid/index.d.ts +57 -2
- package/dist/DataGrid/index.js +3 -2
- package/dist/DatePicker/DatePicker.torp +45 -65
- package/dist/DatePicker/DatePickerContent.torp +82 -0
- package/dist/DatePicker/DatePickerTrigger.torp +56 -0
- package/dist/DatePicker/DatePickerTypes.ts +36 -0
- package/dist/DatePicker/index.d.ts +61 -2
- package/dist/DatePicker/index.js +3 -1
- package/dist/DateRangePicker/DateRangePicker.torp +52 -76
- package/dist/DateRangePicker/DateRangePickerContent.torp +90 -0
- package/dist/DateRangePicker/DateRangePickerTrigger.torp +59 -0
- package/dist/DateRangePicker/DateRangePickerTypes.ts +41 -0
- package/dist/DateRangePicker/index.d.ts +61 -2
- package/dist/DateRangePicker/index.js +3 -1
- package/dist/Progress/Progress.torp +24 -14
- package/dist/Progress/ProgressIndicator.torp +52 -0
- package/dist/Progress/ProgressTypes.ts +8 -0
- package/dist/Progress/index.d.ts +34 -1
- package/dist/Progress/index.js +2 -1
- package/dist/Rating/Rating.torp +41 -37
- package/dist/Rating/RatingStar.torp +71 -0
- package/dist/Rating/RatingTypes.ts +22 -0
- package/dist/Rating/index.d.ts +39 -1
- package/dist/Rating/index.js +2 -1
- package/dist/Slider/Slider.torp +48 -63
- package/dist/Slider/SliderHandle.torp +107 -0
- package/dist/Slider/SliderRange.torp +36 -0
- package/dist/Slider/SliderTypes.ts +34 -0
- package/dist/Slider/index.d.ts +65 -1
- package/dist/Slider/index.js +3 -1
- package/dist/TagInput/TagInput.torp +63 -84
- package/dist/TagInput/TagInputField.torp +68 -0
- package/dist/TagInput/TagInputSuggestions.torp +79 -0
- package/dist/TagInput/TagInputTag.torp +55 -0
- package/dist/TagInput/TagInputTypes.ts +42 -0
- package/dist/TagInput/index.d.ts +93 -1
- package/dist/TagInput/index.js +4 -1
- package/dist/TimePicker/TimePicker.torp +88 -85
- package/dist/TimePicker/TimePickerPart.torp +61 -0
- package/dist/TimePicker/TimePickerPeriod.torp +48 -0
- package/dist/TimePicker/TimePickerTypes.ts +37 -0
- package/dist/TimePicker/index.d.ts +61 -1
- package/dist/TimePicker/index.js +3 -1
- package/package.json +7 -7
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const SliderContextName: unique symbol = Symbol.for("torp.Slider");
|
|
2
|
+
|
|
3
|
+
export interface SliderContext {
|
|
4
|
+
/** The minimum allowed value */
|
|
5
|
+
getMin: () => number;
|
|
6
|
+
/** The maximum allowed value */
|
|
7
|
+
getMax: () => number;
|
|
8
|
+
/** The amount by which the value is increased or decreased */
|
|
9
|
+
getStep: () => number;
|
|
10
|
+
/** The orientation of the slider */
|
|
11
|
+
getOrientation: () => "horizontal" | "vertical";
|
|
12
|
+
/** Whether the user cannot interact with the slider */
|
|
13
|
+
getDisabled: () => boolean;
|
|
14
|
+
/** The current value, clamped to the min and max */
|
|
15
|
+
getValue: () => number;
|
|
16
|
+
/** The percentage of the range that the value represents (0..100) */
|
|
17
|
+
getPct: () => number;
|
|
18
|
+
/** Sets the value, snapped to the step and clamped to the min and max */
|
|
19
|
+
setValue: (value: number) => void;
|
|
20
|
+
/** The accessible name for the slider, from the Slider's ariaLabel */
|
|
21
|
+
getAriaLabel: () => string | undefined;
|
|
22
|
+
/** Called from the handle when it is added */
|
|
23
|
+
registerHandle: (el: HTMLElement) => void;
|
|
24
|
+
/** Called from the handle when it is removed */
|
|
25
|
+
unregisterHandle: (el: HTMLElement) => void;
|
|
26
|
+
/** Whether the slider is contained within a Form Field */
|
|
27
|
+
inField: () => boolean;
|
|
28
|
+
/** Whether the slider's value is currently valid */
|
|
29
|
+
valid: () => boolean;
|
|
30
|
+
/** The ID of the Field's Message component, for aria-describedby */
|
|
31
|
+
messageId: () => string | undefined;
|
|
32
|
+
/** Called when the handle loses focus */
|
|
33
|
+
handleBlur: () => void;
|
|
34
|
+
}
|
package/dist/Slider/index.d.ts
CHANGED
|
@@ -33,6 +33,17 @@ interface SliderProps {
|
|
|
33
33
|
* dragging a thumb along a track or by using the keyboard.
|
|
34
34
|
*
|
|
35
35
|
* See [the WAI ARIA guidelines for Sliders](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
|
|
36
|
+
*
|
|
37
|
+
* The structure of a Slider is:
|
|
38
|
+
*
|
|
39
|
+
* ```
|
|
40
|
+
* <Slider>
|
|
41
|
+
* <SliderRange />
|
|
42
|
+
* <SliderHandle />
|
|
43
|
+
* </Slider>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* With no children, the range and handle are rendered automatically.
|
|
36
47
|
*/
|
|
37
48
|
declare function Slider(
|
|
38
49
|
$parent: ParentNode,
|
|
@@ -42,4 +53,57 @@ declare function Slider(
|
|
|
42
53
|
$slots?: Record<string, SlotRender>,
|
|
43
54
|
): void;
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
interface SliderHandleProps {
|
|
59
|
+
/** An ID for the handle element */
|
|
60
|
+
id?: string;
|
|
61
|
+
/** Classes for the handle element */
|
|
62
|
+
class?: ClassValue;
|
|
63
|
+
/** Styles for the handle element */
|
|
64
|
+
style?: StyleValue;
|
|
65
|
+
/** An accessible name for the slider; falls back to the Slider's ariaLabel */
|
|
66
|
+
ariaLabel?: string;
|
|
67
|
+
/** If set to true, the user cannot interact with this handle */
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The thumb of a Slider that the user drags along the track, or moves with
|
|
73
|
+
* the keyboard. It carries `role="slider"` and all of the slider's ARIA
|
|
74
|
+
* value attributes.
|
|
75
|
+
*
|
|
76
|
+
* See [the WAI ARIA guidelines for Sliders](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
|
|
77
|
+
*/
|
|
78
|
+
declare function SliderHandle(
|
|
79
|
+
$parent: ParentNode,
|
|
80
|
+
$anchor: Node | null,
|
|
81
|
+
$props: SliderHandleProps | undefined,
|
|
82
|
+
$context?: Record<PropertyKey, any>,
|
|
83
|
+
$slots?: Record<string, SlotRender>,
|
|
84
|
+
): void;
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
interface SliderRangeProps {
|
|
89
|
+
/** An ID for the range element */
|
|
90
|
+
id?: string;
|
|
91
|
+
/** Classes for the range element */
|
|
92
|
+
class?: ClassValue;
|
|
93
|
+
/** Styles for the range element */
|
|
94
|
+
style?: StyleValue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The filled portion of a Slider, from the start of the track to the
|
|
99
|
+
* current value.
|
|
100
|
+
*/
|
|
101
|
+
declare function SliderRange(
|
|
102
|
+
$parent: ParentNode,
|
|
103
|
+
$anchor: Node | null,
|
|
104
|
+
$props: SliderRangeProps | undefined,
|
|
105
|
+
$context?: Record<PropertyKey, any>,
|
|
106
|
+
$slots?: Record<string, SlotRender>,
|
|
107
|
+
): void;
|
|
108
|
+
|
|
109
|
+
export { Slider, SliderHandle, SliderRange };
|
package/dist/Slider/index.js
CHANGED
|
@@ -10,6 +10,10 @@ import FormValues from "../Form/FormValues.torp";
|
|
|
10
10
|
import { type Loader, createItemLoader } from "../utils/loader";
|
|
11
11
|
import getId from "../utils/getId";
|
|
12
12
|
import createFormField from "../utils/formField";
|
|
13
|
+
import TagInputField from "./TagInputField.torp";
|
|
14
|
+
import TagInputSuggestions from "./TagInputSuggestions.torp";
|
|
15
|
+
import TagInputTag from "./TagInputTag.torp";
|
|
16
|
+
import { type TagInputContext, TagInputContextName } from "./TagInputTypes";
|
|
13
17
|
|
|
14
18
|
interface TagInputProps {
|
|
15
19
|
/** An ID for the root element */
|
|
@@ -46,9 +50,21 @@ interface TagInputProps {
|
|
|
46
50
|
* An input where the user enters multiple free-text values, each shown as a
|
|
47
51
|
* tag that can be removed. Typing a value and pressing Enter or comma adds
|
|
48
52
|
* it as a tag, and pressing Backspace in an empty field removes the last one.
|
|
53
|
+
*
|
|
54
|
+
* The structure of a TagInput is:
|
|
55
|
+
*
|
|
56
|
+
* ```
|
|
57
|
+
* <TagInput>
|
|
58
|
+
* <TagInputTag />
|
|
59
|
+
* <TagInputField />
|
|
60
|
+
* <TagInputSuggestions />
|
|
61
|
+
* </TagInput>
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* With no children, the tags and suggestions are rendered automatically.
|
|
49
65
|
*/
|
|
50
66
|
export default function TagInput($props: TagInputProps | undefined) {
|
|
51
|
-
let input: HTMLInputElement;
|
|
67
|
+
let input: HTMLInputElement | undefined;
|
|
52
68
|
let root: HTMLElement;
|
|
53
69
|
|
|
54
70
|
// Wire up the enclosing Form and Field, if any
|
|
@@ -116,10 +132,47 @@ export default function TagInput($props: TagInputProps | undefined) {
|
|
|
116
132
|
return `${defaultId}-suggestion-${index}`;
|
|
117
133
|
}
|
|
118
134
|
|
|
135
|
+
function listId(): string {
|
|
136
|
+
return `${defaultId}-suggestions`;
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
function suggestions(): any[] {
|
|
120
140
|
return $load ? $load.items : [];
|
|
121
141
|
}
|
|
122
142
|
|
|
143
|
+
// Set the context to pass down to the tags and suggestions
|
|
144
|
+
let context: TagInputContext = {
|
|
145
|
+
getDisabled,
|
|
146
|
+
removeTag,
|
|
147
|
+
getPlaceholder: () => $props?.placeholder,
|
|
148
|
+
getText: () => $state.text,
|
|
149
|
+
hasLoader: () => hasLoader,
|
|
150
|
+
isSuggestionsOpen: () => $state.suggestionsOpen,
|
|
151
|
+
getActiveSuggestionId: () =>
|
|
152
|
+
$state.activeSuggestion >= 0 ? suggestionId($state.activeSuggestion) : undefined,
|
|
153
|
+
handleFieldInput: handleInput,
|
|
154
|
+
handleFieldKey: handleKey,
|
|
155
|
+
registerField: (el) => {
|
|
156
|
+
input = el;
|
|
157
|
+
},
|
|
158
|
+
unregisterField: (el) => {
|
|
159
|
+
if (input === el) {
|
|
160
|
+
input = undefined;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
getSuggestions: suggestions,
|
|
164
|
+
getSuggestionLabel: getLabel,
|
|
165
|
+
getSuggestionId: suggestionId,
|
|
166
|
+
isActiveSuggestion: (index) => $state.activeSuggestion === index,
|
|
167
|
+
setActiveSuggestion: (index) => {
|
|
168
|
+
$state.activeSuggestion = index;
|
|
169
|
+
},
|
|
170
|
+
pickSuggestion,
|
|
171
|
+
getListId: listId,
|
|
172
|
+
getAriaLabel: () => $props?.ariaLabel,
|
|
173
|
+
};
|
|
174
|
+
$context[TagInputContextName] = context;
|
|
175
|
+
|
|
123
176
|
// Dispatch the change event when the tags change
|
|
124
177
|
$handle((first) => {
|
|
125
178
|
const values = $state.value;
|
|
@@ -271,79 +324,16 @@ export default function TagInput($props: TagInputProps | undefined) {
|
|
|
271
324
|
}
|
|
272
325
|
}}
|
|
273
326
|
>
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
@
|
|
277
|
-
|
|
278
|
-
<
|
|
279
|
-
<button
|
|
280
|
-
type="button"
|
|
281
|
-
class="torp-tag-input-remove"
|
|
282
|
-
aria-label={`Remove ${tag}`}
|
|
283
|
-
disabled={getDisabled()}
|
|
284
|
-
onclick={() => removeTag(index)}
|
|
285
|
-
>
|
|
286
|
-
<slot>×</slot>
|
|
287
|
-
</button>
|
|
288
|
-
</li>
|
|
289
|
-
}
|
|
290
|
-
</ul>
|
|
291
|
-
<input
|
|
292
|
-
class="torp-tag-input-field"
|
|
293
|
-
type="text"
|
|
294
|
-
&ref={input}
|
|
295
|
-
value={$state.text}
|
|
296
|
-
placeholder={$props?.placeholder}
|
|
297
|
-
disabled={getDisabled()}
|
|
298
|
-
aria-label={$props?.ariaLabel}
|
|
299
|
-
role={hasLoader ? "combobox" : undefined}
|
|
300
|
-
aria-expanded={hasLoader ? ($state.suggestionsOpen ? "true" : "false") : undefined}
|
|
301
|
-
aria-controls={$state.suggestionsOpen ? `${defaultId}-suggestions` : undefined}
|
|
302
|
-
aria-autocomplete="list"
|
|
303
|
-
aria-activedescendant={$state.activeSuggestion >= 0 ? suggestionId($state.activeSuggestion) : undefined}
|
|
304
|
-
oninput={handleInput}
|
|
305
|
-
onkeydown={handleKey}
|
|
306
|
-
/>
|
|
307
|
-
@if ($state.suggestionsOpen) {
|
|
308
|
-
@try {
|
|
309
|
-
@await {
|
|
310
|
-
<ul
|
|
311
|
-
id={`${defaultId}-suggestions`}
|
|
312
|
-
class="torp-tag-input-suggestions"
|
|
313
|
-
role="listbox"
|
|
314
|
-
aria-label={$props?.ariaLabel}
|
|
315
|
-
>
|
|
316
|
-
@for (let [index, item] of suggestions().entries()) {
|
|
317
|
-
@key = getLabel(item)
|
|
318
|
-
<li
|
|
319
|
-
class="torp-tag-input-suggestion"
|
|
320
|
-
role="option"
|
|
321
|
-
id={suggestionId(index)}
|
|
322
|
-
aria-selected={$state.activeSuggestion === index ? "true" : "false"}
|
|
323
|
-
data-active={$state.activeSuggestion === index ? "active" : undefined}
|
|
324
|
-
onmouseenter={() => ($state.activeSuggestion = index)}
|
|
325
|
-
onmousedown={(e: MouseEvent) => {
|
|
326
|
-
// mousedown so the field doesn't blur first
|
|
327
|
-
e.preventDefault();
|
|
328
|
-
pickSuggestion(item);
|
|
329
|
-
}}
|
|
330
|
-
>
|
|
331
|
-
{getLabel(item)}
|
|
332
|
-
</li>
|
|
333
|
-
}
|
|
334
|
-
@if (suggestions().length === 0) {
|
|
335
|
-
<li class="torp-tag-input-empty" role="presentation">No matches.</li>
|
|
336
|
-
}
|
|
337
|
-
</ul>
|
|
338
|
-
} with {
|
|
339
|
-
<div class="torp-tag-input-loading" role="status">Loading…</div>
|
|
327
|
+
<slot>
|
|
328
|
+
<ul class="torp-tag-input-tags">
|
|
329
|
+
@for (let [index, tag] of $state.value.entries()) {
|
|
330
|
+
@key = index
|
|
331
|
+
<TagInputTag {index} {tag} />
|
|
340
332
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
}
|
|
333
|
+
</ul>
|
|
334
|
+
<TagInputField />
|
|
335
|
+
<TagInputSuggestions />
|
|
336
|
+
</slot>
|
|
347
337
|
</div>
|
|
348
338
|
|
|
349
339
|
<FormValues name={form.name} value={$state.value} />
|
|
@@ -363,16 +353,5 @@ export default function TagInput($props: TagInputProps | undefined) {
|
|
|
363
353
|
margin: 0;
|
|
364
354
|
padding: 0;
|
|
365
355
|
}
|
|
366
|
-
.torp-tag-input-field {
|
|
367
|
-
flex: 1 1 auto;
|
|
368
|
-
min-width: 4em;
|
|
369
|
-
}
|
|
370
|
-
.torp-tag-input-suggestions {
|
|
371
|
-
position: absolute;
|
|
372
|
-
list-style: none;
|
|
373
|
-
margin: 0;
|
|
374
|
-
padding: 0;
|
|
375
|
-
z-index: 10;
|
|
376
|
-
}
|
|
377
356
|
}
|
|
378
357
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type TagInputContext, TagInputContextName } from "./TagInputTypes";
|
|
3
|
+
|
|
4
|
+
interface TagInputFieldProps {
|
|
5
|
+
/** An ID for the field element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the field element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the field element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
/** Placeholder text for the empty field; falls back to the TagInput's placeholder */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** An accessible name for the field; falls back to the TagInput's ariaLabel */
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
/** If set to true, the user cannot interact with this field */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The text field of a TagInput, where the user types values (and searches
|
|
21
|
+
* suggestions when a `load` function is set).
|
|
22
|
+
*/
|
|
23
|
+
export default function TagInputField($props: TagInputFieldProps | undefined) {
|
|
24
|
+
// Get stuff out of the TagInput context
|
|
25
|
+
const context = $context[TagInputContextName] as TagInputContext;
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error("TagInputField must be contained within a TagInput");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDisabled(): boolean {
|
|
31
|
+
return $props?.disabled === true || context.getDisabled();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@render {
|
|
35
|
+
<input
|
|
36
|
+
{$props?.id}
|
|
37
|
+
class={["torp-tag-input-field", $props?.class]}
|
|
38
|
+
{$props?.style}
|
|
39
|
+
type="text"
|
|
40
|
+
value={context.getText()}
|
|
41
|
+
placeholder={$props?.placeholder ?? context.getPlaceholder()}
|
|
42
|
+
disabled={getDisabled()}
|
|
43
|
+
aria-label={$props?.ariaLabel ?? context.getAriaLabel()}
|
|
44
|
+
role={context.hasLoader() ? "combobox" : undefined}
|
|
45
|
+
aria-expanded={context.hasLoader() ? (context.isSuggestionsOpen() ? "true" : "false") : undefined}
|
|
46
|
+
aria-controls={context.isSuggestionsOpen() ? context.getListId() : undefined}
|
|
47
|
+
aria-autocomplete="list"
|
|
48
|
+
aria-activedescendant={context.getActiveSuggestionId()}
|
|
49
|
+
oninput={context.handleFieldInput}
|
|
50
|
+
onkeydown={context.handleFieldKey}
|
|
51
|
+
onmount={(el) => {
|
|
52
|
+
context.registerField(el);
|
|
53
|
+
return () => {
|
|
54
|
+
context.unregisterField(el);
|
|
55
|
+
};
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@style {
|
|
61
|
+
/* Headless: layout only, so the field shares the row with the tags.
|
|
62
|
+
All appearance is up to the consuming theme */
|
|
63
|
+
.torp-tag-input-field {
|
|
64
|
+
flex: 1 1 auto;
|
|
65
|
+
min-width: 4em;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type TagInputContext, TagInputContextName } from "./TagInputTypes";
|
|
3
|
+
|
|
4
|
+
interface TagInputSuggestionsProps {
|
|
5
|
+
/** An ID for the suggestion list element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the suggestion list element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the suggestion list element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The suggestion list of a TagInput, shown while the user types when a
|
|
15
|
+
* `load` function is set. Selecting a suggestion adds it as a tag.
|
|
16
|
+
*/
|
|
17
|
+
export default function TagInputSuggestions($props: TagInputSuggestionsProps | undefined) {
|
|
18
|
+
// Get stuff out of the TagInput context
|
|
19
|
+
const context = $context[TagInputContextName] as TagInputContext;
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error("TagInputSuggestions must be contained within a TagInput");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@render {
|
|
25
|
+
@if (context.isSuggestionsOpen()) {
|
|
26
|
+
@try {
|
|
27
|
+
@await {
|
|
28
|
+
<ul
|
|
29
|
+
id={$props?.id ?? context.getListId()}
|
|
30
|
+
class={["torp-tag-input-suggestions", $props?.class]}
|
|
31
|
+
{$props?.style}
|
|
32
|
+
role="listbox"
|
|
33
|
+
aria-label={context.getAriaLabel()}
|
|
34
|
+
>
|
|
35
|
+
@for (let [index, item] of context.getSuggestions().entries()) {
|
|
36
|
+
@key = context.getSuggestionLabel(item)
|
|
37
|
+
<li
|
|
38
|
+
class="torp-tag-input-suggestion"
|
|
39
|
+
role="option"
|
|
40
|
+
id={context.getSuggestionId(index)}
|
|
41
|
+
aria-selected={context.isActiveSuggestion(index) ? "true" : "false"}
|
|
42
|
+
data-active={context.isActiveSuggestion(index) ? "active" : undefined}
|
|
43
|
+
onmouseenter={() => context.setActiveSuggestion(index)}
|
|
44
|
+
onmousedown={(e: MouseEvent) => {
|
|
45
|
+
// mousedown so the field doesn't blur first
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
context.pickSuggestion(item);
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{context.getSuggestionLabel(item)}
|
|
51
|
+
</li>
|
|
52
|
+
}
|
|
53
|
+
@if (context.getSuggestions().length === 0) {
|
|
54
|
+
<li class="torp-tag-input-empty" role="presentation">No matches.</li>
|
|
55
|
+
}
|
|
56
|
+
</ul>
|
|
57
|
+
} with {
|
|
58
|
+
<div class="torp-tag-input-loading" role="status">Loading…</div>
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
<div class="torp-tag-input-error" role="alert">
|
|
62
|
+
{err instanceof Error ? err.message : String(err)}
|
|
63
|
+
</div>
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@style {
|
|
69
|
+
/* Headless: layout only, so the list overlays the page. All appearance
|
|
70
|
+
is up to the consuming theme */
|
|
71
|
+
.torp-tag-input-suggestions {
|
|
72
|
+
position: absolute;
|
|
73
|
+
list-style: none;
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 0;
|
|
76
|
+
z-index: 10;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type TagInputContext, TagInputContextName } from "./TagInputTypes";
|
|
3
|
+
|
|
4
|
+
interface TagInputTagProps {
|
|
5
|
+
/** An ID for the tag element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the tag element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the tag element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
/** The tag's position in the list */
|
|
12
|
+
index: number;
|
|
13
|
+
/** The tag's text */
|
|
14
|
+
tag: string;
|
|
15
|
+
/** If set to true, the tag cannot be removed */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One tag of a TagInput: its text with a button to remove it.
|
|
21
|
+
*/
|
|
22
|
+
export default function TagInputTag($props: TagInputTagProps | undefined) {
|
|
23
|
+
// Get stuff out of the TagInput context
|
|
24
|
+
const context = $context[TagInputContextName] as TagInputContext;
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error("TagInputTag must be contained within a TagInput");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getIndex(): number {
|
|
30
|
+
return $props?.index ?? 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getTag(): string {
|
|
34
|
+
return $props?.tag ?? "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDisabled(): boolean {
|
|
38
|
+
return $props?.disabled === true || context.getDisabled();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@render {
|
|
42
|
+
<li {$props?.id} class={["torp-tag-input-tag", $props?.class]} {$props?.style}>
|
|
43
|
+
<span class="torp-tag-input-tag-text">{getTag()}</span>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="torp-tag-input-remove"
|
|
47
|
+
aria-label={`Remove ${getTag()}`}
|
|
48
|
+
disabled={getDisabled()}
|
|
49
|
+
onclick={() => context.removeTag(getIndex())}
|
|
50
|
+
>
|
|
51
|
+
<slot>×</slot>
|
|
52
|
+
</button>
|
|
53
|
+
</li>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const TagInputContextName: unique symbol = Symbol.for("torp.TagInput");
|
|
2
|
+
|
|
3
|
+
export interface TagInputContext {
|
|
4
|
+
/** Whether the user cannot interact with the input */
|
|
5
|
+
getDisabled: () => boolean;
|
|
6
|
+
/** Removes the tag at an index */
|
|
7
|
+
removeTag: (index: number) => void;
|
|
8
|
+
/** Placeholder text for the empty field */
|
|
9
|
+
getPlaceholder: () => string | undefined;
|
|
10
|
+
/** The text currently in the field */
|
|
11
|
+
getText: () => string;
|
|
12
|
+
/** Whether suggestions are loaded from a `load` function */
|
|
13
|
+
hasLoader: () => boolean;
|
|
14
|
+
/** Whether the suggestion list is open */
|
|
15
|
+
isSuggestionsOpen: () => boolean;
|
|
16
|
+
/** The ID of the active suggestion, for aria-activedescendant */
|
|
17
|
+
getActiveSuggestionId: () => string | undefined;
|
|
18
|
+
/** Handles typing in the field */
|
|
19
|
+
handleFieldInput: (e: Event) => void;
|
|
20
|
+
/** Handles keys in the field (arrows, Enter, comma, Backspace, Escape) */
|
|
21
|
+
handleFieldKey: (e: KeyboardEvent) => void;
|
|
22
|
+
/** Called from the field when it is added */
|
|
23
|
+
registerField: (el: HTMLInputElement) => void;
|
|
24
|
+
/** Called from the field when it is removed */
|
|
25
|
+
unregisterField: (el: HTMLInputElement) => void;
|
|
26
|
+
/** The current suggestions; suspends while loading */
|
|
27
|
+
getSuggestions: () => any[];
|
|
28
|
+
/** Extracts the display text for a suggestion */
|
|
29
|
+
getSuggestionLabel: (item: any) => string;
|
|
30
|
+
/** The ID of a suggestion option, for aria-activedescendant */
|
|
31
|
+
getSuggestionId: (index: number) => string;
|
|
32
|
+
/** Whether a suggestion is the active (highlighted) one */
|
|
33
|
+
isActiveSuggestion: (index: number) => boolean;
|
|
34
|
+
/** Highlights a suggestion */
|
|
35
|
+
setActiveSuggestion: (index: number) => void;
|
|
36
|
+
/** Adds a suggestion as a tag */
|
|
37
|
+
pickSuggestion: (item: any) => void;
|
|
38
|
+
/** The ID of the suggestion list, for aria-controls */
|
|
39
|
+
getListId: () => string;
|
|
40
|
+
/** An accessible name for the suggestion list */
|
|
41
|
+
getAriaLabel: () => string | undefined;
|
|
42
|
+
}
|
package/dist/TagInput/index.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ interface TagInputProps {
|
|
|
36
36
|
* An input where the user enters multiple free-text values, each shown as a
|
|
37
37
|
* tag that can be removed. Typing a value and pressing Enter or comma adds
|
|
38
38
|
* it as a tag, and pressing Backspace in an empty field removes the last one.
|
|
39
|
+
*
|
|
40
|
+
* The structure of a TagInput is:
|
|
41
|
+
*
|
|
42
|
+
* ```
|
|
43
|
+
* <TagInput>
|
|
44
|
+
* <TagInputTag />
|
|
45
|
+
* <TagInputField />
|
|
46
|
+
* <TagInputSuggestions />
|
|
47
|
+
* </TagInput>
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* With no children, the tags and suggestions are rendered automatically.
|
|
39
51
|
*/
|
|
40
52
|
declare function TagInput(
|
|
41
53
|
$parent: ParentNode,
|
|
@@ -45,4 +57,84 @@ declare function TagInput(
|
|
|
45
57
|
$slots?: Record<string, SlotRender>,
|
|
46
58
|
): void;
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
interface TagInputFieldProps {
|
|
63
|
+
/** An ID for the field element */
|
|
64
|
+
id?: string;
|
|
65
|
+
/** Classes for the field element */
|
|
66
|
+
class?: ClassValue;
|
|
67
|
+
/** Styles for the field element */
|
|
68
|
+
style?: StyleValue;
|
|
69
|
+
/** Placeholder text for the empty field; falls back to the TagInput's placeholder */
|
|
70
|
+
placeholder?: string;
|
|
71
|
+
/** An accessible name for the field; falls back to the TagInput's ariaLabel */
|
|
72
|
+
ariaLabel?: string;
|
|
73
|
+
/** If set to true, the user cannot interact with this field */
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The text field of a TagInput, where the user types values (and searches
|
|
79
|
+
* suggestions when a `load` function is set).
|
|
80
|
+
*/
|
|
81
|
+
declare function TagInputField(
|
|
82
|
+
$parent: ParentNode,
|
|
83
|
+
$anchor: Node | null,
|
|
84
|
+
$props: TagInputFieldProps | undefined,
|
|
85
|
+
$context?: Record<PropertyKey, any>,
|
|
86
|
+
$slots?: Record<string, SlotRender>,
|
|
87
|
+
): void;
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
interface TagInputSuggestionsProps {
|
|
92
|
+
/** An ID for the suggestion list element */
|
|
93
|
+
id?: string;
|
|
94
|
+
/** Classes for the suggestion list element */
|
|
95
|
+
class?: ClassValue;
|
|
96
|
+
/** Styles for the suggestion list element */
|
|
97
|
+
style?: StyleValue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The suggestion list of a TagInput, shown while the user types when a
|
|
102
|
+
* `load` function is set. Selecting a suggestion adds it as a tag.
|
|
103
|
+
*/
|
|
104
|
+
declare function TagInputSuggestions(
|
|
105
|
+
$parent: ParentNode,
|
|
106
|
+
$anchor: Node | null,
|
|
107
|
+
$props: TagInputSuggestionsProps | undefined,
|
|
108
|
+
$context?: Record<PropertyKey, any>,
|
|
109
|
+
$slots?: Record<string, SlotRender>,
|
|
110
|
+
): void;
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
interface TagInputTagProps {
|
|
115
|
+
/** An ID for the tag element */
|
|
116
|
+
id?: string;
|
|
117
|
+
/** Classes for the tag element */
|
|
118
|
+
class?: ClassValue;
|
|
119
|
+
/** Styles for the tag element */
|
|
120
|
+
style?: StyleValue;
|
|
121
|
+
/** The tag's position in the list */
|
|
122
|
+
index: number;
|
|
123
|
+
/** The tag's text */
|
|
124
|
+
tag: string;
|
|
125
|
+
/** If set to true, the tag cannot be removed */
|
|
126
|
+
disabled?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* One tag of a TagInput: its text with a button to remove it.
|
|
131
|
+
*/
|
|
132
|
+
declare function TagInputTag(
|
|
133
|
+
$parent: ParentNode,
|
|
134
|
+
$anchor: Node | null,
|
|
135
|
+
$props: TagInputTagProps | undefined,
|
|
136
|
+
$context?: Record<PropertyKey, any>,
|
|
137
|
+
$slots?: Record<string, SlotRender>,
|
|
138
|
+
): void;
|
|
139
|
+
|
|
140
|
+
export { TagInput, TagInputField, TagInputSuggestions, TagInputTag };
|
package/dist/TagInput/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import TagInput from "./TagInput.torp";
|
|
2
|
-
|
|
2
|
+
import TagInputField from "./TagInputField.torp";
|
|
3
|
+
import TagInputSuggestions from "./TagInputSuggestions.torp";
|
|
4
|
+
import TagInputTag from "./TagInputTag.torp";
|
|
5
|
+
export { TagInput, TagInputField, TagInputSuggestions, TagInputTag };
|