From 42e64d99995353a091ba14c18b1099d0ddb0b6ce Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:33:50 -0700 Subject: [PATCH 01/14] docs: add non-component hook docs --- .../dev/s2-docs/pages/react-aria/useFocus.mdx | 85 ++++++++++ .../pages/react-aria/useFocusVisible.mdx | 78 +++++++++ .../pages/react-aria/useFocusWithin.mdx | 102 ++++++++++++ .../dev/s2-docs/pages/react-aria/useHover.mdx | 114 ++++++++++++++ .../s2-docs/pages/react-aria/useKeyboard.mdx | 83 ++++++++++ .../s2-docs/pages/react-aria/useLandmark.mdx | 109 +++++++++++++ .../s2-docs/pages/react-aria/useLongPress.mdx | 113 +++++++++++++ .../dev/s2-docs/pages/react-aria/useMove.mdx | 149 ++++++++++++++++++ .../dev/s2-docs/pages/react-aria/usePress.mdx | 112 +++++++++++++ packages/dev/s2-docs/src/FunctionAPI.tsx | 34 ++++ 10 files changed, 979 insertions(+) create mode 100644 packages/dev/s2-docs/pages/react-aria/useFocus.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useHover.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useLandmark.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useLongPress.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useMove.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/usePress.mdx create mode 100644 packages/dev/s2-docs/src/FunctionAPI.tsx diff --git a/packages/dev/s2-docs/pages/react-aria/useFocus.mdx b/packages/dev/s2-docs/pages/react-aria/useFocus.mdx new file mode 100644 index 00000000000..7d750bb41f3 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useFocus.mdx @@ -0,0 +1,85 @@ +{/* 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/interactions'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useFocus + + +## API + + + +## Features + +`useFocus` handles focus interactions for an element. Unlike React's built-in focus events, +`useFocus` does not fire focus events for child elements of the target. This matches DOM +behavior where focus events do not bubble. This is similar to +the [:focus](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus) pseudo class +in CSS. + +To handle focus events on descendants of an element, see [useFocusWithin](useFocusWithin.html). + +## Usage + +`useFocus` returns props that you should spread onto the target element: + + + +`useFocus` supports the following props and event handlers: + + + +## Example + +This example shows a simple input element that handles focus events with `useFocus` and logs them to a list below. + +```tsx render +"use client"; + +import React from 'react'; +import {useFocus} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let {focusProps} = useFocus({ + onFocus: e => setEvents( + events => [...events, 'focus'] + ), + onBlur: e => setEvents( + events => [...events, 'blur'] + ), + onFocusChange: isFocused => setEvents( + events => [...events, `focus change: ${isFocused}`] + ) + }); + + return ( + <> + + +
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx new file mode 100644 index 00000000000..37934263056 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx @@ -0,0 +1,78 @@ +{/* 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/interactions'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + + +# useFocusVisible + +## API + + + +## Features + +`useFocusVisible` handles focus interactions for the page and determines whether keyboard focus +should be visible (e.g. with a focus ring). Focus visibility is computed based on the current +interaction mode of the user. When the user interacts via a mouse or touch, then focus is not +visible. When the user interacts via a keyboard or screen reader, then focus is visible. This is similar to +the [:focus-visible](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo class +in CSS. + +To determine whether a focus ring should be visible for an individual component rather than +globally, see [useFocusRing](useFocusRing.html). + +## Usage + +`useFocusVisible` returns props that you should spread onto the target element: + + + +`useFocusVisible` supports the following props and event handlers: + + + + +## Example + +This example shows focus visible state and updates as you interact with the page. By default, +when the page loads, it is true. If you press anywhere on the page with a mouse or touch, +then focus visible state is set to false. If you keyboard navigate around the page then +it is set to true again. + +Note that this example uses the `isTextInput` option so that only certain navigation keys +cause focus visible state to appear. This prevents focus visible state from appearing when +typing text in a text field. + +```tsx render +"use client"; +import {useFocusVisible} from '@react-aria/interactions'; + +function Example() { + let {isFocusVisible} = useFocusVisible({isTextInput: true}); + + return ( + <> +
Focus visible: {String(isFocusVisible)}
+ + + + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx new file mode 100644 index 00000000000..1fe95217d2a --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx @@ -0,0 +1,102 @@ +{/* 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/interactions'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useFocusWithin + +## API + + + +## Features + +`useFocusWithin` handles focus interactions for an element and its descendants. Focus is "within" +an element when either the element itself or a descendant element has focus. This is similar to +the [:focus-within](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within) pseudo class +in CSS. + +To handle focus events on only the target element, and not descendants, see [useFocus](useFocus.html). + +## Usage + +`useFocusWithin` returns props that you should spread onto the target element: + + + +`useFocusWithin` supports the following event handlers: + + + + +## Example + +This example shows two text fields inside a div, which handles focus within events. It stores focus +within state in local component state, which is updated by an `onFocusWithinChange` handler. This is used +to update the background color and text color of the group while one of the text fields has focus. + +Focus within and blur within events are also logged to the list below. Notice that the events are only +fired when the wrapper gains and loses focus, not when focus moves within the group. + +{/* We don't have component hook docs yet */} +{/* **NOTE: for more advanced text field functionality, see [useTextField](useTextField.html).** */} + +```tsx render +"use client"; +import React from 'react'; +import {useFocusWithin} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let [isFocusWithin, setFocusWithin] = React.useState(false); + let {focusWithinProps} = useFocusWithin({ + onFocusWithin: e => setEvents( + events => [...events, 'focus within'] + ), + onBlurWithin: e => setEvents( + events => [...events, 'blur within'] + ), + onFocusWithinChange: isFocusWithin => setFocusWithin(isFocusWithin) + }); + + return ( + <> +
+ + +
+
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useHover.mdx b/packages/dev/s2-docs/pages/react-aria/useHover.mdx new file mode 100644 index 00000000000..54e735d53a8 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useHover.mdx @@ -0,0 +1,114 @@ +{/* 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/interactions'; +import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + + +# useHover + +## API + + + +## Features + +`useHover` handles hover interactions for an element. A hover interaction begins when a user moves their pointer +over an element, and ends when they move their pointer off of the element. + +* Uses pointer events where available, with fallbacks to mouse and touch events +* Ignores emulated mouse events in mobile browsers + +`useHover` is similar to the [:hover](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) pseudo class in CSS, +but `:hover` is problematic on touch devices due to mouse emulation in mobile browsers. Depending on the browser +and device, `:hover` may never apply, or may apply continuously until the user touches another element. +`useHover` only applies when the pointer is truly capable of hovering, and emulated mouse events are ignored. + +Read our [blog post](/blog/building-a-button-part-2.html) about the complexities of hover event handling to learn more. + +## Usage + +`useHover` returns props that you should spread onto the target element: + + + +`useHover` supports the following props: + + + +Each of these handlers is fired with a `HoverEvent`, which exposes information about the target and the +type of event that triggered the interaction. + + + + +## Accessibility + +Hover interactions should never be the only way to interact with an element because they are not +supported across all devices. Alternative interactions should be provided on touch devices, for +example a long press or an explicit button to tap. + +In addition, even on devices with hover support, users may be using a keyboard or screen reader +to navigate your app, which also do not trigger hover events. Hover interactions should be paired +with focus events in order to expose the content to keyboard users. + +## Example + +This example shows a simple target that handles hover events with `useHover` and logs them to a +list below. It also uses the `isHovered` state to adjust the background +color when the target is hovered. + +```tsx render +"use client"; +import React from 'react'; +import {useHover} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let {hoverProps, isHovered} = useHover({ + onHoverStart: e => setEvents( + events => [...events, `hover start with ${e.pointerType}`] + ), + onHoverEnd: e => setEvents( + events => [...events, `hover end with ${e.pointerType}`] + ) + }); + + return ( + <> +
+ Hover me! +
+
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx new file mode 100644 index 00000000000..62c969e7a87 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx @@ -0,0 +1,83 @@ +{/* 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/interactions'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useKeyboard + +## API + + + +## Features + +`useKeyboard` handles keyboard interactions. The only difference from DOM events is that propagation +is stopped by default if there is an event handler, unless `event.continuePropagation()` is called. +This provides better modularity by default, so that a parent component doesn't respond to an event +that a child already handled. If the child doesn't handle the event (e.g. it was for an unknown key), +it can call `event.continuePropagation()` to allow parents to handle the event. + +## Usage + +`useKeyboard` returns props that you should spread onto the target element: + + + +`useKeyboard` supports the following props: + + + + +## Example + +This example shows a simple input element that handles keyboard events with `useKeyboard` and logs them to a list below. + +{/* Components hooks have not been written yet */} +{/* **NOTE: for more advanced text field functionality, see [useTextField](useTextField.html).** */} + +```tsx render +"use client" +import React from 'react'; +import {useKeyboard} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let {keyboardProps} = useKeyboard({ + onKeyDown: e => setEvents( + events => [`key down: ${e.key}`, ...events] + ), + onKeyUp: e => setEvents( + events => [`key up: ${e.key}`, ...events] + ) + }); + + return ( + <> + + +
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx b/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx new file mode 100644 index 00000000000..d64b1325711 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx @@ -0,0 +1,109 @@ +{/* 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/landmark'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useLandmark + +## API + + + +## Features + +Landmarks provide a way to designate important subsections of a page. They allow screen reader users to get an overview of the various sections of the page, and jump to a specific section. +By default, browsers do not provide a consistent way to navigate between landmarks using the keyboard. +The `useLandmark` hook enables keyboard navigation between landmarks, and provides a consistent experience across browsers. + +* F6 and Shift+F6 key navigation between landmarks +* Alt+F6 key navigation to the main landmark +* Support for navigating nested landmarks + +## Anatomy + +Landmark elements can be registered with the `useLandmark` hook. The `role` prop is required. + +Pressing F6 will move focus to the next landmark on the page, and pressing Shift+F6 will move focus to the previous landmark. +If an element within a landmark was previously focused before leaving that landmark, focus will return to that element when navigating back to that landmark. +Alt+F6 will always move focus to the main landmark if it has been registered. + +If multiple landmarks are registered with the same role, they should have unique labels, which can be provided by aria-label or aria-labelledby. + +{/* Not implemented yet */} +{/* For an example of landmarks in use, see the [useToastRegion](useToast.html#anatomy) documentation. */} + +## Usage + +`useLandmark` returns props that you should spread onto the target element: + + + +`useLandmark` supports the following props: + + + +## Example + +```tsx render +"use client"; +import {useLandmark} from '@react-aria/landmark'; +import {useRef} from 'react'; + +function Navigation(props) { + let ref = useRef(null); + let {landmarkProps} = useLandmark({...props, role: 'navigation'}, ref); + return ( + + ); +} + +function Region(props) { + let ref = useRef(null); + let {landmarkProps} = useLandmark({...props, role: 'region'}, ref); + return ( +
+ {props.children} +
+ ); +} + +function Search(props) { + let ref = useRef(null); + let {landmarkProps} = useLandmark({...props, role: 'search'}, ref); + return ( +
+

Search

+ +
+ ); +} + +
+ +

Navigation

+ +
+ + +

Region

+

Example region with no focusable children.

+
+
+``` diff --git a/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx b/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx new file mode 100644 index 00000000000..1fe7623fa34 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx @@ -0,0 +1,113 @@ +{/* 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/interactions'; +import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useLongPress + +## API + + + +## Features + +`useLongPress` handles long press interactions across both mouse and touch devices. A long press is triggered when a user presses +and holds their pointer over a target for a minimum period of time. If the user moves their pointer off of the target before the +time threshold, the interaction is canceled. Once a long press event is triggered, other pointer interactions that may be active +such as `usePress` and `useMove` will be canceled so that only the long press is activated. + +* Handles mouse and touch events +* Uses pointer events where available, with fallbacks to mouse and touch events +* Ignores emulated mouse events in mobile browsers +* Prevents text selection on touch devices while long pressing +* Prevents browser and OS context menus from appearing while long pressing +* Customizable time threshold for long press +* Supports an accessibility description to indicate to assistive technology users that a long press action is available + +## Usage + +`useLongPress` returns props that you should spread onto the target element: + + + +`useLongPress` supports the following event handlers and options: + + + +Each of these handlers is fired with a `LongPressEvent`, which exposes information about the target and the +type of event that triggered the interaction. + + + +## Example + +This example shows a button that has both a normal press action using [usePress](usePress.html), as well as a long +press action using `useLongPress`. Pressing the button will set the mode to "Normal speed", and long pressing it will +set the mode to "Hyper speed". All of the emitted events are also logged below. Note that when long pressing the button, +only a long press is emitted, and no normal press is emitted on pointer up. + +**Note**: this example does not have a keyboard accessible way to trigger the long press action. Because the method of triggering +this action will differ depending on the component, it is outside the scope of `useLongPress`. Make sure to implement a keyboard +friendly alternative to all long press interactions if you are using this hook directly. + +```tsx render +"use client"; +import React from 'react'; +import {mergeProps} from '@react-aria/utils'; +import {useLongPress, usePress} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let [mode, setMode] = React.useState('Activate'); + let {longPressProps} = useLongPress({ + accessibilityDescription: 'Long press to activate hyper speed', + onLongPressStart: e => setEvents( + events => [`long press start with ${e.pointerType}`, ...events] + ), + onLongPressEnd: e => setEvents( + events => [`long press end with ${e.pointerType}`, ...events] + ), + onLongPress: e => { + setMode('Hyper speed'); + setEvents( + events => [`long press with ${e.pointerType}`, ...events] + ); + } + }); + + let {pressProps} = usePress({ + onPress: e => { + setMode('Normal speed'); + setEvents( + events => [`press with ${e.pointerType}`, ...events] + ); + } + }); + + return ( + <> + +
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useMove.mdx b/packages/dev/s2-docs/pages/react-aria/useMove.mdx new file mode 100644 index 00000000000..28deef659b6 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useMove.mdx @@ -0,0 +1,149 @@ +{/* 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/interactions'; +import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useMove + +## API + + + +## Features + +`useMove` handles move interactions across mouse, touch, and keyboard. A move interaction starts when a +user moves after pressing down with a mouse or their finger on the target, and ends when they lift their pointer. Move +events are fired as the pointer moves around, and specify the distance that the pointer traveled since the last +event. In addition, after a user focuses the target element, move events are fired when the user presses the arrow keys. + +* Handles mouse and touch events +* Handles arrow key presses +* Uses pointer events where available, with fallbacks to mouse and touch events +* Ignores emulated mouse events in mobile browsers +* Handles disabling text selection on mobile while the press interaction is active +* Normalizes many cross browser inconsistencies + +## Usage + +`useMove` returns props that you should spread onto the target element: + + + +`useMove` supports the following event handlers: + + + +Each of these handlers is fired with a `MoveEvent`, which exposes information about the target and the +type of event that triggered the interaction. + + + +## Example + +This example shows a ball that can be moved by dragging with a mouse or touch, or by tabbing to it and using +the arrow keys on your keyboard. The movement is clamped so that the ball cannot be dragged outside a box. +In addition, all of the move events are logged below so that you can inspect what is going on. + +```tsx render +"use client"; +import React from 'react'; +import {useMove} from '@react-aria/interactions'; + +function Example() { + const CONTAINER_SIZE = 200; + const BALL_SIZE = 30; + + let [events, setEvents] = React.useState([]); + let [color, setColor] = React.useState('black'); + let [position, setPosition] = React.useState({ + x: 0, + y: 0 + }); + + let clamp = pos => Math.min(Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE); + let {moveProps} = useMove({ + onMoveStart(e) { + setColor('red'); + setEvents(events => [`move start with pointerType = ${e.pointerType}`, ...events]); + }, + onMove(e) { + setPosition(({x, y}) => { + // Normally, we want to allow the user to continue + // dragging outside the box such that they need to + // drag back over the ball again before it moves. + // This is handled below by clamping during render. + // If using the keyboard, however, we need to clamp + // here so that dragging outside the container and + // then using the arrow keys works as expected. + if (e.pointerType === 'keyboard') { + x = clamp(x); + y = clamp(y); + } + + x += e.deltaX; + y += e.deltaY; + return {x, y}; + }); + + setEvents(events => [`move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`, ...events]); + }, + onMoveEnd(e) { + setPosition(({x, y}) => { + // Clamp position on mouse up + x = clamp(x); + y = clamp(y); + return {x, y}; + }); + setColor('black'); + setEvents(events => [`move end with pointerType = ${e.pointerType}`, ...events]); + } + }); + + return ( + <> +
+
+
+
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/usePress.mdx b/packages/dev/s2-docs/pages/react-aria/usePress.mdx new file mode 100644 index 00000000000..217bffcb533 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/usePress.mdx @@ -0,0 +1,112 @@ +{/* 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/interactions'; +import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# usePress + +## API + + + +## Features + +`usePress` handles press interactions across mouse, touch, keyboard, and screen readers. A press interaction starts when a +user presses down with a mouse or their finger on the target, and ends when they move the pointer off the target. It may +start again if the pointer re-enters the target. `usePress` returns the current press state, which can be used to adjust +the visual appearance of the target. If the pointer is released over the target, then an `onPress` event is fired. + +* Handles mouse and touch events +* Handles Enter or Space key presses +* Handles screen reader virtual clicks +* Uses pointer events where available, with fallbacks to mouse and touch events +* Normalizes focus behavior on mouse and touch interactions across browsers +* Handles disabling text selection on mobile while the press interaction is active +* Handles canceling press interactions on scroll +* Normalizes many cross browser inconsistencies + +Read our [blog post](/blog/building-a-button-part-1.html) about the complexities of press event handling to learn more. + +## Usage + +`usePress` returns props that you should spread onto the target element, along with the current press state: + + + +`usePress` supports the following event handlers: + + + +Each of these handlers is fired with a `PressEvent`, which exposes information about the target and the +type of event that triggered the interaction. + + + +## Example + +This example shows a simple target that handles press events with `usePress` and logs them to a list below. +It also uses the `isPressed` state to adjust the background color when the target is pressed. +Press down on the target and drag your pointer off and over to see when the events are fired, and try focusing +the target with a keyboard and pressing the Enter or Space keys to trigger events as well. + +{/* Not implemented yet */} +{/* **NOTE: for more advanced button functionality, see [useButton](useButton.html).** */} + +```tsx render +"use client"; +import React from 'react'; +import {usePress} from '@react-aria/interactions'; + +function Example() { + let [events, setEvents] = React.useState([]); + let {pressProps, isPressed} = usePress({ + onPressStart: e => setEvents( + events => [...events, `press start with ${e.pointerType}`] + ), + onPressEnd: e => setEvents( + events => [...events, `press end with ${e.pointerType}`] + ), + onPress: e => setEvents( + events => [...events, `press with ${e.pointerType}`] + ) + }); + + return ( + <> +
+ Press me! +
+
    + {events.map((e, i) =>
  • {e}
  • )} +
+ + ); +} +``` diff --git a/packages/dev/s2-docs/src/FunctionAPI.tsx b/packages/dev/s2-docs/src/FunctionAPI.tsx new file mode 100644 index 00000000000..c0b0ae609c3 --- /dev/null +++ b/packages/dev/s2-docs/src/FunctionAPI.tsx @@ -0,0 +1,34 @@ +/* + * 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 {Indent, JoinList, Type, TypeParameters, setLinks} from './types'; +import {styles as codeStyles} from './Code'; +import React from 'react'; + +export function FunctionAPI({function: func, links}) { + let {name, parameters, return: returnType, typeParameters} = func; + + if (links) { + setLinks(links) + } + return ( + + {name} + + + + + {': '} + + + ); +} \ No newline at end of file From c76fdf656b3b917e4e19fb16eaf82c4a8feffa5b Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:38:00 -0700 Subject: [PATCH 02/14] add useClipboard --- .../s2-docs/pages/react-aria/useClipboard.mdx | 481 ++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 packages/dev/s2-docs/pages/react-aria/useClipboard.mdx diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx new file mode 100644 index 00000000000..015c3f7b50f --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx @@ -0,0 +1,481 @@ +{/* 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/dnd'; +export const section = 'Hooks'; +export const description = 'Implementing collections in React Aria'; + +# useClipboard + +## API + + + +## Introduction + +Copy and paste is a common way to transfer data between locations, either within or between apps. Browsers support copy and paste of selected text content by default, but rich objects with custom data can also be copied and pasted using the [clipboard events](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent) API. For example, an app could support copying and pasting a selected card representing a rich object to a new location, or allow a user to paste files from their device to upload them. This can provide a keyboard accessible alternative to drag and drop. + +The hook provides a simple way to implement copy and paste for a focusable element. When focused, users can press keyboard shortcuts like ⌘C and ⌘V, or even use the browser's "Copy" and "Paste" menu commands, to trigger clipboard events. Multiple items can be copied and pasted at once, each represented in one or more different data formats. Because it uses native browser APIs under the hood, copy and paste uses the operating system clipboard, which means it works between applications (e.g. Finder, Windows Explorer, a native email app, etc.) in addition to within the app. + +## Example + +This example shows a simple focusable element which supports copying a string when focused, and another element which supports pasting plain text. + +```tsx render +"use client"; +import React from 'react'; +import type {TextDropItem} from '@react-aria/dnd'; +import {useClipboard} from '@react-aria/dnd'; + +function Copyable() { + let {clipboardProps} = useClipboard({ + getItems() { + return [{ + 'text/plain': 'Hello world' + }]; + } + }); + + return ( +
+ Hello world + ⌘C +
+ ); +} + +function Pasteable() { + let [pasted, setPasted] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + let pasted = await Promise.all( + items + .filter((item) => + item.kind === 'text' && item.types.has('text/plain') + ) + .map((item: TextDropItem) => item.getText('text/plain')) + ); + setPasted(pasted.join('\n')); + } + }); + + return ( +
+ {pasted || 'Paste here'} + ⌘V +
+ ); +} + + + +``` + +
+ Show CSS + +```css +[role=textbox] { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--spectrum-global-color-gray-50); + border: 1px solid var(--spectrum-global-color-gray-400); + padding: 10px; + margin-right: 20px; + border-radius: 8px; +} + +[role=textbox]:focus { + outline: none; + border-color: var(--blue); + box-shadow: 0 0 0 1px var(--blue); +} + +[role=textbox] kbd { + display: inline-block; + margin-left: 10px; + padding: 0 4px; + background: var(--spectrum-global-color-gray-100); + border: 1px solid var(--spectrum-global-color-gray-300); + border-radius: 4px; + font-size: small; + letter-spacing: .2em; +} +``` + +
+ +## Copy data + +Data to copy can be provided in multiple formats at once. This allows the destination where the user pastes to choose the data that it understands. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user pastes in an external application (e.g. an email message). + +This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard [mime type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) for interoperability with external applications, or a custom string for use within your own app. + +In addition to providing items in multiple formats, you can also return multiple drag items from `getItems` to transfer multiple objects in a single copy and paste operation. + +This example copies two items, each of which contains representations as plain text, HTML, and a custom app-specific data format. Pasting on the target will use the custom data format to render formatted items. If you paste in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. + +```tsx render +"use client" +function Copyable() { + let {clipboardProps} = useClipboard({ + getItems() { + return [{ + 'text/plain': 'hello world', + 'text/html': 'hello world', + 'my-app-custom-type': JSON.stringify({ + message: 'hello world', + style: 'bold' + }) + }, { + 'text/plain': 'foo bar', + 'text/html': 'foo bar', + 'my-app-custom-type': JSON.stringify({ + message: 'foo bar', + style: 'italic' + }) + }]; + } + }); + + return ( +
+
+
hello world
+
foo bar
+
+ ⌘C +
+ ); +} +/*- begin collapse -*/ +function Pasteable() { + let [pasted, setPasted] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + let pasted = await Promise.all( + items + .filter(item => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type'))) + .map(async (item: TextDropItem) => { + 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')}; + } + }) + ); + setPasted(pasted); + } + }); + + let message = ['Paste here']; + if (pasted) { + message = pasted.map(d => { + let message = d.message; + if (d.style === 'bold') { + message = {message}; + } else if (d.style === 'italic') { + message = {message}; + } + return
{message}
; + }); + } + + return ( +
+
{message || 'Paste here'}
+ ⌘V +
+ ); +} + + + +/*- end collapse -*/ +``` + +## Paste data + +`useClipboard` allows users to paste one or more items, each of which contains data to be pasted. There are three kinds of items: + +* `text` – represents data inline as a string in one or more formats +* `file` – references a file on the user's device +* `directory` – references the contents of a directory + +### Text + +A represents textual data in one or more different formats. These may be either standard [mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. + +The example below works with the above `Copyable` example using a custom app-specific data format to transfer rich data. If no such data is available, it falls back to pasting plain text data. + +```tsx render +"use client" +/*- begin collapse -*/ +function Copyable() { + let {clipboardProps} = useClipboard({ + getItems() { + return [{ + 'text/plain': 'hello world', + 'text/html': 'hello world', + 'my-app-custom-type': JSON.stringify({ + message: 'hello world', + style: 'bold' + }) + }, { + 'text/plain': 'foo bar', + 'text/html': 'foo bar', + 'my-app-custom-type': JSON.stringify({ + message: 'foo bar', + style: 'italic' + }) + }]; + } + }); + + return ( +
+
+
hello world
+
foo bar
+
+ ⌘C +
+ ); +} +/*- end collapse -*/ +function Pasteable() { + let [pasted, setPasted] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + let pasted = await Promise.all( + items + .filter(item => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type'))) + .map(async (item: TextDropItem) => { + 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')}; + } + }) + ); + setPasted(pasted); + } + }); + + let message = ['Paste here']; + if (pasted) { + message = pasted.map(d => { + let message = d.message; + if (d.style === 'bold') { + message = {message}; + } else if (d.style === 'italic') { + message = {message}; + } + return
{message}
; + }); + } + + return ( +
+
{message || 'Paste here'}
+ ⌘V +
+ ); +} +/*- begin collapse -*/ + + +/*- end collapse -*/ +``` + +### Files + +A references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object which can be attached to form data for uploading. + +This example accepts JPEG and PNG image files, and renders them by creating a local [object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). + +```tsx example +import type {FileDropItem} from '@react-aria/dnd'; + +function Pasteable() { + let [file, setFile] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + let item = items.find(item => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png')) as FileDropItem; + if (item) { + setFile(URL.createObjectURL(await item.getFile())); + } + } + }); + + return ( +
+ {file ? : 'Paste image here'} +
+ ); +} +``` + +### Directories + +A references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. + +The `getEntries` method returns an [async iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) object, which can be used in a `for await...of` loop. This provides each item in the directory as either a or , and you can access the contents of each file as discussed above. + +This example renders the file names within a dropped directory in a grid. + +```tsx example +import type {DirectoryDropItem} from '@react-aria/dnd'; +import File from '@spectrum-icons/workflow/FileTxt'; +import Folder from '@spectrum-icons/workflow/Folder'; + +function Pasteable() { + let [files, setFiles] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + // Find the first dropped item that is a directory. + let dir = items.find(item => item.kind === 'directory') as DirectoryDropItem; + if (dir) { + // Read entries in directory and update state with relevant info. + let files = []; + for await (let entry of dir.getEntries()) { + files.push({ + name: entry.name, + kind: entry.kind + }); + } + setFiles(files); + } + } + }); + + let contents = <>Paste directory here; + if (files) { + contents = ( +
    + {files.map(f => ( +
  • + {f.kind === 'directory' ? : } + {f.name} +
  • + ))} +
+ ); + } + + return ( +
+ {contents} +
+ ); +} +``` + +
+ Show CSS + +```css +.grid { + display: block; + width: auto; + height: auto; + min-height: 80px; +} + +.grid ul { + display: grid; + grid-template-columns: repeat(auto-fit, 100px); + list-style: none; + margin: 0; + padding: 0; + gap: 20px; +} + +.grid li { + display: flex; + align-items: center; + gap: 8px; +} + +.grid li svg { + flex: 0 0 auto; +} + +.grid li span { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +``` + +
+ +## Disabling copy and paste + +If you need to temporarily disable copying and pasting, you can pass the `isDisabled` option to `useClipboard`. This will prevent copying and pasting on the element until it is re-enabled. + +```tsx example +import type {TextDropItem} from '@react-aria/dnd'; +import {useClipboard} from '@react-aria/dnd'; + +function Copyable() { + let {clipboardProps} = useClipboard({ + getItems() { + return [{ + 'text/plain': 'Hello world' + }]; + }, + /*- begin highlight -*/ + isDisabled: true + /*- end highlight -*/ + }); + + return ( +
+ Hello world + ⌘C +
+ ); +} + +function Pasteable() { + let [pasted, setPasted] = React.useState(null); + let {clipboardProps} = useClipboard({ + async onPaste(items) { + let pasted = await Promise.all( + items + .filter((item) => + item.kind === 'text' && item.types.has('text/plain') + ) + .map((item: TextDropItem) => item.getText('text/plain')) + ); + setPasted(pasted.join('\n')); + }, + /*- begin highlight -*/ + isDisabled: true + /*- end highlight -*/ + }); + + return ( +
+ {pasted || 'Paste here'} + ⌘V +
+ ); +} + + + +``` From 5c2263d7f88c5972b5fb6d078c72fb3aeaecfa81 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:01:40 -0700 Subject: [PATCH 03/14] add more hook docs --- .../s2-docs/pages/react-aria/Draggable.tsx | 20 + .../s2-docs/pages/react-aria/DropTarget.tsx | 46 +++ .../s2-docs/pages/react-aria/FocusRing.mdx | 47 +++ .../pages/react-aria/FocusRingExample.css | 14 + .../s2-docs/pages/react-aria/FocusScope.mdx | 129 +++++++ .../s2-docs/pages/react-aria/I18nProvider.mdx | 41 ++ .../pages/react-aria/MyToastRegion.tsx | 25 ++ .../pages/react-aria/PortalProvider.mdx | 108 ++++++ .../s2-docs/pages/react-aria/SSRProvider.mdx | 41 ++ .../pages/react-aria/VisuallyHidden.mdx | 66 ++++ .../dev/s2-docs/pages/react-aria/hooks.css | 5 + .../s2-docs/pages/react-aria/mergeProps.mdx | 70 ++++ .../s2-docs/pages/react-aria/useClipboard.mdx | 175 +++------ .../pages/react-aria/useClipboardExample.css | 31 ++ .../pages/react-aria/useClipboardGrid.css | 33 ++ .../s2-docs/pages/react-aria/useCollator.mdx | 75 ++++ .../pages/react-aria/useDateFormatter.mdx | 58 +++ .../dev/s2-docs/pages/react-aria/useDrag.mdx | 306 +++++++++++++++ .../pages/react-aria/useDragExample.css | 25 ++ .../dev/s2-docs/pages/react-aria/useDrop.mdx | 357 ++++++++++++++++++ .../dev/s2-docs/pages/react-aria/useField.mdx | 67 ++++ .../s2-docs/pages/react-aria/useFilter.mdx | 82 ++++ .../s2-docs/pages/react-aria/useFocusRing.mdx | 77 ++++ .../dev/s2-docs/pages/react-aria/useId.mdx | 36 ++ .../dev/s2-docs/pages/react-aria/useIsSSR.mdx | 45 +++ .../dev/s2-docs/pages/react-aria/useLabel.mdx | 57 +++ .../s2-docs/pages/react-aria/useLocale.mdx | 54 +++ .../pages/react-aria/useNumberFormatter.mdx | 62 +++ .../s2-docs/pages/react-aria/useObjectRef.mdx | 56 +++ packages/dev/s2-docs/src/ClassAPI.tsx | 1 + 30 files changed, 2088 insertions(+), 121 deletions(-) create mode 100644 packages/dev/s2-docs/pages/react-aria/Draggable.tsx create mode 100644 packages/dev/s2-docs/pages/react-aria/DropTarget.tsx create mode 100644 packages/dev/s2-docs/pages/react-aria/FocusRing.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/FocusRingExample.css create mode 100644 packages/dev/s2-docs/pages/react-aria/FocusScope.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx create mode 100644 packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/hooks.css create mode 100644 packages/dev/s2-docs/pages/react-aria/mergeProps.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useClipboardExample.css create mode 100644 packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css create mode 100644 packages/dev/s2-docs/pages/react-aria/useCollator.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useDrag.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useDragExample.css create mode 100644 packages/dev/s2-docs/pages/react-aria/useDrop.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useField.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useFilter.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useId.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useLabel.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useLocale.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx create mode 100644 packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx 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..3927ea1e42a --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx @@ -0,0 +1,46 @@ +"use client"; + +import React from 'react'; +import type {TextDropItem} from '@react-aria/dnd'; +import {useDrop} from '@react-aria/dnd'; + +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 + .filter(item => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type'))) + .map(async (item: TextDropItem) => { + 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: string[] = ['Drop here']; + if (dropped) { + message = dropped.map(d => { + let message = d.message; + if (d.style === 'bold') { + message = {message}; + } else if (d.style === 'italic') { + message = {message}; + } + 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..ff00514e71d --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx @@ -0,0 +1,47 @@ +{/* 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'; +export const description = 'Implementing collections in React Aria'; + +# FocusRing + +## 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..a72c2c109ee --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx @@ -0,0 +1,129 @@ +{/* 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'; +export const description = 'Implementing collections in React Aria'; + +# FocusScope + +## 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..1f6f6d8d23a --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/I18nProvider.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 {GroupedPropTable} from '../../src/PropTable'; +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..7f3ccc9c865 --- /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}) { + 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..9e383a3b3d7 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx @@ -0,0 +1,108 @@ +{/* 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'; +export const description = 'Implementing collections in React Aria'; + +# PortalProvider + +## 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 'react-aria-components'; +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; +} +``` + +## 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..f21f37315c5 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx @@ -0,0 +1,66 @@ +{/* 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'; +export const description = 'Implementing collections in React Aria'; + +# VisuallyHidden + +## 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/hooks.css b/packages/dev/s2-docs/pages/react-aria/hooks.css new file mode 100644 index 00000000000..10903313525 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/hooks.css @@ -0,0 +1,5 @@ +@import '../../../../../starters/docs/src/theme.css'; + +:root { + --tint: var(--blue) +} 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..1c35964f9d6 --- /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'; +export const description = 'Implementing collections in React Aria'; + + +# mergeProps + +## 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/useClipboard.mdx b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx index 015c3f7b50f..c77a288017f 100644 --- a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx @@ -12,7 +12,8 @@ export default Layout; import {GroupedPropTable} from '../../src/PropTable'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/dnd'; -export const section = 'Hooks'; +import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; +export const section = 'Drag and Drop'; export const description = 'Implementing collections in React Aria'; # useClipboard @@ -31,11 +32,12 @@ The hook provid This example shows a simple focusable element which supports copying a string when focused, and another element which supports pasting plain text. -```tsx render +```tsx render files={["packages/dev/s2-docs/pages/react-aria/useClipboardExample.css"]} "use client"; import React from 'react'; import type {TextDropItem} from '@react-aria/dnd'; import {useClipboard} from '@react-aria/dnd'; +import './useClipboardExample.css'; function Copyable() { let {clipboardProps} = useClipboard({ @@ -77,45 +79,12 @@ function Pasteable() { ); } - - -``` - -
- Show CSS - -```css -[role=textbox] { - display: inline-flex; - align-items: center; - justify-content: center; - background: var(--spectrum-global-color-gray-50); - border: 1px solid var(--spectrum-global-color-gray-400); - padding: 10px; - margin-right: 20px; - border-radius: 8px; -} - -[role=textbox]:focus { - outline: none; - border-color: var(--blue); - box-shadow: 0 0 0 1px var(--blue); -} - -[role=textbox] kbd { - display: inline-block; - margin-left: 10px; - padding: 0 4px; - background: var(--spectrum-global-color-gray-100); - border: 1px solid var(--spectrum-global-color-gray-300); - border-radius: 4px; - font-size: small; - letter-spacing: .2em; -} +
+ + +
``` -
- ## Copy data Data to copy can be provided in multiple formats at once. This allows the destination where the user pastes to choose the data that it understands. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user pastes in an external application (e.g. an email message). @@ -128,6 +97,9 @@ This example copies two items, each of which contains representations as plain t ```tsx render "use client" +import React from 'react'; +import {useClipboard} from '@react-aria/dnd'; + function Copyable() { let {clipboardProps} = useClipboard({ getItems() { @@ -159,7 +131,8 @@ function Copyable() {
); } -/*- begin collapse -*/ + +///- begin collapse -/// function Pasteable() { let [pasted, setPasted] = React.useState(null); let {clipboardProps} = useClipboard({ @@ -200,9 +173,11 @@ function Pasteable() { ); } - - -/*- end collapse -*/ +
+ + +
+///- end collapse -/// ``` ## Paste data @@ -215,13 +190,16 @@ function Pasteable() { ### Text -A represents textual data in one or more different formats. These may be either standard [mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. +A represents textual data in one or more different formats. These may be either standard [mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below works with the above `Copyable` example using a custom app-specific data format to transfer rich data. If no such data is available, it falls back to pasting plain text data. ```tsx render -"use client" -/*- begin collapse -*/ +"use client"; +import React from 'react'; +import {useClipboard} from '@react-aria/dnd'; + +///- begin collapse -/// function Copyable() { let {clipboardProps} = useClipboard({ getItems() { @@ -253,7 +231,8 @@ function Copyable() { ); } -/*- end collapse -*/ +///- end collapse -/// + function Pasteable() { let [pasted, setPasted] = React.useState(null); let {clipboardProps} = useClipboard({ @@ -293,19 +272,20 @@ function Pasteable() { ); } -/*- begin collapse -*/ - - -/*- end collapse -*/ +///- begin collapse -/// +
+ + +
+///- end collapse -/// ``` - ### Files - -A references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object which can be attached to form data for uploading. - +A references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local [object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). - -```tsx example +```tsx render +"use client"; +import React from 'react'; +import {useClipboard} from '@react-aria/dnd'; import type {FileDropItem} from '@react-aria/dnd'; function Pasteable() { @@ -318,7 +298,6 @@ function Pasteable() { } } }); - return (
{file ? : 'Paste image here'} @@ -326,19 +305,18 @@ function Pasteable() { ); } ``` - ### Directories - -A references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. - -The `getEntries` method returns an [async iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) object, which can be used in a `for await...of` loop. This provides each item in the directory as either a or , and you can access the contents of each file as discussed above. - +A references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. +The `getEntries` method returns an [async iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) object, which can be used in a `for await...of` loop. This provides each item in the directory as either a or , and you can access the contents of each file as discussed above. This example renders the file names within a dropped directory in a grid. - -```tsx example +```tsx render files={["packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css"]} +"use client"; +import React from 'react'; +import {useClipboard} from '@react-aria/dnd' import type {DirectoryDropItem} from '@react-aria/dnd'; -import File from '@spectrum-icons/workflow/FileTxt'; -import Folder from '@spectrum-icons/workflow/Folder'; +import File from '@react-spectrum/s2/icons/File'; +import Folder from '@react-spectrum/s2/icons/Folder'; +import './useClipboardGrid.css'; function Pasteable() { let [files, setFiles] = React.useState(null); @@ -359,7 +337,6 @@ function Pasteable() { } } }); - let contents = <>Paste directory here; if (files) { contents = ( @@ -373,7 +350,6 @@ function Pasteable() { ); } - return (
{contents} @@ -381,52 +357,11 @@ function Pasteable() { ); } ``` - -
- Show CSS - -```css -.grid { - display: block; - width: auto; - height: auto; - min-height: 80px; -} - -.grid ul { - display: grid; - grid-template-columns: repeat(auto-fit, 100px); - list-style: none; - margin: 0; - padding: 0; - gap: 20px; -} - -.grid li { - display: flex; - align-items: center; - gap: 8px; -} - -.grid li svg { - flex: 0 0 auto; -} - -.grid li span { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -``` - -
- ## Disabling copy and paste - If you need to temporarily disable copying and pasting, you can pass the `isDisabled` option to `useClipboard`. This will prevent copying and pasting on the element until it is re-enabled. - -```tsx example +```tsx render +"use client"; +import React from 'react'; import type {TextDropItem} from '@react-aria/dnd'; import {useClipboard} from '@react-aria/dnd'; @@ -441,7 +376,6 @@ function Copyable() { isDisabled: true /*- end highlight -*/ }); - return (
Hello world @@ -449,7 +383,6 @@ function Copyable() {
); } - function Pasteable() { let [pasted, setPasted] = React.useState(null); let {clipboardProps} = useClipboard({ @@ -467,7 +400,6 @@ function Pasteable() { isDisabled: true /*- end highlight -*/ }); - return (
{pasted || 'Paste here'} @@ -475,7 +407,8 @@ function Pasteable() {
); } - - - -``` +
+ + +
+``` \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css b/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css new file mode 100644 index 00000000000..971bb696cda --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css @@ -0,0 +1,31 @@ +@import './hooks.css'; + +[role=textbox] { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--background-color); + border: 1px solid var(--border-color); + padding: 10px; + margin-right: 20px; + border-radius: 8px; + color: var(--color-white); +} + +[role=textbox]:focus { + outline: none; + border-color: var(--focus-ring-color); + box-shadow: 0 0 0 1px var(--focus-ring-color); +} + +[role=textbox] kbd { + display: inline-block; + margin-left: 10px; + padding: 0 4px; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: small; + letter-spacing: .2em; + color: var(--color-white); +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css b/packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css new file mode 100644 index 00000000000..9d8a2e594ab --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css @@ -0,0 +1,33 @@ +.grid { + display: block; + width: auto; + height: auto; + min-height: 80px; +} + +.grid ul { + display: grid; + grid-template-columns: repeat(auto-fit, 100px); + list-style: none; + margin: 0; + padding: 0; + gap: 20px; +} + +.grid li { + display: flex; + align-items: center; + gap: 8px; +} + +.grid li svg { + flex: 0 0 auto; +} + +.grid li span { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + diff --git a/packages/dev/s2-docs/pages/react-aria/useCollator.mdx b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx new file mode 100644 index 00000000000..9d71e8b06ae --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx @@ -0,0 +1,75 @@ +{/* 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/i18n'; + +export const section = 'Internationalization'; +export const description = 'Implementing collections in React Aria'; + + +# useCollator + +## Introduction + +`useCollator` wraps a builtin browser [Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) +object to provide a React Hook that integrates with the i18n system in React Aria. It handles string comparison according to the current locale, +updating when the locale changes, and caching of collators for performance. See the +[Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) docs for +information. + +## API + + + +## Example + +This example includes two textfields and compares the values of the two fields using a collator according to the current locale. + +```tsx render +'use client'; +import React from 'react'; +import {useCollator} from '@react-aria/i18n'; + +function Example() { + let [first, setFirst] = React.useState(''); + let [second, setSecond] = React.useState(''); + + let collator = useCollator(); + let result = collator.compare(first, second); + + return ( + <> +
+ + setFirst(e.target.value)} /> + + setSecond(e.target.value)} /> +
+

+ {result === 0 + ? 'The strings are the same' + : result < 0 + ? 'First comes before second' + : 'Second comes before first' + } +

+ + ); +} +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx new file mode 100644 index 00000000000..abb2daa8e51 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx @@ -0,0 +1,58 @@ +{/* 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/i18n'; + +export const section = 'Internationalization'; +export const description = 'Implementing collections in React Aria'; + +# useDateFormatter + +## Introduction + +`useDateFormatter` wraps a builtin browser [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) +object to provide a React Hook that integrates with the i18n system in React Aria. It handles formatting dates for the current locale, +updating when the locale changes, and caching of date formatters for performance. See the +[Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) docs for +information on formatting options. + +## API + + + +## Example + +This example displays the current date for two locales: USA, and Russia. Two instances of the `CurrentDate` component are rendered, +using the [I18nProvider](I18nProvider.html) to specify the locale to display. + +```tsx render +'use client'; +import {I18nProvider, useDateFormatter} from '@react-aria/i18n'; + +function CurrentDate() { + let formatter = useDateFormatter(); + + return ( +

{formatter.format(new Date())}

+ ); +} + +<> + + + + + + + +``` diff --git a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx new file mode 100644 index 00000000000..2e84bfda2f0 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx @@ -0,0 +1,306 @@ +{/* 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/dnd'; +import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; +export const section = 'Drag and Drop'; +export const description = 'Implementing collections in React Aria'; + +# useDrag + +## API + + + +## Introduction + +Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen. It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems. + +React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets, and Enter to drop or Escape to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. + +See the [drag and drop introduction](dnd.html) to learn more. + +## Example + +This example shows how to make a simple draggable element that provides data as plain text. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While it is being dragged, it is displayed with a dimmed appearance by applying an additional CSS class. + +```tsx render files={["packages/dev/s2-docs/pages/react-aria/useDragExample.css", "packages/dev/s2-docs/pages/react-aria/DropTarget.tsx"]} +"use client"; +import {useDrag} from '@react-aria/dnd'; +import {DropTarget} from './DropTarget.tsx'; +import './useDragExample.css'; + +function Draggable() { + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + } + }); + + return ( +
+ Drag me +
+ ); +} + +
+ + +
+``` +## Drag data +Data for a draggable element can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). +This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard [mime type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) for interoperability with external applications, or a custom string for use within your own app. +In addition to providing items in multiple formats, you can also return multiple drag items from `getItems` to transfer multiple objects in a single drag operation. +This example drags two items, each of which contains representations as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. + +```tsx render +"use client"; +import {useDrag} from '@react-aria/dnd'; +import {DropTarget} from './DropTarget.tsx'; + +function Draggable() { + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world', + 'text/html': 'hello world', + 'my-app-custom-type': JSON.stringify({ + message: 'hello world', + style: 'bold' + }) + }, { + 'text/plain': 'foo bar', + 'text/html': 'foo bar', + 'my-app-custom-type': JSON.stringify({ + message: 'foo bar', + style: 'italic' + }) + }]; + } + }); + ///- begin collapse -/// + return ( +
+ Drag me +
+ ); + ///- end collapse -/// +} +///- begin collapse -/// +
+ + +
+///- end collapse -/// +``` + +## Drag previews +By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered using the `` component. This accepts a function as a child which receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. The `DragPreview` is linked with `useDrag` via a ref, passed to the `preview` property. The `DragPreview` should be placed in the component hierarchy appropriately, so that it receives any React context or inherited styles that it needs to render correctly. +This example renders a custom drag preview which shows the text of the first drag item. + +```tsx render +"use client"; +import React from 'react'; +import {useDrag} from '@react-aria/dnd'; +import {DropTarget} from './DropTarget.tsx'; +import {DragPreview} from '@react-aria/dnd'; + +function Draggable() { + let preview = React.useRef(null); + let {dragProps, isDragging} = useDrag({ + preview, + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + } + }); + return ( + <> +
+ Drag me +
+ {/*- begin highlight -*/} + + {items =>
{items[0]['text/plain']}
} +
+ {/*- end highlight -*/} + + ); +} +
+ + +
+``` + +## Drop operations +A is an indication of what will happen when dragged data is dropped on a particular drop target. These are: +* `move` – indicates that the dragged data will be moved from its source location to the target location. +* `copy` – indicates that the dragged data will be copied to the target destination. +* `link` – indicates that there will be a relationship established between the source and target locations. +* `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. +Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. +The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged element. +This example removes the draggable element from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. +```tsx render +"use client" +import React from 'react'; +import {useDrag} from '@react-aria/dnd'; +import {DropTarget} from './DropTarget.tsx'; + +function Draggable() { + let [moved, setMoved] = React.useState(false); + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + }, + /*- begin highlight -*/ + onDragEnd(e) { + if (e.dropOperation === 'move') { + setMoved(true); + } + } + /*- end highlight -*/ + }); + if (moved) { + return null; + } + // ... + ///- begin collapse -/// + return ( +
+ Drag me +
+ ); + ///- end collapse -/// +} +///- begin collapse -/// +
+ + +
+///- end collapse -/// +``` + +The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. + +```tsx render +"use client"; +import {useDrag} from '@react-aria/dnd'; +import {DropTarget} from './DropTarget.tsx'; + +function Draggable() { + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + }, + /*- begin highlight -*/ + getAllowedDropOperations() { + return ['copy']; + } + /*- end highlight -*/ + }); + // ... + ///- begin collapse -/// + return ( +
+ Drag me +
+ ); + ///- end collapse -/// +} +///- begin collapse -/// +
+ + +
+///- end collapse -/// +``` + +## Drag button + +In cases where a draggable element has other interactions that conflict with accessible drag and drop (e.g. Enter key), or if the element is not focusable, an explicit drag affordance can be added. This acts as a button that keyboard and screen reader users can use to activate drag and drop. +When the `hasDragButton` option is enabled, the keyboard interactions are moved from the returned `dragProps` to the `dragButtonProps` so that they can be applied to a separate element, while the mouse and touch dragging interactions remain in `dragProps`. + +```tsx render +"use client"; +import React from 'react'; +import {useDrag} from '@react-aria/dnd'; +import {useButton} from '@react-aria/button'; +import {DropTarget} from './DropTarget.tsx'; + +function Draggable() { + let {dragProps, dragButtonProps, isDragging} = useDrag({ + /*- begin highlight -*/ + hasDragButton: true, + /*- end highlight -*/ + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + } + }); + /*- begin highlight -*/ + let ref = React.useRef(null); + let {buttonProps} = useButton({...dragButtonProps, elementType: 'div'}, ref); + /*- end highlight -*/ + return ( +
+ {/*- begin highlight -*/} + + {/*- end highlight -*/} + Some text + +
+ ); +} + +
+ + +
+``` +## Disabling dragging +If you need to temporarily disable dragging, you can pass the `isDisabled` option to `useDrag`. This will prevent dragging an element until it is re-enabled. + +```tsx render +"use client"; +import {useDrag} from '@react-aria/dnd'; +function Draggable() { + let {dragProps, isDragging} = useDrag({ + getItems() { + return [{ + 'text/plain': 'hello world' + }]; + }, + /*- begin highlight -*/ + isDisabled: true + /*- end highlight -*/ + }); + return ( +
+ Drag me +
+ ); +} + +``` \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/useDragExample.css b/packages/dev/s2-docs/pages/react-aria/useDragExample.css new file mode 100644 index 00000000000..9a546076f26 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useDragExample.css @@ -0,0 +1,25 @@ +.draggable { + display: inline-block; + vertical-align: top; + border: 1px solid gray; + padding: 10px; +} + +.draggable.dragging { + opacity: 0.5; +} + +.droppable { + width: 100px; + height: 80px; + border-radius: 6px; + display: inline-block; + padding: 20px; + margin-left: 20px; + border: 2px dotted gray; + white-space: pre-wrap; +} + +.droppable.target { + border: 2px solid var(--blue); +} \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx new file mode 100644 index 00000000000..ac4fecca134 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx @@ -0,0 +1,357 @@ +{/* 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/dnd'; +import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; +import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; +export const section = 'Drag and Drop'; +export const description = 'Implementing collections in React Aria'; + +# useDrop + +## API + + + +## Introduction + +Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen. It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems. + +React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets, and Enter to drop or Escape to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. + +See the [drag and drop introduction](dnd.html) to learn more. + +## Example + +This example shows how to make a simple drop target that accepts plain text data. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While a drag is hovered over it, a blue outline is rendered by applying an additional CSS class. + +```tsx render files={["packages/dev/s2-docs/pages/react-aria/useDragExample.css", "packages/dev/s2-docs/pages/react-aria/Draggable.tsx"]} +'use client'; +import React from 'react'; +import type {TextDropItem} from '@react-aria/dnd'; +import {useDrop} from '@react-aria/dnd'; +import {Draggable} from './Draggable.tsx'; +import './useDragExample.css'; + +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 + .filter(item => item.kind === 'text' && item.types.has('text/plain')) + .map((item: TextDropItem) => item.getText('text/plain')) + ); + setDropped(items.join('\n')); + } + }); + + return ( +
+ {dropped || 'Drop here'} +
+ ); +} + +
+ + +
+``` +## Drop data +`useDrop` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: +* `text` – represents data inline as a string in one or more formats +* `file` – references a file on the user's device +* `directory` – references the contents of a directory +### Text +A represents textual data in one or more different formats. These may be either standard [mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. +The example below finds the first available item that includes a custom app-specific type. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. + +```tsx render +'use client'; +import React from 'react'; +import {useDrop} from '@react-aria/dnd'; +import {Draggable} from './Draggable.tsx'; + +function DropTarget() { + let [dropped, setDropped] = React.useState(null); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + /*- begin highlight -*/ + async onDrop(e) { + let item = e.items.find(item => item.kind === 'text' && item.types.has('my-app-custom-type')) as TextDropItem; + if (item) { + setDropped(await item.getText('my-app-custom-type')); + } + } + /*- end highlight -*/ + }); + // ... + ///- begin collapse -/// + return ( +
+ {dropped || 'Drop here'} +
+ ); + ///- end collapse -/// +} +///- begin collapse -/// +
+ + +
+///- end collapse -/// +``` +### Files +A references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object which can be attached to form data for uploading. +This example accepts JPEG and PNG image files, and renders them by creating a local [object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). + +```tsx render +'use client'; +import React from 'react'; +import type {FileDropItem} from '@react-aria/dnd'; +import {useDrop} from '@react-aria/dnd'; +function DropTarget() { + let [file, setFile] = React.useState(null); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + /*- begin highlight -*/ + async onDrop(e) { + let item = e.items.find(item => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png')) as FileDropItem; + if (item) { + setFile(URL.createObjectURL(await item.getFile())); + } + } + /*- end highlight -*/ + }); + return ( +
+ {file ? : 'Drop image here'} +
+ ); +} +``` +### Directories +A references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. +The `getEntries` method returns an [async iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) object, which can be used in a `for await...of` loop. This provides each item in the directory as either a or , and you can access the contents of each file as discussed above. + +This example renders the file names within a dropped directory in a grid. + +```tsx render files={["packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css"]} +'use client'; +import React from 'react'; +import type {DirectoryDropItem} from '@react-aria/dnd'; +import File from '@react-spectrum/s2/icons/File'; +import Folder from '@react-spectrum/s2/icons/Folder'; +import {useDrop} from '@react-aria/dnd'; +import './useClipboardGrid.css'; + +function DropTarget() { + let [files, setFiles] = React.useState(null); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + /*- begin highlight -*/ + async onDrop(e) { + // Find the first dropped item that is a directory. + let dir = e.items.find(item => item.kind === 'directory') as DirectoryDropItem; + if (dir) { + // Read entries in directory and update state with relevant info. + let files = []; + for await (let entry of dir.getEntries()) { + files.push({ + name: entry.name, + kind: entry.kind + }); + } + setFiles(files); + } + } + /*- end highlight -*/ + }); + let contents = <>Drop directory here; + if (files) { + contents = ( +
    + {files.map(f => ( +
  • + {f.kind === 'directory' ? : } + {f.name} +
  • + ))} +
+ ); + } + return ( +
+ {contents} +
+ ); +} +``` + +## Drop operations + +A is an indication of what will happen when dragged data is dropped on a particular drop target. These are: +* `move` – indicates that the dragged data will be moved from its source location to the target location. +* `copy` – indicates that the dragged data will be copied to the target destination. +* `link` – indicates that there will be a relationship established between the source and target locations. +* `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. +Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. +The drag source can specify which drop operations are allowed for the dragged data (see the [useDrag docs](useDrag.html) for how to customize this). By default, the first allowed operation is allowed by drop targets, meaning that the drop target accepts data of any type and operation. + +### getDropOperation + +The `getDropOperation` function passed to `useDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement `getDropOperation` and return `'cancel'` for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. +When the data is supported, either return one of the drop operations in `allowedOperation` or a specific drop operation if only that drop operation is supported. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. +In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. + +```tsx render +'use client'; +import React from 'react'; +import {useDrop} from '@react-aria/dnd'; +function DropTarget() { + let [file, setFile] = React.useState(null); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + /*- begin highlight -*/ + getDropOperation(types, allowedOperations) { + return types.has('image/png') ? 'copy' : 'cancel'; + }, + /*- end highlight -*/ + async onDrop(e) { + let item = e.items.find(item => item.kind === 'file' && item.type === 'image/png') as FileDropItem; + if (item) { + setFile(URL.createObjectURL(await item.getFile())); + } + } + }); + // ... +///- begin collapse -/// + return ( +
+ {file ? : 'Drop image here'} +
+ ); +///- end collapse -/// +} +``` +### onDrop +The `onDrop` event also includes the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. +```tsx +function DropTarget(props) { + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + async onDrop(e) { + let item = e.items.find(item => item.kind === 'text' && item.types.has('my-app-file')) as TextDropItem; + if (!item) { + return; + } + let data = JSON.parse(await item.getText('my-app-file')); + /*- begin highlight -*/ + switch (e.dropOperation) { + case 'move': + MyAppFileService.move(data.filePath, props.filePath); + break; + case 'copy': + MyAppFileService.copy(data.filePath, props.filePath); + break; + case 'link': + MyAppFileService.link(data.filePath, props.filePath); + break; + } + /*- end highlight -*/ + } + }); + // ... +} +``` +## Events + +Drop targets receive a number of events during a drag session. These are: + +This example logs all events that occur within the drop target: + +```tsx render +'use client'; +import React from 'react'; +import {useDrop} from '@react-aria/dnd'; +import {Draggable} from './Draggable.tsx'; + +function DropTarget() { + let [events, setEvents] = React.useState([]); + let onEvent = e => setEvents(events => [JSON.stringify(e), ...events]); + let ref = React.useRef(null); + let {dropProps, isDropTarget} = useDrop({ + ref, + onDropEnter: onEvent, + onDropMove: onEvent, + onDropExit: onEvent, + onDrop: onEvent + }); + return ( +
    + {events.map((e, i) =>
  • {e}
  • )} +
+ ); +} +
+ + +
+``` +## Disabling dropping + +If you need to temporarily disable dropping, you can pass the `isDisabled` option to `useDrop`. This will prevent the drop target from accepting any drops until it is re-enabled. + +```tsx render +'use client'; +import React from 'react'; +import type {TextDropItem} from '@react-aria/dnd'; +import {useDrop} from '@react-aria/dnd'; +import {Draggable} from './Draggable.tsx'; + +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 + .filter(item => item.kind === 'text' && item.types.has('text/plain')) + .map((item: TextDropItem) => item.getText('text/plain')) + ); + setDropped(items.join('\n')); + }, + /*- begin highlight -*/ + isDisabled: true + /*- end highlight -*/ + }); + return ( +
+ {dropped || 'Drop here'} +
+ ); +} +
+ + +
+``` \ No newline at end of file diff --git a/packages/dev/s2-docs/pages/react-aria/useField.mdx b/packages/dev/s2-docs/pages/react-aria/useField.mdx new file mode 100644 index 00000000000..f4a127bf099 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/useField.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/label'; +import {FunctionAPI} from '../../src/FunctionAPI'; + +export const section = 'Utilities'; +export const description = 'Implementing collections in React Aria'; + +# useField + +## API + + + +## Example + +The `useField` hook associates a form control with a label, and an optional description and/or error message. This is useful for providing context about how users should fill in a field, or a validation message. `useField` takes care of creating ids for each element and associating them with the correct ARIA attributes (`aria-labelledby` and `aria-describedby`). + +By default, `useField` assumes that the label is a native HTML `
); } -///- end collapse -/// +///- end collapse -/// function Pasteable() { let [pasted, setPasted] = React.useState(null); let {clipboardProps} = useClipboard({ @@ -272,6 +271,7 @@ function Pasteable() {
); } + ///- begin collapse -///
@@ -283,7 +283,7 @@ function Pasteable() { A references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local [object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). ```tsx render -"use client"; +'use client'; import React from 'react'; import {useClipboard} from '@react-aria/dnd'; import type {FileDropItem} from '@react-aria/dnd'; @@ -310,7 +310,7 @@ A or , and you can access the contents of each file as discussed above. This example renders the file names within a dropped directory in a grid. ```tsx render files={["packages/dev/s2-docs/pages/react-aria/useClipboardGrid.css"]} -"use client"; +'use client'; import React from 'react'; import {useClipboard} from '@react-aria/dnd' import type {DirectoryDropItem} from '@react-aria/dnd'; @@ -360,7 +360,7 @@ function Pasteable() { ## Disabling copy and paste If you need to temporarily disable copying and pasting, you can pass the `isDisabled` option to `useClipboard`. This will prevent copying and pasting on the element until it is re-enabled. ```tsx render -"use client"; +'use client'; import React from 'react'; import type {TextDropItem} from '@react-aria/dnd'; import {useClipboard} from '@react-aria/dnd'; @@ -407,6 +407,7 @@ function Pasteable() {
); } +
diff --git a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx index 2e84bfda2f0..7809f596e45 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx @@ -34,7 +34,7 @@ See the [drag and drop introduction](dnd.html) to learn more. This example shows how to make a simple draggable element that provides data as plain text. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While it is being dragged, it is displayed with a dimmed appearance by applying an additional CSS class. -```tsx render files={["packages/dev/s2-docs/pages/react-aria/useDragExample.css", "packages/dev/s2-docs/pages/react-aria/DropTarget.tsx"]} +```tsx render files={["packages/dev/s2-docs/pages/react-aria/DropTarget.tsx", "packages/dev/s2-docs/pages/react-aria/useDragExample.css"]} "use client"; import {useDrag} from '@react-aria/dnd'; import {DropTarget} from './DropTarget.tsx'; diff --git a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx index ac4fecca134..1e02f392ca8 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx @@ -9,7 +9,7 @@ governing permissions and limitations under the License. */} import {Layout} from '../../src/Layout'; export default Layout; -import {GroupedPropTable} from '../../src/PropTable'; +import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; @@ -35,7 +35,7 @@ See the [drag and drop introduction](dnd.html) to learn more. This example shows how to make a simple drop target that accepts plain text data. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While a drag is hovered over it, a blue outline is rendered by applying an additional CSS class. -```tsx render files={["packages/dev/s2-docs/pages/react-aria/useDragExample.css", "packages/dev/s2-docs/pages/react-aria/Draggable.tsx"]} +```tsx render files={["packages/dev/s2-docs/pages/react-aria/Draggable.tsx", "packages/dev/s2-docs/pages/react-aria/useDragExample.css"]} 'use client'; import React from 'react'; import type {TextDropItem} from '@react-aria/dnd'; @@ -218,6 +218,7 @@ The drag source can specify which drop operations are allowed for the dragged da The `getDropOperation` function passed to `useDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement `getDropOperation` and return `'cancel'` for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. When the data is supported, either return one of the drop operations in `allowedOperation` or a specific drop operation if only that drop operation is supported. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. + In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. ```tsx render @@ -285,7 +286,8 @@ function DropTarget(props) { ## Events Drop targets receive a number of events during a drag session. These are: - + + This example logs all events that occur within the drop target: ```tsx render From e1b9fb2e63b293ada2e2e41dd843ec5afd44ef02 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:21:40 -0700 Subject: [PATCH 06/14] cleanup focus hooks --- packages/dev/s2-docs/pages/react-aria/FocusScope.mdx | 2 +- packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx index a72c2c109ee..56c4c87c363 100644 --- a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx @@ -78,7 +78,7 @@ function Example() { } ``` -## useFocusManager example +## 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 diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx index 165d15633c4..5e1af34d74b 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx @@ -9,7 +9,7 @@ governing permissions and limitations under the License. */} import {Layout} from '../../src/Layout'; export default Layout; -import {GroupedPropTable} from '../../src/PropTable'; +import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; @@ -33,11 +33,11 @@ If CSS classes are being used for styling, see the [FocusRing](FocusRing.html) c ## Options - + ## Result - + ## Example From a6360cd484143f2285bfbe5f69a48395998eb6c0 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:23:59 -0700 Subject: [PATCH 07/14] cleanup internationalized hooks --- packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx | 1 - packages/dev/s2-docs/pages/react-aria/useCollator.mdx | 1 - packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx | 1 - packages/dev/s2-docs/pages/react-aria/useFilter.mdx | 1 - packages/dev/s2-docs/pages/react-aria/useLocale.mdx | 1 - packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx | 1 - 6 files changed, 6 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx index 1f6f6d8d23a..9bdc658f95d 100644 --- a/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx @@ -9,7 +9,6 @@ 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/i18n'; diff --git a/packages/dev/s2-docs/pages/react-aria/useCollator.mdx b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx index 9d71e8b06ae..45d63d6cfd1 100644 --- a/packages/dev/s2-docs/pages/react-aria/useCollator.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx @@ -9,7 +9,6 @@ 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/i18n'; diff --git a/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx index abb2daa8e51..2f77d300aed 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx @@ -9,7 +9,6 @@ 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/i18n'; diff --git a/packages/dev/s2-docs/pages/react-aria/useFilter.mdx b/packages/dev/s2-docs/pages/react-aria/useFilter.mdx index b6e999e1cfc..ac5c8b7f37c 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFilter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFilter.mdx @@ -9,7 +9,6 @@ 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/i18n'; diff --git a/packages/dev/s2-docs/pages/react-aria/useLocale.mdx b/packages/dev/s2-docs/pages/react-aria/useLocale.mdx index a7ed1ebddb1..978bc50bd40 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLocale.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLocale.mdx @@ -9,7 +9,6 @@ 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/i18n'; diff --git a/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx b/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx index a122d53ed67..35d39fbb6e5 100644 --- a/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx @@ -9,7 +9,6 @@ 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/i18n'; From 898abd8ae0f6dc6f30152a803c231ef87447ef5f Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:06:45 -0700 Subject: [PATCH 08/14] add page description --- packages/dev/s2-docs/pages/react-aria/FocusRing.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/FocusScope.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/mergeProps.mdx | 4 ++-- packages/dev/s2-docs/pages/react-aria/useClipboard.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useDrag.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useDrop.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useField.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx | 2 ++ packages/dev/s2-docs/pages/react-aria/useId.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useLabel.mdx | 3 ++- packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx | 3 ++- 14 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx index ff00514e71d..52d8a4aac79 100644 --- a/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx @@ -14,10 +14,11 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; export const section = 'Focus'; -export const description = 'Implementing collections in React Aria'; # 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. diff --git a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx index 56c4c87c363..bf41919a10d 100644 --- a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx @@ -14,10 +14,11 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; export const section = 'Focus'; -export const description = 'Implementing collections in React Aria'; # FocusScope +{docs.exports.FocusScope.description} + ## Introduction `FocusScope` is a utility component that can be used to manage focus for its descendants. diff --git a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx index 9e383a3b3d7..a275579b6e8 100644 --- a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/overlays'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Server Side Rendering'; -export const description = 'Implementing collections in React Aria'; # PortalProvider +{docs.exports.UNSAFE_PortalProvider.description} + ## Introduction `UNSAFE_PortalProvider` is a utility wrapper component that can be used to set where components like diff --git a/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx index f21f37315c5..14fb52ee5fe 100644 --- a/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx +++ b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/visually-hidden'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; # VisuallyHidden +{docs.exports.VisuallyHidden.description} + ## Introduction `VisuallyHidden` is a utility component that can be used to hide its children visually, diff --git a/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx index 1c35964f9d6..fee98fa10a5 100644 --- a/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx +++ b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx @@ -13,11 +13,11 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; - # mergeProps +{docs.exports.mergeProps.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx index bb601142059..7ca9dbd74af 100644 --- a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx @@ -13,10 +13,11 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; export const section = 'Drag and Drop'; -export const description = 'Implementing collections in React Aria'; # useClipboard +{docs.exports.useClipboard.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx index 7809f596e45..7f028799fa3 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx @@ -14,10 +14,11 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; export const section = 'Drag and Drop'; -export const description = 'Implementing collections in React Aria'; # useDrag +{docs.exports.useDrag.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx index 1e02f392ca8..1f7e1c93c53 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx @@ -15,10 +15,11 @@ import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; export const section = 'Drag and Drop'; -export const description = 'Implementing collections in React Aria'; # useDrop +{docs.exports.useDrop.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useField.mdx b/packages/dev/s2-docs/pages/react-aria/useField.mdx index f4a127bf099..41125ea5648 100644 --- a/packages/dev/s2-docs/pages/react-aria/useField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useField.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/label'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; # useField +{docs.exports.useField.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx index 5e1af34d74b..3cb7dddea29 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx @@ -18,6 +18,8 @@ export const description = 'Implementing collections in React Aria'; # useFocusRing +{docs.exports.useFocusRing.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useId.mdx b/packages/dev/s2-docs/pages/react-aria/useId.mdx index 79f0e08c09e..799434d51ef 100644 --- a/packages/dev/s2-docs/pages/react-aria/useId.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useId.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; # useId +{docs.exports.useId.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx b/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx index bc3c13a71e8..9f9a882d531 100644 --- a/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx @@ -14,10 +14,11 @@ import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Server Side Rendering'; -export const description = 'Implementing collections in React Aria'; # useIsSSR +{docs.exports.useIsSSR.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useLabel.mdx b/packages/dev/s2-docs/pages/react-aria/useLabel.mdx index af397034085..116c53ce63a 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLabel.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLabel.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/label'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; # useLabel +{docs.exports.useLabel.description} + ## API diff --git a/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx b/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx index 176ca62b8b8..fc2d0392b4c 100644 --- a/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx @@ -13,10 +13,11 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; # useObjectRef +{docs.exports.useObjectRef.description} + ## API From e810710a94d1effac4d811c8fc99a225107d05fe Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:50:44 -0700 Subject: [PATCH 09/14] fix types --- .../s2-docs/pages/react-aria/DropTarget.tsx | 25 +++++++++++-------- .../pages/react-aria/MyToastRegion.tsx | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx index 3927ea1e42a..2f4cb2e69ea 100644 --- a/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx +++ b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx @@ -1,19 +1,24 @@ "use client"; -import React from 'react'; +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 [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 + (e.items as TextDropItem[]) .filter(item => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type'))) - .map(async (item: TextDropItem) => { + .map(async item => { if (item.types.has('my-app-custom-type')) { return JSON.parse(await item.getText('my-app-custom-type')); } else { @@ -25,16 +30,16 @@ export function DropTarget() { } }); - let message: string[] = ['Drop here']; + let message: JSX.Element[] = [
{`Drop here`}
]; if (dropped) { - message = dropped.map(d => { - let message = d.message; + message = dropped.map((d, index) => { + let m = d.message; if (d.style === 'bold') { - message = {message}; + message = [{m}]; } else if (d.style === 'italic') { - message = {message}; + message = [{m}]; } - return
{message}
; + return
{message}
; }); } diff --git a/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx b/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx index 7f3ccc9c865..3e5e733fe74 100644 --- a/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx +++ b/packages/dev/s2-docs/pages/react-aria/MyToastRegion.tsx @@ -8,7 +8,7 @@ interface MyToastContent { description?: string } -export function MyToastRegion({queue}) { +export function MyToastRegion({queue}: {queue: ToastQueue}) { return ( {({toast}) => ( From ebdd889504604fdb483be2c455cbc70b617c46e7 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:19:05 -0700 Subject: [PATCH 10/14] fix lint --- packages/dev/s2-docs/src/FunctionAPI.tsx | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/dev/s2-docs/src/FunctionAPI.tsx b/packages/dev/s2-docs/src/FunctionAPI.tsx index c0b0ae609c3..4bbe132fb92 100644 --- a/packages/dev/s2-docs/src/FunctionAPI.tsx +++ b/packages/dev/s2-docs/src/FunctionAPI.tsx @@ -10,25 +10,24 @@ * governing permissions and limitations under the License. */ -import {Indent, JoinList, Type, TypeParameters, setLinks} from './types'; -import {styles as codeStyles} from './Code'; +import {Indent, JoinList, setLinks, Type, TypeParameters} from './types'; import React from 'react'; +import {styles as codeStyles} from './Code'; export function FunctionAPI({function: func, links}) { let {name, parameters, return: returnType, typeParameters} = func; - if (links) { - setLinks(links) + setLinks(links); } return ( - - {name} - - - - - {': '} - - + + {name} + + + + + {': '} + + ); -} \ No newline at end of file +} From 844f2a5eae8f819691aa836fa42c16c32d4129aa Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:47:17 -0700 Subject: [PATCH 11/14] actually fix lint --- packages/dev/s2-docs/src/FunctionAPI.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/src/FunctionAPI.tsx b/packages/dev/s2-docs/src/FunctionAPI.tsx index 4bbe132fb92..e6a7e272364 100644 --- a/packages/dev/s2-docs/src/FunctionAPI.tsx +++ b/packages/dev/s2-docs/src/FunctionAPI.tsx @@ -10,9 +10,9 @@ * governing permissions and limitations under the License. */ +import {styles as codeStyles} from './Code'; import {Indent, JoinList, setLinks, Type, TypeParameters} from './types'; import React from 'react'; -import {styles as codeStyles} from './Code'; export function FunctionAPI({function: func, links}) { let {name, parameters, return: returnType, typeParameters} = func; From 8fff5ce7c0e360fdde207c31b5a235ce34caa2e1 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:47:31 -0700 Subject: [PATCH 12/14] remove console log --- packages/dev/s2-docs/src/ClassAPI.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dev/s2-docs/src/ClassAPI.tsx b/packages/dev/s2-docs/src/ClassAPI.tsx index 457b50d39f1..8d837a3e283 100644 --- a/packages/dev/s2-docs/src/ClassAPI.tsx +++ b/packages/dev/s2-docs/src/ClassAPI.tsx @@ -7,7 +7,6 @@ interface ClassAPIProps { } export function ClassAPI({class: c, links}: ClassAPIProps) { - console.log('c', c); setLinks(links); return ( From 9427e5295c4aaa81ab7b9635a382b1a07f7eafa5 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:11:16 -0700 Subject: [PATCH 13/14] update css import --- packages/dev/s2-docs/pages/react-aria/hooks.css | 5 ----- packages/dev/s2-docs/pages/react-aria/useClipboard.mdx | 1 + .../dev/s2-docs/pages/react-aria/useClipboardExample.css | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 packages/dev/s2-docs/pages/react-aria/hooks.css diff --git a/packages/dev/s2-docs/pages/react-aria/hooks.css b/packages/dev/s2-docs/pages/react-aria/hooks.css deleted file mode 100644 index 10903313525..00000000000 --- a/packages/dev/s2-docs/pages/react-aria/hooks.css +++ /dev/null @@ -1,5 +0,0 @@ -@import '../../../../../starters/docs/src/theme.css'; - -:root { - --tint: var(--blue) -} diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx index 7ca9dbd74af..e044de4e0b1 100644 --- a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx @@ -38,6 +38,7 @@ import React from 'react'; import type {TextDropItem} from '@react-aria/dnd'; import {useClipboard} from '@react-aria/dnd'; import './useClipboardExample.css'; +import 'vanilla-starter/theme.css'; function Copyable() { let {clipboardProps} = useClipboard({ diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css b/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css index 971bb696cda..e4412a91ecf 100644 --- a/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css +++ b/packages/dev/s2-docs/pages/react-aria/useClipboardExample.css @@ -1,5 +1,3 @@ -@import './hooks.css'; - [role=textbox] { display: inline-flex; align-items: center; From ce757a18c9b380a63d942c4e87571a37204087f0 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:53:00 -0700 Subject: [PATCH 14/14] fix styles --- .../pages/react-aria/PortalProvider.mdx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx index a275579b6e8..c161d7d7b79 100644 --- a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx @@ -45,12 +45,11 @@ 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 'react-aria-components'; +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, @@ -88,7 +87,70 @@ function App() { ```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