diff --git a/.changeset/silly-rockets-fly.md b/.changeset/silly-rockets-fly.md new file mode 100644 index 0000000000..4c934890dd --- /dev/null +++ b/.changeset/silly-rockets-fly.md @@ -0,0 +1,13 @@ +--- +"@nextui-org/autocomplete": patch +"@nextui-org/date-picker": patch +"@nextui-org/dropdown": patch +"@nextui-org/modal": patch +"@nextui-org/popover": patch +"@nextui-org/select": patch +"@nextui-org/use-aria-modal-overlay": patch +"@nextui-org/use-aria-multiselect": patch +"@nextui-org/aria-utils": patch +--- + +rollback pr3467. rescheduled to v2.5.0. diff --git a/packages/components/autocomplete/package.json b/packages/components/autocomplete/package.json index aaf4d9b426..eef32019e8 100644 --- a/packages/components/autocomplete/package.json +++ b/packages/components/autocomplete/package.json @@ -41,6 +41,7 @@ "react-dom": ">=18" }, "dependencies": { + "@nextui-org/aria-utils": "workspace:*", "@nextui-org/button": "workspace:*", "@nextui-org/input": "workspace:*", "@nextui-org/listbox": "workspace:*", diff --git a/packages/components/autocomplete/src/use-autocomplete.ts b/packages/components/autocomplete/src/use-autocomplete.ts index bb96df6505..d1cae053f3 100644 --- a/packages/components/autocomplete/src/use-autocomplete.ts +++ b/packages/components/autocomplete/src/use-autocomplete.ts @@ -18,6 +18,7 @@ import {chain, mergeProps} from "@react-aria/utils"; import {ButtonProps} from "@nextui-org/button"; import {AsyncLoadable, PressEvent} from "@react-types/shared"; import {useComboBox} from "@react-aria/combobox"; +import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils"; interface Props extends Omit, keyof ComboBoxProps> { /** @@ -443,6 +444,9 @@ export function useAutocomplete(originalProps: UseAutocomplete ), }), }, + shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside + ? popoverProps.shouldCloseOnInteractOutside + : (element: Element) => ariaShouldCloseOnInteractOutside(element, inputWrapperRef, state), // when the popover is open, the focus should be on input instead of dialog // therefore, we skip dialog focus here disableDialogFocus: true, diff --git a/packages/components/date-picker/package.json b/packages/components/date-picker/package.json index a4ebcb8f4f..7ceb60b502 100644 --- a/packages/components/date-picker/package.json +++ b/packages/components/date-picker/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@internationalized/date": "^3.5.4", + "@nextui-org/aria-utils": "workspace:*", "@nextui-org/button": "workspace:*", "@nextui-org/calendar": "workspace:*", "@nextui-org/date-input": "workspace:*", diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index aeb0337f04..5f7ebd0245 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -15,6 +15,7 @@ import {useDatePickerState} from "@react-stately/datepicker"; import {AriaDatePickerProps, useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; +import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils"; import {useDatePickerBase} from "./use-date-picker-base"; @@ -192,6 +193,9 @@ export function useDatePicker({ ), }), }, + shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside + ? popoverProps.shouldCloseOnInteractOutside + : (element: Element) => ariaShouldCloseOnInteractOutside(element, popoverTriggerRef, state), }; }; diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 372bb6e323..6e45597178 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -21,6 +21,7 @@ import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepick import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; import {dateRangePicker, dateInput, cn} from "@nextui-org/theme"; +import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils"; import {useDatePickerBase} from "./use-date-picker-base"; interface Props @@ -214,6 +215,10 @@ export function useDateRangePicker({ props.className, ), }), + shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside + ? popoverProps.shouldCloseOnInteractOutside + : (element: Element) => + ariaShouldCloseOnInteractOutside(element, popoverTriggerRef, state), }, } as PopoverProps; }; diff --git a/packages/components/dropdown/package.json b/packages/components/dropdown/package.json index c31c63efb2..3e0dfccf11 100644 --- a/packages/components/dropdown/package.json +++ b/packages/components/dropdown/package.json @@ -41,6 +41,7 @@ "react-dom": ">=18" }, "dependencies": { + "@nextui-org/aria-utils": "workspace:*", "@nextui-org/menu": "workspace:*", "@nextui-org/popover": "workspace:*", "@nextui-org/react-utils": "workspace:*", diff --git a/packages/components/dropdown/src/use-dropdown.ts b/packages/components/dropdown/src/use-dropdown.ts index 0ff834f135..74294a4032 100644 --- a/packages/components/dropdown/src/use-dropdown.ts +++ b/packages/components/dropdown/src/use-dropdown.ts @@ -9,6 +9,7 @@ import {useMenuTrigger} from "@react-aria/menu"; import {dropdown} from "@nextui-org/theme"; import {clsx} from "@nextui-org/shared-utils"; import {ReactRef, mergeRefs} from "@nextui-org/react-utils"; +import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils"; import {useMemo, useRef} from "react"; import {mergeProps} from "@react-aria/utils"; import {MenuProps} from "@nextui-org/menu"; @@ -122,6 +123,9 @@ export function useDropdown(props: UseDropdownProps) { ...props.classNames, content: clsx(classNames, classNamesProp?.content, props.className), }, + shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside + ? popoverProps.shouldCloseOnInteractOutside + : (element: Element) => ariaShouldCloseOnInteractOutside(element, triggerRef, state), }; }; diff --git a/packages/components/modal/__tests__/modal.test.tsx b/packages/components/modal/__tests__/modal.test.tsx index 2221f981b8..70b5225690 100644 --- a/packages/components/modal/__tests__/modal.test.tsx +++ b/packages/components/modal/__tests__/modal.test.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import {act, render, fireEvent} from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../src"; @@ -109,38 +108,4 @@ describe("Modal", () => { fireEvent.keyDown(modal, {key: "Escape"}); expect(onClose).toHaveBeenCalledTimes(1); }); - - it("should only hide the top-most modal", async () => { - const onClose1 = jest.fn(); - const onClose2 = jest.fn(); - - render( - - - Modal header - Modal body - Modal footer - - , - ); - - const wrapper2 = render( - - - Modal header - Modal body - Modal footer - - , - ); - - await userEvent.click(document.body); - expect(onClose1).not.toHaveBeenCalled(); - expect(onClose2).toHaveBeenCalledTimes(1); - - wrapper2.unmount(); - - await userEvent.click(document.body); - expect(onClose1).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/components/modal/src/use-modal.ts b/packages/components/modal/src/use-modal.ts index ef05dcad14..66b6f7be63 100644 --- a/packages/components/modal/src/use-modal.ts +++ b/packages/components/modal/src/use-modal.ts @@ -169,6 +169,7 @@ export function useModal(originalProps: UseModalProps) { const getBackdropProps = useCallback( (props = {}) => ({ className: slots.backdrop({class: classNames?.backdrop}), + onClick: () => state.close(), ...underlayProps, ...props, }), diff --git a/packages/components/popover/package.json b/packages/components/popover/package.json index 7879ca258b..48b422e6e5 100644 --- a/packages/components/popover/package.json +++ b/packages/components/popover/package.json @@ -47,7 +47,6 @@ "@nextui-org/react-utils": "workspace:*", "@nextui-org/shared-utils": "workspace:*", "@nextui-org/use-aria-button": "workspace:*", - "@nextui-org/use-aria-overlay": "workspace:*", "@nextui-org/use-safe-layout-effect": "workspace:*", "@react-aria/dialog": "3.5.14", "@react-aria/focus": "3.17.1", diff --git a/packages/components/popover/src/use-aria-popover.ts b/packages/components/popover/src/use-aria-popover.ts index ebac5f83c4..e8d0873634 100644 --- a/packages/components/popover/src/use-aria-popover.ts +++ b/packages/components/popover/src/use-aria-popover.ts @@ -1,15 +1,20 @@ import {RefObject, useEffect} from "react"; import { + useOverlay, AriaPopoverProps, PopoverAria, useOverlayPosition, AriaOverlayProps, } from "@react-aria/overlays"; -import {OverlayPlacement, ariaHideOutside, toReactAriaPlacement} from "@nextui-org/aria-utils"; +import { + OverlayPlacement, + ariaHideOutside, + toReactAriaPlacement, + ariaShouldCloseOnInteractOutside, +} from "@nextui-org/aria-utils"; import {OverlayTriggerState} from "@react-stately/overlays"; import {mergeProps} from "@react-aria/utils"; import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; -import {useAriaOverlay} from "@nextui-org/use-aria-overlay"; export interface Props { /** @@ -66,16 +71,16 @@ export function useReactAriaPopover( const isNonModal = isNonModalProp ?? true; - const {overlayProps, underlayProps} = useAriaOverlay( + const {overlayProps, underlayProps} = useOverlay( { isOpen: state.isOpen, onClose: state.close, shouldCloseOnBlur, isDismissable, isKeyboardDismissDisabled, - shouldCloseOnInteractOutside: - shouldCloseOnInteractOutside || ((el) => !triggerRef.current?.contains(el)), - disableOutsideEvents: !isNonModal, + shouldCloseOnInteractOutside: shouldCloseOnInteractOutside + ? shouldCloseOnInteractOutside + : (element: Element) => ariaShouldCloseOnInteractOutside(element, triggerRef, state), }, popoverRef, ); diff --git a/packages/components/popover/src/use-popover.ts b/packages/components/popover/src/use-popover.ts index b1cf10c0ae..8f91399e42 100644 --- a/packages/components/popover/src/use-popover.ts +++ b/packages/components/popover/src/use-popover.ts @@ -2,11 +2,11 @@ import type {PopoverVariantProps, SlotsToClasses, PopoverSlots} from "@nextui-or import type {HTMLMotionProps} from "framer-motion"; import type {PressEvent} from "@react-types/shared"; -import {RefObject, Ref} from "react"; +import {RefObject, Ref, useEffect} from "react"; import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; import {OverlayTriggerState, useOverlayTriggerState} from "@react-stately/overlays"; import {useFocusRing} from "@react-aria/focus"; -import {useOverlayTrigger} from "@react-aria/overlays"; +import {ariaHideOutside, useOverlayTrigger} from "@react-aria/overlays"; import {OverlayTriggerProps} from "@react-types/overlays"; import { HTMLNextUIProps, @@ -298,6 +298,12 @@ export function usePopover(originalProps: UsePopoverProps) { [slots, state.isOpen, classNames, underlayProps], ); + useEffect(() => { + if (state.isOpen && domRef?.current) { + return ariaHideOutside([domRef?.current]); + } + }, [state.isOpen, domRef]); + return { state, Component, diff --git a/packages/components/select/package.json b/packages/components/select/package.json index adddba9fe2..abb068f0dc 100644 --- a/packages/components/select/package.json +++ b/packages/components/select/package.json @@ -41,6 +41,7 @@ "react-dom": ">=18" }, "dependencies": { + "@nextui-org/aria-utils": "workspace:*", "@nextui-org/listbox": "workspace:*", "@nextui-org/popover": "workspace:*", "@nextui-org/react-utils": "workspace:*", diff --git a/packages/components/select/src/use-select.ts b/packages/components/select/src/use-select.ts index 3988c43f2f..0c8d31733b 100644 --- a/packages/components/select/src/use-select.ts +++ b/packages/components/select/src/use-select.ts @@ -28,6 +28,7 @@ import { } from "@nextui-org/use-aria-multiselect"; import {SpinnerProps} from "@nextui-org/spinner"; import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; +import {ariaShouldCloseOnInteractOutside} from "@nextui-org/aria-utils"; import {CollectionChildren} from "@react-types/shared"; export type SelectedItemProps = { @@ -529,6 +530,9 @@ export function useSelect(originalProps: UseSelectProps) { ? // forces the popover to update its position when the selected items change state.selectedItems.length * 0.00000001 + (slotsProps.popoverProps?.offset || 0) : slotsProps.popoverProps?.offset, + shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside + ? popoverProps.shouldCloseOnInteractOutside + : (element: Element) => ariaShouldCloseOnInteractOutside(element, domRef, state), } as PopoverProps; }, [ diff --git a/packages/hooks/use-aria-modal-overlay/package.json b/packages/hooks/use-aria-modal-overlay/package.json index a98d0541dc..5456230117 100644 --- a/packages/hooks/use-aria-modal-overlay/package.json +++ b/packages/hooks/use-aria-modal-overlay/package.json @@ -34,7 +34,6 @@ "postpack": "clean-package restore" }, "dependencies": { - "@nextui-org/use-aria-overlay": "workspace:*", "@react-aria/overlays": "3.22.1", "@react-aria/utils": "3.24.1", "@react-stately/overlays": "3.6.7", diff --git a/packages/hooks/use-aria-modal-overlay/src/index.ts b/packages/hooks/use-aria-modal-overlay/src/index.ts index 3d65570a64..87d3b4cae8 100644 --- a/packages/hooks/use-aria-modal-overlay/src/index.ts +++ b/packages/hooks/use-aria-modal-overlay/src/index.ts @@ -1,8 +1,8 @@ -import {useAriaOverlay} from "@nextui-org/use-aria-overlay"; import { ariaHideOutside, AriaModalOverlayProps, ModalOverlayAria, + useOverlay, usePreventScroll, useOverlayFocusContain, } from "@react-aria/overlays"; @@ -21,7 +21,7 @@ export function useAriaModalOverlay( state: OverlayTriggerState, ref: RefObject, ): ModalOverlayAria { - let {overlayProps, underlayProps} = useAriaOverlay( + let {overlayProps, underlayProps} = useOverlay( { ...props, isOpen: state.isOpen, diff --git a/packages/hooks/use-aria-multiselect/src/use-multiselect.ts b/packages/hooks/use-aria-multiselect/src/use-multiselect.ts index 41373d4945..ffa17dce10 100644 --- a/packages/hooks/use-aria-multiselect/src/use-multiselect.ts +++ b/packages/hooks/use-aria-multiselect/src/use-multiselect.ts @@ -112,12 +112,6 @@ export function useMultiSelect( typeSelectProps.onKeyDown = typeSelectProps.onKeyDownCapture; delete typeSelectProps.onKeyDownCapture; - menuTriggerProps.onPressStart = (e) => { - if (e.pointerType !== "touch" && e.pointerType !== "keyboard" && !isDisabled) { - state.toggle(e.pointerType === "virtual" ? "first" : null); - } - }; - const domProps = filterDOMProps(props, {labelable: true}); const triggerProps = mergeProps(typeSelectProps, menuTriggerProps, fieldProps); diff --git a/packages/hooks/use-aria-overlay/CHANGELOG.md b/packages/hooks/use-aria-overlay/CHANGELOG.md deleted file mode 100644 index 3e8d2127dc..0000000000 --- a/packages/hooks/use-aria-overlay/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @nextui-org/use-aria-overlay - -## 2.0.1 - -### Patch Changes - -- [#3467](https://github.com/nextui-org/nextui/pull/3467) [`123b7fbc9`](https://github.com/nextui-org/nextui/commit/123b7fbc9fb51613d7568572a00982ad230d02ae) Thanks [@chirokas](https://github.com/chirokas)! - Refactor overlays to reduce its complexity, while improving stability. diff --git a/packages/hooks/use-aria-overlay/README.md b/packages/hooks/use-aria-overlay/README.md deleted file mode 100644 index 4d2fd3045e..0000000000 --- a/packages/hooks/use-aria-overlay/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# @nextui-org/use-aria-overlay - -A Quick description of the component - -> This is an internal utility, not intended for public usage. - -## Installation - -```sh -yarn add @nextui-org/use-aria-overlay -# or -npm i @nextui-org/use-aria-overlay -``` - -## Contribution - -Yes please! See the -[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md) -for details. - -## License - -This project is licensed under the terms of the -[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE). diff --git a/packages/hooks/use-aria-overlay/package.json b/packages/hooks/use-aria-overlay/package.json deleted file mode 100644 index ece097bfbe..0000000000 --- a/packages/hooks/use-aria-overlay/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@nextui-org/use-aria-overlay", - "version": "2.0.1", - "description": "A custom implementation of react aria overlay", - "keywords": [ - "use-aria-overlay" - ], - "author": "Junior Garcia ", - "homepage": "https://nextui.org", - "license": "MIT", - "main": "src/index.ts", - "sideEffects": false, - "files": [ - "dist" - ], - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/nextui-org/nextui.git", - "directory": "packages/hooks/use-aria-overlay" - }, - "bugs": { - "url": "https://github.com/nextui-org/nextui/issues" - }, - "scripts": { - "build": "tsup src --dts", - "build:fast": "tsup src", - "dev": "pnpm build:fast --watch", - "clean": "rimraf dist .turbo", - "typecheck": "tsc --noEmit", - "prepack": "clean-package", - "postpack": "clean-package restore" - }, - "dependencies": { - "@react-aria/focus": "3.17.1", - "@react-aria/interactions": "3.21.3", - "@react-aria/overlays": "3.22.1", - "@react-types/shared": "3.23.1" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "devDependencies": { - "clean-package": "2.2.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "clean-package": "../../../clean-package.config.json", - "tsup": { - "clean": true, - "target": "es2019", - "format": [ - "cjs", - "esm" - ] - } -} diff --git a/packages/hooks/use-aria-overlay/src/index.ts b/packages/hooks/use-aria-overlay/src/index.ts deleted file mode 100644 index 403acd11ab..0000000000 --- a/packages/hooks/use-aria-overlay/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type {AriaOverlayProps, OverlayAria} from "@react-aria/overlays"; -import type {RefObject} from "react"; -import type React from "react"; - -import {isElementInChildOfActiveScope} from "@react-aria/focus"; -import {useFocusWithin, useInteractOutside} from "@react-aria/interactions"; -import {useEffect} from "react"; - -export interface UseAriaOverlayProps extends AriaOverlayProps { - /** - * When `true`, `click/focus` interactions will be disabled on elements outside - * the `Overlay`. Users need to click twice on outside elements to interact with them: - * once to close the overlay, and again to trigger the element. - * - * @default true - */ - disableOutsideEvents?: boolean; -} - -const visibleOverlays: RefObject[] = []; - -/** - * Provides the behavior for overlays such as dialogs, popovers, and menus. - * Hides the overlay when the user interacts outside it, when the Escape key is pressed, - * or optionally, on blur. Only the top-most overlay will close at once. - */ -export function useAriaOverlay(props: UseAriaOverlayProps, ref: RefObject): OverlayAria { - const { - disableOutsideEvents = true, - isDismissable = false, - isKeyboardDismissDisabled = false, - isOpen, - onClose, - shouldCloseOnBlur, - shouldCloseOnInteractOutside, - } = props; - - // Add the overlay ref to the stack of visible overlays on mount, and remove on unmount. - useEffect(() => { - if (isOpen) { - visibleOverlays.push(ref); - } - - return () => { - const index = visibleOverlays.indexOf(ref); - - if (index >= 0) { - visibleOverlays.splice(index, 1); - } - }; - }, [isOpen, ref]); - - // Only hide the overlay when it is the topmost visible overlay in the stack - const onHide = () => { - if (visibleOverlays[visibleOverlays.length - 1] === ref && onClose) { - onClose(); - } - }; - - const onInteractOutsideStart = (e: PointerEvent) => { - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { - if (visibleOverlays[visibleOverlays.length - 1] === ref) { - if (disableOutsideEvents) { - e.stopPropagation(); - e.preventDefault(); - } - } - - // For consistency with React Aria, toggle the combobox/menu on mouse down, but touch up. - if (e.pointerType !== "touch") { - onHide(); - } - } - }; - - const onInteractOutside = (e: PointerEvent) => { - if (e.pointerType !== "touch") { - return; - } - - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { - if (visibleOverlays[visibleOverlays.length - 1] === ref) { - if (disableOutsideEvents) { - e.stopPropagation(); - e.preventDefault(); - } - } - - onHide(); - } - }; - - // Handle the escape key - const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape" && !isKeyboardDismissDisabled && !e.nativeEvent.isComposing) { - e.stopPropagation(); - e.preventDefault(); - onHide(); - } - }; - - // Handle clicking outside the overlay to close it - useInteractOutside({ - isDisabled: !(isDismissable && isOpen), - onInteractOutside, - onInteractOutsideStart, - ref, - }); - - const {focusWithinProps} = useFocusWithin({ - isDisabled: !shouldCloseOnBlur, - onBlurWithin: (e) => { - // Do not close if relatedTarget is null, which means focus is lost to the body. - // That can happen when switching tabs, or due to a VoiceOver/Chrome bug with Control+Option+Arrow navigation. - // Clicking on the body to close the overlay should already be handled by useInteractOutside. - // https://github.com/adobe/react-spectrum/issues/4130 - // https://github.com/adobe/react-spectrum/issues/4922 - // - // If focus is moving into a child focus scope (e.g. menu inside a dialog), - // do not close the outer overlay. At this point, the active scope should - // still be the outer overlay, since blur events run before focus. - if (!e.relatedTarget || isElementInChildOfActiveScope(e.relatedTarget)) { - return; - } - - if ( - !shouldCloseOnInteractOutside || - shouldCloseOnInteractOutside(e.relatedTarget as Element) - ) { - onHide(); - } - }, - }); - - const onPointerDownUnderlay = (e: React.PointerEvent) => { - // fixes a firefox issue that starts text selection https://bugzilla.mozilla.org/show_bug.cgi?id=1675846 - if (e.target === e.currentTarget) { - e.preventDefault(); - } - }; - - return { - overlayProps: { - onKeyDown, - ...focusWithinProps, - }, - underlayProps: { - onPointerDown: onPointerDownUnderlay, - }, - }; -} diff --git a/packages/hooks/use-aria-overlay/tsconfig.json b/packages/hooks/use-aria-overlay/tsconfig.json deleted file mode 100644 index 46e3b466c2..0000000000 --- a/packages/hooks/use-aria-overlay/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "include": ["src", "index.ts"] -} diff --git a/packages/utilities/aria-utils/src/index.ts b/packages/utilities/aria-utils/src/index.ts index 8e70c1b183..6b8d27ad3f 100644 --- a/packages/utilities/aria-utils/src/index.ts +++ b/packages/utilities/aria-utils/src/index.ts @@ -7,6 +7,7 @@ export {isNonContiguousSelectionModifier, isCtrlKeyPressed} from "./utils"; export { ariaHideOutside, + ariaShouldCloseOnInteractOutside, getTransformOrigins, toReactAriaPlacement, toOverlayPlacement, diff --git a/packages/utilities/aria-utils/src/overlays/ariaShouldCloseOnInteractOutside.ts b/packages/utilities/aria-utils/src/overlays/ariaShouldCloseOnInteractOutside.ts new file mode 100644 index 0000000000..24fedac9ae --- /dev/null +++ b/packages/utilities/aria-utils/src/overlays/ariaShouldCloseOnInteractOutside.ts @@ -0,0 +1,45 @@ +import {RefObject} from "react"; + +/** + * Used to handle the outside interaction for popover-based components + * e.g. dropdown, datepicker, date-range-picker, popover, select, autocomplete etc + * @param element - the element outside of the popover ref, originally from `shouldCloseOnInteractOutside` + * @param triggerRef - The trigger ref object + * @param state - The state from the popover component + * @returns - a boolean value which is same as shouldCloseOnInteractOutside + */ +export const ariaShouldCloseOnInteractOutside = ( + element: Element, + triggerRef: RefObject, + state: any, +) => { + const trigger = triggerRef?.current; + + if (!trigger || !trigger.contains(element)) { + // if there is focus scope block, there will be a pair of span[data-focus-scope-start] and span[data-focus-scope-end] + // the element with focus trap resides inbetween these two blocks + // we push all the elements in focus scope to `focusScopeElements` + const startElements = document.querySelectorAll("body > span[data-focus-scope-start]"); + let focusScopeElements: Element[] = []; + + startElements.forEach((startElement) => { + focusScopeElements.push(startElement.nextElementSibling!); + }); + + // if there is just one focusScopeElement, we close the state + // e.g. open a popover A -> click popover B + // then popover A should be closed and popover B should be open + // TODO: handle cases like modal > popover A -> click modal > popover B + // we should close the popover when it is the last opened + // however, currently ariaShouldCloseOnInteractOutside is called recursively + // and we need a way to check if there is something closed before that (i.e. nested elements) + // if so, popover shouldn't be closed in this case + if (focusScopeElements.length === 1) { + state.close(); + + return false; + } + } + + return !trigger || !trigger.contains(element); +}; diff --git a/packages/utilities/aria-utils/src/overlays/index.ts b/packages/utilities/aria-utils/src/overlays/index.ts index ccf839f2d9..6999e8b90c 100644 --- a/packages/utilities/aria-utils/src/overlays/index.ts +++ b/packages/utilities/aria-utils/src/overlays/index.ts @@ -9,3 +9,4 @@ export { } from "./utils"; export {ariaHideOutside} from "./ariaHideOutside"; +export {ariaShouldCloseOnInteractOutside} from "./ariaShouldCloseOnInteractOutside"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb066eb48c..b51699d2f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -689,6 +689,9 @@ importers: packages/components/autocomplete: dependencies: + '@nextui-org/aria-utils': + specifier: workspace:* + version: link:../../utilities/aria-utils '@nextui-org/button': specifier: workspace:* version: link:../button @@ -1327,6 +1330,9 @@ importers: '@internationalized/date': specifier: ^3.5.4 version: 3.5.4 + '@nextui-org/aria-utils': + specifier: workspace:* + version: link:../../utilities/aria-utils '@nextui-org/button': specifier: workspace:* version: link:../button @@ -1425,6 +1431,9 @@ importers: packages/components/dropdown: dependencies: + '@nextui-org/aria-utils': + specifier: workspace:* + version: link:../../utilities/aria-utils '@nextui-org/menu': specifier: workspace:* version: link:../menu @@ -2008,9 +2017,6 @@ importers: '@nextui-org/use-aria-button': specifier: workspace:* version: link:../../hooks/use-aria-button - '@nextui-org/use-aria-overlay': - specifier: workspace:* - version: link:../../hooks/use-aria-overlay '@nextui-org/use-safe-layout-effect': specifier: workspace:* version: link:../../hooks/use-safe-layout-effect @@ -2226,6 +2232,9 @@ importers: packages/components/select: dependencies: + '@nextui-org/aria-utils': + specifier: workspace:* + version: link:../../utilities/aria-utils '@nextui-org/listbox': specifier: workspace:* version: link:../listbox @@ -3251,9 +3260,6 @@ importers: packages/hooks/use-aria-modal-overlay: dependencies: - '@nextui-org/use-aria-overlay': - specifier: workspace:* - version: link:../use-aria-overlay '@react-aria/overlays': specifier: 3.22.1 version: 3.22.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -3332,31 +3338,6 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) - packages/hooks/use-aria-overlay: - dependencies: - '@react-aria/focus': - specifier: 3.17.1 - version: 3.17.1(react@18.2.0) - '@react-aria/interactions': - specifier: 3.21.3 - version: 3.21.3(react@18.2.0) - '@react-aria/overlays': - specifier: 3.22.1 - version: 3.22.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@react-types/shared': - specifier: 3.23.1 - version: 3.23.1(react@18.2.0) - devDependencies: - clean-package: - specifier: 2.2.0 - version: 2.2.0 - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - packages/hooks/use-aria-toggle-button: dependencies: '@nextui-org/use-aria-button': @@ -18120,15 +18101,13 @@ snapshots: transitivePeerDependencies: - '@parcel/core' - '@parcel/cache@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9)': + '@parcel/cache@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))': dependencies: '@parcel/core': 2.12.0(@swc/helpers@0.5.9) '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9) '@parcel/logger': 2.12.0 '@parcel/utils': 2.12.0 lmdb: 2.8.5 - transitivePeerDependencies: - - '@swc/helpers' '@parcel/codeframe@2.12.0': dependencies: @@ -18188,7 +18167,7 @@ snapshots: '@parcel/core@2.12.0(@swc/helpers@0.5.9)': dependencies: '@mischnic/json-sourcemap': 0.1.1 - '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9) + '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9)) '@parcel/diagnostic': 2.12.0 '@parcel/events': 2.12.0 '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9) @@ -18603,7 +18582,7 @@ snapshots: '@parcel/types@2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9)': dependencies: - '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9) + '@parcel/cache': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9)) '@parcel/diagnostic': 2.12.0 '@parcel/fs': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9) '@parcel/package-manager': 2.12.0(@parcel/core@2.12.0(@swc/helpers@0.5.9))(@swc/helpers@0.5.9)