Skip to content

Commit

Permalink
fix: resolve maximum recursive update exceeded in tests (oruga-ui#869)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek committed Mar 27, 2024
1 parent 9ce1971 commit 40d8b94
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ onMounted(() => {
/** Check if the scroll list inside the dropdown reached the top or it's end. */
function checkDropdownScroll(): void {
const dropdown = dropdownRef.value.$content;
const dropdown = unrefElement(dropdownRef.value.$content);
if (!dropdown) return;
const trashhold = dropdown.offsetTop;
const headerHeight = headerRef.value?.clientHeight || 0;
Expand Down
7 changes: 4 additions & 3 deletions packages/oruga/src/components/dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
watch,
onUnmounted,
type PropType,
type Component,
} from "vue";
import PositionWrapper from "../utils/PositionWrapper.vue";
Expand Down Expand Up @@ -307,7 +308,7 @@ const hoverable = computed(() => props.triggers.indexOf("hover") >= 0);
// --- Event Handler ---
const contentRef = ref<HTMLElement>();
const contentRef = ref<HTMLElement | Component>();
const triggerRef = ref<HTMLElement>();
const eventCleanups = [];
Expand Down Expand Up @@ -596,11 +597,11 @@ defineExpose({ $trigger: triggerRef, $content: contentRef });
</component>

<PositionWrapper
v-slot="{ setContent }"
v-model:position="autoPosition"
:teleport="teleport"
:class="[...rootClasses, ...positionWrapperClasses]"
:trigger="triggerRef"
:content="contentRef"
:disabled="!isActive"
default-position="bottom"
:disable-positioning="!isMobileModal">
Expand All @@ -618,7 +619,7 @@ defineExpose({ $trigger: triggerRef, $content: contentRef });
:is="menuTag"
v-show="(!disabled && (isActive || isHovered)) || inline"
:id="menuId"
ref="contentRef"
:ref="(el) => (contentRef = setContent(el))"
v-trap-focus="trapFocus"
:tabindex="menuTabindex"
:class="menuClasses"
Expand Down
5 changes: 3 additions & 2 deletions packages/oruga/src/components/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,17 @@ const contentClasses = defineClasses(
<template>
<div :class="rootClasses" data-oruga="tooltip">
<PositionWrapper
v-slot="{ setContent }"
v-model:position="autoPosition"
:teleport="teleport"
:class="rootClasses"
:trigger="triggerRef"
:content="contentRef"
default-position="top"
:disabled="!isActive">
<transition :name="animation">
<div
v-show="isActive || (always && !disabled)"
ref="contentRef"
:ref="(el) => (contentRef = setContent(el as HTMLElement))"
:class="contentClasses">
<span :class="arrowClasses"></span>

Expand All @@ -375,6 +375,7 @@ const contentClasses = defineClasses(
</div>
</transition>
</PositionWrapper>

<component
:is="triggerTag"
ref="triggerRef"
Expand Down
45 changes: 27 additions & 18 deletions packages/oruga/src/components/utils/PositionWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ const props = defineProps({
type: Object as PropType<HTMLElement | Component>,
default: undefined,
},
content: {
type: Object as PropType<HTMLElement | Component>,
default: undefined,
},
/**
* Position of the component relative to the trigger
* @values auto, top, bottom, left, right, top-right, top-left, bottom-left, bottom-right
Expand Down Expand Up @@ -74,16 +70,30 @@ const emits = defineEmits<{
(e: "update:position", value: string);
}>();
const to = computed(() =>
const teleportTo = computed(() =>
typeof props.teleport === "boolean" ? "body" : props.teleport,
);
const disabled = computed(() =>
const teleportDisabled = computed(() =>
typeof props.teleport === "boolean" || !props.teleport
? !props.teleport
: false,
);
const contentRef = ref<HTMLElement | Component>();
function setContent<T extends typeof contentRef.value>(el: T): typeof el {
contentRef.value = el;
nextTick(() => {
// update positioning
updatePositioning();
// add handler
addHandler();
});
return el;
}
// --- Dynamic Positioning Handling Feature ---
const initialPosition = props.position;
Expand All @@ -97,7 +107,7 @@ if (isClient && window.ResizeObserver) {
// on content or disable state change update event listener
watch(
[() => props.disabled, () => props.content],
() => props.disabled,
() => {
if (!props.disabled) addHandler();
else removeHandler();
Expand All @@ -108,8 +118,7 @@ watch(
// update positioning if props change
watch(
[
() => props.trigger,
() => props.content,
() => !!props.trigger,
() => props.disablePositioning,
() => props.disabled,
],
Expand All @@ -124,9 +133,9 @@ onBeforeUnmount(() => removeHandler());
/** add event listener */
function addHandler(): void {
if (isClient && !scrollingParent.value && props.content) {
if (isClient && !scrollingParent.value && contentRef.value) {
// get parent container
scrollingParent.value = getScrollingParent(unrefElement(props.content));
scrollingParent.value = getScrollingParent(unrefElement(contentRef));
// set event listener
if (
scrollingParent.value &&
Expand Down Expand Up @@ -173,7 +182,7 @@ function updatePositioning(): void {
// do not set content position if not teleport enabled
if (!props.teleport) return;
const content = unrefElement(props.content);
const content = unrefElement(contentRef);
const trigger = unrefElement(props.trigger);
// set content position
Expand Down Expand Up @@ -221,7 +230,7 @@ function updatePositioning(): void {
/** calculate best position if auto */
function getAutoPosition(): string {
let bestPosition = props.defaultPosition;
if (!props.content || !props.trigger) return bestPosition;
if (!props.trigger || !contentRef.value) return bestPosition;
if (!scrollingParent.value) return bestPosition;
// get viewport from container
Expand All @@ -232,7 +241,7 @@ function getAutoPosition(): string {
scrollingParent.value.clientHeight,
);
const contentRect = unrefElement(props.content).getBoundingClientRect();
const contentRect = unrefElement(contentRef).getBoundingClientRect();
const triggerRect = unrefElement(props.trigger).getBoundingClientRect();
// detect auto position
Expand Down Expand Up @@ -302,15 +311,15 @@ const anchors = (rect: DOMRect): Record<Position, Point> => ({
</script>

<template>
<Teleport :to="to" :disabled="disabled">
<template v-if="disabled">
<slot />
<Teleport :to="teleportTo" :disabled="teleportDisabled">
<template v-if="teleportDisabled">
<slot :set-content />
</template>
<template v-else>
<div
v-bind="$attrs"
:style="{ position: 'absolute', left: '0px', top: '0px' }">
<slot />
<slot :set-content />
</div>
</template>
</Teleport>
Expand Down
9 changes: 7 additions & 2 deletions packages/oruga/src/composables/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { onBeforeUnmount, onMounted, type MaybeRefOrGetter } from "vue";
import {
onBeforeUnmount,
onMounted,
type MaybeRefOrGetter,
type Component,
} from "vue";
import { isObject } from "@/utils/helpers";
import { unrefElement } from "./unrefElement";

type EventTarget = Element | Document | Window;
type EventTarget = Element | Document | Window | Component;
type EventListenerOptions = AddEventListenerOptions & {
/** Register event listener immediate or on mounted hook. */
immediate?: boolean;
Expand Down
9 changes: 6 additions & 3 deletions packages/oruga/src/composables/useParentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
onUnmounted,
provide,
ref,
type Component,
type ComputedRef,
type Ref,
} from "vue";
import { unrefElement } from "./unrefElement";

export type ProviderItem<T = unknown> = {
index: number;
Expand Down Expand Up @@ -41,7 +43,7 @@ type ProviderParentOptions<T = unknown> = {
* @param options additional options
*/
export function useProviderParent<ItemData = unknown, ParentData = unknown>(
rootRef?: Ref<HTMLElement>,
rootRef?: Ref<HTMLElement | Component>,
options?: ProviderParentOptions<ParentData>,
): {
childItems: Ref<ProviderItem<ItemData>[]>;
Expand Down Expand Up @@ -81,8 +83,9 @@ export function useProviderParent<ItemData = unknown, ParentData = unknown>(
const ids = childItems.value
.map((item) => `[data-id="${key}-${item.identifier}"]`)
.join(",");
const elements = rootRef.value.querySelectorAll(ids);
const sortedIds = Array.from(elements).map((el: any) =>
const parent = unrefElement(rootRef);
const children = parent.querySelectorAll(ids);
const sortedIds = Array.from(children).map((el: any) =>
el.getAttribute("data-id").replace(`${key}-`, ""),
);

Expand Down

0 comments on commit 40d8b94

Please sign in to comment.