diff --git a/BREAKING.md b/BREAKING.md
index f75b27540c..a882d98d2b 100644
--- a/BREAKING.md
+++ b/BREAKING.md
@@ -22,6 +22,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Content](#version-8x-content)
- [Datetime](#version-8x-datetime)
- [Input](#version-8x-input)
+ - [Item](#version-8x-item)
- [Modal](#version-8x-modal)
- [Nav](#version-8x-nav)
- [Picker](#version-8x-picker)
@@ -168,6 +169,10 @@ For more information on the dynamic font, refer to the [Dynamic Font Scaling doc
- `accept` has been removed from the `ion-input` component. This was previously used in conjunction with the `type="file"`. However, the `file` value for `type` is not a valid value in Ionic Framework.
- The `legacy` property and support for the legacy syntax, which involved placing an `ion-input` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy input syntax, refer to the [Input documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).
+
Item
+
+- Item no longer automatically delegates focus to the first focusable element. While most developers should not need to make any changes to account for this update, usages of `ion-item` with interactive elements such as form controls (inputs, textareas, etc) should be evaluated to verify that interactions still work as expected.
+
Modal
- Detection for Capacitor <= 2 with applying status bar styles has been removed. Developers should ensure they are using Capacitor 3 or later when using the card modal presentation.
diff --git a/core/src/components/input/test/item/input.e2e.ts b/core/src/components/input/test/item/input.e2e.ts
index e367aea697..710e2f00b1 100644
--- a/core/src/components/input/test/item/input.e2e.ts
+++ b/core/src/components/input/test/item/input.e2e.ts
@@ -45,3 +45,30 @@ configs().forEach(({ title, screenshot, config }) => {
});
});
});
+
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('input: item functionality'), () => {
+ test('clicking padded space within item should focus the input', async ({ page }) => {
+ await page.setContent(
+ `
+
+
+
+ `,
+ config
+ );
+ const itemNative = page.locator('.item-native');
+ const input = page.locator('ion-input input');
+
+ // Clicks the padded space within the item
+ await itemNative.click({
+ position: {
+ x: 5,
+ y: 5,
+ },
+ });
+
+ await expect(input).toBeFocused();
+ });
+ });
+});
diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss
index 311ea1c020..214a7dc667 100644
--- a/core/src/components/item/item.scss
+++ b/core/src/components/item/item.scss
@@ -176,7 +176,8 @@
// Item: Interactive
// --------------------------------------------------
-:host(.item-has-interactive-control) {
+// Inputs and textareas do not need the cursor, but other components like checkbox or toggle do.
+:host(.item-control-needs-pointer-cursor) {
cursor: pointer;
}
diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx
index daec41d83d..022c78d40a 100644
--- a/core/src/components/item/item.tsx
+++ b/core/src/components/item/item.tsx
@@ -32,9 +32,7 @@ import type { CounterFormatter } from './item-interface';
ios: 'item.ios.scss',
md: 'item.md.scss',
},
- shadow: {
- delegatesFocus: true,
- },
+ shadow: true,
})
export class Item implements ComponentInterface, AnchorInterface, ButtonInterface {
private labelColorStyles = {};
@@ -359,7 +357,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
private getFirstInteractive() {
const controls = this.el.querySelectorAll(
- 'ion-toggle:not([disabled]), ion-checkbox:not([disabled]), ion-radio:not([disabled]), ion-select:not([disabled])'
+ 'ion-toggle:not([disabled]), ion-checkbox:not([disabled]), ion-radio:not([disabled]), ion-select:not([disabled]), ion-input:not([disabled]), ion-textarea:not([disabled])'
);
return controls[0];
}
@@ -425,7 +423,16 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
*/
const clickedWithinShadowRoot = this.el.shadowRoot!.contains(target);
if (clickedWithinShadowRoot) {
- firstInteractive.click();
+ /**
+ * For input/textarea clicking the padding should focus the
+ * text field (thus making it editable). For everything else,
+ * we want to click the control so it activates.
+ */
+ if (firstInteractive.tagName === 'ION-INPUT' || firstInteractive.tagName === 'ION-TEXTAREA') {
+ (firstInteractive as HTMLIonInputElement | HTMLIonTextareaElement).setFocus();
+ } else {
+ firstInteractive.click();
+ }
}
}
}
@@ -441,6 +448,13 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
const fillValue = fill || 'none';
const inList = hostContext('ion-list', this.el) && !hostContext('ion-radio-group', this.el);
+ /**
+ * Inputs and textareas do not need to show a cursor pointer.
+ * However, other form controls such as checkboxes and radios do.
+ */
+ const firstInteractiveNeedsPointerCursor =
+ firstInteractive !== undefined && !['ION-INPUT', 'ION-TEXTAREA'].includes(firstInteractive.tagName);
+
return (
(focusableQueryString));
- const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
-
- if (lastInput) {
- lastInput.focus();
- } else {
- el.focus();
- }
- }
-
private trapKeyboardFocus(ev: Event, doc: Document) {
const target = ev.target as HTMLElement | null;
+
if (!target) {
return;
}
@@ -439,13 +416,15 @@ export class Menu implements ComponentInterface, MenuI {
* Wrap the focus to either the first or last element.
*/
+ const { el } = this;
+
/**
* Once we call `focusFirstDescendant`, another focus event
* will fire, which will cause `lastFocus` to be updated
* before we can run the code after that. We cache the value
* here to avoid that.
*/
- this.focusFirstDescendant();
+ focusFirstDescendant(el);
/**
* If the cached last focused element is the same as the now-
@@ -454,7 +433,7 @@ export class Menu implements ComponentInterface, MenuI {
* last descendant.
*/
if (this.lastFocus === doc.activeElement) {
- this.focusLastDescendant();
+ focusLastDescendant(el);
}
}
}
diff --git a/core/src/components/menu/test/basic/index.html b/core/src/components/menu/test/basic/index.html
index af0074a4cb..c75e95b0dd 100644
--- a/core/src/components/menu/test/basic/index.html
+++ b/core/src/components/menu/test/basic/index.html
@@ -35,9 +35,7 @@
-
- Button
-
+ ButtonMenu ItemMenu ItemMenu Item
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Chrome-linux.png
index 0c09894f0c..994f88587b 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Firefox-linux.png
index 6f5606f9c7..fc97228d5d 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Safari-linux.png
index c24598fc72..9e463f27fc 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-doc-dir-toggled-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Chrome-linux.png
index b573eb665d..f69f8326a3 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Firefox-linux.png
index dc38336fde..b52501ccbf 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Safari-linux.png
index 5fb32159e8..f53ee50718 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-side-toggled-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Chrome-linux.png
index eea91aa918..56a9c4b99e 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Firefox-linux.png
index 37ed3ffa41..dcb37c772c 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Safari-linux.png
index 7f63395714..b7e0c4fad6 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Chrome-linux.png
index 4950f34553..2d986b4c2f 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Firefox-linux.png
index 142c8b828e..f8ef3f8b1f 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Safari-linux.png
index 26e354ddde..047405a0b9 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Chrome-linux.png
index a1280a4c0f..927be1b666 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Firefox-linux.png
index 176d126f5d..c83e056d68 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Safari-linux.png
index ceae61460c..deb8997662 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Chrome-linux.png
index 0c09894f0c..994f88587b 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Firefox-linux.png
index 6f5606f9c7..fc97228d5d 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Safari-linux.png
index c24598fc72..9e463f27fc 100644
Binary files a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx
index 46f922b834..d765bc3edf 100644
--- a/core/src/components/popover/popover.tsx
+++ b/core/src/components/popover/popover.tsx
@@ -1,18 +1,11 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
+import { focusFirstDescendant } from '@utils/focus-trap';
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
import { addEventListener, raf, hasLazyBuild } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
-import {
- BACKDROP,
- dismiss,
- eventMethod,
- focusFirstDescendant,
- prepareOverlay,
- present,
- setOverlayId,
-} from '@utils/overlays';
+import { BACKDROP, dismiss, eventMethod, prepareOverlay, present, setOverlayId } from '@utils/overlays';
import { isPlatform } from '@utils/platform';
import { getClassMap } from '@utils/theme';
import { deepReady, waitForMount } from '@utils/transition';
@@ -512,7 +505,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
* descendant inside of the popover.
*/
if (this.focusDescendantOnPresent) {
- focusFirstDescendant(this.el, this.el);
+ focusFirstDescendant(el);
}
unlock();
diff --git a/core/src/components/textarea/test/item/textarea.e2e.ts b/core/src/components/textarea/test/item/textarea.e2e.ts
index e0f1b362fd..ed5daeec44 100644
--- a/core/src/components/textarea/test/item/textarea.e2e.ts
+++ b/core/src/components/textarea/test/item/textarea.e2e.ts
@@ -45,3 +45,30 @@ configs().forEach(({ title, screenshot, config }) => {
});
});
});
+
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('textarea: item functionality'), () => {
+ test('clicking padded space within item should focus the textarea', async ({ page }) => {
+ await page.setContent(
+ `
+
+
+
+ `,
+ config
+ );
+ const itemNative = page.locator('.item-native');
+ const textarea = page.locator('ion-textarea textarea');
+
+ // Clicks the padded space within the item
+ await itemNative.click({
+ position: {
+ x: 5,
+ y: 5,
+ },
+ });
+
+ await expect(textarea).toBeFocused();
+ });
+ });
+});
diff --git a/core/src/utils/focus-trap.ts b/core/src/utils/focus-trap.ts
new file mode 100644
index 0000000000..1ac3d351ff
--- /dev/null
+++ b/core/src/utils/focus-trap.ts
@@ -0,0 +1,86 @@
+import { focusVisibleElement } from '@utils/helpers';
+
+/**
+ * This query string selects elements that
+ * are eligible to receive focus. We select
+ * interactive elements that meet the following
+ * criteria:
+ * 1. Element does not have a negative tabindex
+ * 2. Element does not have `hidden`
+ * 3. Element does not have `disabled` for non-Ionic components.
+ * 4. Element does not have `disabled` or `disabled="true"` for Ionic components.
+ * Note: We need this distinction because `disabled="false"` is
+ * valid usage for the disabled property on ion-button.
+ */
+export const focusableQueryString =
+ '[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
+
+/**
+ * Focuses the first descendant in a context
+ * that can receive focus. If none exists,
+ * a fallback element will be focused.
+ * This fallback is typically an ancestor
+ * container such as a menu or overlay so focus does not
+ * leave the container we are trying to trap focus in.
+ *
+ * If no fallback is specified then we focus the container itself.
+ */
+export const focusFirstDescendant = (ref: R, fallbackElement?: T) => {
+ const firstInput = ref.querySelector(focusableQueryString);
+
+ focusElementInContext(firstInput, fallbackElement ?? ref);
+};
+
+/**
+ * Focuses the last descendant in a context
+ * that can receive focus. If none exists,
+ * a fallback element will be focused.
+ * This fallback is typically an ancestor
+ * container such as a menu or overlay so focus does not
+ * leave the container we are trying to trap focus in.
+ *
+ * If no fallback is specified then we focus the container itself.
+ */
+export const focusLastDescendant = (ref: R, fallbackElement?: T) => {
+ const inputs = Array.from(ref.querySelectorAll(focusableQueryString));
+ const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
+
+ focusElementInContext(lastInput, fallbackElement ?? ref);
+};
+
+/**
+ * Focuses a particular element in a context. If the element
+ * doesn't have anything focusable associated with it then
+ * a fallback element will be focused.
+ *
+ * This fallback is typically an ancestor
+ * container such as a menu or overlay so focus does not
+ * leave the container we are trying to trap focus in.
+ * This should be used instead of the focus() method
+ * on most elements because the focusable element
+ * may not be the host element.
+ *
+ * For example, if an ion-button should be focused
+ * then we should actually focus the native