mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 04:14:21 +08:00
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:

committed by
Brandy Carney

parent
28fd75ee6b
commit
c641ae10ed
@ -16,8 +16,8 @@
|
||||
|
||||
// Size
|
||||
--size: #{$checkbox-ios-icon-size};
|
||||
width: var(--size);
|
||||
|
||||
width: 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);
|
||||
|
||||
display: block;
|
||||
|
||||
position: static;
|
||||
}
|
||||
|
||||
|
@ -13,23 +13,28 @@
|
||||
|
||||
// Background
|
||||
--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);
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
|
||||
.checkbox-icon path {
|
||||
stroke-dasharray: 30;
|
||||
stroke-dashoffset: 30;
|
||||
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;
|
||||
|
||||
transition: stroke-dashoffset 90ms linear 90ms;
|
||||
@ -37,21 +42,23 @@
|
||||
|
||||
|
||||
// Material Design Checkbox: Disabled
|
||||
// -----------------------------------------
|
||||
// --------------------------------------------------------
|
||||
|
||||
// TODO .item-md.item-checkbox-disabled ion-label
|
||||
:host(.checkbox-disabled) {
|
||||
opacity: $checkbox-md-disabled-opacity;
|
||||
}
|
||||
|
||||
|
||||
// Material Design Checkbox Within An Item
|
||||
// -----------------------------------------
|
||||
// --------------------------------------------------------
|
||||
|
||||
:host(.in-item) {
|
||||
// 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);
|
||||
|
||||
display: block;
|
||||
|
||||
position: static;
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,18 @@
|
||||
:host {
|
||||
/**
|
||||
* @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 --border-color: Border color of the checkbox icon
|
||||
* @prop --border-radius: Border radius of the checkbox icon
|
||||
* @prop --border-width: Border width 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 --transition: Transition of the checkbox icon
|
||||
*
|
||||
* @prop --checkmark-color: Color of the checkbox checkmark when checked
|
||||
*/
|
||||
--background-checked: #{ion-color(primary, base)};
|
||||
@ -67,19 +71,23 @@ button {
|
||||
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);
|
||||
|
||||
background: var(--background-checked);
|
||||
}
|
||||
|
||||
:host(.checkbox-checked) .checkbox-icon path {
|
||||
:host(.checkbox-checked) .checkbox-icon path,
|
||||
:host(.checkbox-indeterminate) .checkbox-icon path {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
// Disabled Checkbox
|
||||
// ---------------------------------------------
|
||||
|
||||
|
@ -41,6 +41,11 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@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.
|
||||
*/
|
||||
@ -101,6 +106,7 @@ export class Checkbox implements ComponentInterface {
|
||||
onClick() {
|
||||
this.setFocus();
|
||||
this.checked = !this.checked;
|
||||
this.indeterminate = false;
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
@ -134,6 +140,7 @@ export class Checkbox implements ComponentInterface {
|
||||
'in-item': hostContext('ion-item', el),
|
||||
'checkbox-checked': checked,
|
||||
'checkbox-disabled': disabled,
|
||||
'checkbox-indeterminate': this.indeterminate,
|
||||
'interactive': true
|
||||
}
|
||||
};
|
||||
@ -142,12 +149,19 @@ export class Checkbox implements ComponentInterface {
|
||||
render() {
|
||||
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 [
|
||||
<svg class="checkbox-icon" viewBox="0 0 24 24">
|
||||
{ this.mode === 'md'
|
||||
? <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"/>
|
||||
}
|
||||
{path}
|
||||
</svg>,
|
||||
<button
|
||||
type="button"
|
||||
|
@ -188,14 +188,15 @@ export default CheckboxExample;
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------- |
|
||||
| `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` |
|
||||
| `disabled` | `disabled` | If `true`, the user cannot interact with the checkbox. | `boolean` | `false` |
|
||||
| `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` |
|
||||
| `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'` |
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| --------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------- |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `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'` |
|
||||
|
||||
|
||||
## Events
|
||||
|
10
core/src/components/checkbox/test/indeterminate/e2e.ts
Normal file
10
core/src/components/checkbox/test/indeterminate/e2e.ts
Normal 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();
|
||||
});
|
203
core/src/components/checkbox/test/indeterminate/index.html
Normal file
203
core/src/components/checkbox/test/indeterminate/index.html
Normal 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>
|
Reference in New Issue
Block a user