@khanacademy/wonder-blocks-clickable 2.2.5 → 2.3.0

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,25 @@
1
1
  # @khanacademy/wonder-blocks-clickable
2
2
 
3
+ ## 2.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ee6fc773: Added keyboard support to search items when the dropdown is focused, included "Enter" as a key to trigger actions with the "option" role
8
+
9
+ ## 2.2.7
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [5f4a4297]
14
+ - Updated dependencies [2b96fd59]
15
+ - @khanacademy/wonder-blocks-core@4.3.2
16
+
17
+ ## 2.2.6
18
+
19
+ ### Patch Changes
20
+
21
+ - @khanacademy/wonder-blocks-core@4.3.1
22
+
3
23
  ## 2.2.5
4
24
 
5
25
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -9,27 +9,24 @@ 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":
22
20
  case "listbox":
23
- case "option":
24
21
  return {
25
22
  triggerOnEnter: false,
26
23
  triggerOnSpace: true
27
24
  };
28
- // Triggers on both ENTER and SPACE
29
25
 
30
26
  case "button":
31
27
  case "menuitem":
32
28
  case "menu":
29
+ case "option":
33
30
  default:
34
31
  return {
35
32
  triggerOnEnter: true,
@@ -50,8 +47,6 @@ const disabledHandlers = {
50
47
  onTouchCancel: () => void 0,
51
48
  onKeyDown: () => void 0,
52
49
  onKeyUp: () => void 0,
53
- // Clickable components should still be tabbable so they can
54
- // be used as anchors.
55
50
  tabIndex: 0
56
51
  };
57
52
  const keyCodes = {
@@ -64,95 +59,13 @@ const startState = {
64
59
  pressed: false,
65
60
  waiting: false
66
61
  };
67
- /**
68
- * Add hover, focus, and active status updates to a clickable component.
69
- *
70
- * Via mouse:
71
- *
72
- * 1. Hover over button -> hover state
73
- * 2. Mouse down -> active state
74
- * 3. Mouse up -> default state
75
- * 4. Press tab -> focus state
76
- *
77
- * Via touch:
78
- *
79
- * 1. Touch down -> press state
80
- * 2. Touch up -> default state
81
- *
82
- * Via keyboard:
83
- *
84
- * 1. Tab to focus -> focus state
85
- * 2. Keydown (spacebar/enter) -> active state
86
- * 3. Keyup (spacebar/enter) -> focus state
87
- *
88
- * Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
89
- * onMouseDown, onMouseUp, onDragStart, onTouchStart, onTouchEnd, onTouchCancel, onKeyDown,
90
- * onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the component
91
- * that has the ClickableBehavior. You cannot override these handlers without
92
- * potentially breaking the functionality of ClickableBehavior.
93
- *
94
- * There are internal props triggerOnEnter and triggerOnSpace that can be set
95
- * to false if one of those keys shouldn't count as a click on this component.
96
- * Be careful about setting those to false -- make certain that the component
97
- * shouldn't process that key.
98
- *
99
- * See [this document](https://docs.google.com/document/d/1DG5Rg2f0cawIL5R8UqnPQpd7pbdObk8OyjO5ryYQmBM/edit#)
100
- * for a more thorough explanation of expected behaviors and potential cavaets.
101
- *
102
- * `ClickableBehavior` accepts a function as `children` which is passed state
103
- * and an object containing event handlers and some other props. The `children`
104
- * function should return a clickable React Element of some sort.
105
- *
106
- * Example:
107
- *
108
- * ```js
109
- * class MyClickableComponent extends React.Component<Props> {
110
- * render(): React.Node {
111
- * const ClickableBehavior = getClickableBehavior();
112
- * return <ClickableBehavior
113
- * disabled={this.props.disabled}
114
- * onClick={this.props.onClick}
115
- * >
116
- * {({hovered}, childrenProps) =>
117
- * <RoundRect
118
- * textcolor='white'
119
- * backgroundColor={hovered ? 'red' : 'blue'}}
120
- * {...childrenProps}
121
- * >
122
- * {this.props.children}
123
- * </RoundRect>
124
- * }
125
- * </ClickableBehavior>
126
- * }
127
- * }
128
- * ```
129
- *
130
- * This follows a pattern called [Function as Child Components]
131
- * (https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9).
132
- *
133
- * WARNING: Do not use this component directly, use getClickableBehavior
134
- * instead. getClickableBehavior takes three arguments (href, directtNav, and
135
- * router) and returns either the default ClickableBehavior or a react-router
136
- * aware version.
137
- *
138
- * The react-router aware version is returned if `router` is a react-router-dom
139
- * router, `skipClientNav` is not `true`, and `href` is an internal URL.
140
- *
141
- * The `router` can be accessed via __RouterContext (imported from 'react-router')
142
- * from a component rendered as a descendant of a BrowserRouter.
143
- * See https://reacttraining.com/react-router/web/guides/basic-components.
144
- */
145
-
146
62
  class ClickableBehavior extends React.Component {
147
63
  static getDerivedStateFromProps(props, state) {
148
- // If new props are disabled, reset the hovered/pressed states
149
64
  if (props.disabled) {
150
- // Keep the focused state for enabling keyboard navigation.
151
65
  return _extends({}, startState, {
152
66
  focused: state.focused
153
67
  });
154
68
  } else {
155
- // Cannot return undefined
156
69
  return null;
157
70
  }
158
71
  }
@@ -179,7 +92,6 @@ class ClickableBehavior extends React.Component {
179
92
  };
180
93
 
181
94
  this.handleMouseEnter = e => {
182
- // When the left button is pressed already, we want it to be pressed
183
95
  if (e.buttons === 1) {
184
96
  this.dragging = true;
185
97
  this.setState({
@@ -263,17 +175,11 @@ class ClickableBehavior extends React.Component {
263
175
  } = getAppropriateTriggersForRole(role);
264
176
 
265
177
  if (triggerOnEnter && keyCode === keyCodes.enter || triggerOnSpace && keyCode === keyCodes.space) {
266
- // This prevents space from scrolling down. It also prevents the
267
- // space and enter keys from triggering click events. We manually
268
- // call the supplied onClick and handle potential navigation in
269
- // handleKeyUp instead.
270
178
  e.preventDefault();
271
179
  this.setState({
272
180
  pressed: true
273
181
  });
274
182
  } else if (!triggerOnEnter && keyCode === keyCodes.enter) {
275
- // If the component isn't supposed to trigger on enter, we have to
276
- // keep track of the enter keydown to negate the onClick callback
277
183
  this.enterClick = true;
278
184
  }
279
185
  };
@@ -345,8 +251,7 @@ class ClickableBehavior extends React.Component {
345
251
  waiting: false
346
252
  });
347
253
  } else {
348
- window.location.assign(href); // We don't bother clearing the waiting state, the full page
349
- // load navigation will do that for us by loading a new page.
254
+ window.location.assign(href);
350
255
  }
351
256
  }
352
257
  } else {
@@ -363,15 +268,11 @@ class ClickableBehavior extends React.Component {
363
268
  } = this.props;
364
269
 
365
270
  if (history && !skipClientNav || this.props.target === "_blank") {
366
- // client-side nav
367
271
  safeWithNav();
368
272
  this.navigateOrReset(shouldNavigate);
369
273
  return Promise.resolve();
370
274
  } else {
371
275
  if (!this.state.waiting) {
372
- // We only show the spinner for safeWithNav when doing
373
- // a full page load navigation since since the spinner is
374
- // indicating that we're waiting for navigation to occur.
375
276
  this.setState({
376
277
  waiting: true
377
278
  });
@@ -379,19 +280,13 @@ class ClickableBehavior extends React.Component {
379
280
 
380
281
  return safeWithNav().then(() => {
381
282
  if (!this.state.waiting) {
382
- // We only show the spinner for safeWithNav when doing
383
- // a full page load navigation since since the spinner is
384
- // indicating that we're waiting for navigation to occur.
385
283
  this.setState({
386
284
  waiting: true
387
285
  });
388
286
  }
389
287
 
390
288
  return;
391
- }).catch(error => {// We ignore the error here so that we always
392
- // navigate when using safeWithNav regardless of
393
- // whether we're doing a client-side nav or not.
394
- }).finally(() => {
289
+ }).catch(error => {}).finally(() => {
395
290
  this.navigateOrReset(shouldNavigate);
396
291
  });
397
292
  }
@@ -410,9 +305,7 @@ class ClickableBehavior extends React.Component {
410
305
 
411
306
  if (onClick) {
412
307
  onClick(e);
413
- } // If onClick() has called e.preventDefault() then we shouldn't
414
- // navigate.
415
-
308
+ }
416
309
 
417
310
  if (e.defaultPrevented) {
418
311
  shouldNavigate = false;
@@ -426,20 +319,12 @@ class ClickableBehavior extends React.Component {
426
319
 
427
320
  while (target) {
428
321
  if (target instanceof window.HTMLFormElement) {
429
- // This event must be marked as cancelable otherwise calling
430
- // e.preventDefault() on it won't do anything in Firefox.
431
- // Chrome and Safari allow calling e.preventDefault() on
432
- // non-cancelable events, but really they shouldn't.
433
322
  const event = new window.Event("submit", {
434
323
  cancelable: true
435
324
  });
436
325
  target.dispatchEvent(event);
437
326
  break;
438
- } // All events should be typed as SyntheticEvent<HTMLElement>.
439
- // Updating all of the places will take some time so I'll do
440
- // this later
441
- // $FlowFixMe[prop-missing]
442
-
327
+ }
443
328
 
444
329
  target = target.parentElement;
445
330
  }
@@ -465,7 +350,6 @@ class ClickableBehavior extends React.Component {
465
350
 
466
351
  render() {
467
352
  const childrenProps = this.props.disabled ? _extends({}, disabledHandlers, {
468
- // Keep these handlers for keyboard accessibility.
469
353
  onFocus: this.handleFocus,
470
354
  onBlur: this.handleBlur
471
355
  }) : {
@@ -482,15 +366,8 @@ class ClickableBehavior extends React.Component {
482
366
  onKeyUp: this.handleKeyUp,
483
367
  onFocus: this.handleFocus,
484
368
  onBlur: this.handleBlur,
485
- // We set tabIndex to 0 so that users can tab to clickable
486
- // things that aren't buttons or anchors.
487
369
  tabIndex: 0
488
- }; // When the link is set to open in a new window, we want to set some
489
- // `rel` attributes. This is to ensure that the links we're sending folks
490
- // to can't hijack the existing page. These defaults can be overriden
491
- // by passing in a different value for the `rel` prop.
492
- // More info: https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
493
-
370
+ };
494
371
  childrenProps.rel = this.props.rel || (this.props.target === "_blank" ? "noopener noreferrer" : undefined);
495
372
  const {
496
373
  children
@@ -503,12 +380,6 @@ ClickableBehavior.defaultProps = {
503
380
  disabled: false
504
381
  };
505
382
 
506
- /**
507
- * Returns:
508
- * - false for hrefs staring with http://, https://, //.
509
- * - false for '#', 'javascript:...', 'mailto:...', 'tel:...', etc.
510
- * - true for all other values, e.g. /foo/bar
511
- */
512
383
  const isClientSideUrl = href => {
513
384
  if (typeof href !== "string") {
514
385
  return false;
@@ -517,35 +388,9 @@ const isClientSideUrl = href => {
517
388
  return !/^(https?:)?\/\//i.test(href) && !/^([^#]*#[\w-]*|[\w\-.]+:)/.test(href);
518
389
  };
519
390
 
520
- /**
521
- * Returns either the default ClickableBehavior or a react-router aware version.
522
- *
523
- * The react-router aware version is returned if `router` is a react-router-dom
524
- * router, `skipClientNav` is not `true`, and `href` is an internal URL.
525
- *
526
- * The `router` can be accessed via __RouterContext (imported from 'react-router')
527
- * from a component rendered as a descendant of a BrowserRouter.
528
- * See https://reacttraining.com/react-router/web/guides/basic-components.
529
- */
530
391
  const ClickableBehaviorWithRouter = withRouter(ClickableBehavior);
531
- function getClickableBehavior(
532
- /**
533
- * The URL to navigate to.
534
- */
535
- href,
536
- /**
537
- * Should we skip using the react router and go to the page directly.
538
- */
539
- skipClientNav,
540
- /**
541
- * router object added to the React context object by react-router-dom.
542
- */
543
- router) {
392
+ function getClickableBehavior(href, skipClientNav, router) {
544
393
  if (router && skipClientNav !== true && href && isClientSideUrl(href)) {
545
- // We cast to `any` here since the type of ClickableBehaviorWithRouter
546
- // is slightly different from the return type of this function.
547
- // TODO(WB-1037): Always return the wrapped version once all routes have
548
- // been ported to the app-shell in webapp.
549
394
  return ClickableBehaviorWithRouter;
550
395
  }
551
396
 
@@ -556,56 +401,30 @@ const _excluded = ["href", "onClick", "skipClientNav", "beforeNav", "safeWithNav
556
401
  const StyledAnchor = addStyle("a");
557
402
  const StyledButton = addStyle("button");
558
403
  const StyledLink = addStyle(Link);
559
- /**
560
- * A component to turn any custom component into a clickable one.
561
- *
562
- * Works by wrapping ClickableBehavior around the child element and styling the
563
- * child appropriately and encapsulates routing logic which can be customized.
564
- * Expects a function which returns an element as it's child.
565
- *
566
- * Example usage:
567
- * ```jsx
568
- * <Clickable onClick={() => alert("You clicked me!")}>
569
- * {({hovered, focused, pressed}) =>
570
- * <div
571
- * style={[
572
- * hovered && styles.hovered,
573
- * focused && styles.focused,
574
- * pressed && styles.pressed,
575
- * ]}
576
- * >
577
- * Click Me!
578
- * </div>
579
- * }
580
- * </Clickable>
581
- * ```
582
- */
583
-
584
404
  class Clickable extends React.Component {
585
405
  constructor(...args) {
586
406
  super(...args);
587
407
 
588
408
  this.getCorrectTag = (clickableState, router, commonProps) => {
589
409
  const activeHref = this.props.href && !this.props.disabled;
590
- const useClient = router && !this.props.skipClientNav && isClientSideUrl(this.props.href || ""); // NOTE: checking this.props.href here is redundant, but flow
591
- // needs it to refine this.props.href to a string.
410
+ const useClient = router && !this.props.skipClientNav && isClientSideUrl(this.props.href || "");
592
411
 
593
412
  if (activeHref && useClient && this.props.href) {
594
- return /*#__PURE__*/React.createElement(StyledLink, _extends({}, commonProps, {
413
+ return React.createElement(StyledLink, _extends({}, commonProps, {
595
414
  to: this.props.href,
596
415
  role: this.props.role,
597
416
  target: this.props.target || undefined,
598
417
  "aria-disabled": this.props.disabled ? "true" : undefined
599
418
  }), this.props.children(clickableState));
600
419
  } else if (activeHref && !useClient) {
601
- return /*#__PURE__*/React.createElement(StyledAnchor, _extends({}, commonProps, {
420
+ return React.createElement(StyledAnchor, _extends({}, commonProps, {
602
421
  href: this.props.href,
603
422
  role: this.props.role,
604
423
  target: this.props.target || undefined,
605
424
  "aria-disabled": this.props.disabled ? "true" : undefined
606
425
  }), this.props.children(clickableState));
607
426
  } else {
608
- return /*#__PURE__*/React.createElement(StyledButton, _extends({}, commonProps, {
427
+ return React.createElement(StyledButton, _extends({}, commonProps, {
609
428
  type: "button",
610
429
  "aria-disabled": this.props.disabled
611
430
  }), this.props.children(clickableState));
@@ -634,10 +453,10 @@ class Clickable extends React.Component {
634
453
 
635
454
  const ClickableBehavior = getClickableBehavior(href, skipClientNav, router);
636
455
 
637
- const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), style];
456
+ const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), disabled && styles.disabled, style];
638
457
 
639
458
  if (beforeNav) {
640
- return /*#__PURE__*/React.createElement(ClickableBehavior, {
459
+ return React.createElement(ClickableBehavior, {
641
460
  href: href,
642
461
  onClick: onClick,
643
462
  beforeNav: beforeNav,
@@ -650,7 +469,7 @@ class Clickable extends React.Component {
650
469
  style: getStyle(state)
651
470
  }, childrenProps)));
652
471
  } else {
653
- return /*#__PURE__*/React.createElement(ClickableBehavior, {
472
+ return React.createElement(ClickableBehavior, {
654
473
  href: href,
655
474
  onClick: onClick,
656
475
  safeWithNav: safeWithNav,
@@ -666,11 +485,10 @@ class Clickable extends React.Component {
666
485
  }
667
486
 
668
487
  render() {
669
- return /*#__PURE__*/React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
488
+ return React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
670
489
  }
671
490
 
672
- } // Source: https://gist.github.com/MoOx/9137295
673
-
491
+ }
674
492
  Clickable.defaultProps = {
675
493
  light: false,
676
494
  disabled: false,
@@ -685,23 +503,13 @@ const styles = StyleSheet.create({
685
503
  overflow: "visible",
686
504
  background: "transparent",
687
505
  textDecoration: "none",
688
-
689
- /* inherit font & color from ancestor */
690
506
  color: "inherit",
691
507
  font: "inherit",
692
508
  boxSizing: "border-box",
693
- // This removes the 300ms click delay on mobile browsers by indicating that
694
- // "double-tap to zoom" shouldn't be used on this element.
695
509
  touchAction: "manipulation",
696
510
  userSelect: "none",
697
- // This is usual frowned upon b/c of accessibility. We expect users of Clickable
698
- // to define their own focus styles.
699
511
  outline: "none",
700
-
701
- /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
702
512
  lineHeight: "normal",
703
-
704
- /* Corrects font smoothing for webkit */
705
513
  WebkitFontSmoothing: "inherit",
706
514
  MozOsxFontSmoothing: "inherit"
707
515
  },
@@ -709,10 +517,22 @@ const styles = StyleSheet.create({
709
517
  cursor: "pointer"
710
518
  },
711
519
  focused: {
712
- outline: `solid 2px ${Color.blue}`
520
+ ":focus": {
521
+ outline: `solid 2px ${Color.blue}`
522
+ }
713
523
  },
714
524
  focusedLight: {
715
525
  outline: `solid 2px ${Color.white}`
526
+ },
527
+ disabled: {
528
+ color: Color.offBlack32,
529
+ cursor: "not-allowed",
530
+ ":focus": {
531
+ outline: "none"
532
+ },
533
+ ":focus-visible": {
534
+ outline: `solid 2px ${Color.blue}`
535
+ }
716
536
  }
717
537
  });
718
538