Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,20 @@ is version-independent.

## Conformance matrix

| Control (file) | Target interface | Required signal | Optional already present | Missing | Collisions / risk |
| ------------------------------------------------------------------------- | ------------------------------------ | ----------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **checkbox-root** (`checkbox/src/checkbox-root.ts`) | `FormCheckboxControl` | `checked = model` ✅ | `disabled`, `readonly`, `required`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🔴 has `value = input('on')` — interface **forbids** `value`; `checked` is `CheckedState` (`boolean \| 'indeterminate'`), interface wants `boolean` |
| **checkbox-group** (`checkbox/src/checkbox-group.ts`) | `FormValueControl<string[]>` | `value = model` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **switch-root** (`switch/src/switch-root.ts`) | `FormCheckboxControl` | `checked = model<boolean>` ✅ | `disabled`, `required`, `readonly`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🟢 clean — Base UI rewrite added `readonly`/`name` and dropped CDK `_IdGenerator` |
| **radio-root** (`radio/src/radio-root.directive.ts`) | `FormValueControl<string \| null>` | `value = model` ✅ | `disabled`, `readonly`, `required`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🟢 most ready |
| **select-root** (`select/src/select-root.ts`) | `FormValueControl<AcceptableValue…>` | `value = model` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **toggle-group** (`toggle-group/src/toggle-group-base.ts`) | `FormValueControl<string[]>` | `value = model<string[]>` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **slider-root** (`slider/src/slider-root.component.ts`) | `FormValueControl<number[]>` | ❌ named `modelValue` | `disabled`, `min`, `max` | add/rename `value`; `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | 🔴 value signal is `modelValue`, not `value` |
| **number-field-root** (`number-field/src/number-field-root.directive.ts`) | `FormValueControl<number>` | `value = model<number>` ✅ | `disabled`, `readonly`, `required`, `min`, `max` | `invalid`, `errors`, `touched`, `dirty`, `name` | 🟢 `min/max` already align |
| **input** (`input/src/input.directive.ts`) | `FormValueControl<RdxInputValue>` | `value = model` ✅ | `disabled`, `required`, **`invalid`** ✅ | `readonly`, `errors`, `touched`, `dirty`, `minLength/maxLength/pattern`, `name` | 🟢 only control that already has `invalid` |
| **date-field-root** (`date-field/src/date-field-root.directive.ts`) | `FormValueControl<DateValue>` | `value = model` ✅ | `disabled`, `readonly` | `required`, `invalid`, `errors`, `touched`, `dirty`, `name`, `min/max` | — |
| **time-field-root** (`time-field/src/time-field-root.directive.ts`) | `FormValueControl<TimeValue>` | `value = model` ✅ | `disabled`, `readonly` | `required`, `invalid`, `errors`, `touched`, `dirty`, `name`, `min/max` | — |
| **editable-root** (`editable/src/editable-root.ts`) | `FormValueControl<string>` | `value = model<string>` ✅ | `disabled`, `readonly`, `required`, `maxLength` | `invalid`, `errors`, `touched`, `dirty`, `name`, `minLength`, `pattern` | — |
| Control (file) | Target interface | Required signal | Optional already present | Missing | Collisions / risk |
| ------------------------------------------------------------------------- | -------------------------------------- | ----------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **checkbox-root** (`checkbox/src/checkbox-root.ts`) | `FormCheckboxControl` | `checked = model` ✅ | `disabled`, `readonly`, `required`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🔴 has `value = input('on')` — interface **forbids** `value`; `checked` is `CheckedState` (`boolean \| 'indeterminate'`), interface wants `boolean` |
| **checkbox-group** (`checkbox/src/checkbox-group.ts`) | `FormValueControl<string[]>` | `value = model` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **switch-root** (`switch/src/switch-root.ts`) | `FormCheckboxControl` | `checked = model<boolean>` ✅ | `disabled`, `required`, `readonly`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🟢 clean — Base UI rewrite added `readonly`/`name` and dropped CDK `_IdGenerator` |
| **radio-root** (`radio/src/radio-root.directive.ts`) | `FormValueControl<string \| null>` | `value = model` ✅ | `disabled`, `readonly`, `required`, `name` | `invalid`, `errors`, `touched`, `dirty` | 🟢 most ready |
| **select-root** (`select/src/select-root.ts`) | `FormValueControl<AcceptableValue…>` | `value = model` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **toggle-group** (`toggle-group/src/toggle-group-base.ts`) | `FormValueControl<string[]>` | `value = model<string[]>` ✅ | `disabled` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty`, `name` | — |
| **slider-root** (`slider/src/slider-root.ts`) | `FormValueControl<number \| number[]>` | `value = model` ✅ | `disabled`, `min`, `max`, `name`, `form` | `required`, `readonly`, `invalid`, `errors`, `touched`, `dirty` | 🟢 Base UI rewrite renamed `modelValue`→`value`, added `name`/`form`, uses `core` `_IdGenerator`; value is a `number \| number[]` union (single/range) |
| **number-field-root** (`number-field/src/number-field-root.directive.ts`) | `FormValueControl<number>` | `value = model<number>` ✅ | `disabled`, `readonly`, `required`, `min`, `max` | `invalid`, `errors`, `touched`, `dirty`, `name` | 🟢 `min/max` already align |
| **input** (`input/src/input.directive.ts`) | `FormValueControl<RdxInputValue>` | `value = model` ✅ | `disabled`, `required`, **`invalid`** ✅ | `readonly`, `errors`, `touched`, `dirty`, `minLength/maxLength/pattern`, `name` | 🟢 only control that already has `invalid` |
| **date-field-root** (`date-field/src/date-field-root.directive.ts`) | `FormValueControl<DateValue>` | `value = model` ✅ | `disabled`, `readonly` | `required`, `invalid`, `errors`, `touched`, `dirty`, `name`, `min/max` | — |
| **time-field-root** (`time-field/src/time-field-root.directive.ts`) | `FormValueControl<TimeValue>` | `value = model` ✅ | `disabled`, `readonly` | `required`, `invalid`, `errors`, `touched`, `dirty`, `name`, `min/max` | — |
| **editable-root** (`editable/src/editable-root.ts`) | `FormValueControl<string>` | `value = model<string>` ✅ | `disabled`, `readonly`, `required`, `maxLength` | `invalid`, `errors`, `touched`, `dirty`, `name`, `minLength`, `pattern` | — |

## Collisions to resolve by design (Angular 21)

Expand All @@ -64,16 +64,21 @@ is version-independent.
- `checked: CheckedState` (`boolean | 'indeterminate'`) is incompatible with
`checked: model<boolean>()`. Indeterminate is a third state to reconcile.
- Hardest case — prototype first.
2. 🔴 **slider — `modelValue` instead of `value`.** Either rename (breaking for
templates and `valueChange`) or add a parallel `value` model. Needs a decision.
2. ✅ **slider — resolved by the Base UI rewrite.** `modelValue` was renamed to
`value = model<number | number[]>()` (with `name`/`form` added and the CDK
`_IdGenerator` swapped for the `core` one), so the control now satisfies
`FormValueControl`'s required signal. Remaining gap is only the shared
`invalid`/`errors`/`touched`/`dirty` batch (#3). Nuance: the value is a
`number | number[]` union (single thumb vs range), so `FormValueControl<T>` is
parameterised on the union rather than a fixed shape.
3. 🟡 **Most controls lack `invalid`/`errors`/`touched`/`dirty`.** Homogeneous
batch — close it with one shared pattern (see prep #3).

## Readiness ranking

- 🟢 **radio, switch, input, number-field** — clean, minimal edits → best first-pilot candidates.
- 🟡 **select, toggle-group, checkbox-group, date/time-field, editable** — need `invalid/errors/touched/dirty` + `required/name`.
- 🔴 **checkbox, slider** — require name/type collision resolution.
- 🟡 **select, toggle-group, checkbox-group, date/time-field, editable, slider** — need `invalid/errors/touched/dirty` (+ `required/name` where missing; slider may also add optional `required`/`readonly`).
- 🔴 **checkbox** — requires name/type collision resolution (forbidden `value` member + `indeterminate` third state).

## Open question (not answered by the matrix)

Expand Down
103 changes: 69 additions & 34 deletions apps/radix-docs/src/content/primitives/components/slider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,84 +7,119 @@ description: .

# Slider

<Description>An input where the user selects a value from within a given range.</Description>
<Description>An input where the user selects a value, or a range of values, from within a given range.</Description>

<ComponentPreview name="slider" file="slider-demo" />

<FeatureList items={[
'Can be controlled or uncontrolled.',
'Supports multiple thumbs.',
'Supports a minimum value between thumbs.',
'Supports touch or click on track to update value.',
'Supports Right to Left direction.',
'Single value or multiple thumbs for a range.',
'Configurable thumb collision behavior (push, swap, none).',
'Supports a minimum distance between thumbs.',
'Press or drag anywhere on the control to update the value.',
'Horizontal and vertical orientation, with RTL support.',
'Value formatting with Intl.NumberFormat.',
'Full keyboard navigation.'
]} />

## Anatomy

```html
<rdx-slider>
<rdx-slider-track>
<rdx-slider-range />
</rdx-slider-track>
<rdx-slider-thumb />
</rdx-slider>
<div rdxSliderRoot>
<output rdxSliderValue></output>
<div rdxSliderControl>
<div rdxSliderTrack>
<div rdxSliderIndicator></div>
<div rdxSliderThumb>
<input rdxSliderThumbInput />
</div>
</div>
</div>
</div>
```

## API Reference

### Root
`RdxSliderRootComponent`
`RdxSliderRoot`

<PropsTable name="RdxSliderRootComponent" />
<PropsTable name="RdxSliderRoot" />

<EmitsTable name="RdxSliderRootComponent" />
<EmitsTable name="RdxSliderRoot" />

<DataAttributesTable attributes={[
{ name: '[data-disabled]', value: 'Present when disabled' },
{ name: '[data-dragging]', value: 'Present while dragging' },
{ name: '[data-orientation]', value: '"vertical" | "horizontal"' }
]} />

### Track
`RdxSliderTrackComponent`
### Control
`RdxSliderControl`

The track that contains the `SliderRange`.
The interactive area. Handles pointer presses and drags.

<DataAttributesTable attributes={[
{ name: '[data-disabled]', value: 'Present when disabled' },
{ name: '[data-dragging]', value: 'Present while dragging' },
{ name: '[data-orientation]', value: '"vertical" | "horizontal"' }
]} />

### Range
`RdxSliderRangeComponent`
### Track
`RdxSliderTrack`

The range part. Must live inside `SliderTrack`.
The track that contains the `Indicator` and `Thumb`s.

<DataAttributesTable attributes={[
{ name: '[data-disabled]', value: 'Present when disabled' },
{ name: '[data-orientation]', value: '"vertical" | "horizontal"' }
]} />

### Indicator
`RdxSliderIndicator`

Visualises the filled portion of the track. Must live inside `Track`.

<DataAttributesTable attributes={[
{ name: '[data-disabled]', value: 'Present when disabled' },
{ name: '[data-orientation]', value: '"vertical" | "horizontal"' }
]} />

### Thumb
`RdxSliderThumbComponent`
`RdxSliderThumb`

A draggable thumb. You can render multiple thumbs.
A draggable thumb. Render one per value and wrap an `input[rdxSliderThumbInput]`.

<PropsTable name="RdxSliderThumb" />

<DataAttributesTable attributes={[
{ name: '[data-index]', value: 'The index of the thumb' },
{ name: '[data-disabled]', value: 'Present when disabled' },
{ name: '[data-orientation]', value: '"vertical" | "horizontal"' }
]} />

### Thumb Input
`RdxSliderThumbInput`

The nested native `input[type=range]`. Visually hidden but drives keyboard,
accessibility and form submission.

<PropsTable name="RdxSliderThumbInput" />

### Value
`RdxSliderValue`

Displays the formatted value(s).

<PropsTable name="RdxSliderValue" />

## Accessibility
Adheres to the [Slider WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb).

### Keyboard Interactions

<KeyboardTable attributes={[
{
key: 'List',
key: 'Home',
description: 'Sets the value to its minimum.',
},
{
Expand All @@ -93,34 +128,34 @@ Adheres to the [Slider WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/
},
{
key: 'PageDown',
description: ' Decreases the value by a larger step.',
description: 'Decreases the value by a larger step.',
},
{
key: 'PageUP',
description: ' Increases the value by a larger step.',
key: 'PageUp',
description: 'Increases the value by a larger step.',
},
{
key: 'ArrowDown',
description: ' Decreases the value by the step amount.',
description: 'Decreases the value by the step amount.',
},
{
key: 'ArrowRight',
description: 'Increments/decrements by the step value depending on orientation.',
key: 'ArrowUp',
description: 'Increases the value by the step amount.',
},
{
key: 'ArrowUp',
description: ' Increases the value by the step amount.',
key: 'ArrowRight',
description: 'Increments/decrements by the step value depending on direction.',
},
{
key: 'ArrowLeft',
description: ' Increments/decrements by the step value depending on orientation. ',
description: 'Increments/decrements by the step value depending on direction.',
},
{
key: 'Shift + ArrowUp',
description: ' Increases the value by a larger step.',
description: 'Increases the value by a larger step.',
},
{
key: 'Shift + ArrowDown',
description: ' Decreases the value by a larger step.',
description: 'Decreases the value by a larger step.',
}
]} />
Loading
Loading