mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
improvements
This commit is contained in:
@@ -7,7 +7,20 @@ icon: slider
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-slider></wa-slider>
|
||||
<wa-slider
|
||||
label="Number of cats"
|
||||
hint="Limit six per household"
|
||||
name="value"
|
||||
value="3"
|
||||
min="0"
|
||||
max="6"
|
||||
with-markers
|
||||
with-tooltip
|
||||
with-references
|
||||
>
|
||||
<span slot="reference">Less</span>
|
||||
<span slot="reference">More</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
:::info
|
||||
@@ -18,7 +31,7 @@ This component works with standard `<form>` elements. Please refer to the sectio
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the range an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
Use the `label` attribute to give the slider an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Volume" min="0" max="100"></wa-slider>
|
||||
@@ -26,18 +39,234 @@ Use the `label` attribute to give the range an accessible label. For labels that
|
||||
|
||||
### Hint
|
||||
|
||||
Add descriptive hint to a range with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
Add descriptive hint to a slider with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Volume" hint="Controls the volume of the current song." min="0" max="100"></wa-slider>
|
||||
```
|
||||
|
||||
### Min, Max, and Step
|
||||
### Showing tooltips
|
||||
|
||||
Use the `min` and `max` attributes to set the range's minimum and maximum values, respectively. The `step` attribute determines the value's interval when increasing and decreasing.
|
||||
Use the `with-tooltip` attribute to display a tooltip with the current value when the slider is focused or being dragged.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider min="0" max="10" step="1"></wa-slider>
|
||||
<wa-slider label="Quality" name="quality" min="0" max="100" value="50" with-tooltip></wa-slider>
|
||||
```
|
||||
|
||||
### Setting min, max, and step
|
||||
|
||||
Use the `min` and `max` attributes to define the slider's range, and the `step` attribute to control the increment between values.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Between zero and one" min="0" max="1" step="0.1" value="0.5" with-tooltip></wa-slider>
|
||||
```
|
||||
|
||||
### Showing markers
|
||||
|
||||
Use the `with-markers` attribute to display visual indicators at each step increment. This works best with sliders that have a smaller range of values.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Size" name="size" min="0" max="8" value="4" with-markers></wa-slider>
|
||||
```
|
||||
|
||||
### Adding references
|
||||
|
||||
Use the `with-references` attribute along with the `reference` slot to add contextual labels below the slider. References are automatically spaced using `space-between`, making them easy to align with the start, center, and end positions.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider label="Speed" name="speed" min="1" max="5" value="3" with-markers with-references>
|
||||
<span slot="reference">Slow</span>
|
||||
<span slot="reference">Medium</span>
|
||||
<span slot="reference">Fast</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
:::info
|
||||
If you want to show a reference next to a specific marker, you can add `position: absolute` to it and set the `left`, `right`, `top`, or `bottom` property to a percentage that corresponds to the marker's position.
|
||||
:::
|
||||
|
||||
### Formatting the value
|
||||
|
||||
Customize how values are displayed in tooltips and announced to screen readers using the `valueFormatter` property. Set it to a function that accepts a number and returns a formatted string. The [`Intl.NumberFormat API`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) is particularly useful for this.
|
||||
|
||||
```html {.example}
|
||||
<!-- Percent -->
|
||||
<wa-slider
|
||||
id="slider__percent"
|
||||
label="Percentage"
|
||||
name="percentage"
|
||||
value="0.5"
|
||||
min="0"
|
||||
max="1"
|
||||
step=".01"
|
||||
with-tooltip
|
||||
></wa-slider
|
||||
><br />
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__percent');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'percent' });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Duration -->
|
||||
<wa-slider id="slider__duration" label="Duration" name="duration" value="12" min="0" max="24" with-tooltip></wa-slider
|
||||
><br />
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__duration');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'unit', unit: 'hour', unitDisplay: 'long' });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Currency -->
|
||||
<wa-slider id="slider__currency" label="Currency" name="currency" min="0" max="100" value="50" with-tooltip></wa-slider>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__currency');
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
currencyDisplay: 'symbol',
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Range selection
|
||||
|
||||
Use the `range` attribute to enable dual-thumb selection for choosing a range of values. Set the initial thumb positions with the `min-value` and `max-value` attributes.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
label="Price Range"
|
||||
hint="Select minimum and maximum price"
|
||||
name="price"
|
||||
range
|
||||
min="0"
|
||||
max="100"
|
||||
min-value="20"
|
||||
max-value="80"
|
||||
with-tooltip
|
||||
with-references
|
||||
id="slider__range"
|
||||
>
|
||||
<span slot="reference">$0</span>
|
||||
<span slot="reference">$50</span>
|
||||
<span slot="reference">$100</span>
|
||||
</wa-slider>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__range');
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
|
||||
|
||||
customElements.whenDefined('wa-slider').then(() => {
|
||||
slider.valueFormatter = value => formatter.format(value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
For range sliders, the `minValue` and `maxValue` properties represent the current positions of the thumbs. When the form is submitted, both values will be included as separate entries with the same name.
|
||||
|
||||
```ts
|
||||
const slider = document.querySelector('wa-slider[range]');
|
||||
|
||||
// Get the current values
|
||||
console.log(`Min value: ${slider.minValue}, Max value: ${slider.maxValue}`);
|
||||
|
||||
// Set the values programmatically
|
||||
slider.minValue = 30;
|
||||
slider.maxValue = 70;
|
||||
```
|
||||
|
||||
### Changing the orientation
|
||||
|
||||
Set the `orientation` attribute to `vertical` to create a vertical slider. Vertical sliders automatically center themselves and fill the available vertical space.
|
||||
|
||||
```html {.example}
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<wa-slider orientation="vertical" label="Volume" name="volume" value="65" style="width: 80px"></wa-slider>
|
||||
|
||||
<wa-slider orientation="vertical" label="Bass" name="bass" value="50" style="width: 80px"></wa-slider>
|
||||
|
||||
<wa-slider orientation="vertical" label="Treble" name="treble" value="40" style="width: 80px"></wa-slider>
|
||||
</div>
|
||||
```
|
||||
|
||||
Range sliders can also be vertical.
|
||||
|
||||
```html {.example}
|
||||
<div style="height: 300px; display: flex; align-items: center; gap: 2rem;">
|
||||
<wa-slider
|
||||
label="Temperature Range"
|
||||
orientation="vertical"
|
||||
range
|
||||
min="0"
|
||||
max="100"
|
||||
min-value="30"
|
||||
max-value="70"
|
||||
with-tooltip
|
||||
id="slider__vertical-range"
|
||||
>
|
||||
</wa-slider>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const slider = document.getElementById('slider__vertical-range');
|
||||
slider.valueFormatter = value => {
|
||||
return new Intl.NumberFormat('en', {
|
||||
style: 'unit',
|
||||
unit: 'fahrenheit',
|
||||
unitDisplay: 'short',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}).format(value);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Changing the size
|
||||
|
||||
Control the slider's size using the `size` attribute with options ranging from `xs` to `xl`.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider size="xs" value="50" label="Extra small"></wa-slider><br />
|
||||
<wa-slider size="sm" value="50" label="Small"></wa-slider><br />
|
||||
<wa-slider size="md" value="50" label="Medium"></wa-slider><br />
|
||||
<wa-slider size="lg" value="50" label="Large"></wa-slider><br />
|
||||
<wa-slider size="xl" value="50" label="Extra large"></wa-slider>
|
||||
```
|
||||
|
||||
### Changing the indicator's offset
|
||||
|
||||
By default, the filled indicator extends from the minimum value to the current position. Use the `indicator-offset` attribute to change the starting point of this visual indicator.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
label="Cat playfulness"
|
||||
hint="Energy level during playtime"
|
||||
name="value"
|
||||
value="0"
|
||||
min="-5"
|
||||
max="5"
|
||||
indicator-offset="0"
|
||||
with-markers
|
||||
with-tooltip
|
||||
with-references
|
||||
>
|
||||
<span slot="reference">Lazy</span>
|
||||
<span slot="reference">Zoomies</span>
|
||||
</wa-slider>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
@@ -45,77 +274,136 @@ Use the `min` and `max` attributes to set the range's minimum and maximum values
|
||||
Use the `disabled` attribute to disable a slider.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider disabled></wa-slider>
|
||||
<wa-slider label="Disabled" value="50" disabled></wa-slider>
|
||||
```
|
||||
|
||||
### Tooltip Placement
|
||||
### Required
|
||||
|
||||
By default, the tooltip is shown on top. Set `tooltip` to `bottom` to show it below the slider.
|
||||
Mark a slider as required using the `required` attribute. Users must interact with required sliders before the form can be submitted.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider tooltip="bottom"></wa-slider>
|
||||
<form action="about:blank" target="_blank" method="get">
|
||||
<wa-slider name="slide" label="Required slider" min="0" max="10" required></wa-slider>
|
||||
<br />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Disable the Tooltip
|
||||
### Using custom validation
|
||||
|
||||
To disable the tooltip, set `tooltip` to `none`.
|
||||
Set custom validation messages using the `setCustomValidity()` method. Pass an empty string to clear any custom validation errors.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider tooltip="none"></wa-slider>
|
||||
```
|
||||
<form action="about:blank" method="get" target="_blank" id="slider__custom-validation">
|
||||
<wa-slider
|
||||
name="value"
|
||||
label="Select a value"
|
||||
hint="This field will be invalid until custom validation is removed"
|
||||
></wa-slider>
|
||||
<br />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
### Custom Track Colors
|
||||
<script type="module">
|
||||
import { allDefined } from '/dist/webawesome.js';
|
||||
|
||||
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
|
||||
const form = document.getElementById('slider__custom-validation');
|
||||
const slider = form.querySelector('wa-slider');
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
style="
|
||||
--track-color-active: var(--wa-color-brand-fill-loud);
|
||||
--track-color-inactive: var(--wa-color-brand-fill-normal);
|
||||
"
|
||||
></wa-slider>
|
||||
```
|
||||
await allDefined();
|
||||
|
||||
### Custom Track Offset
|
||||
|
||||
You can customize the initial offset of the active track using the `--track-active-offset` custom property.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
min="-100"
|
||||
max="100"
|
||||
style="
|
||||
--track-color-active: var(--wa-color-brand-fill-loud);
|
||||
--track-color-inactive: var(--wa-color-brand-fill-normal);
|
||||
--track-active-offset: 50%;
|
||||
"
|
||||
></wa-slider>
|
||||
```
|
||||
|
||||
### Custom Tooltip Formatter
|
||||
|
||||
You can change the tooltip's content by setting the `tooltipFormatter` property to a function that accepts the range's value as an argument.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider min="0" max="100" step="1" class="range-with-custom-formatter"></wa-slider>
|
||||
|
||||
<script>
|
||||
const range = document.querySelector('.range-with-custom-formatter');
|
||||
range.tooltipFormatter = value => `Total - ${value}%`;
|
||||
slider.setCustomValidity('Not so fast, bubba!');
|
||||
</script>
|
||||
```
|
||||
|
||||
### Right-to-Left languages
|
||||
### Styling validation
|
||||
|
||||
The component adapts to right-to-left (RTL) languages as you would expect.
|
||||
Apply custom styles to valid and invalid sliders using the `:valid` and `:invalid` pseudo-classes.
|
||||
|
||||
```html {.example}
|
||||
<wa-slider
|
||||
dir="rtl"
|
||||
label="مقدار"
|
||||
hint="التحكم في مستوى صوت الأغنية الحالية."
|
||||
style="--track-color-active: var(--wa-color-brand-fill-loud)"
|
||||
value="10"
|
||||
></wa-slider>
|
||||
<form action="about:blank" method="get" target="_blank" class="slider__validation-pseudo">
|
||||
<wa-slider name="value" label="Select a value" min="0" max="5" value="0" with-markers with-tooltip></wa-slider>
|
||||
<br />
|
||||
<button type="submit">Submit</button>
|
||||
<button type="reset">Reset</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.slider__validation-pseudo {
|
||||
wa-slider:valid {
|
||||
outline: solid 2px var(--wa-color-success-border);
|
||||
outline-offset: 1rem;
|
||||
}
|
||||
|
||||
wa-slider:invalid {
|
||||
outline: solid 2px var(--wa-color-danger-border);
|
||||
outline-offset: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import { allDefined } from '/dist/webawesome.js';
|
||||
|
||||
const form = document.querySelector('.slider__validation-pseudo');
|
||||
const slider = form.querySelector('wa-slider');
|
||||
const validationMessage = 'Select a number greater than zero';
|
||||
|
||||
await allDefined();
|
||||
|
||||
slider.setCustomValidity(validationMessage);
|
||||
|
||||
async function updateValidity() {
|
||||
await slider.updateComplete;
|
||||
slider.setCustomValidity(slider.value > 0 ? '' : validationMessage);
|
||||
}
|
||||
|
||||
slider.addEventListener('input', updateValidity);
|
||||
form.addEventListener('reset', updateValidity);
|
||||
</script>
|
||||
```
|
||||
|
||||
However, these selectors will match even before the user has had a chance to fill out the form. More often than not, you'll want to use the `user-valid` and `user-invalid` [custom states](#custom-states) instead. This way, validation styles are only shown _after_ the user interacts with the form control or when the form is submitted.
|
||||
|
||||
```html {.example}
|
||||
<form action="about:blank" method="get" target="_blank" class="slider__validation-custom">
|
||||
<wa-slider name="value" label="Select a value" min="0" max="5" value="0" with-markers with-tooltip></wa-slider>
|
||||
<br />
|
||||
<button type="submit">Submit</button>
|
||||
<button type="reset">Reset</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.slider__validation-custom {
|
||||
wa-slider:state(user-valid) {
|
||||
outline: solid 2px var(--wa-color-success-border);
|
||||
outline-offset: 1rem;
|
||||
}
|
||||
|
||||
wa-slider:state(user-invalid) {
|
||||
outline: solid 2px var(--wa-color-danger-border);
|
||||
outline-offset: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import { allDefined } from '/dist/webawesome.js';
|
||||
|
||||
const form = document.querySelector('.slider__validation-custom');
|
||||
const slider = form.querySelector('wa-slider');
|
||||
const validationMessage = 'Select a number greater than zero';
|
||||
|
||||
await allDefined();
|
||||
|
||||
slider.setCustomValidity(validationMessage);
|
||||
|
||||
async function updateValidity() {
|
||||
await slider.updateComplete;
|
||||
slider.setCustomValidity(slider.value > 0 ? '' : validationMessage);
|
||||
}
|
||||
|
||||
slider.addEventListener('input', updateValidity);
|
||||
form.addEventListener('reset', updateValidity);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
width: var(--marker-width);
|
||||
height: var(--marker-height);
|
||||
border-radius: 50%;
|
||||
background-color: tomato;
|
||||
background-color: var(--wa-color-surface-default);
|
||||
}
|
||||
|
||||
.marker:first-of-type,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import { DraggableElement } from '../../internal/drag.js';
|
||||
import { clamp } from '../../internal/math.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { SliderValidator } from '../../internal/validators/slider-validator.js';
|
||||
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
|
||||
import formControlStyles from '../../styles/component/form-control.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
@@ -68,6 +69,10 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
static observeSlots = true;
|
||||
static css = [formControlStyles, styles];
|
||||
|
||||
static get validators() {
|
||||
return [...super.validators, SliderValidator()];
|
||||
}
|
||||
|
||||
private draggableTrack: DraggableElement;
|
||||
private draggableThumbMin: DraggableElement | null = null;
|
||||
private draggableThumbMax: DraggableElement | null = null;
|
||||
@@ -77,10 +82,16 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
private valueWhenDraggingStarted: number | undefined;
|
||||
private activeThumb: 'min' | 'max' | null = null;
|
||||
private lastTrackPosition: number | null = null; // Track last position for direction detection
|
||||
|
||||
protected get focusableAnchor() {
|
||||
return this.isRange ? this.thumbMin || this.slider : this.slider;
|
||||
}
|
||||
|
||||
/** Override validation target to point to the focusable element */
|
||||
get validationTarget() {
|
||||
return this.focusableAnchor;
|
||||
}
|
||||
|
||||
@query('#slider') slider: HTMLElement;
|
||||
@query('#thumb') thumb: HTMLElement;
|
||||
@query('#thumb-min') thumbMin: HTMLElement;
|
||||
@@ -151,6 +162,9 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
/** The granularity the value must adhere to when incrementing and decrementing. */
|
||||
@property({ type: Number }) step: number = 1;
|
||||
|
||||
/** Makes the slider a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Tells the browser to focus the slider when the page loads or a dialog is shown. */
|
||||
@property({ type: Boolean }) autofocus: boolean;
|
||||
|
||||
@@ -333,8 +347,7 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
// Always be updating
|
||||
this.updateValidity();
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Handle range mode changes
|
||||
if (changedProperties.has('range')) {
|
||||
@@ -354,7 +367,7 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
// Handle value for single thumb mode
|
||||
if (changedProperties.has('value')) {
|
||||
this.value = clamp(this.value, this.min, this.max);
|
||||
this.internals.setFormValue(String(this.value));
|
||||
this.setValue(String(this.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +427,7 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
this.isInvalid = false;
|
||||
this.hadUserInteraction = false;
|
||||
this.wasSubmitted = false;
|
||||
super.formResetCallback();
|
||||
}
|
||||
|
||||
/** Clamps a number to min/max while ensuring it's a valid step interval. */
|
||||
@@ -703,33 +717,10 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
|
||||
const formData = new FormData();
|
||||
formData.append(this.name || '', String(this.minValue));
|
||||
formData.append(this.name || '', String(this.maxValue));
|
||||
this.internals.setFormValue(formData);
|
||||
this.setValue(formData);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the form control's validity */
|
||||
private async updateValidity() {
|
||||
await this.updateComplete;
|
||||
|
||||
const validationMessage = this.internals.validity.customError ? this.internals.validationMessage : '';
|
||||
const hasCustomValidity = validationMessage.length > 0;
|
||||
const flags: ValidityStateFlags = {
|
||||
badInput: false,
|
||||
customError: hasCustomValidity,
|
||||
patternMismatch: false,
|
||||
rangeOverflow: false,
|
||||
rangeUnderflow: false,
|
||||
stepMismatch: false,
|
||||
tooLong: false,
|
||||
tooShort: false,
|
||||
typeMismatch: false,
|
||||
valueMissing: false,
|
||||
};
|
||||
|
||||
this.isInvalid = hasCustomValidity;
|
||||
this.internals.setValidity(flags, validationMessage, this.focusableAnchor);
|
||||
}
|
||||
|
||||
/** Sets focus to the slider. */
|
||||
public focus() {
|
||||
if (this.isRange) {
|
||||
|
||||
123
packages/webawesome/src/internal/validators/slider-validator.ts
Normal file
123
packages/webawesome/src/internal/validators/slider-validator.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type WaSlider from '../../components/slider/slider.js';
|
||||
import type { Validator } from '../webawesome-form-associated-element.js';
|
||||
|
||||
/**
|
||||
* Comprehensive validator for sliders that handles required, range, and step validation
|
||||
*/
|
||||
export const SliderValidator = (): Validator<WaSlider> => {
|
||||
// Create a native range input to get localized validation messages
|
||||
const nativeRequiredRange = Object.assign(document.createElement('input'), {
|
||||
type: 'range',
|
||||
required: true,
|
||||
});
|
||||
|
||||
return {
|
||||
observedAttributes: ['required', 'min', 'max', 'step'],
|
||||
checkValidity(element) {
|
||||
const validity: ReturnType<Validator['checkValidity']> = {
|
||||
message: '',
|
||||
isValid: true,
|
||||
invalidKeys: [],
|
||||
};
|
||||
|
||||
// Create native range input to get localized validation messages
|
||||
const createNativeRange = (value: number, min: number, max: number, step: number) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'range';
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
input.step = String(step);
|
||||
input.value = String(value);
|
||||
|
||||
// Trigger validation
|
||||
input.checkValidity();
|
||||
return input.validationMessage;
|
||||
};
|
||||
|
||||
// Check required validation first
|
||||
if (element.required && !element.hadUserInteraction) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('valueMissing');
|
||||
validity.message = nativeRequiredRange.validationMessage || 'Please fill out this field.';
|
||||
return validity;
|
||||
}
|
||||
|
||||
// For range sliders, validate both values
|
||||
if (element.isRange) {
|
||||
const minValue = element.minValue;
|
||||
const maxValue = element.maxValue;
|
||||
|
||||
// Check range underflow for min value
|
||||
if (minValue < element.min) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeUnderflow');
|
||||
validity.message =
|
||||
createNativeRange(minValue, element.min, element.max, element.step) ||
|
||||
`Value must be greater than or equal to ${element.min}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check range overflow for max value
|
||||
if (maxValue > element.max) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeOverflow');
|
||||
validity.message =
|
||||
createNativeRange(maxValue, element.min, element.max, element.step) ||
|
||||
`Value must be less than or equal to ${element.max}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check step mismatch
|
||||
if (element.step && element.step !== 1) {
|
||||
const minStepMismatch = (minValue - element.min) % element.step !== 0;
|
||||
const maxStepMismatch = (maxValue - element.min) % element.step !== 0;
|
||||
|
||||
if (minStepMismatch || maxStepMismatch) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('stepMismatch');
|
||||
const testValue = minStepMismatch ? minValue : maxValue;
|
||||
validity.message =
|
||||
createNativeRange(testValue, element.min, element.max, element.step) ||
|
||||
`Value must be a multiple of ${element.step}.`;
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single value validation
|
||||
const value = element.value;
|
||||
|
||||
// Check range underflow
|
||||
if (value < element.min) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeUnderflow');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be greater than or equal to ${element.min}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check range overflow
|
||||
if (value > element.max) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('rangeOverflow');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be less than or equal to ${element.max}.`;
|
||||
return validity;
|
||||
}
|
||||
|
||||
// Check step mismatch
|
||||
if (element.step && element.step !== 1 && (value - element.min) % element.step !== 0) {
|
||||
validity.isValid = false;
|
||||
validity.invalidKeys.push('stepMismatch');
|
||||
validity.message =
|
||||
createNativeRange(value, element.min, element.max, element.step) ||
|
||||
`Value must be a multiple of ${element.step}.`;
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
|
||||
return validity;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user