@khanacademy/wonder-blocks-clickable 3.0.13 → 3.1.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/dist/index.js CHANGED
@@ -391,41 +391,40 @@ function getClickableBehavior(href, skipClientNav, router) {
391
391
  return ClickableBehavior;
392
392
  }
393
393
 
394
- const _excluded = ["href", "onClick", "skipClientNav", "beforeNav", "safeWithNav", "style", "target", "testId", "onKeyDown", "onKeyUp", "hideDefaultFocusRing", "light", "disabled"];
394
+ const _excluded = ["href", "onClick", "skipClientNav", "beforeNav", "safeWithNav", "style", "target", "testId", "onKeyDown", "onKeyUp", "hideDefaultFocusRing", "light", "disabled", "tabIndex"];
395
395
  const StyledAnchor = wonderBlocksCore.addStyle("a");
396
396
  const StyledButton = wonderBlocksCore.addStyle("button");
397
397
  const StyledLink = wonderBlocksCore.addStyle(reactRouterDom.Link);
398
- class Clickable extends React__namespace.Component {
399
- constructor(...args) {
400
- super(...args);
401
- this.getCorrectTag = (clickableState, router, commonProps) => {
402
- const activeHref = this.props.href && !this.props.disabled;
403
- const useClient = router && !this.props.skipClientNav && isClientSideUrl(this.props.href || "");
404
- if (activeHref && useClient && this.props.href) {
405
- return React__namespace.createElement(StyledLink, _extends({}, commonProps, {
406
- to: this.props.href,
407
- role: this.props.role,
408
- target: this.props.target || undefined,
409
- "aria-disabled": this.props.disabled ? "true" : undefined
410
- }), this.props.children(clickableState));
411
- } else if (activeHref && !useClient) {
412
- return React__namespace.createElement(StyledAnchor, _extends({}, commonProps, {
413
- href: this.props.href,
414
- role: this.props.role,
415
- target: this.props.target || undefined,
416
- "aria-disabled": this.props.disabled ? "true" : undefined
417
- }), this.props.children(clickableState));
418
- } else {
419
- return React__namespace.createElement(StyledButton, _extends({}, commonProps, {
420
- type: "button",
421
- "aria-disabled": this.props.disabled
422
- }), this.props.children(clickableState));
423
- }
424
- };
425
- }
426
- renderClickableBehavior(router) {
427
- const _this$props = this.props,
428
- {
398
+ const Clickable = React__namespace.forwardRef(function Clickable(props, ref) {
399
+ const getCorrectTag = (clickableState, router, commonProps) => {
400
+ const activeHref = props.href && !props.disabled;
401
+ const useClient = router && !props.skipClientNav && isClientSideUrl(props.href || "");
402
+ if (activeHref && useClient && props.href) {
403
+ return React__namespace.createElement(StyledLink, _extends({}, commonProps, {
404
+ to: props.href,
405
+ role: props.role,
406
+ target: props.target || undefined,
407
+ "aria-disabled": props.disabled ? "true" : undefined,
408
+ ref: ref
409
+ }), props.children(clickableState));
410
+ } else if (activeHref && !useClient) {
411
+ return React__namespace.createElement(StyledAnchor, _extends({}, commonProps, {
412
+ href: props.href,
413
+ role: props.role,
414
+ target: props.target || undefined,
415
+ "aria-disabled": props.disabled ? "true" : undefined,
416
+ ref: ref
417
+ }), props.children(clickableState));
418
+ } else {
419
+ return React__namespace.createElement(StyledButton, _extends({}, commonProps, {
420
+ type: "button",
421
+ "aria-disabled": props.disabled,
422
+ ref: ref
423
+ }), props.children(clickableState));
424
+ }
425
+ };
426
+ const renderClickableBehavior = router => {
427
+ const {
429
428
  href,
430
429
  onClick,
431
430
  skipClientNav,
@@ -438,9 +437,10 @@ class Clickable extends React__namespace.Component {
438
437
  onKeyUp,
439
438
  hideDefaultFocusRing,
440
439
  light,
441
- disabled
442
- } = _this$props,
443
- restProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
440
+ disabled,
441
+ tabIndex
442
+ } = props,
443
+ restProps = _objectWithoutPropertiesLoose(props, _excluded);
444
444
  const ClickableBehavior = getClickableBehavior(href, skipClientNav, router);
445
445
  const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), disabled && styles.disabled, style];
446
446
  if (beforeNav) {
@@ -451,8 +451,9 @@ class Clickable extends React__namespace.Component {
451
451
  safeWithNav: safeWithNav,
452
452
  onKeyDown: onKeyDown,
453
453
  onKeyUp: onKeyUp,
454
- disabled: disabled
455
- }, (state, childrenProps) => this.getCorrectTag(state, router, _extends({}, restProps, {
454
+ disabled: disabled,
455
+ tabIndex: tabIndex
456
+ }, (state, childrenProps) => getCorrectTag(state, router, _extends({}, restProps, {
456
457
  "data-test-id": testId,
457
458
  style: getStyle(state)
458
459
  }, childrenProps)));
@@ -464,17 +465,16 @@ class Clickable extends React__namespace.Component {
464
465
  onKeyDown: onKeyDown,
465
466
  onKeyUp: onKeyUp,
466
467
  target: target,
467
- disabled: disabled
468
- }, (state, childrenProps) => this.getCorrectTag(state, router, _extends({}, restProps, {
468
+ disabled: disabled,
469
+ tabIndex: tabIndex
470
+ }, (state, childrenProps) => getCorrectTag(state, router, _extends({}, restProps, {
469
471
  "data-test-id": testId,
470
472
  style: getStyle(state)
471
473
  }, childrenProps)));
472
474
  }
473
- }
474
- render() {
475
- return React__namespace.createElement(reactRouter.__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
476
- }
477
- }
475
+ };
476
+ return React__namespace.createElement(reactRouter.__RouterContext.Consumer, null, router => renderClickableBehavior(router));
477
+ });
478
478
  Clickable.defaultProps = {
479
479
  light: false,
480
480
  disabled: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-clickable",
3
- "version": "3.0.13",
3
+ "version": "3.1.0",
4
4
  "design": "v1",
5
5
  "description": "Clickable component for Wonder-Blocks.",
6
6
  "main": "dist/index.js",
@@ -506,6 +506,84 @@ describe("Clickable", () => {
506
506
  expect(button).toHaveFocus();
507
507
  });
508
508
 
509
+ test("should not have a tabIndex if one is not set", () => {
510
+ // Arrange
511
+
512
+ // Act
513
+ render(
514
+ <Clickable testId="clickable-button">
515
+ {(eventState: any) => <h1>Click Me!</h1>}
516
+ </Clickable>,
517
+ );
518
+
519
+ const button = screen.getByTestId("clickable-button");
520
+
521
+ // Assert
522
+ expect(button).not.toHaveAttribute("tabIndex");
523
+ });
524
+
525
+ test("should have the tabIndex that is passed in", () => {
526
+ // Arrange
527
+
528
+ // Act
529
+ render(
530
+ <Clickable testId="clickable-button" tabIndex={1}>
531
+ {(eventState: any) => <h1>Click Me!</h1>}
532
+ </Clickable>,
533
+ );
534
+
535
+ const button = screen.getByTestId("clickable-button");
536
+
537
+ // Assert
538
+ expect(button).toHaveAttribute("tabIndex", "1");
539
+ });
540
+
541
+ test("should have the tabIndex that is passed in", () => {
542
+ // Arrange
543
+
544
+ // Act
545
+ render(
546
+ <Clickable testId="clickable-button" tabIndex={1}>
547
+ {(eventState: any) => <h1>Click Me!</h1>}
548
+ </Clickable>,
549
+ );
550
+
551
+ const button = screen.getByTestId("clickable-button");
552
+
553
+ // Assert
554
+ expect(button).toHaveAttribute("tabIndex", "1");
555
+ });
556
+
557
+ test("forwards the ref to the clickable button element", () => {
558
+ // Arrange
559
+ const ref: React.RefObject<HTMLButtonElement> = React.createRef();
560
+
561
+ // Act
562
+ render(
563
+ <Clickable testId="clickable-button" ref={ref}>
564
+ {(eventState: any) => <h1>Click Me!</h1>}
565
+ </Clickable>,
566
+ );
567
+
568
+ // Assert
569
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
570
+ });
571
+
572
+ test("forwards the ref to the clickable anchor element ", () => {
573
+ // Arrange
574
+ const ref: React.RefObject<HTMLAnchorElement> = React.createRef();
575
+
576
+ // Act
577
+ render(
578
+ <Clickable href="/test-url" testId="clickable-anchor" ref={ref}>
579
+ {(eventState: any) => <h1>Click Me!</h1>}
580
+ </Clickable>,
581
+ );
582
+
583
+ // Assert
584
+ expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
585
+ });
586
+
509
587
  describe("raw events", () => {
510
588
  /**
511
589
  * Clickable expect a function as children so we create a simple wrapper to
@@ -47,11 +47,11 @@ type Props =
47
47
  * Sets the default focus ring color to white, instead of blue.
48
48
  * Defaults to false.
49
49
  */
50
- light: boolean;
50
+ light?: boolean;
51
51
  /**
52
52
  * Disables or enables the child; defaults to false
53
53
  */
54
- disabled: boolean;
54
+ disabled?: boolean;
55
55
  /**
56
56
  * An optional id attribute.
57
57
  */
@@ -94,6 +94,10 @@ type Props =
94
94
  * TODO(WB-1262): only allow this prop when `href` is also set.t
95
95
  */
96
96
  target?: "_blank";
97
+ /**
98
+ * Set the tabindex attribute on the rendered element.
99
+ */
100
+ tabIndex?: number;
97
101
  /**
98
102
  * Run async code before navigating. If the promise returned rejects then
99
103
  * navigation will not occur.
@@ -114,11 +118,6 @@ type Props =
114
118
  safeWithNav?: () => Promise<unknown>;
115
119
  };
116
120
 
117
- type DefaultProps = {
118
- light: Props["light"];
119
- disabled: Props["disabled"];
120
- };
121
-
122
121
  const StyledAnchor = addStyle("a");
123
122
  const StyledButton = addStyle("button");
124
123
  const StyledLink = addStyle(Link);
@@ -156,49 +155,50 @@ const StyledLink = addStyle(Link);
156
155
  * </Clickable>
157
156
  * ```
158
157
  */
159
- export default class Clickable extends React.Component<Props> {
160
- static defaultProps: DefaultProps = {
161
- light: false,
162
- disabled: false,
163
- };
164
158
 
165
- getCorrectTag: (
159
+ const Clickable = React.forwardRef(function Clickable(
160
+ props: Props,
161
+ ref: React.ForwardedRef<
162
+ typeof Link | HTMLAnchorElement | HTMLButtonElement
163
+ >,
164
+ ) {
165
+ const getCorrectTag: (
166
166
  clickableState: ClickableState,
167
167
  router: any,
168
168
  commonProps: {
169
169
  [key: string]: any;
170
170
  },
171
171
  ) => React.ReactElement = (clickableState, router, commonProps) => {
172
- const activeHref = this.props.href && !this.props.disabled;
172
+ const activeHref = props.href && !props.disabled;
173
173
  const useClient =
174
- router &&
175
- !this.props.skipClientNav &&
176
- isClientSideUrl(this.props.href || "");
174
+ router && !props.skipClientNav && isClientSideUrl(props.href || "");
177
175
 
178
176
  // NOTE: checking this.props.href here is redundant, but TypeScript
179
177
  // needs it to refine this.props.href to a string.
180
- if (activeHref && useClient && this.props.href) {
178
+ if (activeHref && useClient && props.href) {
181
179
  return (
182
180
  <StyledLink
183
181
  {...commonProps}
184
- to={this.props.href}
185
- role={this.props.role}
186
- target={this.props.target || undefined}
187
- aria-disabled={this.props.disabled ? "true" : undefined}
182
+ to={props.href}
183
+ role={props.role}
184
+ target={props.target || undefined}
185
+ aria-disabled={props.disabled ? "true" : undefined}
186
+ ref={ref as React.Ref<typeof Link>}
188
187
  >
189
- {this.props.children(clickableState)}
188
+ {props.children(clickableState)}
190
189
  </StyledLink>
191
190
  );
192
191
  } else if (activeHref && !useClient) {
193
192
  return (
194
193
  <StyledAnchor
195
194
  {...commonProps}
196
- href={this.props.href}
197
- role={this.props.role}
198
- target={this.props.target || undefined}
199
- aria-disabled={this.props.disabled ? "true" : undefined}
195
+ href={props.href}
196
+ role={props.role}
197
+ target={props.target || undefined}
198
+ aria-disabled={props.disabled ? "true" : undefined}
199
+ ref={ref as React.Ref<HTMLAnchorElement>}
200
200
  >
201
- {this.props.children(clickableState)}
201
+ {props.children(clickableState)}
202
202
  </StyledAnchor>
203
203
  );
204
204
  } else {
@@ -206,15 +206,18 @@ export default class Clickable extends React.Component<Props> {
206
206
  <StyledButton
207
207
  {...commonProps}
208
208
  type="button"
209
- aria-disabled={this.props.disabled}
209
+ aria-disabled={props.disabled}
210
+ ref={ref as React.Ref<HTMLButtonElement>}
210
211
  >
211
- {this.props.children(clickableState)}
212
+ {props.children(clickableState)}
212
213
  </StyledButton>
213
214
  );
214
215
  }
215
216
  };
216
217
 
217
- renderClickableBehavior(router: any): React.ReactNode {
218
+ const renderClickableBehavior: (router: any) => React.ReactNode = (
219
+ router: any,
220
+ ) => {
218
221
  const {
219
222
  href,
220
223
  onClick,
@@ -229,8 +232,9 @@ export default class Clickable extends React.Component<Props> {
229
232
  hideDefaultFocusRing,
230
233
  light,
231
234
  disabled,
235
+ tabIndex,
232
236
  ...restProps
233
- } = this.props;
237
+ } = props;
234
238
  const ClickableBehavior = getClickableBehavior(
235
239
  href,
236
240
  skipClientNav,
@@ -257,9 +261,10 @@ export default class Clickable extends React.Component<Props> {
257
261
  onKeyDown={onKeyDown}
258
262
  onKeyUp={onKeyUp}
259
263
  disabled={disabled}
264
+ tabIndex={tabIndex}
260
265
  >
261
266
  {(state, childrenProps) =>
262
- this.getCorrectTag(state, router, {
267
+ getCorrectTag(state, router, {
263
268
  ...restProps,
264
269
  "data-test-id": testId,
265
270
  style: getStyle(state),
@@ -278,9 +283,10 @@ export default class Clickable extends React.Component<Props> {
278
283
  onKeyUp={onKeyUp}
279
284
  target={target}
280
285
  disabled={disabled}
286
+ tabIndex={tabIndex}
281
287
  >
282
288
  {(state, childrenProps) =>
283
- this.getCorrectTag(state, router, {
289
+ getCorrectTag(state, router, {
284
290
  ...restProps,
285
291
  "data-test-id": testId,
286
292
  style: getStyle(state),
@@ -290,16 +296,21 @@ export default class Clickable extends React.Component<Props> {
290
296
  </ClickableBehavior>
291
297
  );
292
298
  }
293
- }
299
+ };
294
300
 
295
- render(): React.ReactNode {
296
- return (
297
- <__RouterContext.Consumer>
298
- {(router) => this.renderClickableBehavior(router)}
299
- </__RouterContext.Consumer>
300
- );
301
- }
302
- }
301
+ return (
302
+ <__RouterContext.Consumer>
303
+ {(router) => renderClickableBehavior(router)}
304
+ </__RouterContext.Consumer>
305
+ );
306
+ });
307
+
308
+ Clickable.defaultProps = {
309
+ light: false,
310
+ disabled: false,
311
+ };
312
+
313
+ export default Clickable;
303
314
 
304
315
  // Source: https://gist.github.com/MoOx/9137295
305
316
  const styles = StyleSheet.create({