-
-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add complete api * add stories, update test * add comments Co-authored-by: onmax <[email protected]> @onmax
- Loading branch information
Showing
20 changed files
with
655 additions
and
118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.