@khanacademy/wonder-blocks-clickable 2.1.2 → 2.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-clickable",
3
- "version": "2.1.2",
3
+ "version": "2.2.2",
4
4
  "design": "v1",
5
5
  "description": "Clickable component for Wonder-Blocks.",
6
6
  "main": "dist/index.js",
@@ -15,18 +15,17 @@
15
15
  "access": "public"
16
16
  },
17
17
  "dependencies": {
18
- "@babel/runtime": "^7.13.10",
19
- "@khanacademy/wonder-blocks-core": "^3.1.4"
18
+ "@babel/runtime": "^7.16.3",
19
+ "@khanacademy/wonder-blocks-core": "^4.0.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "aphrodite": "^1.2.5",
23
- "prop-types": "^15.6.2",
24
- "react": "^16.4.1",
25
- "react-dom": "^16.4.1",
26
- "react-router-dom": "^4.2.2"
23
+ "react": "16.14.0",
24
+ "react-dom": "16.14.0",
25
+ "react-router": "5.2.1",
26
+ "react-router-dom": "5.3.0"
27
27
  },
28
28
  "devDependencies": {
29
- "wb-dev-build-settings": "^0.1.0"
30
- },
31
- "gitHead": "8022bb419eed74be37f71f71c7621854794a731c"
29
+ "wb-dev-build-settings": "^0.2.0"
30
+ }
32
31
  }
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable max-lines */
2
2
  // @flow
3
3
  import * as React from "react";
4
+ import {render, screen} from "@testing-library/react";
4
5
  import {MemoryRouter, Switch, Route} from "react-router-dom";
5
6
  import {mount, shallow} from "enzyme";
7
+ import "jest-enzyme";
6
8
 
7
9
  import getClickableBehavior from "../../util/get-clickable-behavior.js";
8
10
  import ClickableBehavior from "../clickable-behavior.js";
@@ -22,8 +24,9 @@ const wait = (delay: number = 0) =>
22
24
  describe("ClickableBehavior", () => {
23
25
  beforeEach(() => {
24
26
  // Note: window.location.assign and window.open need mock functions in
25
- // the testing environment.
26
- window.location.assign = jest.fn();
27
+ // the testing environment
28
+ delete window.location;
29
+ window.location = {assign: jest.fn()};
27
30
  window.open = jest.fn();
28
31
  });
29
32
 
@@ -274,6 +277,46 @@ describe("ClickableBehavior", () => {
274
277
  expect(button.state("focused")).toEqual(false);
275
278
  });
276
279
 
280
+ test("tabIndex should be 0", () => {
281
+ // Arrange
282
+ // Act
283
+ render(
284
+ <ClickableBehavior disabled={false} onClick={(e) => {}}>
285
+ {(state, childrenProps) => {
286
+ return (
287
+ <button data-test-id="test-button-1" {...childrenProps}>
288
+ Label
289
+ </button>
290
+ );
291
+ }}
292
+ </ClickableBehavior>,
293
+ );
294
+
295
+ // Assert
296
+ const button = screen.getByTestId("test-button-1");
297
+ expect(button).toHaveAttribute("tabIndex", "0");
298
+ });
299
+
300
+ test("tabIndex should be 0 even for disabled components", () => {
301
+ // Arrange
302
+ // Act
303
+ render(
304
+ <ClickableBehavior disabled={true} onClick={(e) => {}}>
305
+ {(state, childrenProps) => {
306
+ return (
307
+ <button data-test-id="test-button-2" {...childrenProps}>
308
+ Label
309
+ </button>
310
+ );
311
+ }}
312
+ </ClickableBehavior>,
313
+ );
314
+
315
+ // Assert
316
+ const button = screen.getByTestId("test-button-2");
317
+ expect(button).toHaveAttribute("tabIndex", "0");
318
+ });
319
+
277
320
  it("does not change state if disabled", () => {
278
321
  const onClick = jest.fn();
279
322
  const button = shallow(
@@ -566,6 +609,7 @@ describe("ClickableBehavior", () => {
566
609
  const button = wrapper.find("#test-button").first();
567
610
  button.simulate("click", {
568
611
  preventDefault() {
612
+ // $FlowIgnore[object-this-reference]
569
613
  this.defaultPrevented = true;
570
614
  },
571
615
  });
@@ -1052,6 +1096,7 @@ describe("ClickableBehavior", () => {
1052
1096
  const button = wrapper.find("#test-button").first();
1053
1097
  button.simulate("click", {
1054
1098
  preventDefault() {
1099
+ // $FlowIgnore[object-this-reference]
1055
1100
  this.defaultPrevented = true;
1056
1101
  },
1057
1102
  });
@@ -2,6 +2,7 @@
2
2
  import * as React from "react";
3
3
  import {MemoryRouter, Route, Switch} from "react-router-dom";
4
4
  import {mount} from "enzyme";
5
+ import "jest-enzyme";
5
6
 
6
7
  import {View} from "@khanacademy/wonder-blocks-core";
7
8
  import Clickable from "../clickable.js";
@@ -13,6 +14,11 @@ const wait = (delay: number = 0) =>
13
14
  });
14
15
 
15
16
  describe("Clickable", () => {
17
+ beforeEach(() => {
18
+ delete window.location;
19
+ window.location = {assign: jest.fn()};
20
+ });
21
+
16
22
  test("client-side navigation", () => {
17
23
  // Arrange
18
24
  const wrapper = mount(
@@ -129,8 +135,6 @@ describe("Clickable", () => {
129
135
 
130
136
  test("should navigate to a specific link using the keyboard", () => {
131
137
  // Arrange
132
- window.location.assign = jest.fn();
133
-
134
138
  const wrapper = mount(
135
139
  <Clickable testId="button" href="/foo" skipClientNav={true}>
136
140
  {(eventState) => <h1>Click Me!</h1>}
@@ -276,7 +280,6 @@ describe("Clickable", () => {
276
280
 
277
281
  test("safeWithNav with skipClientNav=true waits for promise resolution", async () => {
278
282
  // Arrange
279
- jest.spyOn(window.location, "assign");
280
283
  const wrapper = mount(
281
284
  <MemoryRouter>
282
285
  <div>
@@ -309,7 +312,6 @@ describe("Clickable", () => {
309
312
 
310
313
  test("beforeNav resolution and safeWithNav with skipClientNav=true waits for promise resolution", async () => {
311
314
  // Arrange
312
- jest.spyOn(window.location, "assign");
313
315
  const wrapper = mount(
314
316
  <MemoryRouter>
315
317
  <div>
@@ -336,8 +338,6 @@ describe("Clickable", () => {
336
338
  buttonWrapper.simulate("click", {button: 0});
337
339
  await wait(0);
338
340
  buttonWrapper.update();
339
- await wait(0);
340
- buttonWrapper.update();
341
341
 
342
342
  // Assert
343
343
  expect(window.location.assign).toHaveBeenCalledWith("/foo");
@@ -345,7 +345,6 @@ describe("Clickable", () => {
345
345
 
346
346
  test("safeWithNav with skipClientNav=true waits for promise rejection", async () => {
347
347
  // Arrange
348
- jest.spyOn(window.location, "assign");
349
348
  const wrapper = mount(
350
349
  <MemoryRouter>
351
350
  <div>
@@ -376,9 +375,8 @@ describe("Clickable", () => {
376
375
  expect(window.location.assign).toHaveBeenCalledWith("/foo");
377
376
  });
378
377
 
379
- test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate", async () => {
378
+ test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate", () => {
380
379
  // Arrange
381
- jest.spyOn(window.location, "assign");
382
380
  const safeWithNavMock = jest.fn();
383
381
  const wrapper = mount(
384
382
  <MemoryRouter>
@@ -406,12 +404,16 @@ describe("Clickable", () => {
406
404
 
407
405
  // Assert
408
406
  expect(safeWithNavMock).toHaveBeenCalled();
409
- expect(window.location.assign).toHaveBeenCalledWith("/foo");
407
+ expect(wrapper).toIncludeText(
408
+ "Hello, world!" /*client side nav to /foo*/,
409
+ );
410
+ expect(window.location.assign).not.toHaveBeenCalledWith(
411
+ "/foo" /*not a full page nav*/,
412
+ );
410
413
  });
411
414
 
412
415
  test("safeWithNav with beforeNav resolution and skipClientNav=false calls safeWithNav but doesn't wait to navigate", async () => {
413
416
  // Arrange
414
- jest.spyOn(window.location, "assign");
415
417
  const safeWithNavMock = jest.fn();
416
418
  const wrapper = mount(
417
419
  <MemoryRouter>
@@ -442,7 +444,12 @@ describe("Clickable", () => {
442
444
 
443
445
  // Assert
444
446
  expect(safeWithNavMock).toHaveBeenCalled();
445
- expect(window.location.assign).toHaveBeenCalledWith("/foo");
447
+ expect(wrapper).toIncludeText(
448
+ "Hello, world!" /*client side nav to /foo*/,
449
+ );
450
+ expect(window.location.assign).not.toHaveBeenCalledWith(
451
+ "/foo" /*not a full page nav*/,
452
+ );
446
453
  });
447
454
 
448
455
  describe("raw events", () => {
@@ -223,7 +223,9 @@ const disabledHandlers = {
223
223
  onKeyUp: () => void 0,
224
224
  onFocus: () => void 0,
225
225
  onBlur: () => void 0,
226
- tabIndex: -1,
226
+ // Clickable components should still be tabbable so they can
227
+ // be used as anchors.
228
+ tabIndex: 0,
227
229
  };
228
230
 
229
231
  const keyCodes = {
@@ -312,8 +314,8 @@ const startState: ClickableState = {
312
314
  * The react-router aware version is returned if `router` is a react-router-dom
313
315
  * router, `skipClientNav` is not `true`, and `href` is an internal URL.
314
316
  *
315
- * The `router` can be accessed via this.context.router from a component
316
- * rendered as a descendant of a BrowserRouter.
317
+ * The `router` can be accessed via __RouterContext (imported from 'react-router')
318
+ * from a component rendered as a descendant of a BrowserRouter.
317
319
  * See https://reacttraining.com/react-router/web/guides/basic-components.
318
320
  */
319
321
  export default class ClickableBehavior extends React.Component<
@@ -558,9 +560,8 @@ export default class ClickableBehavior extends React.Component<
558
560
  }
559
561
 
560
562
  const keyCode = e.which || e.keyCode;
561
- const {triggerOnEnter, triggerOnSpace} = getAppropriateTriggersForRole(
562
- role,
563
- );
563
+ const {triggerOnEnter, triggerOnSpace} =
564
+ getAppropriateTriggersForRole(role);
564
565
  if (
565
566
  (triggerOnEnter && keyCode === keyCodes.enter) ||
566
567
  (triggerOnSpace && keyCode === keyCodes.space)
@@ -585,9 +586,8 @@ export default class ClickableBehavior extends React.Component<
585
586
  }
586
587
 
587
588
  const keyCode = e.which || e.keyCode;
588
- const {triggerOnEnter, triggerOnSpace} = getAppropriateTriggersForRole(
589
- role,
590
- );
589
+ const {triggerOnEnter, triggerOnSpace} =
590
+ getAppropriateTriggersForRole(role);
591
591
  if (
592
592
  (triggerOnEnter && keyCode === keyCodes.enter) ||
593
593
  (triggerOnSpace && keyCode === keyCodes.space)
@@ -1,8 +1,8 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {StyleSheet} from "aphrodite";
4
- import * as PropTypes from "prop-types";
5
4
  import {Link} from "react-router-dom";
5
+ import {__RouterContext} from "react-router";
6
6
 
7
7
  import {addStyle} from "@khanacademy/wonder-blocks-core";
8
8
  import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
@@ -169,10 +169,6 @@ type Props =
169
169
  safeWithNav: () => Promise<mixed>,
170
170
  |};
171
171
 
172
- type ContextTypes = {|
173
- router: $FlowFixMe,
174
- |};
175
-
176
172
  type DefaultProps = {|
177
173
  light: $PropertyType<Props, "light">,
178
174
  disabled: $PropertyType<Props, "disabled">,
@@ -208,8 +204,6 @@ const StyledLink = addStyle<typeof Link>(Link);
208
204
  * ```
209
205
  */
210
206
  export default class Clickable extends React.Component<Props> {
211
- static contextTypes: ContextTypes = {router: PropTypes.any};
212
-
213
207
  static defaultProps: DefaultProps = {
214
208
  light: false,
215
209
  disabled: false,
@@ -218,11 +212,12 @@ export default class Clickable extends React.Component<Props> {
218
212
 
219
213
  getCorrectTag: (
220
214
  clickableState: ClickableState,
215
+ router: any,
221
216
  commonProps: {[string]: any, ...},
222
- ) => React.Node = (clickableState, commonProps) => {
217
+ ) => React.Node = (clickableState, router, commonProps) => {
223
218
  const activeHref = this.props.href && !this.props.disabled;
224
219
  const useClient =
225
- this.context.router &&
220
+ router &&
226
221
  !this.props.skipClientNav &&
227
222
  isClientSideUrl(this.props.href || "");
228
223
 
@@ -265,7 +260,7 @@ export default class Clickable extends React.Component<Props> {
265
260
  }
266
261
  };
267
262
 
268
- render(): React.Node {
263
+ renderClickableBehavior(router: any): React.Node {
269
264
  const {
270
265
  href,
271
266
  onClick,
@@ -285,7 +280,7 @@ export default class Clickable extends React.Component<Props> {
285
280
  const ClickableBehavior = getClickableBehavior(
286
281
  href,
287
282
  skipClientNav,
288
- this.context.router,
283
+ router,
289
284
  );
290
285
 
291
286
  const getStyle = (state: ClickableState): StyleType => [
@@ -309,7 +304,7 @@ export default class Clickable extends React.Component<Props> {
309
304
  disabled={disabled}
310
305
  >
311
306
  {(state, childrenProps) =>
312
- this.getCorrectTag(state, {
307
+ this.getCorrectTag(state, router, {
313
308
  ...restProps,
314
309
  "data-test-id": testId,
315
310
  style: getStyle(state),
@@ -330,7 +325,7 @@ export default class Clickable extends React.Component<Props> {
330
325
  disabled={disabled}
331
326
  >
332
327
  {(state, childrenProps) =>
333
- this.getCorrectTag(state, {
328
+ this.getCorrectTag(state, router, {
334
329
  ...restProps,
335
330
  "data-test-id": testId,
336
331
  style: getStyle(state),
@@ -341,6 +336,14 @@ export default class Clickable extends React.Component<Props> {
341
336
  );
342
337
  }
343
338
  }
339
+
340
+ render(): React.Node {
341
+ return (
342
+ <__RouterContext.Consumer>
343
+ {(router) => this.renderClickableBehavior(router)}
344
+ </__RouterContext.Consumer>
345
+ );
346
+ }
344
347
  }
345
348
 
346
349
  // Source: https://gist.github.com/MoOx/9137295
@@ -145,6 +145,13 @@ const styles = StyleSheet.create({
145
145
  </MemoryRouter>
146
146
  ```
147
147
 
148
+ ### Running callbacks on navigation
149
+
150
+ When using the `href` prop, the `onClick`, `beforeNav`, and `safeWithNav` props
151
+ can be used to run callbacks when navigating to the new URL. Which prop to use
152
+ depends on the use case. See the [Button](#section-button) documentation for
153
+ details.
154
+
148
155
  ### Navigating with the Keyboard
149
156
 
150
157
  Clickable adds support to keyboard navigation. This way, your components are
@@ -12,10 +12,10 @@ import {Body} from "@khanacademy/wonder-blocks-typography";
12
12
  import type {StoryComponentType} from "@storybook/react";
13
13
 
14
14
  export default {
15
- title: "Clickable",
15
+ title: "Navigation/Clickable",
16
16
  };
17
17
 
18
- export const basic: StoryComponentType = () => (
18
+ export const Basic: StoryComponentType = () => (
19
19
  <View>
20
20
  <View style={styles.centerText}>
21
21
  <Clickable
@@ -56,7 +56,7 @@ export const basic: StoryComponentType = () => (
56
56
  </View>
57
57
  );
58
58
 
59
- export const keyboardNavigation: StoryComponentType = () => (
59
+ export const KeyboardNavigation: StoryComponentType = () => (
60
60
  <View>
61
61
  <Clickable
62
62
  href="https://www.khanacademy.org/about/tos"
@@ -77,16 +77,14 @@ export const keyboardNavigation: StoryComponentType = () => (
77
77
  </View>
78
78
  );
79
79
 
80
- keyboardNavigation.story = {
81
- parameters: {
82
- chromatic: {
83
- // we don't need screenshots because this story only tests behavior.
84
- disable: true,
85
- },
80
+ KeyboardNavigation.parameters = {
81
+ chromatic: {
82
+ // we don't need screenshots because this story only tests behavior.
83
+ disableSnapshot: true,
86
84
  },
87
85
  };
88
86
 
89
- export const keyboardNavigationTab: StoryComponentType = () => (
87
+ export const KeyboardNavigationTab: StoryComponentType = () => (
90
88
  <View>
91
89
  <Clickable role="tab" aria-controls="panel-1" id="tab-1">
92
90
  {({hovered, focused, pressed}) => (
@@ -5,8 +5,8 @@
5
5
  * The react-router aware version is returned if `router` is a react-router-dom
6
6
  * router, `skipClientNav` is not `true`, and `href` is an internal URL.
7
7
  *
8
- * The `router` can be accessed via this.context.router from a component rendered
9
- * as a descendant of a BrowserRouter.
8
+ * The `router` can be accessed via __RouterContext (imported from 'react-router')
9
+ * from a component rendered as a descendant of a BrowserRouter.
10
10
  * See https://reacttraining.com/react-router/web/guides/basic-components.
11
11
  */
12
12
  import * as React from "react";
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2018 Khan Academy
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.