-
diff --git a/frontend/src/components/VModal/VModalContent.vue b/frontend/src/components/VModal/VModalContent.vue
index 2a4abb6ccfa..488e626e334 100644
--- a/frontend/src/components/VModal/VModalContent.vue
+++ b/frontend/src/components/VModal/VModalContent.vue
@@ -4,6 +4,8 @@
*/
import { toRefs, ref, computed, useAttrs } from "vue"
+import { useElementSize } from "@vueuse/core"
+
import { useDialogContent } from "~/composables/use-dialog-content"
import type { ModalColorMode, ModalVariant } from "~/types/modal"
@@ -16,6 +18,7 @@ defineOptions({
const props = withDefaults(
defineProps<{
+ id: string
visible: boolean
hide: () => void
hideOnEsc?: boolean
@@ -87,6 +90,13 @@ const handleClose = (event: MouseEvent) => {
props.hide()
}
+const modalHeaderRef = ref(null)
+const { height: modalHeaderHeight } = useElementSize(
+ modalHeaderRef,
+ { width: 0, height: 0 },
+ { box: "border-box" }
+)
+
defineExpose({
dialogRef,
deactivateFocusTrap,
@@ -108,6 +118,7 @@ defineExpose({
-
+
diff --git a/frontend/src/components/VPopover/VPopover.vue b/frontend/src/components/VPopover/VPopover.vue
index d1b87223c5d..dec173ad2bd 100644
--- a/frontend/src/components/VPopover/VPopover.vue
+++ b/frontend/src/components/VPopover/VPopover.vue
@@ -15,6 +15,11 @@ import type { Placement, Strategy } from "@floating-ui/dom"
const props = withDefaults(
defineProps<{
+ /**
+ * The id used to keep track of the popover in the open dialog stack, to enable
+ * nested dialogs.
+ */
+ id: string
/**
* Whether the popover should show when the trigger is hovered on.
*/
@@ -131,6 +136,7 @@ const triggerRef = computed(() =>
)
const { close, onTriggerClick, triggerA11yProps } = useDialogControl({
+ id: props.id,
visibleRef,
emit: emit as SetupContext["emit"],
})
@@ -162,6 +168,7 @@ defineExpose({
void
hideOnEsc?: boolean
@@ -62,6 +63,7 @@ const { onKeyDown, onBlur, heightProperties, style } = usePopoverContent({
void
export function useDialogControl({
+ id,
visibleRef,
nodeRef,
lockBodyScroll,
emit,
deactivateFocusTrap,
}: {
+ id: MaybeRefOrGetter
visibleRef?: Ref
nodeRef?: Ref
lockBodyScroll?: ComputedRef | boolean
@@ -59,7 +61,7 @@ export function useDialogControl({
lock = bodyScroll.lock
unlock = bodyScroll.unlock
}
- const shouldLockBodyScroll = computed(() => unref(lockBodyScroll) ?? false)
+ const shouldLockBodyScroll = computed(() => toValue(lockBodyScroll) ?? false)
watch(shouldLockBodyScroll, (shouldLock) => {
if (shouldLock) {
if (internalVisibleRef.value) {
@@ -70,9 +72,13 @@ export function useDialogControl({
}
})
- const open = () => (internalVisibleRef.value = true)
+ const open = () => {
+ internalVisibleRef.value = true
+ pushModalToStack()
+ }
const close = () => {
+ popModalFromStack()
const fn = toValue(deactivateFocusTrap)
if (fn) {
fn()
@@ -80,8 +86,28 @@ export function useDialogControl({
internalVisibleRef.value = false
}
+ const pushModalToStack = () => {
+ const { push } = useDialogStack()
+ const idValue = toValue(id)
+ if (idValue) {
+ push(idValue)
+ }
+ }
+
+ const popModalFromStack = () => {
+ const openModalStack = useDialogStack()
+ const idValue = toValue(id)
+ if (idValue && openModalStack.indexOf(idValue) > -1) {
+ openModalStack.pop()
+ }
+ }
+
const onTriggerClick = () => {
- internalVisibleRef.value = !internalVisibleRef.value
+ if (internalVisibleRef.value) {
+ close()
+ } else {
+ open()
+ }
}
return {
diff --git a/frontend/src/composables/use-dialog-stack.ts b/frontend/src/composables/use-dialog-stack.ts
new file mode 100644
index 00000000000..8ef2fc38ea5
--- /dev/null
+++ b/frontend/src/composables/use-dialog-stack.ts
@@ -0,0 +1,36 @@
+import { computed, ref } from "vue"
+
+const stack = ref([])
+
+/**
+ * This composable allows displaying multiple dialogs on top of each other,
+ * and routing the events to the active, top-most dialog.
+ */
+export const useDialogStack = () => {
+ const push = (id: string) => {
+ stack.value.push(id)
+ }
+ const pop = () => {
+ stack.value.pop()
+ }
+ const clear = () => {
+ stack.value = []
+ }
+ const indexOf = (id: string) => {
+ return stack.value.indexOf(id)
+ }
+
+ /**
+ * The top-level dialog in the UI that intercepts 'outside' events.
+ */
+ const activeDialog = computed(() => stack.value[stack.value.length - 1])
+
+ return {
+ stack,
+ push,
+ pop,
+ clear,
+ indexOf,
+ activeDialog,
+ }
+}
diff --git a/frontend/src/composables/use-event-listener-outside.ts b/frontend/src/composables/use-event-listener-outside.ts
index dcdaf13a1dd..64a1cb17839 100644
--- a/frontend/src/composables/use-event-listener-outside.ts
+++ b/frontend/src/composables/use-event-listener-outside.ts
@@ -1,6 +1,7 @@
-import { Ref, ref, watch } from "vue"
+import { type Ref, ref, watch } from "vue"
import { contains, getDocument, isInDocument } from "~/utils/reakit-utils/dom"
+import { useDialogStack } from "~/composables/use-dialog-stack"
interface Props {
/**
@@ -67,6 +68,11 @@ export const useEventListenerOutside = ({
if (trigger && contains(trigger, target)) {
return
}
+ // Event is in the top-level dialog, so shouldn't be handled by the
+ // parent dialogs
+ if (useDialogStack().activeDialog.value !== container.id) {
+ return
+ }
listener(event)
}
diff --git a/frontend/src/composables/use-hide-on-click-outside.ts b/frontend/src/composables/use-hide-on-click-outside.ts
index dfd67f405f9..520fd5545f2 100644
--- a/frontend/src/composables/use-hide-on-click-outside.ts
+++ b/frontend/src/composables/use-hide-on-click-outside.ts
@@ -1,4 +1,4 @@
-import { ref, watch, computed, Ref } from "vue"
+import { ref, watch, computed, type Ref } from "vue"
import { getDocument } from "~/utils/reakit-utils/dom"
import { useEventListenerOutside } from "~/composables/use-event-listener-outside"
@@ -15,11 +15,10 @@ function useMouseDownTargetRef({
dialogRef,
visibleRef,
hideOnClickOutsideRef,
-}: {
- dialogRef: Props["dialogRef"]
- visibleRef: Props["visibleRef"]
- hideOnClickOutsideRef: Props["hideOnClickOutsideRef"]
-}): Ref {
+}: Pick<
+ Props,
+ "dialogRef" | "visibleRef" | "hideOnClickOutsideRef"
+>): Ref {
const mouseDownTargetRef = ref()
watch(
@@ -82,6 +81,7 @@ export function useHideOnClickOutside({
eventType: "focusin",
listener: (event: Event) => {
const document = getDocument(dialogRef.value)
+
if (event.target !== document) {
hideRef.value()
}
diff --git a/frontend/src/composables/use-popover-content.ts b/frontend/src/composables/use-popover-content.ts
index accde7dac87..bf73fe17e27 100644
--- a/frontend/src/composables/use-popover-content.ts
+++ b/frontend/src/composables/use-popover-content.ts
@@ -10,6 +10,7 @@ import type { Ref, ToRefs, SetupContext } from "vue"
import type { Placement, Strategy } from "@floating-ui/dom"
export type PopoverContentProps = {
+ id: string
visible: boolean
hide: () => void
hideOnEsc: boolean
diff --git a/frontend/src/constants/dialogs.ts b/frontend/src/constants/dialogs.ts
new file mode 100644
index 00000000000..be97de61771
--- /dev/null
+++ b/frontend/src/constants/dialogs.ts
@@ -0,0 +1,8 @@
+export const CONTENT_REPORT_DIALOG = "content-report-dialog"
+export const SEARCH_TYPES_DIALOG = "search-types-dialog"
+export const EXTERNAL_SEARCH_DIALOG = "external-search-dialog"
+export const CONTENT_SETTINGS_DIALOG = "content-settings-dialog"
+export const HOMEPAGE_CONTENT_SETTINGS_DIALOG =
+ "homepage-content-settings-dialog"
+export const RECENT_SEARCHES_DIALOG = "recent-searches-dialog"
+export const PAGES_DIALOG = "pages-dialog"
diff --git a/frontend/src/layouts/search-layout.vue b/frontend/src/layouts/search-layout.vue
index f9182700640..7ff2f8fc8d9 100644
--- a/frontend/src/layouts/search-layout.vue
+++ b/frontend/src/layouts/search-layout.vue
@@ -70,7 +70,7 @@ provide(IsSidebarVisibleKey, isSidebarVisible)
-
+
{{ visible }}
diff --git a/frontend/test/unit/specs/components/v-popover.spec.js b/frontend/test/unit/specs/components/v-popover.spec.js
index 917f851e348..3f38ac67258 100644
--- a/frontend/test/unit/specs/components/v-popover.spec.js
+++ b/frontend/test/unit/specs/components/v-popover.spec.js
@@ -25,7 +25,7 @@ const TestWrapper = createApp({}).component("TestWrapper", {
External area
-
+
{{ visible ? 'Close' : 'Open' }}