diff --git a/core/src/components/checkbox/checkbox.ios.scss b/core/src/components/checkbox/checkbox.ios.scss index 7c7bcafc9fd..6e8a512936c 100644 --- a/core/src/components/checkbox/checkbox.ios.scss +++ b/core/src/components/checkbox/checkbox.ios.scss @@ -31,3 +31,26 @@ :host(.checkbox-disabled) { opacity: $checkbox-ios-disabled-opacity; } + +// iOS Checkbox: Keyboard Focus +// ----------------------------------------- + +:host(.ion-focused) .native-wrapper::after { + @include border-radius(50%); + + display: block; + position: absolute; + + width: 36px; + height: 36px; + + transform: translate(-50%, -50%); + + background: $checkbox-ios-background-color-focused; + + content: ""; + opacity: 0.2; + + inset-block-start: 50%; + inset-inline-start: 50%; +} diff --git a/core/src/components/checkbox/checkbox.ios.vars.scss b/core/src/components/checkbox/checkbox.ios.vars.scss index 78605a85f88..c41efe339a7 100644 --- a/core/src/components/checkbox/checkbox.ios.vars.scss +++ b/core/src/components/checkbox/checkbox.ios.vars.scss @@ -28,3 +28,6 @@ $checkbox-ios-disabled-opacity: $form-control-ios-disabled-opacity; /// @prop - Checkmark width of the checkbox icon $checkbox-ios-icon-checkmark-width: 1.5px; + +/// @prop - Background color of focus indicator for checkbox when focused +$checkbox-ios-background-color-focused: ion-color(primary, tint); diff --git a/core/src/components/checkbox/checkbox.md.scss b/core/src/components/checkbox/checkbox.md.scss index e87f7bbf1a0..6f22e61204d 100644 --- a/core/src/components/checkbox/checkbox.md.scss +++ b/core/src/components/checkbox/checkbox.md.scss @@ -51,3 +51,26 @@ :host(.checkbox-disabled) .native-wrapper { opacity: $checkbox-md-icon-disabled-opacity; } + +// Material Design Checkbox: Keyboard Focus +// ----------------------------------------- + +:host(.ion-focused) .native-wrapper::after { + @include border-radius(50%); + + display: block; + position: absolute; + + width: 36px; + height: 36px; + + transform: translate(-50%, -50%); + + background: $checkbox-md-background-color-focused; + + content: ""; + opacity: 0.2; + + inset-block-start: 50%; + inset-inline-start: 50%; +} diff --git a/core/src/components/checkbox/checkbox.md.vars.scss b/core/src/components/checkbox/checkbox.md.vars.scss index 445d50b6a6e..0b9e5600a14 100644 --- a/core/src/components/checkbox/checkbox.md.vars.scss +++ b/core/src/components/checkbox/checkbox.md.vars.scss @@ -30,6 +30,9 @@ $checkbox-md-transition-duration: 180ms; /// @prop - Transition easing of the checkbox $checkbox-md-transition-easing: cubic-bezier(.4, 0, .2, 1); +/// @prop - Background color of focus indicator for checkbox when focused +$checkbox-md-background-color-focused: ion-color(primary, tint); + /// @prop - Opacity of the disabled checkbox /// This value is used because the checkbox color is set to /// `rgb(0, 0, 0, 0.60)` when enabled and we need it to be diff --git a/core/src/components/checkbox/checkbox.scss b/core/src/components/checkbox/checkbox.scss index 438add273d0..bd248d9ac0e 100644 --- a/core/src/components/checkbox/checkbox.scss +++ b/core/src/components/checkbox/checkbox.scss @@ -119,9 +119,17 @@ input { display: none; } +// The host is focusable, so the browser draws a native focus ring on it. +// Suppress it in favor of the mode-specific keyboard focus indicator. +:host(:focus) { + outline: none; +} + .native-wrapper { display: flex; + position: relative; + align-items: center; } diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 99cb13c474a..c0b56da4ffb 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Build, Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Method, Prop, State, forceUpdate, h } from '@stencil/core'; import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; @@ -37,6 +37,7 @@ export class Checkbox implements ComponentInterface { private errorTextId = `${this.inputId}-error-text`; private inheritedAttributes: Attributes = {}; private validationObserver?: MutationObserver; + private itemFocusObserver?: MutationObserver; @Element() el!: HTMLIonCheckboxElement; @@ -199,6 +200,22 @@ export class Checkbox implements ComponentInterface { // Always set initial state this.isInvalid = checkInvalidState(el); this.hasLabelContent = this.el.textContent !== ''; + + // The item toggles `item-multiple-inputs` after this control renders and as + // inputs are added or removed. Re-render when it flips so the focus + // indicator stays in sync. + const item = el.closest('ion-item'); + if (item && Build.isBrowser && typeof MutationObserver !== 'undefined') { + let wasMultipleInputs = item.classList.contains('item-multiple-inputs'); + this.itemFocusObserver = new MutationObserver(() => { + const isMultipleInputs = item.classList.contains('item-multiple-inputs'); + if (isMultipleInputs !== wasMultipleInputs) { + wasMultipleInputs = isMultipleInputs; + forceUpdate(this); + } + }); + this.itemFocusObserver.observe(item, { attributes: true, attributeFilter: ['class'] }); + } } componentWillLoad() { @@ -210,11 +227,15 @@ export class Checkbox implements ComponentInterface { } disconnectedCallback() { - // Clean up validation observer to prevent memory leaks. + // Clean up observers to prevent memory leaks. if (this.validationObserver) { this.validationObserver.disconnect(); this.validationObserver = undefined; } + if (this.itemFocusObserver) { + this.itemFocusObserver.disconnect(); + this.itemFocusObserver = undefined; + } } /** @internal */ @@ -338,6 +359,8 @@ export class Checkbox implements ComponentInterface { } = this; const mode = getIonMode(this); const path = getSVGPath(mode, indeterminate); + const inItem = hostContext('ion-item', el); + const inMultipleInputsItem = hostContext('ion-item.item-multiple-inputs', el); renderHiddenInput(true, el, name, checked ? value : '', disabled); @@ -360,11 +383,15 @@ export class Checkbox implements ComponentInterface { onClick={this.onClick} class={createColorClasses(color, { [mode]: true, - 'in-item': hostContext('ion-item', el), + 'in-item': inItem, 'checkbox-checked': checked, 'checkbox-disabled': disabled, 'checkbox-indeterminate': indeterminate, interactive: true, + // Focus styling should not apply when the checkbox is in an item, + // since the item handles the focus indicator instead. The exception + // is a multi-input item, which has no single indicator of its own. + 'ion-focusable': !inItem || inMultipleInputsItem, [`checkbox-justify-${justify}`]: justify !== undefined, [`checkbox-alignment-${alignment}`]: alignment !== undefined, [`checkbox-label-placement-${labelPlacement}`]: true, diff --git a/core/src/components/checkbox/test/a11y/checkbox.e2e.ts b/core/src/components/checkbox/test/a11y/checkbox.e2e.ts index 6d0b6645ae5..e1e63f65ef3 100644 --- a/core/src/components/checkbox/test/a11y/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/a11y/checkbox.e2e.ts @@ -25,6 +25,70 @@ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, }); }); +/** + * These tests assert the `ion-focusable` gating class the component controls, + * not the rendered focus ring. `ion-focused` is not asserted directly because + * it depends on keyboard-mode detection, which is unreliable on WebKit. The + * gating logic does not vary across modes. + */ +configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, config }) => { + test.describe(title('checkbox: focus indicator'), () => { + test('standalone checkbox should be focusable', async ({ page }) => { + await page.setContent( + ` + + Checkbox + + `, + config + ); + + const checkbox = page.locator('ion-checkbox'); + await expect(checkbox).toHaveClass(/ion-focusable/); + }); + + test('checkbox in a single-input item should not show its own focus indicator', async ({ page }) => { + await page.setContent( + ` + + + Checkbox + + + `, + config + ); + + // The item owns the focus indicator for single-input items, so the + // checkbox must not become focusable itself. + const checkbox = page.locator('ion-checkbox'); + const item = page.locator('ion-item'); + await expect(checkbox).not.toHaveClass(/ion-focusable/); + await expect(item).toHaveClass(/ion-focusable/); + }); + + test('checkbox in a multi-input item should be focusable', async ({ page }) => { + await page.setContent( + ` + + + Checkbox 1 + Checkbox 2 + + + `, + config + ); + + // Multi-input items do not draw a single focus indicator, so each control + // must be able to show its own. + const checkboxes = page.locator('ion-checkbox'); + await expect(checkboxes.nth(0)).toHaveClass(/ion-focusable/); + await expect(checkboxes.nth(1)).toHaveClass(/ion-focusable/); + }); + }); +}); + configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => { test.describe(title('checkbox: a11y'), () => { test.describe(title('checkbox: font scaling'), () => { diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts index 733f229e4b6..b2b8fd80022 100644 --- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts @@ -47,7 +47,7 @@ configs().forEach(({ title, screenshot, config }) => { /** * This behavior does not vary across modes/directions */ -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('checkbox: ionChange'), () => { test('should fire ionChange when interacting with checkbox', async ({ page }) => { await page.setContent( @@ -138,48 +138,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c }); test.describe(title('checkbox: ionFocus'), () => { - test('should not have visual regressions', async ({ page, pageUtils }) => { - await page.setContent( - ` - - -
- Unchecked -
- `, - config - ); - - await pageUtils.pressKeys('Tab'); - - const container = page.locator('#container'); - - await expect(container).toHaveScreenshot(screenshot(`checkbox-focus`)); - }); - - test('should not have visual regressions when interacting with checkbox in item', async ({ page, pageUtils }) => { - await page.setContent( - ` - - Unchecked - - `, - config - ); - - // Test focus with keyboard navigation - await pageUtils.pressKeys('Tab'); - - const item = page.locator('ion-item'); - - await expect(item).toHaveScreenshot(screenshot(`checkbox-in-item-focus`)); - }); - test('should fire ionFocus when checkbox is focused', async ({ page, pageUtils }) => { await page.setContent( ` @@ -328,3 +286,130 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c }); }); }); + +/** + * The focus indicator UI differs between iOS and MD, so these visual tests run + * in both modes. Direction does not affect the indicator, so only LTR is run. + */ +configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('checkbox: focus visual'), () => { + test('should render focus indicator when unchecked', async ({ page, pageUtils }) => { + // `ion-app` is required so `startFocusVisible` runs and applies the + // `ion-focused` class on keyboard focus, which drives the focus indicator. + await page.setContent( + ` + + + +
+ Unchecked +
+
+ `, + config + ); + + const checkbox = page.locator('ion-checkbox'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(checkbox).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`checkbox-focus`)); + }); + + test('should render focus indicator when checked', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + +
+ Checked +
+
+ `, + config + ); + + const checkbox = page.locator('ion-checkbox'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(checkbox).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`checkbox-focus-checked`)); + }); + + test('should render focus indicator in an item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + Unchecked + + `, + config + ); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`checkbox-in-item-focus`)); + }); + + test('should render focus indicator for a checkbox in a multi-input item', async ({ page, pageUtils }) => { + // A multi-input item cannot draw a single focus indicator, so each + // checkbox shows its own. Focus the first one and confirm its indicator + // renders. + await page.setContent( + ` + + + Checkbox 1 + Checkbox 2 + + + `, + config + ); + + const checkbox = page.locator('ion-checkbox').first(); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(checkbox).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`checkbox-multiple-in-item-focus`)); + }); + }); +}); diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..9e8caf8ee43 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a24d128c04b Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5f268bb2df0 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..769ebd955ad Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b794065ad98 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e96f191e15d Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-checked-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png index 45885364c12..86c807e630c 100644 Binary files a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png index 305f4d89e1b..e328672a417 100644 Binary files a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png index 637fa09bb1e..fdfb755ea67 100644 Binary files a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..112e7ada86f Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b3cc4169e01 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5c86d926c0e Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png index 2c0dcdc0baa..b7c449e6f9e 100644 Binary files a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png index 9dc5aed80d8..b465b40ad93 100644 Binary files a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..40d72cf339f Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..3101f22b855 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..247aba9eb13 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..3d9e7f4f7a8 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d7fc1740783 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..987bc5d9b19 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e919e470843 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d5a636afb4a Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..393c0e0bcd6 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts b/core/src/components/checkbox/test/item/checkbox.e2e.ts index 341839739b1..b043960c9a0 100644 --- a/core/src/components/checkbox/test/item/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/item/checkbox.e2e.ts @@ -126,6 +126,24 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { await expect(list).toHaveScreenshot(screenshot(`checkbox-stacked-label-in-item`)); }); }); + + test.describe(title('checkbox: multiple inputs in item'), () => { + test('should not have visual regressions with multiple checkboxes in an item', async ({ page }) => { + await page.setContent( + ` + + + Checkbox 1 + Checkbox 2 + + + `, + config + ); + const list = page.locator('ion-list'); + await expect(list).toHaveScreenshot(screenshot(`checkbox-multiple-in-item`)); + }); + }); }); configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..de660abf936 Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f5f925dabe6 Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..40be000b83a Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..82895bd4516 Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..723a11e9d84 Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c5297bc2d64 Binary files /dev/null and b/core/src/components/checkbox/test/item/checkbox.e2e.ts-snapshots/checkbox-multiple-in-item-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/item/index.html b/core/src/components/checkbox/test/item/index.html index 2548cbfaad0..60582e73abc 100644 --- a/core/src/components/checkbox/test/item/index.html +++ b/core/src/components/checkbox/test/item/index.html @@ -15,7 +15,7 @@ + + +
+ + Unchecked + +
+
+ `, + config + ); + + const radio = page.locator('ion-radio'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(radio).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`radio-focus`)); + }); + + test('should render focus indicator when checked', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + +
+ + Checked + +
+
+ `, + config + ); + + const radio = page.locator('ion-radio'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(radio).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`radio-focus-checked`)); + }); + + test('should render focus indicator in an item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + Unchecked + + + `, + config + ); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`radio-in-item-focus`)); + }); + + test('should render focus indicator for a radio in a multi-input item', async ({ page, pageUtils }) => { + // A multi-input item cannot draw a single focus indicator, so each radio + // shows its own. Focus the first one and confirm its indicator renders. + await page.setContent( + ` + + + Radio 1 + Radio 2 + + + `, + config + ); + + const radio = page.locator('ion-radio').first(); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(radio).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`radio-multiple-in-item-focus`)); + }); + }); +}); diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5ff92f5d5e3 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ab13bbaa13c Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f8c844b5351 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..af756eb1be7 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c0e80459e81 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e1f652a322a Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-checked-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..749bbf84999 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6a7c560e590 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..595e0f81d24 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..828d7ce2f16 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ff068041cb7 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..a89aa121456 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..41b68fe7c60 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f0ce1209c7a Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..061d593a79c Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..484a4e9a797 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..88b7eb635e1 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..4f462c72853 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-in-item-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1b38932e60d Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f0138baedf6 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..8d3807cbe55 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..eb34b39c866 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..81cc7eccb03 Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..6e06fdd42fc Binary files /dev/null and b/core/src/components/radio/test/basic/radio.e2e.ts-snapshots/radio-multiple-in-item-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/radio/test/item/index.html b/core/src/components/radio/test/item/index.html index 36e46779fd2..e33e518d156 100644 --- a/core/src/components/radio/test/item/index.html +++ b/core/src/components/radio/test/item/index.html @@ -15,7 +15,7 @@ - -
- Unchecked -
- `, - config - ); - - await pageUtils.pressKeys('Tab'); - - const container = page.locator('#container'); - - await expect(container).toHaveScreenshot(screenshot(`toggle-focus`)); - }); - - test('should not have visual regressions when interacting with toggle in item', async ({ page, pageUtils }) => { - await page.setContent( - ` - - Unchecked - - `, - config - ); - - // Test focus with keyboard navigation - await pageUtils.pressKeys('Tab'); - - const item = page.locator('ion-item'); - - await expect(item).toHaveScreenshot(screenshot(`toggle-in-item-focus`)); - }); - test('should fire ionFocus when toggle is focused', async ({ page, pageUtils }) => { await page.setContent( ` @@ -285,3 +243,99 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c }); }); }); + +/** + * The focus indicator UI differs between iOS and MD, so these visual tests run + * in both modes. Direction does not affect the indicator, so only LTR is run. + */ +configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('toggle: focus visual'), () => { + test('should render focus indicator when unchecked', async ({ page, pageUtils }) => { + // `ion-app` is required so `startFocusVisible` runs and applies the + // `ion-focused` class on keyboard focus, which drives the focus indicator. + await page.setContent( + ` + + + +
+ Unchecked +
+
+ `, + config + ); + + const toggle = page.locator('ion-toggle'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(toggle).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`toggle-focus`)); + }); + + test('should render focus indicator when checked', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + +
+ Checked +
+
+ `, + config + ); + + const toggle = page.locator('ion-toggle'); + + // The focus listeners attach asynchronously, so the first Tab can miss + // them. Retry until `ion-focused` sticks before taking the snapshot. + await expect(async () => { + await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); + await pageUtils.pressKeys('Tab'); + await expect(toggle).toHaveClass(/ion-focused/, { timeout: 250 }); + }).toPass({ timeout: 5000 }); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`toggle-focus-checked`)); + }); + + test('should render focus indicator in an item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + Unchecked + + `, + config + ); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`toggle-in-item-focus`)); + }); + }); +}); diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..f5090dcb232 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c2afe2f5941 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..08176dd7004 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..aa2c389f896 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ca472643691 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..009c4be1a29 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-checked-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png index 0d7cbc11195..26dc586af8e 100644 Binary files a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png index bb21be2862e..856c3679238 100644 Binary files a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png index c20935c06b8..8153ee087f6 100644 Binary files a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..df800b723be Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..36c112f3429 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..2fdb6e468f8 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png index ee7a99593cd..e356b738007 100644 Binary files a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png index 19bd30c2be0..50bca49651c 100644 Binary files a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..8e16c7a4b8f Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0743e8f1314 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..84610944bd1 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/toggle.ios.scss b/core/src/components/toggle/toggle.ios.scss index cfe12f1d5b0..02673b45ffc 100644 --- a/core/src/components/toggle/toggle.ios.scss +++ b/core/src/components/toggle/toggle.ios.scss @@ -171,3 +171,10 @@ :host(.toggle-disabled) { opacity: $toggle-ios-disabled-opacity; } + +// iOS Toggle: Keyboard Focus +// ----------------------------------------- + +:host(.ion-focused) .toggle-icon { + box-shadow: 0 0 0 4px $toggle-ios-focus-ring-color; +} diff --git a/core/src/components/toggle/toggle.ios.vars.scss b/core/src/components/toggle/toggle.ios.vars.scss index 65d955e7690..782ddfc9bc2 100644 --- a/core/src/components/toggle/toggle.ios.vars.scss +++ b/core/src/components/toggle/toggle.ios.vars.scss @@ -48,3 +48,6 @@ $toggle-ios-disabled-opacity: .3; /// @prop - The text color of the on/off labels when the toggle is checked $toggle-ios-on-off-label-checked-color: #fff; + +/// @prop - Color of the focus indicator ring for the toggle when focused +$toggle-ios-focus-ring-color: #{ion-color(primary, base, 0.2)}; diff --git a/core/src/components/toggle/toggle.md.scss b/core/src/components/toggle/toggle.md.scss index d0a3bfda360..1a0e01abffa 100644 --- a/core/src/components/toggle/toggle.md.scss +++ b/core/src/components/toggle/toggle.md.scss @@ -76,3 +76,10 @@ :host(.toggle-disabled) { opacity: $toggle-md-disabled-opacity; } + +// Material Design Toggle: Keyboard Focus +// ----------------------------------------- + +:host(.ion-focused) .toggle-icon { + box-shadow: 0 0 0 4px $toggle-md-focus-ring-color; +} diff --git a/core/src/components/toggle/toggle.md.vars.scss b/core/src/components/toggle/toggle.md.vars.scss index 63f9ef72c9a..b61fd016312 100644 --- a/core/src/components/toggle/toggle.md.vars.scss +++ b/core/src/components/toggle/toggle.md.vars.scss @@ -48,3 +48,6 @@ $toggle-md-on-off-label-color: #000; /// @prop - The text color of the on/off labels when the toggle is checked $toggle-md-on-off-label-checked-color: #fff; + +/// @prop - Color of the focus indicator ring for the toggle when focused +$toggle-md-focus-ring-color: #{ion-color(primary, base, 0.2)}; diff --git a/core/src/components/toggle/toggle.scss b/core/src/components/toggle/toggle.scss index b64107e66b0..fdcea3d29c3 100644 --- a/core/src/components/toggle/toggle.scss +++ b/core/src/components/toggle/toggle.scss @@ -59,10 +59,6 @@ width: auto; } -:host(.ion-focused) input { - border: 2px solid #5e9ed6; -} - :host(.toggle-disabled) { pointer-events: none; } @@ -75,6 +71,12 @@ input { display: none; } +// The host is focusable, so the browser draws a native focus ring on it. +// Suppress it in favor of the mode-specific keyboard focus indicator. +:host(:focus) { + outline: none; +} + // Toggle Wrapper // -------------------------------------------------- diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index b46635c8a78..d3963704dbd 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -476,6 +476,10 @@ export class Toggle implements ComponentInterface { class={createColorClasses(color, { [mode]: true, 'in-item': hostContext('ion-item', el), + // A toggle always shows its own focus indicator: unlike checkbox and + // radio, it is excluded from the item's input cover, so an item never + // draws a focus indicator on its behalf. + 'ion-focusable': true, 'toggle-activated': activated, 'toggle-checked': checked, 'toggle-disabled': disabled,