Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(slider): double click on slider handle to reset slider position #3991

Merged
merged 29 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2827c3f
chore(slider): added logic to double click reset value
Feb 1, 2024
15ade1f
chore(slider): correct type of timer
Feb 1, 2024
01eb233
chore(slider): updated tests
Feb 2, 2024
6205062
chore(slider): added tests
Feb 2, 2024
15d0170
chore(slider): updated firstupdated lifecycle
Feb 2, 2024
745e5c0
chore(slider): updated slider handle position
Feb 14, 2024
47ca064
feat(slider): reset default value on dblclick
TarunAdobe Feb 21, 2024
3ce3482
chore: updated golden image hash
TarunAdobe Feb 21, 2024
26956b6
chore(slider): removed repeated function in slider
TarunAdobe Feb 21, 2024
4823191
chore(slider): updated tests for dbclick event
TarunAdobe Feb 21, 2024
194bba5
chore(slider): removed unnecessary delay from tests
TarunAdobe Feb 21, 2024
d136580
feat(slider): reset default value on pressing enter
TarunAdobe Feb 22, 2024
8853675
chore(slider): fixed event type
TarunAdobe Feb 22, 2024
08d021e
feat(slider): added default-value attribute and updated docs
TarunAdobe Feb 26, 2024
a9e2eac
chore: updated golden image hash
TarunAdobe Feb 26, 2024
792484f
chore(slider): updated tests
TarunAdobe Feb 26, 2024
ad9dbf0
Merge branch 'main' into feature/slider-default
TarunAdobe Feb 28, 2024
4f8cb68
chore: updated golden image hash
TarunAdobe Feb 28, 2024
7f52af2
chore(slider): added more tests and updated stories
TarunAdobe Feb 29, 2024
949cc01
feat(slider): reset to default in editable sliders
TarunAdobe Feb 29, 2024
2c97a76
chore(slider): added missing type in slider tests
TarunAdobe Feb 29, 2024
a63f460
Merge branch 'main' into feature/slider-default
TarunAdobe Mar 1, 2024
fa57fcc
chore: update golden image hash
TarunAdobe Mar 1, 2024
d1bb967
chore(slider): migrate inline css to a separate file
TarunAdobe Mar 1, 2024
975bd58
fix(slider): dispatch onInput event on reseting handle value and upda…
TarunAdobe Mar 5, 2024
b7e5c09
Merge branch 'main' into feature/slider-default
TarunAdobe Mar 5, 2024
27ccaa2
chore: updated golden image hash
TarunAdobe Mar 5, 2024
c655213
chore(slider): updated tests to check for input event on reseting def…
TarunAdobe Mar 5, 2024
de6b775
Merge branch 'main' into feature/slider-default
Westbrook Mar 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Expand Up @@ -10,7 +10,7 @@ executors:
parameters:
current_golden_images_hash:
type: string
default: 2b61b82c83bc3f5e7cfff9eaf4695fc67e591aeb
default: a63f4602db4236d25b3401ef7761f68d588e2b28
wireit_cache_name:
type: string
default: wireit
Expand Down
1 change: 1 addition & 0 deletions packages/overlay/stories/overlay.stories.ts
Expand Up @@ -173,6 +173,7 @@ const template = ({
min="0"
max="20"
label="Awesomeness"
default-value="10"
></sp-slider>
<div id="styled-div">
The background of this div should be blue
Expand Down
13 changes: 13 additions & 0 deletions packages/slider/README.md
Expand Up @@ -232,6 +232,19 @@ An `<sp-slider>` element can be paired with an `<sp-number-field>` element via t
<sp-slider quiet disabled editable value="50"></sp-slider>
```

#### 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
<sp-slider value="50" default-value="20"></sp-slider>
```

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 `<sp-number-field>` element and alter its visual delivery until a change has been made to the `<sp-slider>` element at which point the `change` event that is dispatched can be understood as always removing the indeterminate attribute from the `<sp-slider>`.
Expand Down
37 changes: 36 additions & 1 deletion packages/slider/src/HandleController.ts
Expand Up @@ -341,6 +341,22 @@ 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirm whether this dispatches an input event with the same data. See the result of clicking a new value in an <input type="range"> as an example: https://codepen.io/Westbrook/pen/ExJjVRZ?editors=1111

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added dispatchInputEvent on both double-click and escape key interactions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a good test to confirm that this happens before we close this up?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense if I extend the dispatches input of the animation frame test (line 388) or should I make a separate one?

this.requestUpdate();
}
}

public handlePointerdown(event: PointerEvent): void {
const { resolvedInput, model } = this.extractDataFromEvent(event);
if (!model || this.host.disabled || event.button !== 0) {
Expand Down Expand Up @@ -432,7 +448,21 @@ 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;
this.dispatchChangeEvent(input, input.model.handle);
this.requestUpdate();
event.preventDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to dispatch an input event with the same data. See the result of clicking a new value in an as an example: https://codepen.io/Westbrook/pen/ExJjVRZ?editors=1111

event.stopPropagation();
}
return;
}
const input = event.target as InputWithModel;
input.model.handle.highlight = true;
this.requestUpdate();
Expand Down Expand Up @@ -522,12 +552,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}
/>
<span id="slider-description">
Press escape or double click to reset the slider to its
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an entry for this content to #1975

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we display: none in the CSS rather than inline?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 👍

default value.
</span>
</div>
`;
}
Expand Down
12 changes: 9 additions & 3 deletions packages/slider/src/Slider.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -409,10 +408,12 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
></div>
`;
}

private renderHandle(): TemplateResult {
if (this.variant === 'tick') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼

return html``;
}
return html`
${this.variant === 'tick' ? html`` : this.handleController.render()}
${this.handleController.render()}
`;
}

Expand Down Expand Up @@ -442,6 +443,7 @@ export class Slider extends SizedMixin(ObserveSlotText(SliderHandle, ''), {
['pointerup', 'pointercancel', 'pointerleave'],
this.handlePointerup,
],
streamOutside: ['dblclick', this.handleDoubleClick],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half surprised this works, but it does make a nice even API for this.

})}
>
<div id="controls">
Expand Down Expand Up @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/slider/src/SliderHandle.ts
Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions packages/slider/src/slider.css
Expand Up @@ -149,3 +149,7 @@ governing permissions and limitations under the License.
.fill {
z-index: 2;
}

#slider-description {
display: none;
}
51 changes: 51 additions & 0 deletions packages/slider/stories/slider.stories.ts
Expand Up @@ -152,6 +152,26 @@ export const Filled = (args: StoryArgs = {}): TemplateResult => {
`;
};

export const HasADefaultValue = (args: StoryArgs = {}): TemplateResult => {
return html`
<div style="width: 500px; margin-inline: 20px;">
<sp-slider
max="1"
min="0"
value=".5"
step="0.01"
default-value="0.2"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to put default-value into the args, but not a blocker.

@input=${handleEvent(args)}
@change=${handleEvent(args)}
.formatOptions=${{ style: 'percent' }}
...=${spreadProps(args)}
>
double click or press escape key to reset
</sp-slider>
</div>
`;
};

export const FillStart = (args: StoryArgs = {}): TemplateResult => {
return html`
<div style="width: 500px; margin-inline: 20px;">
Expand Down Expand Up @@ -397,6 +417,37 @@ export const editable = (args: StoryArgs = {}): TemplateResult => {

editable.decorators = [editableDecorator];

export const editableWithDefaultValue = (
args: StoryArgs = {}
): TemplateResult => {
return html`
<div style="width: 500px; margin: 12px 20px;">
<sp-slider
editable
max="360"
min="0"
value="90"
step="1"
default-value="180"
@input=${handleEvent(args)}
@change=${handleEvent(args)}
.formatOptions=${{
style: 'unit',
unit: 'degree',
unitDisplay: 'narrow',
}}
...=${spreadProps(args)}
>
Angle
</sp-slider>
</div>
`;
};

editableWithDefaultValue.swc_vrt = {
skip: true,
};

export const editableDisabled = (args: StoryArgs = {}): TemplateResult => {
return html`
<div style="width: 500px; margin: 12px 20px;">
Expand Down
2 changes: 2 additions & 0 deletions packages/slider/test/index.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
129 changes: 128 additions & 1 deletion packages/slider/test/slider.test.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -1604,4 +1607,128 @@ 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 () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a test where the Slider is in an Overlay to confirm it does not close the Overlay.

We should also add default-value to one or some of the Stories in Overlay to confirm this functionality there, as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the test (line 1679) as well as the default-value for stories in overlay.

const el = await fixture<Slider>(
html`
<sp-slider
style="width: 100px"
value="50"
default-value="50"
></sp-slider>
`
);
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);

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);
});
it('manages escape key interactions correctly in an overlaid context', async () => {
const el = await fixture<HTMLDivElement>(
html`
<div>
<sp-button id="trigger">Overlay Trigger</sp-button>
<sp-overlay trigger="trigger@click" placement="bottom">
<sp-popover>
<sp-slider
style="width: 100px"
value="70"
default-value="50"
></sp-slider>
</sp-popover>
</sp-overlay>
</div>
`
);

await elementUpdated(el);

// open the overlay
const trigger = el.querySelector('#trigger') as HTMLButtonElement;
trigger.click();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlay open action should follow the pattern:

const opened = oneEvent(el, 'sp-opened');
trigger.click(); // or similiar
await opened;

Otherwise there's no guarantee the content actually opened and was promoted to the top layer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it!


await elementUpdated(el);

// 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);

// and the overlay should be in open state
const overlay = el.querySelector('sp-overlay') as Overlay;
expect(overlay.open).to.be.true;

// send escape key again
await sendKeys({
press: 'Escape',
});
Comment on lines +1758 to +1761
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to reverse of the above, e.g. const closed = oneEvent(...);


await elementUpdated(el);

// now the overlay should be closed
expect(overlay.open).to.be.false;
});
});