🏃 feat: Keep Modals Open on Escape in Dropdown Menus (#10975)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Publish `@librechat/client` to NPM / build-and-publish (push) Waiting to run
Publish `librechat-data-provider` to NPM / build (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Publish `@librechat/data-schemas` to NPM / build-and-publish (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* fix: filter dropdown now closable with escape, doesn't close whole modal

* refactor: simplify escapekeydown handler logic for tooltips and dropdown menus

* refactor: more specific conditions for preventDefault
This commit is contained in:
Dustin Healy 2025-12-16 06:15:43 -08:00 committed by GitHub
parent 23279b4b14
commit d8b788aecc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -78,29 +78,39 @@ const DialogContent = React.forwardRef<
},
ref,
) => {
/* Handle Escape key to prevent closing dialog if a tooltip is open
const contentRef = React.useRef<HTMLDivElement>(null);
React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement, []);
/* Handle Escape key to prevent closing dialog if a tooltip or dropdown is open
(this is a workaround in order to achieve WCAG compliance which requires
that our tooltips be dismissable with Escape key) */
const handleEscapeKeyDown = React.useCallback(
(event: KeyboardEvent) => {
const tooltips = document.querySelectorAll('.tooltip');
if (!contentRef.current) {
propsOnEscapeKeyDown?.(event);
return;
}
for (const tooltip of Array.from(tooltips)) {
const computedStyle = window.getComputedStyle(tooltip);
const opacity = parseFloat(computedStyle.opacity);
const tooltips = contentRef.current.querySelectorAll('.tooltip');
const dropdownMenus = contentRef.current.querySelectorAll('[role="menu"]');
if (
tooltip.parentElement &&
computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
opacity > 0
) {
for (const tooltip of tooltips) {
const style = window.getComputedStyle(tooltip);
if (style.display !== 'none') {
event.preventDefault();
return;
}
}
for (const dropdownMenu of dropdownMenus) {
const style = window.getComputedStyle(dropdownMenu);
if (style.display !== 'none') {
event.preventDefault();
return;
}
}
// Call the original handler if it exists
propsOnEscapeKeyDown?.(event);
},
[propsOnEscapeKeyDown],
@ -110,7 +120,7 @@ const DialogContent = React.forwardRef<
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
ref={ref}
ref={contentRef}
onEscapeKeyDown={handleEscapeKeyDown}
className={cn(
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid max-h-[90vh] w-full translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-2xl bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',