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,