Refactor range styles to prepare for native ranges

This commit is contained in:
Lea Verou
2024-12-18 18:57:26 -05:00
parent df4393e033
commit deb2752d35
4 changed files with 183 additions and 232 deletions

View File

@@ -103,3 +103,14 @@ You can change the tooltip's content by setting the `tooltipFormatter` property
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-range dir="rtl"
label="مقدار"
hint="التحكم في مستوى صوت الأغنية الحالية."
style="--track-color-active: var(--wa-color-brand-fill-loud)" value="10"></wa-range>
```

View File

@@ -3,23 +3,21 @@
--thumb-gap: calc(var(--thumb-size) * 0.125);
--thumb-shadow: initial;
--thumb-size: calc(1rem * var(--wa-form-control-value-line-height));
--tooltip-offset: calc(var(--wa-tooltip-arrow-size) * 1.375);
--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);
display: block;
}
.range {
position: relative;
display: flex;
align-items: center;
height: max(var(--thumb-size), var(--track-height));
flex-direction: column;
position: relative;
min-height: max(var(--thumb-size), var(--track-height));
}
.control {
input[type='range'] {
--percent: 0%;
-webkit-appearance: none;
border-radius: calc(var(--track-height) / 2);
@@ -29,126 +27,113 @@
line-height: var(--wa-form-control-height-m);
vertical-align: middle;
margin: 0;
--dir: right;
background-image: linear-gradient(
to right,
var(--track-color-inactive) 0%,
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)),
var(--track-color-inactive) 100%
var(--track-color-inactive) max(var(--percent), var(--track-active-offset))
);
}
.range--rtl .control {
background-image: linear-gradient(
to left,
var(--track-color-inactive) 0%,
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)),
var(--track-color-inactive) 100%
);
}
&:dir(rtl) {
--dir: left;
}
/* Webkit */
.control::-webkit-slider-runnable-track {
width: 100%;
height: var(--track-height);
border-radius: 3px;
border: none;
}
&::-webkit-slider-runnable-track {
width: 100%;
height: var(--track-height);
border-radius: 3px;
border: none;
}
.control::-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);
cursor: pointer;
}
&::-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);
cursor: pointer;
}
.control:enabled:focus-visible::-webkit-slider-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
&:enabled {
&:focus-visible::-webkit-slider-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.control:enabled::-webkit-slider-thumb:active {
cursor: grabbing;
}
&::-webkit-slider-thumb:active {
cursor: grabbing;
}
}
/* Firefox */
.control::-moz-focus-outer {
border: 0;
}
&::-moz-focus-outer {
border: 0;
}
.control::-moz-range-progress {
background-color: var(--track-color-active);
border-radius: 3px;
height: var(--track-height);
}
&::-moz-range-progress {
background-color: var(--track-color-active);
border-radius: 3px;
height: var(--track-height);
}
.control::-moz-range-track {
width: 100%;
height: var(--track-height);
background-color: var(--track-color-inactive);
border-radius: 3px;
border: none;
}
&::-moz-range-track {
width: 100%;
height: var(--track-height);
background-color: var(--track-color-inactive);
border-radius: 3px;
border: none;
}
.control::-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 0 var(--thumb-gap) var(--wa-color-surface-default);
transition:
background-color var(--wa-transition-normal) var(--wa-transition-easing),
border-color var(--wa-transition-normal) var(--wa-transition-easing),
box-shadow var(--wa-transition-normal) var(--wa-transition-easing),
color var(--wa-transition-normal) var(--wa-transition-easing);
cursor: pointer;
}
&::-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 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);
cursor: pointer;
}
.control:enabled:focus-visible::-moz-range-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
&:enabled {
&:focus-visible::-moz-range-thumb {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.control:enabled::-moz-range-thumb:active {
cursor: grabbing;
}
&::-moz-range-thumb:active {
cursor: grabbing;
}
}
/* States */
.control:focus-visible {
outline: none;
}
/* States */
.control:disabled {
opacity: 0.5;
}
&:focus-visible {
outline: none;
}
.control:disabled::-webkit-slider-thumb {
cursor: not-allowed;
}
.control:disabled::-moz-range-thumb {
cursor: not-allowed;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* Tooltip output */
.tooltip {
position: absolute;
z-index: 1000;
left: 0;
inset-inline-start: 0;
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;
@@ -159,60 +144,43 @@
padding: var(--wa-space-2xs) var(--wa-space-xs);
transition: var(--wa-transition-normal) opacity;
pointer-events: none;
}
.tooltip:after {
content: '';
position: absolute;
width: 0;
height: 0;
left: 50%;
translate: calc(-1 * var(--wa-tooltip-arrow-size));
}
&: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);
}
.range--tooltip-visible .tooltip {
opacity: 1;
}
&.visible {
opacity: 1;
}
/* Tooltip on top */
.range--tooltip-top .tooltip {
bottom: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
}
--inset-block: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
--border-block: var(--wa-tooltip-arrow-size) solid var(--wa-color-neutral-fill-loud);
.range--tooltip-top .tooltip:after {
border-top: var(--wa-tooltip-arrow-size) solid var(--wa-color-neutral-fill-loud);
border-left: var(--wa-tooltip-arrow-size) solid transparent;
border-right: var(--wa-tooltip-arrow-size) solid transparent;
top: 100%;
@media (forced-colors: active) {
border: solid 1px transparent;
&:after {
display: none;
}
}
}
/* Tooltip on bottom */
.range--tooltip-bottom .tooltip {
top: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
}
:host([tooltip='bottom']) .tooltip {
inset-block-end: auto;
inset-block-start: calc(50% + (var(--thumb-size) / 2) + var(--tooltip-offset));
.range--tooltip-bottom .tooltip:after {
border-bottom: var(--wa-tooltip-arrow-size) solid var(--wa-color-neutral-fill-loud);
border-left: var(--wa-tooltip-arrow-size) solid transparent;
border-right: var(--wa-tooltip-arrow-size) solid transparent;
bottom: 100%;
}
@media (forced-colors: active) {
.control,
.tooltip {
border: solid 1px transparent;
}
.control::-webkit-slider-thumb {
border: solid 1px transparent;
}
.control::-moz-range-thumb {
border: solid 1px transparent;
}
.tooltip:after {
display: none;
&:after {
border-block-end: var(--border-block);
inset-block-start: auto;
inset-block-end: 100%;
}
}

View File

@@ -31,11 +31,10 @@ import styles from './range.css';
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart form-control - The form control that wraps the label, input, and hint.
* @csspart form-control-label - The label's wrapper.
* @csspart form-control-input - The range's wrapper.
* @csspart form-control-label - The input's label.
* @csspart form-control-input - The input's wrapper.
* @csspart hint - The hint's wrapper.
* @csspart base - The component's base wrapper.
* @csspart input - The internal `<input>` element.
* @csspart base - The internal `<input>` element.
* @csspart tooltip - The range's tooltip.
*
* @cssproperty --thumb-color - The color of the thumb.
@@ -280,73 +279,53 @@ export default class WaRange extends WebAwesomeFormAssociatedElement {
// NOTE - always bind value after min/max, otherwise it will be clamped
return html`
<div
part="form-control"
class=${classMap({
'form-control': true,
'form-control--medium': true, // range only has one size
'form-control--has-label': hasLabel,
})}
>
<label part="form-control-label" class="label" for="input" aria-hidden=${hasLabel ? 'false' : 'true'}>
<slot name="label">${this.label}</slot>
</label>
${hasLabel
? html`<label part="form-control-label" class="label" for="input">
<slot name="label">${this.label}</slot>
</label>`
: ''}
<div part="form-control-input" class="form-control-input">
<div
part="base"
class=${classMap({
range: true,
'range--disabled': this.disabled,
'range--focused': this.hasFocus,
'range--rtl': this.localize.dir() === 'rtl',
'range--tooltip-visible': this.hasTooltip,
'range--tooltip-top': this.tooltip === 'top',
'range--tooltip-bottom': this.tooltip === 'bottom',
})}
@mousedown=${this.handleThumbDragStart}
@mouseup=${this.handleThumbDragEnd}
@touchstart=${this.handleThumbDragStart}
@touchend=${this.handleThumbDragEnd}
>
<input
part="input"
id="input"
class="control"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
type="range"
name=${ifDefined(this.name)}
?disabled=${this.disabled}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step)}
.value=${live(this.value.toString())}
aria-describedby="hint"
@change=${this.handleChange}
@focus=${this.handleFocus}
@input=${this.handleInput}
@blur=${this.handleBlur}
/>
${this.tooltip !== 'none' && !this.disabled
? html`
<output part="tooltip" class="tooltip">
${typeof this.tooltipFormatter === 'function' ? this.tooltipFormatter(this.value) : this.value}
</output>
`
: ''}
</div>
</div>
<slot
name="hint"
part="hint"
class=${classMap({
'has-slotted': hasHint,
})}
aria-hidden=${hasHint ? 'false' : 'true'}
>${this.hint}</slot
>
<div part="form-control-input">
<input
part="base"
id="input"
class="control"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
type="range"
name=${ifDefined(this.name)}
?disabled=${this.disabled}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step)}
.value=${live(this.value.toString())}
aria-describedby="hint"
@change=${this.handleChange}
@focus=${this.handleFocus}
@input=${this.handleInput}
@blur=${this.handleBlur}
@mousedown=${this.handleThumbDragStart}
@mouseup=${this.handleThumbDragEnd}
@touchstart=${this.handleThumbDragStart}
@touchend=${this.handleThumbDragEnd}
/>
${this.tooltip !== 'none' && !this.disabled
? html`
<output part="tooltip" class="${classMap({ tooltip: true, visible: this.hasTooltip })}">
${typeof this.tooltipFormatter === 'function' ? this.tooltipFormatter(this.value) : this.value}
</output>
`
: ''}
</div>
<slot
name="hint"
part="hint"
class=${classMap({
'has-slotted': hasHint,
})}
aria-hidden=${hasHint ? 'false' : 'true'}
>${this.hint}</slot
>
`;
}
}

View File

@@ -1,32 +1,25 @@
.form-control .label {
display: none;
}
/* Label */
.form-control--has-label .label {
[part~='form-control-label'] {
display: block;
color: var(--wa-form-control-label-color);
font-weight: var(--wa-form-control-label-font-weight);
line-height: var(--wa-form-control-label-line-height);
margin-block-end: var(--wa-space-xs);
font-size: var(--wa-font-size-m);
&:is(.form-control--small *) {
font-size: var(--wa-font-size-s);
}
&:is(.form-control--medium *) {
font-size: var(--wa-font-size-m);
}
&:is(.form-control--large *) {
font-size: var(--wa-font-size-l);
}
}
:host([required]) .form-control--has-label .label::after {
content: var(--wa-form-control-required-content);
margin-inline-start: var(--wa-form-control-required-content-offset);
color: var(--wa-form-control-required-content-color);
:host([required]) &::after {
content: var(--wa-form-control-required-content);
margin-inline-start: var(--wa-form-control-required-content-offset);
color: var(--wa-form-control-required-content-color);
}
}
/* Help text */