mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
fix(editor): Close card dropdowns when workflows list is scrolled (#21630)
This commit is contained in:
parent
81dcf54a0d
commit
c6434aca03
|
|
@ -2,6 +2,7 @@
|
|||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useParentScroll } from '../../composables/useParentScroll';
|
||||
import type { IUser, UserAction } from '../../types';
|
||||
import type { IconOrientation, IconSize } from '../../types/icon';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
|
@ -22,24 +23,29 @@ interface ActionToggleProps<UserType extends IUser, Actions extends Array<UserAc
|
|||
disabled?: boolean;
|
||||
popperClass?: string;
|
||||
trigger?: 'click' | 'hover';
|
||||
closeOnParentScroll?: boolean;
|
||||
}
|
||||
|
||||
type ActionValue = Actions[number]['value'];
|
||||
|
||||
defineOptions({ name: 'N8nActionToggle' });
|
||||
withDefaults(defineProps<ActionToggleProps<UserType, Array<UserAction<UserType>>>>(), {
|
||||
actions: () => [],
|
||||
placement: 'bottom',
|
||||
size: 'medium',
|
||||
theme: 'default',
|
||||
iconSize: 'medium',
|
||||
iconOrientation: 'vertical',
|
||||
loading: false,
|
||||
loadingRowCount: 3,
|
||||
disabled: false,
|
||||
popperClass: '',
|
||||
trigger: 'click',
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<ActionToggleProps<UserType, Array<UserAction<UserType>>>>(),
|
||||
{
|
||||
actions: () => [],
|
||||
placement: 'bottom',
|
||||
size: 'medium',
|
||||
theme: 'default',
|
||||
iconSize: 'medium',
|
||||
iconOrientation: 'vertical',
|
||||
loading: false,
|
||||
loadingRowCount: 3,
|
||||
disabled: false,
|
||||
popperClass: '',
|
||||
trigger: 'click',
|
||||
closeOnParentScroll: true,
|
||||
},
|
||||
);
|
||||
|
||||
const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null);
|
||||
|
||||
|
|
@ -49,8 +55,26 @@ const emit = defineEmits<{
|
|||
'item-mouseup': [action: UserAction<UserType>];
|
||||
}>();
|
||||
|
||||
// Close dropdown when parent scrolls
|
||||
const { attachScrollListeners, detachScrollListeners } = useParentScroll(actionToggleRef, () => {
|
||||
if (props.closeOnParentScroll) {
|
||||
actionToggleRef.value?.handleClose();
|
||||
}
|
||||
});
|
||||
|
||||
const onCommand = (value: string) => emit('action', value);
|
||||
const onVisibleChange = (value: boolean) => emit('visible-change', value);
|
||||
const onVisibleChange = (value: boolean) => {
|
||||
emit('visible-change', value);
|
||||
|
||||
if (props.closeOnParentScroll) {
|
||||
if (value) {
|
||||
attachScrollListeners();
|
||||
} else {
|
||||
detachScrollListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openActionToggle = (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
actionToggleRef.value?.handleOpen();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import { ref, onBeforeUnmount, nextTick, type Ref } from 'vue';
|
||||
|
||||
type ScrollListener = {
|
||||
element: Element;
|
||||
handler: () => void;
|
||||
};
|
||||
|
||||
const isHTMLElement = (element: unknown): element is HTMLElement => {
|
||||
return element !== undefined && element instanceof HTMLElement;
|
||||
};
|
||||
|
||||
const isScrollable = (element: Element): boolean => {
|
||||
const computedStyle = window.getComputedStyle(element);
|
||||
const overflowY = computedStyle.overflowY;
|
||||
const overflowX = computedStyle.overflowX;
|
||||
|
||||
// Check if element has scrollable overflow
|
||||
const hasScrollableOverflow =
|
||||
overflowY === 'auto' ||
|
||||
overflowY === 'scroll' ||
|
||||
overflowX === 'auto' ||
|
||||
overflowX === 'scroll';
|
||||
|
||||
if (!hasScrollableOverflow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if element actually has scrollable content
|
||||
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
||||
};
|
||||
|
||||
const findFirstScrollableParent = (element: HTMLElement): Element | null => {
|
||||
let parent = element.parentElement;
|
||||
|
||||
while (parent && parent !== document.body) {
|
||||
if (isScrollable(parent)) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
if (document.body && isScrollable(document.body)) {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
if (document.documentElement && isScrollable(document.documentElement)) {
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects scroll events on parent elements and triggers a callback
|
||||
*
|
||||
* @param elementRef - Ref to the element or component instance with $el
|
||||
* @param onScroll - Callback function to execute when scroll is detected
|
||||
*/
|
||||
export function useParentScroll(
|
||||
elementRef: Ref<{ $el?: unknown } | HTMLElement | null | undefined>,
|
||||
onScroll: () => void,
|
||||
) {
|
||||
const scrollListeners = ref<ScrollListener[]>([]);
|
||||
|
||||
const detachScrollListeners = () => {
|
||||
scrollListeners.value.forEach(({ element, handler }) => {
|
||||
element.removeEventListener('scroll', handler, { capture: true });
|
||||
});
|
||||
scrollListeners.value = [];
|
||||
};
|
||||
|
||||
const attachScrollListeners = () => {
|
||||
// Ensure DOM is ready but don't block on it
|
||||
void nextTick(() => {
|
||||
const element = elementRef.value;
|
||||
const dropdownElement = element && '$el' in element ? element.$el : element;
|
||||
|
||||
if (!isHTMLElement(dropdownElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
detachScrollListeners();
|
||||
|
||||
const scrollableParent = findFirstScrollableParent(dropdownElement);
|
||||
|
||||
if (scrollableParent) {
|
||||
const handler = () => onScroll();
|
||||
scrollableParent.addEventListener('scroll', handler, { passive: true, capture: true });
|
||||
scrollListeners.value.push({ element: scrollableParent, handler });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
detachScrollListeners();
|
||||
});
|
||||
|
||||
return {
|
||||
attachScrollListeners,
|
||||
detachScrollListeners,
|
||||
};
|
||||
}
|
||||
|
|
@ -22,10 +22,11 @@ import {
|
|||
N8nIcon,
|
||||
N8nText,
|
||||
} from '@n8n/design-system';
|
||||
|
||||
type Props = {
|
||||
data: FolderResource;
|
||||
personalProject: Project | null;
|
||||
actions: Array<UserAction<IUser>>;
|
||||
actions?: Array<UserAction<IUser>>;
|
||||
readOnly?: boolean;
|
||||
showOwnershipBadge?: boolean;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user