Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/@react-spectrum/s2/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {useScale} from './utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';
import {Virtualizer} from 'react-aria-components/Virtualizer';

export interface ListViewProps<T> extends Omit<GridListProps<T>, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps {
export interface ListViewProps<T> extends Omit<GridListProps<T>, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | 'orientation' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps {
/** Spectrum-defined styles, returned by the `style()` macro. */
styles?: StylesPropWithHeight,
/** The current loading state of the ListView. */
Expand Down Expand Up @@ -864,4 +864,3 @@ export function ListViewItem(props: ListViewItemProps): ReactNode {
</GridListItem>
);
}

2 changes: 1 addition & 1 deletion packages/@react-spectrum/s2/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T
hideArrow>
<div
style={UNSAFE_style}
className={(UNSAFE_className || '') + wrappingDiv}>
className={(UNSAFE_className || '') + mergeStyles(wrappingDiv, styles)}>
{content}
</div>
</Popover>
Expand Down
37 changes: 37 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/GridList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,43 @@ function Example(props) {
}
```

## Layouts

Use the `layout` and `orientation` props to arrange items in horizontal and vertical stacks and grids. This affects keyboard navigation and drag and drop behavior.

```tsx render docs={docs.exports.GridList} links={docs.links} props={['layout', 'orientation', 'keyboardNavigationBehavior']} initialProps={{layout: 'grid', orientation: 'horizontal', keyboardNavigationBehavior: 'tab'}} wide
"use client";
import {Text} from 'react-aria-components';
import {GridList, GridListItem} from 'vanilla-starter/GridList';

///- begin collapse -///
let photos = [
{id: 1, title: 'Desert Sunset', description: 'PNG • 2/3/2024', src: 'https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop'},
{id: 2, title: 'Hiking Trail', description: 'JPEG • 1/10/2022', src: 'https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop'},
{id: 3, title: 'Lion', description: 'JPEG • 8/28/2021', src: 'https://images.unsplash.com/photo-1629812456605-4a044aa38fbc?q=80&w=600&auto=format&fit=crop'},
{id: 4, title: 'Mountain Sunrise', description: 'PNG • 3/15/2015', src: 'https://images.unsplash.com/photo-1722172118908-1a97c312ce8c?q=80&w=600&auto=format&fit=crop'},
{id: 5, title: 'Giraffe tongue', description: 'PNG • 11/27/2019', src: 'https://images.unsplash.com/photo-1574870111867-089730e5a72b?q=80&w=600&auto=format&fit=crop'},
{id: 6, title: 'Golden Hour', description: 'WEBP • 7/24/2024', src: 'https://images.unsplash.com/photo-1718378037953-ab21bf2cf771?q=80&w=600&auto=format&fit=crop'},
];
///- end collapse -///

<GridList
/*- begin highlight -*/
/* PROPS */
/*- end highlight -*/
aria-label="Photos"
items={photos}
selectionMode="multiple">
{item => (
<GridListItem textValue={item.title}>
<img src={item.src} alt="" />
<Text>{item.title}</Text>
<Text slot="description">{item.description}</Text>
</GridListItem>
)}
</GridList>
```

## Drag and drop

GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=GridList) to learn more.
Expand Down
260 changes: 255 additions & 5 deletions packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions packages/dev/s2-docs/src/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,16 +256,18 @@ export function SideNavItem(props) {

export function SideNavLink(props) {
let linkRef = useRef<HTMLAnchorElement | null>(null);
let shouldAutoScrollOnMount = useRef(props.isSelected);
let selected = useContext(SideNavContext);
let {isExternal, ...linkProps} = props;

useEffect(() => {
let link = linkRef.current;
if (!link || !props.isSelected) {
if (!link || !props.isSelected || !shouldAutoScrollOnMount.current) {
return;
}

link.scrollIntoView({block: 'start', behavior: 'smooth'});
shouldAutoScrollOnMount.current = false;
link.scrollIntoView({block: 'start'});
}, [props.isSelected]);

return (
Expand Down
6 changes: 3 additions & 3 deletions packages/dev/test-utils/src/renderOverride.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ function customRender(ui: Parameters<typeof render>[0], options?: Parameters<typ
let reactTestingLibrary = require('@testing-library/react');
// export renderHook and actHook from testing-library/react-hooks library if they don't exist in @testing-library/react
// (i.e. renderHook is only in v13+ of testing library)
export let renderHook = reactTestingLibrary.renderHook as typeof originalRenderHook;
export let renderHook: typeof originalRenderHook = (render, options) => reactTestingLibrary.renderHook(render, {wrapper: StrictModeWrapper, ...options});
export let actHook = reactTestingLibrary.act as typeof originalAct;
if (!renderHook) {
if (!reactTestingLibrary.renderHook) {
let rhtl = require('@testing-library/react-hooks');
renderHook = rhtl.renderHook;
renderHook = (render, options) => rhtl.renderHook(render, {wrapper: StrictModeWrapper, ...options});
actHook = rhtl.act;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,5 @@ export type {ListOptions as ListDataOptions, ListData} from 'react-stately/useLi
export type {TreeOptions as TreeDataOptions, TreeData} from 'react-stately/useTreeData';
export type {AsyncListOptions, AsyncListData, AsyncListLoadFunction, AsyncListLoadOptions, AsyncListStateUpdate} from 'react-stately/useAsyncList';
export type {AutocompleteState} from 'react-stately/private/autocomplete/useAutocompleteState';
export type {ListLayoutOptions, GridLayoutOptions, WaterfallLayoutOptions} from 'react-stately/useVirtualizerState';
export type {ListLayoutOptions, GridLayoutOptions, TableLayoutProps, WaterfallLayoutOptions} from 'react-stately/useVirtualizerState';
export type {RangeValue, ValidationResult, RouterConfig} from '@react-types/shared';
14 changes: 10 additions & 4 deletions packages/react-aria/src/dnd/useDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,16 @@ export function useDrop(options: DropOptions): DropResult {
// events will never be fired for these. This can happen, for example, with drop
// indicators between items, which disappear when the drop target changes.

state.dragOverElements.delete(getEventTarget(e));
for (let element of state.dragOverElements) {
if (!nodeContains(e.currentTarget, element)) {
state.dragOverElements.delete(element);
let target = getEventTarget(e);
state.dragOverElements.delete(target);

// Only remove stale elements when leaving the drop target itself.
// Avoids issues with portal children bubbling dragleave events through the React tree.
if (target === e.currentTarget) {
for (let element of state.dragOverElements) {
if (!nodeContains(e.currentTarget, element)) {
state.dragOverElements.delete(element);
}
}
}

Expand Down
48 changes: 48 additions & 0 deletions packages/react-aria/test/dnd/dnd.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {DataTransfer, DataTransferItem, DragEvent, FileSystemDirectoryEntry, Fil
import {Draggable, Droppable} from './examples';
import {DragTypes} from '../../src/dnd/utils';
import React, {useEffect} from 'react';
import ReactDOM from 'react-dom';
import userEvent from '@testing-library/user-event';

function pointerEvent(type, opts) {
Expand Down Expand Up @@ -397,6 +398,53 @@ describe('useDrag and useDrop', function () {
expect(onDropExit).toHaveBeenCalledTimes(1);
});

it('does not fire onDropEnter and onDropExit repeatedly for portal children', () => {
let onDropEnter = jest.fn();
let onDropExit = jest.fn();
let portalContainer = document.createElement('div');
document.body.appendChild(portalContainer);

try {
let tree = render(
<Droppable onDropEnter={onDropEnter} onDropExit={onDropExit}>
<>
<div>Drop here</div>
{ReactDOM.createPortal(
<>
<div>Portal child 1</div>
<div>Portal child 2</div>
</>,
portalContainer
)}
</>
</Droppable>
);

let portalChild1 = tree.getByText('Portal child 1');
let portalChild2 = tree.getByText('Portal child 2');

let dataTransfer = new DataTransfer();
fireEvent(portalChild1, new DragEvent('dragenter', {dataTransfer, clientX: 1, clientY: 1}));
expect(onDropEnter).toHaveBeenCalledTimes(1);
expect(onDropExit).not.toHaveBeenCalled();

fireEvent(portalChild2, new DragEvent('dragenter', {dataTransfer, clientX: 1, clientY: 1, relatedTarget: portalChild1}));
fireEvent(portalChild1, new DragEvent('dragleave', {dataTransfer, clientX: 1, clientY: 1, relatedTarget: portalChild2}));
expect(onDropEnter).toHaveBeenCalledTimes(1);
expect(onDropExit).not.toHaveBeenCalled();

fireEvent(portalChild1, new DragEvent('dragenter', {dataTransfer, clientX: 1, clientY: 1, relatedTarget: portalChild2}));
fireEvent(portalChild2, new DragEvent('dragleave', {dataTransfer, clientX: 1, clientY: 1, relatedTarget: portalChild1}));
expect(onDropEnter).toHaveBeenCalledTimes(1);
expect(onDropExit).not.toHaveBeenCalled();

fireEvent(portalChild1, new DragEvent('dragleave', {dataTransfer, clientX: 1, clientY: 1}));
expect(onDropExit).toHaveBeenCalledTimes(1);
} finally {
portalContainer.remove();
}
});

describe('nested drag targets', () => {
let onDragStartParent = jest.fn();
let onDragMoveParent = jest.fn();
Expand Down
2 changes: 1 addition & 1 deletion packages/react-stately/src/data/useTreeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
},
insert(parentKey: Key | null, index: number, ...values: T[]) {
setItems(({items, nodeMap: originalMap}) => {
let {items: newNodes, nodeMap: newMap} = buildTree(values, originalMap, parentKey);
let {items: newNodes, nodeMap: newMap} = buildTree(values, new Map(originalMap), parentKey);

// If parentKey is null, insert into the root.
if (parentKey == null) {
Expand Down
Loading
Loading