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
This commit is contained in:
Gavin 2023-05-05 21:40:49 +08:00 committed by GitHub
parent e0ae206c89
commit 3dabce8e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2993 additions and 28 deletions

View File

@ -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<PickerGroupProvide> = Symbol(name);
export const pickerGroupProps = extend(
{
tabs: makeArrayProp<string>(),
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',

View File

@ -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
<van-button type="primary" @click="setActiveTab">
toggle tab, current {{ activeTab }}
</van-button>
<van-picker-group
v-model:active-tab="activeTab"
title="Title"
:tabs="['Date', 'Time']"
@confirm="onConfirm"
@cancel="onCancel"
>
<van-date-picker
v-model="currentDate"
:min-date="minDate"
:max-date="maxDate"
/>
<van-time-picker v-model="currentTime" />
</van-picker-group>
```
```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_ | - |

View File

@ -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
<van-button type="primary" @click="setActiveTab">
点击切换 tab当前为 {{ activeTab }}
</van-button>
<van-picker-group
v-model:active-tab="activeTab"
title="预约日期"
:tabs="['选择日期', '选择时间']"
@confirm="onConfirm"
@cancel="onCancel"
>
<van-date-picker
v-model="currentDate"
:min-date="minDate"
:max-date="maxDate"
/>
<van-time-picker v-model="currentTime" />
</van-picker-group>
```
```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_ | - |

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useTranslate } from '../../../docs/site';
import VanPickerGroup from '..';
import VanButton from '../../button';
import VanTimePicker from '../../time-picker';
import VanDatePicker from '../../date-picker';
import { showToast } from '../../toast';
const t = useTranslate({
'zh-CN': {
date: '选择日期',
time: '选择时间',
title: '预约日期',
btnText: '点击切换 tab当前为 ',
},
'en-US': {
date: 'Date',
time: 'Time',
title: 'Title',
btnText: 'toggle tab, current ',
},
});
const activeTab = ref(0);
const currentTime = ref(['12', '00']);
const currentDate = ref(['2022', '06', '01']);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2025, 5, 1);
const onConfirm = () => {
showToast(`${currentDate.value.join('/')} ${currentTime.value.join(':')}`);
};
const onCancel = () => {
showToast('cancel');
};
const setActiveTab = () => {
activeTab.value = activeTab.value ? 0 : 1;
};
</script>
<template>
<van-button style="margin: 10px 0" type="primary" @click="setActiveTab">
{{ t('btnText') + activeTab }}
</van-button>
<van-picker-group
v-model:active-tab="activeTab"
:title="t('title')"
:tabs="[t('date'), t('time')]"
@confirm="onConfirm"
@cancel="onCancel"
>
<van-date-picker
v-model="currentDate"
:min-date="minDate"
:max-date="maxDate"
/>
<van-time-picker v-model="currentTime" />
</van-picker-group>
</template>

View File

@ -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',
},
});
</script>
@ -37,4 +40,8 @@ const t = useTranslate({
<demo-block card :title="t('selectTimeRange')">
<select-time-range />
</demo-block>
<demo-block card :title="t('controlled')">
<control-tab />
</demo-block>
</template>

View File

@ -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 (
<PickerGroup
activeTab={activeTab.value}
title="Title"
tabs={['Tab1', 'Tab2']}
>
<Picker
v-model={value1.value}
columns={[{ text: '1', value: '1' }]}
/>
<Picker
v-model={value2.value}
columns={[{ text: '2', value: '2' }]}
/>
</PickerGroup>
);
},
});
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');
});