From 730105964ab8c645e3c1f09fbc6fdcb3b924f1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Mon, 25 Mar 2024 13:06:30 +0000 Subject: [PATCH] Add new theme to checkbox - add features for ionic theme of checkbox; --- core/api.txt | 4 + core/src/components.d.ts | 24 ++ .../components/checkbox/checkbox.ionic.scss | 166 ++++++++++ .../checkbox/checkbox.ionic.vars.scss | 63 ++++ core/src/components/checkbox/checkbox.tsx | 31 +- .../checkbox/test/theme-ionic/checkbox.e2e.ts | 43 +++ .../checkbox/test/theme-ionic/index.html | 300 ++++++++++++++++++ packages/angular/src/directives/proxies.ts | 4 +- packages/vue/src/proxies.ts | 3 + 9 files changed, 635 insertions(+), 3 deletions(-) create mode 100644 core/src/components/checkbox/checkbox.ionic.scss create mode 100644 core/src/components/checkbox/checkbox.ionic.vars.scss create mode 100644 core/src/components/checkbox/test/theme-ionic/checkbox.e2e.ts create mode 100644 core/src/components/checkbox/test/theme-ionic/index.html diff --git a/core/api.txt b/core/api.txt index 3b7214ae15..97c2a45329 100644 --- a/core/api.txt +++ b/core/api.txt @@ -321,6 +321,9 @@ 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,required,boolean,false,false,false +ion-checkbox,prop,shape,"rectangular" | "soft" | undefined,'soft',false,true +ion-checkbox,prop,size,"default" | "small" | undefined,'default',false,true ion-checkbox,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-checkbox,prop,value,any,'on',false,false ion-checkbox,event,ionBlur,void,true @@ -338,6 +341,7 @@ ion-checkbox,css-prop,--checkmark-width ion-checkbox,css-prop,--size ion-checkbox,css-prop,--transition ion-checkbox,part,container +ion-checkbox,part,focus-ring ion-checkbox,part,label ion-checkbox,part,mark diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 91503120f1..00c4895bc9 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -724,6 +724,18 @@ export namespace Components { * The name of the control, which is submitted with the form data. */ "name": string; + /** + * If `true`, the checkbox will be presented with an error style when it is unchecked. + */ + "required": boolean; + /** + * Set to `"soft"` for a checkbox with more rounded corners. + */ + "shape"?: 'soft' | 'rectangular'; + /** + * Set to `"small"` for a checkbox with less height and padding or to `"default"` for a checkbox with the default height and padding. + */ + "size"?: 'small' | 'default'; /** * The theme determines the visual appearance of the component. */ @@ -5926,6 +5938,18 @@ declare namespace LocalJSX { * Emitted when the checkbox has focus. */ "onIonFocus"?: (event: IonCheckboxCustomEvent) => void; + /** + * If `true`, the checkbox will be presented with an error style when it is unchecked. + */ + "required"?: boolean; + /** + * Set to `"soft"` for a checkbox with more rounded corners. + */ + "shape"?: 'soft' | 'rectangular'; + /** + * Set to `"small"` for a checkbox with less height and padding or to `"default"` for a checkbox with the default height and padding. + */ + "size"?: 'small' | 'default'; /** * The theme determines the visual appearance of the component. */ diff --git a/core/src/components/checkbox/checkbox.ionic.scss b/core/src/components/checkbox/checkbox.ionic.scss new file mode 100644 index 0000000000..65aa92c09e --- /dev/null +++ b/core/src/components/checkbox/checkbox.ionic.scss @@ -0,0 +1,166 @@ +@import "./checkbox"; +@import "./checkbox.ionic.vars"; + +// ionic Checkbox +// -------------------------------------------------- + +:host { + // Border + --border-radius: calc(var(--size) * .125); + --border-width: #{$checkbox-ionic-icon-border-width}; + --border-style: #{$checkbox-ionic-icon-border-style}; + --border-color: #{$checkbox-ionic-icon-border-color-off}; + --checkmark-width: 3; + --padding-top: #{$checkbox-ionic-padding-top}; + --padding-bottom: #{$checkbox-ionic-padding-bottom}; + + // Background + --checkbox-background: #{$checkbox-ionic-icon-background-color-off}; + + // Transition + --transition: #{background $checkbox-ionic-transition-duration $checkbox-ionic-transition-easing}; + + // Size + --size: #{$checkbox-ionic-icon-size}; + // add to existing selector + + // margin is required to make room for outline on focus, otherwise the outline may get cut off + @include margin($checkbox-ionic-outline-width); + @include padding(--padding-top, null, --padding-bottom, null); + + // 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-small) { + --padding-top: #{$checkbox-ionic-small-padding-top}; + --padding-bottom: #{$checkbox-ionic-small-padding-bottom}; + + // Size + --size: #{$checkbox-ionic-small-icon-size}; +} + +// Ionic Design Checkbox Shapes +// -------------------------------------------------- +:host(.checkbox-rectangular) { + --border-radius: #{$checkbox-ionic-rectangular-border}; +} + +// Ionic Design Checkbox Disabled +// -------------------------------------------------- +// disabled, indeterminate checkbox +:host(.checkbox-disabled.checkbox-indeterminate) .checkbox-icon { + border-width: 0; + + background-color: #74aafc; +} + +// disabled, unchecked checkbox +:host(.checkbox-disabled) .checkbox-icon { + border-color: #c9c9c9; + + background-color: #f5f5f5; +} +// disabled, checked checkbox +:host(.checkbox-disabled.checkbox-checked) .checkbox-icon { + border-width: 0; + + background-color: #A8C8F8; +} + +// Ionic Design Checkbox Required State +// -------------------------------------------------- +// Unhecked checkbox with `required` property set to true +:host(.checkbox-required:not(.checkbox-checked):not(.checkbox-indeterminate)) { + .checkbox-icon { + border-color: #f72c2c; + } +} + +// Focused: Unchecked checkbox with `required` property set to true +:host(.ion-focusable.checkbox-required:not(.checkbox-checked):not(.checkbox-indeterminate)) .checkbox-icon { + outline-color: #ffafaf; +} + +// Ionic Design Checkbox Focus Ring +// -------------------------------------------------- +:host(.ion-focused:not(.checkbox-disabled)) .focus-ring { + @include position(-4px, -4px, -4px, -4px); + position: absolute; + + width: calc(100% + 8px); + height: calc(100% + 8px); + + transition: border-color 0.3s; + + border: 2px solid $checkbox-ionic-focus-ring-color; + + /* stylelint-disable-next-line property-disallowed-list */ + border-radius: var(--border-radius); + + content: ""; + box-sizing: border-box; +} + +// Required state +:host(.ion-focused.checkbox-required) .focus-ring { + border-color:$checkbox-ionic-focus-required-ring-color; +} + +// Checkbox: Hover +// -------------------------------------------------------- +@media (any-hover: hover) { +:host(:hover) .checkbox-icon { + background-color: #ececec; // mix of 'white', '#121212', 0.08, 'rgb' +} + +:host(:hover.checkbox-checked) .checkbox-icon, +:host(:hover.checkbox-indeterminate) .checkbox-icon { + background-color: #1061da; // mix of '#1068eb', '#121212', 0.08, 'rgb' +} + +// unchecked checkbox with `required` property set to true +:host(:hover.checkbox-required:not(.checkbox-checked):not(.checkbox-indeterminate)) { + .checkbox-icon { + border-color: #ee2b2b; + } +} +} + +// Checkbox: Active +// -------------------------------------------------------- +:host(.ion-activated) .checkbox-icon { + background-color: #e3e3e3; // mix of 'white', '#121212', 0.12, 'rgb' +} + +:host(.ion-activated.checkbox-checked) .checkbox-icon, +:host(.ion-activated.checkbox-indeterminate) .checkbox-icon { + background-color: #105ed1; // mix of '#1068eb', '#121212', 0.12, 'rgb' +} + +:host(.checkbox-label-placement-start) { + display: flex; + + justify-content: space-between; +} \ No newline at end of file diff --git a/core/src/components/checkbox/checkbox.ionic.vars.scss b/core/src/components/checkbox/checkbox.ionic.vars.scss new file mode 100644 index 0000000000..66e493298e --- /dev/null +++ b/core/src/components/checkbox/checkbox.ionic.vars.scss @@ -0,0 +1,63 @@ +@import "../../themes/ionic.globals.md"; +@import "../item/item.md.vars"; + +// ionic Checkbox +// -------------------------------------------------- + +/// @prop - Background color of the checkbox icon when off +$checkbox-ionic-icon-background-color-off: $item-md-background !default; + +/// @prop - Size of the checkbox icon +/// The icon size does not use dynamic font +/// because it does not scale in native. +$checkbox-ionic-icon-size: 24px !default; + +/// @prop - Icon size of the checkbox for the small size +$checkbox-ionic-small-icon-size: 16px !default; + +/// @prop - Border width of the checkbox icon +$checkbox-ionic-icon-border-width: 1px !default; + +/// @prop - Border style of the checkbox icon +$checkbox-ionic-icon-border-style: solid !default; + +/// @prop - Border color of the checkbox icon when off +$checkbox-ionic-icon-border-color-off: #9a9a9a !default; + +/// @prop - Outline width of the checkbox +$checkbox-ionic-outline-width: 2px !default; + +/// @prop - Padding top of the checkbox for the default size +$checkbox-ionic-padding-top: 12px !default; + +/// @prop - Padding bottom of the button for the default size +$checkbox-ionic-padding-bottom: 12px !default; + +/// @prop - Padding top of the checkbox for the small size +$checkbox-ionic-small-padding-top: 16px !default; + +/// @prop - Padding bottom of the button for the small size +$checkbox-ionic-small-padding-bottom: 16px !default; + +/// @prop - Focus color of the checkbox +$checkbox-ionic-focus-ring-color: #9ec4fd !default; + +/// @prop - Focus color of the required checkbox +$checkbox-ionic-focus-required-ring-color: #FFAFAF !default; + +/// @prop - Transition duration of the checkbox +$checkbox-ionic-transition-duration: 180ms !default; + +/// @prop - Transition easing of the checkbox +$checkbox-ionic-transition-easing: cubic-bezier(.4, 0, .2, 1) !default; + + +// Checkbox Shapes +// ------------------------------------------------------------------------------- + +/* Rectangular */ +/// @prop - Rectangular border radius of the checkbox +$checkbox-ionic-rectangular-border: 0 !default; + + + diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 44b3d66ecd..fb6b55a93e 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -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,22 @@ export class Checkbox implements ComponentInterface { */ @Prop() alignment: 'start' | 'center' = 'center'; + /** + * If `true`, the checkbox will be presented with an error style when it is unchecked. + */ + @Prop() required = false; + + /** + * Set to `"soft"` for a checkbox with more rounded corners. + */ + @Prop({ reflect: true }) shape?: 'soft' | 'rectangular' = 'soft'; + + /** + * Set to `"small"` for a checkbox with less height and padding or to `"default"` + * for a checkbox with the default height and padding. + */ + @Prop({ reflect: true }) size?: 'small' | 'default' = 'default'; + /** * Emitted when the checked property has changed * as a result of a user action such as a click. @@ -181,6 +197,9 @@ export class Checkbox implements ComponentInterface { name, value, alignment, + required, + size, + shape, } = this; const theme = getIonTheme(this); @@ -201,6 +220,9 @@ export class Checkbox implements ComponentInterface { [`checkbox-justify-${justify}`]: true, [`checkbox-alignment-${alignment}`]: true, [`checkbox-label-placement-${labelPlacement}`]: true, + 'checkbox-required': required, + [`checkbox-${size}`]: true, + [`checkbox-${shape}`]: true, })} onClick={this.onClick} > @@ -233,6 +255,7 @@ export class Checkbox implements ComponentInterface { {path} + {theme === 'ionic' &&
} @@ -252,6 +275,12 @@ export class Checkbox implements ComponentInterface { ) : ( ); + } else if (theme === 'ionic') { + path = indeterminate ? ( + + ) : ( + + ); } return path; diff --git a/core/src/components/checkbox/test/theme-ionic/checkbox.e2e.ts b/core/src/components/checkbox/test/theme-ionic/checkbox.e2e.ts new file mode 100644 index 0000000000..d14f97bd82 --- /dev/null +++ b/core/src/components/checkbox/test/theme-ionic/checkbox.e2e.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import type { E2EPage } from '@utils/test/playwright'; +import { configs, test } from '@utils/test/playwright'; + +class CheckboxFixture { + readonly page: E2EPage; + readonly screenshotFn?: (file: string) => string; + + constructor(page: E2EPage, screenshot?: (file: string) => string) { + this.page = page; + this.screenshotFn = screenshot; + } + + async checkScreenshot(modifier: string) { + const { screenshotFn } = this; + + if (!screenshotFn) { + throw new Error( + 'A screenshot function is required to take a screenshot. Pass one in when creating CheckboxFixture.' + ); + } + + const wrapper = this.page.locator('#screenshot-wrapper'); + + await expect(wrapper).toHaveScreenshot(screenshotFn(`${modifier}-checkbox`)); + } +} + +configs({ themes: ['ionic'], modes: ['md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('checkbox: theme ionic'), () => { + let checkboxFixture!: CheckboxFixture; + + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/checkbox/test/theme-ionic`, config); + await page.setIonViewport(); + checkboxFixture = new CheckboxFixture(page, screenshot); + }); + + test('default', async () => { + await checkboxFixture.checkScreenshot(`default`); + }); + }); +}); diff --git a/core/src/components/checkbox/test/theme-ionic/index.html b/core/src/components/checkbox/test/theme-ionic/index.html new file mode 100644 index 0000000000..65ceba4514 --- /dev/null +++ b/core/src/components/checkbox/test/theme-ionic/index.html @@ -0,0 +1,300 @@ + + + + + Checkbox - Basic + + + + + + + + + + + + + Checkbox - Basic + + + + +
+ Label Placement Default + + + Unchecked + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + Checked + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + + Disabled + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + + Required + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Small Focused + + + Rect Focus + Rect Focus + + + + + + Indeterminate + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + +
+ +
+ Label Placement End + + + Unchecked + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + Checked + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + + Disabled + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + + + + + Required + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Small Focused + + + Rect Focus + Rect Focus + + + + + + Indeterminate + + + Default + Small + + + Default + Default + + + Rectangular + Rectangular + + + Focused + Focused + + + Rect Focus + Rect Focus + + +
+
+
+ + diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 259216a2c8..c90e0f0501 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -509,14 +509,14 @@ export declare interface IonCardTitle extends Components.IonCardTitle {} @ProxyCmp({ - inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'] + inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'shape', 'size', 'theme', 'value'] }) @Component({ selector: 'ion-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'], + inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'shape', 'size', 'theme', 'value'], }) export class IonCheckbox { protected el: HTMLElement; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index eab6fe4a36..637ab6bbb2 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -216,6 +216,9 @@ export const IonCheckbox = /*@__PURE__*/ defineContainer