From 0a3ee8065f66a9ad93c2d990ee25026436159c67 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Wed, 18 Jun 2025 12:59:54 +0800 Subject: [PATCH 1/6] refactor(system): remove `@react-aria/i18n` package --- packages/core/system/package.json | 1 - packages/core/system/src/i18n-provider.tsx | 150 +++++++++++++++++++ packages/core/system/src/provider.tsx | 11 +- packages/core/system/src/ssr-provider.tsx | 161 +++++++++++++++++++++ pnpm-lock.yaml | 3 - 5 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 packages/core/system/src/i18n-provider.tsx create mode 100644 packages/core/system/src/ssr-provider.tsx diff --git a/packages/core/system/package.json b/packages/core/system/package.json index 267ec8de0b..43077d1e28 100644 --- a/packages/core/system/package.json +++ b/packages/core/system/package.json @@ -56,7 +56,6 @@ "dependencies": { "@heroui/react-utils": "workspace:*", "@heroui/system-rsc": "workspace:*", - "@react-aria/i18n": "3.12.10", "@react-aria/overlays": "3.27.3", "@react-aria/utils": "3.29.1" } diff --git a/packages/core/system/src/i18n-provider.tsx b/packages/core/system/src/i18n-provider.tsx new file mode 100644 index 0000000000..1689d02eaf --- /dev/null +++ b/packages/core/system/src/i18n-provider.tsx @@ -0,0 +1,150 @@ +import type {ReactNode} from "react"; + +import React, {useEffect, useMemo, useState} from "react"; + +import {useIsSSR} from "./ssr-provider"; + +export type Direction = "ltr" | "rtl"; + +export interface I18nProviderProps { + children: ReactNode; + locale?: string; +} + +export interface Locale { + locale: string; + direction: Direction; +} + +const RTL_SCRIPTS = new Set([ + "Arab", + "Syrc", + "Samr", + "Mand", + "Thaa", + "Mend", + "Nkoo", + "Adlm", + "Rohg", + "Hebr", +]); + +const RTL_LANGS = new Set([ + "ae", + "ar", + "arc", + "bcc", + "bqi", + "ckb", + "dv", + "fa", + "glk", + "he", + "ku", + "mzn", + "nqo", + "pnb", + "ps", + "sd", + "ug", + "ur", + "yi", +]); + +export function isRTL(localeString: string): boolean { + if (Intl.Locale) { + let locale = new Intl.Locale(localeString).maximize(); + let textInfo = + // @ts-ignore + typeof locale.getTextInfo === "function" ? locale.getTextInfo() : locale.textInfo; + + if (textInfo) { + return textInfo.direction === "rtl"; + } + if (locale.script) { + return RTL_SCRIPTS.has(locale.script); + } + } + let lang = localeString.split("-")[0]; + + return RTL_LANGS.has(lang); +} + +const localeSymbol = Symbol.for("react-aria.i18n.locale"); + +const I18nContext = React.createContext(null); + +export function getDefaultLocale(): Locale { + let locale = + (typeof window !== "undefined" && window[localeSymbol]) || + // @ts-ignore + (typeof navigator !== "undefined" && (navigator.language || navigator.userLanguage)) || + "en-US"; + + try { + Intl.DateTimeFormat.supportedLocalesOf([locale]); + } catch { + locale = "en-US"; + } + + return { + locale, + direction: isRTL(locale) ? "rtl" : "ltr", + }; +} + +let currentLocale = getDefaultLocale(); + +let listeners = new Set<(locale: Locale) => void>(); + +function updateLocale() { + currentLocale = getDefaultLocale(); + for (let listener of listeners) { + listener(currentLocale); + } +} + +export function useDefaultLocale(): Locale { + let isSSR = useIsSSR(); + let [defaultLocale, setDefaultLocale] = useState(currentLocale); + + useEffect(() => { + if (listeners.size === 0) { + window.addEventListener("languagechange", updateLocale); + } + listeners.add(setDefaultLocale); + + return () => { + listeners.delete(setDefaultLocale); + if (listeners.size === 0) { + window.removeEventListener("languagechange", updateLocale); + } + }; + }, []); + + if (isSSR) { + return { + locale: "en-US", + direction: "ltr", + }; + } + + return defaultLocale; +} + +export function I18nProvider(props: I18nProviderProps): JSX.Element { + let {locale, children} = props; + let defaultLocale = useDefaultLocale(); + let value: Locale = useMemo(() => { + if (!locale) { + return defaultLocale; + } + + return { + locale, + direction: isRTL(locale) ? "rtl" : "ltr", + }; + }, [defaultLocale, locale]); + + return {children}; +} diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index 1c9d16734e..04a4e92aa5 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,15 +1,20 @@ import type {ModalProviderProps} from "@react-aria/overlays"; import type {ProviderContextProps} from "./provider-context"; -import type {Href, RouterOptions} from "@react-types/shared"; -import type {I18nProviderProps} from "@react-aria/i18n"; +import type {I18nProviderProps} from "./i18n-provider"; -import {I18nProvider} from "@react-aria/i18n"; import {RouterProvider} from "@react-aria/utils"; import {OverlayProvider} from "@react-aria/overlays"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; import {ProviderContext} from "./provider-context"; +import {I18nProvider} from "./i18n-provider"; + +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 HeroUIProviderProps extends Omit, diff --git a/packages/core/system/src/ssr-provider.tsx b/packages/core/system/src/ssr-provider.tsx new file mode 100644 index 0000000000..5be3b9fc1c --- /dev/null +++ b/packages/core/system/src/ssr-provider.tsx @@ -0,0 +1,161 @@ +import type {JSX, ReactNode} from "react"; + +import React, {useContext, useLayoutEffect, useMemo, useRef, useState} from "react"; + +interface SSRContextValue { + prefix: string; + current: number; +} + +const defaultContext: SSRContextValue = { + prefix: String(Math.round(Math.random() * 10000000000)), + current: 0, +}; + +const SSRContext = React.createContext(defaultContext); +const IsSSRContext = React.createContext(false); + +export interface SSRProviderProps { + children: ReactNode; +} + +function LegacySSRProvider(props: SSRProviderProps): JSX.Element { + let cur = useContext(SSRContext); + let counter = useCounter(cur === defaultContext); + let [isSSR, setIsSSR] = useState(true); + let value: SSRContextValue = useMemo( + () => ({ + prefix: cur === defaultContext ? "" : `${cur.prefix}-${counter}`, + current: 0, + }), + [cur, counter], + ); + + if (typeof document !== "undefined") { + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + setIsSSR(false); + }, []); + } + + return ( + + {props.children} + + ); +} + +let warnedAboutSSRProvider = false; + +export function SSRProvider(props: SSRProviderProps): JSX.Element { + if (typeof React["useId"] === "function") { + if ( + process.env.NODE_ENV !== "test" && + process.env.NODE_ENV !== "production" && + !warnedAboutSSRProvider + ) { + // eslint-disable-next-line + console.warn( + "In React 18, SSRProvider is not necessary and is a noop. You can remove it from your app.", + ); + warnedAboutSSRProvider = true; + } + + return <>{props.children}; + } + + return ; +} + +let canUseDOM = Boolean( + typeof window !== "undefined" && window.document && window.document.createElement, +); + +let componentIds = new WeakMap(); + +function useCounter(isDisabled = false) { + let ctx = useContext(SSRContext); + let ref = useRef(null); + + if (ref.current === null && !isDisabled) { + let currentOwner = + // @ts-ignore + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner?.current; + + if (currentOwner) { + let prevComponentValue = componentIds.get(currentOwner); + + if (prevComponentValue == null) { + // On the first render, and first call to useId, store the id and state in our weak map. + componentIds.set(currentOwner, { + id: ctx.current, + state: currentOwner.memoizedState, + }); + } else if (currentOwner.memoizedState !== prevComponentValue.state) { + // On the second render, the memoizedState gets reset by React. + // Reset the counter, and remove from the weak map so we don't + // do this for subsequent useId calls. + ctx.current = prevComponentValue.id; + componentIds.delete(currentOwner); + } + } + + ref.current = ++ctx.current; + } + + return ref.current; +} + +function useLegacySSRSafeId(defaultId?: string): string { + let ctx = useContext(SSRContext); + + if (ctx === defaultContext && !canUseDOM && process.env.NODE_ENV !== "production") { + // eslint-disable-next-line + console.warn( + "When server rendering, you must wrap your application in an to ensure consistent ids are generated between the client and server.", + ); + } + let counter = useCounter(!!defaultId); + let prefix = + ctx === defaultContext && process.env.NODE_ENV === "test" + ? "react-aria" + : `react-aria${ctx.prefix}`; + + return defaultId || `${prefix}-${counter}`; +} + +function useModernSSRSafeId(defaultId?: string): string { + let id = React.useId(); + let [didSSR] = useState(useIsSSR()); + let prefix = + didSSR || process.env.NODE_ENV === "test" ? "react-aria" : `react-aria${defaultContext.prefix}`; + + return defaultId || `${prefix}-${id}`; +} + +/** @private */ +export const useSSRSafeId = + typeof React["useId"] === "function" ? useModernSSRSafeId : useLegacySSRSafeId; + +function getSnapshot() { + return false; +} + +function getServerSnapshot() { + return true; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function subscribe(onStoreChange: () => void): () => void { + // noop + return () => {}; +} + +export function useIsSSR(): boolean { + if (typeof React["useSyncExternalStore"] === "function") { + return React["useSyncExternalStore"](subscribe, getSnapshot, getServerSnapshot); + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + return useContext(IsSSRContext); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7489e4b36..fc0baa0515 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3247,9 +3247,6 @@ importers: '@heroui/system-rsc': specifier: workspace:* version: link:../system-rsc - '@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) From 9fc249e000759ae66f3ef741d1656bdea4f4a34b Mon Sep 17 00:00:00 2001 From: WK Wong Date: Wed, 18 Jun 2025 16:53:30 +0800 Subject: [PATCH 2/6] refactor(system): remove `@react-aria/overlays` package --- packages/core/system/package.json | 1 - packages/core/system/src/overlay-provider.tsx | 87 +++++++++++++++++++ packages/core/system/src/provider.tsx | 4 +- pnpm-lock.yaml | 3 - 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 packages/core/system/src/overlay-provider.tsx diff --git a/packages/core/system/package.json b/packages/core/system/package.json index 43077d1e28..1ef89392ef 100644 --- a/packages/core/system/package.json +++ b/packages/core/system/package.json @@ -56,7 +56,6 @@ "dependencies": { "@heroui/react-utils": "workspace:*", "@heroui/system-rsc": "workspace:*", - "@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/overlay-provider.tsx b/packages/core/system/src/overlay-provider.tsx new file mode 100644 index 0000000000..ca421a3cb1 --- /dev/null +++ b/packages/core/system/src/overlay-provider.tsx @@ -0,0 +1,87 @@ +import type { + AriaAttributes, + AriaRole, + CSSProperties, + DOMAttributes as ReactDOMAttributes, + JSX, + ReactNode, +} from "react"; + +import React, {useContext, useMemo, useState} from "react"; + +export interface FocusableElement extends Element, HTMLOrSVGElement {} + +export interface DOMAttributes extends AriaAttributes, ReactDOMAttributes { + id?: string | undefined; + role?: AriaRole | undefined; + tabIndex?: number | undefined; + style?: CSSProperties | undefined; + className?: string | undefined; +} + +export interface ModalProviderProps extends DOMAttributes { + children: ReactNode; +} + +export interface ModalProviderAria { + modalProviderProps: AriaAttributes; +} + +interface ModalContext { + parent: ModalContext | null; + modalCount: number; + addModal: () => void; + removeModal: () => void; +} + +const Context = React.createContext(null); + +export 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}; +} + +export function useModalProvider(): ModalProviderAria { + let context = useContext(Context); + + return { + modalProviderProps: { + "aria-hidden": context && context.modalCount > 0 ? true : undefined, + }, + }; +} +export function OverlayContainerDOM(props: ModalProviderProps) { + let {modalProviderProps} = useModalProvider(); + + return
; +} + +export function OverlayProvider(props: ModalProviderProps): JSX.Element { + return ( + + + + ); +} diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index 04a4e92aa5..a90d8d1972 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,14 +1,14 @@ -import type {ModalProviderProps} from "@react-aria/overlays"; +import type {ModalProviderProps} from "./overlay-provider"; import type {ProviderContextProps} from "./provider-context"; import type {I18nProviderProps} from "./i18n-provider"; import {RouterProvider} from "@react-aria/utils"; -import {OverlayProvider} from "@react-aria/overlays"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; import {ProviderContext} from "./provider-context"; import {I18nProvider} from "./i18n-provider"; +import {OverlayProvider} from "./overlay-provider"; export interface RouterConfig {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc0baa0515..5109fe3b1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3247,9 +3247,6 @@ importers: '@heroui/system-rsc': specifier: workspace:* version: link:../system-rsc - '@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) From 4986575d274b93991df248a1612aee05a0d65264 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Wed, 18 Jun 2025 16:54:27 +0800 Subject: [PATCH 3/6] refactor(system): remove `@react-aria/overlays` package --- .changeset/honest-dolphins-reflect.md | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .changeset/honest-dolphins-reflect.md diff --git a/.changeset/honest-dolphins-reflect.md b/.changeset/honest-dolphins-reflect.md new file mode 100644 index 0000000000..ae91653746 --- /dev/null +++ b/.changeset/honest-dolphins-reflect.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 +--- + +remove RA dependencies (overlays & i18n) From 77a75da409edfa8939403f47fc0a77fdbbd7f3ea Mon Sep 17 00:00:00 2001 From: WK Wong Date: Wed, 18 Jun 2025 17:36:20 +0800 Subject: [PATCH 4/6] refactor: move to ext folder and add credit --- packages/core/system/src/{ => ext}/i18n-provider.tsx | 3 +++ packages/core/system/src/{ => ext}/overlay-provider.tsx | 3 +++ packages/core/system/src/{ => ext}/ssr-provider.tsx | 3 +++ packages/core/system/src/provider.tsx | 8 ++++---- 4 files changed, 13 insertions(+), 4 deletions(-) rename packages/core/system/src/{ => ext}/i18n-provider.tsx (95%) rename packages/core/system/src/{ => ext}/overlay-provider.tsx (94%) rename packages/core/system/src/{ => ext}/ssr-provider.tsx (97%) diff --git a/packages/core/system/src/i18n-provider.tsx b/packages/core/system/src/ext/i18n-provider.tsx similarity index 95% rename from packages/core/system/src/i18n-provider.tsx rename to packages/core/system/src/ext/i18n-provider.tsx index 1689d02eaf..703dec51af 100644 --- a/packages/core/system/src/i18n-provider.tsx +++ b/packages/core/system/src/ext/i18n-provider.tsx @@ -1,3 +1,6 @@ +// Partial code from react-spectrum to avoid importing the entire package +// ref: packages/@react-aria/i18n/src/context.tsx + import type {ReactNode} from "react"; import React, {useEffect, useMemo, useState} from "react"; diff --git a/packages/core/system/src/overlay-provider.tsx b/packages/core/system/src/ext/overlay-provider.tsx similarity index 94% rename from packages/core/system/src/overlay-provider.tsx rename to packages/core/system/src/ext/overlay-provider.tsx index ca421a3cb1..1df9168ec3 100644 --- a/packages/core/system/src/overlay-provider.tsx +++ b/packages/core/system/src/ext/overlay-provider.tsx @@ -1,3 +1,6 @@ +// Partial code from react-spectrum to avoid importing the entire package +// ref: packages/@react-aria/overlays/src/useModal.tsx + import type { AriaAttributes, AriaRole, diff --git a/packages/core/system/src/ssr-provider.tsx b/packages/core/system/src/ext/ssr-provider.tsx similarity index 97% rename from packages/core/system/src/ssr-provider.tsx rename to packages/core/system/src/ext/ssr-provider.tsx index 5be3b9fc1c..8a2b15ac96 100644 --- a/packages/core/system/src/ssr-provider.tsx +++ b/packages/core/system/src/ext/ssr-provider.tsx @@ -1,3 +1,6 @@ +// Partial code from react-spectrum to avoid importing the entire package +// ref: packages/@react-aria/ssr/src/SSRProvider.tsx + import type {JSX, ReactNode} from "react"; import React, {useContext, useLayoutEffect, useMemo, useRef, useState} from "react"; diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index a90d8d1972..b81f1cfd3d 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,14 +1,14 @@ -import type {ModalProviderProps} from "./overlay-provider"; +import type {ModalProviderProps} from "./ext/overlay-provider"; +import type {I18nProviderProps} from "./ext/i18n-provider"; import type {ProviderContextProps} from "./provider-context"; -import type {I18nProviderProps} from "./i18n-provider"; import {RouterProvider} from "@react-aria/utils"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; import {ProviderContext} from "./provider-context"; -import {I18nProvider} from "./i18n-provider"; -import {OverlayProvider} from "./overlay-provider"; +import {I18nProvider} from "./ext/i18n-provider"; +import {OverlayProvider} from "./ext/overlay-provider"; export interface RouterConfig {} From 635e62902380d33d6fb7d7636fb2a4ecf5ac59e7 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Wed, 18 Jun 2025 23:38:18 +0800 Subject: [PATCH 5/6] chore: rollback --- .changeset/honest-dolphins-reflect.md | 2 +- packages/core/system/package.json | 1 + .../core/system/src/ext/i18n-provider.tsx | 153 ---------------- packages/core/system/src/ext/ssr-provider.tsx | 164 ------------------ packages/core/system/src/provider.tsx | 4 +- pnpm-lock.yaml | 3 + 6 files changed, 7 insertions(+), 320 deletions(-) delete mode 100644 packages/core/system/src/ext/i18n-provider.tsx delete mode 100644 packages/core/system/src/ext/ssr-provider.tsx diff --git a/.changeset/honest-dolphins-reflect.md b/.changeset/honest-dolphins-reflect.md index ae91653746..729819a3a0 100644 --- a/.changeset/honest-dolphins-reflect.md +++ b/.changeset/honest-dolphins-reflect.md @@ -87,4 +87,4 @@ "@heroui/theme": patch --- -remove RA dependencies (overlays & i18n) +remove RA dependencies (overlays) diff --git a/packages/core/system/package.json b/packages/core/system/package.json index 1ef89392ef..71161d19b4 100644 --- a/packages/core/system/package.json +++ b/packages/core/system/package.json @@ -56,6 +56,7 @@ "dependencies": { "@heroui/react-utils": "workspace:*", "@heroui/system-rsc": "workspace:*", + "@react-aria/i18n": "3.12.10", "@react-aria/utils": "3.29.1" } } \ No newline at end of file diff --git a/packages/core/system/src/ext/i18n-provider.tsx b/packages/core/system/src/ext/i18n-provider.tsx deleted file mode 100644 index 703dec51af..0000000000 --- a/packages/core/system/src/ext/i18n-provider.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// Partial code from react-spectrum to avoid importing the entire package -// ref: packages/@react-aria/i18n/src/context.tsx - -import type {ReactNode} from "react"; - -import React, {useEffect, useMemo, useState} from "react"; - -import {useIsSSR} from "./ssr-provider"; - -export type Direction = "ltr" | "rtl"; - -export interface I18nProviderProps { - children: ReactNode; - locale?: string; -} - -export interface Locale { - locale: string; - direction: Direction; -} - -const RTL_SCRIPTS = new Set([ - "Arab", - "Syrc", - "Samr", - "Mand", - "Thaa", - "Mend", - "Nkoo", - "Adlm", - "Rohg", - "Hebr", -]); - -const RTL_LANGS = new Set([ - "ae", - "ar", - "arc", - "bcc", - "bqi", - "ckb", - "dv", - "fa", - "glk", - "he", - "ku", - "mzn", - "nqo", - "pnb", - "ps", - "sd", - "ug", - "ur", - "yi", -]); - -export function isRTL(localeString: string): boolean { - if (Intl.Locale) { - let locale = new Intl.Locale(localeString).maximize(); - let textInfo = - // @ts-ignore - typeof locale.getTextInfo === "function" ? locale.getTextInfo() : locale.textInfo; - - if (textInfo) { - return textInfo.direction === "rtl"; - } - if (locale.script) { - return RTL_SCRIPTS.has(locale.script); - } - } - let lang = localeString.split("-")[0]; - - return RTL_LANGS.has(lang); -} - -const localeSymbol = Symbol.for("react-aria.i18n.locale"); - -const I18nContext = React.createContext(null); - -export function getDefaultLocale(): Locale { - let locale = - (typeof window !== "undefined" && window[localeSymbol]) || - // @ts-ignore - (typeof navigator !== "undefined" && (navigator.language || navigator.userLanguage)) || - "en-US"; - - try { - Intl.DateTimeFormat.supportedLocalesOf([locale]); - } catch { - locale = "en-US"; - } - - return { - locale, - direction: isRTL(locale) ? "rtl" : "ltr", - }; -} - -let currentLocale = getDefaultLocale(); - -let listeners = new Set<(locale: Locale) => void>(); - -function updateLocale() { - currentLocale = getDefaultLocale(); - for (let listener of listeners) { - listener(currentLocale); - } -} - -export function useDefaultLocale(): Locale { - let isSSR = useIsSSR(); - let [defaultLocale, setDefaultLocale] = useState(currentLocale); - - useEffect(() => { - if (listeners.size === 0) { - window.addEventListener("languagechange", updateLocale); - } - listeners.add(setDefaultLocale); - - return () => { - listeners.delete(setDefaultLocale); - if (listeners.size === 0) { - window.removeEventListener("languagechange", updateLocale); - } - }; - }, []); - - if (isSSR) { - return { - locale: "en-US", - direction: "ltr", - }; - } - - return defaultLocale; -} - -export function I18nProvider(props: I18nProviderProps): JSX.Element { - let {locale, children} = props; - let defaultLocale = useDefaultLocale(); - let value: Locale = useMemo(() => { - if (!locale) { - return defaultLocale; - } - - return { - locale, - direction: isRTL(locale) ? "rtl" : "ltr", - }; - }, [defaultLocale, locale]); - - return {children}; -} diff --git a/packages/core/system/src/ext/ssr-provider.tsx b/packages/core/system/src/ext/ssr-provider.tsx deleted file mode 100644 index 8a2b15ac96..0000000000 --- a/packages/core/system/src/ext/ssr-provider.tsx +++ /dev/null @@ -1,164 +0,0 @@ -// Partial code from react-spectrum to avoid importing the entire package -// ref: packages/@react-aria/ssr/src/SSRProvider.tsx - -import type {JSX, ReactNode} from "react"; - -import React, {useContext, useLayoutEffect, useMemo, useRef, useState} from "react"; - -interface SSRContextValue { - prefix: string; - current: number; -} - -const defaultContext: SSRContextValue = { - prefix: String(Math.round(Math.random() * 10000000000)), - current: 0, -}; - -const SSRContext = React.createContext(defaultContext); -const IsSSRContext = React.createContext(false); - -export interface SSRProviderProps { - children: ReactNode; -} - -function LegacySSRProvider(props: SSRProviderProps): JSX.Element { - let cur = useContext(SSRContext); - let counter = useCounter(cur === defaultContext); - let [isSSR, setIsSSR] = useState(true); - let value: SSRContextValue = useMemo( - () => ({ - prefix: cur === defaultContext ? "" : `${cur.prefix}-${counter}`, - current: 0, - }), - [cur, counter], - ); - - if (typeof document !== "undefined") { - // eslint-disable-next-line react-hooks/rules-of-hooks - useLayoutEffect(() => { - setIsSSR(false); - }, []); - } - - return ( - - {props.children} - - ); -} - -let warnedAboutSSRProvider = false; - -export function SSRProvider(props: SSRProviderProps): JSX.Element { - if (typeof React["useId"] === "function") { - if ( - process.env.NODE_ENV !== "test" && - process.env.NODE_ENV !== "production" && - !warnedAboutSSRProvider - ) { - // eslint-disable-next-line - console.warn( - "In React 18, SSRProvider is not necessary and is a noop. You can remove it from your app.", - ); - warnedAboutSSRProvider = true; - } - - return <>{props.children}; - } - - return ; -} - -let canUseDOM = Boolean( - typeof window !== "undefined" && window.document && window.document.createElement, -); - -let componentIds = new WeakMap(); - -function useCounter(isDisabled = false) { - let ctx = useContext(SSRContext); - let ref = useRef(null); - - if (ref.current === null && !isDisabled) { - let currentOwner = - // @ts-ignore - React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner?.current; - - if (currentOwner) { - let prevComponentValue = componentIds.get(currentOwner); - - if (prevComponentValue == null) { - // On the first render, and first call to useId, store the id and state in our weak map. - componentIds.set(currentOwner, { - id: ctx.current, - state: currentOwner.memoizedState, - }); - } else if (currentOwner.memoizedState !== prevComponentValue.state) { - // On the second render, the memoizedState gets reset by React. - // Reset the counter, and remove from the weak map so we don't - // do this for subsequent useId calls. - ctx.current = prevComponentValue.id; - componentIds.delete(currentOwner); - } - } - - ref.current = ++ctx.current; - } - - return ref.current; -} - -function useLegacySSRSafeId(defaultId?: string): string { - let ctx = useContext(SSRContext); - - if (ctx === defaultContext && !canUseDOM && process.env.NODE_ENV !== "production") { - // eslint-disable-next-line - console.warn( - "When server rendering, you must wrap your application in an to ensure consistent ids are generated between the client and server.", - ); - } - let counter = useCounter(!!defaultId); - let prefix = - ctx === defaultContext && process.env.NODE_ENV === "test" - ? "react-aria" - : `react-aria${ctx.prefix}`; - - return defaultId || `${prefix}-${counter}`; -} - -function useModernSSRSafeId(defaultId?: string): string { - let id = React.useId(); - let [didSSR] = useState(useIsSSR()); - let prefix = - didSSR || process.env.NODE_ENV === "test" ? "react-aria" : `react-aria${defaultContext.prefix}`; - - return defaultId || `${prefix}-${id}`; -} - -/** @private */ -export const useSSRSafeId = - typeof React["useId"] === "function" ? useModernSSRSafeId : useLegacySSRSafeId; - -function getSnapshot() { - return false; -} - -function getServerSnapshot() { - return true; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function subscribe(onStoreChange: () => void): () => void { - // noop - return () => {}; -} - -export function useIsSSR(): boolean { - if (typeof React["useSyncExternalStore"] === "function") { - return React["useSyncExternalStore"](subscribe, getSnapshot, getServerSnapshot); - } - - // eslint-disable-next-line react-hooks/rules-of-hooks - return useContext(IsSSRContext); -} diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index b81f1cfd3d..434174b6aa 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,13 +1,13 @@ import type {ModalProviderProps} from "./ext/overlay-provider"; -import type {I18nProviderProps} from "./ext/i18n-provider"; +import type {I18nProviderProps} from "@react-aria/i18n"; import type {ProviderContextProps} from "./provider-context"; import {RouterProvider} from "@react-aria/utils"; +import {I18nProvider} from "@react-aria/i18n"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; import {ProviderContext} from "./provider-context"; -import {I18nProvider} from "./ext/i18n-provider"; import {OverlayProvider} from "./ext/overlay-provider"; export interface RouterConfig {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5109fe3b1c..f83e94c3a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3247,6 +3247,9 @@ importers: '@heroui/system-rsc': specifier: workspace:* version: link:../system-rsc + '@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/utils': specifier: 3.29.1 version: 3.29.1(react-dom@18.3.0(react@18.3.0))(react@18.3.0) From ad0f69e26c7d8e7038e6af2874ec372a8896b869 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Thu, 19 Jun 2025 00:19:59 +0800 Subject: [PATCH 6/6] refactor(system): remove `@react-aria/utils` package --- .changeset/honest-dolphins-reflect.md | 2 +- packages/core/system/package.json | 3 +- .../core/system/src/ext/overlay-provider.tsx | 44 ++- .../core/system/src/ext/router-provider.tsx | 280 ++++++++++++++++++ packages/core/system/src/ext/shared.ts | 29 ++ packages/core/system/src/provider.tsx | 9 +- pnpm-lock.yaml | 3 - 7 files changed, 347 insertions(+), 23 deletions(-) create mode 100644 packages/core/system/src/ext/router-provider.tsx create mode 100644 packages/core/system/src/ext/shared.ts diff --git a/.changeset/honest-dolphins-reflect.md b/.changeset/honest-dolphins-reflect.md index 729819a3a0..6fb41f17a7 100644 --- a/.changeset/honest-dolphins-reflect.md +++ b/.changeset/honest-dolphins-reflect.md @@ -87,4 +87,4 @@ "@heroui/theme": patch --- -remove RA dependencies (overlays) +remove RA dependencies (overlays & utils) diff --git a/packages/core/system/package.json b/packages/core/system/package.json index 71161d19b4..2747ce0607 100644 --- a/packages/core/system/package.json +++ b/packages/core/system/package.json @@ -56,7 +56,6 @@ "dependencies": { "@heroui/react-utils": "workspace:*", "@heroui/system-rsc": "workspace:*", - "@react-aria/i18n": "3.12.10", - "@react-aria/utils": "3.29.1" + "@react-aria/i18n": "3.12.10" } } \ 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 index 1df9168ec3..850d7a56e7 100644 --- a/packages/core/system/src/ext/overlay-provider.tsx +++ b/packages/core/system/src/ext/overlay-provider.tsx @@ -12,9 +12,9 @@ import type { import React, {useContext, useMemo, useState} from "react"; -export interface FocusableElement extends Element, HTMLOrSVGElement {} +interface FocusableElement extends Element, HTMLOrSVGElement {} -export interface DOMAttributes extends AriaAttributes, ReactDOMAttributes { +interface DOMAttributes extends AriaAttributes, ReactDOMAttributes { id?: string | undefined; role?: AriaRole | undefined; tabIndex?: number | undefined; @@ -22,11 +22,7 @@ export interface DOMAttributes extends AriaAttributes, Rea className?: string | undefined; } -export interface ModalProviderProps extends DOMAttributes { - children: ReactNode; -} - -export interface ModalProviderAria { +interface ModalProviderAria { modalProviderProps: AriaAttributes; } @@ -37,9 +33,21 @@ interface ModalContext { removeModal: () => void; } +export interface ModalProviderProps extends DOMAttributes { + children: ReactNode; +} + const Context = React.createContext(null); -export function ModalProvider(props: ModalProviderProps): JSX.Element { +/** + * 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); @@ -66,7 +74,11 @@ export function ModalProvider(props: ModalProviderProps): JSX.Element { return {children}; } -export function useModalProvider(): ModalProviderAria { +/** + * 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 { @@ -75,12 +87,24 @@ export function useModalProvider(): ModalProviderAria { }, }; } -export function OverlayContainerDOM(props: ModalProviderProps) { + +/** + * 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 new file mode 100644 index 0000000000..d86539ac0d --- /dev/null +++ b/packages/core/system/src/ext/router-provider.tsx @@ -0,0 +1,280 @@ +// 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, 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}; +} + +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 + ); +} + +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 new file mode 100644 index 0000000000..eddef43342 --- /dev/null +++ b/packages/core/system/src/ext/shared.ts @@ -0,0 +1,29 @@ +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/provider.tsx b/packages/core/system/src/provider.tsx index 434174b6aa..2332cd7c3f 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -1,20 +1,15 @@ import type {ModalProviderProps} from "./ext/overlay-provider"; import type {I18nProviderProps} from "@react-aria/i18n"; import type {ProviderContextProps} from "./provider-context"; +import type {Href, RouterOptions} from "./ext/shared"; -import {RouterProvider} from "@react-aria/utils"; import {I18nProvider} from "@react-aria/i18n"; import {useMemo} from "react"; import {MotionConfig, MotionGlobalConfig} from "framer-motion"; import {ProviderContext} from "./provider-context"; import {OverlayProvider} from "./ext/overlay-provider"; - -export interface RouterConfig {} - -export type Href = RouterConfig extends {href: infer H} ? H : string; - -export type RouterOptions = RouterConfig extends {routerOptions: infer O} ? O : never; +import {RouterProvider} from "./ext/router-provider"; export interface HeroUIProviderProps extends Omit, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f83e94c3a6..5c53474982 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3250,9 +3250,6 @@ 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/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