@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.
Files changed (60) hide show
  1. package/dist/ColorPicker/ColorPicker.torp +36 -75
  2. package/dist/ColorPicker/ColorPickerInput.torp +122 -0
  3. package/dist/ColorPicker/ColorPickerTypes.ts +12 -0
  4. package/dist/ColorPicker/index.d.ts +40 -1
  5. package/dist/ColorPicker/index.js +2 -1
  6. package/dist/CommandPalette/CommandPalette.torp +49 -47
  7. package/dist/CommandPalette/CommandPaletteInput.torp +51 -0
  8. package/dist/CommandPalette/CommandPaletteItem.torp +62 -0
  9. package/dist/CommandPalette/CommandPaletteList.torp +55 -0
  10. package/dist/CommandPalette/CommandPaletteTypes.ts +32 -0
  11. package/dist/CommandPalette/index.d.ts +88 -2
  12. package/dist/CommandPalette/index.js +4 -1
  13. package/dist/DataGrid/DataGrid.torp +22 -22
  14. package/dist/DataGrid/DataGridCell.torp +50 -0
  15. package/dist/DataGrid/DataGridColumnHeader.torp +54 -0
  16. package/dist/DataGrid/DataGridTypes.ts +17 -0
  17. package/dist/DataGrid/index.d.ts +57 -2
  18. package/dist/DataGrid/index.js +3 -2
  19. package/dist/DatePicker/DatePicker.torp +45 -65
  20. package/dist/DatePicker/DatePickerContent.torp +82 -0
  21. package/dist/DatePicker/DatePickerTrigger.torp +56 -0
  22. package/dist/DatePicker/DatePickerTypes.ts +36 -0
  23. package/dist/DatePicker/index.d.ts +61 -2
  24. package/dist/DatePicker/index.js +3 -1
  25. package/dist/DateRangePicker/DateRangePicker.torp +52 -76
  26. package/dist/DateRangePicker/DateRangePickerContent.torp +90 -0
  27. package/dist/DateRangePicker/DateRangePickerTrigger.torp +59 -0
  28. package/dist/DateRangePicker/DateRangePickerTypes.ts +41 -0
  29. package/dist/DateRangePicker/index.d.ts +61 -2
  30. package/dist/DateRangePicker/index.js +3 -1
  31. package/dist/Progress/Progress.torp +24 -14
  32. package/dist/Progress/ProgressIndicator.torp +52 -0
  33. package/dist/Progress/ProgressTypes.ts +8 -0
  34. package/dist/Progress/index.d.ts +34 -1
  35. package/dist/Progress/index.js +2 -1
  36. package/dist/Rating/Rating.torp +41 -37
  37. package/dist/Rating/RatingStar.torp +71 -0
  38. package/dist/Rating/RatingTypes.ts +22 -0
  39. package/dist/Rating/index.d.ts +39 -1
  40. package/dist/Rating/index.js +2 -1
  41. package/dist/Slider/Slider.torp +48 -63
  42. package/dist/Slider/SliderHandle.torp +107 -0
  43. package/dist/Slider/SliderRange.torp +36 -0
  44. package/dist/Slider/SliderTypes.ts +34 -0
  45. package/dist/Slider/index.d.ts +65 -1
  46. package/dist/Slider/index.js +3 -1
  47. package/dist/TagInput/TagInput.torp +63 -84
  48. package/dist/TagInput/TagInputField.torp +68 -0
  49. package/dist/TagInput/TagInputSuggestions.torp +79 -0
  50. package/dist/TagInput/TagInputTag.torp +55 -0
  51. package/dist/TagInput/TagInputTypes.ts +42 -0
  52. package/dist/TagInput/index.d.ts +93 -1
  53. package/dist/TagInput/index.js +4 -1
  54. package/dist/TimePicker/TimePicker.torp +88 -85
  55. package/dist/TimePicker/TimePickerPart.torp +61 -0
  56. package/dist/TimePicker/TimePickerPeriod.torp +48 -0
  57. package/dist/TimePicker/TimePickerTypes.ts +37 -0
  58. package/dist/TimePicker/index.d.ts +61 -1
  59. package/dist/TimePicker/index.js +3 -1
  60. 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
+ }
@@ -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
- export { Progress };
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 };
@@ -1,2 +1,3 @@
1
1
  import Progress from "./Progress.torp";
2
- export { Progress };
2
+ import ProgressIndicator from "./ProgressIndicator.torp";
3
+ export { Progress, ProgressIndicator };
@@ -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
- function handleClick(value: number) {
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
- @for (let value of $state.stars) {
184
- @key = value
185
- <button
186
- type="button"
187
- class="torp-rating-star"
188
- role="radio"
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
+ }
@@ -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
- export { Rating };
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 };
@@ -1,2 +1,3 @@
1
1
  import Rating from "./Rating.torp";
2
- export { Rating };
2
+ import RatingStar from "./RatingStar.torp";
3
+ export { Rating, RatingStar };
@@ -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 thumb: HTMLElement;
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
- thumb.focus();
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
- <div class="torp-slider-fill" style={`${getOrientation() === "vertical" ? "height" : "width"}: ${getPct()}%;`} />
213
- <div
214
- class="torp-slider-thumb"
215
- style={`position: absolute; transform: translate(-50%, -50%); ${getOrientation() === "vertical" ? `top: ${getPct()}%; left: 50%` : `left: ${getPct()}%; top: 50%`};`}
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: all positioning is done with inline styles (so that theme
238
- CSS can't dislodge the thumb from the track). All appearance is up
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
+ }