Skip to content

Commit

Permalink
fix(many): update form controls (radio, checkbox, toggle, input, sele…
Browse files Browse the repository at this point in the history
…ct) 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 <[email protected]>
  • Loading branch information
brandyscarney and Ionitron authored May 17, 2023
1 parent ae88c3a commit 995a848
Show file tree
Hide file tree
Showing 149 changed files with 409 additions and 77 deletions.
3 changes: 3 additions & 0 deletions core/src/components/checkbox/checkbox.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/checkbox/checkbox.ios.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion core/src/components/checkbox/checkbox.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions core/src/components/checkbox/checkbox.md.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
36 changes: 0 additions & 36 deletions core/src/components/checkbox/test/basic/checkbox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
54 changes: 54 additions & 0 deletions core/src/components/checkbox/test/states/checkbox.e2e.ts
Original file line number Diff line number Diff line change
@@ -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`));
});
});
});
78 changes: 78 additions & 0 deletions core/src/components/checkbox/test/states/index.html
Original file line number Diff line number Diff line change
@@ -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>
11 changes: 11 additions & 0 deletions core/src/components/input/input.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
3 changes: 3 additions & 0 deletions core/src/components/input/input.ios.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 4 additions & 0 deletions core/src/components/input/input.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@

// 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};
}

// Input Bottom
// ----------------------------------------------------------------

/**
* If the input has a validity state, the
* border and label should reflect that as a color.
Expand Down
6 changes: 2 additions & 4 deletions core/src/components/input/input.md.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 0 additions & 8 deletions core/src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -193,7 +186,6 @@
opacity: 1;
}


// Clear Input Icon
// --------------------------------------------------

Expand Down
10 changes: 10 additions & 0 deletions core/src/components/item/test/disabled/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions core/src/components/item/test/legacy/disabled/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/radio/radio.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/radio/radio.ios.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 995a848

Please sign in to comment.