Skip to content

Commit

Permalink
Merge branch 'main' of github.com:adobe/react-spectrum into S1_treeview
Browse files Browse the repository at this point in the history
  • Loading branch information
LFDanLu committed Apr 24, 2024
2 parents 503c6e3 + a597a0c commit 3882ad3
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 37 deletions.
18 changes: 16 additions & 2 deletions packages/@react-aria/dnd/src/useDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export interface DragOptions {
* Whether the item has an explicit focusable drag affordance to initiate accessible drag and drop mode.
* If true, the dragProps will omit these event handlers, and they will be applied to dragButtonProps instead.
*/
hasDragButton?: boolean
hasDragButton?: boolean,
/**
* Whether the drag operation is disabled. If true, the element will not be draggable.
*/
isDisabled?: boolean
}

export interface DragResult {
Expand Down Expand Up @@ -70,7 +74,7 @@ const MESSAGES = {
* based drag and drop, in addition to full parity for keyboard and screen reader users.
*/
export function useDrag(options: DragOptions): DragResult {
let {hasDragButton} = options;
let {hasDragButton, isDisabled} = options;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/dnd');
let state = useRef({
options,
Expand Down Expand Up @@ -332,6 +336,16 @@ export function useDrag(options: DragOptions): DragResult {
};
}

if (isDisabled) {
return {
dragProps: {
draggable: 'false'
},
dragButtonProps: {},
isDragging: false
};
}

return {
dragProps: {
...interactions,
Expand Down
10 changes: 8 additions & 2 deletions packages/@react-aria/dnd/stories/dnd.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,12 @@ export const MultipleCollectionDropTargets = {

export const Reorderable = () => <ReorderableGridExample />;

export function Draggable() {
export const DraggableDisabled = {
render: () => <Draggable isDisabled />,
name: 'Draggable isDisabled'
};

export function Draggable({isDisabled = false}) {
let {dragProps, isDragging} = useDrag({
getItems() {
return [{
Expand All @@ -262,7 +267,8 @@ export function Draggable() {
},
onDragStart: action('onDragStart'),
// onDragMove: action('onDragMove'),
onDragEnd: action('onDragEnd')
onDragEnd: action('onDragEnd'),
isDisabled
});

let {clipboardProps} = useClipboard({
Expand Down
128 changes: 128 additions & 0 deletions packages/@react-aria/dnd/test/dnd.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,64 @@ describe('useDrag and useDrop', function () {
});
});

it('useDrag should support isDisabled', async () => {
let onDragStart = jest.fn();
let onDragMove = jest.fn();
let onDragEnd = jest.fn();
let onDropEnter = jest.fn();
let onDropMove = jest.fn();
let onDrop = jest.fn();
let tree = render(<>
<Draggable isDisabled onDragStart={onDragStart} onDragMove={onDragMove} onDragEnd={onDragEnd} />
<Droppable onDropEnter={onDropEnter} onDropMove={onDropMove} onDrop={onDrop} />
</>);
let draggable = tree.getByText('Drag me');
let droppable = tree.getByText('Drop here');
expect(draggable).toHaveAttribute('draggable', 'false');

let dataTransfer = new DataTransfer();
fireEvent(draggable, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0}));
act(() => jest.runAllTimers());
expect(draggable).toHaveAttribute('data-dragging', 'false');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

expect(onDragStart).not.toHaveBeenCalled();
expect(onDragMove).not.toHaveBeenCalled();
expect(onDragEnd).not.toHaveBeenCalled();
expect(onDropEnter).not.toHaveBeenCalled();
expect(onDropMove).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
});

it('useDrop should support isDisabled', async () => {
let onDragStart = jest.fn();
let onDragMove = jest.fn();
let onDragEnd = jest.fn();
let onDropEnter = jest.fn();
let onDropMove = jest.fn();
let onDrop = jest.fn();
let tree = render(<>
<Draggable onDragStart={onDragStart} onDragMove={onDragMove} onDragEnd={onDragEnd} />
<Droppable isDisabled onDropEnter={onDropEnter} onDropMove={onDropMove} onDrop={onDrop} />
</>);
let draggable = tree.getByText('Drag me');
let droppable = tree.getByText('Drop here');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

let dataTransfer = new DataTransfer();
fireEvent(draggable, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0}));
act(() => jest.runAllTimers());
expect(draggable).toHaveAttribute('data-dragging', 'true');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

expect(onDragStart).toHaveBeenCalledTimes(1);
expect(onDragMove).not.toHaveBeenCalled();
expect(onDragEnd).not.toHaveBeenCalled();
expect(onDropEnter).not.toHaveBeenCalled();
expect(onDropMove).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
});

describe('events', () => {
it('fires onDragMove only when the drag actually moves', () => {
let onDragStart = jest.fn();
Expand Down Expand Up @@ -1433,6 +1491,76 @@ describe('useDrag and useDrop', function () {
expect(droppable2).toHaveAttribute('data-droptarget', 'false');
});

it('useDrag should support isDisabled', async () => {
let onDragStart = jest.fn();
let onDragMove = jest.fn();
let onDragEnd = jest.fn();
let onDropEnter = jest.fn();
let onDropMove = jest.fn();
let onDropExit = jest.fn();
let onDrop = jest.fn();
let tree = render(<>
<Draggable isDisabled onDragStart={onDragStart} onDragMove={onDragMove} onDragEnd={onDragEnd} />
<Droppable onDropEnter={onDropEnter} onDropMove={onDropMove} onDrop={onDrop} onDropExit={onDropExit} />
</>);
let draggable = tree.getByText('Drag me');
let droppable = tree.getByText('Drop here');
expect(draggable).toHaveAttribute('draggable', 'false');
expect(draggable).toHaveAttribute('data-dragging', 'false');

await user.tab();
expect(document.activeElement).toBe(draggable);
expect(draggable).not.toHaveAttribute('aria-describedby');

await user.keyboard('{Enter}');
act(() => jest.runAllTimers());

expect(document.activeElement).toBe(draggable);
expect(draggable).toHaveAttribute('data-dragging', 'false');
expect(droppable).toHaveAttribute('data-droptarget', 'false');
expect(onDragStart).not.toHaveBeenCalled();
expect(onDragMove).not.toHaveBeenCalled();
expect(onDragEnd).not.toHaveBeenCalled();
expect(onDropEnter).not.toHaveBeenCalled();
expect(onDropMove).not.toHaveBeenCalled();
expect(onDropExit).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
});

it('useDrop should support isDisabled', async () => {
let onDragStart = jest.fn();
let onDragMove = jest.fn();
let onDragEnd = jest.fn();
let onDropEnter = jest.fn();
let onDropMove = jest.fn();
let onDropExit = jest.fn();
let onDrop = jest.fn();
let tree = render(<>
<Draggable onDragStart={onDragStart} onDragMove={onDragMove} onDragEnd={onDragEnd} />
<Droppable isDisabled onDropEnter={onDropEnter} onDropMove={onDropMove} onDrop={onDrop} onDropExit={onDropExit} />
</>);
let draggable = tree.getByText('Drag me');
let droppable = tree.getByText('Drop here');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

await user.tab();
expect(document.activeElement).toBe(draggable);

await user.keyboard('{Enter}');
act(() => jest.runAllTimers());

expect(document.activeElement).toBe(draggable);
expect(droppable).toHaveAttribute('data-droptarget', 'false');

expect(onDragStart).toHaveBeenCalledTimes(1);
expect(onDragMove).not.toHaveBeenCalled();
expect(onDragEnd).not.toHaveBeenCalled();
expect(onDropEnter).not.toHaveBeenCalled();
expect(onDropMove).not.toHaveBeenCalled();
expect(onDropExit).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
});

describe('keyboard navigation', () => {
it('should Tab forward and skip non drop target elements', async () => {
let tree = render(<>
Expand Down
11 changes: 5 additions & 6 deletions packages/@react-spectrum/menu/docs/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,9 @@ callbacks will not fire for items made unavailable by a ContextualHelpTrigger.

The example below illustrates how one would setup a Menu to use ContextualHelpTrigger.

```tsx example
```tsx example keepIndividualImports
import {Content, Dialog, Heading} from '@adobe/react-spectrum';
import {ContextualHelpTrigger} from '@react-spectrum/menu';
import {Dialog} from '@react-spectrum/dialog';
import {Heading} from '@react-spectrum/text';
import {Content} from '@react-spectrum/view';

<MenuTrigger>
<ActionButton>Edit</ActionButton>
Expand Down Expand Up @@ -374,7 +372,8 @@ Each submenu's Menu accepts its own set of menu props, allowing you to customize

### Static

```tsx example
```tsx example keepIndividualImports

import {SubmenuTrigger} from '@react-spectrum/menu';

<MenuTrigger>
Expand Down Expand Up @@ -406,7 +405,7 @@ import {SubmenuTrigger} from '@react-spectrum/menu';

You can define a recursive function to render the nested menu items dynamically.

```tsx example
```tsx example keepIndividualImports
import {SubmenuTrigger} from '@react-spectrum/menu';

let items = [
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/tree/src/TreeView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* Copyright 2024 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
Expand Down
2 changes: 1 addition & 1 deletion packages/dev/parcel-transformer-mdx-docs/MDXTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ function transformExample(node, preRelease, keepIndividualImports) {

traverse(ast, {
ImportDeclaration(path) {
if (/^(@react-spectrum|@react-aria|@react-stately)/.test(path.node.source.value) && !keepIndividualImports) {
if (/^(@react-spectrum|@react-aria|@react-stately)/.test(path.node.source.value) && !/test-utils/.test(path.node.source.value) && !keepIndividualImports) {
let lib = path.node.source.value.split('/')[0];
let s = path.node.importKind === 'type' ? typeSpecifiers : specifiers;
if (!s[lib]) {
Expand Down
9 changes: 6 additions & 3 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {CollectionProps, ItemRenderProps, useCachedChildren, useCollection, useS
import {ContextValue, DEFAULT_SLOT, forwardRefType, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
import {DragAndDropContext, DragAndDropHooks, DropIndicator, DropIndicatorContext, DropIndicatorProps} from './useDragAndDrop';
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
import {Key, LinkDOMProps} from '@react-types/shared';
import {HoverEvents, Key, LinkDOMProps} from '@react-types/shared';
import {ListStateContext} from './ListBox';
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, RefObject, useContext, useEffect, useRef} from 'react';
import {TextContext} from './Text';
Expand Down Expand Up @@ -223,7 +223,7 @@ export {_GridList as GridList};

export interface GridListItemRenderProps extends ItemRenderProps {}

export interface GridListItemProps<T = object> extends RenderProps<GridListItemRenderProps>, LinkDOMProps {
export interface GridListItemProps<T = object> extends RenderProps<GridListItemRenderProps>, LinkDOMProps, HoverEvents {
/** The unique id of the item. */
id?: Key,
/** The object value that this item represents. When using dynamic collections, this is set automatically. */
Expand Down Expand Up @@ -263,7 +263,10 @@ function GridListRow({item}) {
);

let {hoverProps, isHovered} = useHover({
isDisabled: !states.allowsSelection && !states.hasAction
isDisabled: !states.allowsSelection && !states.hasAction,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
});

let {isFocusVisible, focusProps} = useFocusRing();
Expand Down
11 changes: 8 additions & 3 deletions packages/react-aria-components/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {MenuTriggerProps as BaseMenuTriggerProps, Node, TreeState, useMenuTrigge
import {ContextValue, forwardRefType, Provider, RenderProps, ScrollableProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
import {filterDOMProps, mergeRefs, useObjectRef, useResizeObserver} from '@react-aria/utils';
import {Header} from './Header';
import {Key, LinkDOMProps} from '@react-types/shared';
import {HoverEvents, Key, LinkDOMProps} from '@react-types/shared';
import {KeyboardContext} from './Keyboard';
import {OverlayTriggerStateContext} from './Dialog';
import {PopoverContext, PopoverProps} from './Popover';
Expand Down Expand Up @@ -284,7 +284,7 @@ export interface MenuItemRenderProps extends ItemRenderProps {
isOpen: boolean
}

export interface MenuItemProps<T = object> extends RenderProps<MenuItemRenderProps>, LinkDOMProps {
export interface MenuItemProps<T = object> extends RenderProps<MenuItemRenderProps>, LinkDOMProps, HoverEvents {
/** The unique id of the item. */
id?: Key,
/** The object value that this item represents. When using dynamic collections, this is set automatically. */
Expand Down Expand Up @@ -320,7 +320,12 @@ function MenuItemInner<T>({item}: MenuItemInnerProps<T>) {

let props: MenuItemProps<T> = item.props;
let {isFocusVisible, focusProps} = useFocusRing();
let {hoverProps, isHovered} = useHover({isDisabled: states.isDisabled});
let {hoverProps, isHovered} = useHover({
isDisabled: states.isDisabled,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
});
let renderProps = useRenderProps({
...props,
id: undefined,
Expand Down
7 changes: 5 additions & 2 deletions packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ export {_TableBody as TableBody};

export interface RowRenderProps extends ItemRenderProps {}

export interface RowProps<T> extends StyleRenderProps<RowRenderProps>, LinkDOMProps {
export interface RowProps<T> extends StyleRenderProps<RowRenderProps>, LinkDOMProps, HoverEvents {
/** The unique id of the row. */
id?: Key,
/** A list of columns used when dynamically rendering cells. */
Expand Down Expand Up @@ -1018,7 +1018,10 @@ function TableRow<T>({item}: {item: GridNode<T>}) {
);
let {isFocused, isFocusVisible, focusProps} = useFocusRing();
let {hoverProps, isHovered} = useHover({
isDisabled: !states.allowsSelection && !states.hasAction
isDisabled: !states.allowsSelection && !states.hasAction,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
});

let {checkboxProps} = useTableSelectionCheckbox(
Expand Down
9 changes: 6 additions & 3 deletions packages/react-aria-components/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {ButtonContext} from './Button';
import {CollectionDocumentContext, CollectionProps, ItemRenderProps, useCachedChildren, useCollectionDocument, useCollectionPortal, useSSRCollectionNode} from './Collection';
import {ContextValue, DOMProps, forwardRefType, Provider, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
import {filterDOMProps, mergeProps, useObjectRef} from '@react-aria/utils';
import {Key, LinkDOMProps} from '@react-types/shared';
import {HoverEvents, Key, LinkDOMProps} from '@react-types/shared';
import {LabelContext} from './Label';
import {ListState, Node, useListState} from 'react-stately';
import {ListStateContext} from './ListBox';
Expand Down Expand Up @@ -186,7 +186,7 @@ export interface TagRenderProps extends Omit<ItemRenderProps, 'allowsDragging' |
allowsRemoving: boolean
}

export interface TagProps extends RenderProps<TagRenderProps>, LinkDOMProps {
export interface TagProps extends RenderProps<TagRenderProps>, LinkDOMProps, HoverEvents {
/** A unique id for the tag. */
id?: Key,
/**
Expand Down Expand Up @@ -215,7 +215,10 @@ function TagItem({item}) {
let {rowProps, gridCellProps, removeButtonProps, ...states} = useTag({item}, state, ref);

let {hoverProps, isHovered} = useHover({
isDisabled: !states.allowsSelection
isDisabled: !states.allowsSelection,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
});

let props: TagProps = item.props;
Expand Down
11 changes: 7 additions & 4 deletions packages/react-aria-components/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {BaseCollection, CollectionProps, CollectionRendererContext, ItemRenderPr
import {ButtonContext} from './Button';
import {CheckboxContext} from './Checkbox';
import {ContextValue, DEFAULT_SLOT, forwardRefType, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
import {DisabledBehavior, Expandable, Key, LinkDOMProps} from '@react-types/shared';
import {DisabledBehavior, Expandable, HoverEvents, Key, LinkDOMProps} from '@react-types/shared';
import {filterDOMProps, isAndroid, useObjectRef} from '@react-aria/utils';
import {FocusScope, mergeProps, useFocusRing, useGridListSelectionCheckbox, useHover, useLocalizedStringFormatter} from 'react-aria';
import {Collection as ICollection, Node, SelectionBehavior, TreeState, useTreeState} from 'react-stately';
Expand Down Expand Up @@ -275,8 +275,8 @@ export interface TreeItemRenderProps extends Omit<ItemRenderProps, 'allowsDraggi
isFocusVisibleWithin: boolean
}

export interface TreeItemProps<T = object> extends StyleRenderProps<TreeItemRenderProps>, LinkDOMProps {
/** The unique id of the tree item. */
export interface TreeItemProps<T = object> extends StyleRenderProps<TreeItemRenderProps>, LinkDOMProps, HoverEvents {
/** The unique id of the tree row. */
id?: Key,
/** The object value that this tree item represents. When using dynamic collections, this is set automatically. */
value?: T,
Expand Down Expand Up @@ -357,7 +357,10 @@ function TreeRow<T>({item}: {item: Node<T>}) {
let level = rowProps['aria-level'] || 1;

let {hoverProps, isHovered} = useHover({
isDisabled: !states.allowsSelection && !states.hasAction
isDisabled: !states.allowsSelection && !states.hasAction,
onHoverStart: item.props.onHoverStart,
onHoverChange: item.props.onHoverChange,
onHoverEnd: item.props.onHoverEnd
});

let {isFocusVisible, focusProps} = useFocusRing();
Expand Down
Loading

0 comments on commit 3882ad3

Please sign in to comment.