Skip to content

Commit

Permalink
[Popover]: Complete API
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Jul 26, 2023
1 parent b9904bb commit 13fa41b
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 129 deletions.
15 changes: 15 additions & 0 deletions packages/radix-vue/src/Popover/PopoverAnchor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import type { PopperAnchorProps } from "@/Popper";
export interface PopoverAnchorProps extends PopperAnchorProps { }
</script>

<script setup lang="ts">
import { PopperAnchor } from "@/Popper";
const props = defineProps<PopoverAnchorProps>();
</script>

<template>
<PopperAnchor v-bind="props"></PopperAnchor>
</template>
10 changes: 9 additions & 1 deletion packages/radix-vue/src/Popover/PopoverArrow.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
<script lang="ts">
import type { PopperArrowProps } from "@/Popper/PopperArrow.vue";
export interface PopoverArrowProps extends PopperArrowProps { }
</script>

<script setup lang="ts">
import { PopperArrow } from "@/Popper";
const props = defineProps<PopoverArrowProps>();
</script>

<template>
<PopperArrow></PopperArrow>
<PopperArrow v-bind="props"></PopperArrow>
</template>
20 changes: 6 additions & 14 deletions packages/radix-vue/src/Popover/PopoverClose.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
<script lang="ts">
export interface PopoverCloseProps extends PrimitiveProps {}
export interface PopoverCloseProps extends PrimitiveProps { }
</script>

<script setup lang="ts">
import { inject } from "vue";
import { PrimitiveButton, type PrimitiveProps } from "@/Primitive";
import {
POPOVER_INJECTION_KEY,
type PopoverProvideValue,
} from "./PopoverRoot.vue";
import { inject } from "vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
const injectedValue = inject<PopoverProvideValue>(POPOVER_INJECTION_KEY);
const injectedValue = inject(POPOVER_INJECTION_KEY);
const props = defineProps<PopoverCloseProps>();
</script>

<template>
<PrimitiveButton
type="button"
:aria-expanded="injectedValue?.open.value || false"
:data-state="injectedValue?.open.value ? 'open' : 'closed'"
:as-child="props.asChild"
@click="injectedValue?.hidePopover"
>
<PrimitiveButton type="button" :aria-expanded="injectedValue?.open.value || false"
:data-state="injectedValue?.dataState.value" :as-child="props.asChild" @click="injectedValue?.hidePopover">
<slot />
</PrimitiveButton>
</template>
209 changes: 147 additions & 62 deletions packages/radix-vue/src/Popover/PopoverContent.vue
Original file line number Diff line number Diff line change
@@ -1,90 +1,175 @@
<script lang="ts">
export interface PopoverContentProps extends PopperContentProps {
forceMount?: boolean;
import type { PopperContentProps } from "@/Popper";
import { trapFocus } from "@/shared";
import { onClickOutside } from "@vueuse/core";
export interface popoverContentProps
extends PrimitiveProps,
Pick<
PopperContentProps,
| "side"
| "sideOffset"
| "align"
| "alignOffset"
| "avoidCollisions"
| "collisionBoundary"
| "collisionPadding"
| "arrowPadding"
| "sticky"
| "hideWhenDetached"
> {
/**
* Whether or not prevent default be called on the pointerDownOutside event.
*
* @default true
*/
onOpenAutoFocus?: MaybeRef<boolean>;
/**
* Whether or not prevent default be called on the focusOutside event.
*/
onCloseAutoFocus?: MaybeRef<boolean>;
/**
* Whether or not prevent default be called on the escapeKeyDown event.
*/
onEscapeKeyDown?: MaybeRef<boolean>;
/**
* Whether or not prevent default be called on the pointerDownOutside event.
*/
onPointerDownOutside?: MaybeRef<boolean>;
/**
* Whether or not prevent default be called on the focusOutside event.
*/
onInteractOutside?: MaybeRef<boolean>;
}
export interface PopoverContentEmit {
/**
* Event handler called when focus moves into the component after opening.
*/
(e: "openAutoFocus", event: Event): void;
/**
* Event handler called when focus moves to the trigger after closing.
*/
(e: "closeAutoFocus", event: Event): void;
/**
* Event handler called when the escape key is down.
*/
(e: "escapeKeyDown", event: KeyboardEvent): void;
/**
* Event handler called when a pointer event occurs outside the bounds of the component.
*/
(e: "pointerDownOutside", event: PointerEvent): void;
/**
* Event handler called when focus moves outside the bounds of the component.
*/
(e: "focusOutside", event: Event): void;
/**
* Event handler called when an interaction (pointer or focus event) happens outside the bounds of the component.
*/
(e: "interactOutside", event: Event): void;
}
</script>

<script setup lang="ts">
import { inject, onUnmounted, watchEffect } from "vue";
import { trapFocus } from "../shared/trap-focus";
import { useClickOutside } from "../shared/useClickOutside";
import { PopperContent } from "@/Popper";
import {
POPOVER_INJECTION_KEY,
type PopoverProvideValue,
} from "./PopoverRoot.vue";
import { PopperContent, type PopperContentProps } from "@/Popper";
import { PrimitiveDiv, usePrimitiveElement } from "@/Primitive";
PrimitiveDiv,
usePrimitiveElement,
type PrimitiveProps,
} from "@/Primitive";
import { inject, watch, type MaybeRef } from "vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
const injectedValue = inject<PopoverProvideValue>(POPOVER_INJECTION_KEY);
const injectedValue = inject(POPOVER_INJECTION_KEY);
const props = withDefaults(defineProps<PopoverContentProps>(), {
const props = withDefaults(defineProps<popoverContentProps>(), {
asChild: false,
side: "bottom",
sideOffset: 0,
align: "center",
avoidCollisions: true,
collisionBoundary: () => [],
collisionPadding: 0,
arrowPadding: 0,
sticky: "partial",
hideWhenDetached: false,
onOpenAutoFocus: true,
onCloseAutoFocus: false,
onEscapeKeyDown: false,
onPointerDownOutside: true,
onInteractOutside: false,
});
const { primitiveElement, currentElement: tooltipContentElement } =
const { primitiveElement, currentElement: popoverContentElement } =
usePrimitiveElement();
watchEffect(() => {
if (tooltipContentElement.value) {
if (injectedValue?.open.value) {
trapFocus(tooltipContentElement.value!);
window.addEventListener("mousedown", closeDialogWhenClickOutside);
window.addEventListener("keydown", closePopoverOnEscape);
} else {
if (injectedValue?.triggerElement.value) {
injectedValue?.triggerElement.value.focus();
clearEvent();
}
}
}
});
watch(
() => injectedValue?.open.value,
() => {
if (!props.onOpenAutoFocus) return;
function closeDialogWhenClickOutside(e: MouseEvent) {
if (injectedValue?.triggerElement.value?.contains(e.target as Node)) {
return;
if (popoverContentElement.value && injectedValue?.open.value) {
trapFocus(popoverContentElement.value!);
} else if (injectedValue?.triggerElement.value) {
injectedValue?.triggerElement.value.focus();
}
}
);
const clickOutside = useClickOutside(e, tooltipContentElement.value!);
if (clickOutside) {
injectedValue?.hidePopover();
e.preventDefault();
e.stopPropagation();
}
}
const emit = defineEmits<PopoverContentEmit>();
function closePopoverOnEscape(e: KeyboardEvent) {
if (e.key === "Escape") {
injectedValue?.hidePopover();
let onClickOutsideCleanup: (() => void) | undefined;
watch(
() => props.onPointerDownOutside,
() => {
if (props.onPointerDownOutside) {
onClickOutsideCleanup = onClickOutside(popoverContentElement, (e) => {
emit("pointerDownOutside", e);
});
} else {
onClickOutsideCleanup?.();
}
},
{
immediate: true,
flush: "post",
}
}
);
function clearEvent() {
window.removeEventListener("mousedown", closeDialogWhenClickOutside);
window.removeEventListener("keydown", closePopoverOnEscape);
function onEscapeKeyDownFn(event: KeyboardEvent) {
injectedValue?.hidePopover();
emit("escapeKeyDown", event);
}
onUnmounted(() => {
clearEvent();
});
</script>

<template>
<PopperContent
ref="primitiveElement"
v-bind="props"
v-if="injectedValue?.open.value"
>
<PrimitiveDiv
v-if="injectedValue?.open.value"
:data-state="injectedValue?.open.value ? 'open' : 'closed'"
:data-side="props.side"
:data-align="props.align"
role="tooltip"
:as-child="props.asChild"
>
<PopperContent v-if="injectedValue?.open.value" ref="primitiveElement" :side="props.side" :sideOffset="props.sideOffset"
:align="props.align" :alignOffset="props.alignOffset" :avoidCollisions="props.avoidCollisions"
:collisionBoundary="props.collisionBoundary" :collisionPadding="props.collisionPadding"
:arrowPadding="props.arrowPadding" :sticky="props.sticky" :hideWhenDetached="props.hideWhenDetached" style="
--radix-popover-content-transform-origin: var(
--radix-popper-transform-origin
);
--radix-popover-content-available-width: var(
--radix-popper-available-width
);
--radix-popover-content-available-height: var(
--radix-popper-available-height
);
--radix-popover-trigger-width: var(--radix-popper-anchor-width);
--radix-popover-trigger-height: var(--radix-popper-anchor-height);
" @keydown.esc="props.onEscapeKeyDown && onEscapeKeyDownFn($event)">
<PrimitiveDiv :data-state="injectedValue?.dataState.value" :data-side="props.side" :data-align="props.align"
:as-child="props.asChild" role="dialog" tabindex="-1" :id="injectedValue?.contentId">
<slot />
</PrimitiveDiv>
</PopperContent>
Expand Down
25 changes: 22 additions & 3 deletions packages/radix-vue/src/Popover/PopoverRoot.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
<script lang="ts">
import type { Ref, InjectionKey } from "vue";
import type { Ref, InjectionKey, ComputedRef } from "vue";
import { useId } from "@/shared";
type PopoverState = "open" | "closed";
export interface PopoverRootProps {
/**
* The open state of the popover when it is initially rendered. Use when you do not need to control its open state.
*/
defaultOpen?: boolean;
/**
* The controlled open state of the popover.
*/
open?: boolean;
//onOpenChange?: void;
/**
* The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.
*
* @default false
*/
modal?: boolean;
}
Expand All @@ -16,11 +31,13 @@ export type PopoverProvideValue = {
showPopover(): void;
hidePopover(): void;
triggerElement: Ref<HTMLElement | undefined>;
dataState: ComputedRef<PopoverState>;
contentId: string;
};
</script>

<script setup lang="ts">
import { provide, ref } from "vue";
import { computed, provide, ref } from "vue";
import { useVModel } from "@vueuse/core";
import { PopperRoot } from "@/Popper";
Expand Down Expand Up @@ -50,6 +67,8 @@ provide<PopoverProvideValue>(POPOVER_INJECTION_KEY, {
open.value = false;
},
triggerElement,
dataState: computed(() => (open.value ? "open" : "closed")),
contentId: useId(),
});
</script>

Expand Down
13 changes: 4 additions & 9 deletions packages/radix-vue/src/Popover/PopoverTrigger.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
export interface PopoverTriggerProps extends PrimitiveProps {}
export interface PopoverTriggerProps extends PrimitiveProps { }
</script>

<script setup lang="ts">
Expand Down Expand Up @@ -37,14 +37,9 @@ function handleClick() {

<template>
<PopperAnchor asChild>
<PrimitiveButton
type="button"
ref="primitiveElement"
:aria-expanded="injectedValue?.open.value || false"
:data-state="injectedValue?.open.value ? 'open' : 'closed'"
:as-child="props.asChild"
@click="handleClick"
>
<PrimitiveButton type="button" ref="primitiveElement" :aria-expanded="injectedValue?.open.value || false"
:aria-controls="injectedValue?.contentId" aria-haspopup="dialog" :data-state="injectedValue?.dataState.value"
:as-child="props.asChild" @click="handleClick">
<slot />
</PrimitiveButton>
</PopperAnchor>
Expand Down
2 changes: 1 addition & 1 deletion packages/radix-vue/src/Popper/PopperAnchor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "@/Primitive";
import { POPPER_ROOT_KEY } from "./PopperRoot.vue";
interface PopperAnchorProps extends PrimitiveProps {
export interface PopperAnchorProps extends PrimitiveProps {
element?: HTMLElement;
}
Expand Down
Loading

0 comments on commit 13fa41b

Please sign in to comment.