fix(many): update form controls (radio, checkbox, toggle, input, select) to have consistent disabled opacity (#27396)

Issue number: resolves #27184

---------

## What is the current behavior?
The modern form controls do not use the same opacity for their labels
when disabled, resulting in inconsistent UI when using two different
types in the same view (select vs checkbox, for example).

## What is the new behavior?
The checkbox, input, radio, range, select, textarea and toggle labels
have been updated on both modes to use the same opacity as each other
when disabled. The checkbox and radio icons have been updated to use a
different opacity than the label for `md` mode.

- Updates `ios` mode so all form controls use the same opacity of `0.3`
- I could not find any guidelines by Apple for what color these should
be, so I decided to just make them the same as what is most commonly
used & match item
- Updates `md` mode so all form control labels use the same opacity of
`0.38`
- I used the [Material Design 3
documentation](https://m2.material.io/components) to get this number as
well as the opacity of the disabled selection controls in the [Material
Design 2 figma design
kit](https://www.figma.com/community/file/778763161265841481). The
Material Design 2 documentation does not mention the opacity, but this
is also the number used by Material Design 1 so it's safe to assume it
is what the disabled form labels should also use for Material Design 2.
- Updates the `md` range so the slotted elements are also included when
setting the opacity on the label
- Updates the range, radio & checkbox tests to make sure there are
screenshots in the different disabled states
- Updates the item/disabled test to include radio & textarea so you can
see all form controls side by side

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

I downloaded screenshots of the item disabled tests and put them side by
side for `main` and this branch in order to see the differences in the
labels. The grey boxes to the left of each item are just a bigger
version of the label color for that item, so it's easier to see when
they aren't the same.

![iOS before and
after](https://github.com/ionic-team/ionic-framework/assets/6577830/2ce53625-e4e3-4565-a741-a47e27cf0275)
![MD before and
after](https://github.com/ionic-team/ionic-framework/assets/6577830/9cd83cdc-ab45-49bf-a0dc-1d78ba7f43be)

---------

Co-authored-by: ionitron <hi@ionicframework.com>
This commit is contained in:
Brandy Carney
2023-05-17 10:25:06 -04:00
committed by GitHub
parent ae88c3a25f
commit 995a848575
149 changed files with 409 additions and 77 deletions

View File

@ -21,6 +21,9 @@
// iOS Checkbox: Disabled
// -----------------------------------------
// The checkbox and label should use the
// same opacity and match the other form
// controls
:host(.checkbox-disabled) {
opacity: $checkbox-ios-disabled-opacity;

View File

@ -54,7 +54,7 @@ $checkbox-ios-checkmark-width: math.div($checkbox-ios-icon-size, 6) +
$checkbox-ios-checkmark-height: $checkbox-ios-icon-size * 0.5 !default;
/// @prop - Opacity of the disabled checkbox
$checkbox-ios-disabled-opacity: .3 !default;
$checkbox-ios-disabled-opacity: $form-control-ios-disabled-opacity !default;
/// @prop - Margin top of the left checkbox item
$checkbox-ios-item-start-margin-top: 8px !default;

View File

@ -40,11 +40,19 @@
// Material Design Checkbox: Disabled
// --------------------------------------------------------
// The checkbox itself should use the disabled
// opacity set by its spec, while the label
// should match the other form controls
:host(.checkbox-disabled) {
:host(.legacy-checkbox.checkbox-disabled),
:host(.checkbox-disabled) .label-text-wrapper {
opacity: $checkbox-md-disabled-opacity;
}
:host(.checkbox-disabled) .native-wrapper {
opacity: $checkbox-md-icon-disabled-opacity;
}
// Material Design Checkbox Within An Item
// TODO(FW-3100): remove this

View File

@ -4,8 +4,8 @@
// Material Design Checkbox
// --------------------------------------------------
/// @prop - Opacity of the disabled checkbox
$checkbox-md-disabled-opacity: .3 !default;
/// @prop - Opacity of the disabled checkbox label
$checkbox-md-disabled-opacity: $form-control-md-disabled-opacity !default;
/// @prop - Background color of the checkbox icon when off
$checkbox-md-icon-background-color-off: $item-md-background !default;
@ -72,3 +72,12 @@ $checkbox-md-item-end-margin-bottom: $checkbox-md-item-end-margin-top !defau
/// @prop - Margin start of the end checkbox item
$checkbox-md-item-end-margin-start: 0 !default;
/// @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
/// `rgb(0, 0, 0, 0.38)` when disabled but the disabled
/// opacity is applied on top of the transparent color so
/// this opacity gets us the equivalent of applying `0.38`
/// on top of an opaque checkbox `rgb(0, 0, 0, 1.0)`
$checkbox-md-icon-disabled-opacity: 0.63 !default;

View File

@ -3,42 +3,6 @@ import { configs, test } from '@utils/test/playwright';
configs().forEach(({ title, screenshot, config }) => {
test.describe(title('checkbox: basic visual tests'), () => {
test('should render unchecked checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox>Unchecked</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-unchecked`));
});
test('should render checked checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox checked>Checked</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-checked`));
});
test('should render disabled checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox checked disabled>Disabled</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-disabled`));
});
test('should render custom checkmark-width correctly', async ({ page }) => {
await page.setContent(
`

View File

@ -0,0 +1,54 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('checkbox: states'), () => {
test('should render disabled checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox disabled>Label</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(screenshot(`checkbox-disabled`));
});
test('should render disabled checked checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox checked disabled>Label</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(screenshot(`checkbox-checked-disabled`));
});
test('should render checked checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox checked>Label</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(screenshot(`checkbox-checked`));
});
test('should render unchecked checkbox correctly', async ({ page }) => {
await page.setContent(
`
<ion-checkbox>Label</ion-checkbox>
`,
config
);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(screenshot(`checkbox-unchecked`));
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Checkbox - States</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
ion-checkbox {
width: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Checkbox - States</ion-title>
</ion-toolbar>
</ion-header>
<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Unchecked</h2>
<ion-checkbox>Enable Notifications</ion-checkbox>
</div>
<div class="grid-item">
<h2>Checked</h2>
<ion-checkbox checked>Enable Notifications</ion-checkbox>
</div>
<div class="grid-item">
<h2>Disabled, Unchecked</h2>
<ion-checkbox disabled="true">Enable Notifications</ion-checkbox>
</div>
<div class="grid-item">
<h2>Disabled, Checked</h2>
<ion-checkbox disabled="true" checked>Enable Notifications</ion-checkbox>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -44,3 +44,14 @@
:host(.input-label-placement-stacked) {
min-height: 56px;
}
// Input - Disabled
// ----------------------------------------------------------------
// The input, label, helper text, char counter and placeholder
// should use the same opacity and match the other form controls
:host(.legacy-input) .native-input[disabled],
:host(.input-disabled) {
opacity: #{$input-ios-disabled-opacity};
}

View File

@ -72,3 +72,6 @@ $input-ios-highlight-color-valid: ion-color(success, base) !default;
/// @prop - Color of the input highlight when invalid
$input-ios-highlight-color-invalid: ion-color(danger, base) !default;
/// @prop - The opacity of the input text, label, helper text, char counter and placeholder of a disabled input
$input-ios-disabled-opacity: $form-control-ios-disabled-opacity !default;

View File

@ -36,6 +36,9 @@
// Input - Disabled
// ----------------------------------------------------------------
// The input, label, helper text, char counter and placeholder
// should use the same opacity and match the other form controls
:host(.legacy-input) .native-input[disabled],
:host(.input-disabled) {
opacity: #{$input-md-disabled-opacity};
@ -43,6 +46,7 @@
// Input Bottom
// ----------------------------------------------------------------
/**
* If the input has a validity state, the
* border and label should reflect that as a color.

View File

@ -70,7 +70,5 @@ $input-md-inset-margin-start: $item-md-padding-start !default;
/// @prop - The amount of whitespace to display on either side of the floating label
$input-md-floating-label-padding: 4px !default;
/// @prop - The disabled opacity of the input text, the label, helper text, char counter and placeholder of a disabled input.
/// This value comes from the Material Design spec:
/// - https://github.com/material-components/material-web/pull/549
$input-md-disabled-opacity: 0.37 !default;
/// @prop - The opacity of the input text, label, helper text, char counter and placeholder of a disabled input
$input-md-disabled-opacity: $form-control-md-disabled-opacity !default;

View File

@ -161,13 +161,6 @@
}
}
:host(.legacy-input) .native-input[disabled],
:host(.input-disabled) {
opacity: 0.4;
}
// Input Cover: Unfocused
// --------------------------------------------------
// The input cover is the div that actually receives the
@ -193,7 +186,6 @@
opacity: 1;
}
// Clear Input Icon
// --------------------------------------------------

View File

@ -55,6 +55,10 @@
<ion-input label="Disabled Input" placeholder="Disabled" disabled></ion-input>
</ion-item>
<ion-item>
<ion-textarea label="Disabled Textarea" placeholder="Disabled" disabled></ion-textarea>
</ion-item>
<ion-item>
<ion-toggle disabled checked> Disabled Toggle </ion-toggle>
</ion-item>
@ -63,6 +67,12 @@
<ion-checkbox disabled checked> Disabled Checkbox </ion-checkbox>
</ion-item>
<ion-radio-group value="radio">
<ion-item>
<ion-radio disabled value="radio"> Disabled Radio </ion-radio>
</ion-item>
</ion-radio-group>
<ion-item>
<ion-range disabled value="10">
<span slot="label">Disabled Range</span>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -57,6 +57,11 @@
<ion-input placeholder="Disabled" disabled></ion-input>
</ion-item>
<ion-item>
<ion-label>Disabled Textarea</ion-label>
<ion-textarea placeholder="Disabled" disabled></ion-textarea>
</ion-item>
<ion-item>
<ion-label>Disabled Toggle</ion-label>
<ion-toggle disabled checked slot="end"></ion-toggle>
@ -67,6 +72,13 @@
<ion-checkbox disabled checked slot="start"></ion-checkbox>
</ion-item>
<ion-radio-group value="radio">
<ion-item>
<ion-label>Disabled Radio</ion-label>
<ion-radio disabled value="radio" slot="start"></ion-radio>
</ion-item>
</ion-radio-group>
<ion-item>
<ion-label>Disabled Range</ion-label>
<ion-range disabled value="10"></ion-range>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -42,6 +42,9 @@
// iOS Radio: Disabled
// -----------------------------------------
// The radio and label should use the
// same opacity and match the other form
// controls
:host(.radio-disabled) {
opacity: $radio-ios-disabled-opacity;

View File

@ -23,7 +23,7 @@ $radio-ios-icon-border-width: 2px !default;
$radio-ios-icon-border-style: solid !default;
/// @prop - Opacity of the disabled radio
$radio-ios-disabled-opacity: .3 !default;
$radio-ios-disabled-opacity: $form-control-ios-disabled-opacity !default;
/// @prop - Margin top of the item-start in a radio
$radio-ios-item-start-margin-top: 8px !default;

View File

@ -70,12 +70,19 @@
// Material Design Radio: Disabled
// -----------------------------------------
// The radio itself should use the disabled
// opacity set by its spec, while the label
// should match the other form controls
// .item-md.item-radio-disabled ion-label {
:host(.radio-disabled) {
:host(.legacy-radio.radio-disabled),
:host(.radio-disabled) .label-text-wrapper {
opacity: $radio-md-disabled-opacity;
}
:host(.radio-disabled) .native-wrapper {
opacity: $radio-md-icon-disabled-opacity;
}
// Material Design Radio: Keyboard Focus
// -----------------------------------------

View File

@ -34,8 +34,8 @@ $radio-md-transition-duration: 280ms !default;
/// @prop - Transition easing of the radio
$radio-md-transition-easing: cubic-bezier(.4, 0, .2, 1) !default;
/// @prop - Opacity of the disabled radio
$radio-md-disabled-opacity: .3 !default;
/// @prop - Opacity of the disabled radio label
$radio-md-disabled-opacity: $form-control-md-disabled-opacity !default;
/// @prop - Margin top of the item-start in a radio
$radio-md-item-start-margin-top: 11px !default;
@ -60,3 +60,12 @@ $radio-md-item-end-margin-bottom: $radio-md-item-end-margin-top !default;
/// @prop - Margin start of the item-end in a radio
$radio-md-item-end-margin-start: 0 !default;
/// @prop - Opacity of the disabled radio
/// This value is used because the radio color is set to
/// `rgb(0, 0, 0, 0.60)` when enabled and we need it to be
/// `rgb(0, 0, 0, 0.38)` when disabled but the disabled
/// opacity is applied on top of the transparent color so
/// this opacity gets us the equivalent of applying `0.38`
/// on top of an opaque radio `rgb(0, 0, 0, 1.0)`
$radio-md-icon-disabled-opacity: 0.63 !default;

View File

@ -3,6 +3,20 @@ import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('radio: states'), () => {
test('should render disabled radio correctly', async ({ page }) => {
await page.setContent(
`
<ion-radio-group>
<ion-radio disabled="true">Label</ion-radio>
</ion-radio-group>
`,
config
);
const radio = page.locator('ion-radio');
expect(await radio.screenshot()).toMatchSnapshot(screenshot(`radio-disabled`));
});
test('should render disabled checked radio correctly', async ({ page }) => {
await page.setContent(
`

Some files were not shown because too many files have changed in this diff Show More