Compare commits

...

9 Commits

Author SHA1 Message Date
konnorrogers
e30a85482b slider test fixes 2025-06-05 16:22:35 -04:00
Cory LaViska
5b851361a8 Merge branch 'next' into slider-rework 2025-06-05 15:17:49 -04:00
Cory LaViska
5794aa1e3b whitespace 2025-06-05 14:00:09 -04:00
Cory LaViska
9c19ea7d88 fix hint aria 2025-06-05 13:59:51 -04:00
Cory LaViska
9a75a181f9 update changelog 2025-06-05 13:55:13 -04:00
Cory LaViska
8158168bdb update changelog 2025-06-05 13:47:38 -04:00
Cory LaViska
1e62e67813 add size styles 2025-06-05 13:44:16 -04:00
Cory LaViska
37fc1359de improvements 2025-06-05 13:28:35 -04:00
Cory LaViska
83bd02b613 initial rework 2025-06-05 12:40:35 -04:00
8 changed files with 1589 additions and 481 deletions

View File

@@ -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,233 @@ 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;
```
### Vertical Sliders
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
tooltip-placement="right"
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>
```
### Size
Control the slider's size using the `size` attribute. Valid options include `small`, `medium`, and `large`.
```html {.example}
<wa-slider size="small" value="50" label="Small"></wa-slider><br />
<wa-slider size="medium" value="50" label="Medium"></wa-slider><br />
<wa-slider size="large" value="50" label="Large"></wa-slider>
```
### Indicator 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,74 +273,17 @@ 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>
```
### Disable the Tooltip
To disable the tooltip, set `tooltip` to `none`.
```html {.example}
<wa-slider tooltip="none"></wa-slider>
```
### Custom Track Colors
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
```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>
```
### 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}%`;
</script>
```
### Right-to-Left languages
The component adapts to right-to-left (RTL) languages as you would expect.
```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" 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>
```

View File

@@ -38,6 +38,16 @@ During the alpha period, things might break! We take breaking changes very serio
- Added convenience tokens for `--wa-font-size-smaller` and `--wa-font-size-larger`
- Updated components to use relative `em` values for internal padding and margin wherever appropriate
- 🚨 BREAKING: removed the `hint` property and slot from `<wa-radio>`; please apply hints directly to `<wa-radio-group>` instead
- 🚨 BREAKING: redesigned `<wa-slider>` with extensive new functionality
- Added support for range sliders with dual thumbs using the `range` attribute
- Added vertical orientation support with `orientation="vertical"`
- Added visual markers at each step with `with-markers`
- Added contextual reference labels with `with-references` and the `reference` slot
- Added tooltips showing current values with `with-tooltip`
- Added customizable indicator offset with `indicator-offset` attribute
- Added value formatting support with the `valueFormatter` property
- Improved the styling API to be consistent and more powerful (no more browser-specific selectors and pseudo elements to style)
- Updated to use consistent `with-*` attribute naming pattern
- 🚨 BREAKING: removed `<wa-icon-button>`; use `<wa-button><wa-icon name="..." label="..."></wa-icon></wa-button>` instead
- Added a new free component: `<wa-popover>` (#2 of 14 per stretch goals)
- Added a `min-block-size` to `<wa-divider orientation="vertical">` to ensure the divider is visible regardless of container height [issue:675]
@@ -60,8 +70,8 @@ During the alpha period, things might break! We take breaking changes very serio
## 3.0.0-alpha.13
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparison>` and improved compatibility for non-image content
- 🚨 BREAKING: Added slot detection to `<wa-dialog>` and `<wa-drawer>` so you don't need to specify `with-header` and `with-footer`; headers are on by default now, but you can use the `without-header` attribute to turn them off
- 🚨 BREAKING: Renamed the `image` slot to `media` for a more appropriate naming convention
- 🚨 BREAKING: Added slot detection to `<wa-dialog>` and `<wa-drawer>` so you don't need to specify `with-header` and `with-footer`; headers are on by default now, but you can use the `without-header` attribute to turn them off
- 🚨 BREAKING: Renamed the `image` slot to `media` for a more appropriate naming convention
- Added [a theme builder](/docs/themes/edit/) to create your own themes
- Added a new Blog & News pattern category
- Added a new free component: `<wa-scroller>` (#1 of 14 per stretch goals)
@@ -123,7 +133,7 @@ During the alpha period, things might break! We take breaking changes very serio
### Design Tokens
- Added `--wa-color-[hue]` tokens with the "core" color of each scale, regardless of which tint it lives on.
You can find them in the first column of each color palette.
You can find them in the first column of each color palette.
### Themes
@@ -148,20 +158,21 @@ You can find them in the first column of each color palette.
- Fixed an incorrect CSS value in the expand icon
- Fixed a bug that prevented the description from being read by screen readers
#### `<wa-option>`
#### `<wa-option>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
- Dropped `base` part for easier styling. CSS can now be applied directly to the element itself.
#### `<wa-menu-item>`
#### `<wa-menu-item>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
#### `<wa-card>`
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
@@ -347,12 +358,12 @@ Here's a list of some of the things that have changed since Shoelace v2. For que
- Removed `inline` from `<wa-color-picker>`
- Removed `getFormControls()` since we now use Form Associated Custom Elements and can reliably access Web Awesome Elements via `formElement.elements`.
- Removed `valueAsDate` from `<wa-input>`; use the following to mimic native behaviors:
setter: `waInput.value = new Date().toLocaleDateString()`
getter: `new Date(waInput.value)`
setter: `waInput.value = new Date().toLocaleDateString()`
getter: `new Date(waInput.value)`
- Removed `valueAsNumber` from `<wa-input>`; use the following to mimic native behaviors:
setter: `waInput.value = 5.toString()`
getter: `Number(waInput.value)`
setter: `waInput.value = 5.toString()`
getter: `Number(waInput.value)`
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)
Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)
Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)

View File

@@ -1,214 +1,226 @@
:host {
--thumb-color: var(--wa-form-control-activated-color);
--thumb-gap: calc(var(--thumb-size) * 0.125);
--thumb-shadow: initial;
--thumb-size: calc(1em * var(--wa-form-control-value-line-height));
--track-color-active: var(--wa-color-neutral-fill-normal);
--track-color-inactive: var(--wa-color-neutral-fill-normal);
--track-active-offset: 0%;
--track-height: calc(var(--thumb-size) * 0.25);
--tooltip-offset: calc(var(--wa-tooltip-arrow-size) * 1.375);
display: flex;
flex-direction: column;
position: relative;
min-height: max(var(--thumb-size), var(--track-height));
--track-size: 0.5em;
--thumb-width: 1.4em;
--thumb-height: 1.4em;
--marker-width: 0.1875em;
--marker-height: 0.1875em;
}
input[type='range'] {
--percent: 0%;
-webkit-appearance: none;
border-radius: calc(var(--track-height) / 2);
width: 100%;
height: var(--track-height);
font-size: inherit;
line-height: var(--wa-form-control-height);
vertical-align: middle;
margin: 0;
--dir: right;
:host([orientation='vertical']) {
width: auto;
}
background-image: linear-gradient(
to var(--dir),
var(--track-color-inactive) min(var(--percent), var(--track-active-offset)),
var(--track-color-active) min(var(--percent), var(--track-active-offset)),
var(--track-color-active) max(var(--percent), var(--track-active-offset)),
var(--track-color-inactive) max(var(--percent), var(--track-active-offset))
);
#label:has(~ .vertical) {
display: block;
order: 2;
max-width: none;
text-align: center;
}
#description:has(~ .vertical) {
order: 3;
text-align: center;
}
#slider {
/* Orientation */
&.horizontal {
margin-block-start: 0.5em;
}
&.vertical {
margin-block-end: 0.5em;
}
&:focus {
outline: none;
}
&:focus-visible:not(.disabled) #thumb,
&:focus-visible:not(.disabled) #thumb-min,
&:focus-visible:not(.disabled) #thumb-max {
outline: var(--wa-focus-ring);
/* intentionally no offset due to border */
}
}
#track {
position: relative;
border-radius: 9999px;
background: var(--wa-color-neutral-fill-normal);
isolation: isolate;
}
/* Orientation */
.horizontal #track {
height: var(--track-size);
}
.vertical #track {
order: 1;
width: var(--track-size);
height: 200px;
}
/* Disabled */
.disabled #track {
cursor: not-allowed;
opacity: 0.5;
}
/* Indicator */
#indicator {
position: absolute;
border-radius: inherit;
background-color: var(--wa-form-control-activated-color);
&:dir(ltr) {
right: calc(100% - max(var(--start), var(--end)));
left: min(var(--start), var(--end));
}
&:dir(rtl) {
--dir: left;
}
&::-webkit-slider-runnable-track {
width: 100%;
height: var(--track-height);
border-radius: 3px;
border: none;
}
&::-webkit-slider-thumb {
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;
background-color: var(--thumb-color);
box-shadow:
var(--thumb-shadow, 0 0 transparent),
0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
-webkit-appearance: none;
margin-top: calc(var(--thumb-size) / -2 + var(--track-height) / 2);
transition: var(--wa-transition-fast);
transition-property: width, height;
}
&:enabled {
&:focus-visible::-webkit-slider-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
&::-webkit-slider-thumb {
cursor: pointer;
}
&::-webkit-slider-thumb:active {
cursor: grabbing;
}
}
&::-moz-focus-outer {
border: 0;
}
&::-moz-range-progress {
background-color: var(--track-color-active);
border-radius: 3px;
height: var(--track-height);
}
&::-moz-range-track {
width: 100%;
height: var(--track-height);
background-color: var(--track-color-inactive);
border-radius: 3px;
border: none;
}
&::-moz-range-thumb {
height: var(--thumb-size);
width: var(--thumb-size);
border-radius: 50%;
background-color: var(--thumb-color);
box-shadow:
var(--thumb-shadow, 0 0 transparent),
0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
transition-property: background-color, border-color, box-shadow, color;
transition-duration: var(--wa-transition-normal);
transition-timing-function: var(--wa-transition-easing);
}
&:enabled {
&:focus-visible::-moz-range-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
&::-moz-range-thumb {
cursor: pointer;
}
&::-moz-range-thumb:active {
cursor: grabbing;
}
right: min(var(--start), var(--end));
left: calc(100% - max(var(--start), var(--end)));
}
}
/* States */
/* nesting these styles yields broken results in Safari */
input[type='range']:focus {
outline: none;
.horizontal #indicator {
top: 0;
height: 100%;
}
:host :has(:disabled) input[type='range'] {
opacity: 0.5;
cursor: not-allowed;
&::-moz-range-thumb,
&::-webkit-slider-thumb {
cursor: not-allowed;
}
.vertical #indicator {
top: calc(100% - var(--end));
bottom: var(--start);
left: 0;
width: 100%;
}
/* Tooltip output */
.tooltip {
/* Thumbs */
#thumb,
#thumb-min,
#thumb-max {
z-index: 3;
position: absolute;
z-index: 1000;
inset-inline-start: 0;
width: var(--thumb-width);
height: var(--thumb-height);
border: solid 0.125em var(--wa-color-surface-default);
border-radius: 50%;
background-color: var(--wa-form-control-activated-color);
cursor: pointer;
}
inset-block-end: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
border-radius: var(--wa-tooltip-border-radius);
background-color: var(--wa-tooltip-background-color);
font-family: inherit;
font-size: var(--wa-tooltip-font-size);
line-height: var(--wa-tooltip-line-height);
color: var(--wa-tooltip-content-color);
opacity: 0;
padding: 0.25em 0.5em;
transition: var(--wa-transition-normal) opacity;
.disabled #thumb,
.disabled #thumb-min,
.disabled #thumb-max {
cursor: inherit;
}
.horizontal #thumb,
.horizontal #thumb-min,
.horizontal #thumb-max {
top: calc(50% - var(--thumb-height) / 2);
&:dir(ltr) {
right: auto;
left: calc(var(--position) - var(--thumb-width) / 2);
}
&:dir(rtl) {
right: calc(var(--position) - var(--thumb-width) / 2);
left: auto;
}
}
.vertical #thumb,
.vertical #thumb-min,
.vertical #thumb-max {
bottom: calc(var(--position) - var(--thumb-height) / 2);
left: calc(50% - var(--thumb-width) / 2);
}
/* Range-specific thumb styles */
:host([range]) {
#thumb-min:focus-visible,
#thumb-max:focus-visible {
z-index: 4; /* Ensure focused thumb appears on top */
outline: var(--wa-focus-ring);
/* intentionally no offset due to border */
}
}
/* Markers */
#markers {
pointer-events: none;
}
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
inset-inline-start: 50%;
inset-block-start: 100%;
translate: calc(-1 * var(--wa-tooltip-arrow-size));
border-inline: var(--wa-tooltip-arrow-size) solid transparent;
border-block-start: var(--border-block);
.marker {
z-index: 2;
position: absolute;
width: var(--marker-width);
height: var(--marker-height);
border-radius: 50%;
background-color: var(--wa-color-surface-default);
}
.marker:first-of-type,
.marker:last-of-type {
display: none;
}
.horizontal .marker {
top: calc(50% - var(--marker-height) / 2);
left: calc(var(--position) - var(--marker-width) / 2);
}
.vertical .marker {
top: calc(var(--position) - var(--marker-height) / 2);
left: calc(50% - var(--marker-width) / 2);
}
/* Marker labels */
#references {
position: relative;
slot {
display: flex;
justify-content: space-between;
height: 100%;
}
&:dir(rtl)::after {
translate: var(--wa-tooltip-arrow-size);
::slotted(*) {
color: var(--wa-color-text-quiet);
font-size: 0.875em;
line-height: 1;
}
}
.horizontal {
#references {
margin-block-start: 0.5em;
}
}
.vertical {
display: flex;
margin-inline: auto;
#track {
order: 1;
}
&.visible {
opacity: 1;
}
#references {
order: 2;
width: min-content;
margin-inline-start: 0.75em;
--inset-block: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
--border-block: var(--wa-tooltip-arrow-size) solid var(--wa-tooltip-background-color);
@media (forced-colors: active) {
border: solid 1px transparent;
&::after {
display: none;
slot {
flex-direction: column;
}
}
}
/* RTL tooltip positioning */
:host(:dir(rtl)) .tooltip {
inset-inline-start: auto;
inset-inline-end: 0;
}
/* Tooltip on bottom */
:host([tooltip='bottom']) .tooltip {
inset-block-end: auto;
inset-block-start: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
&::after {
border-block-end: var(--border-block);
inset-block-start: auto;
inset-block-end: 100%;
}
}
/* Bottom tooltip RTL fix */
:host([tooltip='bottom']:dir(rtl)) .tooltip {
inset-inline-start: auto;
inset-inline-end: 0;
.vertical #references slot {
flex-direction: column;
}

View File

@@ -21,9 +21,8 @@ describe('<wa-slider>', () => {
it('default properties', async () => {
const el = await fixture<WaSlider>(html` <wa-slider></wa-slider> `);
expect(el.name).to.equal('');
expect(el.name).to.equal(null);
expect(el.value).to.equal(0);
expect(el.title).to.equal('');
expect(el.label).to.equal('');
expect(el.hint).to.equal('');
expect(el.disabled).to.be.false;
@@ -31,22 +30,16 @@ describe('<wa-slider>', () => {
expect(el.min).to.equal(0);
expect(el.max).to.equal(100);
expect(el.step).to.equal(1);
expect(el.tooltip).to.equal('top');
expect(el.tooltipPlacement).to.equal('top');
expect(el.defaultValue).to.equal(0);
});
it('should have title if title attribute is set', async () => {
const el = await fixture<WaSlider>(html` <wa-slider title="Test"></wa-slider> `);
const input = el.shadowRoot!.querySelector('input')!;
expect(input.title).to.equal('Test');
});
it('should be disabled with the disabled attribute', async () => {
const el = await fixture<WaSlider>(html` <wa-slider disabled></wa-slider> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('.control')!;
const input = el.shadowRoot!.querySelector<HTMLElement>("[role='slider']")!;
expect(input.disabled).to.be.true;
expect(el.matches(":disabled")).to.be.true
expect(input.getAttribute("aria-disabled")).to.equal("true");
});
describe('when the value changes', () => {

File diff suppressed because it is too large Load Diff

View File

@@ -43,3 +43,132 @@ export function drag(container: HTMLElement, options?: Partial<DragOptions>) {
move(options.initialEvent);
}
}
const supportsTouch = typeof window !== 'undefined' && 'ontouchstart' in window;
/**
* Attaches the necessary events to make an element draggable.
*
* This by itself will not make the element draggable, but it provides the events and callbacks necessary to facilitate
* dragging. Use the `clientX` and `clientY` arguments of each callback to update the UI as desired when dragging.
*
* Drag functionality will be enabled as soon as the constructor is called. A `start()` and `stop()` method can be used
* to start and stop it, if needed.
*
* @usage
*
* const draggable = new DraggableElement(element, {
* start: (clientX, clientY) => { ... },
* move: (clientX, clientY) => { ... },
* stop: (clientX, clientY) => { ... }
* });
*/
export class DraggableElement {
private element: Element;
private isActive = false;
private isDragging = false;
private options: DraggableElementOptions;
constructor(el: Element, options: Partial<DraggableElementOptions>) {
this.element = el;
this.options = {
start: () => undefined,
stop: () => undefined,
move: () => undefined,
...options,
};
this.start();
}
private handleDragStart = (event: PointerEvent | TouchEvent) => {
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
// Prevent scrolling while dragging
event.preventDefault();
if (
this.isDragging ||
// Prevent right-clicks from triggering drags
(!supportsTouch && (event as PointerEvent).buttons > 1)
) {
return;
}
this.isDragging = true;
document.addEventListener('pointerup', this.handleDragStop);
document.addEventListener('pointermove', this.handleDragMove);
document.addEventListener('touchend', this.handleDragStop);
document.addEventListener('touchmove', this.handleDragMove);
this.options.start(clientX, clientY);
};
private handleDragStop = (event: PointerEvent | TouchEvent) => {
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
this.isDragging = false;
document.removeEventListener('pointerup', this.handleDragStop);
document.removeEventListener('pointermove', this.handleDragMove);
document.removeEventListener('touchend', this.handleDragStop);
document.removeEventListener('touchmove', this.handleDragMove);
this.options.stop(clientX, clientY);
};
private handleDragMove = (event: PointerEvent | TouchEvent) => {
const clientX = supportsTouch && 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
const clientY = supportsTouch && 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
// Prevent text selection while dragging
window.getSelection()?.removeAllRanges();
this.options.move(clientX, clientY);
};
/** Start listening to drags. */
public start() {
if (!this.isActive) {
this.element.addEventListener('pointerdown', this.handleDragStart);
if (supportsTouch) {
this.element.addEventListener('touchstart', this.handleDragStart);
}
this.isActive = true;
}
}
/** Stop listening to drags. */
public stop() {
document.removeEventListener('pointerup', this.handleDragStop);
document.removeEventListener('pointermove', this.handleDragMove);
document.removeEventListener('touchend', this.handleDragStop);
document.removeEventListener('touchmove', this.handleDragMove);
this.element.removeEventListener('pointerdown', this.handleDragStart);
if (supportsTouch) {
this.element.removeEventListener('touchstart', this.handleDragStart);
}
this.isActive = false;
this.isDragging = false;
}
/** Starts or stops the drag listeners. */
public toggle(isActive?: boolean) {
const isGoingToBeActive = isActive !== undefined ? isActive : !this.isActive;
if (isGoingToBeActive) {
this.start();
} else {
this.stop();
}
}
}
export interface DraggableElementOptions {
/** Runs when dragging starts. */
start: (clientX: number, clientY: number) => void;
/** Runs as the user is dragging. This may execute often, so avoid expensive operations. */
move: (clientX: number, clientY: number) => void;
/** Runs when dragging ends. */
stop: (clientX: number, clientY: number) => void;
}

View File

@@ -162,7 +162,8 @@ function runAllValidityTests(
const form = await fixture(html`<form id="${formId}"></form>`);
const control = await createControl();
expect(control.getForm()).to.equal(null);
control.form = 'test-form';
// control.setAttribute("form", 'test-form');
control.form = "test-form"
await control.updateComplete;
expect(control.getForm()).to.equal(form);
});

View 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.hasInteracted) {
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;
},
};
};