feat(Popover): support uncontrolled mode (#11244)

This commit is contained in:
neverland 2022-11-12 13:14:58 +08:00 committed by GitHub
parent f071e48be2
commit 4e9c301012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 10 deletions

View File

@ -0,0 +1,22 @@
import { Ref, ref, watch } from 'vue';
export const useSyncPropRef = <T>(
getProp: () => T,
setProp: (value: T) => void
) => {
const propRef = ref<T>(getProp()) as Ref<T>;
watch(getProp, (value) => {
if (value !== propRef.value) {
propRef.value = value;
}
});
watch(propRef, (value) => {
if (value !== getProp()) {
setProp(value);
}
});
return propRef;
};

View File

@ -29,6 +29,7 @@ import {
// Composables // Composables
import { useClickAway } from '@vant/use'; import { useClickAway } from '@vant/use';
import { useSyncPropRef } from '../composables/use-sync-prop-ref';
// Components // Components
import { Icon } from '../icon'; import { Icon } from '../icon';
@ -45,7 +46,6 @@ import {
const [name, bem] = createNamespace('popover'); const [name, bem] = createNamespace('popover');
const popupProps = [ const popupProps = [
'show',
'overlay', 'overlay',
'duration', 'duration',
'teleport', 'teleport',
@ -95,6 +95,11 @@ export default defineComponent({
const wrapperRef = ref<HTMLElement>(); const wrapperRef = ref<HTMLElement>();
const popoverRef = ref<ComponentInstance>(); const popoverRef = ref<ComponentInstance>();
const show = useSyncPropRef(
() => props.show,
(value) => emit('update:show', value)
);
const getPopoverOptions = () => ({ const getPopoverOptions = () => ({
placement: props.placement, placement: props.placement,
modifiers: [ modifiers: [
@ -126,7 +131,7 @@ export default defineComponent({
const updateLocation = () => { const updateLocation = () => {
nextTick(() => { nextTick(() => {
if (!props.show) { if (!show.value) {
return; return;
} }
@ -138,11 +143,13 @@ export default defineComponent({
}); });
}; };
const updateShow = (value: boolean) => emit('update:show', value); const updateShow = (value: boolean) => {
show.value = value;
};
const onClickWrapper = () => { const onClickWrapper = () => {
if (props.trigger === 'click') { if (props.trigger === 'click') {
updateShow(!props.show); show.value = !show.value;
} }
}; };
@ -154,17 +161,17 @@ export default defineComponent({
emit('select', action, index); emit('select', action, index);
if (props.closeOnClickAction) { if (props.closeOnClickAction) {
updateShow(false); show.value = false;
} }
}; };
const onClickAway = () => { const onClickAway = () => {
if ( if (
props.show && show.value &&
props.closeOnClickOutside && props.closeOnClickOutside &&
(!props.overlay || props.closeOnClickOverlay) (!props.overlay || props.closeOnClickOverlay)
) { ) {
updateShow(false); show.value = false;
} }
}; };
@ -215,7 +222,7 @@ export default defineComponent({
} }
}); });
watch(() => [props.show, props.offset, props.placement], updateLocation); watch(() => [show.value, props.offset, props.placement], updateLocation);
useClickAway([wrapperRef, popupRef], onClickAway, { useClickAway([wrapperRef, popupRef], onClickAway, {
eventName: 'touchstart', eventName: 'touchstart',
@ -228,6 +235,7 @@ export default defineComponent({
</span> </span>
<Popup <Popup
ref={popoverRef} ref={popoverRef}
show={show.value}
class={bem([props.theme])} class={bem([props.theme])}
position={''} position={''}
transition="van-popover-zoom" transition="van-popover-zoom"

View File

@ -204,6 +204,41 @@ export default {
}; };
``` ```
### Uncontrolled
You can use Popover as a controlled or uncontrolled component:
- When binding `v-model:show`, Popover is a controlled component, and the display of the component is completely controlled by the value of `v-model:show`.
- When `v-model:show` is not used, Popover is an uncontrolled component. You can pass in a default value through the `show` prop, and the display is controlled by the component itself.
```html
<van-popover :actions="actions" position="top-start" @select="onSelect">
<template #reference>
<van-button type="primary">Uncontrolled</van-button>
</template>
</van-popover>
```
```js
import { ref } from 'vue';
import { showToast } from 'vant';
export default {
setup() {
const actions = [
{ text: '选项一' },
{ text: '选项二' },
{ text: '选项三' },
];
const onSelect = (action) => showToast(action.text);
return {
actions,
onSelect,
};
},
};
```
## API ## API
### Props ### Props

View File

@ -214,6 +214,41 @@ export default {
}; };
``` ```
### 非受控模式
你可以把 Popover 当做受控组件或非受控组件使用:
- 当绑定 `v-model:show`Popover 为受控组件,此时组件的显示完全由 `v-model:show` 的值决定。
- 当未绑定 `v-model:show`Popover 为非受控组件,此时你可以通过 `show` 属性传入一个默认值,组件值的显示由组件自身控制。
```html
<van-popover :actions="actions" position="top-start" @select="onSelect">
<template #reference>
<van-button type="primary">非受控模式</van-button>
</template>
</van-popover>
```
```js
import { ref } from 'vue';
import { showToast } from 'vant';
export default {
setup() {
const actions = [
{ text: '选项一' },
{ text: '选项二' },
{ text: '选项三' },
];
const onSelect = (action) => showToast(action.text);
return {
actions,
onSelect,
};
},
};
```
## API ## API
### Props ### Props

View File

@ -29,6 +29,7 @@ const t = useTranslate({
darkTheme: '深色风格', darkTheme: '深色风格',
lightTheme: '浅色风格', lightTheme: '浅色风格',
showPopover: '点击弹出气泡', showPopover: '点击弹出气泡',
uncontrolled: '非受控模式',
actionOptions: '选项配置', actionOptions: '选项配置',
customContent: '自定义内容', customContent: '自定义内容',
disableAction: '禁用选项', disableAction: '禁用选项',
@ -52,6 +53,7 @@ const t = useTranslate({
darkTheme: 'Dark Theme', darkTheme: 'Dark Theme',
lightTheme: 'Light Theme', lightTheme: 'Light Theme',
showPopover: 'Show Popover', showPopover: 'Show Popover',
uncontrolled: 'Uncontrolled',
actionOptions: 'Action Options', actionOptions: 'Action Options',
customContent: 'Custom Content', customContent: 'Custom Content',
disableAction: 'Disable Action', disableAction: 'Disable Action',
@ -120,6 +122,7 @@ const onSelect = (action: { text: string }) => showToast(action.text);
</van-button> </van-button>
</template> </template>
</van-popover> </van-popover>
<van-popover <van-popover
v-model:show="show.darkTheme" v-model:show="show.darkTheme"
theme="dark" theme="dark"
@ -225,6 +228,20 @@ const onSelect = (action: { text: string }) => showToast(action.text);
</template> </template>
</van-popover> </van-popover>
</demo-block> </demo-block>
<demo-block :title="t('uncontrolled')">
<van-popover
:actions="t('actions')"
placement="top-start"
@select="onSelect"
>
<template #reference>
<van-button type="primary">
{{ t('uncontrolled') }}
</van-button>
</template>
</van-popover>
</demo-block>
</template> </template>
<style lang="less"> <style lang="less">

View File

@ -89,4 +89,17 @@ exports[`should render demo and match snapshot 1`] = `
</button> </button>
</span> </span>
</div> </div>
<div>
<span class="van-popover__wrapper">
<button type="button"
class="van-button van-button--primary van-button--normal"
>
<div class="van-button__content">
<span class="van-button__text">
Uncontrolled
</span>
</div>
</button>
</span>
</div>
`; `;

View File

@ -139,11 +139,11 @@ test('should close popover when touch outside content', async () => {
}); });
const popover = root.querySelector('.van-popover'); const popover = root.querySelector('.van-popover');
trigger(popover!, 'touchstart'); await trigger(popover!, 'touchstart');
expect(wrapper.emitted('update:show')).toBeFalsy(); expect(wrapper.emitted('update:show')).toBeFalsy();
document.body.appendChild(root); document.body.appendChild(root);
trigger(document.body, 'touchstart'); await trigger(document.body, 'touchstart');
expect(wrapper.emitted('update:show')![0]).toEqual([false]); expect(wrapper.emitted('update:show')![0]).toEqual([false]);
}); });