Skip to content

Commit 5b8f2ff

Browse files
committed
Focus and keyboard handling documentation
The Range Slider is now focusable, the elements that gets the focus when the component is active are the slider's handles and progress tracker. The full keyboard handling specifications are implemented.
1 parent 0d62527 commit 5b8f2ff

File tree

5 files changed

+83
-24
lines changed

5 files changed

+83
-24
lines changed

packages/main/src/RangeSlider.hbs

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
{{>include "./SliderBase.hbs"}}
22

33
{{#*inline "handles"}}
4-
<div class="ui5-slider-handle ui5-slider-handle--start" style="{{styles.startHandle}}" @focusout={{_onfocusout}}
5-
@focusin={{_onfocusin}} tabindex="{{tabIndex}}">
4+
<div class="ui5-slider-handle ui5-slider-handle--start" style="{{styles.startHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
65
{{#if showTooltip}}
76
<div class="ui5-slider-tooltip ui5-slider-tooltip--start" style="{{styles.tooltip}}">
87
<span class="ui5-slider-tooltip-value">{{tooltipStartValue}}</span>
98
</div>
109
{{/if}}
1110
</div>
12-
<div class="ui5-slider-handle ui5-slider-handle--end" style="{{styles.endHandle}}" tabindex="{{tabIndex}}" @focusout={{_onfocusout}}
13-
@focusin={{_onfocusin}}>
11+
<div class="ui5-slider-handle ui5-slider-handle--end" style="{{styles.endHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
1412
{{#if showTooltip}}
1513
<div class="ui5-slider-tooltip ui5-slider-tooltip--end" style="{{styles.tooltip}}">
1614
<span class="ui5-slider-tooltip-value">{{tooltipEndValue}}</span>

packages/main/src/RangeSlider.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class RangeSlider extends SliderBase {
162162
}
163163

164164
_onkeydown(event) {
165-
this._onKeyDownBase(event);
165+
this._handleKeyDown(event);
166166
}
167167

168168
_handleActionKeyPress(event) {

packages/main/src/Slider.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{{>include "./SliderBase.hbs"}}
22

33
{{#*inline "handles"}}
4-
<div class="ui5-slider-handle" style="{{styles.handle}}">
4+
<div class="ui5-slider-handle" style="{{styles.handle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
55
{{#if showTooltip}}
66
<div class="ui5-slider-tooltip" style="{{styles.tooltip}}">
77
<span class="ui5-slider-tooltip-value">{{tooltipValue}}</span>

packages/main/src/SliderBase.hbs

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
{{/if}}
2424

2525
<div class="ui5-slider-progress-container">
26-
<div class="ui5-slider-progress" style="{{styles.progress}}" @focusout={{_onfocusout}}
27-
@focusin={{_onfocusin}} tabindex="{{tabIndexProgress}}"></div>
26+
<div class="ui5-slider-progress" style="{{styles.progress}}" @focusout={{_onfocusout}} @focusin={{_onfocusin}} tabindex="{{tabIndexProgress}}"></div>
2827
</div>
2928
{{> handles}}
3029
</div>

packages/main/src/SliderBase.js

+78-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import Float from "@ui5/webcomponents-base/dist/types/Float.js";
44
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
55
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
66
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
7-
import { isEscape, isHome, isEnd, isUp, isDown, isRight, isLeft, isUpCtrl, isDownCtrl, isRightCtrl, isLeftCtrl, isPlus, isMinus, isPageUp, isPageDown, getCtrlKey } from "@ui5/webcomponents-base/dist/Keys.js";
7+
import {
8+
isEscape, isHome, isEnd, isUp, isDown, isRight, isLeft, isUpCtrl, isDownCtrl, isRightCtrl, isLeftCtrl, isPlus, isMinus, isPageUp, isPageDown,
9+
} from "@ui5/webcomponents-base/dist/Keys.js";
810
import { getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
911

1012
// Styles
@@ -292,21 +294,27 @@ class SliderBase extends UI5Element {
292294
}
293295
}
294296

297+
/**
298+
* Sets initial value when the component is focused in, can be restored with ESC key
299+
*
300+
* @private
301+
*/
295302
_setInitialValue(valueType, value) {
296303
this[`_${valueType}Initial`] = value;
297-
}
304+
}
298305

299306
_getInitialValue(valueType) {
300307
return this[`_${valueType}Initial`];
301308
}
302309

303-
_onKeyDownBase(event) {
310+
_handleKeyDown(event) {
304311
if (this.disabled) {
305312
return;
306313
}
307314

308315
if (SliderBase._isActionKey(event)) {
309316
event.preventDefault();
317+
310318
this._isUserInteraction = true;
311319
this._handleActionKeyPress(event);
312320
}
@@ -320,18 +328,52 @@ class SliderBase extends UI5Element {
320328
this._isUserInteraction = false;
321329
}
322330

323-
static _isActionKey(event) {
324-
return this.ACTION_KEYS.some(actionKey => actionKey(event));
331+
/**
332+
* Flags if an inner element is currently being focused
333+
*
334+
* @private
335+
*/
336+
_preserveFocus(isFocusing) {
337+
this._isInnerElementFocusing = isFocusing;
325338
}
326-
339+
340+
/**
341+
* Return if an inside element within the component is currently being focused
342+
*
343+
* @private
344+
*/
327345
_isFocusing() {
328-
return this._isInProcessOfFocusing;
346+
return this._isInnerElementFocusing;
329347
}
330348

331-
_setIsFocusing(isInProcessOfFocusing) {
332-
this._isInProcessOfFocusing = isInProcessOfFocusing;
333-
}
349+
/**
350+
* Prevent focus out when inner element within the component is currently being in process of focusing in.
351+
* In theory this can be achieved either if the shadow root is focusable and 'delegatesFocus' attribute of
352+
* the .attachShadow() customElement method is set to true, or if we forward it manually.
334353
354+
* As we use lit-element as base of our core UI5 element class that 'delegatesFocus' property is not set to 'true' and
355+
* we have to manage the focus here. If at some point in the future this changes, the focus delegating logic could be
356+
* removed as it will become redundant.
357+
*
358+
* When we manually set the focus on mouseDown to the first focusable element inside the shadowDom,
359+
* that inner focus (shadowRoot.activeElement) is set a moment before the global document.activeElement
360+
* is set to the customElement (ui5-slider) causing a 'race condition'.
361+
*
362+
* In order for a element within the shadowRoot to be focused, the global document.activeElement MUST be the parent
363+
* customElement of the shadow root, in our case the ui5-slider component. Because of that after our focusin of the handle,
364+
* a focusout event fired by the browser immidiatly after, resetting the focus. Focus out must be manually prevented
365+
* in both initial focusing and switching the focus between inner elements of the component cases.
366+
367+
* Note: If we set the focus to the handle with a timeout or a bit later in time, on a mouseup or click event it will
368+
* work fine and we will avoid the described race condition as our host customElement will be already finished focusing.
369+
* However, that does not work for us as we need the focus to be set to the handle exactly on mousedown,
370+
* because of the nature of the component and its available drag interactions.
371+
*
372+
* @private
373+
*/
374+
_preventFocusOut() {
375+
this._focusInnerElement();
376+
}
335377

336378
/**
337379
* Handle the responsiveness of the Slider's UI elements when resizing
@@ -363,7 +405,6 @@ class SliderBase extends UI5Element {
363405
return;
364406
}
365407

366-
367408
// Check if there are any overlapping labels.
368409
// If so - only the first and the last one should be visible
369410
const labelItems = this.shadowRoot.querySelectorAll(".ui5-slider-labels li");
@@ -397,11 +438,24 @@ class SliderBase extends UI5Element {
397438
SliderBase.UP_EVENTS.forEach(upEventType => window.addEventListener(upEventType, this._upHandler));
398439
window.addEventListener(this._moveEventType, this._moveHandler);
399440

400-
this._setIsFocusing(true);
401-
this._focusInnerElement();
441+
this._handleFocusOnMouseDown(event);
402442
return newValue;
403443
}
404444

445+
/**
446+
* Forward the focus to an inner inner part within the component on press
447+
*
448+
* @private
449+
*/
450+
_handleFocusOnMouseDown(event) {
451+
const focusedElement = this.shadowRoot.activeElement;
452+
453+
if (!focusedElement || focusedElement !== event.target) {
454+
this._preserveFocus(true);
455+
this._focusInnerElement();
456+
}
457+
}
458+
405459
/**
406460
* Called when the user finish interacting with the slider
407461
* Fires an <code>change</code> event indicating a final value change, after user interaction is finished.
@@ -418,7 +472,7 @@ class SliderBase extends UI5Element {
418472

419473
this._moveEventType = null;
420474
this._isUserInteraction = false;
421-
this._setIsFocusing(false);
475+
this._preserveFocus(false);
422476
}
423477

424478
/**
@@ -435,6 +489,15 @@ class SliderBase extends UI5Element {
435489
}
436490
}
437491

492+
/**
493+
* Goes through the key shortcuts available for the component and returns 'true' if the event is triggered by one.
494+
*
495+
* @private
496+
*/
497+
static _isActionKey(event) {
498+
return this.ACTION_KEYS.some(actionKey => actionKey(event));
499+
}
500+
438501
/**
439502
* Locks the given value between min and max boundaries based on slider properties
440503
*
@@ -688,7 +751,6 @@ class SliderBase extends UI5Element {
688751
}
689752

690753
_handleActionKeyPress(event, affectedValue) {
691-
const isDownAction = SliderBase._isDecreaseValueAction(event);
692754
const isUpAction = SliderBase._isIncreaseValueAction(event);
693755
const isBigStep = SliderBase._isBigStepAction(event);
694756

@@ -707,7 +769,7 @@ class SliderBase extends UI5Element {
707769
}
708770

709771
if (isHome(event)) {
710-
return (currentValue - min) * - 1;
772+
return (currentValue - min) * -1;
711773
}
712774

713775
return isUpAction ? step : step * -1;

0 commit comments

Comments
 (0)