diff --git a/packages/components/popper/src/popperAnchor.ts b/packages/components/popper/src/popperAnchor.ts index 2ec99461b..1d896a02c 100644 --- a/packages/components/popper/src/popperAnchor.ts +++ b/packages/components/popper/src/popperAnchor.ts @@ -1,5 +1,5 @@ import type { PropType, Ref } from 'vue' -import { defineComponent, h, ref, toRefs, watch } from 'vue' +import { defineComponent, h, onMounted, ref, toRefs } from 'vue' import type { ElementType, @@ -48,7 +48,7 @@ const popperAnchor = defineComponent({ const forwardedRef = useForwardRef() const composedRefs = useComposedRefs(_ref, forwardedRef) - watch(_ref, () => { + onMounted(() => { inject.anchor.value = virtualRef.value?.value || (_ref.value as Measurable) }) @@ -62,11 +62,7 @@ const popperAnchor = defineComponent({ ...attrsAnchor, asChild: asChild.value, ref: composedRefs, - }, - { - default: () => slots.default && slots.default?.(), - }, - ) + }, slots) return originalReturn }, diff --git a/packages/components/popper/src/popperArrow.ts b/packages/components/popper/src/popperArrow.ts index 02c71388c..b3365eb7c 100644 --- a/packages/components/popper/src/popperArrow.ts +++ b/packages/components/popper/src/popperArrow.ts @@ -42,7 +42,7 @@ const popperArrow = defineComponent({ const { height, width, asChild, scopeOkuPopper } = toRefs(props) const contentInject = usePopperContentInject(ARROW_NAME, scopeOkuPopper.value) const baseSide = computed(() => { - return OPPOSITE_SIDE[contentInject.placedSide.value] + return contentInject?.placedSide.value ? OPPOSITE_SIDE[contentInject.placedSide.value] : '' }) const forwardedRef = useForwardRef() @@ -52,26 +52,26 @@ const popperArrow = defineComponent({ 'span', { ref: (el: any) => { - contentInject.onAnchorChange(el) + contentInject.onArrowChange(el) return undefined }, style: { position: 'absolute', - left: contentInject.arrowX?.value, - top: contentInject.arrowY?.value, + left: `${contentInject.arrowX?.value}px`, + top: `${contentInject.arrowY?.value}px`, [baseSide.value]: '0px', transformOrigin: { top: '', right: '0px 0px', bottom: 'center 0px', left: '100% 0px', - }[contentInject.placedSide.value], + }[contentInject.placedSide.value!], transform: { top: 'translateY(100%)', right: 'translateY(50%) rotate(90deg) translateX(-50%)', bottom: 'rotate(180deg)', left: 'translateY(50%) rotate(-90deg) translateX(50%)', - }[contentInject.placedSide.value], + }[contentInject.placedSide.value!], visibility: contentInject.shouldHideArrow.value ? 'hidden' : undefined, diff --git a/packages/components/popper/src/popperContent.ts b/packages/components/popper/src/popperContent.ts index f3fad5ec9..e2706185d 100644 --- a/packages/components/popper/src/popperContent.ts +++ b/packages/components/popper/src/popperContent.ts @@ -3,7 +3,7 @@ import { computed, defineComponent, h, onMounted, ref, toRefs, watch, watchEffec import { Primitive, primitiveProps } from '@oku-ui/primitive' import type { ElementType, PrimitiveProps } from '@oku-ui/primitive' -import { computedEager, useCallbackRef, useComposedRefs, useForwardRef, useSize } from '@oku-ui/use-composable' +import { useComposedRefs, useForwardRef, useSize } from '@oku-ui/use-composable' import { autoUpdate, flip, arrow as floatingUIarrow, hide, limitShift, offset, shift, size, useFloating } from '@floating-ui/vue' import type { DetectOverflowOptions, @@ -12,21 +12,18 @@ import type { Placement, } from '@floating-ui/vue' import type { Align, ScopePopper, Side } from './utils' -import { ALIGN_OPTIONS, SIDE_OPTIONS, getSideAndAlignFromPlacement, isDefined, isNotNull, scopePopperProps, transformOrigin } from './utils' -import type { PopperProvideValue } from './popper' +import { ALIGN_OPTIONS, SIDE_OPTIONS, getSideAndAlignFromPlacement, isNotNull, scopePopperProps, transformOrigin } from './utils' import { createPopperProvider, usePopperInject } from './popper' const CONTENT_NAME = 'OkuPopperContent' type PopperContentContextValue = { - placedSide: Ref - arrow: Ref - arrowX?: Ref - arrowY?: Ref - shouldHideArrow: Ref - x?: Ref - y?: Ref -} & PopperProvideValue + placedSide: Ref + onArrowChange(arrow: HTMLSpanElement | null): void + arrowX?: Ref + arrowY?: Ref + shouldHideArrow: Ref +} export const [popperContentProvider, usePopperContentInject] = createPopperProvider(CONTENT_NAME) @@ -127,7 +124,7 @@ const PopperContent = defineComponent({ ...primitiveProps, }, emits: popperContentProps.emits, - setup(props, { attrs, slots }) { + setup(props, { attrs, slots, emit }) { const { side, sideOffset, @@ -147,7 +144,7 @@ const PopperContent = defineComponent({ const inject = usePopperInject(CONTENT_NAME, props.scopeOkuPopper) const content = ref(null) - const composedRefs = useComposedRefs(content, useForwardRef()) + const composedRefs = useComposedRefs(useForwardRef(), content) const arrow = ref(null) const arrowSize = useSize(arrow) @@ -158,21 +155,26 @@ const PopperContent = defineComponent({ const desiredPlacement = computed(() => (side.value + (align.value !== 'center' ? `-${align.value}` : '')) as Placement) const collisionPadding - = typeof collisionPaddingProp.value === 'number' + = computed(() => typeof collisionPaddingProp.value === 'number' ? collisionPaddingProp.value as Padding - : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp.value } as Padding + : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp.value } as Padding) + + const boundary = computed(() => Array.isArray(collisionBoundary.value) + ? collisionBoundary.value + : [collisionBoundary.value]) - const boundary = Array.isArray(collisionBoundary.value) ? collisionBoundary.value : [collisionBoundary.value] - const hasExplicitBoundaries = boundary.length > 0 + const hasExplicitBoundaries = computed(() => boundary.value.length > 0) - const detectOverflowOptions = { - padding: collisionPadding, - boundary: boundary.filter(isNotNull), - // with `strategy: 'fixed'`, this is the only way to get it to respect boundaries - altBoundary: hasExplicitBoundaries, - } as DetectOverflowOptions + const detectOverflowOptions = computed(() => { + return { + padding: collisionPadding.value, + boundary: boundary.value.filter(isNotNull), + // with `strategy: 'fixed'`, this is the only way to get it to respect boundaries + altBoundary: hasExplicitBoundaries.value, + } as DetectOverflowOptions + }) - const computedMiddleware = computedEager(() => { + const computedMiddleware = computed(() => { return [ offset({ mainAxis: sideOffset.value + arrowHeight.value, alignmentAxis: alignOffset.value }), @@ -180,13 +182,13 @@ const PopperContent = defineComponent({ mainAxis: true, crossAxis: false, limiter: sticky.value === 'partial' ? limitShift() : undefined, - ...detectOverflowOptions, + ...detectOverflowOptions.value, }), - avoidCollisions.value && flip({ ...detectOverflowOptions }), + avoidCollisions.value && flip({ ...detectOverflowOptions.value }), size({ - ...detectOverflowOptions, + ...detectOverflowOptions.value, apply: ({ elements, rects, availableWidth, availableHeight }) => { const { width: anchorWidth, height: anchorHeight } = rects.reference const contentStyle = elements.floating.style @@ -201,23 +203,27 @@ const PopperContent = defineComponent({ transformOrigin({ arrowWidth: arrowWidth.value, arrowHeight: arrowHeight.value }), - hideWhenDetached.value && hide({ strategy: 'referenceHidden', ...detectOverflowOptions }), - ].filter(isDefined) as Middleware[] + hideWhenDetached.value && hide({ strategy: 'referenceHidden', ...detectOverflowOptions.value }), + ] as Middleware[] }) const refElement = ref() - const { x, y, placement, isPositioned, middlewareData, update, strategy } = useFloating(inject.anchor, refElement, { + const { strategy, placement, isPositioned, middlewareData, update, floatingStyles } = useFloating( + computed(() => inject.anchor.value), + computed(() => refElement.value), + { // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues - strategy: 'fixed', - placement: desiredPlacement, - whileElementsMounted: (...args) => { - const cleanup = autoUpdate(...args, { - animationFrame: updatePositionStrategy.value === 'optimized', - }) - return cleanup - }, - middleware: computedMiddleware, - }) + strategy: 'fixed', + placement: desiredPlacement, + whileElementsMounted: (...args) => { + const cleanup = autoUpdate(...args, { + animationFrame: updatePositionStrategy.value === 'always', + }) + return cleanup + }, + middleware: computedMiddleware, + transform: false, + }) const placedSide = computed( () => getSideAndAlignFromPlacement(placement.value)[0], @@ -228,25 +234,11 @@ const PopperContent = defineComponent({ watchEffect(() => { if (isPositioned.value) - props.onPlaced?.() - }) - - onMounted(() => { - update() - }) - - const handlePlaced = useCallbackRef(props.onPlaced) - - watch([isPositioned, handlePlaced], () => { - if (isPositioned.value) - handlePlaced.value() + emit('placed') }) - const floatingTop = computed(() => `${y.value ?? 0}px`) - const floatingLeft = computed(() => `${x.value ?? 0}px`) - - const arrowX = computed(() => `${middlewareData.value.arrow?.x || 0}px`) - const arrowY = computed(() => `${middlewareData.value.arrow?.y || 0}px`) + const arrowX = computed(() => middlewareData.value.arrow?.x || 0) + const arrowY = computed(() => middlewareData.value.arrow?.y || 0) const cannotCenterArrow = computed(() => middlewareData.value.arrow?.centerOffset !== 0) const contentZIndex = ref() @@ -257,16 +249,22 @@ const PopperContent = defineComponent({ }) popperContentProvider({ - arrowX, - arrowY, scope: props.scopeOkuPopper, - shouldHideArrow: cannotCenterArrow, - onAnchorChange(anchor: HTMLElement | null) { + placedSide, + onArrowChange(anchor: HTMLElement | null) { arrow.value = anchor }, - arrow, - placedSide, - anchor: inject.anchor, + arrowX, + arrowY, + shouldHideArrow: cannotCenterArrow, + }) + + watch([inject.anchor, refElement], () => { + update() + }) + + onMounted(() => { + update() }) const originalReturn = () => @@ -275,9 +273,9 @@ const PopperContent = defineComponent({ 'ref': refElement, 'data-oku-popper-content-wrapper': '', 'style': { - 'top': floatingTop.value, - 'left': floatingLeft.value, + ...floatingStyles.value, 'position': strategy.value, + 'transform': isPositioned ? floatingStyles.value.transform : 'translate(0, -200%)', // keep off the page when measuring 'min-width': 'max-content', 'zIndex': contentZIndex.value, ['--oku-popper-transform-origin' as any]: [ @@ -303,10 +301,7 @@ const PopperContent = defineComponent({ // hide the content if using the hide middleware and should be hidden opacity: middlewareData.value.hide?.referenceHidden ? 0 : undefined, } as StyleValue, - }, - { - default: () => slots.default?.(), - }, + }, slots, ), ], ) diff --git a/packages/components/popper/src/stories/PopperDemo.vue b/packages/components/popper/src/stories/PopperDemo.vue index 3643e16b5..ee93dec17 100644 --- a/packages/components/popper/src/stories/PopperDemo.vue +++ b/packages/components/popper/src/stories/PopperDemo.vue @@ -47,8 +47,8 @@ function setPressed(value: boolean) {