You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// These go on the Thumb element (the focusable role="slider" element).
106
+
// Inlined return type (no explicit annotation — TS infers).
106
107
107
108
valueFromPercent(percent:number):number;
108
109
// Clamps to [min, max], snaps to step.
@@ -116,12 +117,13 @@ class SliderCore {
116
117
117
118
### ARIA Output
118
119
119
-
`getThumbAttrs()` returns attributes for the **Thumb** element — the focusable `role="slider"` element per the [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) and the [Media Seek Slider Example](https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-seek/):
120
+
`getAttrs()` returns attributes for the **Thumb** element — the focusable `role="slider"` element per the [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) and the [Media Seek Slider Example](https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-seek/):
120
121
121
122
```ts
122
123
{
123
124
role: 'slider',
124
-
tabIndex: 0,
125
+
tabindex: 0,
126
+
autocomplete: 'off',
125
127
'aria-valuemin': 0,
126
128
'aria-valuemax': 100,
127
129
'aria-valuenow': 45,
@@ -130,50 +132,41 @@ class SliderCore {
130
132
}
131
133
```
132
134
133
-
`aria-label` and `aria-valuetext` are NOT set by `SliderCore` — they're domain-specific. `TimeSliderCore` and `VolumeSliderCore` add them.
135
+
`aria-label` and `aria-valuetext` are NOT set by `SliderCore` — they're domain-specific. `TimeSliderCore` and `VolumeSliderCore` add them via `override getAttrs()`.
134
136
135
137
Root handles pointer events and provides context (data attrs, raw state for CSS vars) to children. Thumb carries `role="slider"`, receives keyboard focus, and owns all ARIA attributes. There is no separate Control element. Children other than Thumb (Track, Fill, Buffer, Preview) are purely visual.
136
138
137
139
## Value Snapping Precision
138
140
139
141
`valueFromPercent()` must handle floating-point precision carefully. Without rounding, values like `0.1 + 0.2` produce `0.30000000000000004`.
140
142
141
-
### `roundValueToStep`
143
+
### `roundToStep` and `clamp`
142
144
143
-
Rounds a value to the nearest step, anchored at `min`:
145
+
Located in `@videojs/utils/number` — shared utilities used by `SliderCore.valueFromPercent()`:
144
146
145
147
```ts
146
-
function roundValueToStep(value:number, step:number, min:number):number {
function clamp(value:number, min:number, max:number):number;
149
+
function roundToStep(value:number, step:number, min:number):number;
150
150
```
151
151
152
-
The `toFixed(getDecimalPrecision(step))` call derives precision from the step value itself. If `step = 0.01`, the result is fixed to 2 decimal places. If `step = 5`, no decimal places. This prevents accumulating floating-point drift.
153
-
154
-
### `getDecimalPrecision`
152
+
`roundToStep` rounds a value to the nearest step, anchored at `min`. For fractional steps, it derives decimal precision from the step's string representation to avoid floating-point drift:
Integer steps skip `toFixed` entirely. Fractional steps (e.g., `step = 0.1`) get cleaned up using the step's own decimal count.
163
+
169
164
### Where It's Used
170
165
171
-
-`SliderCore.valueFromPercent()` — snaps computed value to step precision after percent → value conversion.
166
+
-`SliderCore.valueFromPercent()` — clamps to `[min, max]` then snaps to step precision after percent → value conversion.
172
167
- Keyboard handler — rounds current value to nearest step before computing next value. Without this, a pointer drag that landed at 47.3 on a step-5 slider would produce unexpected keyboard steps (47.3 → 52.3 instead of 45 → 50).
173
168
- All percentage calculations for CSS vars use 3 decimal places (`45.123%`) for smooth visual animation, separate from value-level step precision.
174
169
175
-
Pattern taken from Base UI's `roundValueToStep` utility.
176
-
177
170
## Documentation Constants
178
171
179
172
Data attributes and CSS custom properties each get a `as const` object with JSDoc descriptions. The site docs builder extracts these to generate API reference tables.
// aria-valuetext="2 minutes, 30 seconds of 10 minutes"
264
-
265
-
formatValue(value:number):string;
266
-
// Returns formatted time string (e.g., "1:30")
256
+
// Inlined return type.
267
257
}
268
258
```
269
259
270
-
```ts
271
-
interfaceTimeMediaState {
272
-
currentTime:number;
273
-
duration:number;
274
-
seeking:boolean;
275
-
bufferedEnd:number; // end of last buffered range
276
-
}
277
-
```
260
+
Uses `MediaTimeState & MediaBufferState` from `@videojs/core` directly — no custom `TimeMediaState` wrapper. `TimeSliderState` uses `Pick<>` to select the specific fields it exposes (`currentTime`, `duration`, `seeking`), keeping the state interface focused.
278
261
279
-
Core owns the value swap: when not dragging, `value` = `currentTime`. When dragging, `value` = `valueFromPercent(interaction.dragPercent)`. This domain logic lives in Core so both frameworks get it for free.
262
+
Core owns the value swap: when not dragging, `value` = `currentTime`. When dragging, `value` = `valueFromPercent(interaction.dragPercent)`. `bufferedEnd` is computed internally from `media.buffered` (the end of the last buffered range). This domain logic lives in Core so both frameworks get it for free.
280
263
281
264
### Time Formatting
282
265
283
-
Uses `TimeCore` (already exists in `@videojs/core`) for `aria-valuetext`. On initialization and focus, the value text follows the pattern `"{current} of {duration}"`. During value changes, duration is omitted (`"{current}"` only) to reduce screen reader verbosity. Each uses a human-readable phrase from `formatTimeAsPhrase()`. See [decisions.md](decisions.md#time-slider-aria-valuetext-format).
266
+
Uses `formatTimeAsPhrase()` from `@videojs/utils/time` for `aria-valuetext`. The value text follows the pattern `"{current} of {duration}"` using human-readable phrases. See [decisions.md](decisions.md#time-slider-aria-valuetext-format).
// aria-valuetext="75 percent" (or "75 percent, muted" when muted)
290
+
// Inlined return type.
309
291
}
310
292
```
311
293
312
-
```ts
313
-
interfaceVolumeMediaState {
314
-
volume:number; // 0-1 from store
315
-
muted:boolean;
316
-
}
317
-
```
294
+
Uses `MediaVolumeState` from `@videojs/core` directly — no custom `VolumeMediaState` wrapper. `VolumeSliderState` uses `Pick<>` to select the specific fields it exposes (`volume`, `muted`).
Volume is stored as 0-1 in the store but displayed as 0-100 in the slider. `VolumeSliderCore` handles this conversion.
322
299
@@ -442,7 +419,7 @@ All via `onKeyDown` on the **Thumb** element (the focusable `role="slider"` elem
442
419
443
420
**RTL direction:**`createSlider` accepts an `isRTL` callback. When RTL, `ArrowRight` subtracts `stepPercent` (decreases) and `ArrowLeft` adds `stepPercent` (increases). `ArrowUp`/`ArrowDown` are unaffected. Both Base UI and Vidstack implement this.
444
421
445
-
**Round before stepping:** Before computing the next value from a keyboard step, the current value is rounded to the nearest step via `roundValueToStep()`. This prevents drift when the current value isn't aligned to a step boundary (e.g., after a pointer drag landed between steps). See [Value Snapping Precision](#value-snapping-precision).
422
+
**Round before stepping:** Before computing the next value from a keyboard step, the current value is rounded to the nearest step via `roundToStep()` (from `@videojs/utils/number`). This prevents drift when the current value isn't aligned to a step boundary (e.g., after a pointer drag landed between steps). See [Value Snapping Precision](#value-snapping-precision).
446
423
447
424
**Numeric keys** match YouTube behavior (0 = start, 5 = midpoint, 9 = 90%). Only active when `metaKey` is not held.
**Decision:**`SliderCore.getState()` accepts `SliderInteraction` and a value separately. Domain cores (`TimeSliderCore`, `VolumeSliderCore`) accept `(interaction, media)` where media is a domain-specific type (`TimeMediaState`, `VolumeMediaState`). Core owns the merge logic — e.g., the value swap (`dragging ? valueFromPercent(dragPercent) : currentTime`).
348
+
**Decision:**`SliderCore.getState()` accepts `SliderInteraction` and a value separately. Domain cores (`TimeSliderCore`, `VolumeSliderCore`) accept `(media, interaction)` where media is the canonical type from `@videojs/core` (`MediaTimeState & MediaBufferState`, `MediaVolumeState`). Core owns the merge logic — e.g., the value swap (`dragging ? valueFromPercent(dragPercent) : currentTime`). Media is the first parameter since it's the primary input; interaction is secondary context.
349
349
350
350
**Alternatives:**
351
351
@@ -419,7 +419,7 @@ This follows the [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patte
419
419
420
420
### Domain Sliders Set aria-label and aria-valuetext
421
421
422
-
**Decision:** Generic `SliderCore.getThumbAttrs()` sets `role`, `tabIndex`, `aria-valuemin/max/now`, `aria-orientation`, `aria-disabled` on the Thumb element. Domain cores (`TimeSliderCore`, `VolumeSliderCore`) extend with `aria-label` and `aria-valuetext`. Domain Roots accept a `label` prop (default `"Seek"` / `"Volume"`) that feeds into `aria-label`, and provide the complete ARIA attrs to Thumb via context.
422
+
**Decision:** Generic `SliderCore.getAttrs()` sets `role`, `tabindex`, `autocomplete`, `aria-valuemin/max/now`, `aria-orientation`, `aria-disabled` on the Thumb element. Domain cores (`TimeSliderCore`, `VolumeSliderCore`) extend via `override getAttrs()` with `aria-label` and `aria-valuetext`. Domain Roots accept a `label` prop (default `"Seek"` / `"Volume"`) that feeds into `aria-label`, and provide the complete ARIA attrs to Thumb via context.
@@ -492,7 +492,7 @@ Inherited by all children (including `data-seeking`).
492
492
-**Value formatting:** Provides time formatter (`formatTime`) to `Slider.Value` children — `type="current"` shows formatted current time (`1:30`), `type="pointer"` shows formatted pointer time.
493
493
-**Data attributes:** All slider data attributes + `data-seeking` are propagated to children.
494
494
-**ARIA for Thumb:** Provides domain-specific ARIA attrs to `Slider.Thumb` via context.
495
-
-**Keyboard step values:**`step` defaults to `5` (seconds), `largeStep` defaults to `10` (seconds).
495
+
-**Keyboard step values:**`step` defaults to `1` (second), `largeStep` defaults to `10` (seconds).
496
496
497
497
#### ARIA (on Thumb)
498
498
@@ -565,7 +565,7 @@ import { VolumeSlider } from '@videojs/react';
565
565
| ---- | ---- | ------- | ----------- |
566
566
|`label`|`string`|`'Volume'`| Accessible label for the slider. Sets `aria-label` on Thumb. |
0 commit comments