-
Notifications
You must be signed in to change notification settings - Fork 13.3k
fix(checkbox,radio,toggle): add missing keyboard focus indicators #31295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
33e40af
81736bf
a135348
5953965
1568d4a
409a0ba
6a3c199
084902b
a105ea7
c00892d
fc10735
1c38112
910cddb
edcfb72
f758350
3dfbf81
0c4b6a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is the same as |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| @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%; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| /// @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 | ||
|
|
||
|
brandyscarney marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'), () => { | ||||||||||||||||
|
brandyscarney marked this conversation as resolved.
|
||||||||||||||||
| test('should not have visual regressions', async ({ page, pageUtils }) => { | ||||||||||||||||
| await page.setContent( | ||||||||||||||||
| ` | ||||||||||||||||
| <style> | ||||||||||||||||
| #container { | ||||||||||||||||
| display: inline-block; | ||||||||||||||||
| padding: 10px; | ||||||||||||||||
| } | ||||||||||||||||
| </style> | ||||||||||||||||
|
|
||||||||||||||||
| <div id="container"> | ||||||||||||||||
| <ion-checkbox>Unchecked</ion-checkbox> | ||||||||||||||||
| </div> | ||||||||||||||||
| `, | ||||||||||||||||
| 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( | ||||||||||||||||
| ` | ||||||||||||||||
| <ion-item class="ion-focused"> | ||||||||||||||||
| <ion-checkbox>Unchecked</ion-checkbox> | ||||||||||||||||
| </ion-item> | ||||||||||||||||
| `, | ||||||||||||||||
| 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,81 @@ 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. | ||||||||||||||||
| */ | ||||||||||||||||
|
Comment on lines
+290
to
+293
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a nit for consistency
Suggested change
|
||||||||||||||||
| 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( | ||||||||||||||||
| ` | ||||||||||||||||
| <style> | ||||||||||||||||
| #container { | ||||||||||||||||
| width: fit-content; | ||||||||||||||||
| padding: 10px; | ||||||||||||||||
| } | ||||||||||||||||
| </style> | ||||||||||||||||
|
|
||||||||||||||||
| <ion-app> | ||||||||||||||||
| <div id="container"> | ||||||||||||||||
| <ion-checkbox>Unchecked</ion-checkbox> | ||||||||||||||||
| </div> | ||||||||||||||||
| </ion-app> | ||||||||||||||||
| `, | ||||||||||||||||
| 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( | ||||||||||||||||
| ` | ||||||||||||||||
| <style> | ||||||||||||||||
| #container { | ||||||||||||||||
| width: fit-content; | ||||||||||||||||
| padding: 10px; | ||||||||||||||||
| } | ||||||||||||||||
| </style> | ||||||||||||||||
|
|
||||||||||||||||
| <ion-app> | ||||||||||||||||
| <div id="container"> | ||||||||||||||||
| <ion-checkbox checked>Checked</ion-checkbox> | ||||||||||||||||
| </div> | ||||||||||||||||
| </ion-app> | ||||||||||||||||
| `, | ||||||||||||||||
| 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`)); | ||||||||||||||||
| }); | ||||||||||||||||
| }); | ||||||||||||||||
| }); | ||||||||||||||||
|
brandyscarney marked this conversation as resolved.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All but one line of this is the same for
md. Please move those over tocheckbox.scss. By combining it now, it makes it easier for the Modular Ionic work.