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`