kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
fix(ui): auto-close ancestral popover on any single choice #2429
rodzic
a90bd4bf33
commit
1f2ed52430
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']" />
|
||||
|
|
|
|||
|
|
@ -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,11 +30,14 @@ emit('setId', id)
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<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="icon"
|
||||
|
|
@ -48,6 +58,7 @@ emit('setId', id)
|
|||
:to="to"
|
||||
class="popover-item"
|
||||
@mouseover="hoveredItem = id"
|
||||
@click="() => { if (!keepOpen) { close()} }"
|
||||
>
|
||||
<i
|
||||
v-if="icon"
|
||||
|
|
@ -67,7 +78,7 @@ emit('setId', id)
|
|||
v-else
|
||||
ghost
|
||||
thin-font
|
||||
v-bind="$attrs"
|
||||
v-bind="{ ...$attrs, onClick: undefined }"
|
||||
style="
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
|
@ -75,6 +86,10 @@ emit('setId', id)
|
|||
"
|
||||
:icon="icon"
|
||||
class="popover-item"
|
||||
:on-click="(event) => {
|
||||
($attrs.onClick as Function | undefined)?.(event);
|
||||
if (!keepOpen) { close() }
|
||||
}"
|
||||
@mouseover="hoveredItem = id"
|
||||
>
|
||||
<slot />
|
||||
|
|
@ -88,6 +103,7 @@ emit('setId', id)
|
|||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div { color:var(--fw-text-color); }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>[]>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -207,6 +209,31 @@ const bcPrivacy = ref("pod");
|
|||
</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>
|
||||
|
||||
## Items
|
||||
|
||||
Popovers contain a list of menu items. Items can contain different information based on their type.
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue