Skip to content

Commit

Permalink
feat: Popover 2/3 (#312)
Browse files Browse the repository at this point in the history
* add complete api

* add stories, update test

* add comments

Co-authored-by: onmax <[email protected]> @onmax
  • Loading branch information
zernonia authored Aug 18, 2023
1 parent d975c70 commit df969d4
Show file tree
Hide file tree
Showing 20 changed files with 655 additions and 118 deletions.
23 changes: 23 additions & 0 deletions packages/radix-vue/src/Popover/PopoverAnchor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { PopperAnchor, type PopperAnchorProps } from "@/Popper";
import { inject, onBeforeMount, onUnmounted } from "vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
export interface PopoverAnchorProps extends PopperAnchorProps {}
const props = defineProps<PopoverAnchorProps>();
const context = inject(POPOVER_INJECTION_KEY);
onBeforeMount(() => {
context!.hasCustomAnchor.value = true;
});
onUnmounted(() => {
context!.hasCustomAnchor.value = false;
});
</script>

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

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

<script setup lang="ts">
import { inject } from "vue";
import { Primitive, type PrimitiveProps } from "@/Primitive";
Expand All @@ -10,8 +6,9 @@ import {
type PopoverProvideValue,
} from "./PopoverRoot.vue";
const injectedValue = inject<PopoverProvideValue>(POPOVER_INJECTION_KEY);
const context = inject<PopoverProvideValue>(POPOVER_INJECTION_KEY);
export interface PopoverCloseProps extends PrimitiveProps {}
const props = withDefaults(defineProps<PopoverCloseProps>(), {
as: "button",
});
Expand All @@ -21,10 +18,8 @@ const props = withDefaults(defineProps<PopoverCloseProps>(), {
<Primitive
:type="as === 'button' ? 'button' : undefined"
:as="as"
:aria-expanded="injectedValue?.open.value || false"
:data-state="injectedValue?.open.value ? 'open' : 'closed'"
:as-child="props.asChild"
@click="injectedValue?.hidePopover"
@click="context?.onOpenChange(false)"
>
<slot />
</Primitive>
Expand Down
103 changes: 33 additions & 70 deletions packages/radix-vue/src/Popover/PopoverContent.vue
Original file line number Diff line number Diff line change
@@ -1,82 +1,45 @@
<script lang="ts">
export interface PopoverContentProps extends PopperContentProps {
forceMount?: boolean;
}
</script>

<script setup lang="ts">
import { inject, onUnmounted, watchEffect } from "vue";
import { trapFocus } from "../shared/trap-focus";
import PopoverContentModal from "./PopoverContentModal.vue";
import PopoverContentNonModal from "./PopoverContentNonModal.vue";
import {
POPOVER_INJECTION_KEY,
type PopoverProvideValue,
} from "./PopoverRoot.vue";
import { PopperContent, type PopperContentProps } from "@/Popper";
import { Primitive, usePrimitiveElement } from "@/Primitive";
import { onClickOutside } from "@vueuse/core";
const injectedValue = inject<PopoverProvideValue>(POPOVER_INJECTION_KEY);
type PopoverContentImplProps,
type PopoverContentImplEmits,
} from "./PopoverContentImpl.vue";
import { useEmitAsProps } from "@/shared";
import { Presence } from "@/Presence";
import { inject } from "vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
import { PopperContentPropsDefaultValue } from "@/Popper";
export interface PopoverContentProps extends PopoverContentImplProps {
/**
* Used to force mounting when more control is needed. Useful when
* controlling animation with React animation libraries.
*/
forceMount?: true;
}
export type PopoverContentEmits = PopoverContentImplEmits;
const props = withDefaults(defineProps<PopoverContentProps>(), {
side: "bottom",
align: "center",
avoidCollisions: true,
});
const { primitiveElement, currentElement: tooltipContentElement } =
usePrimitiveElement();
watchEffect(() => {
if (tooltipContentElement.value) {
if (injectedValue?.open.value) {
trapFocus(tooltipContentElement.value!);
window.addEventListener("keydown", closePopoverOnEscape);
} else {
if (injectedValue?.triggerElement.value) {
injectedValue?.triggerElement.value.focus();
clearEvent();
}
}
}
});
onClickOutside(tooltipContentElement, (event) => {
injectedValue?.hidePopover();
event.preventDefault();
event.stopPropagation();
...PopperContentPropsDefaultValue,
});
const emits = defineEmits<PopoverContentEmits>();
function closePopoverOnEscape(e: KeyboardEvent) {
if (e.key === "Escape") {
injectedValue?.hidePopover();
}
}
const context = inject(POPOVER_INJECTION_KEY);
function clearEvent() {
window.removeEventListener("keydown", closePopoverOnEscape);
}
onUnmounted(() => {
clearEvent();
});
const emitsAsProps = useEmitAsProps(emits);
</script>

<template>
<PopperContent
ref="primitiveElement"
v-bind="props"
v-if="injectedValue?.open.value"
>
<Primitive
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"
:as="as"
<Presence :present="forceMount || context!.open.value">
<PopoverContentModal
v-if="context?.modal.value"
v-bind="{ ...props, ...emitsAsProps }"
>
<slot />
</Primitive>
</PopperContent>
<slot></slot>
</PopoverContentModal>
<PopoverContentNonModal v-else v-bind="{ ...props, ...emitsAsProps }">
<slot></slot>
</PopoverContentNonModal>
</Presence>
</template>
80 changes: 80 additions & 0 deletions packages/radix-vue/src/Popover/PopoverContentImpl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { PopperContent, type PopperContentProps } from "@/Popper";
import {
DismissableLayer,
type DismissableLayerEmits,
type DismissableLayerProps,
} from "@/DismissableLayer";
import { FocusScope, type FocusScopeProps } from "@/FocusScope";
import { useFocusGuards } from "@/shared";
import { inject } from "vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
export interface PopoverContentImplProps
extends PopperContentProps,
DismissableLayerProps {
/**
* Whether focus should be trapped within the `MenuContent`
* (default: false)
*/
trapFocus?: FocusScopeProps["trapped"];
}
export type PopoverContentImplEmits = DismissableLayerEmits & {
/**
* Event handler called when auto-focusing on open.
* Can be prevented.
*/
(e: "openAutoFocus", event: Event): void;
/**
* Event handler called when auto-focusing on close.
* Can be prevented.
*/
(e: "closeAutoFocus", event: Event): void;
};
const props = defineProps<PopoverContentImplProps>();
const emits = defineEmits<PopoverContentImplEmits>();
const context = inject(POPOVER_INJECTION_KEY);
useFocusGuards();
</script>

<template>
<FocusScope
asChild
loop
:trapped="trapFocus"
@mount-auto-focus="emits('openAutoFocus', $event)"
@unmount-auto-focus="emits('closeAutoFocus', $event)"
>
<DismissableLayer
asChild
:disable-outside-pointer-events="disableOutsidePointerEvents"
@pointer-down-outside="emits('pointerDownOutside', $event)"
@interact-outside="emits('interactOutside', $event)"
@escape-key-down="emits('escapeKeyDown', $event)"
@focus-outside="emits('focusOutside', $event)"
@dismiss="context?.onOpenChange(false)"
>
<PopperContent
v-bind="props"
:data-state="context?.open.value ? 'open' : 'closed'"
role="dialog"
:id="context?.contentId"
: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)',
}"
>
<slot></slot>
</PopperContent>
</DismissableLayer>
</FocusScope>
</template>
55 changes: 55 additions & 0 deletions packages/radix-vue/src/Popover/PopoverContentModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { useBodyScrollLock, useEmitAsProps } from "@/shared";
import { inject, ref } from "vue";
import PopoverContentImpl, {
type PopoverContentImplProps,
type PopoverContentImplEmits,
} from "./PopoverContentImpl.vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
const context = inject(POPOVER_INJECTION_KEY);
const isRightClickOutsideRef = ref(false);
useBodyScrollLock(true);
const props = defineProps<PopoverContentImplProps>();
const emits = defineEmits<PopoverContentImplEmits>();
const emitsAsProps = useEmitAsProps(emits);
// aria-hide everything except the content (better supported equivalent to setting aria-modal)
// React.useEffect(() => {
// const content = contentRef.current;
// if (content) return hideOthers(content);
// }, []);
</script>

<template>
<PopoverContentImpl
v-bind="{ ...props, ...emitsAsProps }"
:trap-focus="context?.open.value"
disableOutsidePointerEvents
@close-auto-focus.prevent="
(event) => {
emits('closeAutoFocus', event);
if (!isRightClickOutsideRef) context?.triggerElement.value?.focus();
}
"
@pointer-down-outside="
(event) => {
emits('pointerDownOutside', event);
const originalEvent = event.detail.originalEvent;
const ctrlLeftClick =
originalEvent.button === 0 && originalEvent.ctrlKey === true;
const isRightClick = originalEvent.button === 2 || ctrlLeftClick;
isRightClickOutsideRef = isRightClick;
}
"
@focus-outside.prevent
>
<slot></slot>
</PopoverContentImpl>
</template>
72 changes: 72 additions & 0 deletions packages/radix-vue/src/Popover/PopoverContentNonModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { useEmitAsProps } from "@/shared";
import { inject, ref } from "vue";
import PopoverContentImpl, {
type PopoverContentImplProps,
type PopoverContentImplEmits,
} from "./PopoverContentImpl.vue";
import { POPOVER_INJECTION_KEY } from "./PopoverRoot.vue";
const context = inject(POPOVER_INJECTION_KEY);
const hasInteractedOutsideRef = ref(false);
const hasPointerDownOutsideRef = ref(false);
const props = defineProps<PopoverContentImplProps>();
const emits = defineEmits<PopoverContentImplEmits>();
const emitsAsProps = useEmitAsProps(emits);
</script>

<template>
<PopoverContentImpl
v-bind="{ ...props, ...emitsAsProps }"
:trap-focus="false"
:disableOutsidePointerEvents="false"
@close-auto-focus="
(event) => {
emits('closeAutoFocus', event);
if (!event.defaultPrevented) {
if (!hasInteractedOutsideRef) context?.triggerElement.value?.focus();
// Always prevent auto focus because we either focus manually or want user agent focus
event.preventDefault();
}
hasInteractedOutsideRef = false;
hasPointerDownOutsideRef = false;
}
"
@interact-outside="
async (event) => {
emits('interactOutside', event);
if (!event.defaultPrevented) {
hasInteractedOutsideRef = true;
if (event.detail.originalEvent.type === 'pointerdown') {
hasPointerDownOutsideRef = true;
}
}
// Prevent dismissing when clicking the trigger.
// As the trigger is already setup to close, without doing so would
// cause it to close and immediately open.
const target = event.target as HTMLElement;
const targetIsTrigger = context?.triggerElement.value?.contains(target);
if (targetIsTrigger) event.preventDefault();
// On Safari if the trigger is inside a container with tabIndex={0}, when clicked
// we will get the pointer down outside event on the trigger, but then a subsequent
// focus outside event on the container, we ignore any focus outside event when we've
// already had a pointer down outside event.
if (
event.detail.originalEvent.type === 'focusin' &&
hasPointerDownOutsideRef
) {
event.preventDefault();
}
}
"
>
<slot></slot>
</PopoverContentImpl>
</template>
Loading

0 comments on commit df969d4

Please sign in to comment.