-
Notifications
You must be signed in to change notification settings - Fork 280
Expand file tree
/
Copy pathSlider.ts
More file actions
420 lines (354 loc) · 12.9 KB
/
Slider.ts
File metadata and controls
420 lines (354 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { isEscape, isF2 } from "@ui5/webcomponents-base/dist/Keys.js";
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import SliderBase from "./SliderBase.js";
import type SliderTooltip from "./SliderTooltip.js";
// Template
import SliderTemplate from "./SliderTemplate.js";
import type { SliderTooltipChangeEventDetails } from "./SliderTooltip.js";
// Texts
import {
SLIDER_ARIA_DESCRIPTION,
SLIDER_TOOLTIP_INPUT_DESCRIPTION,
SLIDER_TOOLTIP_INPUT_LABEL,
} from "./generated/i18n/i18n-defaults.js";
/**
* @class
*
* ### Overview
* The Slider component represents a numerical range and a handle (grip).
* The purpose of the component is to enable visual selection of a value in
* a continuous numerical range by moving an adjustable handle.
*
* ### Structure
* The most important properties of the Slider are:
*
* - min - The minimum value of the slider range.
* - max - The maximum value of the slider range.
* - value - The current value of the slider range.
* - step - Determines the increments in which the slider will move.
* - showTooltip - Determines if a tooltip should be displayed above the handle.
* - showTickmarks - Displays a visual divider between the step values.
* - labelInterval - Labels some or all of the tickmarks with their values.
*
* ### Usage
* The most common use case is to select values on a continuous numerical scale (e.g. temperature, volume, etc. ).
*
* ### Responsive Behavior
* The `ui5-slider` component adjusts to the size of its parent container by recalculating and
* resizing the width of the control. You can move the slider handle in several different ways:
*
* - Drag and drop the handle to the desired value.
* - Click/tap on the range bar to move the handle to that location.
*
* ### Keyboard Handling
*
* - `Left or Down Arrow` - Moves the handle one step to the left, effectively decreasing the component's value by `step` amount;
* - `Right or Up Arrow` - Moves the handle one step to the right, effectively increasing the component's value by `step` amount;
* - `Left or Down Arrow + Ctrl/Cmd` - Moves the handle to the left with step equal to 1/10th of the entire range, effectively decreasing the component's value by 1/10th of the range;
* - `Right or Up Arrow + Ctrl/Cmd` - Moves the handle to the right with step equal to 1/10th of the entire range, effectively increasing the component's value by 1/10th of the range;
* - `Plus` - Same as `Right or Up Arrow`;
* - `Minus` - Same as `Left or Down Arrow`;
* - `Home` - Moves the handle to the beginning of the range;
* - `End` - Moves the handle to the end of the range;
* - `Page Up` - Same as `Right or Up + Ctrl/Cmd`;
* - `Page Down` - Same as `Left or Down + Ctrl/Cmd`;
* - `Escape` - Resets the value property after interaction, to the position prior the component's focusing;
*
* ### ES6 Module Import
*
* `import "@ui5/webcomponents/dist/Slider.js";`
* @constructor
* @extends SliderBase
* @since 1.0.0-rc.11
* @public
* @csspart progress-container - Used to style the progress container, the horizontal bar that visually represents the range between the minimum and maximum values, of the `ui5-slider`.
* @csspart progress-bar - Used to style the progress bar, which shows the progress of the `ui5-slider`.
* @csspart handle - Used to style the handle of the `ui5-slider`.
*/
@customElement({
tag: "ui5-slider",
languageAware: true,
formAssociated: true,
template: SliderTemplate,
})
class Slider extends SliderBase implements IFormInputElement {
/**
* Current value of the slider
* @default 0
* @formEvents change input
* @formProperty
* @public
*/
@property({ type: Number })
value = 0;
@property()
tooltipValueState: `${ValueState}` = "None";
@property()
tooltipValue = "";
_valueInitial?: number;
_valueOnInteractionStart?: number;
_progressPercentage = 0;
_handlePositionFromStart = 0;
_lastValidInputValue: string;
get formFormattedValue() {
return this.value.toString();
}
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;
constructor() {
super();
this._stateStorage.value = undefined;
this._lastValidInputValue = this.min.toString();
}
/**
*
* Check if the previously saved state is outdated. That would mean
* either it is the initial rendering or that a property has been changed
* programmatically - because the previous state is always updated in
* the interaction handlers.
*
* Normalize current properties, update the previously stored state.
* Update the visual UI representation of the Slider.
*
*/
onBeforeRendering() {
if (!this.isCurrentStateOutdated()) {
return;
}
this.notResized = true;
this.syncUIAndState();
this._updateHandleAndProgress(this.value);
}
onAfterRendering(): void {
super.onAfterRendering();
this.tooltip?.repositionTooltip();
}
syncUIAndState() {
// Validate step and update the stored state for the step property.
if (this.isPropertyUpdated("step")) {
this._validateStep(this.step);
this.storePropertyState("step");
}
// Recalculate the tickmarks and labels and update the stored state.
if (this.isPropertyUpdated("min", "max", "value")) {
this.storePropertyState("min", "max");
// Here the value props are changed programmatically (not by user interaction)
// and it won't be "stepified" (rounded to the nearest step). 'Clip' them within
// min and max bounderies and update the previous state reference.
this.value = SliderBase.clipValue(this.value, this._effectiveMin, this._effectiveMax);
this.updateStateStorageAndFireInputEvent("value");
this.storePropertyState("value");
}
// Labels must be updated if any of the min/max/step/labelInterval props are changed
if (this.labelInterval && this.showTickmarks) {
this._createLabels();
}
// Update the stored state for the labelInterval, if changed
if (this.isPropertyUpdated("labelInterval")) {
this.storePropertyState("labelInterval");
}
}
/**
* Called when the user starts interacting with the slider
* @private
*/
_onmousedown(e: TouchEvent | MouseEvent) {
// If step is 0 no interaction is available because there is no constant
// (equal for all user environments) quantitative representation of the value
if (this.disabled || this.step === 0 || (e.target as HTMLElement).hasAttribute("ui5-slider-tooltip")) {
return;
}
const newValue = this.handleDownBase(e);
this._valueOnInteractionStart = this.value;
// Set initial value if one is not set previously on focus in.
// It will be restored if ESC key is pressed.
if (this._valueInitial === undefined) {
this._valueInitial = this.value;
}
// Do not yet update the Slider if press is over a handle. It will be updated if the user drags the mouse.
const ctor = this.constructor as typeof Slider;
if (!this._isHandlePressed(ctor.getPageXValueFromEvent(e))) {
this._updateHandleAndProgress(newValue);
this.value = newValue;
this.updateStateStorageAndFireInputEvent("value");
}
}
_onfocusin() {
// Set initial value if one is not set previously on focus in.
// It will be restored if ESC key is pressed.
if (this._valueInitial === undefined) {
this._valueInitial = this.value;
}
if (this.showTooltip) {
this._tooltipsOpen = true;
}
}
_onfocusout(e: FocusEvent) {
// Prevent focusout when the focus is getting set within the slider internal
// element (on the handle), before the Slider' customElement itself is finished focusing
if (this._isFocusing()) {
this._preventFocusOut();
return;
}
// Reset focus state and the stored Slider's initial
// value that was saved when it was first focused in
this._valueInitial = undefined;
if (this.showTooltip && !(e.relatedTarget as HTMLElement)?.hasAttribute("ui5-slider-tooltip")) {
this._tooltipsOpen = false;
}
}
_onTooltipChange(e: CustomEvent<SliderTooltipChangeEventDetails>) {
const value = parseFloat(e.detail.value);
const isInvalid = value < this._effectiveMin || value > this._effectiveMax;
if (isInvalid) {
this.tooltipValueState = "Negative";
this.tooltipValue = `${value}`;
return;
}
this.value = value;
this.fireDecoratorEvent("change");
}
_onTooltipFocusChange() {
const value = parseFloat(this.tooltipValue);
const isInvalid = value < this._effectiveMin || value > this._effectiveMax;
if (isInvalid) {
this.tooltipValueState = "None";
this.tooltipValue = this.value.toString();
}
}
_onTooltipKeydown(e: KeyboardEvent) {
if (isF2(e)) {
e.preventDefault();
this._sliderHandle.focus();
}
}
_onTooltipOpen() {
const ctor = this.constructor as typeof Slider;
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
this.tooltipValue = this.value.toFixed(stepPrecision);
}
_onTooltipInput(e: CustomEvent) {
this.tooltipValue = e.detail.value;
}
/**
* Called when the user moves the slider
* @private
*/
_handleMove(e: TouchEvent | MouseEvent) {
e.preventDefault();
// If step is 0 no interaction is available because there is no constant
// (equal for all user environments) quantitative representation of the value
if (this.disabled || this._effectiveStep === 0) {
return;
}
const ctor = this.constructor as typeof Slider;
const newValue = ctor.getValueFromInteraction(e, this._effectiveStep, this._effectiveMin, this._effectiveMax, this.getBoundingClientRect(), this.directionStart);
this._updateHandleAndProgress(newValue);
this.value = newValue;
this.tooltipValue = newValue.toString();
this.updateStateStorageAndFireInputEvent("value");
}
/** Called when the user finish interacting with the slider
* @private
*/
_handleUp() {
if (this._valueOnInteractionStart !== this.value) {
this.fireDecoratorEvent("change");
}
this.handleUpBase();
this._valueOnInteractionStart = undefined;
}
_onkeyup(e: KeyboardEvent) {
const isActionKey = SliderBase._isActionKey(e);
this._onKeyupBase();
if (isActionKey && this._valueOnInteractionStart !== this.value) {
this.fireDecoratorEvent("change");
}
this._valueOnInteractionStart = this.value;
}
/** Determines if the press is over the handle
* @private
*/
_isHandlePressed(clientX: number) {
const sliderHandleDomRect = this._sliderHandle.getBoundingClientRect();
return clientX >= sliderHandleDomRect.left && clientX <= sliderHandleDomRect.right;
}
/** Updates the UI representation of the progress bar and handle position
* @private
*/
_updateHandleAndProgress(newValue: number) {
const max = this._effectiveMax;
const min = this._effectiveMin;
// The progress (completed) percentage of the slider.
this._progressPercentage = (newValue - min) / (max - min);
// How many pixels from the left end of the slider will be the placed the affected by the user action handle
this._handlePositionFromStart = this._progressPercentage * 100;
}
_handleActionKeyPress(e: KeyboardEvent) {
const min = this._effectiveMin;
const max = this._effectiveMax;
const currentValue = this.value;
const ctor = this.constructor as typeof Slider;
const newValue = isEscape(e) ? this._valueInitial : ctor.clipValue(this._handleActionKeyPressBase(e, "value") + currentValue, min, max);
if (newValue !== currentValue) {
this._updateHandleAndProgress(newValue!);
this.value = newValue!;
this.tooltipValue = this.value.toString();
this.updateStateStorageAndFireInputEvent("value");
}
}
_onTooltopForwardFocus(e: CustomEvent) {
const tooltip = e.target as SliderTooltip;
tooltip.followRef?.focus();
}
get inputValue() {
return this.value.toString();
}
get tooltip() {
return this.getDomRef()?.querySelector<SliderTooltip>("[ui5-slider-tooltip]");
}
get styles() {
return {
progress: {
"width": `${this._handlePositionFromStart}%`,
"border": this._handlePositionFromStart === 0 ? "none" : "",
},
handle: {
[this.directionStart]: `${this._handlePositionFromStart}%`,
},
};
}
get _sliderHandle() : HTMLElement {
return this.shadowRoot!.querySelector(".ui5-slider-handle")!;
}
get _ariaDisabled() {
return this.disabled || undefined;
}
get _ariaLabelledByText() {
return Slider.i18nBundle.getText(SLIDER_ARIA_DESCRIPTION);
}
get _ariaDescribedByInputText() {
return Slider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_DESCRIPTION);
}
get _ariaLabelledByInputText() {
return Slider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL);
}
get tickmarksObject() {
const count = this._tickmarksCount;
const arr = [];
if (this._hiddenTickmarks) {
return [true, false];
}
for (let i = 0; i <= count; i++) {
arr.push(this._effectiveMin + (i * this.step) <= this.value);
}
return arr;
}
}
Slider.define();
export default Slider;