fix(ui): auto-close ancestral popover on any single choice #2429

2501-fix-compatibility-with-older-browsers
Flupsi 2025-08-03 14:28:19 +02:00
rodzic a90bd4bf33
commit 1f2ed52430
8 zmienionych plików z 130 dodań i 65 usunięć

Wyświetl plik

@ -100,11 +100,15 @@ onScopeDispose(() => {
stack?.splice(stack.indexOf(isOpen), 1)
})
// Check if there's an ancestral context to inherit close function from
const ancestralContext = inject(POPOVER_CONTEXT_INJECTION_KEY, null)
// Provide context for child items
const hoveredItem = ref(-2)
provide(POPOVER_CONTEXT_INJECTION_KEY, {
items: ref(0),
hoveredItem
hoveredItem,
close: ancestralContext?.close ?? (() => { isOpen.value = false })
})
// Closing
@ -154,10 +158,7 @@ watch(isOpen, (isOpen) => {
v-bind="color(colorProps)()"
style="display:flex; flex-direction:column;"
>
<slot
name="items"
:close="() => isOpen = false"
/>
<slot name="items" />
</div>
</div>
</teleport>

Wyświetl plik

@ -7,6 +7,7 @@ const value = defineModel<boolean>()
<template>
<PopoverItem
class="checkbox"
:keep-open="true"
@click="value = !value"
>
<i :class="['bi', value ? 'bi-check-square' : 'bi-square']" />

Wyświetl plik

@ -2,20 +2,27 @@
import { inject, ref } from 'vue'
import { type RouterLinkProps, RouterLink } from 'vue-router'
import { POPOVER_CONTEXT_INJECTION_KEY, type PopoverContext } from '~/injection-keys'
import { refDebounced } from '@vueuse/core'
import Button from '~/components/ui/Button.vue'
const emit = defineEmits<{ setId: [value: number] }>()
const isOpen = ref(true)
// Delay closing by 300ms
const isOpenDelayed = refDebounced(isOpen, () => isOpen.value ? 0 : 300)
const { parentPopoverContext, to } = defineProps<{
parentPopoverContext?: PopoverContext;
to?:RouterLinkProps['to'];
icon?: string;
iconAfter?: string;
keepOpen?: boolean;
}>()
const { items, hoveredItem } = parentPopoverContext ?? inject(POPOVER_CONTEXT_INJECTION_KEY, {
const { items, hoveredItem, close } = parentPopoverContext ?? inject(POPOVER_CONTEXT_INJECTION_KEY, {
items: ref(0),
hoveredItem: ref(-2)
hoveredItem: ref(-2),
close: () => { isOpen.value = false }
})
const id = items.value++
@ -23,74 +30,83 @@ emit('setId', id)
</script>
<template>
<a
v-if="to && typeof to === 'string' && to.startsWith('http')"
:href="to.toString()"
class="popover-item"
target="_blank"
>
<i
v-if="icon"
:class="['bi', icon]"
/>
<slot />
<div class="after">
<template v-if="isOpenDelayed">
<a
v-if="to && typeof to === 'string' && to.startsWith('http')"
:href="to.toString()"
class="popover-item"
target="_blank"
@mouseover="hoveredItem = id"
@click="() => { if (!keepOpen) { close() } }"
>
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
v-if="icon"
:class="['bi', icon]"
/>
<slot name="after" />
</div>
</a>
<RouterLink
v-else-if="to"
:to="to"
class="popover-item"
@mouseover="hoveredItem = id"
>
<i
v-if="icon"
:class="['bi', icon]"
/>
<slot />
<slot />
<div class="after">
<div class="after">
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
/>
<slot name="after" />
</div>
</a>
<RouterLink
v-else-if="to"
:to="to"
class="popover-item"
@mouseover="hoveredItem = id"
@click="() => { if (!keepOpen) { close()} }"
>
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
v-if="icon"
:class="['bi', icon]"
/>
<slot name="after" />
</div>
</RouterLink>
<Button
v-else
ghost
thin-font
v-bind="$attrs"
style="
<slot />
<div class="after">
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
/>
<slot name="after" />
</div>
</RouterLink>
<Button
v-else
ghost
thin-font
v-bind="{ ...$attrs, onClick: undefined }"
style="
width: 100%;
text-align: left;
gap: 8px;
"
:icon="icon"
class="popover-item"
@mouseover="hoveredItem = id"
>
<slot />
:icon="icon"
class="popover-item"
:on-click="(event) => {
($attrs.onClick as Function | undefined)?.(event);
if (!keepOpen) { close() }
}"
@mouseover="hoveredItem = id"
>
<slot />
<div class="after">
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
/>
<slot name="after" />
</div>
</Button>
<div class="after">
<i
v-if="iconAfter"
:class="['bi', iconAfter]"
/>
<slot name="after" />
</div>
</Button>
</template>
</template>
<style scoped>
div { color:var(--fw-text-color); }
div { color:var(--fw-text-color); }
</style>
<style lang="scss">

Wyświetl plik

@ -3,11 +3,12 @@ import PopoverRadioItem from './PopoverRadioItem.vue'
import { computed } from 'vue'
const { choices } = defineProps<{ choices:string[] }>()
const { choices, keepOpen } = defineProps<{ choices:string[], keepOpen?: false }>()
const filteredChoices = computed(() => new Set(choices))
const value = defineModel<string>('modelValue', { required: true })
const isOpen = defineModel<boolean>('isOpen', { default: true })
// NOTE: Due to the usage of a ref inside a Proxy, this is reactive.
const choiceValues = new Proxy<Record<string, boolean>>(Object.create(null), {
@ -19,6 +20,10 @@ const choiceValues = new Proxy<Record<string, boolean>>(Object.create(null), {
if (!val || typeof key === 'symbol') return false
value.value = key
if (!keepOpen) {
isOpen.value = false
}
return true
}
})
@ -29,6 +34,7 @@ const choiceValues = new Proxy<Record<string, boolean>>(Object.create(null), {
v-for="choice of filteredChoices"
:key="choice"
v-model="choiceValues[choice]"
:keep-open="keepOpen"
>
{{ choice }}
</PopoverRadioItem>

Wyświetl plik

@ -1,12 +1,15 @@
<script setup lang="ts">
import PopoverItem from './PopoverItem.vue'
const { keepOpen } = defineProps<{ keepOpen?: boolean }>()
const value = defineModel<boolean>('modelValue', { required: true })
</script>
<template>
<PopoverItem
class="checkbox"
:keep-open="keepOpen"
@click="value = !value"
>
<i :class="['bi', value ? 'bi-record-circle' : 'bi-circle']" />

Wyświetl plik

@ -7,7 +7,8 @@ import PopoverItem from './PopoverItem.vue'
const context = inject(POPOVER_CONTEXT_INJECTION_KEY, {
items: ref(0),
hoveredItem: ref(-2)
hoveredItem: ref(-2),
close: () => { isOpen.value = false }
})
const isOpen = ref(false)
@ -25,6 +26,7 @@ watchEffect(() => {
<PopoverItem
:parent-popover-context="context"
class="submenu"
:keep-open="true"
@click="isOpen = !isOpen"
@internal:id="id = $event"
>

Wyświetl plik

@ -15,6 +15,7 @@ export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{
export interface PopoverContext {
items: Ref<number>
hoveredItem: Ref<number>
close: () => void
}
export const POPOVER_INJECTION_KEY = Symbol('popover') as InjectionKey<Ref<boolean>[]>

Wyświetl plik

@ -10,6 +10,7 @@ import PopoverCheckbox from "~/components/ui/popover/PopoverCheckbox.vue"
import PopoverItem from "~/components/ui/popover/PopoverItem.vue"
import PopoverRadio from "~/components/ui/popover/PopoverRadio.vue"
import PopoverSubmenu from "~/components/ui/popover/PopoverSubmenu.vue"
import Toggle from "~/components/ui/Toggle.vue"
// String values
@ -40,6 +41,7 @@ const extraItemsMenu = ref(false)
const linksMenu = ref(false)
const fullMenu= ref(false)
const isOpen = ref(false)
const keepOpen = ref(false)
</script>
```ts
@ -203,7 +205,32 @@ const bcPrivacy = ref("pod");
</Pill>
</template>
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices"/>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" />
</template>
</Popover>
## Keep the popover open
By default, the popover closes when a radiobutton, link, or other item is chosen. The exception is with checkboxes because the user can select multiple options. Override the default behavior by setting the `keep-open` prop on any of the following components:
- `<PopoverRadio>` - keep the popover open when any radio item is selected
- `<PopoverRadioItem>` - keep the popover open when a specific radio item is selected
- `<PopoverItem>` - keep the popover open when the link or button is activated
- `<Popover>` - keep the popover open when any item is selected (clicking outside the popover still closes it)
```vue
<Popover v-model="keepOpen">
<Toggle v-model="keepOpen" label="Show privacy controls" />
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" keep-open />
</template>
</Popover>
```
<Popover v-model="keepOpen">
<Toggle v-model="keepOpen" label="Show privacy controls" />
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" keep-open />
</template>
</Popover>
@ -411,6 +438,10 @@ const isOpen = ref(false)
<PopoverCheckbox v-model="bc">
Bandcamp
</PopoverCheckbox>
<PopoverItem :to="{ name: 'learn-more' }">
Learn more...
</PopoverItem>
<PopoverItem>Cancel</PopoverItem>
</template>
</PopoverSubmenu>
</template>
@ -428,6 +459,10 @@ const isOpen = ref(false)
<PopoverCheckbox v-model="bc">
Bandcamp
</PopoverCheckbox>
<PopoverItem to="https://docs.funkwhale.audio">
Learn more...
</PopoverItem>
<PopoverItem>Cancel</PopoverItem>
</template>
</PopoverSubmenu>
</template>