Skip to content

Commit 3dc96e5

Browse files
authored
ListView: Draggable Rows (#2593)
1 parent c630dea commit 3dc96e5

File tree

22 files changed

+1028
-140
lines changed

22 files changed

+1028
-140
lines changed

bin/imports.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const substrings = ['-', '+'];
1818

1919
module.exports = function (context) {
2020
let processNode = (node) => {
21-
if (!node.source) {
21+
if (!node.source || node.importKind === 'type') {
2222
return;
2323
}
2424

packages/@react-aria/dnd/src/useDraggableItem.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import intlMessages from '../intl/*.json';
1818
import {useDrag} from './useDrag';
1919
import {useMessageFormatter} from '@react-aria/i18n';
2020

21-
interface DraggableItemProps {
21+
export interface DraggableItemProps {
2222
key: Key
2323
}
2424

25-
interface DraggableItemResult {
25+
export interface DraggableItemResult {
2626
dragProps: HTMLAttributes<HTMLElement>,
2727
dragButtonProps: AriaButtonProps
2828
}

packages/@react-aria/dnd/stories/dnd.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function Draggable() {
233233
);
234234
}
235235

236-
function Droppable({type, children, actionId = ''}: any) {
236+
export function Droppable({type, children, actionId = ''}: any) {
237237
let ref = React.useRef();
238238
let {dropProps, isDropTarget} = useDrop({
239239
ref,

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export function usePress(props: PressHookProps): PressResult {
321321

322322
// Due to browser inconsistencies, especially on mobile browsers, we prevent
323323
// default on pointer down and handle focusing the pressable element ourselves.
324-
if (shouldPreventDefault(e.target as Element)) {
324+
if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
325325
e.preventDefault();
326326
}
327327

@@ -359,7 +359,7 @@ export function usePress(props: PressHookProps): PressResult {
359359
// Chrome and Firefox on touch Windows devices require mouse down events
360360
// to be canceled in addition to pointer events, or an extra asynchronous
361361
// focus event will be fired.
362-
if (shouldPreventDefault(e.target as Element)) {
362+
if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
363363
e.preventDefault();
364364
}
365365

@@ -443,7 +443,7 @@ export function usePress(props: PressHookProps): PressResult {
443443

444444
// Due to browser inconsistencies, especially on mobile browsers, we prevent
445445
// default on mouse down and handle focusing the pressable element ourselves.
446-
if (shouldPreventDefault(e.target as Element)) {
446+
if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
447447
e.preventDefault();
448448
}
449449

@@ -764,9 +764,9 @@ function isOverTarget(point: EventPoint, target: HTMLElement) {
764764
return areRectanglesOverlapping(rect, pointRect);
765765
}
766766

767-
function shouldPreventDefault(target: Element) {
768-
// We cannot prevent default if the target is inside a draggable element.
769-
return !target.closest('[draggable="true"]');
767+
function shouldPreventDefault(target: HTMLElement) {
768+
// We cannot prevent default if the target is a draggable element.
769+
return !target.draggable;
770770
}
771771

772772
function shouldPreventDefaultKeyboard(target: Element) {

packages/@react-aria/interactions/test/usePress.test.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {theme} from '@react-spectrum/theme-default';
2121
import {usePress} from '../';
2222

2323
function Example(props) {
24-
let {elementType: ElementType = 'div', style, ...otherProps} = props;
24+
let {elementType: ElementType = 'div', style, draggable, ...otherProps} = props;
2525
let {pressProps} = usePress(otherProps);
26-
return <ElementType {...pressProps} style={style} tabIndex="0">test</ElementType>;
26+
return <ElementType {...pressProps} style={style} tabIndex="0" draggable={draggable}>test</ElementType>;
2727
}
2828

2929
function pointerEvent(type, opts) {
@@ -507,13 +507,26 @@ describe('usePress', function () {
507507
expect(allowDefault).toBe(false);
508508
});
509509

510-
it('should not prevent default when in a draggable container', function () {
510+
it('should still prevent default when pressing on a non draggable + pressable item in a draggable container', function () {
511511
let res = render(
512512
<div draggable="true">
513513
<Example />
514514
</div>
515515
);
516516

517+
let el = res.getByText('test');
518+
let allowDefault = fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse'}));
519+
expect(allowDefault).toBe(false);
520+
521+
allowDefault = fireEvent.mouseDown(el);
522+
expect(allowDefault).toBe(false);
523+
});
524+
525+
it('should not prevent default when pressing on a draggable item', function () {
526+
let res = render(
527+
<Example draggable="true" />
528+
);
529+
517530
let el = res.getByText('test');
518531
let allowDefault = fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse'}));
519532
expect(allowDefault).toBe(true);
@@ -1036,13 +1049,24 @@ describe('usePress', function () {
10361049
expect(allowDefault).toBe(false);
10371050
});
10381051

1039-
it('should not prevent default when in a draggable container', function () {
1052+
it('should still prevent default when pressing on a non draggable + pressable item in a draggable container', function () {
10401053
let res = render(
10411054
<div draggable="true">
10421055
<Example />
10431056
</div>
10441057
);
10451058

1059+
let el = res.getByText('test');
1060+
let allowDefault = fireEvent.mouseDown(el);
1061+
expect(allowDefault).toBe(false);
1062+
});
1063+
1064+
1065+
it('should not prevent default when pressing on a draggable item', function () {
1066+
let res = render(
1067+
<Example draggable="true" />
1068+
);
1069+
10461070
let el = res.getByText('test');
10471071
let allowDefault = fireEvent.mouseDown(el);
10481072
expect(allowDefault).toBe(true);

packages/@react-aria/listbox/src/useOption.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
131131
key,
132132
ref,
133133
shouldSelectOnPressUp,
134+
allowsDifferentPressOrigin: shouldSelectOnPressUp,
134135
isVirtualized,
135136
shouldUseVirtualFocus,
136137
isDisabled

packages/@react-aria/menu/src/useMenuItem.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
152152
selectionManager: state.selectionManager,
153153
key,
154154
ref,
155-
shouldSelectOnPressUp: true
155+
shouldSelectOnPressUp: true,
156+
allowsDifferentPressOrigin: true
156157
});
157158

158159
let {pressProps} = usePress({onPressStart, onPressUp, isDisabled});

packages/@react-aria/selection/src/useSelectableItem.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ interface SelectableItemOptions {
3636
* item causes the UI to disappear immediately (e.g. menus).
3737
*/
3838
shouldSelectOnPressUp?: boolean,
39+
/**
40+
* Whether selection requires the pointer/mouse down and up events to occur on the same target or triggers selection on
41+
* the target of the pointer/mouse up event.
42+
*/
43+
allowsDifferentPressOrigin?: boolean,
3944
/**
4045
* Whether the option is contained in a virtual scroller.
4146
*/
@@ -79,7 +84,8 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
7984
shouldUseVirtualFocus,
8085
focus,
8186
isDisabled,
82-
onAction
87+
onAction,
88+
allowsDifferentPressOrigin
8389
} = options;
8490

8591
let onSelect = (e: PressEvent | LongPressEvent | PointerEvent) => {
@@ -155,13 +161,27 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
155161
}
156162
};
157163

158-
itemPressProps.onPressUp = (e) => {
159-
if (e.pointerType !== 'keyboard') {
160-
onSelect(e);
161-
}
162-
};
164+
// If allowsDifferentPressOrigin, make selection happen on pressUp (e.g. open menu on press down, selection on menu item happens on press up.)
165+
// Otherwise, have selection happen onPress (prevents listview row selection when clicking on interactable elements in the row)
166+
if (!allowsDifferentPressOrigin) {
167+
itemPressProps.onPress = (e) => {
168+
if (e.pointerType !== 'keyboard') {
169+
onSelect(e);
170+
}
163171

164-
itemPressProps.onPress = hasPrimaryAction ? () => onAction() : null;
172+
if (hasPrimaryAction) {
173+
onAction();
174+
}
175+
};
176+
} else {
177+
itemPressProps.onPressUp = (e) => {
178+
if (e.pointerType !== 'keyboard') {
179+
onSelect(e);
180+
}
181+
};
182+
183+
itemPressProps.onPress = hasPrimaryAction ? () => onAction() : null;
184+
}
165185
} else {
166186
// On touch, it feels strange to select on touch down, so we special case this.
167187
itemPressProps.onPressStart = (e) => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @react-spectrum/dnd
2+
3+
This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.

packages/@react-spectrum/dnd/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2022 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
export * from './src';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@react-spectrum/dnd",
3+
"version": "3.0.0-alpha.1",
4+
"description": "Spectrum UI components in React",
5+
"license": "Apache-2.0",
6+
"main": "dist/main.js",
7+
"module": "dist/module.js",
8+
"types": "dist/types.d.ts",
9+
"source": "src/index.ts",
10+
"files": [
11+
"dist",
12+
"src"
13+
],
14+
"sideEffects": [
15+
"*.css"
16+
],
17+
"targets": {
18+
"main": {
19+
"includeNodeModules": [
20+
"@adobe/spectrum-css-temp"
21+
]
22+
},
23+
"module": {
24+
"includeNodeModules": [
25+
"@adobe/spectrum-css-temp"
26+
]
27+
}
28+
},
29+
"repository": {
30+
"type": "git",
31+
"url": "https://github.com/adobe/react-spectrum"
32+
},
33+
"dependencies": {
34+
"@babel/runtime": "^7.6.2",
35+
"@react-aria/dnd": "3.0.0-alpha.5",
36+
"@react-aria/utils": "^3.0.0",
37+
"@react-spectrum/utils": "^3.0.0",
38+
"@react-stately/dnd": "3.0.0-alpha.4",
39+
"@react-types/shared": "^3.0.0"
40+
},
41+
"devDependencies": {
42+
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
43+
},
44+
"peerDependencies": {
45+
"react": "^16.8.0 || ^17.0.0-rc.1",
46+
"@react-spectrum/provider": "^3.0.0"
47+
},
48+
"publishConfig": {
49+
"access": "public"
50+
}
51+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2022 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/// <reference types="css-module-types" />
14+
15+
export * from './useDragHooks';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {DraggableCollectionOptions, DraggableCollectionState, useDraggableCollectionState} from '@react-stately/dnd';
2+
import {DraggableItemProps, DraggableItemResult, useDraggableItem} from '@react-aria/dnd';
3+
import {useMemo} from 'react';
4+
5+
export interface DragHooks {
6+
useDraggableCollectionState(props: Omit<DraggableCollectionOptions, 'getItems'>): DraggableCollectionState,
7+
useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult
8+
}
9+
10+
export type DragHookOptions = Omit<DraggableCollectionOptions, 'collection' | 'selectionManager' | 'isDragging' | 'getKeysForDrag'>
11+
12+
export function useDragHooks(options: DragHookOptions): DragHooks {
13+
return useMemo(() => ({
14+
useDraggableCollectionState(props: DraggableCollectionOptions) {
15+
let {
16+
collection,
17+
selectionManager,
18+
allowsDraggingItem,
19+
getItems,
20+
renderPreview
21+
} = props;
22+
23+
return useDraggableCollectionState({
24+
collection,
25+
selectionManager,
26+
allowsDraggingItem,
27+
getItems,
28+
renderPreview,
29+
...options
30+
});
31+
},
32+
useDraggableItem
33+
}), [options]);
34+
}

packages/@react-spectrum/list/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"dependencies": {
3434
"@babel/runtime": "^7.6.2",
35+
"@react-aria/button": "^3.4.1",
3536
"@react-aria/focus": "^3.5.2",
3637
"@react-aria/grid": "^3.2.3",
3738
"@react-aria/i18n": "^3.3.6",
@@ -40,8 +41,10 @@
4041
"@react-aria/separator": "^3.1.5",
4142
"@react-aria/utils": "^3.11.2",
4243
"@react-aria/virtualizer": "^3.3.7",
44+
"@react-aria/visually-hidden": "^3.2.5",
4345
"@react-spectrum/button": "^3.7.1",
4446
"@react-spectrum/checkbox": "^3.3.1",
47+
"@react-spectrum/dnd": "3.0.0-alpha.1",
4548
"@react-spectrum/layout": "^3.2.3",
4649
"@react-spectrum/listbox": "^3.5.5",
4750
"@react-spectrum/progress": "^3.1.5",
@@ -54,12 +57,15 @@
5457
"@react-stately/layout": "^3.4.4",
5558
"@react-stately/list": "^3.4.3",
5659
"@react-stately/virtualizer": "^3.1.7",
60+
"@react-types/button": "^3.4.3",
5761
"@react-types/listbox": "^3.2.3",
5862
"@react-types/shared": "^3.11.1",
5963
"@spectrum-icons/ui": "^3.2.3"
6064
},
6165
"devDependencies": {
62-
"@adobe/spectrum-css-temp": "^3.0.0-alpha.1"
66+
"@adobe/spectrum-css-temp": "^3.0.0-alpha.1",
67+
"@react-aria/dnd": "3.0.0-alpha.5",
68+
"@react-stately/dnd": "3.0.0-alpha.4"
6369
},
6470
"peerDependencies": {
6571
"react": "^16.8.0 || ^17.0.0-rc.1",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import React from 'react';
14+
15+
export default function DragHandle() {
16+
return (
17+
<svg width="16" height="32" viewBox="0 0 16 32">
18+
<g transform="translate(5.5 10.5)">
19+
<circle cx="1" cy="1" r="1" transform="translate(0 9)" fill="#6e6e6e" />
20+
<circle cx="1" cy="1" r="1" transform="translate(0 6)" fill="#6e6e6e" />
21+
<circle cx="1" cy="1" r="1" transform="translate(0 3)" fill="#6e6e6e" />
22+
<circle cx="1" cy="1" r="1" fill="#6e6e6e" />
23+
<circle cx="1" cy="1" r="1" transform="translate(3 9)" fill="#6e6e6e" />
24+
<circle cx="1" cy="1" r="1" transform="translate(3 6)" fill="#6e6e6e" />
25+
<circle cx="1" cy="1" r="1" transform="translate(3 3)" fill="#6e6e6e" />
26+
<circle cx="1" cy="1" r="1" transform="translate(3)" fill="#6e6e6e" />
27+
</g>
28+
</svg>
29+
);
30+
}

0 commit comments

Comments
 (0)