mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-05-29 09:49:16 +08:00
feat(Popover): support uncontrolled mode (#11244)
This commit is contained in:
parent
f071e48be2
commit
4e9c301012
22
packages/vant/src/composables/use-sync-prop-ref.ts
Normal file
22
packages/vant/src/composables/use-sync-prop-ref.ts
Normal 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;
|
||||
};
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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]);
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user