Skip to content

Commit 439b92f

Browse files
b-ryuClaudéric Demers
authored and
Claudéric Demers
committed
feat: add support for keyboard sorting (#501)
Adds support for keyboard sorting! To enable it, make sure your `SortableElement` or `SortableHandle` is focusable. This can be done by setting `tabIndex={0}` on the outermost HTML node rendered by the component you're enhancing with `SortableElement` or `SortableHandle`. Once an item is focused/tabbed to, pressing `SPACE` picks it up, `ArrowUp` or `ArrowLeft` moves it one place backward in the list, `ArrowDown` or `ArrowRight` moves items one place forward in the list, pressing `SPACE` again drops the item in its new position. Pressing `ESC` before the item is dropped will cancel the sort operations.
1 parent 98e78bd commit 439b92f

File tree

14 files changed

+677
-157
lines changed

14 files changed

+677
-157
lines changed

README.md

+33-25
Large diffs are not rendered by default.

src/.stories/Storybook.scss

+47-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@import url(https://fonts.googleapis.com/css?family=Montserrat:400);
22

3+
$focusedOutlineColor: #4c9ffe;
4+
35
.root {
46
display: flex;
57
height: 100%;
@@ -21,6 +23,17 @@
2123
.item {
2224
position: relative;
2325
border-bottom: 1px solid #999;
26+
27+
cursor: grab;
28+
touch-action: manipulation;
29+
30+
&.sorting {
31+
pointer-events: none;
32+
}
33+
}
34+
35+
.containsDragHandle {
36+
cursor: default;
2437
}
2538

2639
// Stylized
@@ -32,6 +45,7 @@
3245
border-radius: 3px;
3346
outline: none;
3447
}
48+
3549
.stylizedItem {
3650
display: flex;
3751
align-items: center;
@@ -41,9 +55,15 @@
4155
border-bottom: 1px solid #efefef;
4256
box-sizing: border-box;
4357
user-select: none;
58+
outline: none;
4459

4560
color: #333;
4661
font-weight: 400;
62+
63+
&:focus:not(.containsDragHandle) {
64+
text-indent: -2px;
65+
border: 2px solid $focusedOutlineColor;
66+
}
4767
}
4868

4969
.disabled {
@@ -52,13 +72,24 @@
5272
}
5373

5474
// Drag handle
75+
.handleWrapper {
76+
width: 18px;
77+
height: 18px;
78+
outline: none;
79+
}
80+
5581
.handle {
5682
display: block;
5783
width: 18px;
5884
height: 18px;
59-
opacity: 0.25;
6085
margin-right: 20px;
61-
cursor: row-resize;
86+
overflow: hidden;
87+
88+
> svg {
89+
opacity: 0.3;
90+
}
91+
92+
cursor: grab;
6293
}
6394

6495
// Horizontal list
@@ -147,26 +178,35 @@
147178
.helper {
148179
box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2),
149180
0 -5px 5px -5px rgba(0, 0, 0, 0.2);
181+
182+
cursor: grabbing;
150183
}
184+
151185
.stylizedHelper {
152-
box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2),
153-
0 -5px 5px -5px rgba(0, 0, 0, 0.2);
154-
background-color: rgba(255, 255, 255, 0.8);
155-
cursor: row-resize;
186+
border: 1px solid #efefef;
187+
box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2);
188+
background-color: rgba(255, 255, 255, 0.9);
189+
border-radius: 3px;
156190

157191
&.horizontalItem {
158192
cursor: col-resize;
159193
}
194+
160195
&.gridItem {
161196
background-color: transparent;
162197
white-space: nowrap;
163198
box-shadow: none;
199+
border: none;
164200

165201
.wrapper {
166-
background-color: rgba(255, 255, 255, 0.8);
202+
background-color: rgba(255, 255, 255, 0.9);
167203
box-shadow: 0 0 7px rgba(0, 0, 0, 0.15);
168204
}
169205
}
206+
207+
&:focus {
208+
box-shadow: 0 0px 5px 1px $focusedOutlineColor;
209+
}
170210
}
171211

172212
.shrinkedHelper {

src/.stories/grouping-items/Item/Item.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@ import {sortableElement} from '../../../../src';
44

55
import styles from './Item.scss';
66

7+
const ENTER_KEY = 13;
8+
79
function Item(props) {
8-
const {dragging, onClick, selected, selectedItemsCount, value} = props;
10+
const {
11+
dragging,
12+
sorting,
13+
onClick,
14+
selected,
15+
selectedItemsCount,
16+
value,
17+
} = props;
918
const shouldRenderItemCountBadge = dragging && selectedItemsCount > 1;
1019

1120
return (
@@ -14,8 +23,15 @@ function Item(props) {
1423
styles.Item,
1524
selected && !dragging && styles.selected,
1625
dragging && styles.dragging,
26+
sorting && styles.sorting,
1727
)}
1828
onClick={() => onClick(value)}
29+
onKeyPress={(event) => {
30+
if (event.which === ENTER_KEY) {
31+
onClick(value);
32+
}
33+
}}
34+
tabIndex={0}
1935
>
2036
Item {value}
2137
{shouldRenderItemCountBadge ? <Badge count={selectedItemsCount} /> : null}

src/.stories/grouping-items/Item/Item.scss

+28-11
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,62 @@ $borderWidth: 1px;
1212
$borderColor: #efefef;
1313

1414
$selectedColor: $white;
15-
$selectedBackgroundColor: #759fff;
16-
$selectedBorderColor: #5e83d6;
15+
$selectedBackgroundColor: rgba(216, 232, 251, 0.9);
16+
$selectedBorderColor: #bbcee8;
1717

1818
$badgeColor: $white;
1919
$badgeBackgroundColor: #f75959;
2020
$badgeBorderColor: #da4553;
2121

22+
$focusedOutlineColor: #4c9ffe;
23+
2224
.Item {
2325
display: flex;
2426
align-items: center;
2527
width: 100%;
26-
padding: 20px;
28+
height: 59px;
29+
padding: 0 20px;
2730
background-color: $backgroundColor;
28-
border-top: $borderWidth solid #efefef;
31+
border-bottom: $borderWidth solid #efefef;
2932
box-sizing: border-box;
3033
user-select: none;
34+
outline: none;
3135

3236
color: $color;
3337
font-weight: $fontWeight-regular;
3438

35-
&:first-child {
36-
border-top: none;
39+
cursor: grab;
40+
41+
&:last-child {
42+
border-bottom: none;
3743
}
3844

3945
&.selected {
4046
background: $selectedBackgroundColor;
41-
border: 1px solid $selectedBorderColor;
42-
color: $selectedColor;
43-
font-weight: $fontWeight-bold;
47+
border-bottom-color: $selectedBorderColor;
4448

45-
& + .Item {
46-
border-top: none;
49+
&:focus {
50+
border-bottom-color: $focusedOutlineColor;
4751
}
4852
}
4953

54+
&.sorting {
55+
pointer-events: none;
56+
}
57+
5058
&.dragging {
5159
border-radius: $borderRadius;
5260
border: $borderWidth solid #efefef;
5361
box-shadow: $boxShadow;
62+
63+
&:focus {
64+
box-shadow: 0 0px 5px 1px $focusedOutlineColor;
65+
}
66+
}
67+
68+
&:focus {
69+
text-indent: -2px;
70+
border: 2px solid $focusedOutlineColor;
5471
}
5572
}
5673

src/.stories/grouping-items/List/List.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Item from '../Item';
55

66
import styles from './List.scss';
77

8-
function List({items, selectedItems, sortingItemKey, onItemSelect}) {
8+
function List({items, isSorting, selectedItems, sortingItemKey, onItemSelect}) {
99
return (
1010
<div className={styles.List}>
1111
{items.map((value, index) => {
@@ -17,6 +17,7 @@ function List({items, selectedItems, sortingItemKey, onItemSelect}) {
1717
key={`item-${value}`}
1818
selected={isSelected}
1919
dragging={itemIsBeingDragged}
20+
sorting={isSorting}
2021
index={index}
2122
value={value}
2223
onClick={onItemSelect}

src/.stories/grouping-items/index.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ class GroupedItems extends React.Component {
1111
};
1212

1313
render() {
14-
const {items, selectedItems, sortingItemKey} = this.state;
14+
const {items, isSorting, selectedItems, sortingItemKey} = this.state;
1515

1616
return (
1717
<SortableList
1818
items={items.filter(this.filterItems)}
19+
isSorting={isSorting}
1920
sortingItemKey={sortingItemKey}
2021
selectedItems={selectedItems}
2122
onItemSelect={this.handleItemSelect}
2223
shouldCancelStart={this.handleShouldCancelStart}
2324
updateBeforeSortStart={this.handleUpdateBeforeSortStart}
25+
onSortStart={this.handleSortStart}
2426
onSortEnd={this.handleSortEnd}
2527
distance={3}
2628
/>
@@ -56,6 +58,10 @@ class GroupedItems extends React.Component {
5658
);
5759
};
5860

61+
handleSortStart() {
62+
document.body.style.cursor = 'grabbing';
63+
}
64+
5965
handleSortEnd = ({oldIndex, newIndex}) => {
6066
const {selectedItems} = this.state;
6167
let newItems;
@@ -80,6 +86,8 @@ class GroupedItems extends React.Component {
8086
sortingItemKey: null,
8187
selectedItems: [],
8288
});
89+
90+
document.body.style.cursor = '';
8391
};
8492

8593
handleItemSelect = (item) => {

0 commit comments

Comments
 (0)