diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5200e93142..125054fa0c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -10,7 +10,7 @@ executors:
parameters:
current_golden_images_hash:
type: string
- default: e8720ac5d0ee0d076e61309294041334afe2c160
+ default: c9b480126af6dccfec997ef6f684fd019db6b7b0
wireit_cache_name:
type: string
default: wireit
diff --git a/packages/slider/README.md b/packages/slider/README.md
index 7d41b44687..23381353e7 100644
--- a/packages/slider/README.md
+++ b/packages/slider/README.md
@@ -83,10 +83,75 @@ import { Slider } from '@spectrum-web-components/slider';
### Filled
```html
-
+
+```
+
+### Filled Offset with only fill-start
+
+```html
+
+
+```
+
+### Filled Offset with fill-start value
+
+```html
+
+
+
```
diff --git a/packages/slider/src/Slider.ts b/packages/slider/src/Slider.ts
index bef7ab7b50..1e5de918d3 100644
--- a/packages/slider/src/Slider.ts
+++ b/packages/slider/src/Slider.ts
@@ -14,6 +14,7 @@ import {
CSSResultArray,
html,
nothing,
+ PropertyValues,
SizedMixin,
TemplateResult,
} from '@spectrum-web-components/base';
@@ -91,6 +92,9 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
@property()
public type = '';
+ @property({ reflect: true })
+ public override dir!: 'ltr' | 'rtl';
+
@property({ type: String })
public set variant(variant: string) {
const oldVariant = this.variant;
@@ -160,6 +164,9 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
@property({ type: Boolean, reflect: true })
public override disabled = false;
+ @property({ type: Number, reflect: true, attribute: 'fill-start' })
+ public fillStart?: number | boolean;
+
/**
* Applies `quiet` to the underlying `sp-number-field` when `editable === true`.
*/
@@ -348,6 +355,61 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
`;
}
+ private _cachedValue: number | undefined;
+ private centerPoint: number | undefined;
+
+ /**
+ * @description calculates the fill width
+ * @param fillStartValue
+ * @param currentValue
+ * @param cachedValue
+ * @returns
+ */
+ private getOffsetWidth(
+ fillStartValue: number,
+ currentValue: number
+ ): number {
+ const distance = Math.abs(currentValue - fillStartValue);
+ return (distance / (this.max - this.min)) * 100;
+ }
+
+ /**
+ * @description calculates the fill width starting point to fill width
+ * @param value
+ */
+ private getOffsetPosition(value: number): number {
+ return ((value - this.min) / (this.max - this.min)) * 100;
+ }
+
+ private fillStyles(centerPoint: number): StyleInfo {
+ const position = this.dir === 'rtl' ? 'right' : 'left';
+ const offsetPosition =
+ this.value > centerPoint
+ ? this.getOffsetPosition(centerPoint)
+ : this.getOffsetPosition(this.value);
+ const offsetWidth = this.getOffsetWidth(centerPoint, this.value);
+ const styles: StyleInfo = {
+ [position]: `${offsetPosition}%`,
+ width: `${offsetWidth}%`,
+ };
+ return styles;
+ }
+
+ private renderFillOffset(): TemplateResult {
+ if (!this._cachedValue || !this.centerPoint) {
+ return html``;
+ }
+ return html`
+
this.centerPoint,
+ })}
+ style=${styleMap(this.fillStyles(this.centerPoint))}
+ >
+ `;
+ }
+
private renderTrack(): TemplateResult {
const segments = this.handleController.trackSegments();
const handleItems = [
@@ -360,6 +422,7 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
id: `track${index + 1}`,
html: this.renderTrackSegment(start, end),
})),
+ { id: 'fill', html: this.renderFillOffset() },
];
return html`
@@ -438,8 +501,12 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
const size = end - start;
const styles: StyleInfo = {
width: `${size * 100}%`,
- '--spectrum-slider-track-background-size': `${(1 / size) * 100}%`,
- '--spectrum-slider-track-segment-position': `${start * 100}%`,
+ ...(this.handleController.size > 1 && {
+ '--spectrum-slider-track-background-size': `${
+ (1 / size) * 100
+ }%`,
+ '--spectrum-slider-track-segment-position': `${start * 100}%`,
+ }),
};
return styles;
}
@@ -455,4 +522,17 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
await this.handleController.handleUpdatesComplete();
return complete;
}
+
+ protected override willUpdate(changed: PropertyValues): void {
+ if (changed.has('value') && changed.has('fillStart')) {
+ this._cachedValue = Number(this.value);
+ if (this.fillStart) {
+ this.centerPoint = Number(this.fillStart);
+ } else {
+ this.centerPoint =
+ (Number(this.max) - Number(this.min)) / 2 +
+ Number(this.min);
+ }
+ }
+ }
}
diff --git a/packages/slider/src/spectrum-config.js b/packages/slider/src/spectrum-config.js
index 2f2bba16fd..d7968b7c47 100644
--- a/packages/slider/src/spectrum-config.js
+++ b/packages/slider/src/spectrum-config.js
@@ -99,7 +99,6 @@ const config = {
},
converter.classToId('spectrum-Slider-buffer', 'buffer'),
converter.classToId('spectrum-Slider-controls', 'controls'),
- converter.classToId('spectrum-Slider-fill', 'fill'),
converter.classToId('spectrum-Slider-label', 'label'),
converter.classToId(
'spectrum-Slider-labelContainer',
@@ -110,6 +109,8 @@ const config = {
converter.classToClass('spectrum-Slider-handle', 'handle'),
converter.classToClass('spectrum-Slider-input', 'input'),
converter.classToClass('spectrum-Slider-tick', 'tick'),
+ converter.classToClass('spectrum-Slider-fill--right', 'offset'),
+ converter.classToClass('spectrum-Slider-fill', 'fill'),
converter.classToClass(
'spectrum-Slider-tickLabel',
'tickLabel'
diff --git a/packages/slider/src/spectrum-slider.css b/packages/slider/src/spectrum-slider.css
index 1f11c0641c..bf3fafe97b 100644
--- a/packages/slider/src/spectrum-slider.css
+++ b/packages/slider/src/spectrum-slider.css
@@ -207,7 +207,7 @@ governing permissions and limitations under the License.
var(--spectrum-slider-control-height)
);
}
-#fill,
+.fill,
.track {
block-size: var(
--mod-slider-track-fill-thickness,
@@ -237,7 +237,7 @@ governing permissions and limitations under the License.
position: absolute;
z-index: 1;
}
-#fill:before,
+.fill:before,
.track:before {
block-size: 100%;
border-end-end-radius: 0;
@@ -303,7 +303,7 @@ governing permissions and limitations under the License.
var(--spectrum-slider-track-middle-handleoffset)
);
}
-#fill {
+.fill {
margin-inline-start: 0;
padding-block: 0;
padding-inline-end: 0;
@@ -315,7 +315,7 @@ governing permissions and limitations under the License.
var(--spectrum-slider-handle-gap, var(--spectrum-slider-handle-gap))
);
}
-.spectrum-Slider-fill--right {
+.offset {
padding-block: 0;
padding-inline-end: calc(
var(
@@ -737,7 +737,7 @@ governing permissions and limitations under the License.
)
);
}
-#fill:before {
+.fill:before {
background: var(
--highcontrast-slider-track-fill-color,
var(
@@ -929,7 +929,7 @@ governing permissions and limitations under the License.
)
);
}
-:host([disabled]) #fill:before {
+:host([disabled]) .fill:before {
background: var(
--highcontrast-slider-track-fill-color-disabled,
var(
diff --git a/packages/slider/stories/slider.stories.ts b/packages/slider/stories/slider.stories.ts
index ee594df89a..1d04cfe788 100644
--- a/packages/slider/stories/slider.stories.ts
+++ b/packages/slider/stories/slider.stories.ts
@@ -132,6 +132,81 @@ export const Default = (args: StoryArgs = {}): TemplateResult => {
`;
};
+export const Filled = (args: StoryArgs = {}): TemplateResult => {
+ return html`
+
+
+ Slider Label
+
+
+ `;
+};
+
+export const FillStart = (args: StoryArgs = {}): TemplateResult => {
+ return html`
+
+
+ Slider label
+
+
+ `;
+};
+
+export const FillStartWithValue = (args: StoryArgs = {}): TemplateResult => {
+ return html`
+
+
+ Value Greater than Fill Start
+
+
+
+
+ Value Less than Fill Start
+
+
+ `;
+};
+
export const autofocus = (args: StoryArgs = {}): TemplateResult => {
return html`
diff --git a/packages/slider/test/slider.test.ts b/packages/slider/test/slider.test.ts
index ed61064976..68139c2248 100644
--- a/packages/slider/test/slider.test.ts
+++ b/packages/slider/test/slider.test.ts
@@ -844,6 +844,89 @@ describe('Slider', () => {
expect(el.variant).to.equal('tick');
expect(el.getAttribute('variant')).to.equal('tick');
});
+ it('renders fill from the centerPoint of the track when fill-start has no value', async () => {
+ const el = await fixture(
+ html`
+
+ `
+ );
+
+ await elementUpdated(el);
+ await nextFrame();
+ await nextFrame();
+ const fillElement = el.shadowRoot.querySelector(
+ '.fill'
+ ) as HTMLDivElement;
+
+ expect(fillElement).to.exist;
+ expect(fillElement.style.left).to.equal('50%');
+ expect(fillElement.style.width).to.equal('0%');
+ expect(el.values).to.deep.equal({ value: 10 });
+ });
+ it('renders fill from fill-start point', async () => {
+ const el = await fixture(
+ html`
+
+ `
+ );
+
+ await elementUpdated(el);
+ await nextFrame();
+ await nextFrame();
+ const fillElement = el.shadowRoot.querySelector(
+ '.fill'
+ ) as HTMLDivElement;
+
+ expect(fillElement).to.exist;
+ expect(fillElement.style.left).to.equal('10%');
+ expect(fillElement.style.width).to.equal('5%');
+ expect(el.values).to.deep.equal({ value: 10 });
+
+ const handle = el.shadowRoot.querySelector('.handle') as HTMLDivElement;
+ const handleBoundingRect = handle.getBoundingClientRect();
+ const position: [number, number] = [
+ handleBoundingRect.x + handleBoundingRect.width / 2,
+ handleBoundingRect.y + handleBoundingRect.height / 2,
+ ];
+ await sendMouse({
+ steps: [
+ {
+ type: 'move',
+ position,
+ },
+ {
+ type: 'down',
+ },
+ ],
+ });
+
+ await elementUpdated(el);
+ await sendMouse({
+ steps: [
+ {
+ type: 'move',
+ position: [
+ 200,
+ handleBoundingRect.y + handleBoundingRect.height + 100,
+ ],
+ },
+ ],
+ });
+ await nextFrame();
+
+ expect(el.value).to.equal(24);
+ });
it('has a `focusElement`', async () => {
const el = await fixture(
html`