diff --git a/.circleci/config.yml b/.circleci/config.yml index 79f0eac7c4..2a4c10800d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ executors: parameters: current_golden_images_hash: type: string - default: 824fb67e89d53a25416b315fc8dda02aa83e5482 + default: b7e5c09d7b9eb5384d503cd9a4409f757a549115 wireit_cache_name: type: string default: wireit diff --git a/packages/overlay/stories/overlay.stories.ts b/packages/overlay/stories/overlay.stories.ts index f8a1fd764c..ee68f52f43 100644 --- a/packages/overlay/stories/overlay.stories.ts +++ b/packages/overlay/stories/overlay.stories.ts @@ -173,6 +173,7 @@ const template = ({ min="0" max="20" label="Awesomeness" + default-value="10" >
The background of this div should be blue diff --git a/packages/slider/README.md b/packages/slider/README.md index 23381353e7..5eb9b2905c 100644 --- a/packages/slider/README.md +++ b/packages/slider/README.md @@ -232,6 +232,19 @@ An `` element can be paired with an `` element via t ``` +#### Default value + +Slider will reset to its `default-value` when the user double clicks on the slider handle or if the user presses the `escape` key when the slider handle is focused. + +```html + +``` + +Note: If a slider with `default-value` attribute is contained in a modal and the slider-handle is focused then the following interaction will occur on pressing the `escape` key: + +- If the slider value is different from the default value then the slider value will be reset to the default value and the modal will not be closed. +- If the slider value is equal to the default value then the modal will be closed. + #### Indeterminate The indeterminate attribute will be passed to the internal `` element and alter its visual delivery until a change has been made to the `` element at which point the `change` event that is dispatched can be understood as always removing the indeterminate attribute from the ``. diff --git a/packages/slider/src/HandleController.ts b/packages/slider/src/HandleController.ts index 279bf21042..ffc7195150 100644 --- a/packages/slider/src/HandleController.ts +++ b/packages/slider/src/HandleController.ts @@ -341,6 +341,23 @@ export class HandleController { private _activePointerEventData!: DataFromPointerEvent | undefined; + /** + * @description check for defaultvalue(value) property in sp-slider and reset on double click on sliderHandle + * @param event + */ + public handleDoubleClick(event: PointerEvent): void { + const input = (event.target as Element).querySelector( + '.input' + ) as InputWithModel; + + if (input.model?.handle.defaultValue !== undefined) { + input.model.handle.value = input.model.handle.defaultValue; + this.dispatchChangeEvent(input, input.model.handle); + input.model.handle.dispatchInputEvent(); + this.requestUpdate(); + } + } + public handlePointerdown(event: PointerEvent): void { const { resolvedInput, model } = this.extractDataFromEvent(event); if (!model || this.host.disabled || event.button !== 0) { @@ -432,7 +449,22 @@ export class HandleController { this.requestUpdate(); }; - private onInputKeydown = (event: Event): void => { + private onInputKeydown = (event: KeyboardEvent): void => { + if (event.key == 'Escape') { + const input = event.target as InputWithModel; + if ( + input.model.handle?.defaultValue !== undefined && + input.model.handle.value !== input.model.handle.defaultValue + ) { + input.model.handle.value = input.model.handle.defaultValue; + input.model.handle.dispatchInputEvent(); + this.dispatchChangeEvent(input, input.model.handle); + this.requestUpdate(); + event.preventDefault(); + event.stopPropagation(); + } + return; + } const input = event.target as InputWithModel; input.model.handle.highlight = true; this.requestUpdate(); @@ -522,12 +554,17 @@ export class HandleController { aria-label=${ifDefined(model.ariaLabel)} aria-labelledby=${ariaLabelledBy} aria-valuetext=${this.formattedValueForHandle(model)} + aria-describedby="slider-description" @change=${this.onInputChange} @focus=${this.onInputFocus} @blur=${this.onInputBlur} @keydown=${this.onInputKeydown} .model=${model} /> + + Press escape or double click to reset the slider to its + default value. +
`; } diff --git a/packages/slider/src/Slider.ts b/packages/slider/src/Slider.ts index 2c6cf8563f..817f428f99 100644 --- a/packages/slider/src/Slider.ts +++ b/packages/slider/src/Slider.ts @@ -362,7 +362,6 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), { * @description calculates the fill width * @param fillStartValue * @param currentValue - * @param cachedValue * @returns */ private getOffsetWidth( @@ -409,10 +408,12 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), { > `; } - private renderHandle(): TemplateResult { + if (this.variant === 'tick') { + return html``; + } return html` - ${this.variant === 'tick' ? html`` : this.handleController.render()} + ${this.handleController.render()} `; } @@ -442,6 +443,7 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), { ['pointerup', 'pointercancel', 'pointerleave'], this.handlePointerup, ], + streamOutside: ['dblclick', this.handleDoubleClick], })} >
@@ -475,6 +477,10 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), { `; } + protected handleDoubleClick(event: PointerEvent): void { + this.handleController.handleDoubleClick(event); + } + protected handlePointerdown(event: PointerEvent): void { this.handleController.handlePointerdown(event); } diff --git a/packages/slider/src/SliderHandle.ts b/packages/slider/src/SliderHandle.ts index d680e02c32..100d567589 100644 --- a/packages/slider/src/SliderHandle.ts +++ b/packages/slider/src/SliderHandle.ts @@ -92,6 +92,13 @@ export class SliderHandle extends Focusable { @property({ type: Number }) value!: number; + /** + * Set the default value of the handle. Setting this property will cause the + * handle to reset to the default value on double click or pressing the `escape` key. + */ + @property({ type: Number, attribute: 'default-value' }) + defaultValue!: number; + @property({ type: Boolean, reflect: true }) public dragging = false; diff --git a/packages/slider/src/slider.css b/packages/slider/src/slider.css index cc8175be54..3633e926fe 100644 --- a/packages/slider/src/slider.css +++ b/packages/slider/src/slider.css @@ -149,3 +149,7 @@ governing permissions and limitations under the License. .fill { z-index: 2; } + +#slider-description { + display: none; +} diff --git a/packages/slider/stories/slider.stories.ts b/packages/slider/stories/slider.stories.ts index 76b9b76219..1629652b12 100644 --- a/packages/slider/stories/slider.stories.ts +++ b/packages/slider/stories/slider.stories.ts @@ -152,6 +152,26 @@ export const Filled = (args: StoryArgs = {}): TemplateResult => { `; }; +export const HasADefaultValue = (args: StoryArgs = {}): TemplateResult => { + return html` +
+ + double click or press escape key to reset + +
+ `; +}; + export const FillStart = (args: StoryArgs = {}): TemplateResult => { return html`
@@ -397,6 +417,37 @@ export const editable = (args: StoryArgs = {}): TemplateResult => { editable.decorators = [editableDecorator]; +export const editableWithDefaultValue = ( + args: StoryArgs = {} +): TemplateResult => { + return html` +
+ + Angle + +
+ `; +}; + +editableWithDefaultValue.swc_vrt = { + skip: true, +}; + export const editableDisabled = (args: StoryArgs = {}): TemplateResult => { return html`
diff --git a/packages/slider/test/index.ts b/packages/slider/test/index.ts index 4a24bc80e6..68c3ca0b2e 100644 --- a/packages/slider/test/index.ts +++ b/packages/slider/test/index.ts @@ -310,6 +310,7 @@ export const testEditableSlider = (type: string): void => { pointerType: 'mouse', }) ); + await elementUpdated(el); expect(el.dragging, 'it is dragging 1').to.be.true; @@ -340,6 +341,7 @@ export const testEditableSlider = (type: string): void => { pointerType: 'mouse', }) ); + await elementUpdated(el); expect(el.dragging, 'it is dragging 2').to.be.true; diff --git a/packages/slider/test/slider.test.ts b/packages/slider/test/slider.test.ts index 68139c2248..a0258aec5c 100644 --- a/packages/slider/test/slider.test.ts +++ b/packages/slider/test/slider.test.ts @@ -12,6 +12,10 @@ governing permissions and limitations under the License. import '@spectrum-web-components/slider/sp-slider.js'; import '@spectrum-web-components/slider/sp-slider-handle.js'; +import '@spectrum-web-components/button/sp-button.js'; +import '@spectrum-web-components/overlay/sp-overlay.js'; +import '@spectrum-web-components/popover/sp-popover.js'; +import { Overlay } from '@spectrum-web-components/overlay'; import { Slider, SliderHandle } from '@spectrum-web-components/slider'; import { tick } from '../stories/slider.stories.js'; import { @@ -169,7 +173,6 @@ describe('Slider', () => { }) ); await elementUpdated(el); - expect(el.dragging, 'it is dragging 1').to.be.true; expect(pointerId, '2').to.equal(1); @@ -1604,4 +1607,161 @@ describe('Slider', () => { await elementUpdated(el); expect(el.values).to.deep.equal({ a: 10, b: 20, c: 29 }); }); + it('resets to default value on double click after moving pointer', async () => { + const inputSpy = spy(); + const changeSpy = spy(); + + const el = await fixture( + html` + { + inputSpy(event.target.value); + }} + @change=${(event: Event & { target: Slider }) => { + changeSpy(event.target.value); + }} + > + ` + ); + await elementUpdated(el); + expect(el.value, 'initial').to.equal(50); + + 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: [ + 150, + handleBoundingRect.y + handleBoundingRect.height + 100, + ], + }, + { + type: 'up', + }, + ], + }); + + await elementUpdated(el); + + // since we've moved the pointer, the new value should be 100 + expect(el.value).to.equal(100); + + inputSpy.resetHistory(); + changeSpy.resetHistory(); + + handle.dispatchEvent( + new PointerEvent('dblclick', { + clientX: 0, + cancelable: true, + button: 0, + composed: true, + bubbles: true, + }) + ); + + await elementUpdated(el); + + expect( + el.value, + 'reset to default value on double click after moving pointer' + ).to.equal(50); + + // input and change events should have been fired + expect(inputSpy.callCount).to.equal(1); + expect(changeSpy.callCount).to.equal(1); + }); + it('manages escape key interactions correctly in an overlaid context', async () => { + const inputSpy = spy(); + const changeSpy = spy(); + + const el = await fixture( + html` +
+ Overlay Trigger + + + { + inputSpy(event.target.value); + }} + @change=${( + event: Event & { target: Slider } + ) => { + changeSpy(event.target.value); + }} + > + + +
+ ` + ); + + await elementUpdated(el); + + // open the overlay + const trigger = el.querySelector('#trigger') as HTMLButtonElement; + const opened = oneEvent(el, 'sp-opened'); + trigger.click(); + await opened; + + // current slider value should be 70 + const slider = el.querySelector('sp-slider') as Slider; + expect(slider.value).to.equal(70); + + slider.focus(); + // send escape key + await sendKeys({ + press: 'Escape', + }); + + await elementUpdated(el); + + // now the slider value should be 50 + expect(slider.value).to.equal(50); + + // input and change events should have been fired + expect(inputSpy.callCount).to.equal(1); + expect(changeSpy.callCount).to.equal(1); + + // and the overlay should be in open state + const overlay = el.querySelector('sp-overlay') as Overlay; + expect(overlay.open).to.be.true; + + const closed = oneEvent(el, 'sp-closed'); + // send escape key again + await sendKeys({ + press: 'Escape', + }); + await closed; + + // now the overlay should be closed + expect(overlay.open).to.be.false; + }); });