From 37f4500e3c47832a83e3f2f3916218cf15129311 Mon Sep 17 00:00:00 2001
From: Gavin <gavinwjw@163.com>
Date: Sun, 27 Aug 2023 15:21:44 +0800
Subject: [PATCH] feat(Checkbox): add indeterminate status (#12216)

* feat(Checkbox): add indeterminate status

* chore: update

* chore: update test

* chore: update

* chore: update

* chore: update
---
 packages/vant/src/checkbox/Checkbox.tsx       |  10 +-
 packages/vant/src/checkbox/Checker.tsx        |  13 +-
 packages/vant/src/checkbox/README.md          |  53 ++++++++
 packages/vant/src/checkbox/README.zh-CN.md    |  55 ++++++++
 packages/vant/src/checkbox/demo/index.vue     |  40 ++++++
 packages/vant/src/checkbox/index.less         |  11 ++
 .../test/__snapshots__/demo-ssr.spec.ts.snap  | 124 ++++++++++++++++++
 .../test/__snapshots__/demo.spec.ts.snap      |  76 +++++++++++
 8 files changed, 378 insertions(+), 4 deletions(-)

diff --git a/packages/vant/src/checkbox/Checkbox.tsx b/packages/vant/src/checkbox/Checkbox.tsx
index c6f672e34..d5da36af6 100644
--- a/packages/vant/src/checkbox/Checkbox.tsx
+++ b/packages/vant/src/checkbox/Checkbox.tsx
@@ -25,6 +25,10 @@ const [name, bem] = createNamespace('checkbox');
 export const checkboxProps = extend({}, checkerProps, {
   shape: String as PropType<CheckerShape>,
   bindGroup: truthProp,
+  indeterminate: {
+    type: Boolean as PropType<boolean | null>,
+    default: null,
+  },
 });
 
 export type CheckboxProps = ExtractPropTypes<typeof checkboxProps>;
@@ -80,11 +84,15 @@ export default defineComponent({
       } else {
         emit('update:modelValue', newValue);
       }
+
+      if (props.indeterminate !== null) emit('change', newValue);
     };
 
     watch(
       () => props.modelValue,
-      (value) => emit('change', value),
+      (value) => {
+        if (props.indeterminate === null) emit('change', value);
+      },
     );
 
     useExpose<CheckboxExpose>({ toggle, props, checked });
diff --git a/packages/vant/src/checkbox/Checker.tsx b/packages/vant/src/checkbox/Checker.tsx
index 17bede19d..6c0e6d69f 100644
--- a/packages/vant/src/checkbox/Checker.tsx
+++ b/packages/vant/src/checkbox/Checker.tsx
@@ -45,6 +45,10 @@ export default defineComponent({
     parent: Object as PropType<CheckerParent | null>,
     checked: Boolean,
     bindGroup: truthProp,
+    indeterminate: {
+      type: Boolean as PropType<boolean | null>,
+      default: null,
+    },
   }),
 
   emits: ['click', 'toggle'],
@@ -106,7 +110,7 @@ export default defineComponent({
     };
 
     const renderIcon = () => {
-      const { bem, checked } = props;
+      const { bem, checked, indeterminate } = props;
       const iconSize = props.iconSize || getParentProp('iconSize');
 
       return (
@@ -114,7 +118,7 @@ export default defineComponent({
           ref={iconRef}
           class={bem('icon', [
             shape.value,
-            { disabled: disabled.value, checked },
+            { disabled: disabled.value, checked, indeterminate },
           ])}
           style={
             shape.value !== 'dot'
@@ -129,7 +133,10 @@ export default defineComponent({
           {slots.icon ? (
             slots.icon({ checked, disabled: disabled.value })
           ) : shape.value !== 'dot' ? (
-            <Icon name="success" style={iconStyle.value} />
+            <Icon
+              name={indeterminate ? 'minus' : 'success'}
+              style={iconStyle.value}
+            />
           ) : (
             <div
               class={bem('icon--dot__icon')}
diff --git a/packages/vant/src/checkbox/README.md b/packages/vant/src/checkbox/README.md
index bafda4935..5ba1e0bd4 100644
--- a/packages/vant/src/checkbox/README.md
+++ b/packages/vant/src/checkbox/README.md
@@ -265,6 +265,58 @@ export default {
 };
 ```
 
+### indeterminate
+
+```html
+<van-checkbox
+  v-model="isCheckAll"
+  :indeterminate="isIndeterminate"
+  @change="checkAllChange"
+>
+  Check All
+</van-checkbox>
+
+<van-checkbox-group v-model="checkedResult" @change="checkedResultChange">
+  <van-checkbox v-for="item in list" :key="item" :name="item">
+    Checkbox {{ item }}
+  </van-checkbox>
+</van-checkbox-group>
+```
+
+```js
+import { ref } from 'vue';
+
+export default {
+  setup() {
+    const list = ['a', 'b', 'c', 'd']
+
+    const isCheckAll = ref(false);
+    const checkedResult = ref(['a', 'b', 'd']);
+    const isIndeterminate = ref(true);
+
+    const checkAllChange = (val: boolean) => {
+      checkedResult.value = val ? list : []
+      isIndeterminate.value = false
+    }
+
+    const checkedResultChange = (value: string[]) => {
+      const checkedCount = value.length
+      isCheckAll.value = checkedCount === list.length
+      isIndeterminate.value = checkedCount > 0 && checkedCount < list.length
+    }
+
+    return {
+      list,
+      isCheckAll,
+      checkedResult,
+      checkAllChange,
+      isIndeterminate,
+      checkedResultChange
+    };
+  },
+};
+```
+
 ## API
 
 ### Checkbox Props
@@ -280,6 +332,7 @@ export default {
 | icon-size | Icon size | _number \| string_ | `20px` |
 | checked-color | Checked color | _string_ | `#1989fa` |
 | bind-group | Whether to bind with CheckboxGroup | _boolean_ | `true` |
+| indeterminate | Whether indeterminate status | _boolean_ | `false` |
 
 ### CheckboxGroup Props
 
diff --git a/packages/vant/src/checkbox/README.zh-CN.md b/packages/vant/src/checkbox/README.zh-CN.md
index f33b53c5d..947f03385 100644
--- a/packages/vant/src/checkbox/README.zh-CN.md
+++ b/packages/vant/src/checkbox/README.zh-CN.md
@@ -282,6 +282,60 @@ export default {
 };
 ```
 
+### 不确定状态
+
+通过 `indeterminate` 设置复选框是否为不确定状态。
+
+```html
+<van-checkbox
+  v-model="isCheckAll"
+  :indeterminate="isIndeterminate"
+  @change="checkAllChange"
+>
+  全选
+</van-checkbox>
+
+<van-checkbox-group v-model="checkedResult" @change="checkedResultChange">
+  <van-checkbox v-for="item in list" :key="item" :name="item">
+    复选框 {{ item }}
+  </van-checkbox>
+</van-checkbox-group>
+```
+
+```js
+import { ref } from 'vue';
+
+export default {
+  setup() {
+    const list = ['a', 'b', 'c', 'd']
+
+    const isCheckAll = ref(false);
+    const checkedResult = ref(['a', 'b', 'd']);
+    const isIndeterminate = ref(true);
+
+    const checkAllChange = (val: boolean) => {
+      checkedResult.value = val ? list : []
+      isIndeterminate.value = false
+    }
+
+    const checkedResultChange = (value: string[]) => {
+      const checkedCount = value.length
+      isCheckAll.value = checkedCount === list.length
+      isIndeterminate.value = checkedCount > 0 && checkedCount < list.length
+    }
+
+    return {
+      list,
+      isCheckAll,
+      checkedResult,
+      checkAllChange,
+      isIndeterminate,
+      checkedResultChange
+    };
+  },
+};
+```
+
 ## API
 
 ### Checkbox Props
@@ -297,6 +351,7 @@ export default {
 | icon-size | 图标大小,默认单位为 `px` | _number \| string_ | `20px` |
 | checked-color | 选中状态颜色 | _string_ | `#1989fa` |
 | bind-group | 是否与复选框组绑定 | _boolean_ | `true` |
+| indeterminate | 是否为不确定状态 | _boolean_ | `false` |
 
 ### CheckboxGroup Props
 
diff --git a/packages/vant/src/checkbox/demo/index.vue b/packages/vant/src/checkbox/demo/index.vue
index d6a08f537..745f57d5e 100644
--- a/packages/vant/src/checkbox/demo/index.vue
+++ b/packages/vant/src/checkbox/demo/index.vue
@@ -26,6 +26,7 @@ const t = useTranslate({
     inverse: '反选',
     horizontal: '水平排列',
     disableLabel: '禁用文本点击',
+    indeterminate: '不确定状态',
   },
   'en-US': {
     checkbox: 'Checkbox',
@@ -42,6 +43,7 @@ const t = useTranslate({
     inverse: 'Inverse',
     horizontal: 'Horizontal',
     disableLabel: 'Disable label click',
+    indeterminate: 'indeterminate',
   },
 });
 
@@ -49,6 +51,8 @@ const state = reactive({
   checkbox1: true,
   checkbox2: true,
   checkbox3: true,
+  isCheckAll: false,
+  isIndeterminate: true,
   checkboxLabel: true,
   checkboxIcon: true,
   leftLabel: false,
@@ -57,10 +61,13 @@ const state = reactive({
   checkboxShape: ['a', 'b'],
   result2: [],
   result3: [],
+  result4: ['a', 'b', 'd'],
   checkAllResult: [],
   horizontalResult: [],
 });
 
+const list = ['a', 'b', 'c', 'd'];
+
 const activeIcon = cdnURL('user-active.png');
 const inactiveIcon = cdnURL('user-inactive.png');
 
@@ -78,6 +85,17 @@ const checkAll = () => {
 const toggleAll = () => {
   group.value?.toggleAll();
 };
+
+const checkAllChange = (val: boolean) => {
+  state.result4 = val ? list : [];
+  state.isIndeterminate = false;
+};
+
+const checkedResultChange = (value: string[]) => {
+  const checkedCount = value.length;
+  state.isCheckAll = checkedCount === list.length;
+  state.isIndeterminate = checkedCount > 0 && checkedCount < list.length;
+};
 </script>
 
 <template>
@@ -190,6 +208,22 @@ const toggleAll = () => {
       </van-cell-group>
     </van-checkbox-group>
   </demo-block>
+
+  <demo-block :title="t('indeterminate')">
+    <van-checkbox
+      v-model="state.isCheckAll"
+      :indeterminate="state.isIndeterminate"
+      @change="checkAllChange"
+    >
+      {{ t('checkAll') }}
+    </van-checkbox>
+    <div class="divider" />
+    <van-checkbox-group v-model="state.result4" @change="checkedResultChange">
+      <van-checkbox v-for="item in list" :key="item" :name="item">
+        {{ t('checkbox') }} {{ item }}
+      </van-checkbox>
+    </van-checkbox-group>
+  </demo-block>
 </template>
 
 <style lang="less">
@@ -220,4 +254,10 @@ const toggleAll = () => {
     margin-top: -8px;
   }
 }
+
+.divider {
+  margin: 20px;
+  height: 1px;
+  background: #ccc;
+}
 </style>
diff --git a/packages/vant/src/checkbox/index.less b/packages/vant/src/checkbox/index.less
index f64a42e98..9ef71cd7d 100644
--- a/packages/vant/src/checkbox/index.less
+++ b/packages/vant/src/checkbox/index.less
@@ -56,6 +56,17 @@
       }
     }
 
+    &--indeterminate {
+      .van-icon {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: var(--van-white);
+        border-color: var(--van-checkbox-checked-icon-color);
+        background-color: var(--van-checkbox-checked-icon-color);
+      }
+    }
+
     &--checked {
       .van-icon {
         color: var(--van-white);
diff --git a/packages/vant/src/checkbox/test/__snapshots__/demo-ssr.spec.ts.snap b/packages/vant/src/checkbox/test/__snapshots__/demo-ssr.spec.ts.snap
index fc07e09d8..dfe53d717 100644
--- a/packages/vant/src/checkbox/test/__snapshots__/demo-ssr.spec.ts.snap
+++ b/packages/vant/src/checkbox/test/__snapshots__/demo-ssr.spec.ts.snap
@@ -611,4 +611,128 @@ exports[`should render demo and match snapshot 1`] = `
     </div>
   </div>
 </div>
+<div>
+  <!--[-->
+  <div
+    role="checkbox"
+    class="van-checkbox"
+    tabindex="0"
+    aria-checked="false"
+  >
+    <!--[-->
+    <div
+      class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--indeterminate"
+      style
+    >
+      <i
+        class="van-badge__wrapper van-icon van-icon-minus"
+        style
+      >
+        <!--[-->
+      </i>
+    </div>
+    <span class="van-checkbox__label">
+      <!--[-->
+      Check All
+    </span>
+  </div>
+  <div class="divider">
+  </div>
+  <div class="van-checkbox-group">
+    <!--[-->
+    <!--[-->
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <!--[-->
+      <div
+        class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked"
+        style
+      >
+        <i
+          class="van-badge__wrapper van-icon van-icon-success"
+          style
+        >
+          <!--[-->
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        <!--[-->
+        Checkbox a
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <!--[-->
+      <div
+        class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked"
+        style
+      >
+        <i
+          class="van-badge__wrapper van-icon van-icon-success"
+          style
+        >
+          <!--[-->
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        <!--[-->
+        Checkbox b
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="false"
+    >
+      <!--[-->
+      <div
+        class="van-checkbox__icon van-checkbox__icon--round"
+        style
+      >
+        <i
+          class="van-badge__wrapper van-icon van-icon-success"
+          style
+        >
+          <!--[-->
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        <!--[-->
+        Checkbox c
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <!--[-->
+      <div
+        class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked"
+        style
+      >
+        <i
+          class="van-badge__wrapper van-icon van-icon-success"
+          style
+        >
+          <!--[-->
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        <!--[-->
+        Checkbox d
+      </span>
+    </div>
+  </div>
+</div>
 `;
diff --git a/packages/vant/src/checkbox/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/checkbox/test/__snapshots__/demo.spec.ts.snap
index c82b16ea5..2a202ba6a 100644
--- a/packages/vant/src/checkbox/test/__snapshots__/demo.spec.ts.snap
+++ b/packages/vant/src/checkbox/test/__snapshots__/demo.spec.ts.snap
@@ -390,4 +390,80 @@ exports[`should render demo and match snapshot 1`] = `
     </div>
   </div>
 </div>
+<div>
+  <div
+    role="checkbox"
+    class="van-checkbox"
+    tabindex="0"
+    aria-checked="false"
+  >
+    <div class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--indeterminate">
+      <i class="van-badge__wrapper van-icon van-icon-minus">
+      </i>
+    </div>
+    <span class="van-checkbox__label">
+      Check All
+    </span>
+  </div>
+  <div class="divider">
+  </div>
+  <div class="van-checkbox-group">
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <div class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked">
+        <i class="van-badge__wrapper van-icon van-icon-success">
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        Checkbox a
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <div class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked">
+        <i class="van-badge__wrapper van-icon van-icon-success">
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        Checkbox b
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="false"
+    >
+      <div class="van-checkbox__icon van-checkbox__icon--round">
+        <i class="van-badge__wrapper van-icon van-icon-success">
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        Checkbox c
+      </span>
+    </div>
+    <div
+      role="checkbox"
+      class="van-checkbox"
+      tabindex="0"
+      aria-checked="true"
+    >
+      <div class="van-checkbox__icon van-checkbox__icon--round van-checkbox__icon--checked">
+        <i class="van-badge__wrapper van-icon van-icon-success">
+        </i>
+      </div>
+      <span class="van-checkbox__label">
+        Checkbox d
+      </span>
+    </div>
+  </div>
+</div>
 `;