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 @@
+
+ No focusable elements
+
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 @@
+
+ No focusable elements
+
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 {
+
+}