diff --git a/.changeset/tender-nails-suffer.md b/.changeset/tender-nails-suffer.md new file mode 100644 index 0000000000..3d8097b70a --- /dev/null +++ b/.changeset/tender-nails-suffer.md @@ -0,0 +1,90 @@ +--- +"@heroui/use-intersection-observer": patch +"@heroui/use-data-scroll-overflow": patch +"@heroui/use-aria-accordion-item": patch +"@heroui/use-aria-modal-overlay": patch +"@heroui/use-safe-layout-effect": patch +"@heroui/use-aria-multiselect": patch +"@heroui/use-infinite-scroll": patch +"@heroui/use-scroll-position": patch +"@heroui/react-rsc-utils": patch +"@heroui/scroll-shadow": patch +"@heroui/use-aria-accordion": patch +"@heroui/autocomplete": patch +"@heroui/number-input": patch +"@heroui/use-update-effect": patch +"@heroui/dom-animation": patch +"@heroui/stories-utils": patch +"@heroui/breadcrumbs": patch +"@heroui/date-picker": patch +"@heroui/use-aria-overlay": patch +"@heroui/use-callback-ref": patch +"@heroui/framer-utils": patch +"@heroui/shared-icons": patch +"@heroui/shared-utils": patch +"@heroui/date-input": patch +"@heroui/pagination": patch +"@heroui/use-aria-button": patch +"@heroui/react-utils": patch +"@heroui/accordion": patch +"@heroui/input-otp": patch +"@heroui/use-disclosure": patch +"@heroui/use-is-mounted": patch +"@heroui/use-pagination": patch +"@heroui/use-real-shape": patch +"@heroui/aria-utils": patch +"@heroui/test-utils": patch +"@heroui/calendar": patch +"@heroui/checkbox": patch +"@heroui/dropdown": patch +"@heroui/progress": patch +"@heroui/skeleton": patch +"@heroui/use-aria-link": patch +"@heroui/use-clipboard": patch +"@heroui/use-draggable": patch +"@heroui/use-is-mobile": patch +"@heroui/use-ref-state": patch +"@heroui/divider": patch +"@heroui/listbox": patch +"@heroui/popover": patch +"@heroui/snippet": patch +"@heroui/spinner": patch +"@heroui/tooltip": patch +"@heroui/avatar": patch +"@heroui/button": patch +"@heroui/drawer": patch +"@heroui/navbar": patch +"@heroui/ripple": patch +"@heroui/select": patch +"@heroui/slider": patch +"@heroui/spacer": patch +"@heroui/switch": patch +"@heroui/use-measure": patch +"@heroui/alert": patch +"@heroui/badge": patch +"@heroui/image": patch +"@heroui/input": patch +"@heroui/modal": patch +"@heroui/radio": patch +"@heroui/table": patch +"@heroui/toast": patch +"@heroui/use-resize": patch +"@heroui/card": patch +"@heroui/chip": patch +"@heroui/code": patch +"@heroui/form": patch +"@heroui/link": patch +"@heroui/menu": patch +"@heroui/tabs": patch +"@heroui/user": patch +"@heroui/system-rsc": patch +"@heroui/use-image": patch +"@heroui/use-theme": patch +"@heroui/kbd": patch +"@heroui/use-ssr": patch +"@heroui/system": patch +"@heroui/react": patch +"@heroui/theme": patch +--- + +add back RA deps (overlays & utils) \ No newline at end of file diff --git a/packages/components/pagination/package.json b/packages/components/pagination/package.json index 6a414dbb3f..f69726c09a 100644 --- a/packages/components/pagination/package.json +++ b/packages/components/pagination/package.json @@ -45,6 +45,7 @@ "@heroui/shared-icons": "workspace:*", "@heroui/use-intersection-observer": "workspace:*", "@heroui/use-pagination": "workspace:*", + "@react-aria/utils": "3.29.1", "@react-aria/focus": "3.20.5", "@react-aria/i18n": "3.12.10", "@react-aria/interactions": "3.25.3", diff --git a/packages/components/pagination/src/use-pagination-item.ts b/packages/components/pagination/src/use-pagination-item.ts index b9e2515222..4bfe56632c 100644 --- a/packages/components/pagination/src/use-pagination-item.ts +++ b/packages/components/pagination/src/use-pagination-item.ts @@ -4,7 +4,7 @@ import type {LinkDOMProps, PressEvent} from "@react-types/shared"; import type {PaginationItemValue} from "@heroui/use-pagination"; import {useMemo} from "react"; -import {shouldClientNavigate, useRouter} from "@heroui/system"; +import {shouldClientNavigate, useRouter} from "@react-aria/utils"; import {clsx, dataAttr, chain, mergeProps} from "@heroui/shared-utils"; import {filterDOMProps, useDOMRef} from "@heroui/react-utils"; import {useHover, usePress} from "@react-aria/interactions"; diff --git a/packages/core/system/package.json b/packages/core/system/package.json index e9f6e59cab..c8c4d38bfd 100644 --- a/packages/core/system/package.json +++ b/packages/core/system/package.json @@ -56,6 +56,8 @@ "dependencies": { "@heroui/react-utils": "workspace:*", "@heroui/system-rsc": "workspace:*", - "@react-aria/i18n": "3.12.10" + "@react-aria/i18n": "3.12.10", + "@react-aria/overlays": "3.27.3", + "@react-aria/utils": "3.29.1" } } \ No newline at end of file diff --git a/packages/core/system/src/ext/overlay-provider.tsx b/packages/core/system/src/ext/overlay-provider.tsx deleted file mode 100644 index 850d7a56e7..0000000000 --- a/packages/core/system/src/ext/overlay-provider.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// Partial code from react-spectrum to avoid importing the entire package -// ref: packages/@react-aria/overlays/src/useModal.tsx - -import type { - AriaAttributes, - AriaRole, - CSSProperties, - DOMAttributes as ReactDOMAttributes, - JSX, - ReactNode, -} from "react"; - -import React, {useContext, useMemo, useState} from "react"; - -interface FocusableElement extends Element, HTMLOrSVGElement {} - -interface DOMAttributes extends AriaAttributes, ReactDOMAttributes { - id?: string | undefined; - role?: AriaRole | undefined; - tabIndex?: number | undefined; - style?: CSSProperties | undefined; - className?: string | undefined; -} - -interface ModalProviderAria { - modalProviderProps: AriaAttributes; -} - -interface ModalContext { - parent: ModalContext | null; - modalCount: number; - addModal: () => void; - removeModal: () => void; -} - -export interface ModalProviderProps extends DOMAttributes { - children: ReactNode; -} - -const Context = React.createContext(null); - -/** - * Each ModalProvider tracks how many modals are open in its subtree. On mount, the modals - * trigger `addModal` to increment the count, and trigger `removeModal` on unmount to decrement it. - * This is done recursively so that all parent providers are incremented and decremented. - * If the modal count is greater than zero, we add `aria-hidden` to this provider to hide its - * subtree from screen readers. This is done using React context in order to account for things - * like portals, which can cause the React tree and the DOM tree to differ significantly in structure. - */ -function ModalProvider(props: ModalProviderProps): JSX.Element { - let {children} = props; - let parent = useContext(Context); - let [modalCount, setModalCount] = useState(0); - let context = useMemo( - () => ({ - parent, - modalCount, - addModal() { - setModalCount((count) => count + 1); - if (parent) { - parent.addModal(); - } - }, - removeModal() { - setModalCount((count) => count - 1); - if (parent) { - parent.removeModal(); - } - }, - }), - [parent, modalCount], - ); - - return {children}; -} - -/** - * Used to determine if the tree should be aria-hidden based on how many - * modals are open. - */ -function useModalProvider(): ModalProviderAria { - let context = useContext(Context); - - return { - modalProviderProps: { - "aria-hidden": context && context.modalCount > 0 ? true : undefined, - }, - }; -} - -/** - * Creates a root node that will be aria-hidden if there are other modals open. - */ -function OverlayContainerDOM(props: ModalProviderProps) { - let {modalProviderProps} = useModalProvider(); - - return
; -} - -/** - * An OverlayProvider acts as a container for the top-level application. - * Any application that uses modal dialogs or other overlays should - * be wrapped in a ``. This is used to ensure that - * the main content of the application is hidden from screen readers - * if a modal or other overlay is opened. Only the top-most modal or - * overlay should be accessible at once. - */ -export function OverlayProvider(props: ModalProviderProps): JSX.Element { - return ( - - - - ); -} diff --git a/packages/core/system/src/ext/router-provider.tsx b/packages/core/system/src/ext/router-provider.tsx deleted file mode 100644 index 0681ffa22e..0000000000 --- a/packages/core/system/src/ext/router-provider.tsx +++ /dev/null @@ -1,284 +0,0 @@ -// Partial code from react-spectrum to avoid importing the entire package -// ref: packages/@react-aria/utils/src/openLink.tsx -// ref: packages/@react-aria/utils/src/focusWithoutScrolling.ts - -import type {Href, RouterOptions, FocusableElement} from "./shared"; -import type {JSX, ReactNode} from "react"; - -import {createContext, useContext, useMemo} from "react"; - -interface Modifiers { - metaKey?: boolean; - ctrlKey?: boolean; - altKey?: boolean; - shiftKey?: boolean; -} - -interface Router { - isNative: boolean; - open: ( - target: Element, - modifiers: Modifiers, - href: Href, - routerOptions: RouterOptions | undefined, - ) => void; - useHref: (href: Href) => string; -} - -interface RouterProviderProps { - navigate: (path: Href, routerOptions: RouterOptions | undefined) => void; - useHref?: (href: Href) => string; - children: ReactNode; -} - -// This is a polyfill for element.focus({preventScroll: true}); -// Currently necessary for Safari and old Edge: -// https://caniuse.com/#feat=mdn-api_htmlelement_focus_preventscroll_option -// See https://bugs.webkit.org/show_bug.cgi?id=178583 -// - -// Original licensing for the following methods can be found in the -// NOTICE file in the root directory of this source tree. -// See https://github.com/calvellido/focus-options-polyfill -interface ScrollableElement { - element: HTMLElement; - scrollTop: number; - scrollLeft: number; -} - -const RouterContext = createContext({ - isNative: true, - open: openSyntheticLink, - useHref: (href) => href, -}); - -let supportsPreventScrollCached: boolean | null = null; - -function supportsPreventScroll() { - if (supportsPreventScrollCached == null) { - supportsPreventScrollCached = false; - try { - let focusElem = document.createElement("div"); - - focusElem.focus({ - get preventScroll() { - supportsPreventScrollCached = true; - - return true; - }, - }); - } catch { - // Ignore - } - } - - return supportsPreventScrollCached; -} - -function focusWithoutScrolling(element: FocusableElement): void { - if (supportsPreventScroll()) { - element.focus({preventScroll: true}); - } else { - let scrollableElements = getScrollableElements(element); - - element.focus(); - restoreScrollPosition(scrollableElements); - } -} - -function getScrollableElements(element: FocusableElement): ScrollableElement[] { - let parent = element.parentNode; - let scrollableElements: ScrollableElement[] = []; - let rootScrollingElement = document.scrollingElement || document.documentElement; - - while (parent instanceof HTMLElement && parent !== rootScrollingElement) { - if (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth) { - scrollableElements.push({ - element: parent, - scrollTop: parent.scrollTop, - scrollLeft: parent.scrollLeft, - }); - } - parent = parent.parentNode; - } - - if (rootScrollingElement instanceof HTMLElement) { - scrollableElements.push({ - element: rootScrollingElement, - scrollTop: rootScrollingElement.scrollTop, - scrollLeft: rootScrollingElement.scrollLeft, - }); - } - - return scrollableElements; -} - -function restoreScrollPosition(scrollableElements: ScrollableElement[]) { - for (let {element, scrollTop, scrollLeft} of scrollableElements) { - element.scrollTop = scrollTop; - element.scrollLeft = scrollLeft; - } -} - -function cached(fn: () => boolean) { - if (process.env.NODE_ENV === "test") { - return fn; - } - - let res: boolean | null = null; - - return () => { - if (res == null) { - res = fn(); - } - - return res; - }; -} - -function testUserAgent(re: RegExp) { - if (typeof window === "undefined" || window.navigator == null) { - return false; - } - - return ( - window.navigator["userAgentData"]?.brands.some((brand: {brand: string; version: string}) => - re.test(brand.brand), - ) || re.test(window.navigator.userAgent) - ); -} - -function testPlatform(re: RegExp) { - return typeof window !== "undefined" && window.navigator != null - ? re.test(window.navigator["userAgentData"]?.platform || window.navigator.platform) - : false; -} - -const isChrome = cached(function () { - return testUserAgent(/Chrome/i); -}); - -const isWebKit = cached(function () { - return testUserAgent(/AppleWebKit/i) && !isChrome(); -}); - -const isMac = cached(function () { - return testPlatform(/^Mac/i); -}); - -const isFirefox = cached(function () { - return testUserAgent(/Firefox/i); -}); - -const isIPad = cached(function () { - return testPlatform(/^iPad/i) || (isMac() && navigator.maxTouchPoints > 1); -}); - -export function RouterProvider(props: RouterProviderProps): JSX.Element { - let {children, navigate, useHref} = props; - - let ctx = useMemo( - () => ({ - isNative: false, - open: ( - target: Element, - modifiers: Modifiers, - href: Href, - routerOptions: RouterOptions | undefined, - ) => { - getSyntheticLink(target, (link) => { - if (shouldClientNavigate(link, modifiers)) { - navigate(href, routerOptions); - } else { - openLink(link, modifiers); - } - }); - }, - useHref: useHref || ((href) => href), - }), - [navigate, useHref], - ); - - return {children}; -} - -export function shouldClientNavigate(link: HTMLAnchorElement, modifiers: Modifiers): boolean { - let target = link.getAttribute("target"); - - return ( - (!target || target === "_self") && - link.origin === location.origin && - !link.hasAttribute("download") && - !modifiers.metaKey && - !modifiers.ctrlKey && - !modifiers.altKey && - !modifiers.shiftKey - ); -} - -export function useRouter(): Router { - return useContext(RouterContext); -} - -function openLink(target: HTMLAnchorElement, modifiers: Modifiers, setOpening = true): void { - let {metaKey, ctrlKey, altKey, shiftKey} = modifiers; - - if (isFirefox() && window.event?.type?.startsWith("key") && target.target === "_blank") { - if (isMac()) { - metaKey = true; - } else { - ctrlKey = true; - } - } - - let event = - isWebKit() && isMac() && !isIPad() && process.env.NODE_ENV !== "test" - ? // @ts-ignore - new KeyboardEvent("keydown", {keyIdentifier: "Enter", metaKey, ctrlKey, altKey, shiftKey}) - : new MouseEvent("click", { - metaKey, - ctrlKey, - altKey, - shiftKey, - bubbles: true, - cancelable: true, - }); - - (openLink as any).isOpening = setOpening; - focusWithoutScrolling(target); - target.dispatchEvent(event); - (openLink as any).isOpening = false; -} -(openLink as any).isOpening = false; - -function getSyntheticLink(target: Element, open: (link: HTMLAnchorElement) => void) { - if (target instanceof HTMLAnchorElement) { - open(target); - } else if (target.hasAttribute("data-href")) { - let link = document.createElement("a"); - - link.href = target.getAttribute("data-href")!; - if (target.hasAttribute("data-target")) { - link.target = target.getAttribute("data-target")!; - } - if (target.hasAttribute("data-rel")) { - link.rel = target.getAttribute("data-rel")!; - } - if (target.hasAttribute("data-download")) { - link.download = target.getAttribute("data-download")!; - } - if (target.hasAttribute("data-ping")) { - link.ping = target.getAttribute("data-ping")!; - } - if (target.hasAttribute("data-referrer-policy")) { - link.referrerPolicy = target.getAttribute("data-referrer-policy")!; - } - target.appendChild(link); - open(link); - target.removeChild(link); - } -} - -function openSyntheticLink(target: Element, modifiers: Modifiers) { - getSyntheticLink(target, (link) => openLink(link, modifiers)); -} diff --git a/packages/core/system/src/ext/shared.ts b/packages/core/system/src/ext/shared.ts deleted file mode 100644 index eddef43342..0000000000 --- a/packages/core/system/src/ext/shared.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type {HTMLAttributeAnchorTarget, HTMLAttributeReferrerPolicy} from "react"; - -export interface RouterConfig {} - -export type Href = RouterConfig extends {href: infer H} ? H : string; - -export type RouterOptions = RouterConfig extends {routerOptions: infer O} ? O : never; - -export interface LinkDOMProps { - /** A URL to link to. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#href). */ - href?: Href; - /** Hints at the human language of the linked URL. See[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#hreflang). */ - hrefLang?: string; - /** The target window for the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target). */ - target?: HTMLAttributeAnchorTarget; - /** The relationship between the linked resource and the current page. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). */ - rel?: string; - /** Causes the browser to download the linked URL. A string may be provided to suggest a file name. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download). */ - download?: boolean | string; - /** A space-separated list of URLs to ping when the link is followed. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#ping). */ - ping?: string; - /** How much of the referrer to send when following the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#referrerpolicy). */ - referrerPolicy?: HTMLAttributeReferrerPolicy; - /** Options for the configured client side router. */ - routerOptions?: RouterOptions; -} - -/** Any focusable element, including both HTML and SVG elements. */ -export interface FocusableElement extends Element, HTMLOrSVGElement {} diff --git a/packages/core/system/src/index.ts b/packages/core/system/src/index.ts index 2ff5724fdb..ebc192950c 100644 --- a/packages/core/system/src/index.ts +++ b/packages/core/system/src/index.ts @@ -34,5 +34,3 @@ export {HeroUIProvider} from "./provider"; export {ProviderContext, useProviderContext} from "./provider-context"; export {useLabelPlacement, useInputLabelPlacement} from "./hooks"; - -export {shouldClientNavigate, useRouter} from "./ext/router-provider"; diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index 2332cd7c3f..4c661c59e5 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,15 +1,15 @@ -import type {ModalProviderProps} from "./ext/overlay-provider"; +import type {ModalProviderProps} from "@react-aria/overlays"; import type {I18nProviderProps} from "@react-aria/i18n"; +import type {Href, RouterOptions} from "@react-types/shared"; import type {ProviderContextProps} from "./provider-context"; -import type {Href, RouterOptions} from "./ext/shared"; import {I18nProvider} from "@react-aria/i18n"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; +import {RouterProvider} from "@react-aria/utils"; +import {OverlayProvider} from "@react-aria/overlays"; import {ProviderContext} from "./provider-context"; -import {OverlayProvider} from "./ext/overlay-provider"; -import {RouterProvider} from "./ext/router-provider"; export interface HeroUIProviderProps extends Omit, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a3eb81ff0..bd6aa5bd3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2109,6 +2109,9 @@ importers: '@react-aria/interactions': specifier: 3.25.3 version: 3.25.3(react-dom@18.3.0(react@18.3.0))(react@18.3.0) + '@react-aria/utils': + specifier: 3.29.1 + version: 3.29.1(react-dom@18.3.0(react@18.3.0))(react@18.3.0) scroll-into-view-if-needed: specifier: 3.0.10 version: 3.0.10 @@ -3163,6 +3166,12 @@ importers: '@react-aria/i18n': specifier: 3.12.10 version: 3.12.10(react-dom@18.3.0(react@18.3.0))(react@18.3.0) + '@react-aria/overlays': + specifier: 3.27.3 + version: 3.27.3(react-dom@18.3.0(react@18.3.0))(react@18.3.0) + '@react-aria/utils': + specifier: 3.29.1 + version: 3.29.1(react-dom@18.3.0(react@18.3.0))(react@18.3.0) devDependencies: clean-package: specifier: 2.2.0