summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/utils/focusable.ts
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/utils/focusable.ts')
-rw-r--r--polygerrit-ui/app/utils/focusable.ts67
1 files changed, 67 insertions, 0 deletions
diff --git a/polygerrit-ui/app/utils/focusable.ts b/polygerrit-ui/app/utils/focusable.ts
new file mode 100644
index 0000000000..d5bed09c07
--- /dev/null
+++ b/polygerrit-ui/app/utils/focusable.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const FOCUSABLE_QUERY =
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
+
+/**
+ * Gets an ordered list of focusable elements nested within a containing
+ * element that may contain shadow DOMs.
+ *
+ * This goes depth-first, so that the order of elements follows the a11y tree.
+ */
+export function* getFocusableElements(
+ el: HTMLElement | SVGElement
+): Generator<HTMLElement | SVGElement> {
+ const style = window.getComputedStyle(el);
+ if (style.display === 'none' || style.visibility === 'hidden') return;
+ if (el.matches(FOCUSABLE_QUERY)) {
+ yield el;
+ }
+
+ let children = [];
+ if (el.localName === 'slot') {
+ children = (el as HTMLSlotElement).assignedNodes({flatten: true});
+ } else {
+ children = [...(el.shadowRoot || el).children];
+ }
+
+ for (const node of children.filter(
+ node => node instanceof HTMLElement || node instanceof SVGElement
+ )) {
+ yield* getFocusableElements(node as HTMLElement | SVGElement);
+ }
+}
+
+/**
+ * Gets an ordered list of focusable elements nested within a containing
+ * element that may contain shadow DOMs.
+ *
+ * This returns in reverse a11 order.
+ */
+export function* getFocusableElementsReverse(
+ el: HTMLElement | SVGElement
+): Generator<HTMLElement | SVGElement> {
+ const style = window.getComputedStyle(el);
+ if (style.display === 'none' || style.visibility === 'hidden') return;
+
+ let children = [];
+ if (el.localName === 'slot') {
+ children = (el as HTMLSlotElement).assignedNodes({flatten: true});
+ } else {
+ children = [...(el.shadowRoot || el).children];
+ }
+
+ for (const node of children
+ .filter(node => node instanceof HTMLElement || node instanceof SVGElement)
+ .reverse()) {
+ yield* getFocusableElementsReverse(node as HTMLElement | SVGElement);
+ }
+
+ if (el.matches(FOCUSABLE_QUERY)) {
+ yield el;
+ }
+}