@aurodesignsystem-dev/auro-library 0.0.0-pr187.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.husky/commit-msg +4 -0
  2. package/.husky/pre-commit +4 -0
  3. package/.tool-versions +1 -0
  4. package/CHANGELOG.md +664 -0
  5. package/LICENSE +201 -0
  6. package/README.md +235 -0
  7. package/bin/generateDocs.mjs +210 -0
  8. package/bin/generateDocs_index.mjs +210 -0
  9. package/package.json +92 -0
  10. package/scripts/build/generateDocs.mjs +24 -0
  11. package/scripts/build/generateReadme.mjs +60 -0
  12. package/scripts/build/generateWcaComponent.mjs +43 -0
  13. package/scripts/build/postCss.mjs +66 -0
  14. package/scripts/build/postinstall.mjs +31 -0
  15. package/scripts/build/pre-commit.mjs +17 -0
  16. package/scripts/build/prepWcaCompatibleCode.mjs +19 -0
  17. package/scripts/build/processors/defaultDocsProcessor.mjs +83 -0
  18. package/scripts/build/processors/defaultDotGithubSync.mjs +83 -0
  19. package/scripts/build/staticStyles-template.js +2 -0
  20. package/scripts/build/syncGithubFiles.mjs +25 -0
  21. package/scripts/build/versionWriter.js +26 -0
  22. package/scripts/runtime/FocusTrap/FocusTrap.mjs +194 -0
  23. package/scripts/runtime/FocusTrap/index.mjs +1 -0
  24. package/scripts/runtime/FocusTrap/test/FocusTrap.test.js +168 -0
  25. package/scripts/runtime/Focusables/Focusables.mjs +157 -0
  26. package/scripts/runtime/Focusables/index.mjs +1 -0
  27. package/scripts/runtime/Focusables/test/Focusables.test.js +165 -0
  28. package/scripts/runtime/dateUtilities/baseDateUtilities.mjs +58 -0
  29. package/scripts/runtime/dateUtilities/dateConstraints.mjs +11 -0
  30. package/scripts/runtime/dateUtilities/dateFormatter.mjs +104 -0
  31. package/scripts/runtime/dateUtilities/dateUtilities.mjs +218 -0
  32. package/scripts/runtime/dateUtilities/index.mjs +26 -0
  33. package/scripts/runtime/dependencyTagVersioning.mjs +42 -0
  34. package/scripts/runtime/floatingUI.mjs +646 -0
  35. package/scripts/test-plugin/iterateWithA11Check.mjs +82 -0
  36. package/scripts/utils/auroFileHandler.mjs +70 -0
  37. package/scripts/utils/auroLibraryUtils.mjs +206 -0
  38. package/scripts/utils/auroTemplateFiller.mjs +178 -0
  39. package/scripts/utils/logger.mjs +73 -0
  40. package/scripts/utils/runtimeUtils.mjs +70 -0
  41. package/scripts/utils/sharedFileProcessorUtils.mjs +270 -0
  42. package/shellScripts/README.md +58 -0
  43. package/shellScripts/automation.sh +104 -0
@@ -0,0 +1,83 @@
1
+ import {Logger} from "../../utils/logger.mjs";
2
+ import {
3
+ fromAuroComponentRoot,
4
+ generateWCGeneratorUrl,
5
+ processContentForFile,
6
+ templateFiller
7
+ } from "../../utils/sharedFileProcessorUtils.mjs";
8
+
9
+ /**
10
+ * Processor config object.
11
+ * @typedef {Object} ProcessorConfig
12
+ * @property {boolean} [overwriteLocalCopies=true] - The release version tag to use instead of master.
13
+ * @property {string} [generatorTemplateVersion="master"] - The release version tag to use instead of master.
14
+ * (like "_esm" to make README_esm.md).
15
+ */
16
+
17
+ const DOT_GITHUB_PATH = '.github';
18
+ const ISSUE_TEMPLATE_PATH = `${DOT_GITHUB_PATH}/ISSUE_TEMPLATE`;
19
+
20
+ /**
21
+ * @type {ProcessorConfig} config - The default configuration for this processor.
22
+ */
23
+ const defaultGitHubSyncConfig = {
24
+ overwriteLocalCopies: true,
25
+ generatorTemplateVersion: "master",
26
+ };
27
+
28
+ /**
29
+ * @param {ProcessorConfig} config - The configuration for this processor.
30
+ * @returns {import('../utils/sharedFileProcessorUtils').FileProcessorConfig[]}
31
+ */
32
+ const defaultGitHubTemplateFiles = (config = defaultGitHubSyncConfig) => [
33
+ // bug_report.yml
34
+ {
35
+ identifier: 'bug_report.yml',
36
+ input: {
37
+ remoteUrl: generateWCGeneratorUrl(config.generatorTemplateVersion, `templates/${ISSUE_TEMPLATE_PATH}/bug_report.yml`),
38
+ fileName: fromAuroComponentRoot(`docTemplates/${ISSUE_TEMPLATE_PATH}/bug_report.yml`),
39
+ overwrite: config.overwriteLocalCopies
40
+ },
41
+ output: fromAuroComponentRoot(`${ISSUE_TEMPLATE_PATH}/bug_report.yml`)
42
+ },
43
+ // config.yml
44
+ {
45
+ identifier: 'config.yml',
46
+ input: {
47
+ remoteUrl: generateWCGeneratorUrl(config.generatorTemplateVersion, `templates/${ISSUE_TEMPLATE_PATH}/config.yml`),
48
+ fileName: fromAuroComponentRoot(`docTemplates/${ISSUE_TEMPLATE_PATH}/config.yml`),
49
+ overwrite: config.overwriteLocalCopies
50
+ },
51
+ output: fromAuroComponentRoot(`${ISSUE_TEMPLATE_PATH}/config.yml`)
52
+ },
53
+ // PULL_REQUEST_TEMPLATE.md
54
+ {
55
+ identifier: 'PULL_REQUEST_TEMPLATE.md',
56
+ input: {
57
+ remoteUrl: generateWCGeneratorUrl(config.generatorTemplateVersion, `templates/${DOT_GITHUB_PATH}/PULL_REQUEST_TEMPLATE.md`),
58
+ fileName: fromAuroComponentRoot(`docTemplates/${DOT_GITHUB_PATH}/PULL_REQUEST_TEMPLATE.md`),
59
+ overwrite: config.overwriteLocalCopies
60
+ },
61
+ output: fromAuroComponentRoot(`${DOT_GITHUB_PATH}/PULL_REQUEST_TEMPLATE.md`)
62
+ },
63
+ ];
64
+
65
+ /**
66
+ *
67
+ * @param {ProcessorConfig} config - The configuration for this processor.
68
+ * @return {Promise<void>}
69
+ */
70
+ export async function syncGithubFiles(config = defaultGitHubSyncConfig) {
71
+ // setup
72
+ await templateFiller.extractNames();
73
+
74
+ for (const file of defaultGitHubTemplateFiles(config)) {
75
+ try {
76
+ Logger.log(`Processing file: ${file.identifier}`);
77
+ // eslint-disable-next-line no-await-in-loop
78
+ await processContentForFile(file);
79
+ } catch (error) {
80
+ Logger.error(`Error processing file: ${file.identifier}, ${error.message}`);
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,2 @@
1
+ import { css } from 'lit';
2
+ export default css`<% content %>`;
@@ -0,0 +1,25 @@
1
+ // ------------------------------------------------------
2
+ // GitHub Config Sync
3
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ //
5
+ // This script will synchronize shared GitHub config
6
+ // files to the local repo (eventually, anything that lives
7
+ // in the .github directory).
8
+ //
9
+ // The actions in this file live in:
10
+ // scripts/build/processors/defaultDotGithubSync.mjs
11
+ //
12
+ // This script is intended to be run AS-IS without modification.
13
+ // To run a different processor, please see the defaultDotGithubSync.mjs file
14
+ // and re-create in your repo OR add a new processor file.
15
+ // ------------------------------------------------------
16
+
17
+ import {syncGithubFiles} from "./processors/defaultDotGithubSync.mjs";
18
+ import {Logger} from "../utils/logger.mjs";
19
+
20
+ syncGithubFiles().then(() => {
21
+ Logger.log('Docs processed successfully');
22
+ }).
23
+ catch((err) => {
24
+ Logger.error(`Error processing docs: ${err.message}`);
25
+ });
@@ -0,0 +1,26 @@
1
+ // Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
2
+ // See LICENSE in the project root for license information.
3
+
4
+ // ---------------------------------------------------------------------
5
+
6
+ /* eslint-disable no-undef */
7
+
8
+ const auroSubNameIndex = 5;
9
+
10
+ /**
11
+ * Writes a version file for the specified dependency package into the `src` directory.
12
+ * @param {string} pkg Dependency to write version file for.
13
+ */
14
+ function writeDepVersionFile(pkg) {
15
+ const fs = require('fs');
16
+ const path = `${pkg}/package.json`;
17
+ const json = require(path);
18
+ const {version} = json;
19
+ const elemSubName = pkg.substring(pkg.indexOf('auro-') + auroSubNameIndex);
20
+ const versionFilePath = `./src/${elemSubName}Version.js`;
21
+
22
+ fs.writeFileSync(versionFilePath, `export default '${version}'`);
23
+ }
24
+
25
+ // add the code below
26
+ module.exports = { writeDepVersionFile };
@@ -0,0 +1,194 @@
1
+ import { getFocusableElements } from '../Focusables/Focusables.mjs';
2
+
3
+ /**
4
+ * FocusTrap manages keyboard focus within a specified container element, ensuring that focus does not leave the container when tabbing.
5
+ * It is commonly used for modal dialogs or overlays to improve accessibility by trapping focus within interactive UI components.
6
+ */
7
+ export class FocusTrap {
8
+ /**
9
+ * Creates a new FocusTrap instance for the given container element.
10
+ * Initializes event listeners and prepares the container for focus management.
11
+ *
12
+ * @param {HTMLElement} container The DOM element to trap focus within.
13
+ * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
14
+ * @throws {Error} If the provided container is not a valid HTMLElement.
15
+ */
16
+ constructor(container, controlTabOrder = false) {
17
+ if (!container || !(container instanceof HTMLElement)) {
18
+ throw new Error("FocusTrap requires a valid HTMLElement.");
19
+ }
20
+
21
+ this.container = container;
22
+ this.tabDirection = 'forward'; // or 'backward';
23
+ this.controlTabOrder = controlTabOrder;
24
+
25
+ this._init();
26
+ }
27
+
28
+ /**
29
+ * Initializes the focus trap by setting up event listeners and attributes on the container.
30
+ * Prepares the container for focus management, including support for shadow DOM and inert attributes.
31
+ *
32
+ * @private
33
+ */
34
+ _init() {
35
+
36
+ // Add inert attribute to prevent focusing programmatically as well (if supported)
37
+ if ('inert' in HTMLElement.prototype) {
38
+ this.container.inert = false; // Ensure the container isn't inert
39
+ this.container.setAttribute('data-focus-trap-container', true); // Mark for identification
40
+ }
41
+
42
+ // Track tab direction
43
+ this.container.addEventListener('keydown', this._onKeydown);
44
+ }
45
+
46
+ /**
47
+ * Gets an array of currently active (focused) elements in the document and shadow DOM.
48
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
49
+ * @private
50
+ */
51
+ _getActiveElements() {
52
+ // Get the active element(s) in the document and shadow root
53
+ // This will include the active element in the shadow DOM if it exists
54
+ // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
55
+ let {activeElement} = document;
56
+ const actives = [activeElement];
57
+ while (activeElement?.shadowRoot?.activeElement) {
58
+ actives.push(activeElement.shadowRoot.activeElement);
59
+ activeElement = activeElement.shadowRoot.activeElement;
60
+ }
61
+ return actives;
62
+ }
63
+
64
+ /**
65
+ * Gets the next focus index based on the current index and focusable elements.
66
+ * @param {number} currentIndex The current index of the focused element.
67
+ * @param {Array<HTMLElement>} focusables The array of focusable elements.
68
+ * @returns {number|null} The next focus index or null if not determined.
69
+ */
70
+ _getNextFocusIndex(currentIndex, focusables, actives) {
71
+
72
+ // Determine if we need to wrap
73
+ const atFirst = actives.includes(focusables[0]) || actives.includes(this.container);
74
+ const atLast = actives.includes(focusables[focusables.length - 1]);
75
+
76
+ if (this.controlTabOrder) {
77
+ // Calculate the new index based on the current index and tab direction
78
+ let newFocusIndex = currentIndex + (this.tabDirection === 'forward' ? 1 : -1);
79
+
80
+ // Wrap-around logic
81
+ if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
82
+ if (newFocusIndex >= focusables.length) newFocusIndex = 0;
83
+
84
+ return newFocusIndex;
85
+ }
86
+
87
+ // Only wrap if at the ends
88
+ if (this.tabDirection === 'backward' && atFirst) {
89
+ return focusables.length - 1;
90
+ }
91
+
92
+ if (this.tabDirection === 'forward' && atLast) {
93
+ return 0;
94
+ }
95
+
96
+ // No wrap, so don't change focus, return early
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Handles the Tab key press event to manage focus within the container.
102
+ * @param {KeyboardEvent} e The keyboard event triggered by the user.
103
+ * @returns {void}
104
+ */
105
+ _handleTabKey(e) {
106
+
107
+ // Update the focusable elements
108
+ const focusables = this._getFocusableElements();
109
+
110
+ if (!focusables.length) {
111
+ console.warn('FocusTrap: No focusable elements found in the container.');
112
+ return;
113
+ }
114
+
115
+ // Set the tab direction based on the key pressed
116
+ this.tabDirection = e.shiftKey ? 'backward' : 'forward';
117
+
118
+ // Get the active elements that are currently focused
119
+ const actives = this._getActiveElements();
120
+
121
+ // If we're at either end of the focusable elements, wrap around to the other end
122
+ let focusIndex = focusables.findIndex((el) => actives.includes(el));
123
+
124
+ // Fallback if we have no focused element
125
+ if (focusIndex === -1) focusIndex = 0;
126
+
127
+ // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
128
+ // Is null if no new focus index is determined
129
+ let newFocusIndex = this._getNextFocusIndex(focusIndex, focusables, actives);
130
+
131
+ // If we have a new focus index, set focus to that element
132
+ if (newFocusIndex !== null) {
133
+ e.preventDefault();
134
+ focusables[newFocusIndex].focus();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Catches the keydown event
140
+ * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
141
+ * @private
142
+ */
143
+ _onKeydown = (e) => {
144
+
145
+ // Handle tab
146
+ if (e.key === 'Tab') this._handleTabKey(e);
147
+ };
148
+
149
+ /**
150
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
151
+ * Returns a unique, ordered array of elements that can receive focus.
152
+ *
153
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
154
+ * @private
155
+ */
156
+ _getFocusableElements() {
157
+ // Use the imported utility function to get focusable elements
158
+ const elements = getFocusableElements(this.container);
159
+
160
+ // Return the elements found
161
+ return elements;
162
+ }
163
+
164
+ /**
165
+ * Moves focus to the first focusable element within the container.
166
+ * Useful for setting initial focus when activating the focus trap.
167
+ */
168
+ focusFirstElement() {
169
+ const focusables = this._getFocusableElements();
170
+ if (focusables.length) focusables[0].focus();
171
+ }
172
+
173
+ /**
174
+ * Moves focus to the last focusable element within the container.
175
+ * Useful for setting focus when deactivating or cycling focus in reverse.
176
+ */
177
+ focusLastElement() {
178
+ const focusables = this._getFocusableElements();
179
+ if (focusables.length) focusables[focusables.length - 1].focus();
180
+ }
181
+
182
+ /**
183
+ * Removes event listeners and attributes added by the focus trap.
184
+ * Call this method to clean up when the focus trap is no longer needed.
185
+ */
186
+ disconnect() {
187
+
188
+ if (this.container.hasAttribute('data-focus-trap-container')) {
189
+ this.container.removeAttribute('data-focus-trap-container');
190
+ }
191
+
192
+ this.container.removeEventListener('keydown', this._onKeydown);
193
+ }
194
+ }
@@ -0,0 +1 @@
1
+ export { FocusTrap } from './FocusTrap.mjs';
@@ -0,0 +1,168 @@
1
+ // Try this alternative approach
2
+ import { expect } from '@open-wc/testing';
3
+ import { html, render } from 'lit';
4
+ import sinon from 'sinon';
5
+ import { FocusTrap } from '../FocusTrap.mjs';
6
+
7
+ async function fixture(template) {
8
+ const wrapper = document.createElement('div');
9
+ render(template, wrapper);
10
+ document.body.appendChild(wrapper);
11
+ await new Promise(resolve => setTimeout(resolve, 0)); // Give browser time to render
12
+ return wrapper.firstElementChild;
13
+ }
14
+
15
+ describe('FocusTrap', () => {
16
+ let container;
17
+ let focusTrap;
18
+
19
+ beforeEach(async () => {
20
+ // Create a container with focusable elements
21
+ container = await fixture(html`
22
+ <div>
23
+ <button id="first">First</button>
24
+ <input id="middle" type="text" />
25
+ <a href="#" id="last">Last</a>
26
+ </div>
27
+ `);
28
+ });
29
+
30
+ afterEach(() => {
31
+ if (focusTrap) {
32
+ focusTrap.disconnect();
33
+ focusTrap = null;
34
+ }
35
+ });
36
+
37
+ describe('constructor', () => {
38
+ it('should create a new instance with a valid container', () => {
39
+ expect(() => new FocusTrap(container)).to.not.throw();
40
+ });
41
+
42
+ it('should throw an error if container is not a valid HTMLElement', () => {
43
+ expect(() => new FocusTrap(null)).to.throw('FocusTrap requires a valid HTMLElement');
44
+ expect(() => new FocusTrap({})).to.throw('FocusTrap requires a valid HTMLElement');
45
+ });
46
+ });
47
+
48
+ describe('initialization', () => {
49
+ it('should set attributes on the container', () => {
50
+ focusTrap = new FocusTrap(container);
51
+
52
+ if ('inert' in HTMLElement.prototype) {
53
+ expect(container.inert).to.be.false;
54
+ expect(container.hasAttribute('data-focus-trap-container')).to.be.true;
55
+ }
56
+ });
57
+ });
58
+
59
+ describe('focus management', () => {
60
+ it('should focus the first element when focusFirstElement is called', () => {
61
+ focusTrap = new FocusTrap(container);
62
+ const firstButton = container.querySelector('#first');
63
+
64
+ focusTrap.focusFirstElement();
65
+ expect(document.activeElement).to.equal(firstButton);
66
+ });
67
+
68
+ it('should focus the last element when focusLastElement is called', () => {
69
+ focusTrap = new FocusTrap(container);
70
+ const lastLink = container.querySelector('#last');
71
+
72
+ focusTrap.focusLastElement();
73
+ expect(document.activeElement).to.equal(lastLink);
74
+ });
75
+ });
76
+
77
+ describe('keyboard navigation', () => {
78
+ it('should trap focus and cycle to the first element when tabbing from the last element', async () => {
79
+ focusTrap = new FocusTrap(container);
80
+ const lastLink = container.querySelector('#last');
81
+ const firstButton = container.querySelector('#first');
82
+
83
+ // Create a spy on the focus method of the first button
84
+ const firstButtonFocusSpy = sinon.spy(firstButton, 'focus');
85
+
86
+ lastLink.focus();
87
+ expect(document.activeElement).to.equal(lastLink);
88
+
89
+ // Simulate Tab key press
90
+ const tabEvent = new KeyboardEvent('keydown', {
91
+ key: 'Tab',
92
+ bubbles: true,
93
+ composed: true
94
+ });
95
+
96
+ lastLink.dispatchEvent(tabEvent);
97
+
98
+ // Check if focus method was called on the first element
99
+ expect(firstButtonFocusSpy.calledOnce).to.be.true;
100
+ firstButtonFocusSpy.restore(); // Clean up the spy
101
+ });
102
+
103
+ it('should trap focus and cycle to the last element when shift-tabbing from the first element', async () => {
104
+ focusTrap = new FocusTrap(container);
105
+ const firstButton = container.querySelector('#first');
106
+ const lastLink = container.querySelector('#last');
107
+
108
+ // Create a spy on the focus method of the last link
109
+ const lastLinkFocusSpy = sinon.spy(lastLink, 'focus');
110
+
111
+ firstButton.focus();
112
+ expect(document.activeElement).to.equal(firstButton);
113
+
114
+ // Simulate Shift+Tab key press
115
+ const shiftTabEvent = new KeyboardEvent('keydown', {
116
+ key: 'Tab',
117
+ shiftKey: true,
118
+ bubbles: true,
119
+ cancelable: true
120
+ });
121
+
122
+ firstButton.dispatchEvent(shiftTabEvent);
123
+
124
+ // Check if focus method was called on the last element
125
+ expect(lastLinkFocusSpy.calledOnce).to.be.true;
126
+ lastLinkFocusSpy.restore(); // Clean up the spy
127
+ });
128
+ });
129
+
130
+ describe('cleanup', () => {
131
+ it('should remove attributes when disconnected', () => {
132
+ focusTrap = new FocusTrap(container);
133
+
134
+ focusTrap.disconnect();
135
+
136
+ expect(container.hasAttribute('data-focus-trap-container')).to.be.false;
137
+ });
138
+ });
139
+
140
+ describe('shadow DOM support', () => {
141
+ it('should work with shadow DOM elements', async () => {
142
+ // Create a custom element with shadow DOM
143
+ const shadowHost = await fixture(html`
144
+ <div id="shadow-host"></div>
145
+ `);
146
+
147
+ // Create shadow root
148
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
149
+
150
+ // Add focusable elements to shadow DOM
151
+ shadowRoot.innerHTML = `
152
+ <button id="shadow-first">First</button>
153
+ <input id="shadow-middle" type="text" />
154
+ <a href="#" id="shadow-last">Last</a>
155
+ `;
156
+
157
+ focusTrap = new FocusTrap(shadowHost);
158
+ const firstButton = shadowRoot.querySelector('#shadow-first');
159
+ const lastLink = shadowRoot.querySelector('#shadow-last');
160
+
161
+ focusTrap.focusFirstElement();
162
+ expect(shadowRoot.activeElement).to.equal(firstButton);
163
+
164
+ focusTrap.focusLastElement();
165
+ expect(shadowRoot.activeElement).to.equal(lastLink);
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,157 @@
1
+ // Selectors for focusable elements
2
+ export const FOCUSABLE_SELECTORS = [
3
+ 'a[href]',
4
+ 'button:not([disabled])',
5
+ 'textarea:not([disabled])',
6
+ 'input:not([disabled])',
7
+ 'select:not([disabled])',
8
+ '[role="tab"]:not([disabled])',
9
+ '[role="link"]:not([disabled])',
10
+ '[role="button"]:not([disabled])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ '[contenteditable]:not([contenteditable="false"])'
13
+ ];
14
+
15
+ // List of custom components that are known to be focusable
16
+ export const FOCUSABLE_COMPONENTS = [
17
+ 'auro-checkbox',
18
+ 'auro-radio',
19
+ 'auro-dropdown',
20
+ 'auro-button',
21
+ 'auro-combobox',
22
+ 'auro-input',
23
+ 'auro-counter',
24
+ // 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
25
+ 'auro-select',
26
+ 'auro-datepicker',
27
+ 'auro-hyperlink',
28
+ 'auro-accordion',
29
+ ];
30
+
31
+ /**
32
+ * Determines if a given element is a custom focusable component.
33
+ * Returns true if the element matches a known focusable component and is not disabled.
34
+ *
35
+ * @param {HTMLElement} element The element to check for focusability.
36
+ * @returns {boolean} True if the element is a focusable custom component, false otherwise.
37
+ */
38
+ export function isFocusableComponent(element) {
39
+ const componentName = element.tagName.toLowerCase();
40
+
41
+ // Guard Clause: Element is a focusable component
42
+ if (!FOCUSABLE_COMPONENTS.some((name) => element.hasAttribute(name) || componentName === name)) return false;
43
+
44
+ // Guard Clause: Element is not disabled
45
+ if (element.hasAttribute('disabled')) return false;
46
+
47
+ // Guard Clause: The element is a hyperlink and has no href attribute
48
+ if (componentName.match("hyperlink") && !element.hasAttribute('href')) return false;
49
+
50
+ // If all guard clauses pass, the element is a focusable component
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
56
+ * Returns a unique, ordered array of elements that can receive focus.
57
+ * Also sorts elements with tabindex first, preserving their order.
58
+ *
59
+ * @param {HTMLElement} container The container to search within
60
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
61
+ */
62
+ export function getFocusableElements(container) {
63
+ // Get elements in DOM order by walking the tree
64
+ const orderedFocusableElements = [];
65
+
66
+ // Define a recursive function to collect focusable elements in DOM order
67
+ const collectFocusableElements = (root) => {
68
+ // Check if current element is focusable
69
+ if (root.nodeType === Node.ELEMENT_NODE) {
70
+ // Check if this is a custom component that is focusable
71
+ const isComponentFocusable = isFocusableComponent(root);
72
+
73
+ if (isComponentFocusable) {
74
+ // Add the component itself as a focusable element and don't traverse its shadow DOM
75
+ orderedFocusableElements.push(root);
76
+ return; // Skip traversing inside this component
77
+ }
78
+
79
+ // Check if the element itself matches any selector
80
+ for (const selector of FOCUSABLE_SELECTORS) {
81
+ if (root.matches?.(selector)) {
82
+ orderedFocusableElements.push(root);
83
+ break; // Once we know it's focusable, no need to check other selectors
84
+ }
85
+ }
86
+
87
+ // Process shadow DOM only for non-Auro components
88
+ if (root.shadowRoot) {
89
+ // Process shadow DOM children in order
90
+ if (root.shadowRoot.children) {
91
+ Array.from(root.shadowRoot.children).forEach(child => {
92
+ collectFocusableElements(child);
93
+ });
94
+ }
95
+ }
96
+
97
+ // Process slots and their assigned nodes in order
98
+ if (root.tagName === 'SLOT') {
99
+ const assignedNodes = root.assignedNodes({ flatten: true });
100
+ for (const node of assignedNodes) {
101
+ collectFocusableElements(node);
102
+ }
103
+ } else {
104
+ // Process light DOM children in order
105
+ if (root.children) {
106
+ Array.from(root.children).forEach(child => {
107
+ collectFocusableElements(child);
108
+ });
109
+ }
110
+ }
111
+ }
112
+ };
113
+
114
+ // Start the traversal from the container
115
+ collectFocusableElements(container);
116
+
117
+ // Remove duplicates that might have been collected through different paths
118
+ // while preserving order
119
+ const uniqueElements = [];
120
+ const seen = new Set();
121
+
122
+ for (const element of orderedFocusableElements) {
123
+ if (!seen.has(element)) {
124
+ seen.add(element);
125
+ uniqueElements.push(element);
126
+ }
127
+ }
128
+
129
+ // Move tab-indexed elements to the front while preserving their order
130
+ // This ensures that elements with tabindex are prioritized in the focus order
131
+
132
+ // First extract elements with tabindex
133
+ const elementsWithTabindex = uniqueElements.filter(el => el.hasAttribute('tabindex') && (parseInt(el.getAttribute('tabindex')) ?? -1) > 0);
134
+
135
+ // Sort these elements by their tabindex value
136
+ elementsWithTabindex.sort((a, b) => {
137
+ return parseInt(a.getAttribute('tabindex'), 10) - parseInt(b.getAttribute('tabindex'), 10);
138
+ });
139
+
140
+ // Elements without tabindex (preserving their original order)
141
+ const elementsWithoutTabindex = uniqueElements.filter(el =>
142
+
143
+ // Elements without tabindex
144
+ !el.hasAttribute('tabindex') ||
145
+
146
+ // Preserve tab order of elements with tabindex of 0
147
+ (parseInt(el.getAttribute('tabindex')) ?? -1) === 0
148
+ );
149
+
150
+ // Combine both arrays with tabindex elements first
151
+ const tabIndexedUniqueElements = [
152
+ ...elementsWithTabindex,
153
+ ...elementsWithoutTabindex
154
+ ];
155
+
156
+ return tabIndexedUniqueElements;
157
+ }
@@ -0,0 +1 @@
1
+ export { isFocusableComponent, getFocusableElements, FOCUSABLE_SELECTORS, FOCUSABLE_COMPONENTS } from './Focusables.mjs';