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 - + Button Menu Item Menu Item Menu 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