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

refactor: move focus utils to component-base #3141

Merged
merged 7 commits into from
Dec 3, 2021
Merged

Conversation

vursen
Copy link
Contributor

@vursen vursen commented Dec 1, 2021

Description

  • Decomposed the FocusablesHelper class into plain functions.
  • Moved those to @vaadin/component-base/src/focus-utils.js.
  • Added unit tests.
  • Added TypeScript declarations.

Part of #3140.

Type of change

  • Internal

Checklist

  • I have read the contribution guide: https://vaadin.com/docs-beta/latest/guide/contributing/overview/
  • I have added a description following the guideline.
  • The issue is created in the corresponding repository and I have referenced it.
  • I have added tests to ensure my change is effective and works as intended.
  • New and existing tests are passing locally with my change.
  • I have performed self-review and corrected misspellings.

@vursen vursen marked this pull request as draft December 1, 2021 15:39
@vursen vursen marked this pull request as ready for review December 2, 2021 10:00
* @param {!HTMLElement} element
* @return {boolean}
*/
function isElementHidden(element) {
Copy link
Contributor Author

@vursen vursen Dec 2, 2021

Choose a reason for hiding this comment

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

note: Renamed isVisible to isElementHidden to invert the condition here.

static _isVisible(element) {
// Check inline style first to save a re-flow. If looks good, check also
// computed style.
let style = element.style;
if (style.visibility !== 'hidden' && style.display !== 'none') {
style = window.getComputedStyle(element);
return style.visibility !== 'hidden' && style.display !== 'none';
}
return false;
}

* @param {HTMLElement} element
* @return {boolean}
*/
export function isElementFocused(element) {
Copy link
Contributor Author

@vursen vursen Dec 2, 2021

Choose a reason for hiding this comment

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

note: A new utility function.

* @param {HTMLElement} element
* @return {HTMLElement[]}
*/
export function getFocusableElements(element) {
Copy link
Contributor Author

@vursen vursen Dec 2, 2021

Choose a reason for hiding this comment

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

note: Renamed getTabbableNodes to getFocusableElements.

static getTabbableNodes(node) {
const result = [];
// If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex.
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) {
return this._sortByTabIndex(result);
}
return result;
}

Comment on lines 110 to 148
function collectFocusableNodes(node, result) {
// If the node is not an element or hidden, no need to traverse children.
if (node.nodeType !== Node.ELEMENT_NODE || isElementHidden(node)) {
return false;
}
const element = /** @type {HTMLElement} */ (node);
const tabIndex = normalizeTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}

// In ShadowDOM v1, tab order is affected by the order of distribution.
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
// in ShadowDOM v0 tab order is not affected by the distribution order,
// in fact getTabbableNodes(#root) returns [#B, #A].
// <div id="root">
// <!-- shadow -->
// <slot name="a">
// <slot name="b">
// <!-- /shadow -->
// <input id="A" slot="a">
// <input id="B" slot="b" tabindex="1">
// </div>
let children;
if (element.localName === 'slot') {
children = element.assignedNodes({ flatten: true });
} else {
// Use shadow root if possible, will check for distributed nodes.
children = (element.shadowRoot || element).children;
}
if (children) {
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = collectFocusableNodes(children[i], result) || needsSort;
}
}
return needsSort;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: Renamed _collectTabbableNodes to collectFocusableNodes.

static _collectTabbableNodes(node, result) {
// If not an element or not visible, no need to explore children.
if (node.nodeType !== Node.ELEMENT_NODE || !this._isVisible(node)) {
return false;
}
const element = /** @type {!HTMLElement} */ (node);
const tabIndex = this._normalizedTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}
// In ShadowDOM v1, tab order is affected by the order of distribution.
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
// in ShadowDOM v0 tab order is not affected by the distribution order,
// in fact getTabbableNodes(#root) returns [#B, #A].
// <div id="root">
// <!-- shadow -->
// <slot name="a">
// <slot name="b">
// <!-- /shadow -->
// <input id="A" slot="a">
// <input id="B" slot="b" tabindex="1">
// </div>
let children;
if (element.localName === 'slot') {
children = element.assignedNodes({ flatten: true });
} else {
// Use shadow root if possible, will check for distributed nodes.
children = (element.shadowRoot || element).children;
}
if (children) {
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
}
}
return needsSort;
}

* @param {HTMLElement} element
* @return {boolean}
*/
export function isElementFocusable(element) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: Renamed isFocusable to isElementFocusable.

static isFocusable(element) {
// From http://stackoverflow.com/a/1600194/4228703:
// There isn't a definite list, it's up to the browser. The only
// standard we have is DOM Level 2 HTML
// https://www.w3.org/TR/DOM-Level-2-HTML/html.html, according to which the
// only elements that have a focus() method are HTMLInputElement,
// HTMLSelectElement, HTMLTextAreaElement and HTMLAnchorElement. This
// notably omits HTMLButtonElement and HTMLAreaElement. Referring to these
// tests with tabbables in different browsers
// http://allyjs.io/data-tables/focusable.html
// Elements that cannot be focused if they have [disabled] attribute.
if (element.matches('input, select, textarea, button, object')) {
return element.matches(':not([disabled])');
}
// Elements that can be focused even if they have [disabled] attribute.
return element.matches('a[href], area[href], iframe, [tabindex], [contentEditable]');
}

/**
* @param {Element[]} elements
* @return {number}
* @protected
*/
_focusedIndex(elements) {
elements = elements || this._getFocusableElements();
return elements.indexOf(elements.filter(this._isFocused).pop());
return elements.indexOf(elements.filter((element) => element && isElementFocused(element)).pop());
Copy link
Contributor Author

@vursen vursen Dec 2, 2021

Choose a reason for hiding this comment

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

note: It is the caller's responsibility to make sure that the element is not undefined or null before it is passed to the isElementFocused function.

@vursen vursen mentioned this pull request Dec 2, 2021
7 tasks
@vursen vursen force-pushed the feat/focus-utils branch 6 times, most recently from 5778897 to c060b97 Compare December 3, 2021 07:44
@sonarcloud
Copy link

sonarcloud bot commented Dec 3, 2021

Please retry analysis of this Pull-Request directly on SonarCloud.

@vaadin-bot
Copy link
Collaborator

This ticket/PR has been released with platform 23.0.0.alpha1 and is also targeting the upcoming stable 23.0.0 version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants