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
import { useClickAway } from '@vant/use';
import { useSyncPropRef } from '../composables/use-sync-prop-ref';
// Components
import { Icon } from '../icon';
@ -45,7 +46,6 @@ import {
const [name, bem] = createNamespace('popover');
const popupProps = [
'show',
'overlay',
'duration',
'teleport',
@ -95,6 +95,11 @@ export default defineComponent({
const wrapperRef = ref<HTMLElement>();
const popoverRef = ref<ComponentInstance>();
const show = useSyncPropRef(
() => props.show,
(value) => emit('update:show', value)
);
const getPopoverOptions = () => ({
placement: props.placement,
modifiers: [
@ -126,7 +131,7 @@ export default defineComponent({
const updateLocation = () => {
nextTick(() => {
if (!props.show) {
if (!show.value) {
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 = () => {
if (props.trigger === 'click') {
updateShow(!props.show);
show.value = !show.value;
}
};
@ -154,17 +161,17 @@ export default defineComponent({
emit('select', action, index);
if (props.closeOnClickAction) {
updateShow(false);
show.value = false;
}
};
const onClickAway = () => {
if (
props.show &&
show.value &&
props.closeOnClickOutside &&
(!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, {
eventName: 'touchstart',
@ -228,6 +235,7 @@ export default defineComponent({
</span>
<Popup
ref={popoverRef}
show={show.value}
class={bem([props.theme])}
position={''}
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
### 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
### Props

View File

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

View File

@ -89,4 +89,17 @@ exports[`should render demo and match snapshot 1`] = `
</button>
</span>
</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');
trigger(popover!, 'touchstart');
await trigger(popover!, 'touchstart');
expect(wrapper.emitted('update:show')).toBeFalsy();
document.body.appendChild(root);
trigger(document.body, 'touchstart');
await trigger(document.body, 'touchstart');
expect(wrapper.emitted('update:show')![0]).toEqual([false]);
});