From 3dabce8e983db5d4f0d2aa41970405a81f866cc7 Mon Sep 17 00:00:00 2001 From: Gavin Date: Fri, 5 May 2023 21:40:49 +0800 Subject: [PATCH] feat(PickerGroup): the tab of PickerGroup supports controlled mode (#11771) * feat(PickerGroup): add method to set the active of tab * chore: update * chore: format code * chore: update * chore: update * docs: update docs --- .../vant/src/picker-group/PickerGroup.tsx | 28 +- packages/vant/src/picker-group/README.md | 105 +- .../vant/src/picker-group/README.zh-CN.md | 106 +- .../vant/src/picker-group/demo/ControlTab.vue | 60 + packages/vant/src/picker-group/demo/index.vue | 7 + .../test/__snapshots__/demo-ssr.spec.ts.snap | 1338 ++++++++++++++++ .../test/__snapshots__/demo.spec.ts.snap | 1340 +++++++++++++++++ .../vant/src/picker-group/test/index.spec.tsx | 37 +- 8 files changed, 2993 insertions(+), 28 deletions(-) create mode 100644 packages/vant/src/picker-group/demo/ControlTab.vue diff --git a/packages/vant/src/picker-group/PickerGroup.tsx b/packages/vant/src/picker-group/PickerGroup.tsx index 2d3d19bd9..9a1526c29 100644 --- a/packages/vant/src/picker-group/PickerGroup.tsx +++ b/packages/vant/src/picker-group/PickerGroup.tsx @@ -1,15 +1,17 @@ -import { - ref, - defineComponent, - type InjectionKey, - type ExtractPropTypes, -} from 'vue'; +import { defineComponent, type InjectionKey, type ExtractPropTypes } from 'vue'; // Utils -import { extend, pick, makeArrayProp, createNamespace } from '../utils'; +import { + pick, + extend, + makeArrayProp, + makeNumericProp, + createNamespace, +} from '../utils'; // Composables import { useChildren } from '@vant/use'; +import { useSyncPropRef } from '../composables/use-sync-prop-ref'; // Components import { Tab } from '../tab'; @@ -28,6 +30,7 @@ export const PICKER_GROUP_KEY: InjectionKey = Symbol(name); export const pickerGroupProps = extend( { tabs: makeArrayProp(), + activeTab: makeNumericProp(0), nextStepText: String, }, pickerToolbarProps @@ -40,20 +43,23 @@ export default defineComponent({ props: pickerGroupProps, - emits: ['confirm', 'cancel'], + emits: ['confirm', 'cancel', 'update:activeTab'], setup(props, { emit, slots }) { - const activeTab = ref(0); + const activeTab = useSyncPropRef( + () => props.activeTab, + (value) => emit('update:activeTab', value) + ); const { children, linkChildren } = useChildren(PICKER_GROUP_KEY); linkChildren(); const showNextButton = () => - activeTab.value < props.tabs.length - 1 && props.nextStepText; + +activeTab.value < props.tabs.length - 1 && props.nextStepText; const onConfirm = () => { if (showNextButton()) { - activeTab.value++; + activeTab.value = +activeTab.value + 1; } else { emit( 'confirm', diff --git a/packages/vant/src/picker-group/README.md b/packages/vant/src/picker-group/README.md index 3a044b4ae..4d6186287 100644 --- a/packages/vant/src/picker-group/README.md +++ b/packages/vant/src/picker-group/README.md @@ -56,11 +56,13 @@ export default { setup() { const currentDate = ref(['2022', '06', '01']); const currentTime = ref(['12', '00']); + const onConfirm = () => { showToast( `${currentDate.value.join('/')} ${currentTime.value.join(':')}` ); }; + const onCancel = () => { showToast('cancel'); }; @@ -70,6 +72,8 @@ export default { maxDate: new Date(2025, 5, 1), currentDate, currentTime, + onConfirm, + onCancel, }; }, }; @@ -104,11 +108,13 @@ export default { setup() { const currentDate = ref(['2022', '06', '01']); const currentTime = ref(['12', '00']); + const onConfirm = () => { showToast( `${currentDate.value.join('/')} ${currentTime.value.join(':')}` ); }; + const onCancel = () => { showToast('cancel'); }; @@ -118,6 +124,8 @@ export default { maxDate: new Date(2025, 5, 1), currentDate, currentTime, + onConfirm, + onCancel, }; }, }; @@ -155,6 +163,7 @@ export default { const onConfirm = () => { showToast(`${startDate.value.join('/')} ${endDate.value.join('/')}`); }; + const onCancel = () => { showToast('cancel'); }; @@ -164,6 +173,8 @@ export default { maxDate: new Date(2025, 5, 1), endDate, startDate, + onConfirm, + onCancel, }; }, }; @@ -197,6 +208,7 @@ export default { const onConfirm = () => { showToast(`${startTime.value.join(':')} ${endTime.value.join(':')}`); }; + const onCancel = () => { showToast('cancel'); }; @@ -204,6 +216,72 @@ export default { return { endTime, startTime, + onConfirm, + onCancel, + }; + }, +}; +``` + +### Controlled Mode + +Supports both uncontrolled and controlled modes: + +- When `v-model:active-tab` is not bound, the PickerGroup component completely controls the `tab` switching. +- When `v-model:active-tab` is bound, PickerGroup supports controlled mode, and the `tab` switching is controlled by both the `v-model:active-tab` value and the component itself. + +```html + + toggle tab, current {{ activeTab }} + + + + + +``` + +```js +import { ref } from 'vue'; +import { showToast } from 'vant'; + +export default { + setup() { + const activeTab = ref(0); + const currentDate = ref(['2022', '06', '01']); + const currentTime = ref(['12', '00']); + + const setActiveTab = () => { + activeTab.value = activeTab.value ? 0 : 1; + }; + + const onConfirm = () => { + showToast( + `${currentDate.value.join('/')} ${currentTime.value.join(':')}` + ); + }; + const onCancel = () => { + showToast('cancel'); + }; + + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 5, 1), + activeTab, + currentDate, + currentTime, + setActiveTab, + onConfirm, + onCancel, }; }, }; @@ -213,13 +291,14 @@ export default { ### Props -| Attribute | Description | Type | Default | -| ----------------------- | ------------------------ | ---------- | --------- | -| tabs | Titles of tabs | _string[]_ | `[]` | -| title | Toolbar title | _string_ | `''` | -| next-step-text `v4.0.8` | Text of next step button | _string_ | `''` | -| confirm-button-text | Text of confirm button | _string_ | `Confirm` | -| cancel-button-text | Text of cancel button | _string_ | `Cancel` | +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| v-model:active-tab | Set index of active tab | _number \| string_ | `0` | +| tabs | Titles of tabs | _string[]_ | `[]` | +| title | Toolbar title | _string_ | `''` | +| next-step-text `v4.0.8` | Text of next step button | _string_ | `''` | +| confirm-button-text | Text of confirm button | _string_ | `Confirm` | +| cancel-button-text | Text of cancel button | _string_ | `Cancel` | ### Slots @@ -235,5 +314,15 @@ export default { The component exports the following type definitions: ```ts -import type { DatePickerProps, DatePickerColumnType } from 'vant'; +import type { PickerGroupProps, PickerGroupThemeVars } from 'vant'; ``` + +## Theming + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/config-provider). + +| Name | Default Value | Description | +| ----------------------------- | -------------------- | ----------- | +| --van-picker-group-background | _--van-background-2_ | - | diff --git a/packages/vant/src/picker-group/README.zh-CN.md b/packages/vant/src/picker-group/README.zh-CN.md index b798820ae..78a6c4cb7 100644 --- a/packages/vant/src/picker-group/README.zh-CN.md +++ b/packages/vant/src/picker-group/README.zh-CN.md @@ -56,11 +56,13 @@ export default { setup() { const currentDate = ref(['2022', '06', '01']); const currentTime = ref(['12', '00']); + const onConfirm = () => { showToast( `${currentDate.value.join('/')} ${currentTime.value.join(':')}` ); }; + const onCancel = () => { showToast('cancel'); }; @@ -70,6 +72,8 @@ export default { maxDate: new Date(2025, 5, 1), currentDate, currentTime, + onConfirm, + onCancel, }; }, }; @@ -104,11 +108,13 @@ export default { setup() { const currentDate = ref(['2022', '06', '01']); const currentTime = ref(['12', '00']); + const onConfirm = () => { showToast( `${currentDate.value.join('/')} ${currentTime.value.join(':')}` ); }; + const onCancel = () => { showToast('cancel'); }; @@ -118,6 +124,8 @@ export default { maxDate: new Date(2025, 5, 1), currentDate, currentTime, + onConfirm, + onCancel, }; }, }; @@ -155,6 +163,7 @@ export default { const onConfirm = () => { showToast(`${startDate.value.join('/')} ${endDate.value.join('/')}`); }; + const onCancel = () => { showToast('cancel'); }; @@ -164,6 +173,8 @@ export default { maxDate: new Date(2025, 5, 1), endDate, startDate, + onConfirm, + onCancel, }; }, }; @@ -197,6 +208,7 @@ export default { const onConfirm = () => { showToast(`${startTime.value.join(':')} ${endTime.value.join(':')}`); }; + const onCancel = () => { showToast('cancel'); }; @@ -204,6 +216,73 @@ export default { return { endTime, startTime, + onConfirm, + onCancel, + }; + }, +}; +``` + +### 受控模式 + +`PickerGroup` 中 `tab` 的切换支持非受控模式和受控模式: + +- 当未绑定 `v-model:active-tab` 时,PickerGroup 组件 `tab` 的切换完全由组件自身控制。 +- 当绑定 `v-model:active-tab` 时,PickerGroup 支持受控模式,此时组件 `tab` 的切换同时支持 `v-model:active-tab` 的值和组件本身控制。 + +```html + + 点击切换 tab,当前为 {{ activeTab }} + + + + + +``` + +```js +import { ref } from 'vue'; +import { showToast } from 'vant'; + +export default { + setup() { + const activeTab = ref(0); + const currentDate = ref(['2022', '06', '01']); + const currentTime = ref(['12', '00']); + + const setActiveTab = () => { + activeTab.value = activeTab.value ? 0 : 1; + }; + + const onConfirm = () => { + showToast( + `${currentDate.value.join('/')} ${currentTime.value.join(':')}` + ); + }; + + const onCancel = () => { + showToast('cancel'); + }; + + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 5, 1), + activeTab, + currentDate, + currentTime, + setActiveTab, + onConfirm, + onCancel, }; }, }; @@ -213,13 +292,14 @@ export default { ### Props -| 参数 | 说明 | 类型 | 默认值 | -| ----------------------- | ---------------- | ---------- | ------ | -| tabs | 设置标签页的标题 | _string[]_ | `[]` | -| title | 顶部栏标题 | _string_ | `''` | -| next-step-text `v4.0.8` | 下一步按钮的文字 | _string_ | `''` | -| confirm-button-text | 确认按钮的文字 | _string_ | `确认` | -| cancel-button-text | 取消按钮的文字 | _string_ | `取消` | +| 参数 | 说明 | 类型 | 默认值 | +| ----------------------- | ------------------ | ------------------ | ------ | +| v-model:active-tab | 设置当前选中的标签 | _number \| string_ | `0` | +| tabs | 设置标签页的标题 | _string[]_ | `[]` | +| title | 顶部栏标题 | _string_ | `''` | +| next-step-text `v4.0.8` | 下一步按钮的文字 | _string_ | `''` | +| confirm-button-text | 确认按钮的文字 | _string_ | `确认` | +| cancel-button-text | 取消按钮的文字 | _string_ | `取消` | ### Slots @@ -235,5 +315,15 @@ export default { 组件导出以下类型定义: ```ts -import type { PickerGroupProps } from 'vant'; +import type { PickerGroupProps, PickerGroupThemeVars } from 'vant'; ``` + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。 + +| 名称 | 默认值 | 描述 | +| ----------------------------- | -------------------- | ---- | +| --van-picker-group-background | _--van-background-2_ | - | diff --git a/packages/vant/src/picker-group/demo/ControlTab.vue b/packages/vant/src/picker-group/demo/ControlTab.vue new file mode 100644 index 000000000..969ec3d87 --- /dev/null +++ b/packages/vant/src/picker-group/demo/ControlTab.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/vant/src/picker-group/demo/index.vue b/packages/vant/src/picker-group/demo/index.vue index 8bde31f55..a07d70171 100644 --- a/packages/vant/src/picker-group/demo/index.vue +++ b/packages/vant/src/picker-group/demo/index.vue @@ -3,6 +3,7 @@ import SelectDateTime from './SelectDateTime.vue'; import SelectTimeRange from './SelectTimeRange.vue'; import SelectDateRange from './SelectDateRange.vue'; import NextStepButton from './NextStepButton.vue'; +import ControlTab from './ControlTab.vue'; import { useTranslate } from '../../../docs/site'; const t = useTranslate({ @@ -11,12 +12,14 @@ const t = useTranslate({ selectDateRange: '选择日期范围', selectTimeRange: '选择时间范围', nextStepButton: '下一步按钮', + controlled: '受控模式', }, 'en-US': { selectDateTime: 'Select Date Time', selectDateRange: 'Select Date Range', selectTimeRange: 'Select Time Range', nextStepButton: 'Next Step Button', + controlled: 'Controlled Mode', }, }); @@ -37,4 +40,8 @@ const t = useTranslate({ + + + + diff --git a/packages/vant/src/picker-group/test/__snapshots__/demo-ssr.spec.ts.snap b/packages/vant/src/picker-group/test/__snapshots__/demo-ssr.spec.ts.snap index 5477e1318..71fe2f176 100644 --- a/packages/vant/src/picker-group/test/__snapshots__/demo-ssr.spec.ts.snap +++ b/packages/vant/src/picker-group/test/__snapshots__/demo-ssr.spec.ts.snap @@ -5288,4 +5288,1342 @@ exports[`should render demo and match snapshot 1`] = ` +
+ + + +
+
+ + +
+ Title +
+ +
+
+ +
+
+ +
+
+
+
+
+ + + + + + +
+
+
+
+
+
`; diff --git a/packages/vant/src/picker-group/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/picker-group/test/__snapshots__/demo.spec.ts.snap index f8132f1f1..ee71e13b8 100644 --- a/packages/vant/src/picker-group/test/__snapshots__/demo.spec.ts.snap +++ b/packages/vant/src/picker-group/test/__snapshots__/demo.spec.ts.snap @@ -5303,4 +5303,1344 @@ exports[`should render demo and match snapshot 1`] = ` +
+ +
+
+ +
+ Title +
+ +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
    +
  • +
    + 2020 +
    +
  • +
  • +
    + 2021 +
    +
  • +
  • +
    + 2022 +
    +
  • +
  • +
    + 2023 +
    +
  • +
  • +
    + 2024 +
    +
  • +
  • +
    + 2025 +
    +
  • +
+
+
+
    +
  • +
    + 01 +
    +
  • +
  • +
    + 02 +
    +
  • +
  • +
    + 03 +
    +
  • +
  • +
    + 04 +
    +
  • +
  • +
    + 05 +
    +
  • +
  • +
    + 06 +
    +
  • +
  • +
    + 07 +
    +
  • +
  • +
    + 08 +
    +
  • +
  • +
    + 09 +
    +
  • +
  • +
    + 10 +
    +
  • +
  • +
    + 11 +
    +
  • +
  • +
    + 12 +
    +
  • +
+
+
+
    +
  • +
    + 01 +
    +
  • +
  • +
    + 02 +
    +
  • +
  • +
    + 03 +
    +
  • +
  • +
    + 04 +
    +
  • +
  • +
    + 05 +
    +
  • +
  • +
    + 06 +
    +
  • +
  • +
    + 07 +
    +
  • +
  • +
    + 08 +
    +
  • +
  • +
    + 09 +
    +
  • +
  • +
    + 10 +
    +
  • +
  • +
    + 11 +
    +
  • +
  • +
    + 12 +
    +
  • +
  • +
    + 13 +
    +
  • +
  • +
    + 14 +
    +
  • +
  • +
    + 15 +
    +
  • +
  • +
    + 16 +
    +
  • +
  • +
    + 17 +
    +
  • +
  • +
    + 18 +
    +
  • +
  • +
    + 19 +
    +
  • +
  • +
    + 20 +
    +
  • +
  • +
    + 21 +
    +
  • +
  • +
    + 22 +
    +
  • +
  • +
    + 23 +
    +
  • +
  • +
    + 24 +
    +
  • +
  • +
    + 25 +
    +
  • +
  • +
    + 26 +
    +
  • +
  • +
    + 27 +
    +
  • +
  • +
    + 28 +
    +
  • +
  • +
    + 29 +
    +
  • +
  • +
    + 30 +
    +
  • +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
`; diff --git a/packages/vant/src/picker-group/test/index.spec.tsx b/packages/vant/src/picker-group/test/index.spec.tsx index cb0033971..4922573c5 100644 --- a/packages/vant/src/picker-group/test/index.spec.tsx +++ b/packages/vant/src/picker-group/test/index.spec.tsx @@ -1,5 +1,5 @@ import { ref } from 'vue'; -import { mount } from '../../../test'; +import { later, mount } from '../../../test'; import { Picker, PickerConfirmEventParams } from '../../picker'; import { PickerGroup } from '..'; @@ -108,3 +108,38 @@ test('should switch to next step when click confirm button', async () => { ], ]); }); + +test('support controlled mode to set active-tab', async () => { + const value1 = ref(['1']); + const value2 = ref(['2']); + const activeTab = ref(0); + + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + + await later(); + const tabs = wrapper.findAll('.van-tab'); + expect(tabs[0]?.classes()).toContain('van-tab--active'); + + activeTab.value = 1; + await later(); + expect(tabs[1]?.classes()).toContain('van-tab--active'); +});