feat(checkbox): add ionic theme styling (#29335)
Issue number: N/A --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Checkbox does not have an ionic theme implementation. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Adds the ionic theme styles and initial features for checkbox. - This PR is a combination of all the individual PRs already reviewed. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> We should confirm that the visual styles and behavior function as we expect with all the changes combined in this PR. --------- Co-authored-by: João Ferreira <60441552+JoaoFerreira-FrontEnd@users.noreply.github.com> Co-authored-by: ionitron <hi@ionicframework.com>
@ -322,6 +322,8 @@ ion-checkbox,prop,justify,"end" | "space-between" | "start",'space-between',fals
|
||||
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
|
||||
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-checkbox,prop,name,string,this.inputId,false,false
|
||||
ion-checkbox,prop,shape,"rectangular" | "soft" | undefined,'soft',false,false
|
||||
ion-checkbox,prop,size,"small" | undefined,undefined,false,false
|
||||
ion-checkbox,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
ion-checkbox,prop,value,any,'on',false,false
|
||||
ion-checkbox,event,ionBlur,void,true
|
||||
|
16
core/src/components.d.ts
vendored
@ -728,6 +728,14 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* Set to `"soft"` for a checkbox with more rounded corners. Only available when the theme is `"ionic"`.
|
||||
*/
|
||||
"shape"?: 'soft' | 'rectangular';
|
||||
/**
|
||||
* Set to `"small"` for a checkbox with less height and padding.
|
||||
*/
|
||||
"size"?: 'small';
|
||||
/**
|
||||
* The theme determines the visual appearance of the component.
|
||||
*/
|
||||
@ -5972,6 +5980,14 @@ declare namespace LocalJSX {
|
||||
* Emitted when the checkbox has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonCheckboxCustomEvent<void>) => void;
|
||||
/**
|
||||
* Set to `"soft"` for a checkbox with more rounded corners. Only available when the theme is `"ionic"`.
|
||||
*/
|
||||
"shape"?: 'soft' | 'rectangular';
|
||||
/**
|
||||
* Set to `"small"` for a checkbox with less height and padding.
|
||||
*/
|
||||
"size"?: 'small';
|
||||
/**
|
||||
* The theme determines the visual appearance of the component.
|
||||
*/
|
||||
|
137
core/src/components/checkbox/checkbox.ionic.scss
Normal file
@ -0,0 +1,137 @@
|
||||
@import "./checkbox";
|
||||
@import "./checkbox.ionic.vars";
|
||||
|
||||
// Ionic Checkbox
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
// Border
|
||||
--border-radius: #{$checkbox-ionic-border-radius};
|
||||
--border-width: #{$checkbox-ionic-border-width};
|
||||
--border-style: #{$checkbox-ionic-border-style};
|
||||
--border-color: #{$checkbox-ionic-background-color-off};
|
||||
--checkmark-width: 3;
|
||||
|
||||
// Focus
|
||||
--focus-ring-color: #9ec4fd;
|
||||
--focus-ring-width: 2px;
|
||||
--focus-ring-offset: 2px;
|
||||
|
||||
// Size
|
||||
--size: #{$checkbox-ionic-size};
|
||||
|
||||
// Checkbox Target area
|
||||
// --------------------------------------------------
|
||||
&::after {
|
||||
@include position(50%, 0, null, 0);
|
||||
|
||||
position: absolute;
|
||||
|
||||
height: 100%;
|
||||
min-height: 48px;
|
||||
|
||||
transform: translateY(-50%);
|
||||
|
||||
content: "";
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.native-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
// Ionic Design Checkbox Sizes
|
||||
// --------------------------------------------------
|
||||
:host(.checkbox-size-small) {
|
||||
// Size
|
||||
--size: #{$checkbox-ionic-small-size};
|
||||
}
|
||||
|
||||
// Ionic Design Checkbox Invalid
|
||||
// --------------------------------------------------
|
||||
:host(.ion-invalid) {
|
||||
--focus-ring-color: #ffafaf;
|
||||
|
||||
.checkbox-icon {
|
||||
border-color: #f72c2c;
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox Disabled
|
||||
// --------------------------------------------------
|
||||
// disabled, indeterminate checkbox
|
||||
:host(.checkbox-disabled.checkbox-indeterminate) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
border-width: 0;
|
||||
|
||||
background-color: #{$background-color-step-600};
|
||||
}
|
||||
|
||||
// disabled, unchecked checkbox
|
||||
:host(.checkbox-disabled) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
border-color: #c9c9c9;
|
||||
|
||||
background-color: #f5f5f5; // mix of #f5f5f5 with 60% #FFF
|
||||
}
|
||||
|
||||
// disabled, checked checkbox
|
||||
:host(.checkbox-disabled.checkbox-checked) .checkbox-icon {
|
||||
border-width: 0;
|
||||
|
||||
background-color: $background-color-step-100;
|
||||
}
|
||||
|
||||
// Checkbox Hover
|
||||
// --------------------------------------------------------
|
||||
@media (any-hover: hover) {
|
||||
:host(:hover) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
background-color: #ececec; // mix of 'white', '#121212', 0.08, 'rgb'
|
||||
}
|
||||
|
||||
:host(:hover.checkbox-checked) .checkbox-icon,
|
||||
:host(:hover.checkbox-indeterminate) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
background-color: #1061da; // mix of '#1068eb', '#121212', 0.08, 'rgb'
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox Focus
|
||||
// --------------------------------------------------
|
||||
// Only show the focus ring when the checkbox is focused and not disabled
|
||||
:host(.ion-focused:not(.checkbox-disabled)) .checkbox-icon {
|
||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
|
||||
// Checkbox: Active
|
||||
// --------------------------------------------------------
|
||||
:host(.ion-activated) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
background-color: #e3e3e3; // mix of 'white', '#121212', 0.12, 'rgb'
|
||||
}
|
||||
|
||||
:host(.ion-activated.checkbox-checked) .checkbox-icon,
|
||||
:host(.ion-activated.checkbox-indeterminate) .checkbox-icon {
|
||||
/* TODO(FW-6183): Use design token variables */
|
||||
background-color: #105ed1; // mix of '#1068eb', '#121212', 0.12, 'rgb'
|
||||
}
|
||||
|
||||
// Ionic Design Checkbox Shapes
|
||||
// --------------------------------------------------
|
||||
:host(.checkbox-shape-soft) {
|
||||
--border-radius: #{$checkbox-ionic-border-radius};
|
||||
}
|
||||
|
||||
:host(.checkbox-shape-rectangular) {
|
||||
--border-radius: #{$checkbox-ionic-rectangular-border};
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
min-height: 48px;
|
||||
}
|
31
core/src/components/checkbox/checkbox.ionic.vars.scss
Normal file
@ -0,0 +1,31 @@
|
||||
@import "../../themes/ionic.globals.ionic";
|
||||
|
||||
// Ionic Checkbox Variables
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - The default width and height of the checkbox
|
||||
$checkbox-ionic-size: 24px !default;
|
||||
|
||||
/// @prop - The background color of the checkbox when the checkbox is unchecked
|
||||
$checkbox-ionic-background-color-off: $background-color-step-400 !default;
|
||||
|
||||
/// @prop - Border style of the checkbox
|
||||
$checkbox-ionic-border-style: solid !default;
|
||||
|
||||
/// @prop - Border width of the checkbox
|
||||
$checkbox-ionic-border-width: 1px !default;
|
||||
|
||||
/// @prop - The border radius of the checkbox
|
||||
/// With a default size of 24px, the border radius is calculated as 24px / 4 - 2px = 4px
|
||||
/// With a small size of 16px, the border radius is calculated as 16px / 4 - 2px = 2px;
|
||||
$checkbox-ionic-border-radius: calc(var(--size) / 4 - 2px) !default;
|
||||
|
||||
/// @prop - Icon size of the checkbox for the small size
|
||||
$checkbox-ionic-small-size: 16px !default;
|
||||
|
||||
// Checkbox Shapes
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
/* Rectangular */
|
||||
/// @prop - Rectangular border radius of the checkbox
|
||||
$checkbox-ionic-rectangular-border: 0 !default;
|
@ -24,7 +24,7 @@ import type { CheckboxChangeEventDetail } from './checkbox-interface';
|
||||
styleUrls: {
|
||||
ios: 'checkbox.ios.scss',
|
||||
md: 'checkbox.md.scss',
|
||||
ionic: 'checkbox.md.scss',
|
||||
ionic: 'checkbox.ionic.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
@ -98,6 +98,16 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@Prop() alignment: 'start' | 'center' = 'center';
|
||||
|
||||
/**
|
||||
* Set to `"small"` for a checkbox with less height and padding.
|
||||
*/
|
||||
@Prop() size?: 'small';
|
||||
|
||||
/**
|
||||
* Set to `"soft"` for a checkbox with more rounded corners. Only available when the theme is `"ionic"`.
|
||||
*/
|
||||
@Prop() shape?: 'soft' | 'rectangular' = 'soft';
|
||||
|
||||
/**
|
||||
* Emitted when the checked property has changed
|
||||
* as a result of a user action such as a click.
|
||||
@ -181,6 +191,8 @@ export class Checkbox implements ComponentInterface {
|
||||
name,
|
||||
value,
|
||||
alignment,
|
||||
size,
|
||||
shape,
|
||||
} = this;
|
||||
const theme = getIonTheme(this);
|
||||
|
||||
@ -201,6 +213,8 @@ export class Checkbox implements ComponentInterface {
|
||||
[`checkbox-justify-${justify}`]: true,
|
||||
[`checkbox-alignment-${alignment}`]: true,
|
||||
[`checkbox-label-placement-${labelPlacement}`]: true,
|
||||
[`checkbox-size-${size}`]: size !== undefined,
|
||||
[`checkbox-shape-${shape}`]: true,
|
||||
})}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
@ -252,6 +266,12 @@ export class Checkbox implements ComponentInterface {
|
||||
) : (
|
||||
<path d="M1.73,12.91 8.1,19.28 22.79,4.59" part="mark" />
|
||||
);
|
||||
} else if (theme === 'ionic') {
|
||||
path = indeterminate ? (
|
||||
<path d="M6.5 12H17.5" stroke-linecap="round" part="mark" />
|
||||
) : (
|
||||
<path d="M6 12.5L10 16.5L18.5 8" stroke-linecap="round" stroke-linejoin="round" part="mark" />
|
||||
);
|
||||
}
|
||||
|
||||
return path;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('checkbox: basic visual tests'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
@ -123,3 +123,92 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('checkbox: basic visual tests'), () => {
|
||||
test('should have a small size applied correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<div id="checkboxes">
|
||||
<ion-checkbox size="small">Small</ion-checkbox>
|
||||
<ion-checkbox size="small" checked="true">Small - Checked</ion-checkbox>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const checkboxes = page.locator('#checkboxes');
|
||||
await expect(checkboxes).toHaveScreenshot(screenshot(`checkbox-small`));
|
||||
});
|
||||
|
||||
test('should have an invalid visual applied correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<div id="checkboxes" style="padding: 8px">
|
||||
<ion-checkbox class="ion-invalid">Invalid</ion-checkbox>
|
||||
<ion-checkbox class="ion-invalid ion-focused">Invalid</ion-checkbox>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const checkboxes = page.locator('#checkboxes');
|
||||
await expect(checkboxes).toHaveScreenshot(screenshot(`checkbox-invalid`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('checkbox: safe area'), () => {
|
||||
test('should click the safe area of a small checkbox', async ({ page }) => {
|
||||
await page.setContent(`<ion-checkbox size="small">Small</ion-checkbox>`, config);
|
||||
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
const box = await checkbox.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + 47);
|
||||
}
|
||||
await expect(checkbox).toBeFocused();
|
||||
});
|
||||
|
||||
test('should click the safe area of a default checkbox', async ({ page }) => {
|
||||
await page.setContent(`<ion-checkbox>Default</ion-checkbox>`, config);
|
||||
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
const box = await checkbox.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + 47);
|
||||
}
|
||||
await expect(checkbox).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('checkbox: shapes'), () => {
|
||||
test('should have a soft shape applied correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<div id="checkboxes">
|
||||
<ion-checkbox >soft</ion-checkbox>
|
||||
<ion-checkbox shape="soft">Soft</ion-checkbox>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const checkboxes = page.locator('#checkboxes');
|
||||
await expect(checkboxes).toHaveScreenshot(screenshot(`checkbox-shape-soft`));
|
||||
});
|
||||
|
||||
test('should have a rectangular shape applied correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<div id="checkboxes">
|
||||
<ion-checkbox shape="rectangular">Rectangular</ion-checkbox>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const checkboxes = page.locator('#checkboxes');
|
||||
await expect(checkboxes).toHaveScreenshot(screenshot(`checkbox-shape-rectangular`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.4 KiB |
@ -9,7 +9,7 @@ import { configs, test } from '@utils/test/playwright';
|
||||
* we set the width of the checkbox so we can
|
||||
* see the justification results.
|
||||
*/
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('checkbox: label'), () => {
|
||||
test.describe('checkbox: start placement', () => {
|
||||
test('should render a start justification with label in the start position', async ({ page }) => {
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |