feat(checkbox): component can be used outside ion-item (#26518)

This commit is contained in:
Amanda Johnston
2023-01-05 13:49:04 -06:00
committed by GitHub
parent 96147ec1b0
commit 9d52e70361
297 changed files with 1189 additions and 274 deletions

View File

@@ -105,7 +105,9 @@ This section details the desktop browser, JavaScript framework, and mobile platf
<h4 id="version-7x-checkbox">Checkbox</h4>
`ionChange` is no longer emitted when the `checked` property of `ion-checkbox` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the checkbox.
- `ionChange` is no longer emitted when the `checked` property of `ion-checkbox` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the checkbox.
- The `--background` and `--background-checked` CSS variables have been renamed to `--checkbox-background` and `--checkbox-background-checked` respectively.
<h4 id="version-7x-datetime">Datetime</h4>

View File

@@ -529,13 +529,13 @@ setting the checked property.
@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['checked', 'color', 'disabled', 'indeterminate', 'mode', 'name', 'value']
inputs: ['checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value']
})
@Component({
selector: 'ion-checkbox',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['checked', 'color', 'disabled', 'indeterminate', 'mode', 'name', 'value']
inputs: ['checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value']
})
export class IonCheckbox {
protected el: HTMLElement;

View File

@@ -295,19 +295,22 @@ ion-checkbox,prop,checked,boolean,false,false,false
ion-checkbox,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-checkbox,prop,disabled,boolean,false,false,false
ion-checkbox,prop,indeterminate,boolean,false,false,false
ion-checkbox,prop,justify,"end" | "space-between" | "start",'space-between',false,false
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-checkbox,prop,legacy,boolean | undefined,undefined,false,false
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
ion-checkbox,prop,name,string,this.inputId,false,false
ion-checkbox,prop,value,any,'on',false,false
ion-checkbox,event,ionBlur,void,true
ion-checkbox,event,ionChange,CheckboxChangeEventDetail<any>,true
ion-checkbox,event,ionFocus,void,true
ion-checkbox,css-prop,--background
ion-checkbox,css-prop,--background-checked
ion-checkbox,css-prop,--border-color
ion-checkbox,css-prop,--border-color-checked
ion-checkbox,css-prop,--border-radius
ion-checkbox,css-prop,--border-style
ion-checkbox,css-prop,--border-width
ion-checkbox,css-prop,--checkbox-background
ion-checkbox,css-prop,--checkbox-background-checked
ion-checkbox,css-prop,--checkmark-color
ion-checkbox,css-prop,--checkmark-width
ion-checkbox,css-prop,--size

View File

@@ -554,6 +554,18 @@ export namespace Components {
* If `true`, the checkbox will visually appear as indeterminate.
*/
"indeterminate": boolean;
/**
* How to pack the label and checkbox within a line. `'start'`: The label and checkbox will appear on the left in LTR and on the right in RTL. `'end'`: The label and checkbox will appear on the right in LTR and on the left in RTL. `'space-between'`: The label and checkbox will appear on opposite ends of the line with space between the two elements.
*/
"justify": 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the checkbox. `'start'`: The label will appear to the left of the checkbox in LTR and to the right in RTL. `'end'`: The label will appear to the right of the checkbox in LTR and to the left in RTL. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement": 'start' | 'end' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt checkboxes in to the modern form markup when they are using either the `aria-label` attribute or have text in the default slot. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* The mode determines which platform styles to use.
*/
@@ -4466,6 +4478,18 @@ declare namespace LocalJSX {
* If `true`, the checkbox will visually appear as indeterminate.
*/
"indeterminate"?: boolean;
/**
* How to pack the label and checkbox within a line. `'start'`: The label and checkbox will appear on the left in LTR and on the right in RTL. `'end'`: The label and checkbox will appear on the right in LTR and on the left in RTL. `'space-between'`: The label and checkbox will appear on opposite ends of the line with space between the two elements.
*/
"justify"?: 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the checkbox. `'start'`: The label will appear to the left of the checkbox in LTR and to the right in RTL. `'end'`: The label will appear to the right of the checkbox in LTR and to the left in RTL. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt checkboxes in to the modern form markup when they are using either the `aria-label` attribute or have text in the default slot. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* The mode determines which platform styles to use.
*/

View File

@@ -12,13 +12,10 @@
--border-color: #{$checkbox-ios-icon-border-color-off};
// Background
--background: #{$checkbox-ios-background-color-off};
--checkbox-background: #{$checkbox-ios-background-color-off};
// Size
--size: #{$checkbox-ios-icon-size};
width: var(--size);
height: var(--size);
}
@@ -31,9 +28,10 @@
// iOS Checkbox Within An Item
// TODO(FW-3100): remove this
// -----------------------------------------
:host(.in-item) {
:host(.in-item.legacy-checkbox) {
// end position by default
@include margin($checkbox-ios-item-end-margin-top, $checkbox-ios-item-end-margin-end, $checkbox-ios-item-end-margin-bottom, $checkbox-ios-item-end-margin-start);
@@ -42,6 +40,6 @@
position: static;
}
:host(.in-item[slot="start"]) {
:host(.in-item.legacy-checkbox[slot="start"]) {
@include margin($checkbox-ios-item-start-margin-top, $checkbox-ios-item-start-margin-end, $checkbox-ios-item-start-margin-bottom, $checkbox-ios-item-start-margin-start);
}

View File

@@ -13,16 +13,13 @@
--checkmark-width: 3;
// Background
--background: #{$checkbox-md-icon-background-color-off};
--checkbox-background: #{$checkbox-md-icon-background-color-off};
// Transition
--transition: #{background $checkbox-md-transition-duration $checkbox-md-transition-easing};
// Size
--size: #{$checkbox-md-icon-size};
width: var(--size);
height: var(--size);
}
.checkbox-icon path {
@@ -50,9 +47,10 @@
// Material Design Checkbox Within An Item
// TODO(FW-3100): remove this
// --------------------------------------------------------
:host(.in-item) {
:host(.in-item.legacy-checkbox) {
// end position by default
@include margin($checkbox-md-item-end-margin-top, $checkbox-md-item-end-margin-end, $checkbox-md-item-end-margin-bottom, $checkbox-md-item-end-margin-start);
@@ -61,11 +59,6 @@
position: static;
}
:host(.in-item[slot="start"]) {
:host(.in-item.legacy-checkbox[slot="start"]) {
@include margin($checkbox-md-item-start-margin-top, $checkbox-md-item-start-margin-end, $checkbox-md-item-start-margin-bottom, $checkbox-md-item-start-margin-start);
}
// REVIEW
// .checkbox-md + .item-inner ion-label {
// @include margin-horizontal(0, null);
// }

View File

@@ -7,8 +7,8 @@
/**
* @prop --size: Size of the checkbox icon
*
* @prop --background: Background of the checkbox icon
* @prop --background-checked: Background of the checkbox icon when checked
* @prop --checkbox-background: Background of the checkbox icon
* @prop --checkbox-background-checked: Background of the checkbox icon when checked
*
* @prop --border-color: Border color of the checkbox icon
* @prop --border-radius: Border radius of the checkbox icon
@@ -21,26 +21,41 @@
* @prop --checkmark-color: Color of the checkbox checkmark when checked
* @prop --checkmark-width: Stroke width of the checkbox checkmark
*/
--background-checked: #{ion-color(primary, base)};
--checkbox-background-checked: #{ion-color(primary, base)};
--border-color-checked: #{ion-color(primary, base)};
--checkmark-color: #{ion-color(primary, contrast)};
--checkmark-width: 1;
--transition: none;
display: inline-block;
position: relative;
cursor: pointer;
user-select: none;
z-index: $z-index-item-input;
}
:host(.in-item) {
width: 100%;
height: 100%;
}
// TODO(FW-3100): remove this
:host(.legacy-checkbox) {
width: var(--size);
height: var(--size);
}
:host(.ion-color) {
--background-checked: #{current-color(base)};
--checkbox-background-checked: #{current-color(base)};
--border-color-checked: #{current-color(base)};
--checkmark-color: #{current-color(contrast)};
}
label {
// TODO(FW-3100): remove this
:host(.legacy-checkbox) label {
@include input-cover();
display: flex;
@@ -50,30 +65,82 @@ label {
opacity: 0;
}
.checkbox-wrapper {
display: flex;
flex-grow: 1;
align-items: center;
height: 100%;
cursor: inherit;
}
.label-text-wrapper {
/**
* This ensures that double tapping this text
* clicks the <label> and focuses the checkbox
* when a screen reader is enabled.
*/
pointer-events: none;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
/**
* If no label text is placed into the slot
* then the element should be hidden otherwise
* there will be additional margins added.
*/
.label-text-wrapper-hidden {
display: none;
}
input {
@include visually-hidden();
}
.native-wrapper {
display: flex;
align-items: center;
}
.checkbox-icon {
@include border-radius(var(--border-radius));
display: block;
position: relative;
width: 100%;
height: 100%;
transition: var(--transition);
border-width: var(--border-width);
border-style: var(--border-style);
border-color: var(--border-color);
background: var(--background);
background: var(--checkbox-background);
box-sizing: border-box;
}
// TODO(FW-3100): remove this
:host(.legacy-checkbox) .checkbox-icon {
display: block;
width: 100%;
height: 100%;
}
// TODO(FW-3100): merge this with other .checkbox-icon styles above
:host(:not(.legacy-checkbox)) .checkbox-icon {
width: var(--size);
height: var(--size);
}
.checkbox-icon path {
fill: none;
stroke: var(--checkmark-color);
@@ -83,6 +150,89 @@ input {
}
// Justify Content
// ---------------------------------------------
:host(.checkbox-justify-space-between) .checkbox-wrapper {
justify-content: space-between;
}
:host(.checkbox-justify-start) .checkbox-wrapper {
justify-content: start;
}
:host(.checkbox-justify-end) .checkbox-wrapper {
justify-content: end;
}
// Label Placement - Start
// ----------------------------------------------------------------
/**
* Label is on the left of the checkbox in LTR and
* on the right in RTL.
*/
:host(.checkbox-label-placement-start) .checkbox-wrapper {
flex-direction: row;
}
:host(.checkbox-label-placement-start) .label-text-wrapper {
/**
* The margin between the label and
* the checkbox should be on the end
* when the label sits at the start.
*/
@include margin(0, 8px, 0, 0);
}
// Label Placement - End
// ----------------------------------------------------------------
/**
* Label is on the right of the checkbox in LTR and
* on the left in RTL.
*/
:host(.checkbox-label-placement-end) .checkbox-wrapper {
flex-direction: row-reverse;
}
/**
* The margin between the label and
* the checkbox should be on the start
* when the label sits at the end.
*/
:host(.checkbox-label-placement-end) .label-text-wrapper {
@include margin(0, 0, 0, 8px);
}
// Label Placement - Fixed
// ----------------------------------------------------------------
:host(.checkbox-label-placement-fixed) .label-text-wrapper {
/**
* The margin between the label and
* the checkbox should be on the end
* when the label sits at the start.
*/
@include margin(0, 8px, 0, 0);
}
/**
* Label is on the left of the checkbox in LTR and
* on the right in RTL. Label also has a fixed width.
*/
:host(.checkbox-label-placement-fixed) .label-text-wrapper {
flex: 0 0 100px;
width: 100px;
min-width: 100px;
max-width: 200px;
}
// Checked / Indeterminate Checkbox
// ---------------------------------------------
@@ -90,7 +240,7 @@ input {
:host(.checkbox-indeterminate) .checkbox-icon {
border-color: var(--border-color-checked);
background: var(--background-checked);
background: var(--checkbox-background-checked);
}
:host(.checkbox-checked) .checkbox-icon path,

View File

@@ -1,14 +1,21 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core';
// TODO(FW-2845) - Use @utils/forms and @utils/logging when https://github.com/ionic-team/stencil/issues/3826 is resolved
import { getIonMode } from '../../global/ionic-global';
import type { CheckboxChangeEventDetail, Color, StyleEventDetail } from '../../interface';
import { getAriaLabel, renderHiddenInput } from '../../utils/helpers';
import type { CheckboxChangeEventDetail, Color, Mode, StyleEventDetail } from '../../interface';
import type { LegacyFormController } from '../../utils/forms';
import { createLegacyFormController } from '../../utils/forms';
import type { Attributes } from '../../utils/helpers';
import { getAriaLabel, inheritAriaAttributes, renderHiddenInput } from '../../utils/helpers';
import { printIonWarning } from '../../utils/logging';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot - The label text to associate with the checkbox. Use the "labelPlacement" property to control where the label is placed relative to the checkbox.
*
* @part container - The container for the checkbox mark.
* @part mark - The checkmark used to indicate the checked state.
*/
@@ -23,8 +30,14 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Checkbox implements ComponentInterface {
private inputId = `ion-cb-${checkboxIds++}`;
private focusEl?: HTMLElement;
private legacyFormController!: LegacyFormController; // TODO(FW-3100): remove this
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;
// TODO(FW-3100): remove this
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
@Element() el!: HTMLIonCheckboxElement;
/**
* The color to use from your application's color palette.
@@ -62,6 +75,38 @@ export class Checkbox implements ComponentInterface {
*/
@Prop() value: any | null = 'on';
/**
* Where to place the label relative to the checkbox.
* `'start'`: The label will appear to the left of the checkbox in LTR and to the right in RTL.
* `'end'`: The label will appear to the right of the checkbox in LTR and to the left in RTL.
* `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' = 'start';
/**
* How to pack the label and checkbox within a line.
* `'start'`: The label and checkbox will appear on the left in LTR and
* on the right in RTL.
* `'end'`: The label and checkbox will appear on the right in LTR and
* on the left in RTL.
* `'space-between'`: The label and checkbox will appear on opposite
* ends of the line with space between the two elements.
*/
@Prop() justify: 'start' | 'end' | 'space-between' = 'space-between';
// TODO(FW-3100): remove this
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup.
* Ionic will only opt checkboxes in to the modern form markup when they are
* using either the `aria-label` attribute or have text in the default slot. As a result,
* the `legacy` property should only be used as an escape hatch when you want to
* avoid this automatic opt-in behavior.
*
* Note that this property will be removed in an upcoming major release
* of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@Prop() legacy?: boolean;
/**
* Emitted when the checked property has changed
* as a result of a user action such as a click.
@@ -86,8 +131,20 @@ export class Checkbox implements ComponentInterface {
*/
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
// TODO(FW-3100): remove this
connectedCallback() {
this.legacyFormController = createLegacyFormController(this.el);
}
componentWillLoad() {
this.emitStyle();
// TODO(FW-3100): remove check
if (!this.legacyFormController.hasLegacyControl()) {
this.inheritedAttributes = {
...inheritAriaAttributes(this.el),
};
}
}
@Watch('checked')
@@ -100,11 +157,14 @@ export class Checkbox implements ComponentInterface {
this.emitStyle();
}
// TODO(FW-3100): remove this
private emitStyle() {
this.ionStyle.emit({
'checkbox-checked': this.checked,
'interactive-disabled': this.disabled,
});
if (this.legacyFormController.hasLegacyControl()) {
this.ionStyle.emit({
'checkbox-checked': this.checked,
'interactive-disabled': this.disabled,
});
}
}
private setFocus() {
@@ -143,27 +203,105 @@ export class Checkbox implements ComponentInterface {
this.ionBlur.emit();
};
// TODO(FW-3100): run contents of renderCheckbox directly instead
render() {
const { color, checked, disabled, el, indeterminate, inputId, name, value } = this;
const { legacyFormController } = this;
return legacyFormController.hasLegacyControl() ? this.renderLegacyCheckbox() : this.renderCheckbox();
}
private renderCheckbox() {
const {
color,
checked,
disabled,
el,
getSVGPath,
indeterminate,
inheritedAttributes,
inputId,
justify,
labelPlacement,
name,
value,
} = this;
const mode = getIonMode(this);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
const path = getSVGPath(mode, indeterminate);
renderHiddenInput(true, el, name, checked ? value : '', disabled);
let path = indeterminate ? (
<path d="M6 12L18 12" part="mark" />
) : (
<path d="M5.9,12.5l3.8,3.8l8.8-8.8" part="mark" />
return (
<Host
aria-hidden={disabled ? 'true' : null}
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
'checkbox-checked': checked,
'checkbox-disabled': disabled,
'checkbox-indeterminate': indeterminate,
interactive: true,
[`checkbox-justify-${justify}`]: true,
[`checkbox-label-placement-${labelPlacement}`]: true,
})}
>
<label class="checkbox-wrapper">
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': el.textContent === '',
}}
>
<slot></slot>
</div>
<div class="native-wrapper">
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
{path}
</svg>
</div>
<input
type="checkbox"
aria-checked={`${checked}`}
disabled={disabled}
id={inputId}
onChange={this.toggleChecked}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
{...inheritedAttributes}
/>
</label>
</Host>
);
}
if (mode === 'md') {
path = indeterminate ? (
<path d="M2 12H22" part="mark" />
) : (
<path d="M1.73,12.91 8.1,19.28 22.79,4.59" part="mark" />
// TODO(FW-3100): remove this
private renderLegacyCheckbox() {
if (!this.hasLoggedDeprecationWarning) {
printIonWarning(
`Using ion-checkbox with an ion-label has been deprecated. To migrate, remove the ion-label and pass your label directly into ion-checkbox instead.
Example: <ion-checkbox>Label</ion-checkbox>
For checkboxes that do not have a visible label, developers should use "aria-label" so screen readers can announce the purpose of the checkbox.`,
this.el
);
if (this.legacy) {
printIonWarning(
`ion-checkbox is being used with the "legacy" property enabled which will forcibly enable the legacy form markup. This property will be removed in an upcoming major release of Ionic where this form control will use the modern form markup.
Developers can dismiss this warning by removing their usage of the "legacy" property and using the new checkbox syntax.`,
this.el
);
}
this.hasLoggedDeprecationWarning = true;
}
const { color, checked, disabled, el, getSVGPath, indeterminate, inputId, name, value } = this;
const mode = getIonMode(this);
const { label, labelId, labelText } = getAriaLabel(el, inputId);
const path = getSVGPath(mode, indeterminate);
renderHiddenInput(true, el, name, checked ? value : '', disabled);
return (
<Host
aria-labelledby={label ? labelId : null}
@@ -176,6 +314,7 @@ export class Checkbox implements ComponentInterface {
'checkbox-checked': checked,
'checkbox-disabled': disabled,
'checkbox-indeterminate': indeterminate,
'legacy-checkbox': true,
interactive: true,
})}
>
@@ -196,6 +335,24 @@ export class Checkbox implements ComponentInterface {
</Host>
);
}
private getSVGPath(mode: Mode, indeterminate: boolean): HTMLElement {
let path = indeterminate ? (
<path d="M6 12L18 12" part="mark" />
) : (
<path d="M5.9,12.5l3.8,3.8l8.8-8.8" part="mark" />
);
if (mode === 'md') {
path = indeterminate ? (
<path d="M2 12H22" part="mark" />
) : (
<path d="M1.73,12.91 8.1,19.28 22.79,4.59" part="mark" />
);
}
return path;
}
}
let checkboxIds = 0;

View File

@@ -0,0 +1,17 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: a11y', () => {
test.beforeEach(async ({ skip }) => {
skip.rtl();
skip.mode('md');
});
test('should not have accessibility violations', async ({ page }) => {
await page.goto(`/src/components/checkbox/test/a11y`);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Checkbox - a11y</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<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>
</head>
<body>
<main>
<h1>Checkbox - a11y</h1>
<ion-checkbox>Label</ion-checkbox><br />
<ion-checkbox aria-label="my aria label"></ion-checkbox><br />
<ion-item>
<ion-checkbox>Checkbox in item</ion-checkbox>
</ion-item>
</main>
</body>
</html>

View File

@@ -1,39 +1,78 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: basic', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/checkbox/test/basic`);
test.describe('checkbox: basic visual tests', () => {
test('should render unchecked checkbox correctly', async ({ page }) => {
await page.setContent(`
<ion-checkbox>Unchecked</ion-checkbox>
`);
await page.setIonViewport();
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-unchecked-${page.getSnapshotSettings()}.png`);
});
expect(await page.screenshot()).toMatchSnapshot(`checkbox-basic-${page.getSnapshotSettings()}.png`);
test('should render checked checkbox correctly', async ({ page }) => {
await page.setContent(`
<ion-checkbox checked>Checked</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-checked-${page.getSnapshotSettings()}.png`);
});
test('should render disabled checkbox correctly', async ({ page }) => {
await page.setContent(`
<ion-checkbox checked disabled>Disabled</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-disabled-${page.getSnapshotSettings()}.png`);
});
test('should render custom checkmark-width correctly', async ({ page }) => {
await page.setContent(`
<ion-checkbox checked style="--checkmark-width: 7">Checkmark Width</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-checkmark-width-${page.getSnapshotSettings()}.png`);
});
test('should render custom size correctly', async ({ page }) => {
await page.setContent(`
<ion-checkbox checked style="--size: 100px">Size</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-size-${page.getSnapshotSettings()}.png`);
});
});
test.describe('checkbox: ionChange', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
skip.mode('ios');
});
test('should fire ionChange when interacting with checkbox', async ({ page }) => {
await page.setContent(`
<ion-checkbox value="my-checkbox"></ion-checkbox>
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
`);
const ionChange = await page.spyOnEvent('ionChange');
const checkbox = page.locator('ion-checkbox');
await checkbox.click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true });
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true });
await checkbox.click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false });
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false });
});
test('should fire ionChange when interacting with checkbox in item', async ({ page }) => {
await page.setContent(`
<ion-item>
<ion-checkbox value="my-checkbox"></ion-checkbox>
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
</ion-item>
`);
@@ -41,21 +80,21 @@ test.describe('checkbox: ionChange', () => {
const item = page.locator('ion-item');
await item.click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true });
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true });
await item.click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false });
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false });
});
test('should not fire when programmatically setting a value', async ({ page }) => {
await page.setContent(`
<ion-checkbox value="my-checkbox"></ion-checkbox>
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
`);
const ionChange = await page.spyOnEvent('ionChange');
const checkbox = page.locator('ion-checkbox');
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
await expect(ionChange).not.toHaveReceivedEvent();
expect(ionChange).not.toHaveReceivedEvent();
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -14,6 +14,11 @@
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<style>
ion-checkbox {
display: block;
margin-bottom: 8px;
}
ion-checkbox.checkbox-part::part(mark) {
transform: scale(0.5);
transform-origin: center;
@@ -27,81 +32,16 @@
</ion-toolbar>
</ion-header>
<ion-content id="content">
<ion-item onClick="clickItem()">
<ion-label>Clickable Item</ion-label>
<ion-checkbox></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Default</ion-label>
<ion-checkbox checked></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Primary</ion-label>
<ion-checkbox checked color="primary" slot="end"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Secondary, disabled</ion-label>
<ion-checkbox disabled checked color="secondary"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Tertiary</ion-label>
<ion-checkbox checked color="tertiary"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Success</ion-label>
<ion-checkbox checked color="success"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Warning</ion-label>
<ion-checkbox checked color="warning"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Dark, --size</ion-label>
<ion-checkbox checked color="dark" style="--size: 100px"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Danger</ion-label>
<ion-checkbox checked color="danger" slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Light</ion-label>
<ion-checkbox checked color="light" slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Medium</ion-label>
<ion-checkbox checked color="medium" slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checkmark width</ion-label>
<ion-checkbox checked color="dark" slot="start" style="--checkmark-width: 7"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checkmark ::part</ion-label>
<ion-checkbox checked color="dark" slot="start" class="checkbox-part"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Unchecked by Default</ion-label>
<ion-checkbox></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Disabled</ion-label>
<ion-checkbox disabled></ion-checkbox>
</ion-item>
<ion-content class="ion-padding">
<div id="checkboxes">
<ion-checkbox justify="start">Unchecked</ion-checkbox>
<ion-checkbox justify="start" checked>Checked</ion-checkbox>
<ion-checkbox justify="start" disabled>Disabled</ion-checkbox>
<ion-checkbox justify="start" disabled checked>Disabled, Checked</ion-checkbox>
<ion-checkbox justify="start" checked style="--checkmark-width: 7">Checkmark Width</ion-checkbox>
<ion-checkbox justify="start" checked class="checkbox-part">Checkmark Shadow Part</ion-checkbox>
<ion-checkbox justify="start" checked style="--size: 100px">--size</ion-checkbox>
</div>
</ion-content>
</ion-app>
</body>

View File

@@ -0,0 +1,26 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: color', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test('should apply color when checked', async ({ page }) => {
await page.setContent(`
<ion-checkbox color="danger" checked="true">Label</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-color-checked-${page.getSnapshotSettings()}.png`);
});
test('should not apply color when unchecked', async ({ page }) => {
await page.setContent(`
<ion-checkbox color="danger">Label</ion-checkbox>
`);
const checkbox = page.locator('ion-checkbox');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-color-unchecked-${page.getSnapshotSettings()}.png`);
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -2,11 +2,12 @@ import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: indeterminate', () => {
test('should not have visual regressions', async ({ page }) => {
test('should not have visual regressions', async ({ page, skip }) => {
skip.rtl();
await page.goto(`/src/components/checkbox/test/indeterminate`);
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`checkbox-indeterminate-${page.getSnapshotSettings()}.png`);
const checkbox = page.locator('ion-checkbox:first-child');
expect(await checkbox.screenshot()).toMatchSnapshot(`checkbox-indeterminate-${page.getSnapshotSettings()}.png`);
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

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