@torpor/ui 1.0.1 → 1.1.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/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,52 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type ProgressContext, ProgressContextName } from "./ProgressTypes";
|
|
3
|
+
|
|
4
|
+
interface ProgressIndicatorProps {
|
|
5
|
+
/** An ID for the indicator element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the indicator element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the indicator element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bar of a Progress that fills the track in proportion to the current
|
|
15
|
+
* value, or animates while the progress is indeterminate.
|
|
16
|
+
*/
|
|
17
|
+
export default function ProgressIndicator($props: ProgressIndicatorProps | undefined) {
|
|
18
|
+
// Get stuff out of the Progress context
|
|
19
|
+
const context = $context[ProgressContextName] as ProgressContext;
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error("ProgressIndicator must be contained within a Progress");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function state(): "indeterminate" | "determinate" {
|
|
25
|
+
return context.isIndeterminate() ? "indeterminate" : "determinate";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function widthStyle(): string | undefined {
|
|
29
|
+
return context.isIndeterminate() ? undefined : `width: ${context.getPct()}%;`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@render {
|
|
33
|
+
<div
|
|
34
|
+
{$props?.id}
|
|
35
|
+
class={["torp-progress-bar", $props?.class]}
|
|
36
|
+
style={[widthStyle(), $props?.style]}
|
|
37
|
+
data-state={state()}
|
|
38
|
+
/>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@style {
|
|
42
|
+
/* Headless: layout only, so the bar fills the track proportionally.
|
|
43
|
+
All appearance is up to the consuming theme */
|
|
44
|
+
.torp-progress-bar {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0 auto 0 0;
|
|
47
|
+
}
|
|
48
|
+
.torp-progress-bar[data-state="indeterminate"] {
|
|
49
|
+
width: 100%;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const ProgressContextName: unique symbol = Symbol.for("torp.Progress");
|
|
2
|
+
|
|
3
|
+
export interface ProgressContext {
|
|
4
|
+
/** Whether the extent is unknown (no value was passed) */
|
|
5
|
+
isIndeterminate: () => boolean;
|
|
6
|
+
/** The percentage of the range that the value represents (0..100) */
|
|
7
|
+
getPct: () => number;
|
|
8
|
+
}
|
package/dist/Progress/index.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ interface ProgressProps {
|
|
|
25
25
|
* specific value or as indeterminate when the extent is unknown.
|
|
26
26
|
*
|
|
27
27
|
* See [the WAI ARIA guidelines for Progress Bars](https://www.w3.org/WAI/ARIA/apg/patterns/progressbar/).
|
|
28
|
+
*
|
|
29
|
+
* The structure of a Progress is:
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* <Progress>
|
|
33
|
+
* <ProgressIndicator />
|
|
34
|
+
* </Progress>
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* With no children, the indicator is rendered automatically.
|
|
28
38
|
*/
|
|
29
39
|
declare function Progress(
|
|
30
40
|
$parent: ParentNode,
|
|
@@ -34,4 +44,27 @@ declare function Progress(
|
|
|
34
44
|
$slots?: Record<string, SlotRender>,
|
|
35
45
|
): void;
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
interface ProgressIndicatorProps {
|
|
50
|
+
/** An ID for the indicator element */
|
|
51
|
+
id?: string;
|
|
52
|
+
/** Classes for the indicator element */
|
|
53
|
+
class?: ClassValue;
|
|
54
|
+
/** Styles for the indicator element */
|
|
55
|
+
style?: StyleValue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bar of a Progress that fills the track in proportion to the current
|
|
60
|
+
* value, or animates while the progress is indeterminate.
|
|
61
|
+
*/
|
|
62
|
+
declare function ProgressIndicator(
|
|
63
|
+
$parent: ParentNode,
|
|
64
|
+
$anchor: Node | null,
|
|
65
|
+
$props: ProgressIndicatorProps | undefined,
|
|
66
|
+
$context?: Record<PropertyKey, any>,
|
|
67
|
+
$slots?: Record<string, SlotRender>,
|
|
68
|
+
): void;
|
|
69
|
+
|
|
70
|
+
export { Progress, ProgressIndicator };
|
package/dist/Progress/index.js
CHANGED
package/dist/Rating/Rating.torp
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "../Form/FormTypes";
|
|
9
9
|
import FormValues from "../Form/FormValues.torp";
|
|
10
10
|
import createFormField from "../utils/formField";
|
|
11
|
+
import RatingStar from "./RatingStar.torp";
|
|
12
|
+
import { type RatingContext, RatingContextName } from "./RatingTypes";
|
|
11
13
|
|
|
12
14
|
interface RatingProps {
|
|
13
15
|
/** An ID for the root element */
|
|
@@ -40,6 +42,16 @@ interface RatingProps {
|
|
|
40
42
|
* matching the current rating is checked, and arrow keys move the rating.
|
|
41
43
|
*
|
|
42
44
|
* See [the WAI ARIA guidelines for Rating](https://www.w3.org/WAI/ARIA/apg/patterns/rating/).
|
|
45
|
+
*
|
|
46
|
+
* The structure of a Rating is:
|
|
47
|
+
*
|
|
48
|
+
* ```
|
|
49
|
+
* <Rating>
|
|
50
|
+
* <RatingStar />
|
|
51
|
+
* </Rating>
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* With no children, the stars are rendered automatically.
|
|
43
55
|
*/
|
|
44
56
|
export default function Rating($props: RatingProps | undefined) {
|
|
45
57
|
let stars: (HTMLButtonElement | undefined)[] = [];
|
|
@@ -109,7 +121,29 @@ export default function Rating($props: RatingProps | undefined) {
|
|
|
109
121
|
return getDisabled() ? getValue() : Math.max(getValue(), $state.hover);
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
// Set the context to pass down to the stars
|
|
125
|
+
let context: RatingContext = {
|
|
126
|
+
getDisplayValue,
|
|
127
|
+
getDisabled,
|
|
128
|
+
isTabStop: (value) => value === ($state.value > 0 ? $state.value : 1),
|
|
129
|
+
isChecked: (value) => value === getValue(),
|
|
130
|
+
selectValue,
|
|
131
|
+
handleKey,
|
|
132
|
+
setHover: (value) => {
|
|
133
|
+
$state.hover = value;
|
|
134
|
+
},
|
|
135
|
+
registerStar: (value, el) => {
|
|
136
|
+
stars[value - 1] = el;
|
|
137
|
+
},
|
|
138
|
+
unregisterStar: (value, el) => {
|
|
139
|
+
if (stars[value - 1] === el) {
|
|
140
|
+
stars[value - 1] = undefined;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
$context[RatingContextName] = context;
|
|
145
|
+
|
|
146
|
+
function selectValue(value: number) {
|
|
113
147
|
if (getDisabled()) return;
|
|
114
148
|
if (value === $state.value && ($props?.allowClear ?? true)) {
|
|
115
149
|
setValue(0);
|
|
@@ -159,12 +193,6 @@ export default function Rating($props: RatingProps | undefined) {
|
|
|
159
193
|
}
|
|
160
194
|
}
|
|
161
195
|
|
|
162
|
-
// Roving tabindex: the star matching the current rating (or the first
|
|
163
|
-
// star when there is no rating) is the single tab stop
|
|
164
|
-
function tabStop(value: number): boolean {
|
|
165
|
-
return value === ($state.value > 0 ? $state.value : 1);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
196
|
@render {
|
|
169
197
|
<div
|
|
170
198
|
{$props?.id}
|
|
@@ -180,36 +208,12 @@ export default function Rating($props: RatingProps | undefined) {
|
|
|
180
208
|
onfocusout={handleFocusOut}
|
|
181
209
|
&ref={root}
|
|
182
210
|
>
|
|
183
|
-
|
|
184
|
-
@
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tabindex={tabStop(value) ? 0 : -1}
|
|
190
|
-
aria-checked={value === getValue() ? "true" : "false"}
|
|
191
|
-
aria-label={`${value} ${value === 1 ? "star" : "stars"}`}
|
|
192
|
-
aria-disabled={getDisabled() ? "true" : undefined}
|
|
193
|
-
data-state={value <= getDisplayValue() ? "filled" : "empty"}
|
|
194
|
-
onmount={(el) => {
|
|
195
|
-
stars[value - 1] = el;
|
|
196
|
-
return () => {
|
|
197
|
-
if (stars[value - 1] === el) {
|
|
198
|
-
stars[value - 1] = undefined;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}}
|
|
202
|
-
onclick={() => handleClick(value)}
|
|
203
|
-
onkeydown={handleKey}
|
|
204
|
-
onmouseenter={() => {
|
|
205
|
-
if (!getDisabled()) $state.hover = value;
|
|
206
|
-
}}
|
|
207
|
-
>
|
|
208
|
-
<slot item={{ value: value, filled: value <= getDisplayValue() }}>
|
|
209
|
-
<span aria-hidden="true">★</span>
|
|
210
|
-
</slot>
|
|
211
|
-
</button>
|
|
212
|
-
}
|
|
211
|
+
<slot>
|
|
212
|
+
@for (let value of $state.stars) {
|
|
213
|
+
@key = value
|
|
214
|
+
<RatingStar {value} />
|
|
215
|
+
}
|
|
216
|
+
</slot>
|
|
213
217
|
</div>
|
|
214
218
|
|
|
215
219
|
<FormValues name={form.name} value={getValue() > 0 ? getValue() : undefined} />
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type RatingContext, RatingContextName } from "./RatingTypes";
|
|
3
|
+
|
|
4
|
+
interface RatingStarProps {
|
|
5
|
+
/** An ID for the star element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the star element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the star element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
/** The star's value (1-based position in the rating) */
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One star of a Rating, acting as a radio button. The star matching the
|
|
17
|
+
* current rating is checked, and the stars up to the rating (or the hovered
|
|
18
|
+
* star) are marked as filled.
|
|
19
|
+
*
|
|
20
|
+
* See [the WAI ARIA guidelines for Rating](https://www.w3.org/WAI/ARIA/apg/patterns/rating/).
|
|
21
|
+
*/
|
|
22
|
+
export default function RatingStar($props: RatingStarProps | undefined) {
|
|
23
|
+
// Get stuff out of the Rating context
|
|
24
|
+
const context = $context[RatingContextName] as RatingContext;
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error("RatingStar must be contained within a Rating");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getValue(): number {
|
|
30
|
+
return $props?.value ?? 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDisabled(): boolean {
|
|
34
|
+
return context.getDisabled();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function label(): string {
|
|
38
|
+
const value = getValue();
|
|
39
|
+
return `${value} ${value === 1 ? "star" : "stars"}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@render {
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
{$props?.id}
|
|
46
|
+
class={["torp-rating-star", $props?.class]}
|
|
47
|
+
{$props?.style}
|
|
48
|
+
role="radio"
|
|
49
|
+
tabindex={context.isTabStop(getValue()) ? 0 : -1}
|
|
50
|
+
aria-checked={context.isChecked(getValue()) ? "true" : "false"}
|
|
51
|
+
aria-label={label()}
|
|
52
|
+
aria-disabled={getDisabled() ? "true" : undefined}
|
|
53
|
+
data-state={getValue() <= context.getDisplayValue() ? "filled" : "empty"}
|
|
54
|
+
onmount={(el) => {
|
|
55
|
+
context.registerStar(getValue(), el);
|
|
56
|
+
return () => {
|
|
57
|
+
context.unregisterStar(getValue(), el);
|
|
58
|
+
};
|
|
59
|
+
}}
|
|
60
|
+
onclick={() => context.selectValue(getValue())}
|
|
61
|
+
onkeydown={context.handleKey}
|
|
62
|
+
onmouseenter={() => {
|
|
63
|
+
if (!getDisabled()) context.setHover(getValue());
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<slot item={{ value: getValue(), filled: getValue() <= context.getDisplayValue() }}>
|
|
67
|
+
<span aria-hidden="true">★</span>
|
|
68
|
+
</slot>
|
|
69
|
+
</button>
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const RatingContextName: unique symbol = Symbol.for("torp.Rating");
|
|
2
|
+
|
|
3
|
+
export interface RatingContext {
|
|
4
|
+
/** The number of filled stars to display, taking the hovered star into account */
|
|
5
|
+
getDisplayValue: () => number;
|
|
6
|
+
/** Whether the user cannot interact with the rating */
|
|
7
|
+
getDisabled: () => boolean;
|
|
8
|
+
/** Whether the star is the single tab stop (roving tabindex) */
|
|
9
|
+
isTabStop: (value: number) => boolean;
|
|
10
|
+
/** Whether the star matches the current rating */
|
|
11
|
+
isChecked: (value: number) => boolean;
|
|
12
|
+
/** Selects a star, clearing the rating if it is already selected */
|
|
13
|
+
selectValue: (value: number) => void;
|
|
14
|
+
/** Handles arrow/Home/End keys on a star */
|
|
15
|
+
handleKey: (e: KeyboardEvent) => void;
|
|
16
|
+
/** Previews a rating while the pointer hovers a star */
|
|
17
|
+
setHover: (value: number) => void;
|
|
18
|
+
/** Called from a star when it is added */
|
|
19
|
+
registerStar: (value: number, el: HTMLButtonElement | undefined) => void;
|
|
20
|
+
/** Called from a star when it is removed */
|
|
21
|
+
unregisterStar: (value: number, el: HTMLButtonElement | undefined) => void;
|
|
22
|
+
}
|
package/dist/Rating/index.d.ts
CHANGED
|
@@ -32,6 +32,16 @@ interface RatingProps {
|
|
|
32
32
|
* matching the current rating is checked, and arrow keys move the rating.
|
|
33
33
|
*
|
|
34
34
|
* See [the WAI ARIA guidelines for Rating](https://www.w3.org/WAI/ARIA/apg/patterns/rating/).
|
|
35
|
+
*
|
|
36
|
+
* The structure of a Rating is:
|
|
37
|
+
*
|
|
38
|
+
* ```
|
|
39
|
+
* <Rating>
|
|
40
|
+
* <RatingStar />
|
|
41
|
+
* </Rating>
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* With no children, the stars are rendered automatically.
|
|
35
45
|
*/
|
|
36
46
|
declare function Rating(
|
|
37
47
|
$parent: ParentNode,
|
|
@@ -41,4 +51,32 @@ declare function Rating(
|
|
|
41
51
|
$slots?: Record<string, SlotRender>,
|
|
42
52
|
): void;
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
interface RatingStarProps {
|
|
57
|
+
/** An ID for the star element */
|
|
58
|
+
id?: string;
|
|
59
|
+
/** Classes for the star element */
|
|
60
|
+
class?: ClassValue;
|
|
61
|
+
/** Styles for the star element */
|
|
62
|
+
style?: StyleValue;
|
|
63
|
+
/** The star's value (1-based position in the rating) */
|
|
64
|
+
value: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One star of a Rating, acting as a radio button. The star matching the
|
|
69
|
+
* current rating is checked, and the stars up to the rating (or the hovered
|
|
70
|
+
* star) are marked as filled.
|
|
71
|
+
*
|
|
72
|
+
* See [the WAI ARIA guidelines for Rating](https://www.w3.org/WAI/ARIA/apg/patterns/rating/).
|
|
73
|
+
*/
|
|
74
|
+
declare function RatingStar(
|
|
75
|
+
$parent: ParentNode,
|
|
76
|
+
$anchor: Node | null,
|
|
77
|
+
$props: RatingStarProps | undefined,
|
|
78
|
+
$context?: Record<PropertyKey, any>,
|
|
79
|
+
$slots?: Record<string, SlotRender>,
|
|
80
|
+
): void;
|
|
81
|
+
|
|
82
|
+
export { Rating, RatingStar };
|
package/dist/Rating/index.js
CHANGED
package/dist/Slider/Slider.torp
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
} from "../Form/FormTypes";
|
|
9
9
|
import FormValues from "../Form/FormValues.torp";
|
|
10
10
|
import createFormField from "../utils/formField";
|
|
11
|
+
import SliderHandle from "./SliderHandle.torp";
|
|
12
|
+
import SliderRange from "./SliderRange.torp";
|
|
13
|
+
import { type SliderContext, SliderContextName } from "./SliderTypes";
|
|
11
14
|
|
|
12
15
|
interface SliderProps {
|
|
13
16
|
/** An ID for the root element */
|
|
@@ -41,10 +44,21 @@ interface SliderProps {
|
|
|
41
44
|
* dragging a thumb along a track or by using the keyboard.
|
|
42
45
|
*
|
|
43
46
|
* See [the WAI ARIA guidelines for Sliders](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
|
|
47
|
+
*
|
|
48
|
+
* The structure of a Slider is:
|
|
49
|
+
*
|
|
50
|
+
* ```
|
|
51
|
+
* <Slider>
|
|
52
|
+
* <SliderRange />
|
|
53
|
+
* <SliderHandle />
|
|
54
|
+
* </Slider>
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* With no children, the range and handle are rendered automatically.
|
|
44
58
|
*/
|
|
45
59
|
export default function Slider($props: SliderProps | undefined) {
|
|
46
60
|
let track: HTMLElement;
|
|
47
|
-
let
|
|
61
|
+
let handle: HTMLElement | undefined;
|
|
48
62
|
|
|
49
63
|
// Wire up the enclosing Form and Field, if any
|
|
50
64
|
const form = createFormField(
|
|
@@ -116,6 +130,32 @@ export default function Slider($props: SliderProps | undefined) {
|
|
|
116
130
|
return false;
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
// Set the context to pass down to the range and handle
|
|
134
|
+
let context: SliderContext = {
|
|
135
|
+
getMin,
|
|
136
|
+
getMax,
|
|
137
|
+
getStep,
|
|
138
|
+
getOrientation,
|
|
139
|
+
getDisabled,
|
|
140
|
+
getValue,
|
|
141
|
+
getPct,
|
|
142
|
+
setValue,
|
|
143
|
+
getAriaLabel: () => $props?.ariaLabel,
|
|
144
|
+
registerHandle: (el) => {
|
|
145
|
+
handle = el;
|
|
146
|
+
},
|
|
147
|
+
unregisterHandle: (el) => {
|
|
148
|
+
if (handle === el) {
|
|
149
|
+
handle = undefined;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
inField: () => form.inField,
|
|
153
|
+
valid: () => form.valid,
|
|
154
|
+
messageId: () => form.messageId,
|
|
155
|
+
handleBlur: () => form.handleBlur(),
|
|
156
|
+
};
|
|
157
|
+
$context[SliderContextName] = context;
|
|
158
|
+
|
|
119
159
|
// Convert a pointer position into a value along the track
|
|
120
160
|
function valueFromPointer(e: { clientX: number; clientY: number }): number {
|
|
121
161
|
const rect = track.getBoundingClientRect();
|
|
@@ -141,7 +181,7 @@ export default function Slider($props: SliderProps | undefined) {
|
|
|
141
181
|
e.preventDefault();
|
|
142
182
|
dragging = true;
|
|
143
183
|
setValue(valueFromPointer(e));
|
|
144
|
-
|
|
184
|
+
handle?.focus();
|
|
145
185
|
addEventListener("mousemove", handleMouseMove);
|
|
146
186
|
addEventListener("mouseup", handleMouseUp);
|
|
147
187
|
}
|
|
@@ -162,45 +202,6 @@ export default function Slider($props: SliderProps | undefined) {
|
|
|
162
202
|
return () => removeDragListeners();
|
|
163
203
|
});
|
|
164
204
|
|
|
165
|
-
function handleKey(e: KeyboardEvent) {
|
|
166
|
-
if (getDisabled()) return;
|
|
167
|
-
|
|
168
|
-
switch (e.key) {
|
|
169
|
-
case "ArrowRight":
|
|
170
|
-
case "ArrowUp": {
|
|
171
|
-
e.preventDefault();
|
|
172
|
-
setValue($state.value + getStep());
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
case "ArrowLeft":
|
|
176
|
-
case "ArrowDown": {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
setValue($state.value - getStep());
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
case "PageUp": {
|
|
182
|
-
e.preventDefault();
|
|
183
|
-
setValue($state.value + getStep() * 10);
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
case "PageDown": {
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
setValue($state.value - getStep() * 10);
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
case "Home": {
|
|
192
|
-
e.preventDefault();
|
|
193
|
-
setValue(getMin());
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
case "End": {
|
|
197
|
-
e.preventDefault();
|
|
198
|
-
setValue(getMax());
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
205
|
@render {
|
|
205
206
|
<div
|
|
206
207
|
{$props?.id}
|
|
@@ -209,34 +210,18 @@ export default function Slider($props: SliderProps | undefined) {
|
|
|
209
210
|
onmousedown={handleMouseDown}
|
|
210
211
|
&ref={track}
|
|
211
212
|
>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
role="slider"
|
|
217
|
-
tabindex={getDisabled() ? -1 : 0}
|
|
218
|
-
aria-orientation={getOrientation()}
|
|
219
|
-
aria-valuemin={getMin()}
|
|
220
|
-
aria-valuemax={getMax()}
|
|
221
|
-
aria-valuenow={getValue()}
|
|
222
|
-
aria-label={$props?.ariaLabel}
|
|
223
|
-
aria-disabled={getDisabled() ? "true" : undefined}
|
|
224
|
-
data-valid={form.valid ? "valid" : "invalid"}
|
|
225
|
-
aria-describedby={form.messageId}
|
|
226
|
-
aria-invalid={form.inField && !form.valid ? "true" : undefined}
|
|
227
|
-
onblur={form.handleBlur}
|
|
228
|
-
onkeydown={handleKey}
|
|
229
|
-
&ref={thumb}
|
|
230
|
-
/>
|
|
213
|
+
<slot>
|
|
214
|
+
<SliderRange />
|
|
215
|
+
<SliderHandle />
|
|
216
|
+
</slot>
|
|
231
217
|
</div>
|
|
232
218
|
|
|
233
219
|
<FormValues name={form.name} value={getValue()} />
|
|
234
220
|
}
|
|
235
221
|
|
|
236
222
|
@style {
|
|
237
|
-
/* Headless:
|
|
238
|
-
|
|
239
|
-
to the consuming theme */
|
|
223
|
+
/* Headless: layout only, so the range and handle position against the
|
|
224
|
+
track. All appearance is up to the consuming theme */
|
|
240
225
|
.torp-slider {
|
|
241
226
|
position: relative;
|
|
242
227
|
touch-action: none;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type SliderContext, SliderContextName } from "./SliderTypes";
|
|
3
|
+
|
|
4
|
+
interface SliderHandleProps {
|
|
5
|
+
/** An ID for the handle element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the handle element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the handle element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
/** An accessible name for the slider; falls back to the Slider's ariaLabel */
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
/** If set to true, the user cannot interact with this handle */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The thumb of a Slider that the user drags along the track, or moves with
|
|
19
|
+
* the keyboard. It carries `role="slider"` and all of the slider's ARIA
|
|
20
|
+
* value attributes.
|
|
21
|
+
*
|
|
22
|
+
* See [the WAI ARIA guidelines for Sliders](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
|
|
23
|
+
*/
|
|
24
|
+
export default function SliderHandle($props: SliderHandleProps | undefined) {
|
|
25
|
+
// Get stuff out of the Slider context
|
|
26
|
+
const context = $context[SliderContextName] as SliderContext;
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error("SliderHandle must be contained within a Slider");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getDisabled(): boolean {
|
|
32
|
+
return $props?.disabled === true || context.getDisabled();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function positionStyle(): string {
|
|
36
|
+
if (context.getOrientation() === "vertical") {
|
|
37
|
+
return `position: absolute; transform: translate(-50%, -50%); top: ${context.getPct()}%; left: 50%;`;
|
|
38
|
+
}
|
|
39
|
+
return `position: absolute; transform: translate(-50%, -50%); left: ${context.getPct()}%; top: 50%;`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleKey(e: KeyboardEvent) {
|
|
43
|
+
if (getDisabled()) return;
|
|
44
|
+
|
|
45
|
+
switch (e.key) {
|
|
46
|
+
case "ArrowRight":
|
|
47
|
+
case "ArrowUp": {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
context.setValue(context.getValue() + context.getStep());
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "ArrowLeft":
|
|
53
|
+
case "ArrowDown": {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
context.setValue(context.getValue() - context.getStep());
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "PageUp": {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
context.setValue(context.getValue() + context.getStep() * 10);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "PageDown": {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
context.setValue(context.getValue() - context.getStep() * 10);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "Home": {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
context.setValue(context.getMin());
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "End": {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
context.setValue(context.getMax());
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@render {
|
|
82
|
+
<div
|
|
83
|
+
{$props?.id}
|
|
84
|
+
class={["torp-slider-thumb", $props?.class]}
|
|
85
|
+
style={[positionStyle(), $props?.style]}
|
|
86
|
+
role="slider"
|
|
87
|
+
tabindex={getDisabled() ? -1 : 0}
|
|
88
|
+
aria-orientation={context.getOrientation()}
|
|
89
|
+
aria-valuemin={context.getMin()}
|
|
90
|
+
aria-valuemax={context.getMax()}
|
|
91
|
+
aria-valuenow={context.getValue()}
|
|
92
|
+
aria-label={$props?.ariaLabel ?? context.getAriaLabel()}
|
|
93
|
+
aria-disabled={getDisabled() ? "true" : undefined}
|
|
94
|
+
data-valid={context.valid() ? "valid" : "invalid"}
|
|
95
|
+
aria-describedby={context.messageId()}
|
|
96
|
+
aria-invalid={context.inField() && !context.valid() ? "true" : undefined}
|
|
97
|
+
onblur={() => context.handleBlur()}
|
|
98
|
+
onkeydown={handleKey}
|
|
99
|
+
onmount={(el) => {
|
|
100
|
+
context.registerHandle(el);
|
|
101
|
+
return () => {
|
|
102
|
+
context.unregisterHandle(el);
|
|
103
|
+
};
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ClassValue, StyleValue } from "@torpor/view";
|
|
2
|
+
import { type SliderContext, SliderContextName } from "./SliderTypes";
|
|
3
|
+
|
|
4
|
+
interface SliderRangeProps {
|
|
5
|
+
/** An ID for the range element */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Classes for the range element */
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
/** Styles for the range element */
|
|
10
|
+
style?: StyleValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The filled portion of a Slider, from the start of the track to the
|
|
15
|
+
* current value.
|
|
16
|
+
*/
|
|
17
|
+
export default function SliderRange($props: SliderRangeProps | undefined) {
|
|
18
|
+
// Get stuff out of the Slider context
|
|
19
|
+
const context = $context[SliderContextName] as SliderContext;
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error("SliderRange must be contained within a Slider");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fillStyle(): string {
|
|
25
|
+
const property = context.getOrientation() === "vertical" ? "height" : "width";
|
|
26
|
+
return `${property}: ${context.getPct()}%;`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@render {
|
|
30
|
+
<div
|
|
31
|
+
{$props?.id}
|
|
32
|
+
class={["torp-slider-fill", $props?.class]}
|
|
33
|
+
style={[fillStyle(), $props?.style]}
|
|
34
|
+
/>
|
|
35
|
+
}
|
|
36
|
+
}
|