diff --git a/packages/@react-aria/i18n/src/useDefaultLocale.ts b/packages/@react-aria/i18n/src/useDefaultLocale.ts index 456f68764c7..092e76de732 100644 --- a/packages/@react-aria/i18n/src/useDefaultLocale.ts +++ b/packages/@react-aria/i18n/src/useDefaultLocale.ts @@ -80,8 +80,9 @@ export function useDefaultLocale(): Locale { // We cannot determine the browser's language on the server, so default to // en-US. This will be updated after hydration on the client to the correct value. if (isSSR) { + let locale = typeof window !== 'undefined' && window[localeSymbol]; return { - locale: 'en-US', + locale: locale || 'en-US', direction: 'ltr' }; } diff --git a/packages/@react-spectrum/s2/src/Divider.tsx b/packages/@react-spectrum/s2/src/Divider.tsx index d41e3a0580e..ce33d027e63 100644 --- a/packages/@react-spectrum/s2/src/Divider.tsx +++ b/packages/@react-spectrum/s2/src/Divider.tsx @@ -29,7 +29,7 @@ interface DividerSpectrumProps { */ size?: 'S' | 'M' | 'L', /** - * How thick the Divider should be. + * The orientation of the Divider. * @default 'horizontal' */ orientation?: 'horizontal' | 'vertical', diff --git a/packages/dev/parcel-transformer-s2-icon/IconTransformer.js b/packages/dev/parcel-transformer-s2-icon/IconTransformer.js index 94835e06a85..6eb319b2dc1 100644 --- a/packages/dev/parcel-transformer-s2-icon/IconTransformer.js +++ b/packages/dev/parcel-transformer-s2-icon/IconTransformer.js @@ -82,7 +82,7 @@ function template(asset, svg) { let normalizedPath = asset.filePath.replaceAll('\\', '/'); let fn = asset.pipeline === 'illustration' || normalizedPath.includes('@react-spectrum/s2/spectrum-illustrations') ? 'createIllustration' : 'createIcon'; return ( -` +`"use client"; import {${fn}} from '${normalizedPath.includes('@react-spectrum/s2') ? '~/src/Icon' : '@react-spectrum/s2'}'; ${svg.replace('import { SVGProps } from "react";', '')} diff --git a/packages/dev/s2-docs/pages/react-aria/Draggable.tsx b/packages/dev/s2-docs/pages/react-aria/Draggable.tsx new file mode 100644 index 00000000000..07a4cf27a3a --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Draggable.tsx @@ -0,0 +1,20 @@ +"use client"; +import React from 'react'; +import {useDrag} from '@react-aria/dnd'; + +export function Draggable() { + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world', + 'my-app-custom-type': JSON.stringify({message: 'hello world'}) + }]; + } + }); + + return ( +
+ Drag me +
+ ); +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx new file mode 100644 index 00000000000..2f4cb2e69ea --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React, {JSX} from 'react'; +import type {TextDropItem} from '@react-aria/dnd'; +import {useDrop} from '@react-aria/dnd'; + +interface DroppedItem { + message: string; + style?: 'bold' | 'italic'; +} + +export function DropTarget() { + let [dropped, setDropped] = React.useState(null); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + async onDrop(e) { + let items = await Promise.all( + (e.items as TextDropItem[]) + .filter(item => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type'))) + .map(async item => { + if (item.types.has('my-app-custom-type')) { + return JSON.parse(await item.getText('my-app-custom-type')); + } else { + return {message: await item.getText('text/plain')}; + } + }) + ); + setDropped(items); + } + }); + + let message: JSX.Element[] = [
{`Drop here`}
]; + if (dropped) { + message = dropped.map((d, index) => { + let m = d.message; + if (d.style === 'bold') { + message = [{m}]; + } else if (d.style === 'italic') { + message = [{m}]; + } + return
{message}
; + }); + } + + return ( +
+ {message} +
+ ); +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx new file mode 100644 index 00000000000..52d8a4aac79 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx @@ -0,0 +1,48 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import {GroupedPropTable} from '../../src/PropTable'; +import {FunctionAPI} from '../../src/FunctionAPI'; +import docs from 'docs:@react-aria/focus'; + +export const section = 'Focus'; + +# FocusRing + +{docs.exports.FocusRing.description} + +## Introduction + +`FocusRing` is a utility component that can be used to apply a CSS class when an element has keyboard focus. +This helps keyboard users determine which element on a page or in an application has keyboard focus as they +navigate around. Focus rings are only visible when interacting with a keyboard so as not to distract mouse +and touch screen users. When we are unable to detect if the user is using a mouse or touch screen, such as +switching in from a different tab, we show the focus ring. + +If CSS classes are not being used for styling, see [useFocusRing](useFocusRing.html) for a hooks version. + +## Props + + + +## Example + +This example shows how to use `` to apply a CSS class when keyboard focus is on a button. + +```tsx render files={["packages/dev/s2-docs/pages/react-aria/FocusRingExample.css"]} +'use client'; +import {FocusRing} from '@react-aria/focus'; +import './FocusRingExample.css'; + + + + +``` diff --git a/packages/dev/s2-docs/pages/react-aria/FocusRingExample.css b/packages/dev/s2-docs/pages/react-aria/FocusRingExample.css new file mode 100644 index 00000000000..3d335cd4e5b --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/FocusRingExample.css @@ -0,0 +1,14 @@ +.button { + -webkit-appearance: none; + appearance: none; + background: green; + border: none; + color: white; + font-size: 14px; + padding: 4px 8px; +} + +.button.focus-ring { + outline: 2px solid dodgerblue; + outline-offset: 2px; +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx new file mode 100644 index 00000000000..bf41919a10d --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx @@ -0,0 +1,130 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import {GroupedPropTable} from '../../src/PropTable'; +import {FunctionAPI} from '../../src/FunctionAPI'; +import docs from 'docs:@react-aria/focus'; + +export const section = 'Focus'; + +# FocusScope + +{docs.exports.FocusScope.description} + +## Introduction + +`FocusScope` is a utility component that can be used to manage focus for its descendants. +When the `contain` prop is set, focus is contained within the scope. This is useful when +implementing overlays like modal dialogs, which should not allow focus to escape them while open. +In addition, the `restoreFocus` prop can be used to restore focus back to the previously focused +element when the focus scope unmounts, for example, back to a button which opened a dialog. +A FocusScope can also optionally auto focus the first focusable element within it on mount +when the `autoFocus` prop is set. + +The hook can also be used +in combination with a FocusScope to programmatically move focus within the scope. For example, +arrow key navigation could be implemented by handling keyboard events and using a focus manager +to move focus to the next and previous elements. + +## Props + + + +## FocusManager Interface + +To get a focus manager, call the hook +from a component within the FocusScope. A focus manager supports the following methods: + + + +## Example + +A basic example of a focus scope that contains focus within it is below. Clicking the "Open" +button mounts a FocusScope, which auto focuses the first input inside it. Once open, you can +press the Tab key to move within the scope, but focus is contained inside. Clicking the "Close" +button unmounts the focus scope, which restores focus back to the button. + +{/* Not implemented yet */} +{/* For a full example of building a modal dialog, see [useDialog](useDialog.html). */} + +```tsx render +'use client'; +import React from 'react'; +import {FocusScope} from '@react-aria/focus'; + +function Example() { + let [isOpen, setOpen] = React.useState(false); + return ( + <> + + {isOpen && + + + + + + + + } + + ); +} +``` + +## useFocusManager Example + +This example shows how to use `useFocusManager` to programmatically move focus within a +`FocusScope`. It implements a basic toolbar component, which allows using the left and +right arrow keys to move focus to the previous and next buttons. The `wrap` option is +used to make focus wrap around when it reaches the first or last button. + +```tsx render +'use client'; +import {FocusScope} from '@react-aria/focus'; +import {useFocusManager} from '@react-aria/focus'; + +function Toolbar(props) { + return ( +
+ + {props.children} + +
+ ); +} + +function ToolbarButton(props) { + let focusManager = useFocusManager(); + let onKeyDown = (e) => { + switch (e.key) { + case 'ArrowRight': + focusManager.focusNext({wrap: true}); + break; + case 'ArrowLeft': + focusManager.focusPrevious({wrap: true}); + break; + } + }; + + return ( + + ); +} + + + Cut + Copy + Paste + +``` diff --git a/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx new file mode 100644 index 00000000000..9bdc658f95d --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx @@ -0,0 +1,40 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import {FunctionAPI} from '../../src/FunctionAPI'; +import docs from 'docs:@react-aria/i18n'; + +export const section = 'Internationalization'; +export const description = 'Implementing collections in React Aria'; + + +# I18nProvider + +## Introduction + +`I18nProvider` allows you to override the default locale as determined by the browser/system setting +with a locale defined by your application (e.g. application setting). This should be done by wrapping +your entire application in the provider, which will be cause all child elements to receive the new locale +information via [useLocale](useLocale.html). + +## Props + + + +## Example + +```tsx +import {I18nProvider} from '@react-aria/i18n'; + + + + +``` diff --git a/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx b/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx new file mode 100644 index 00000000000..3e5e733fe74 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx @@ -0,0 +1,25 @@ +'use client'; +import React from 'react'; +import {UNSTABLE_ToastRegion as ToastRegion, UNSTABLE_Toast as Toast, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastContent as ToastContent, Button, Text} from 'react-aria-components'; + +// Define the type for your toast content. +interface MyToastContent { + title: string, + description?: string +} + +export function MyToastRegion({queue}: {queue: ToastQueue}) { + return ( + + {({toast}) => ( + + + {toast.content.title} + {toast.content.description} + + + + )} + + ); +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx new file mode 100644 index 00000000000..c161d7d7b79 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx @@ -0,0 +1,171 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import docs from 'docs:@react-aria/overlays'; +import {FunctionAPI} from '../../src/FunctionAPI'; + +export const section = 'Server Side Rendering'; + +# PortalProvider + +{docs.exports.UNSAFE_PortalProvider.description} + +## Introduction + +`UNSAFE_PortalProvider` is a utility wrapper component that can be used to set where components like +Modals, Popovers, Toasts, and Tooltips will portal their overlay element to. This is typically used when +your app is already portalling other elements to a location other than the `document.body` and thus requires +your React Aria components to send their overlays to the same container. + +Please note that `UNSAFE_PortalProvider` is considered `UNSAFE` because it is an escape hatch, and there are +many places that an application could portal to. Not all of them will work, either with styling, accessibility, +or for a variety of other reasons. Typically, it is best to portal to the root of the entire application, e.g. the `body` element, +outside of any possible overflow or stacking contexts. We envision `UNSAFE_PortalProvider` being used to group all of the portalled +elements into a single container at the root of the app or to control the order of children of the `body` element, but you may have use cases +that need to do otherwise. + +## Props + + + +## Example + +The example below shows how you can use `UNSAFE_PortalProvider` to portal your Toasts to an arbitrary container. Note that +the Toast in this example is taken directly from the [React Aria Components Toast documentation](Toast.html#example), please visit that page for +a detailed explanation of its implementation. + +```tsx render files={["packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx"]} +'use client'; +import React from 'react'; +import {Button} from 'vanilla-starter/Button'; +import {MyToastRegion} from './MyToastRegion.tsx' +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; +import {UNSTABLE_ToastRegion as ToastRegion, UNSTABLE_Toast as Toast, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastContent as ToastContent, Button, Text} from 'react-aria-components'; + +// Define the type for your toast content. +interface MyToastContent { + title: string, + description?: string +} + +// Create a global ToastQueue. +const queue = new ToastQueue(); + +// See the above Toast docs link for the ToastRegion implementation +function App() { + let container = React.useRef(null); + return ( + <> + container.current}> + + + +
+ Toasts are portalled here! +
+ + ); +} + + +``` + +```css render hidden +.react-aria-ToastRegion { + position: unset; + bottom: 16px; + right: 16px; + display: flex; + flex-direction: column-reverse; + gap: 8px; + border-radius: 8px; + outline: none; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } +} + +.react-aria-Toast { + display: flex; + align-items: center; + gap: 16px; + background: var(--highlight-background); + color: white; + padding: 12px 16px; + border-radius: 8px; + outline: none; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + .react-aria-ToastContent { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0px; + + [slot=title] { + font-weight: bold; + } + } + + .react-aria-Button[slot=close] { + flex: 0 0 auto; + background: none; + border: none; + appearance: none; + border-radius: 50%; + height: 32px; + width: 32px; + font-size: 16px; + border: 1px solid var(--highlight-foreground); + color: white; + padding: 0; + outline: none; + + &[data-focus-visible] { + box-shadow: 0 0 0 2px var(--highlight-background), 0 0 0 4px var(--highlight-foreground); + } + + &[data-pressed] { + background: var(--highlight-pressed); + } + } +} + +``` + +## Contexts + +The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSAFE_PortalContext`. This can be +used by custom overlay components to ensure that they are also being consistently portalled throughout your app. + + + +```tsx +import {useUNSAFE_PortalContext} from '@react-aria/overlays'; + +function MyOverlay(props) { + let {children} = props; + let {getContainer} = useUNSAFE_PortalContext(); + return ReactDOM.createPortal(children, getContainer()); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx b/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx new file mode 100644 index 00000000000..37f23f1bbbe --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx @@ -0,0 +1,41 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import docs from 'docs:@react-aria/ssr'; + +export const section = 'Server Side Rendering'; +export const description = 'Implementing collections in React Aria'; + +# SSRProvider + +## Introduction + +If you're using React 16 or 17, `SSRProvider` should be used as a wrapper for the entire application during server side rendering. +It works together with the [useId](useId.html) hook to ensure that auto generated ids are consistent +between the client and server by resetting the id internal counter on each request. +See the [server side rendering](ssr.html) docs for more information. + +**Note**: if you're using React 18 or newer, `SSRProvider` is not necessary and can be removed from your app. React Aria uses the +[React.useId](https://react.dev/reference/react/useId) hook internally when using React 18, which ensures ids are consistent. + +## Props + + + +## Example + +```tsx +import {SSRProvider} from '@react-aria/ssr'; + + + + +``` diff --git a/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx new file mode 100644 index 00000000000..14fb52ee5fe --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx @@ -0,0 +1,67 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import docs from 'docs:@react-aria/visually-hidden'; +import {FunctionAPI} from '../../src/FunctionAPI'; + +export const section = 'Utilities'; + +# VisuallyHidden + +{docs.exports.VisuallyHidden.description} + +## Introduction + +`VisuallyHidden` is a utility component that can be used to hide its children visually, +while keeping them visible to screen readers and other assistive technology. This would +typically be used when you want to take advantage of the behavior and semantics of a +native element like a checkbox or radio button, but replace it with a custom styled +element visually. + +## Props + + + +{/* not implemented yet */} +{/* ## Example + +See [useRadioGroup](useRadioGroup.html#styling) and [useCheckbox](useCheckbox.html#styling) +for examples of using `VisuallyHidden` to hide native form elements visually. */} + +## Hook + +In order to allow additional rendering flexibility, the `useVisuallyHidden` hook can be +used in custom components instead of the `VisuallyHidden` component. It supports the same +options as the component, and returns props to spread onto the element that should be hidden. + + + +```tsx +import {useVisuallyHidden} from '@react-aria/visually-hidden'; + +let {visuallyHiddenProps} = useVisuallyHidden(); + +
I am hidden
+``` + +## Gotchas + +VisuallyHidden is positioned absolutely, so it needs a positioned ancestor. Otherwise, it will be positioned relative to the nearest positioned ancestor, which may be the body, causing undesired scroll bars to appear. + +```tsx + +``` + diff --git a/packages/dev/s2-docs/pages/react-aria/assets/datepicker-rtl.png b/packages/dev/s2-docs/pages/react-aria/assets/datepicker-rtl.png new file mode 100644 index 00000000000..a4447de1f80 Binary files /dev/null and b/packages/dev/s2-docs/pages/react-aria/assets/datepicker-rtl.png differ diff --git a/packages/dev/s2-docs/pages/react-aria/examples/index.mdx b/packages/dev/s2-docs/pages/react-aria/examples/index.mdx index 9fe89b7dee7..96549d6fb18 100644 --- a/packages/dev/s2-docs/pages/react-aria/examples/index.mdx +++ b/packages/dev/s2-docs/pages/react-aria/examples/index.mdx @@ -3,6 +3,7 @@ export default Layout; export const section = 'Getting started'; export const hideFromSearch = true; +export const title = 'Examples'; # Examples diff --git a/packages/dev/s2-docs/pages/react-aria/frameworks.mdx b/packages/dev/s2-docs/pages/react-aria/frameworks.mdx new file mode 100644 index 00000000000..a0569848d18 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/frameworks.mdx @@ -0,0 +1,372 @@ +import {Layout} from '../../src/Layout'; +export default Layout; + +import {InstallCommand} from '../../src/InstallCommand'; +import {Command} from '../../src/Command'; +import {BundlerSwitcher, BundlerSwitcherItem} from '../../src/BundlerSwitcher'; +import {Tabs, TabList, Tab, TabPanel, Text} from '@react-spectrum/s2'; +import {StepList, Step, Counter} from '../../src/Step'; +import Nextjs from '../../src/icons/Nextjs'; +import ReactRouter from '../../src/icons/ReactRouter'; +import Vite from '../../src/icons/Vite'; +import Parcel from '../../src/icons/Parcel'; +import Webpack from '../../src/icons/Webpack'; +import Rollup from '../../src/icons/Rollup'; +import ESBuild from '../../src/icons/Esbuild'; +import Routers from '../../src/routers.mdx'; + +export const section = 'Guides'; +export const tags = ['framework', 'setup', 'routing', 'ssr']; +export const description = 'Integrate React Aria with your framework'; + +# Framework setup + +How to integrate React Aria with your framework. + + + Next.jsReact RouterParcelVitewebpackRollupESBuild + + To integrate with Next.js (app router), ensure the locale on the server matches the client, and configure React Aria links to use the Next.js router. + + + + In your root layout, determine the user's preferred language and set the `lang` and `dir` attributes on the `` element. + + ```tsx + // app/layout.tsx + import {headers} from 'next/headers'; + import {isRTL} from 'react-aria-components'; + import {ClientProviders} from './provider'; + + export default function RootLayout({children}) { + // Get the user's preferred language from the Accept-Language header. + // You could also get this from a database, URL param, etc. + const acceptLanguage = (await headers()).get('accept-language'); + const lang = acceptLanguage?.split(/[,;]/)[0] || 'en-US'; + + return ( + + + + {children} + + + + ); + } + ``` + + + Create `app/provider.tsx`. This should render an `I18nProvider` to set the locale used by React Aria, and a `RouterProvider` to integrate with the Next.js router. + + ```tsx + // app/provider.tsx + "use client"; + + import {useRouter} from 'next/navigation'; + import {RouterProvider, I18nProvider} from 'react-aria-components'; + + // Configure the type of the `routerOptions` prop on all React Aria components. + declare module 'react-aria-components' { + interface RouterConfig { + routerOptions: NonNullable['push']>[1]> + } + } + + export function ClientProviders({lang, children}) { + let router = useRouter(); + + return ( + + + {children} + + + ); + } + ``` + + + + + To integrate with React Router (framework mode), ensure the locale on the server matches the client, configure React Aria links to use client side routing, and exclude localized strings from the client bundle. If you're using declarative mode, choose your bundler above. + + + + Run the following command to reveal [entry.server.tsx](https://remix.run/docs/en/main/file-conventions/entry.server) if you don't already have one. + + + + Make the following changes to `entry.server.tsx`. This will set the locale used by React Aria using the `Accept-Language` HTTP header, and inject the localized strings for the user's language into the HTML. + + ```tsx + // app/entry.server.tsx + import type {EntryContext} from 'react-router'; + import {ServerRouter} from 'react-router'; + import {renderToPipeableStream} from 'react-dom/server'; + /*- begin highlight -*/ + import {I18nProvider} from 'react-aria-components'; + import {getLocalizationScript} from 'react-aria-components/i18n'; + /*- end highlight -*/ + + export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + ) { + // Get the requested language (e.g. from headers, URL param, database, etc.) + /*- begin highlight -*/ + const acceptLanguage = request.headers.get('accept-language'); + const lang = acceptLanguage?.split(/[,;]/)[0] || 'en-US'; + /*- end highlight -*/ + + return new Promise((resolve, reject) => { + const {pipe, abort} = renderToPipeableStream( + {/* Wrap in an I18nProvider to set locale used by React Aria. */} + {/*- begin highlight -*/} + + {/*- end highlight -*/} + + , + { + /*- begin highlight -*/ + // Inject localized strings into the HTML. + bootstrapScriptContent: getLocalizationScript(lang), + /*- end highlight -*/ + // ... + } + ); + }); + } + ``` + + + In your root layout, set the `lang` and `dir` attributes on the `` element, and render a `RouterProvider` to configure React Aria links to use React Router. + + ```tsx + // app/root.tsx + import {useLocale} from 'react-aria-components'; + import {useNavigate, useHref, type NavigateOptions} from 'react-router'; + + /*- begin highlight -*/ + // Configure the type of the `routerOptions` prop on all React Aria components. + declare module 'react-aria-components' { + interface RouterConfig { + routerOptions: NavigateOptions + } + } + /*- end highlight -*/ + + export function Layout({children}) { + /*- begin highlight -*/ + let {locale, direction} = useLocale(); + let navigate = useNavigate(); + /*- end highlight -*/ + + return ( + /*- begin highlight -*/ + + {/*- end highlight -*/} + + {/* ... */} + + + {/*- begin highlight -*/} + + {/*- end highlight -*/} + {children} + + {/* ... */} + + + ); + } + ``` + + + Install the locale optimization bundler plugin. This will omit the localized strings from the JavaScript bundle. The strings for the user's language are injected into the HTML instead. + + + + Edit `vite.config.ts` to add the plugin. + + ```ts + // vite.config.ts + import {reactRouter} from "@react-router/dev/vite"; + import {defineConfig} from 'vite'; + /*- begin highlight -*/ + import localesPlugin from '@react-aria/optimize-locales-plugin'; + /*- end highlight -*/ + + export default defineConfig({ + plugins: [ + reactRouter(), + // Don't include any locale strings in the client JS bundle. + /*- begin highlight -*/ + {...localesPlugin.vite({locales: []}), enforce: 'pre'} + /*- end highlight -*/ + ], + }); + ``` + + + + + To integrate with a client-only Parcel SPA, configure React Aria links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + + + + By default, React Aria includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + + Add the following to your `.parcelrc`. + + ```json + { + "extends": "@parcel/config-default", + "resolvers": ["@react-aria/parcel-resolver-optimize-locales", "..."] + } + ``` + + + In your project root `package.json`, add a `"locales"` field containing the languages you want to support. + + ```json + { + "locales": ["en-US", "fr-FR"] + } + ``` + + + + + To integrate with a client-only Vite SPA, configure React Aria links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + + + + By default, React Aria includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + + Edit `vite.config.ts` to add the plugin, and configure the `locales` parameter. + + ```ts + // vite.config.ts + import {defineConfig} from 'vite'; + import localesPlugin from '@react-aria/optimize-locales-plugin'; + + export default defineConfig({ + plugins: [ + { + ...optimizeLocales.vite({ + locales: ['en-US', 'fr-FR'] + }), + enforce: 'pre' + } + ], + }); + ``` + + + + + To integrate with a client-only webpack SPA, configure React Aria links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + + + + By default, React Aria includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + + Edit `webpack.config.ts` to add the plugin, and configure the `locales` parameter. + + ```ts + // webpack.config.js + const optimizeLocales = require('@react-aria/optimize-locales-plugin'); + + module.exports = { + // ... + plugins: [ + optimizeLocales.webpack({ + locales: ['en-US', 'fr-FR'] + }) + ] + }; + ``` + + + + + To integrate with a client-only Rollup SPA, configure React Aria links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + + + + By default, React Aria includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + + Edit `rollup.config.js` to add the plugin, and configure the `locales` parameter. + + ```ts + // rollup.config.js + import optimizeLocales from '@react-aria/optimize-locales-plugin'; + + export default { + // ... + plugins: [ + optimizeLocales.rollup({ + locales: ['en-US', 'fr-FR'] + }) + ] + }; + ``` + + + + + To integrate with a client-only ESBuild SPA, configure React Aria links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + + + + By default, React Aria includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + + Edit your build script to add the plugin, and configure the `locales` parameter. + + ```ts + import {build} from 'esbuild'; + import optimizeLocales from '@react-aria/optimize-locales-plugin'; + + build({ + // ... + plugins: [ + optimizeLocales.esbuild({ + locales: ['en-US', 'fr-FR'] + }) + ] + }); + ``` + + + + diff --git a/packages/dev/s2-docs/pages/react-aria/getting-started.mdx b/packages/dev/s2-docs/pages/react-aria/getting-started.mdx new file mode 100644 index 00000000000..37f581a7c33 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/getting-started.mdx @@ -0,0 +1,186 @@ +import {Layout} from '../../src/Layout'; +export default Layout; + +import {InstallCommand} from '../../src/InstallCommand'; +import {Command} from '../../src/Command'; +import {BundlerSwitcher, BundlerSwitcherItem} from '../../src/BundlerSwitcher'; +import {StepList, Step, Counter} from '../../src/Step'; +import {ShadcnCommand} from '../../src/ShadcnCommand'; +import {StarterKits} from '../../src/StarterKits'; +import docs from 'docs:react-aria-components'; +import '../../tailwind/tailwind.css'; + +export const section = 'Getting started'; +export const tags = ['introduction', 'installation']; +export const description = 'Install and build your first component'; + +# Getting started + +How to install React Aria and build your first component. + +## Install + +Install React Aria with your preferred package manager. + + + +## Quick start + +The documentation for each component includes vanilla CSS and [Tailwind](https://tailwindcss.com) examples. Copy and paste these into your project and make them your own. You can also download each example as a ZIP or open in CodeSandbox or StackBlitz. + + + ```tsx render docs={docs.exports.Select} links={docs.links} props={[]} type="vanilla" files={["starters/docs/src/Select.tsx", "starters/docs/src/Select.css"]} showCoachMark + "use client"; + import {Select, SelectItem} from 'vanilla-starter/Select'; + + + ``` + + ```tsx render docs={docs.exports.Select} links={docs.links} props={[]} type="tailwind" files={["starters/tailwind/src/Select.tsx"]} showCoachMark + "use client"; + import {Select, SelectItem} from 'tailwind-starter/Select'; + + + ``` + + + +### shadcn CLI + +Each example can also be installed with the [shadcn](https://ui.shadcn.com/docs/cli) CLI. This will add the component source code, styles, and all dependencies to your project automatically. + + + +### Storybook starter kits + +If you're building a full component library, download a pre-built [Storybook](https://storybook.js.org/) starter kit. These include every component in a standalone development environment. + + + +### Working with AI + +Use the menu at the top of each page in the docs to open or copy it into your favorite AI assistant. We also have an [MCP server](mcp.html) which can be used directly in your IDE, and [llms.txt](../llms.txt) which can help AI agents navigate the docs. + +## Build a component from scratch + +In this tutorial, we'll build a custom [Select](Select.html) component. + + + + ### Import and assemble the parts + + Each React Aria component renders a single DOM element. Complex components like `Select` compose together multiple parts to build a complete pattern. + + ```tsx + import {Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue} from 'react-aria-components'; + + + ``` + + + ### Add your styles + + React Aria does not include any styles by default, allowing you to build custom designs to fit your application or design system. You can use any styling solution, including vanilla CSS, Tailwind CSS, CSS-in-JS, etc. + + Each React Aria component includes a default class name to use in your CSS, and data attributes for states such as pressed, hovered, selected, etc. + + ```css + .react-aria-ListBoxItem { + color: black; + + &[data-selected] { + background: black; + color: white; + } + } + ``` + + You can also override these defaults with a custom `className` prop, and access states via render props. Check out our [styling guide](styling.html) to learn more. + + + + ### Create a reusable component + + To reuse styles throughout your project, wrap all of the parts into a reusable component. Create your own API by extending React Aria's types with additional props. Components such as Popover can also be shared with other patterns so they don't need be styled separately each time. + + ```tsx + import type {SelectProps as AriaSelectProps, ListBoxItemProps} from 'react-aria-components'; + import {Select as AriaSelect, Button, Label, ListBox, ListBoxItem, SelectValue} from 'react-aria-components'; + import {Popover} from './Popover'; + import './Select.css'; + + export interface SelectProps extends AriaSelectProps { + label?: string + } + + export function Select(props: SelectProps) { + return ( + + + + + + {props.children} + + + + ); + } + + export function SelectItem(props: ListBoxItemProps) { + return ; + } + ``` + + + ### Use your component + + Now you can render a consistently styled ` + Cat + Dog + Kangaroo + + ); + } + ``` + + + +## Framework setup + +React Aria works out of the box in any React framework. When you're ready, follow our [framework setup](frameworks.html) guide to optimize the bundle size, configure internationalization, and integrate with client side routers. diff --git a/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx new file mode 100644 index 00000000000..fee98fa10a5 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx @@ -0,0 +1,70 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../src/Layout'; +export default Layout; +import docs from 'docs:@react-aria/utils'; +import {FunctionAPI} from '../../src/FunctionAPI'; + +export const section = 'Utilities'; + +# mergeProps + +{docs.exports.mergeProps.description} + +## API + + + +## Introduction + +`mergeProps` is a utility function that combines multiple props objects together in a smart way. +This can be useful when you need to combine the results of multiple react-aria hooks onto a single +element. For example, both hooks may return the same event handlers, class names, or ids, and only +one of these can be applied. `mergeProps` handles combining these props together so that multiple +event handlers will be chained, multiple classes will be combined, and ids will be deduplicated. +For all other props, the last prop object overrides all previous ones. + +## Example + +```tsx +import {mergeProps} from '@react-aria/utils'; + +let a = { + className: 'foo', + onKeyDown(e) { + if (e.key === 'Enter') { + console.log('enter') + } + } +}; + +let b = { + className: 'bar', + onKeyDown(e) { + if (e.key === ' ') { + console.log('space') + } + } +}; + +let merged = mergeProps(a, b); +``` + +The result of the above example will be equivalent to this: + +```tsx +let merged = { + className: 'foo bar', + onKeyDown(e) { + a.onKeyDown(e); + b.onKeyDown(e); + } +}; +``` diff --git a/packages/dev/s2-docs/pages/react-aria/releases/index.mdx b/packages/dev/s2-docs/pages/react-aria/releases/index.mdx new file mode 100644 index 00000000000..cb881be4e06 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/releases/index.mdx @@ -0,0 +1,20 @@ +{/* Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../../src/Layout'; +import {ReleasesList} from '../../../src/ReleasesList'; +export default Layout; + +export const section = 'Releases'; +export const tags = ['changelog', 'versions', 'updates']; +export const title = 'Releases'; + +# Releases + + page.name.includes('react-aria') && page.name.includes('releases') && !page.name.includes('index.html')) ?? []} /> diff --git a/packages/dev/s2-docs/pages/react-aria/releases/v1-0-0.mdx b/packages/dev/s2-docs/pages/react-aria/releases/v1-0-0.mdx new file mode 100644 index 00000000000..738e42604bd --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/releases/v1-0-0.mdx @@ -0,0 +1,199 @@ +{/* Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '../../../src/Layout'; +export default Layout; + +import docs from 'docs:@react-spectrum/s2'; +import {Time} from '../../../src/ReleasesList'; + +export const section = ''; +export const tags = ['release', 'React Aria']; +export const date = 'December 20, 2023'; +export const title = 'v1.0.0'; +export const description = 'In this release, we\'re promoting React Aria Components from RC to GA 🎉, optimizing our included translation files, shipping a storybook based on all our React Aria Components with a starter theme, and fixing plenty of bugs.'; +export const isSubpage = true; + +# v1.0.0 +