mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
9 Commits
native-cod
...
konnorroge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e30a85482b | ||
|
|
5b851361a8 | ||
|
|
5794aa1e3b | ||
|
|
9c19ea7d88 | ||
|
|
9a75a181f9 | ||
|
|
8158168bdb | ||
|
|
1e62e67813 | ||
|
|
37fc1359de | ||
|
|
83bd02b613 |
@@ -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>
|
||||
```
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
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.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;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user