@khanacademy/wonder-blocks-clickable 2.2.3 → 2.2.6
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 +19 -0
- package/dist/es/index.js +22 -209
- package/dist/index.js +11 -6
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2 -2
- package/src/components/__tests__/clickable-behavior.test.js +2 -2
- package/src/components/__tests__/clickable.test.js +43 -0
- package/src/components/clickable-behavior.js +10 -5
- package/src/components/clickable.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-clickable
|
|
2
2
|
|
|
3
|
+
## 2.2.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @khanacademy/wonder-blocks-core@4.3.1
|
|
8
|
+
|
|
9
|
+
## 2.2.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [246a921d]
|
|
14
|
+
- @khanacademy/wonder-blocks-core@4.3.0
|
|
15
|
+
|
|
16
|
+
## 2.2.4
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 166ecc97: Use `aria-disabled` instead of disabled, fix focused + disabled styles.
|
|
21
|
+
|
|
3
22
|
## 2.2.3
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -9,13 +9,11 @@ import Color from '@khanacademy/wonder-blocks-color';
|
|
|
9
9
|
|
|
10
10
|
const getAppropriateTriggersForRole = role => {
|
|
11
11
|
switch (role) {
|
|
12
|
-
// Triggers on ENTER, but not SPACE
|
|
13
12
|
case "link":
|
|
14
13
|
return {
|
|
15
14
|
triggerOnEnter: true,
|
|
16
15
|
triggerOnSpace: false
|
|
17
16
|
};
|
|
18
|
-
// Triggers on SPACE, but not ENTER
|
|
19
17
|
|
|
20
18
|
case "checkbox":
|
|
21
19
|
case "radio":
|
|
@@ -25,7 +23,6 @@ const getAppropriateTriggersForRole = role => {
|
|
|
25
23
|
triggerOnEnter: false,
|
|
26
24
|
triggerOnSpace: true
|
|
27
25
|
};
|
|
28
|
-
// Triggers on both ENTER and SPACE
|
|
29
26
|
|
|
30
27
|
case "button":
|
|
31
28
|
case "menuitem":
|
|
@@ -50,10 +47,6 @@ const disabledHandlers = {
|
|
|
50
47
|
onTouchCancel: () => void 0,
|
|
51
48
|
onKeyDown: () => void 0,
|
|
52
49
|
onKeyUp: () => void 0,
|
|
53
|
-
onFocus: () => void 0,
|
|
54
|
-
onBlur: () => void 0,
|
|
55
|
-
// Clickable components should still be tabbable so they can
|
|
56
|
-
// be used as anchors.
|
|
57
50
|
tabIndex: 0
|
|
58
51
|
};
|
|
59
52
|
const keyCodes = {
|
|
@@ -66,92 +59,13 @@ const startState = {
|
|
|
66
59
|
pressed: false,
|
|
67
60
|
waiting: false
|
|
68
61
|
};
|
|
69
|
-
/**
|
|
70
|
-
* Add hover, focus, and active status updates to a clickable component.
|
|
71
|
-
*
|
|
72
|
-
* Via mouse:
|
|
73
|
-
*
|
|
74
|
-
* 1. Hover over button -> hover state
|
|
75
|
-
* 2. Mouse down -> active state
|
|
76
|
-
* 3. Mouse up -> default state
|
|
77
|
-
* 4. Press tab -> focus state
|
|
78
|
-
*
|
|
79
|
-
* Via touch:
|
|
80
|
-
*
|
|
81
|
-
* 1. Touch down -> press state
|
|
82
|
-
* 2. Touch up -> default state
|
|
83
|
-
*
|
|
84
|
-
* Via keyboard:
|
|
85
|
-
*
|
|
86
|
-
* 1. Tab to focus -> focus state
|
|
87
|
-
* 2. Keydown (spacebar/enter) -> active state
|
|
88
|
-
* 3. Keyup (spacebar/enter) -> focus state
|
|
89
|
-
*
|
|
90
|
-
* Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
|
|
91
|
-
* onMouseDown, onMouseUp, onDragStart, onTouchStart, onTouchEnd, onTouchCancel, onKeyDown,
|
|
92
|
-
* onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the component
|
|
93
|
-
* that has the ClickableBehavior. You cannot override these handlers without
|
|
94
|
-
* potentially breaking the functionality of ClickableBehavior.
|
|
95
|
-
*
|
|
96
|
-
* There are internal props triggerOnEnter and triggerOnSpace that can be set
|
|
97
|
-
* to false if one of those keys shouldn't count as a click on this component.
|
|
98
|
-
* Be careful about setting those to false -- make certain that the component
|
|
99
|
-
* shouldn't process that key.
|
|
100
|
-
*
|
|
101
|
-
* See [this document](https://docs.google.com/document/d/1DG5Rg2f0cawIL5R8UqnPQpd7pbdObk8OyjO5ryYQmBM/edit#)
|
|
102
|
-
* for a more thorough explanation of expected behaviors and potential cavaets.
|
|
103
|
-
*
|
|
104
|
-
* `ClickableBehavior` accepts a function as `children` which is passed state
|
|
105
|
-
* and an object containing event handlers and some other props. The `children`
|
|
106
|
-
* function should return a clickable React Element of some sort.
|
|
107
|
-
*
|
|
108
|
-
* Example:
|
|
109
|
-
*
|
|
110
|
-
* ```js
|
|
111
|
-
* class MyClickableComponent extends React.Component<Props> {
|
|
112
|
-
* render(): React.Node {
|
|
113
|
-
* const ClickableBehavior = getClickableBehavior();
|
|
114
|
-
* return <ClickableBehavior
|
|
115
|
-
* disabled={this.props.disabled}
|
|
116
|
-
* onClick={this.props.onClick}
|
|
117
|
-
* >
|
|
118
|
-
* {({hovered}, childrenProps) =>
|
|
119
|
-
* <RoundRect
|
|
120
|
-
* textcolor='white'
|
|
121
|
-
* backgroundColor={hovered ? 'red' : 'blue'}}
|
|
122
|
-
* {...childrenProps}
|
|
123
|
-
* >
|
|
124
|
-
* {this.props.children}
|
|
125
|
-
* </RoundRect>
|
|
126
|
-
* }
|
|
127
|
-
* </ClickableBehavior>
|
|
128
|
-
* }
|
|
129
|
-
* }
|
|
130
|
-
* ```
|
|
131
|
-
*
|
|
132
|
-
* This follows a pattern called [Function as Child Components]
|
|
133
|
-
* (https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9).
|
|
134
|
-
*
|
|
135
|
-
* WARNING: Do not use this component directly, use getClickableBehavior
|
|
136
|
-
* instead. getClickableBehavior takes three arguments (href, directtNav, and
|
|
137
|
-
* router) and returns either the default ClickableBehavior or a react-router
|
|
138
|
-
* aware version.
|
|
139
|
-
*
|
|
140
|
-
* The react-router aware version is returned if `router` is a react-router-dom
|
|
141
|
-
* router, `skipClientNav` is not `true`, and `href` is an internal URL.
|
|
142
|
-
*
|
|
143
|
-
* The `router` can be accessed via __RouterContext (imported from 'react-router')
|
|
144
|
-
* from a component rendered as a descendant of a BrowserRouter.
|
|
145
|
-
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
146
|
-
*/
|
|
147
|
-
|
|
148
62
|
class ClickableBehavior extends React.Component {
|
|
149
63
|
static getDerivedStateFromProps(props, state) {
|
|
150
|
-
// If new props are disabled, reset the hovered/focused/pressed states
|
|
151
64
|
if (props.disabled) {
|
|
152
|
-
return startState
|
|
65
|
+
return _extends({}, startState, {
|
|
66
|
+
focused: state.focused
|
|
67
|
+
});
|
|
153
68
|
} else {
|
|
154
|
-
// Cannot return undefined
|
|
155
69
|
return null;
|
|
156
70
|
}
|
|
157
71
|
}
|
|
@@ -178,7 +92,6 @@ class ClickableBehavior extends React.Component {
|
|
|
178
92
|
};
|
|
179
93
|
|
|
180
94
|
this.handleMouseEnter = e => {
|
|
181
|
-
// When the left button is pressed already, we want it to be pressed
|
|
182
95
|
if (e.buttons === 1) {
|
|
183
96
|
this.dragging = true;
|
|
184
97
|
this.setState({
|
|
@@ -262,17 +175,11 @@ class ClickableBehavior extends React.Component {
|
|
|
262
175
|
} = getAppropriateTriggersForRole(role);
|
|
263
176
|
|
|
264
177
|
if (triggerOnEnter && keyCode === keyCodes.enter || triggerOnSpace && keyCode === keyCodes.space) {
|
|
265
|
-
// This prevents space from scrolling down. It also prevents the
|
|
266
|
-
// space and enter keys from triggering click events. We manually
|
|
267
|
-
// call the supplied onClick and handle potential navigation in
|
|
268
|
-
// handleKeyUp instead.
|
|
269
178
|
e.preventDefault();
|
|
270
179
|
this.setState({
|
|
271
180
|
pressed: true
|
|
272
181
|
});
|
|
273
182
|
} else if (!triggerOnEnter && keyCode === keyCodes.enter) {
|
|
274
|
-
// If the component isn't supposed to trigger on enter, we have to
|
|
275
|
-
// keep track of the enter keydown to negate the onClick callback
|
|
276
183
|
this.enterClick = true;
|
|
277
184
|
}
|
|
278
185
|
};
|
|
@@ -344,8 +251,7 @@ class ClickableBehavior extends React.Component {
|
|
|
344
251
|
waiting: false
|
|
345
252
|
});
|
|
346
253
|
} else {
|
|
347
|
-
window.location.assign(href);
|
|
348
|
-
// load navigation will do that for us by loading a new page.
|
|
254
|
+
window.location.assign(href);
|
|
349
255
|
}
|
|
350
256
|
}
|
|
351
257
|
} else {
|
|
@@ -362,15 +268,11 @@ class ClickableBehavior extends React.Component {
|
|
|
362
268
|
} = this.props;
|
|
363
269
|
|
|
364
270
|
if (history && !skipClientNav || this.props.target === "_blank") {
|
|
365
|
-
// client-side nav
|
|
366
271
|
safeWithNav();
|
|
367
272
|
this.navigateOrReset(shouldNavigate);
|
|
368
273
|
return Promise.resolve();
|
|
369
274
|
} else {
|
|
370
275
|
if (!this.state.waiting) {
|
|
371
|
-
// We only show the spinner for safeWithNav when doing
|
|
372
|
-
// a full page load navigation since since the spinner is
|
|
373
|
-
// indicating that we're waiting for navigation to occur.
|
|
374
276
|
this.setState({
|
|
375
277
|
waiting: true
|
|
376
278
|
});
|
|
@@ -378,19 +280,13 @@ class ClickableBehavior extends React.Component {
|
|
|
378
280
|
|
|
379
281
|
return safeWithNav().then(() => {
|
|
380
282
|
if (!this.state.waiting) {
|
|
381
|
-
// We only show the spinner for safeWithNav when doing
|
|
382
|
-
// a full page load navigation since since the spinner is
|
|
383
|
-
// indicating that we're waiting for navigation to occur.
|
|
384
283
|
this.setState({
|
|
385
284
|
waiting: true
|
|
386
285
|
});
|
|
387
286
|
}
|
|
388
287
|
|
|
389
288
|
return;
|
|
390
|
-
}).catch(error => {
|
|
391
|
-
// navigate when using safeWithNav regardless of
|
|
392
|
-
// whether we're doing a client-side nav or not.
|
|
393
|
-
}).finally(() => {
|
|
289
|
+
}).catch(error => {}).finally(() => {
|
|
394
290
|
this.navigateOrReset(shouldNavigate);
|
|
395
291
|
});
|
|
396
292
|
}
|
|
@@ -409,9 +305,7 @@ class ClickableBehavior extends React.Component {
|
|
|
409
305
|
|
|
410
306
|
if (onClick) {
|
|
411
307
|
onClick(e);
|
|
412
|
-
}
|
|
413
|
-
// navigate.
|
|
414
|
-
|
|
308
|
+
}
|
|
415
309
|
|
|
416
310
|
if (e.defaultPrevented) {
|
|
417
311
|
shouldNavigate = false;
|
|
@@ -425,20 +319,12 @@ class ClickableBehavior extends React.Component {
|
|
|
425
319
|
|
|
426
320
|
while (target) {
|
|
427
321
|
if (target instanceof window.HTMLFormElement) {
|
|
428
|
-
// This event must be marked as cancelable otherwise calling
|
|
429
|
-
// e.preventDefault() on it won't do anything in Firefox.
|
|
430
|
-
// Chrome and Safari allow calling e.preventDefault() on
|
|
431
|
-
// non-cancelable events, but really they shouldn't.
|
|
432
322
|
const event = new window.Event("submit", {
|
|
433
323
|
cancelable: true
|
|
434
324
|
});
|
|
435
325
|
target.dispatchEvent(event);
|
|
436
326
|
break;
|
|
437
|
-
}
|
|
438
|
-
// Updating all of the places will take some time so I'll do
|
|
439
|
-
// this later
|
|
440
|
-
// $FlowFixMe[prop-missing]
|
|
441
|
-
|
|
327
|
+
}
|
|
442
328
|
|
|
443
329
|
target = target.parentElement;
|
|
444
330
|
}
|
|
@@ -463,7 +349,10 @@ class ClickableBehavior extends React.Component {
|
|
|
463
349
|
}
|
|
464
350
|
|
|
465
351
|
render() {
|
|
466
|
-
const childrenProps = this.props.disabled ? disabledHandlers
|
|
352
|
+
const childrenProps = this.props.disabled ? _extends({}, disabledHandlers, {
|
|
353
|
+
onFocus: this.handleFocus,
|
|
354
|
+
onBlur: this.handleBlur
|
|
355
|
+
}) : {
|
|
467
356
|
onClick: this.handleClick,
|
|
468
357
|
onMouseEnter: this.handleMouseEnter,
|
|
469
358
|
onMouseLeave: this.handleMouseLeave,
|
|
@@ -477,15 +366,8 @@ class ClickableBehavior extends React.Component {
|
|
|
477
366
|
onKeyUp: this.handleKeyUp,
|
|
478
367
|
onFocus: this.handleFocus,
|
|
479
368
|
onBlur: this.handleBlur,
|
|
480
|
-
// We set tabIndex to 0 so that users can tab to clickable
|
|
481
|
-
// things that aren't buttons or anchors.
|
|
482
369
|
tabIndex: 0
|
|
483
|
-
};
|
|
484
|
-
// `rel` attributes. This is to ensure that the links we're sending folks
|
|
485
|
-
// to can't hijack the existing page. These defaults can be overriden
|
|
486
|
-
// by passing in a different value for the `rel` prop.
|
|
487
|
-
// More info: https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
|
|
488
|
-
|
|
370
|
+
};
|
|
489
371
|
childrenProps.rel = this.props.rel || (this.props.target === "_blank" ? "noopener noreferrer" : undefined);
|
|
490
372
|
const {
|
|
491
373
|
children
|
|
@@ -498,12 +380,6 @@ ClickableBehavior.defaultProps = {
|
|
|
498
380
|
disabled: false
|
|
499
381
|
};
|
|
500
382
|
|
|
501
|
-
/**
|
|
502
|
-
* Returns:
|
|
503
|
-
* - false for hrefs staring with http://, https://, //.
|
|
504
|
-
* - false for '#', 'javascript:...', 'mailto:...', 'tel:...', etc.
|
|
505
|
-
* - true for all other values, e.g. /foo/bar
|
|
506
|
-
*/
|
|
507
383
|
const isClientSideUrl = href => {
|
|
508
384
|
if (typeof href !== "string") {
|
|
509
385
|
return false;
|
|
@@ -512,35 +388,9 @@ const isClientSideUrl = href => {
|
|
|
512
388
|
return !/^(https?:)?\/\//i.test(href) && !/^([^#]*#[\w-]*|[\w\-.]+:)/.test(href);
|
|
513
389
|
};
|
|
514
390
|
|
|
515
|
-
/**
|
|
516
|
-
* Returns either the default ClickableBehavior or a react-router aware version.
|
|
517
|
-
*
|
|
518
|
-
* The react-router aware version is returned if `router` is a react-router-dom
|
|
519
|
-
* router, `skipClientNav` is not `true`, and `href` is an internal URL.
|
|
520
|
-
*
|
|
521
|
-
* The `router` can be accessed via __RouterContext (imported from 'react-router')
|
|
522
|
-
* from a component rendered as a descendant of a BrowserRouter.
|
|
523
|
-
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
524
|
-
*/
|
|
525
391
|
const ClickableBehaviorWithRouter = withRouter(ClickableBehavior);
|
|
526
|
-
function getClickableBehavior(
|
|
527
|
-
/**
|
|
528
|
-
* The URL to navigate to.
|
|
529
|
-
*/
|
|
530
|
-
href,
|
|
531
|
-
/**
|
|
532
|
-
* Should we skip using the react router and go to the page directly.
|
|
533
|
-
*/
|
|
534
|
-
skipClientNav,
|
|
535
|
-
/**
|
|
536
|
-
* router object added to the React context object by react-router-dom.
|
|
537
|
-
*/
|
|
538
|
-
router) {
|
|
392
|
+
function getClickableBehavior(href, skipClientNav, router) {
|
|
539
393
|
if (router && skipClientNav !== true && href && isClientSideUrl(href)) {
|
|
540
|
-
// We cast to `any` here since the type of ClickableBehaviorWithRouter
|
|
541
|
-
// is slightly different from the return type of this function.
|
|
542
|
-
// TODO(WB-1037): Always return the wrapped version once all routes have
|
|
543
|
-
// been ported to the app-shell in webapp.
|
|
544
394
|
return ClickableBehaviorWithRouter;
|
|
545
395
|
}
|
|
546
396
|
|
|
@@ -551,58 +401,32 @@ const _excluded = ["href", "onClick", "skipClientNav", "beforeNav", "safeWithNav
|
|
|
551
401
|
const StyledAnchor = addStyle("a");
|
|
552
402
|
const StyledButton = addStyle("button");
|
|
553
403
|
const StyledLink = addStyle(Link);
|
|
554
|
-
/**
|
|
555
|
-
* A component to turn any custom component into a clickable one.
|
|
556
|
-
*
|
|
557
|
-
* Works by wrapping ClickableBehavior around the child element and styling the
|
|
558
|
-
* child appropriately and encapsulates routing logic which can be customized.
|
|
559
|
-
* Expects a function which returns an element as it's child.
|
|
560
|
-
*
|
|
561
|
-
* Example usage:
|
|
562
|
-
* ```jsx
|
|
563
|
-
* <Clickable onClick={() => alert("You clicked me!")}>
|
|
564
|
-
* {({hovered, focused, pressed}) =>
|
|
565
|
-
* <div
|
|
566
|
-
* style={[
|
|
567
|
-
* hovered && styles.hovered,
|
|
568
|
-
* focused && styles.focused,
|
|
569
|
-
* pressed && styles.pressed,
|
|
570
|
-
* ]}
|
|
571
|
-
* >
|
|
572
|
-
* Click Me!
|
|
573
|
-
* </div>
|
|
574
|
-
* }
|
|
575
|
-
* </Clickable>
|
|
576
|
-
* ```
|
|
577
|
-
*/
|
|
578
|
-
|
|
579
404
|
class Clickable extends React.Component {
|
|
580
405
|
constructor(...args) {
|
|
581
406
|
super(...args);
|
|
582
407
|
|
|
583
408
|
this.getCorrectTag = (clickableState, router, commonProps) => {
|
|
584
409
|
const activeHref = this.props.href && !this.props.disabled;
|
|
585
|
-
const useClient = router && !this.props.skipClientNav && isClientSideUrl(this.props.href || "");
|
|
586
|
-
// needs it to refine this.props.href to a string.
|
|
410
|
+
const useClient = router && !this.props.skipClientNav && isClientSideUrl(this.props.href || "");
|
|
587
411
|
|
|
588
412
|
if (activeHref && useClient && this.props.href) {
|
|
589
|
-
return
|
|
413
|
+
return React.createElement(StyledLink, _extends({}, commonProps, {
|
|
590
414
|
to: this.props.href,
|
|
591
415
|
role: this.props.role,
|
|
592
416
|
target: this.props.target || undefined,
|
|
593
417
|
"aria-disabled": this.props.disabled ? "true" : undefined
|
|
594
418
|
}), this.props.children(clickableState));
|
|
595
419
|
} else if (activeHref && !useClient) {
|
|
596
|
-
return
|
|
420
|
+
return React.createElement(StyledAnchor, _extends({}, commonProps, {
|
|
597
421
|
href: this.props.href,
|
|
598
422
|
role: this.props.role,
|
|
599
423
|
target: this.props.target || undefined,
|
|
600
424
|
"aria-disabled": this.props.disabled ? "true" : undefined
|
|
601
425
|
}), this.props.children(clickableState));
|
|
602
426
|
} else {
|
|
603
|
-
return
|
|
427
|
+
return React.createElement(StyledButton, _extends({}, commonProps, {
|
|
604
428
|
type: "button",
|
|
605
|
-
disabled: this.props.disabled
|
|
429
|
+
"aria-disabled": this.props.disabled
|
|
606
430
|
}), this.props.children(clickableState));
|
|
607
431
|
}
|
|
608
432
|
};
|
|
@@ -632,7 +456,7 @@ class Clickable extends React.Component {
|
|
|
632
456
|
const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), style];
|
|
633
457
|
|
|
634
458
|
if (beforeNav) {
|
|
635
|
-
return
|
|
459
|
+
return React.createElement(ClickableBehavior, {
|
|
636
460
|
href: href,
|
|
637
461
|
onClick: onClick,
|
|
638
462
|
beforeNav: beforeNav,
|
|
@@ -645,7 +469,7 @@ class Clickable extends React.Component {
|
|
|
645
469
|
style: getStyle(state)
|
|
646
470
|
}, childrenProps)));
|
|
647
471
|
} else {
|
|
648
|
-
return
|
|
472
|
+
return React.createElement(ClickableBehavior, {
|
|
649
473
|
href: href,
|
|
650
474
|
onClick: onClick,
|
|
651
475
|
safeWithNav: safeWithNav,
|
|
@@ -661,11 +485,10 @@ class Clickable extends React.Component {
|
|
|
661
485
|
}
|
|
662
486
|
|
|
663
487
|
render() {
|
|
664
|
-
return
|
|
488
|
+
return React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
|
|
665
489
|
}
|
|
666
490
|
|
|
667
|
-
}
|
|
668
|
-
|
|
491
|
+
}
|
|
669
492
|
Clickable.defaultProps = {
|
|
670
493
|
light: false,
|
|
671
494
|
disabled: false,
|
|
@@ -680,23 +503,13 @@ const styles = StyleSheet.create({
|
|
|
680
503
|
overflow: "visible",
|
|
681
504
|
background: "transparent",
|
|
682
505
|
textDecoration: "none",
|
|
683
|
-
|
|
684
|
-
/* inherit font & color from ancestor */
|
|
685
506
|
color: "inherit",
|
|
686
507
|
font: "inherit",
|
|
687
508
|
boxSizing: "border-box",
|
|
688
|
-
// This removes the 300ms click delay on mobile browsers by indicating that
|
|
689
|
-
// "double-tap to zoom" shouldn't be used on this element.
|
|
690
509
|
touchAction: "manipulation",
|
|
691
510
|
userSelect: "none",
|
|
692
|
-
// This is usual frowned upon b/c of accessibility. We expect users of Clickable
|
|
693
|
-
// to define their own focus styles.
|
|
694
511
|
outline: "none",
|
|
695
|
-
|
|
696
|
-
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
|
697
512
|
lineHeight: "normal",
|
|
698
|
-
|
|
699
|
-
/* Corrects font smoothing for webkit */
|
|
700
513
|
WebkitFontSmoothing: "inherit",
|
|
701
514
|
MozOsxFontSmoothing: "inherit"
|
|
702
515
|
},
|
package/dist/index.js
CHANGED
|
@@ -164,8 +164,6 @@ const disabledHandlers = {
|
|
|
164
164
|
onTouchCancel: () => void 0,
|
|
165
165
|
onKeyDown: () => void 0,
|
|
166
166
|
onKeyUp: () => void 0,
|
|
167
|
-
onFocus: () => void 0,
|
|
168
|
-
onBlur: () => void 0,
|
|
169
167
|
// Clickable components should still be tabbable so they can
|
|
170
168
|
// be used as anchors.
|
|
171
169
|
tabIndex: 0
|
|
@@ -261,9 +259,12 @@ const startState = {
|
|
|
261
259
|
|
|
262
260
|
class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
263
261
|
static getDerivedStateFromProps(props, state) {
|
|
264
|
-
// If new props are disabled, reset the hovered/
|
|
262
|
+
// If new props are disabled, reset the hovered/pressed states
|
|
265
263
|
if (props.disabled) {
|
|
266
|
-
|
|
264
|
+
// Keep the focused state for enabling keyboard navigation.
|
|
265
|
+
return { ...startState,
|
|
266
|
+
focused: state.focused
|
|
267
|
+
};
|
|
267
268
|
} else {
|
|
268
269
|
// Cannot return undefined
|
|
269
270
|
return null;
|
|
@@ -577,7 +578,11 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
577
578
|
}
|
|
578
579
|
|
|
579
580
|
render() {
|
|
580
|
-
const childrenProps = this.props.disabled ? disabledHandlers
|
|
581
|
+
const childrenProps = this.props.disabled ? { ...disabledHandlers,
|
|
582
|
+
// Keep these handlers for keyboard accessibility.
|
|
583
|
+
onFocus: this.handleFocus,
|
|
584
|
+
onBlur: this.handleBlur
|
|
585
|
+
} : {
|
|
581
586
|
onClick: this.handleClick,
|
|
582
587
|
onMouseEnter: this.handleMouseEnter,
|
|
583
588
|
onMouseLeave: this.handleMouseLeave,
|
|
@@ -1214,7 +1219,7 @@ class Clickable extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1214
1219
|
} else {
|
|
1215
1220
|
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](StyledButton, _extends({}, commonProps, {
|
|
1216
1221
|
type: "button",
|
|
1217
|
-
disabled: this.props.disabled
|
|
1222
|
+
"aria-disabled": this.props.disabled
|
|
1218
1223
|
}), this.props.children(clickableState));
|
|
1219
1224
|
}
|
|
1220
1225
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-clickable",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Clickable component for Wonder-Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@babel/runtime": "^7.16.3",
|
|
19
|
-
"@khanacademy/wonder-blocks-core": "^4.
|
|
19
|
+
"@khanacademy/wonder-blocks-core": "^4.3.1"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"aphrodite": "^1.2.5",
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"react-router-dom": "5.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"wb-dev-build-settings": "^0.
|
|
29
|
+
"wb-dev-build-settings": "^0.4.0"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -21,9 +21,9 @@ exports[`wonder-blocks-clickable example 1 1`] = `
|
|
|
21
21
|
}
|
|
22
22
|
>
|
|
23
23
|
<button
|
|
24
|
+
aria-disabled={false}
|
|
24
25
|
aria-label=""
|
|
25
26
|
className=""
|
|
26
|
-
disabled={false}
|
|
27
27
|
onBlur={[Function]}
|
|
28
28
|
onClick={[Function]}
|
|
29
29
|
onDragStart={[Function]}
|
|
@@ -128,9 +128,9 @@ exports[`wonder-blocks-clickable example 2 1`] = `
|
|
|
128
128
|
}
|
|
129
129
|
>
|
|
130
130
|
<button
|
|
131
|
+
aria-disabled={false}
|
|
131
132
|
aria-label=""
|
|
132
133
|
className=""
|
|
133
|
-
disabled={false}
|
|
134
134
|
onBlur={[Function]}
|
|
135
135
|
onClick={[Function]}
|
|
136
136
|
onDragStart={[Function]}
|
|
@@ -378,7 +378,7 @@ describe("ClickableBehavior", () => {
|
|
|
378
378
|
expect(button.state("pressed")).toEqual(false);
|
|
379
379
|
|
|
380
380
|
button.simulate("focus");
|
|
381
|
-
expect(button.state("focused")).toEqual(
|
|
381
|
+
expect(button.state("focused")).toEqual(true);
|
|
382
382
|
|
|
383
383
|
const anchor = shallow(
|
|
384
384
|
<ClickableBehavior
|
|
@@ -464,7 +464,7 @@ describe("ClickableBehavior", () => {
|
|
|
464
464
|
|
|
465
465
|
expect(button.state("hovered")).toEqual(false);
|
|
466
466
|
expect(button.state("pressed")).toEqual(false);
|
|
467
|
-
expect(button.state("focused")).toEqual(
|
|
467
|
+
expect(button.state("focused")).toEqual(true);
|
|
468
468
|
});
|
|
469
469
|
|
|
470
470
|
describe("full page load navigation", () => {
|
|
@@ -3,6 +3,8 @@ import * as React from "react";
|
|
|
3
3
|
import {MemoryRouter, Route, Switch} from "react-router-dom";
|
|
4
4
|
import {mount} from "enzyme";
|
|
5
5
|
import "jest-enzyme";
|
|
6
|
+
import {render, screen} from "@testing-library/react";
|
|
7
|
+
import userEvent from "@testing-library/user-event";
|
|
6
8
|
|
|
7
9
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
8
10
|
import Clickable from "../clickable.js";
|
|
@@ -452,6 +454,47 @@ describe("Clickable", () => {
|
|
|
452
454
|
);
|
|
453
455
|
});
|
|
454
456
|
|
|
457
|
+
test("should add aria-disabled if disabled is set", () => {
|
|
458
|
+
// Arrange
|
|
459
|
+
|
|
460
|
+
// Act
|
|
461
|
+
render(
|
|
462
|
+
<Clickable testId="clickable-button" disabled={true}>
|
|
463
|
+
{(eventState) => <h1>Click Me!</h1>}
|
|
464
|
+
</Clickable>,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const button = screen.getByTestId("clickable-button");
|
|
468
|
+
|
|
469
|
+
// Assert
|
|
470
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("allow keyboard navigation when disabled is set", () => {
|
|
474
|
+
// Arrange
|
|
475
|
+
render(
|
|
476
|
+
<div>
|
|
477
|
+
<button>First focusable button</button>
|
|
478
|
+
<Clickable testId="clickable-button" disabled={true}>
|
|
479
|
+
{(eventState) => <h1>Click Me!</h1>}
|
|
480
|
+
</Clickable>
|
|
481
|
+
</div>,
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Act
|
|
485
|
+
// RTL's focuses on `document.body` by default, so we need to focus on
|
|
486
|
+
// the first button
|
|
487
|
+
userEvent.tab();
|
|
488
|
+
|
|
489
|
+
// Then we focus on our Clickable button.
|
|
490
|
+
userEvent.tab();
|
|
491
|
+
|
|
492
|
+
const button = screen.getByTestId("clickable-button");
|
|
493
|
+
|
|
494
|
+
// Assert
|
|
495
|
+
expect(button).toHaveFocus();
|
|
496
|
+
});
|
|
497
|
+
|
|
455
498
|
describe("raw events", () => {
|
|
456
499
|
/**
|
|
457
500
|
* Clickable expect a function as children so we create a simple wrapper to
|
|
@@ -166,6 +166,7 @@ type Props =
|
|
|
166
166
|
* A target destination window for a link to open in. Should only be used
|
|
167
167
|
* when `href` is specified.
|
|
168
168
|
*/
|
|
169
|
+
// TODO(WB-1262): only allow this prop when `href` is also set.
|
|
169
170
|
target?: "_blank",
|
|
170
171
|
|}
|
|
171
172
|
| {|
|
|
@@ -221,8 +222,6 @@ const disabledHandlers = {
|
|
|
221
222
|
onTouchCancel: () => void 0,
|
|
222
223
|
onKeyDown: () => void 0,
|
|
223
224
|
onKeyUp: () => void 0,
|
|
224
|
-
onFocus: () => void 0,
|
|
225
|
-
onBlur: () => void 0,
|
|
226
225
|
// Clickable components should still be tabbable so they can
|
|
227
226
|
// be used as anchors.
|
|
228
227
|
tabIndex: 0,
|
|
@@ -334,9 +333,10 @@ export default class ClickableBehavior extends React.Component<
|
|
|
334
333
|
props: Props,
|
|
335
334
|
state: ClickableState,
|
|
336
335
|
): ?Partial<ClickableState> {
|
|
337
|
-
// If new props are disabled, reset the hovered/
|
|
336
|
+
// If new props are disabled, reset the hovered/pressed states
|
|
338
337
|
if (props.disabled) {
|
|
339
|
-
|
|
338
|
+
// Keep the focused state for enabling keyboard navigation.
|
|
339
|
+
return {...startState, focused: state.focused};
|
|
340
340
|
} else {
|
|
341
341
|
// Cannot return undefined
|
|
342
342
|
return null;
|
|
@@ -610,7 +610,12 @@ export default class ClickableBehavior extends React.Component<
|
|
|
610
610
|
|
|
611
611
|
render(): React.Node {
|
|
612
612
|
const childrenProps: ChildrenProps = this.props.disabled
|
|
613
|
-
?
|
|
613
|
+
? {
|
|
614
|
+
...disabledHandlers,
|
|
615
|
+
// Keep these handlers for keyboard accessibility.
|
|
616
|
+
onFocus: this.handleFocus,
|
|
617
|
+
onBlur: this.handleBlur,
|
|
618
|
+
}
|
|
614
619
|
: {
|
|
615
620
|
onClick: this.handleClick,
|
|
616
621
|
onMouseEnter: this.handleMouseEnter,
|
|
@@ -111,6 +111,8 @@ type Props =
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* A target destination window for a link to open in.
|
|
114
|
+
*
|
|
115
|
+
* TODO(WB-1262): only allow this prop when `href` is also set.t
|
|
114
116
|
*/
|
|
115
117
|
target?: "_blank",
|
|
116
118
|
|}
|
|
@@ -143,6 +145,8 @@ type Props =
|
|
|
143
145
|
|
|
144
146
|
/**
|
|
145
147
|
* A target destination window for a link to open in.
|
|
148
|
+
*
|
|
149
|
+
* TODO(WB-1262): only allow this prop when `href` is also set.t
|
|
146
150
|
*/
|
|
147
151
|
target?: "_blank",
|
|
148
152
|
|}
|
|
@@ -252,7 +256,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
252
256
|
<StyledButton
|
|
253
257
|
{...commonProps}
|
|
254
258
|
type="button"
|
|
255
|
-
disabled={this.props.disabled}
|
|
259
|
+
aria-disabled={this.props.disabled}
|
|
256
260
|
>
|
|
257
261
|
{this.props.children(clickableState)}
|
|
258
262
|
</StyledButton>
|