feat(checkbox): implement indeterminate state (#16951)

This adds an `indeterminate` prop to the `ion-checkbox` component, which visually renders the checkbox with a dash to indicate an indeterminate state.

closes #16943
This commit is contained in:
Simon Hänisch
2019-03-04 17:16:41 +01:00
committed by Brandy Carney
parent 28fd75ee6b
commit c641ae10ed
10 changed files with 278 additions and 25 deletions

View File

@ -140,7 +140,7 @@ export class IonCardTitle {
proxyInputs(IonCardTitle, ['color', 'mode']); proxyInputs(IonCardTitle, ['color', 'mode']);
export declare interface IonCheckbox extends StencilComponents<'IonCheckbox'> {} export declare interface IonCheckbox extends StencilComponents<'IonCheckbox'> {}
@Component({ selector: 'ion-checkbox', changeDetection: 0, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'name', 'checked', 'disabled', 'value'] }) @Component({ selector: 'ion-checkbox', changeDetection: 0, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'name', 'checked', 'indeterminate', 'disabled', 'value'] })
export class IonCheckbox { export class IonCheckbox {
ionChange!: EventEmitter<CustomEvent>; ionChange!: EventEmitter<CustomEvent>;
ionFocus!: EventEmitter<CustomEvent>; ionFocus!: EventEmitter<CustomEvent>;
@ -152,7 +152,7 @@ export class IonCheckbox {
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
} }
} }
proxyInputs(IonCheckbox, ['color', 'mode', 'name', 'checked', 'disabled', 'value']); proxyInputs(IonCheckbox, ['color', 'mode', 'name', 'checked', 'indeterminate', 'disabled', 'value']);
export declare interface IonChip extends StencilComponents<'IonChip'> {} export declare interface IonChip extends StencilComponents<'IonChip'> {}
@Component({ selector: 'ion-chip', changeDetection: 0, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'outline'] }) @Component({ selector: 'ion-chip', changeDetection: 0, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'outline'] })

View File

@ -195,6 +195,7 @@ ion-checkbox,shadow
ion-checkbox,prop,checked,boolean,false,false,false ion-checkbox,prop,checked,boolean,false,false,false
ion-checkbox,prop,color,string | undefined,undefined,false,false ion-checkbox,prop,color,string | undefined,undefined,false,false
ion-checkbox,prop,disabled,boolean,false,false,false ion-checkbox,prop,disabled,boolean,false,false,false
ion-checkbox,prop,indeterminate,boolean,false,false,false
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
ion-checkbox,prop,name,string,this.inputId,false,false ion-checkbox,prop,name,string,this.inputId,false,false
ion-checkbox,prop,value,string,'on',false,false ion-checkbox,prop,value,string,'on',false,false

View File

@ -749,6 +749,10 @@ export namespace Components {
*/ */
'disabled': boolean; 'disabled': boolean;
/** /**
* If `true`, the checkbox will visually appear as indeterminate.
*/
'indeterminate': boolean;
/**
* The mode determines which platform styles to use. * The mode determines which platform styles to use.
*/ */
'mode': Mode; 'mode': Mode;
@ -775,6 +779,10 @@ export namespace Components {
*/ */
'disabled'?: boolean; 'disabled'?: boolean;
/** /**
* If `true`, the checkbox will visually appear as indeterminate.
*/
'indeterminate'?: boolean;
/**
* The mode determines which platform styles to use. * The mode determines which platform styles to use.
*/ */
'mode'?: Mode; 'mode'?: Mode;

View File

@ -16,8 +16,8 @@
// Size // Size
--size: #{$checkbox-ios-icon-size}; --size: #{$checkbox-ios-icon-size};
width: var(--size);
width: var(--size);
height: var(--size); height: var(--size);
} }
@ -39,6 +39,7 @@
@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); @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);
display: block; display: block;
position: static; position: static;
} }

View File

@ -13,23 +13,28 @@
// Background // Background
--background: #{$checkbox-md-icon-background-color-off}; --background: #{$checkbox-md-icon-background-color-off};
// Transition
--transition: #{background $checkbox-md-transition-duration $checkbox-md-transition-easing}; --transition: #{background $checkbox-md-transition-duration $checkbox-md-transition-easing};
// Size // Size
--size: #{$checkbox-md-icon-size}; --size: #{$checkbox-md-icon-size};
width: var(--size);
width: var(--size);
height: var(--size); height: var(--size);
} }
.checkbox-icon path { .checkbox-icon path {
stroke-dasharray: 30; stroke-dasharray: 30;
stroke-dashoffset: 30; stroke-dashoffset: 30;
stroke-width: 3; stroke-width: 3;
} }
:host(.checkbox-checked) .checkbox-icon path { // Material Design Checkbox: Checked / Indeterminate
// --------------------------------------------------------
:host(.checkbox-checked) .checkbox-icon path,
:host(.checkbox-indeterminate) .checkbox-icon path {
stroke-dashoffset: 0; stroke-dashoffset: 0;
transition: stroke-dashoffset 90ms linear 90ms; transition: stroke-dashoffset 90ms linear 90ms;
@ -37,21 +42,23 @@
// Material Design Checkbox: Disabled // Material Design Checkbox: Disabled
// ----------------------------------------- // --------------------------------------------------------
// TODO .item-md.item-checkbox-disabled ion-label // TODO .item-md.item-checkbox-disabled ion-label
:host(.checkbox-disabled) { :host(.checkbox-disabled) {
opacity: $checkbox-md-disabled-opacity; opacity: $checkbox-md-disabled-opacity;
} }
// Material Design Checkbox Within An Item // Material Design Checkbox Within An Item
// ----------------------------------------- // --------------------------------------------------------
:host(.in-item) { :host(.in-item) {
// end position by default // 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); @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);
display: block; display: block;
position: static; position: static;
} }

View File

@ -6,14 +6,18 @@
:host { :host {
/** /**
* @prop --size: Size of the checkbox icon * @prop --size: Size of the checkbox icon
*
* @prop --background: Background of the checkbox icon * @prop --background: Background of the checkbox icon
* @prop --background-checked: Background of the checkbox icon when checked
*
* @prop --border-color: Border color of the checkbox icon * @prop --border-color: Border color of the checkbox icon
* @prop --border-radius: Border radius of the checkbox icon * @prop --border-radius: Border radius of the checkbox icon
* @prop --border-width: Border width of the checkbox icon * @prop --border-width: Border width of the checkbox icon
* @prop --border-style: Border style of the checkbox icon * @prop --border-style: Border style of the checkbox icon
* @prop --transition: Transition of the checkbox icon
* @prop --background-checked: Background of the checkbox icon when checked
* @prop --border-color-checked: Border color of the checkbox icon when checked * @prop --border-color-checked: Border color of the checkbox icon when checked
*
* @prop --transition: Transition of the checkbox icon
*
* @prop --checkmark-color: Color of the checkbox checkmark when checked * @prop --checkmark-color: Color of the checkbox checkmark when checked
*/ */
--background-checked: #{ion-color(primary, base)}; --background-checked: #{ion-color(primary, base)};
@ -67,19 +71,23 @@ button {
opacity: 0; opacity: 0;
} }
// Checked Checkbox
// Checked / Indeterminate Checkbox
// --------------------------------------------- // ---------------------------------------------
:host(.checkbox-checked) .checkbox-icon { :host(.checkbox-checked) .checkbox-icon,
:host(.checkbox-indeterminate) .checkbox-icon {
border-color: var(--border-color-checked); border-color: var(--border-color-checked);
background: var(--background-checked); background: var(--background-checked);
} }
:host(.checkbox-checked) .checkbox-icon path { :host(.checkbox-checked) .checkbox-icon path,
:host(.checkbox-indeterminate) .checkbox-icon path {
opacity: 1; opacity: 1;
} }
// Disabled Checkbox // Disabled Checkbox
// --------------------------------------------- // ---------------------------------------------

View File

@ -41,6 +41,11 @@ export class Checkbox implements ComponentInterface {
*/ */
@Prop({ mutable: true }) checked = false; @Prop({ mutable: true }) checked = false;
/**
* If `true`, the checkbox will visually appear as indeterminate.
*/
@Prop({ mutable: true }) indeterminate = false;
/** /**
* If `true`, the user cannot interact with the checkbox. * If `true`, the user cannot interact with the checkbox.
*/ */
@ -101,6 +106,7 @@ export class Checkbox implements ComponentInterface {
onClick() { onClick() {
this.setFocus(); this.setFocus();
this.checked = !this.checked; this.checked = !this.checked;
this.indeterminate = false;
} }
private setFocus() { private setFocus() {
@ -134,6 +140,7 @@ export class Checkbox implements ComponentInterface {
'in-item': hostContext('ion-item', el), 'in-item': hostContext('ion-item', el),
'checkbox-checked': checked, 'checkbox-checked': checked,
'checkbox-disabled': disabled, 'checkbox-disabled': disabled,
'checkbox-indeterminate': this.indeterminate,
'interactive': true 'interactive': true
} }
}; };
@ -142,12 +149,19 @@ export class Checkbox implements ComponentInterface {
render() { render() {
renderHiddenInput(true, this.el, this.name, (this.checked ? this.value : ''), this.disabled); renderHiddenInput(true, this.el, this.name, (this.checked ? this.value : ''), this.disabled);
let path = this.indeterminate
? <path d="M6 12L18 12"/>
: <path d="M5.9,12.5l3.8,3.8l8.8-8.8" />;
if (this.mode === 'md') {
path = this.indeterminate
? <path d="M2 12H22"/>
: <path d="M1.73,12.91 8.1,19.28 22.79,4.59"/>;
}
return [ return [
<svg class="checkbox-icon" viewBox="0 0 24 24"> <svg class="checkbox-icon" viewBox="0 0 24 24">
{ this.mode === 'md' {path}
? <path d="M1.73,12.91 8.1,19.28 22.79,4.59"></path>
: <path d="M5.9,12.5l3.8,3.8l8.8-8.8"/>
}
</svg>, </svg>,
<button <button
type="button" type="button"

View File

@ -189,10 +189,11 @@ export default CheckboxExample;
## Properties ## Properties
| Property | Attribute | Description | Type | Default | | Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------- | | --------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------- |
| `checked` | `checked` | If `true`, the checkbox is selected. | `boolean` | `false` | | `checked` | `checked` | If `true`, the checkbox is selected. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | | `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the checkbox. | `boolean` | `false` | | `disabled` | `disabled` | If `true`, the user cannot interact with the checkbox. | `boolean` | `false` |
| `indeterminate` | `indeterminate` | If `true`, the checkbox will visually appear as indeterminate. | `boolean` | `false` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | | `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` | | `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` |
| `value` | `value` | The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`. | `string` | `'on'` | | `value` | `value` | The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`. | `string` | `'on'` |

View File

@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('checkbox: indeterminate', async () => {
const page = await newE2EPage({
url: '/src/components/checkbox/test/indeterminate?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Checkbox - Indeterminate</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 src="../../../../../dist/ionic.js"></script>
</head>
<body onLoad="onLoad()">
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Checkbox - Indeterminate</ion-title>
</ion-toolbar>
</ion-header>
<ion-content id="content">
<ion-list-header>
Native
</ion-list-header>
<div class="ion-padding-start">
<!-- Default to unchecked -->
<label for="unchecked">Unchecked</label>
<input name="unchecked" type="checkbox">
<br>
<!-- Default to checked -->
<label for="checked">Checked</label>
<input name="checked" type="checkbox" checked />
<br>
<!-- Default to indeterminate -->
<label for="indeterminate">Indeterminate</label>
<input name="indeterminate" type="checkbox" class="indeterminate">
<br>
<!-- Default to checked / indeterminate -->
<label for="both">Checked / Indeterminate</label>
<input name="both" type="checkbox" checked class="indeterminate">
<br>
</div>
<ion-list-header>
Ionic
</ion-list-header>
<ion-item>
<ion-label>Unchecked</ion-label>
<ion-checkbox slot="end"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checked</ion-label>
<ion-checkbox slot="end" checked></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Indeterminate</ion-label>
<ion-checkbox slot="end" indeterminate></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checked / Indeterminate</ion-label>
<ion-checkbox slot="end" checked indeterminate></ion-checkbox>
</ion-item>
<ion-list-header>
Colors
</ion-list-header>
<div class="ion-padding-start">
<ion-checkbox indeterminate></ion-checkbox>
<ion-checkbox indeterminate color="secondary"></ion-checkbox>
<ion-checkbox indeterminate color="tertiary"></ion-checkbox>
<ion-checkbox indeterminate color="success"></ion-checkbox>
<ion-checkbox indeterminate color="warning"></ion-checkbox>
<ion-checkbox indeterminate color="danger"></ion-checkbox>
<ion-checkbox indeterminate color="dark"></ion-checkbox>
<ion-checkbox indeterminate color="medium"></ion-checkbox>
<ion-checkbox indeterminate color="light"></ion-checkbox>
</div>
<ion-list-header>
Parent
</ion-list-header>
<ul>
<li>
<ion-checkbox name="tall" id="tall" indeterminate></ion-checkbox>
<label for="tall">Tall Things</label>
<ul>
<li>
<ion-checkbox name="tall-1" id="tall-1" checked></ion-checkbox>
<label for="tall-1">Skyscrapers</label>
</li>
<li>
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
<label for="tall-2">Trees</label>
</li>
<li>
<ion-checkbox name="tall-2" id="tall-2"></ion-checkbox>
<label for="tall-2">Giants</label>
</li>
</ul>
</li>
</ul>
</ion-content>
</ion-app>
<style>
ul {
list-style: none;
margin: 5px 20px;
padding: 0;
}
li {
margin: 10px 0;
}
ul label {
display: inline-block;
vertical-align: top;
margin-top: 4px;
}
</style>
<script>
var indeterminateCheckboxes = document.getElementsByClassName("indeterminate");
for (var i = 0; i < indeterminateCheckboxes.length; i++) {
var checkbox = indeterminateCheckboxes[i];
checkbox.indeterminate = true;
}
function onLoad() {
var checkboxes = document.getElementsByTagName("ion-checkbox");
for (var i = 0; i < checkboxes.length; i++) {
var checkbox = checkboxes[i];
checkbox.addEventListener('ionChange', function (event) {
checkboxChanged(this, event);
});
}
}
function checkboxChanged(el, ev) {
var isParent = el.id === "tall";
if (isParent) {
checkChildren(el.checked);
} else {
checkParent();
}
}
function checkParent() {
var parent = document.getElementById("tall");
var children = getChildren();
var countChecked = 0;
for(var i = 0; i < children.length; i++) {
var child = children[i];
if (child.checked) {
countChecked = ++countChecked;
}
}
// None checked, uncheck parent
if (countChecked == 0) {
parent.checked = false;
parent.indeterminate = false;
// All checked, check parent
} else if (countChecked == children.length) {
parent.checked = true;
parent.indeterminate = false;
// One checked, indeterminate parent
} else {
parent.indeterminate = true;
}
}
function checkChildren(shouldCheck) {
var children = getChildren();
for (var i = 0; i < children.length; i++) {
var child = children[i];
child.checked = shouldCheck;
}
}
function getChildren() {
return document.querySelectorAll("ion-checkbox[name^=tall-]");
}
</script>
</body>
</html>