@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 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); // We don't bother clearing the waiting state, the full page
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 => {// We ignore the error here so that we always
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
- } // If onClick() has called e.preventDefault() then we shouldn't
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
- } // All events should be typed as SyntheticEvent<HTMLElement>.
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
- }; // When the link is set to open in a new window, we want to set some
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 || ""); // NOTE: checking this.props.href here is redundant, but flow
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 /*#__PURE__*/React.createElement(StyledLink, _extends({}, commonProps, {
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 /*#__PURE__*/React.createElement(StyledAnchor, _extends({}, commonProps, {
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 /*#__PURE__*/React.createElement(StyledButton, _extends({}, commonProps, {
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 /*#__PURE__*/React.createElement(ClickableBehavior, {
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 /*#__PURE__*/React.createElement(ClickableBehavior, {
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 /*#__PURE__*/React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
488
+ return React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
665
489
  }
666
490
 
667
- } // Source: https://gist.github.com/MoOx/9137295
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/focused/pressed states
262
+ // If new props are disabled, reset the hovered/pressed states
265
263
  if (props.disabled) {
266
- return startState;
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",
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.2.1"
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.3.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(false);
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(false);
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/focused/pressed states
336
+ // If new props are disabled, reset the hovered/pressed states
338
337
  if (props.disabled) {
339
- return startState;
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
- ? disabledHandlers
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>