diff --git a/packages/lwc-engine/src/faux-shadow/focus.ts b/packages/lwc-engine/src/faux-shadow/focus.ts index a6ac569b03..1d1135ba75 100644 --- a/packages/lwc-engine/src/faux-shadow/focus.ts +++ b/packages/lwc-engine/src/faux-shadow/focus.ts @@ -62,7 +62,7 @@ function getFocusableSegments(host: HTMLElement): QuerySegments { const all = documentQuerySelectorAll.call(document, PossibleFocusableElementQuery); const inner = querySelectorAll.call(host, PossibleFocusableElementQuery); if (process.env.NODE_ENV !== 'production') { - assert.invariant(inner.length > 0, `When focusin event is received, there has to be a focusable target at least.`); + assert.invariant(inner.length > 0 || (tabIndexGetter.call(host) === 0 && isDelegatingFocus(host)), `When focusin event is received, there has to be a focusable target at least.`); assert.invariant(tabIndexGetter.call(host) === -1 || isDelegatingFocus(host), `The focusin event is only relevant when the tabIndex property is -1 on the host.`); } const firstChild = inner[0]; @@ -72,8 +72,11 @@ function getFocusableSegments(host: HTMLElement): QuerySegments { // Host element can show up in our "previous" section if its tabindex is 0 // We want to filter that out here const firstChildIndex = (hostIndex > -1) ? hostIndex : ArrayIndexOf.call(all, firstChild); + + // Account for an empty inner list + const lastChildIndex = inner.length === 0 ? firstChildIndex + 1 : ArrayIndexOf.call(all, lastChild) + 1; const prev = ArraySlice.call(all, 0, firstChildIndex); - const next = ArraySlice.call(all, ArrayIndexOf.call(all, lastChild) + 1); + const next = ArraySlice.call(all, lastChildIndex); return { prev, inner, @@ -158,8 +161,12 @@ function isLastFocusableChild(target: EventTarget, segments: QuerySegments): boo function handleDelegateFocusFocusIn(host: HTMLElement, target: HTMLElement, segments: QuerySegments, position: number) { if (position === 1) { // probably tabbing into element - const { inner } = segments; - inner[0].focus(); + const first = getFirstFocusableMatch(segments.inner); + if (first) { + first.focus(); + } else { + focusOnNextOrBlur(target, segments); + } return; } else if (host === target) { // Shift tabbed back to the host focusOnPrevOrBlur(host, segments); @@ -188,7 +195,6 @@ function keyboardFocusInHandler(event: FocusEvent) { const isFirstFocusableChildReceivingFocus = isFirstFocusableChild(target, segments); const isLastFocusableChildReceivingFocus = isLastFocusableChild(target, segments); - // Determine where the focus is coming from (Tab or Shift+Tab) const post = relatedTargetPosition(host as HTMLElement, relatedTarget as HTMLElement); @@ -202,7 +208,7 @@ function keyboardFocusInHandler(event: FocusEvent) { // The host element itself is receiving focus // Handle delegates focus with tabIndex=0 if (target === host) { - handleDelegateFocusFocusIn(host as HTMLElement, target as HTMLElement, segments, post) + handleDelegateFocusFocusIn(host as HTMLElement, target as HTMLElement, segments, post); return; } diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.spec.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.spec.js new file mode 100644 index 0000000000..0a70bdf7c0 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.spec.js @@ -0,0 +1,19 @@ +const assert = require('assert'); +describe('Delegate focus with tabindex 0, no tabbable elements, and no tabbable elements after', () => { + const URL = 'http://localhost:4567/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements'; + let element; + + before(() => { + browser.url(URL); + }); + + it('should correctly have no activeelement', function () { + browser.keys(['Tab']); + browser.keys(['Tab']); + let active = browser.execute(function () { + return document.activeElement; + }); + + assert.deepEqual(active.getTagName().toLowerCase(), 'body'); + }); +}); diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.html b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.html new file mode 100644 index 0000000000..7ec096d9d4 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.js new file mode 100644 index 0000000000..5f02c1a35d --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static delegatesFocus = true; +} diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.html b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.html new file mode 100644 index 0000000000..6e82dee59e --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.html @@ -0,0 +1,4 @@ + diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.js new file mode 100644 index 0000000000..dcb0c41530 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements/delegates-focus-tab-index-zero-no-focusable-elements-no-after-elements.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement { + +} diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.spec.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.spec.js new file mode 100644 index 0000000000..2c7461c22a --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.spec.js @@ -0,0 +1,27 @@ +const assert = require('assert'); +describe('Delegate focus with tabindex 0 and no tabbable elements', () => { + const URL = 'http://localhost:4567/delegates-focus-tab-index-zero-no-focusable-elements'; + let element; + + before(() => { + browser.url(URL); + }); + + it('should correctly skip the custom element', function () { + browser.keys(['Tab']); + browser.keys(['Tab']); + let active = browser.execute(function () { + return document.activeElement.shadowRoot.activeElement; + }); + + assert.deepEqual(active.getText(), 'second button') + + browser.keys(['Shift', 'Tab']); + + active = browser.execute(function () { + return document.activeElement.shadowRoot.activeElement; + }); + + assert.deepEqual(active.getText(), 'first button'); + }); +}); diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.html b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.html new file mode 100644 index 0000000000..7ec096d9d4 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.js new file mode 100644 index 0000000000..5f02c1a35d --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static delegatesFocus = true; +} diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.html b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.html new file mode 100644 index 0000000000..b8d3be8ad0 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.html @@ -0,0 +1,5 @@ + diff --git a/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.js b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.js new file mode 100644 index 0000000000..dcb0c41530 --- /dev/null +++ b/packages/lwc-integration/src/components/accessibility/test-delegates-focus-tab-index-zero-no-focusable-elements/integration/delegates-focus-tab-index-zero-no-focusable-elements/delegates-focus-tab-index-zero-no-focusable-elements.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement { + +}