@khanacademy/wonder-blocks-clickable 2.3.3 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/es/index.js +5 -24
- package/dist/index.js +11 -34
- package/package.json +1 -1
- package/src/components/__docs__/clickable-behavior.argtypes.js +9 -0
- package/src/components/__docs__/clickable-behavior.stories.js +87 -1
- package/src/components/__docs__/clickable.stories.js +7 -0
- package/src/components/__tests__/clickable-behavior.test.js +101 -19
- package/src/components/clickable-behavior.js +17 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-clickable
|
|
2
2
|
|
|
3
|
+
## 2.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4c682709: handleClick no longer redundantly triggers on mouseup
|
|
8
|
+
|
|
9
|
+
## 2.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- ceb111df: ClickableBehavior no longer has tabIndex 0 by default. It must be passed in.
|
|
14
|
+
|
|
3
15
|
## 2.3.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -41,13 +41,11 @@ const disabledHandlers = {
|
|
|
41
41
|
onMouseLeave: () => void 0,
|
|
42
42
|
onMouseDown: () => void 0,
|
|
43
43
|
onMouseUp: () => void 0,
|
|
44
|
-
onDragStart: () => void 0,
|
|
45
44
|
onTouchStart: () => void 0,
|
|
46
45
|
onTouchEnd: () => void 0,
|
|
47
46
|
onTouchCancel: () => void 0,
|
|
48
47
|
onKeyDown: () => void 0,
|
|
49
|
-
onKeyUp: () => void 0
|
|
50
|
-
tabIndex: 0
|
|
48
|
+
onKeyUp: () => void 0
|
|
51
49
|
};
|
|
52
50
|
const keyCodes = {
|
|
53
51
|
enter: 13,
|
|
@@ -92,12 +90,7 @@ class ClickableBehavior extends React.Component {
|
|
|
92
90
|
};
|
|
93
91
|
|
|
94
92
|
this.handleMouseEnter = e => {
|
|
95
|
-
if (
|
|
96
|
-
this.dragging = true;
|
|
97
|
-
this.setState({
|
|
98
|
-
pressed: true
|
|
99
|
-
});
|
|
100
|
-
} else if (!this.waitingForClick) {
|
|
93
|
+
if (!this.waitingForClick) {
|
|
101
94
|
this.setState({
|
|
102
95
|
hovered: true
|
|
103
96
|
});
|
|
@@ -106,7 +99,6 @@ class ClickableBehavior extends React.Component {
|
|
|
106
99
|
|
|
107
100
|
this.handleMouseLeave = () => {
|
|
108
101
|
if (!this.waitingForClick) {
|
|
109
|
-
this.dragging = false;
|
|
110
102
|
this.setState({
|
|
111
103
|
hovered: false,
|
|
112
104
|
pressed: false,
|
|
@@ -122,22 +114,12 @@ class ClickableBehavior extends React.Component {
|
|
|
122
114
|
};
|
|
123
115
|
|
|
124
116
|
this.handleMouseUp = e => {
|
|
125
|
-
if (this.dragging) {
|
|
126
|
-
this.dragging = false;
|
|
127
|
-
this.handleClick(e);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
117
|
this.setState({
|
|
131
118
|
pressed: false,
|
|
132
119
|
focused: false
|
|
133
120
|
});
|
|
134
121
|
};
|
|
135
122
|
|
|
136
|
-
this.handleDragStart = e => {
|
|
137
|
-
this.dragging = true;
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
};
|
|
140
|
-
|
|
141
123
|
this.handleTouchStart = () => {
|
|
142
124
|
this.setState({
|
|
143
125
|
pressed: true
|
|
@@ -227,7 +209,6 @@ class ClickableBehavior extends React.Component {
|
|
|
227
209
|
this.state = startState;
|
|
228
210
|
this.waitingForClick = false;
|
|
229
211
|
this.enterClick = false;
|
|
230
|
-
this.dragging = false;
|
|
231
212
|
}
|
|
232
213
|
|
|
233
214
|
navigateOrReset(shouldNavigate) {
|
|
@@ -351,14 +332,14 @@ class ClickableBehavior extends React.Component {
|
|
|
351
332
|
render() {
|
|
352
333
|
const childrenProps = this.props.disabled ? _extends({}, disabledHandlers, {
|
|
353
334
|
onFocus: this.handleFocus,
|
|
354
|
-
onBlur: this.handleBlur
|
|
335
|
+
onBlur: this.handleBlur,
|
|
336
|
+
tabIndex: this.props.tabIndex
|
|
355
337
|
}) : {
|
|
356
338
|
onClick: this.handleClick,
|
|
357
339
|
onMouseEnter: this.handleMouseEnter,
|
|
358
340
|
onMouseLeave: this.handleMouseLeave,
|
|
359
341
|
onMouseDown: this.handleMouseDown,
|
|
360
342
|
onMouseUp: this.handleMouseUp,
|
|
361
|
-
onDragStart: this.handleDragStart,
|
|
362
343
|
onTouchStart: this.handleTouchStart,
|
|
363
344
|
onTouchEnd: this.handleTouchEnd,
|
|
364
345
|
onTouchCancel: this.handleTouchCancel,
|
|
@@ -366,7 +347,7 @@ class ClickableBehavior extends React.Component {
|
|
|
366
347
|
onKeyUp: this.handleKeyUp,
|
|
367
348
|
onFocus: this.handleFocus,
|
|
368
349
|
onBlur: this.handleBlur,
|
|
369
|
-
tabIndex:
|
|
350
|
+
tabIndex: this.props.tabIndex
|
|
370
351
|
};
|
|
371
352
|
childrenProps.rel = this.props.rel || (this.props.target === "_blank" ? "noopener noreferrer" : undefined);
|
|
372
353
|
const {
|
package/dist/index.js
CHANGED
|
@@ -158,15 +158,11 @@ const disabledHandlers = {
|
|
|
158
158
|
onMouseLeave: () => void 0,
|
|
159
159
|
onMouseDown: () => void 0,
|
|
160
160
|
onMouseUp: () => void 0,
|
|
161
|
-
onDragStart: () => void 0,
|
|
162
161
|
onTouchStart: () => void 0,
|
|
163
162
|
onTouchEnd: () => void 0,
|
|
164
163
|
onTouchCancel: () => void 0,
|
|
165
164
|
onKeyDown: () => void 0,
|
|
166
|
-
onKeyUp: () => void 0
|
|
167
|
-
// Clickable components should still be tabbable so they can
|
|
168
|
-
// be used as anchors.
|
|
169
|
-
tabIndex: 0
|
|
165
|
+
onKeyUp: () => void 0
|
|
170
166
|
};
|
|
171
167
|
const keyCodes = {
|
|
172
168
|
enter: 13,
|
|
@@ -200,7 +196,7 @@ const startState = {
|
|
|
200
196
|
* 3. Keyup (spacebar/enter) -> focus state
|
|
201
197
|
*
|
|
202
198
|
* Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
|
|
203
|
-
* onMouseDown, onMouseUp,
|
|
199
|
+
* onMouseDown, onMouseUp, onTouchStart, onTouchEnd, onTouchCancel,
|
|
204
200
|
* onKeyDown, onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the
|
|
205
201
|
* component that has the ClickableBehavior. You cannot override these handlers
|
|
206
202
|
* without potentially breaking the functionality of ClickableBehavior.
|
|
@@ -225,7 +221,11 @@ const startState = {
|
|
|
225
221
|
* const ClickableBehavior = getClickableBehavior();
|
|
226
222
|
*
|
|
227
223
|
* return (
|
|
228
|
-
* <ClickableBehavior
|
|
224
|
+
* <ClickableBehavior
|
|
225
|
+
* disabled={props.disabled}
|
|
226
|
+
* onClick={props.onClick}
|
|
227
|
+
* tabIndex={0}
|
|
228
|
+
* >
|
|
229
229
|
* {({hovered}, childrenProps) => (
|
|
230
230
|
* <RoundRect
|
|
231
231
|
* textcolor="white"
|
|
@@ -292,13 +292,7 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
this.handleMouseEnter = e => {
|
|
295
|
-
|
|
296
|
-
if (e.buttons === 1) {
|
|
297
|
-
this.dragging = true;
|
|
298
|
-
this.setState({
|
|
299
|
-
pressed: true
|
|
300
|
-
});
|
|
301
|
-
} else if (!this.waitingForClick) {
|
|
295
|
+
if (!this.waitingForClick) {
|
|
302
296
|
this.setState({
|
|
303
297
|
hovered: true
|
|
304
298
|
});
|
|
@@ -307,7 +301,6 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
307
301
|
|
|
308
302
|
this.handleMouseLeave = () => {
|
|
309
303
|
if (!this.waitingForClick) {
|
|
310
|
-
this.dragging = false;
|
|
311
304
|
this.setState({
|
|
312
305
|
hovered: false,
|
|
313
306
|
pressed: false,
|
|
@@ -323,22 +316,12 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
323
316
|
};
|
|
324
317
|
|
|
325
318
|
this.handleMouseUp = e => {
|
|
326
|
-
if (this.dragging) {
|
|
327
|
-
this.dragging = false;
|
|
328
|
-
this.handleClick(e);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
319
|
this.setState({
|
|
332
320
|
pressed: false,
|
|
333
321
|
focused: false
|
|
334
322
|
});
|
|
335
323
|
};
|
|
336
324
|
|
|
337
|
-
this.handleDragStart = e => {
|
|
338
|
-
this.dragging = true;
|
|
339
|
-
e.preventDefault();
|
|
340
|
-
};
|
|
341
|
-
|
|
342
325
|
this.handleTouchStart = () => {
|
|
343
326
|
this.setState({
|
|
344
327
|
pressed: true
|
|
@@ -434,7 +417,6 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
434
417
|
this.state = startState;
|
|
435
418
|
this.waitingForClick = false;
|
|
436
419
|
this.enterClick = false;
|
|
437
|
-
this.dragging = false;
|
|
438
420
|
}
|
|
439
421
|
|
|
440
422
|
navigateOrReset(shouldNavigate) {
|
|
@@ -580,14 +562,14 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
580
562
|
const childrenProps = this.props.disabled ? { ...disabledHandlers,
|
|
581
563
|
// Keep these handlers for keyboard accessibility.
|
|
582
564
|
onFocus: this.handleFocus,
|
|
583
|
-
onBlur: this.handleBlur
|
|
565
|
+
onBlur: this.handleBlur,
|
|
566
|
+
tabIndex: this.props.tabIndex
|
|
584
567
|
} : {
|
|
585
568
|
onClick: this.handleClick,
|
|
586
569
|
onMouseEnter: this.handleMouseEnter,
|
|
587
570
|
onMouseLeave: this.handleMouseLeave,
|
|
588
571
|
onMouseDown: this.handleMouseDown,
|
|
589
572
|
onMouseUp: this.handleMouseUp,
|
|
590
|
-
onDragStart: this.handleDragStart,
|
|
591
573
|
onTouchStart: this.handleTouchStart,
|
|
592
574
|
onTouchEnd: this.handleTouchEnd,
|
|
593
575
|
onTouchCancel: this.handleTouchCancel,
|
|
@@ -595,9 +577,7 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
595
577
|
onKeyUp: this.handleKeyUp,
|
|
596
578
|
onFocus: this.handleFocus,
|
|
597
579
|
onBlur: this.handleBlur,
|
|
598
|
-
|
|
599
|
-
// things that aren't buttons or anchors.
|
|
600
|
-
tabIndex: 0
|
|
580
|
+
tabIndex: this.props.tabIndex
|
|
601
581
|
}; // When the link is set to open in a new window, we want to set some
|
|
602
582
|
// `rel` attributes. This is to ensure that the links we're sending folks
|
|
603
583
|
// to can't hijack the existing page. These defaults can be overriden
|
|
@@ -1136,19 +1116,16 @@ function _extends() {
|
|
|
1136
1116
|
module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
1137
1117
|
for (var i = 1; i < arguments.length; i++) {
|
|
1138
1118
|
var source = arguments[i];
|
|
1139
|
-
|
|
1140
1119
|
for (var key in source) {
|
|
1141
1120
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1142
1121
|
target[key] = source[key];
|
|
1143
1122
|
}
|
|
1144
1123
|
}
|
|
1145
1124
|
}
|
|
1146
|
-
|
|
1147
1125
|
return target;
|
|
1148
1126
|
}, module.exports.__esModule = true, module.exports["default"] = module.exports;
|
|
1149
1127
|
return _extends.apply(this, arguments);
|
|
1150
1128
|
}
|
|
1151
|
-
|
|
1152
1129
|
module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
|
|
1153
1130
|
|
|
1154
1131
|
/***/ }),
|
package/package.json
CHANGED
|
@@ -14,6 +14,15 @@ export default {
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
|
+
tabIndex: {
|
|
18
|
+
control: {type: "number"},
|
|
19
|
+
description: `Used to indicate the tab order of an element.
|
|
20
|
+
Use 0 to make an element focusable, and use -1 to make an
|
|
21
|
+
element non-focusable via keyboard navigation.`,
|
|
22
|
+
table: {
|
|
23
|
+
type: {summary: "number"},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
17
26
|
/**
|
|
18
27
|
* States
|
|
19
28
|
*/
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import {StyleSheet} from "aphrodite";
|
|
4
4
|
|
|
5
5
|
import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
|
|
6
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
import {View, addStyle} from "@khanacademy/wonder-blocks-core";
|
|
7
7
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
8
8
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
9
9
|
|
|
@@ -72,12 +72,98 @@ Default.parameters = {
|
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
export const WrappingButton: StoryComponentType = (args) => {
|
|
76
|
+
const ClickableBehavior = getClickableBehavior();
|
|
77
|
+
const StyledButton = addStyle("button");
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<ClickableBehavior {...args}>
|
|
81
|
+
{(state, childrenProps) => {
|
|
82
|
+
const {pressed, hovered, focused} = state;
|
|
83
|
+
return (
|
|
84
|
+
<StyledButton
|
|
85
|
+
style={[
|
|
86
|
+
styles.clickable,
|
|
87
|
+
styles.newButton,
|
|
88
|
+
hovered && styles.hovered,
|
|
89
|
+
focused && styles.focused,
|
|
90
|
+
pressed && styles.pressed,
|
|
91
|
+
]}
|
|
92
|
+
{...childrenProps}
|
|
93
|
+
>
|
|
94
|
+
This is an element wrapped with ClickableBehavior
|
|
95
|
+
</StyledButton>
|
|
96
|
+
);
|
|
97
|
+
}}
|
|
98
|
+
</ClickableBehavior>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
WrappingButton.parameters = {
|
|
103
|
+
chromatic: {
|
|
104
|
+
// we don't need screenshots because this story only displays the
|
|
105
|
+
// resting/default state.
|
|
106
|
+
disableSnapshot: true,
|
|
107
|
+
},
|
|
108
|
+
docs: {
|
|
109
|
+
storyDescription: `This is an example of a \`<ClickableBehavior>\`
|
|
110
|
+
wrapping a button. Since buttons have a built in tabIndex,
|
|
111
|
+
a tabIndex does not need to be added to \`<ClickableBehavior>\`
|
|
112
|
+
here.`,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const WithTabIndex: StoryComponentType = () => {
|
|
117
|
+
const ClickableBehavior = getClickableBehavior();
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ClickableBehavior role="button" tabIndex={0}>
|
|
121
|
+
{(state, childrenProps) => {
|
|
122
|
+
const {pressed, hovered, focused} = state;
|
|
123
|
+
return (
|
|
124
|
+
<View
|
|
125
|
+
style={[
|
|
126
|
+
styles.clickable,
|
|
127
|
+
hovered && styles.hovered,
|
|
128
|
+
focused && styles.focused,
|
|
129
|
+
pressed && styles.pressed,
|
|
130
|
+
]}
|
|
131
|
+
{...childrenProps}
|
|
132
|
+
>
|
|
133
|
+
This is an element wrapped with ClickableBehavior
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}}
|
|
137
|
+
</ClickableBehavior>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
WithTabIndex.parameters = {
|
|
142
|
+
chromatic: {
|
|
143
|
+
// we don't need screenshots because this story only displays the
|
|
144
|
+
// resting/default state.
|
|
145
|
+
disableSnapshot: true,
|
|
146
|
+
},
|
|
147
|
+
docs: {
|
|
148
|
+
storyDescription: `A \`<ClickableBehavior>\` element does not have
|
|
149
|
+
a tabIndex by default, as many elements it could wrap may have
|
|
150
|
+
their own built in tabIndex attribute, such as buttons. If this
|
|
151
|
+
is not the case, a tabIndex should be passed in using the
|
|
152
|
+
\`tabIndex\` prop.`,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
75
156
|
const styles = StyleSheet.create({
|
|
76
157
|
clickable: {
|
|
77
158
|
cursor: "pointer",
|
|
78
159
|
padding: Spacing.medium_16,
|
|
79
160
|
textAlign: "center",
|
|
80
161
|
},
|
|
162
|
+
newButton: {
|
|
163
|
+
border: "none",
|
|
164
|
+
backgroundColor: Color.white,
|
|
165
|
+
width: "100%",
|
|
166
|
+
},
|
|
81
167
|
hovered: {
|
|
82
168
|
textDecoration: "underline",
|
|
83
169
|
backgroundColor: Color.blue,
|
|
@@ -66,6 +66,13 @@ export const Default: StoryComponentType = (args) => (
|
|
|
66
66
|
</Clickable>
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
+
Default.args = {
|
|
70
|
+
onClick: () => {
|
|
71
|
+
// eslint-disable-next-line no-alert
|
|
72
|
+
alert("Click!");
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
69
76
|
export const Basic: StoryComponentType = () => (
|
|
70
77
|
<View style={styles.centerText}>
|
|
71
78
|
<Clickable
|
|
@@ -64,7 +64,7 @@ describe("ClickableBehavior", () => {
|
|
|
64
64
|
expect(onClick).toHaveBeenCalled();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it("changes
|
|
67
|
+
it("changes hovered state on mouse enter/leave", () => {
|
|
68
68
|
const onClick = jest.fn();
|
|
69
69
|
render(
|
|
70
70
|
<ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
|
|
@@ -82,7 +82,7 @@ describe("ClickableBehavior", () => {
|
|
|
82
82
|
expect(button).not.toHaveTextContent("hovered");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it("changes
|
|
85
|
+
it("changes hovered state on mouse enter while dragging", () => {
|
|
86
86
|
const onClick = jest.fn();
|
|
87
87
|
render(
|
|
88
88
|
<ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
|
|
@@ -93,18 +93,34 @@ describe("ClickableBehavior", () => {
|
|
|
93
93
|
</ClickableBehavior>,
|
|
94
94
|
);
|
|
95
95
|
const button = screen.getByRole("button");
|
|
96
|
+
expect(button).not.toHaveTextContent("hovered");
|
|
97
|
+
expect(button).not.toHaveTextContent("pressed");
|
|
98
|
+
|
|
99
|
+
fireEvent.mouseEnter(button, {buttons: 1});
|
|
100
|
+
expect(button).not.toHaveTextContent("pressed");
|
|
101
|
+
expect(button).toHaveTextContent("hovered");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("changes pressed and hover states on mouse leave while dragging", () => {
|
|
105
|
+
const onClick = jest.fn();
|
|
106
|
+
render(
|
|
107
|
+
<ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
|
|
108
|
+
{(state, childrenProps) => {
|
|
109
|
+
const label = labelForState(state);
|
|
110
|
+
return <button {...childrenProps}>{label}</button>;
|
|
111
|
+
}}
|
|
112
|
+
</ClickableBehavior>,
|
|
113
|
+
);
|
|
114
|
+
const button = screen.getByRole("button");
|
|
115
|
+
expect(button).not.toHaveTextContent("hovered");
|
|
96
116
|
expect(button).not.toHaveTextContent("pressed");
|
|
97
117
|
|
|
98
118
|
fireEvent.mouseDown(button);
|
|
99
|
-
fireEvent.dragStart(button);
|
|
100
|
-
fireEvent.mouseMove(button);
|
|
101
119
|
expect(button).toHaveTextContent("pressed");
|
|
102
120
|
|
|
103
121
|
fireEvent.mouseLeave(button);
|
|
122
|
+
expect(button).not.toHaveTextContent("hovered");
|
|
104
123
|
expect(button).not.toHaveTextContent("pressed");
|
|
105
|
-
|
|
106
|
-
fireEvent.mouseEnter(button, {buttons: 1});
|
|
107
|
-
expect(button).toHaveTextContent("pressed");
|
|
108
124
|
});
|
|
109
125
|
|
|
110
126
|
it("changes pressed state on mouse down/up", () => {
|
|
@@ -282,7 +298,7 @@ describe("ClickableBehavior", () => {
|
|
|
282
298
|
expect(button).not.toHaveTextContent("focused");
|
|
283
299
|
});
|
|
284
300
|
|
|
285
|
-
test("tabIndex
|
|
301
|
+
test("should not have a tabIndex if one is not passed in", () => {
|
|
286
302
|
// Arrange
|
|
287
303
|
// Act
|
|
288
304
|
render(
|
|
@@ -299,14 +315,18 @@ describe("ClickableBehavior", () => {
|
|
|
299
315
|
|
|
300
316
|
// Assert
|
|
301
317
|
const button = screen.getByTestId("test-button-1");
|
|
302
|
-
expect(button).toHaveAttribute("tabIndex"
|
|
318
|
+
expect(button).not.toHaveAttribute("tabIndex");
|
|
303
319
|
});
|
|
304
320
|
|
|
305
|
-
test("
|
|
321
|
+
test("should have the tabIndex that is passed in", () => {
|
|
306
322
|
// Arrange
|
|
307
323
|
// Act
|
|
308
324
|
render(
|
|
309
|
-
<ClickableBehavior
|
|
325
|
+
<ClickableBehavior
|
|
326
|
+
disabled={false}
|
|
327
|
+
onClick={(e) => {}}
|
|
328
|
+
tabIndex={1}
|
|
329
|
+
>
|
|
310
330
|
{(state, childrenProps) => {
|
|
311
331
|
return (
|
|
312
332
|
<button data-test-id="test-button-2" {...childrenProps}>
|
|
@@ -319,7 +339,53 @@ describe("ClickableBehavior", () => {
|
|
|
319
339
|
|
|
320
340
|
// Assert
|
|
321
341
|
const button = screen.getByTestId("test-button-2");
|
|
322
|
-
expect(button).toHaveAttribute("tabIndex", "
|
|
342
|
+
expect(button).toHaveAttribute("tabIndex", "1");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("should have the tabIndex that is passed in even if disabled", () => {
|
|
346
|
+
// Arrange
|
|
347
|
+
// Act
|
|
348
|
+
render(
|
|
349
|
+
<ClickableBehavior disabled={true} onClick={(e) => {}} tabIndex={1}>
|
|
350
|
+
{(state, childrenProps) => {
|
|
351
|
+
return (
|
|
352
|
+
<button data-test-id="test-button-3" {...childrenProps}>
|
|
353
|
+
Label
|
|
354
|
+
</button>
|
|
355
|
+
);
|
|
356
|
+
}}
|
|
357
|
+
</ClickableBehavior>,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Assert
|
|
361
|
+
const button = screen.getByTestId("test-button-3");
|
|
362
|
+
expect(button).toHaveAttribute("tabIndex", "1");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("should make non-interactive children keyboard focusable if tabIndex 0 is passed", () => {
|
|
366
|
+
// Arrange
|
|
367
|
+
render(
|
|
368
|
+
<ClickableBehavior
|
|
369
|
+
disabled={false}
|
|
370
|
+
onClick={(e) => {}}
|
|
371
|
+
tabIndex={1}
|
|
372
|
+
>
|
|
373
|
+
{(state, childrenProps) => {
|
|
374
|
+
return (
|
|
375
|
+
<div data-test-id="test-div-1" {...childrenProps}>
|
|
376
|
+
Label
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
}}
|
|
380
|
+
</ClickableBehavior>,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Act
|
|
384
|
+
const button = screen.getByTestId("test-div-1");
|
|
385
|
+
userEvent.tab();
|
|
386
|
+
|
|
387
|
+
// Assert
|
|
388
|
+
expect(button).toHaveFocus();
|
|
323
389
|
});
|
|
324
390
|
|
|
325
391
|
it("does not change state if disabled", () => {
|
|
@@ -713,7 +779,17 @@ describe("ClickableBehavior", () => {
|
|
|
713
779
|
expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
|
|
714
780
|
});
|
|
715
781
|
|
|
716
|
-
|
|
782
|
+
// The following two tests involve click behavior when dragging.
|
|
783
|
+
// Here are some notable related actions that cannot be tested using
|
|
784
|
+
// existing jest/RTL events since these click types are handled
|
|
785
|
+
// by browsers but aren't registered as clicks by RTL/jest:
|
|
786
|
+
// 1. Mousedown in the button, drag within the button, and mouseup
|
|
787
|
+
// in the button (mouse doesn't leave the button at any point).
|
|
788
|
+
// This should result in a successful click.
|
|
789
|
+
// 2. Mouse down in the button, drag out of the button (don't let go),
|
|
790
|
+
// drag back into the button, and mouseup inside the button.
|
|
791
|
+
// This should result in a successful click.
|
|
792
|
+
it("does not call onClick on mouseup when the mouse presses inside and drags away", () => {
|
|
717
793
|
const onClick = jest.fn();
|
|
718
794
|
render(
|
|
719
795
|
<ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
|
|
@@ -725,19 +801,25 @@ describe("ClickableBehavior", () => {
|
|
|
725
801
|
|
|
726
802
|
const button = screen.getByRole("button");
|
|
727
803
|
fireEvent.mouseDown(button);
|
|
728
|
-
fireEvent.dragStart(button);
|
|
729
804
|
fireEvent.mouseLeave(button);
|
|
730
805
|
fireEvent.mouseUp(button);
|
|
731
806
|
expect(onClick).toHaveBeenCalledTimes(0);
|
|
807
|
+
});
|
|
732
808
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
809
|
+
it("does not call onClick on mouseup when the mouse presses outside and drags in", () => {
|
|
810
|
+
const onClick = jest.fn();
|
|
811
|
+
render(
|
|
812
|
+
<ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
|
|
813
|
+
{(state, childrenProps) => {
|
|
814
|
+
return <button {...childrenProps}>Label</button>;
|
|
815
|
+
}}
|
|
816
|
+
</ClickableBehavior>,
|
|
817
|
+
);
|
|
737
818
|
|
|
819
|
+
const button = screen.getByRole("button");
|
|
738
820
|
fireEvent.mouseEnter(button, {buttons: 1});
|
|
739
821
|
fireEvent.mouseUp(button);
|
|
740
|
-
expect(onClick).toHaveBeenCalledTimes(
|
|
822
|
+
expect(onClick).toHaveBeenCalledTimes(0);
|
|
741
823
|
});
|
|
742
824
|
|
|
743
825
|
it("doesn't trigger enter key when browser doesn't stop the click", () => {
|
|
@@ -88,6 +88,13 @@ type CommonProps = {|
|
|
|
88
88
|
|
|
89
89
|
skipClientNav?: boolean,
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Used to indicate the tab order of an element.
|
|
93
|
+
* Use 0 to make an element focusable, and use -1 to make an
|
|
94
|
+
* element non-focusable via keyboard navigation.
|
|
95
|
+
*/
|
|
96
|
+
tabIndex?: number,
|
|
97
|
+
|
|
91
98
|
/**
|
|
92
99
|
* A function to be executed `onclick`.
|
|
93
100
|
*/
|
|
@@ -198,7 +205,6 @@ export type ChildrenProps = {|
|
|
|
198
205
|
onMouseLeave: () => mixed,
|
|
199
206
|
onMouseDown: () => mixed,
|
|
200
207
|
onMouseUp: (e: SyntheticMouseEvent<>) => mixed,
|
|
201
|
-
onDragStart: (e: SyntheticMouseEvent<>) => mixed,
|
|
202
208
|
onTouchStart: () => mixed,
|
|
203
209
|
onTouchEnd: () => mixed,
|
|
204
210
|
onTouchCancel: () => mixed,
|
|
@@ -206,7 +212,7 @@ export type ChildrenProps = {|
|
|
|
206
212
|
onKeyUp: (e: SyntheticKeyboardEvent<>) => mixed,
|
|
207
213
|
onFocus: (e: SyntheticFocusEvent<>) => mixed,
|
|
208
214
|
onBlur: (e: SyntheticFocusEvent<>) => mixed,
|
|
209
|
-
tabIndex
|
|
215
|
+
tabIndex?: number,
|
|
210
216
|
rel?: string,
|
|
211
217
|
|};
|
|
212
218
|
|
|
@@ -216,15 +222,11 @@ const disabledHandlers = {
|
|
|
216
222
|
onMouseLeave: () => void 0,
|
|
217
223
|
onMouseDown: () => void 0,
|
|
218
224
|
onMouseUp: () => void 0,
|
|
219
|
-
onDragStart: () => void 0,
|
|
220
225
|
onTouchStart: () => void 0,
|
|
221
226
|
onTouchEnd: () => void 0,
|
|
222
227
|
onTouchCancel: () => void 0,
|
|
223
228
|
onKeyDown: () => void 0,
|
|
224
229
|
onKeyUp: () => void 0,
|
|
225
|
-
// Clickable components should still be tabbable so they can
|
|
226
|
-
// be used as anchors.
|
|
227
|
-
tabIndex: 0,
|
|
228
230
|
};
|
|
229
231
|
|
|
230
232
|
const keyCodes = {
|
|
@@ -261,7 +263,7 @@ const startState: ClickableState = {
|
|
|
261
263
|
* 3. Keyup (spacebar/enter) -> focus state
|
|
262
264
|
*
|
|
263
265
|
* Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
|
|
264
|
-
* onMouseDown, onMouseUp,
|
|
266
|
+
* onMouseDown, onMouseUp, onTouchStart, onTouchEnd, onTouchCancel,
|
|
265
267
|
* onKeyDown, onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the
|
|
266
268
|
* component that has the ClickableBehavior. You cannot override these handlers
|
|
267
269
|
* without potentially breaking the functionality of ClickableBehavior.
|
|
@@ -286,7 +288,11 @@ const startState: ClickableState = {
|
|
|
286
288
|
* const ClickableBehavior = getClickableBehavior();
|
|
287
289
|
*
|
|
288
290
|
* return (
|
|
289
|
-
* <ClickableBehavior
|
|
291
|
+
* <ClickableBehavior
|
|
292
|
+
* disabled={props.disabled}
|
|
293
|
+
* onClick={props.onClick}
|
|
294
|
+
* tabIndex={0}
|
|
295
|
+
* >
|
|
290
296
|
* {({hovered}, childrenProps) => (
|
|
291
297
|
* <RoundRect
|
|
292
298
|
* textcolor="white"
|
|
@@ -322,7 +328,6 @@ export default class ClickableBehavior extends React.Component<
|
|
|
322
328
|
> {
|
|
323
329
|
waitingForClick: boolean;
|
|
324
330
|
enterClick: boolean;
|
|
325
|
-
dragging: boolean;
|
|
326
331
|
|
|
327
332
|
static defaultProps: DefaultProps = {
|
|
328
333
|
disabled: false,
|
|
@@ -348,7 +353,6 @@ export default class ClickableBehavior extends React.Component<
|
|
|
348
353
|
this.state = startState;
|
|
349
354
|
this.waitingForClick = false;
|
|
350
355
|
this.enterClick = false;
|
|
351
|
-
this.dragging = false;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
navigateOrReset(shouldNavigate: boolean) {
|
|
@@ -505,18 +509,13 @@ export default class ClickableBehavior extends React.Component<
|
|
|
505
509
|
};
|
|
506
510
|
|
|
507
511
|
handleMouseEnter: (e: SyntheticMouseEvent<>) => void = (e) => {
|
|
508
|
-
|
|
509
|
-
if (e.buttons === 1) {
|
|
510
|
-
this.dragging = true;
|
|
511
|
-
this.setState({pressed: true});
|
|
512
|
-
} else if (!this.waitingForClick) {
|
|
512
|
+
if (!this.waitingForClick) {
|
|
513
513
|
this.setState({hovered: true});
|
|
514
514
|
}
|
|
515
515
|
};
|
|
516
516
|
|
|
517
517
|
handleMouseLeave: () => void = () => {
|
|
518
518
|
if (!this.waitingForClick) {
|
|
519
|
-
this.dragging = false;
|
|
520
519
|
this.setState({hovered: false, pressed: false, focused: false});
|
|
521
520
|
}
|
|
522
521
|
};
|
|
@@ -526,18 +525,9 @@ export default class ClickableBehavior extends React.Component<
|
|
|
526
525
|
};
|
|
527
526
|
|
|
528
527
|
handleMouseUp: (e: SyntheticMouseEvent<>) => void = (e) => {
|
|
529
|
-
if (this.dragging) {
|
|
530
|
-
this.dragging = false;
|
|
531
|
-
this.handleClick(e);
|
|
532
|
-
}
|
|
533
528
|
this.setState({pressed: false, focused: false});
|
|
534
529
|
};
|
|
535
530
|
|
|
536
|
-
handleDragStart: (e: SyntheticMouseEvent<>) => void = (e) => {
|
|
537
|
-
this.dragging = true;
|
|
538
|
-
e.preventDefault();
|
|
539
|
-
};
|
|
540
|
-
|
|
541
531
|
handleTouchStart: () => void = () => {
|
|
542
532
|
this.setState({pressed: true});
|
|
543
533
|
};
|
|
@@ -614,6 +604,7 @@ export default class ClickableBehavior extends React.Component<
|
|
|
614
604
|
// Keep these handlers for keyboard accessibility.
|
|
615
605
|
onFocus: this.handleFocus,
|
|
616
606
|
onBlur: this.handleBlur,
|
|
607
|
+
tabIndex: this.props.tabIndex,
|
|
617
608
|
}
|
|
618
609
|
: {
|
|
619
610
|
onClick: this.handleClick,
|
|
@@ -621,7 +612,6 @@ export default class ClickableBehavior extends React.Component<
|
|
|
621
612
|
onMouseLeave: this.handleMouseLeave,
|
|
622
613
|
onMouseDown: this.handleMouseDown,
|
|
623
614
|
onMouseUp: this.handleMouseUp,
|
|
624
|
-
onDragStart: this.handleDragStart,
|
|
625
615
|
onTouchStart: this.handleTouchStart,
|
|
626
616
|
onTouchEnd: this.handleTouchEnd,
|
|
627
617
|
onTouchCancel: this.handleTouchCancel,
|
|
@@ -629,9 +619,7 @@ export default class ClickableBehavior extends React.Component<
|
|
|
629
619
|
onKeyUp: this.handleKeyUp,
|
|
630
620
|
onFocus: this.handleFocus,
|
|
631
621
|
onBlur: this.handleBlur,
|
|
632
|
-
|
|
633
|
-
// things that aren't buttons or anchors.
|
|
634
|
-
tabIndex: 0,
|
|
622
|
+
tabIndex: this.props.tabIndex,
|
|
635
623
|
};
|
|
636
624
|
|
|
637
625
|
// When the link is set to open in a new window, we want to set some
|