@khanacademy/wonder-blocks-clickable 2.1.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/LICENSE +21 -0
- package/dist/es/index.js +712 -0
- package/dist/index.js +1056 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +7 -0
- package/package.json +32 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +426 -0
- package/src/__tests__/generated-snapshot.test.js +176 -0
- package/src/components/__tests__/clickable-behavior.test.js +1313 -0
- package/src/components/__tests__/clickable.test.js +500 -0
- package/src/components/clickable-behavior.js +646 -0
- package/src/components/clickable.js +388 -0
- package/src/components/clickable.md +196 -0
- package/src/components/clickable.stories.js +129 -0
- package/src/index.js +15 -0
- package/src/util/__tests__/get-clickable-behavior.test.js +105 -0
- package/src/util/__tests__/is-client-side-url.js.test.js +50 -0
- package/src/util/get-clickable-behavior.js +43 -0
- package/src/util/is-client-side-url.js +16 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {
|
|
3
|
+
ChildrenProps,
|
|
4
|
+
ClickableState,
|
|
5
|
+
ClickableRole,
|
|
6
|
+
} from "./components/clickable-behavior.js";
|
|
7
|
+
import Clickable from "./components/clickable.js";
|
|
8
|
+
|
|
9
|
+
export {default as ClickableBehavior} from "./components/clickable-behavior.js";
|
|
10
|
+
export {default as getClickableBehavior} from "./util/get-clickable-behavior.js";
|
|
11
|
+
export {isClientSideUrl} from "./util/is-client-side-url.js";
|
|
12
|
+
|
|
13
|
+
export {Clickable as default};
|
|
14
|
+
|
|
15
|
+
export type {ChildrenProps, ClickableState, ClickableRole};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {MemoryRouter} from "react-router-dom";
|
|
4
|
+
import ClickableBehavior from "../../components/clickable-behavior.js";
|
|
5
|
+
import getClickableBehavior from "../get-clickable-behavior.js";
|
|
6
|
+
|
|
7
|
+
describe("getClickableBehavior", () => {
|
|
8
|
+
test("Without href, returns ClickableBehavior", () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const url = undefined;
|
|
11
|
+
const skipClientNav = undefined;
|
|
12
|
+
const router = undefined;
|
|
13
|
+
const expectation = ClickableBehavior;
|
|
14
|
+
|
|
15
|
+
// Act
|
|
16
|
+
const result = getClickableBehavior(url, skipClientNav, router);
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(result).toBe(expectation);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("with href", () => {
|
|
23
|
+
test("External URL, returns ClickableBehavior", () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const url = "http://google.com";
|
|
26
|
+
const skipClientNav = undefined;
|
|
27
|
+
const router = <MemoryRouter />;
|
|
28
|
+
const expectation = ClickableBehavior;
|
|
29
|
+
|
|
30
|
+
// Act
|
|
31
|
+
const result = getClickableBehavior(url, skipClientNav, router);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(result).toBe(expectation);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Internal URL", () => {
|
|
38
|
+
describe("skipClientNav is undefined", () => {
|
|
39
|
+
test("No router, returns ClickableBehavior", () => {
|
|
40
|
+
// Arrange
|
|
41
|
+
const url = "/prep/lsat";
|
|
42
|
+
const skipClientNav = undefined;
|
|
43
|
+
const router = undefined;
|
|
44
|
+
const expectation = ClickableBehavior;
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const result = getClickableBehavior(
|
|
48
|
+
url,
|
|
49
|
+
skipClientNav,
|
|
50
|
+
router,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Assert
|
|
54
|
+
expect(result).toBe(expectation);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Router, returns ClickableBehaviorWithRouter", () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const url = "/prep/lsat";
|
|
60
|
+
const skipClientNav = undefined;
|
|
61
|
+
const router = <MemoryRouter />;
|
|
62
|
+
const expectation = "withRouter(ClickableBehavior)";
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const result = getClickableBehavior(
|
|
66
|
+
url,
|
|
67
|
+
skipClientNav,
|
|
68
|
+
router,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(result.displayName).toBe(expectation);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("skipClientNav is false, returns ClickableBehaviorWithRouter", () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const url = "/prep/lsat";
|
|
79
|
+
const skipClientNav = false;
|
|
80
|
+
const router = <MemoryRouter />;
|
|
81
|
+
const expectation = "withRouter(ClickableBehavior)";
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
const result = getClickableBehavior(url, skipClientNav, router);
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
expect(result.displayName).toBe(expectation);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("skipClientNav is true, returns ClickableBehavior", () => {
|
|
91
|
+
// Arrange
|
|
92
|
+
const url = "/prep/lsat";
|
|
93
|
+
const skipClientNav = true;
|
|
94
|
+
const router = <MemoryRouter />;
|
|
95
|
+
const expectation = ClickableBehavior;
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
const result = getClickableBehavior(url, skipClientNav, router);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect(result).toBe(expectation);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {isClientSideUrl} from "../is-client-side-url.js";
|
|
3
|
+
|
|
4
|
+
describe("isClientSideUrl", () => {
|
|
5
|
+
test("returns boolean based on the url", () => {
|
|
6
|
+
// external URLs
|
|
7
|
+
expect(
|
|
8
|
+
isClientSideUrl(
|
|
9
|
+
"//khanacademy.zendesk.com/hc/en-us/articles/236355907",
|
|
10
|
+
),
|
|
11
|
+
).toEqual(false);
|
|
12
|
+
expect(
|
|
13
|
+
isClientSideUrl(
|
|
14
|
+
"https://khanacademy.zendesk.com/hc/en-us/articles/360007253831",
|
|
15
|
+
),
|
|
16
|
+
).toEqual(false);
|
|
17
|
+
expect(isClientSideUrl("http://external.com")).toEqual(false);
|
|
18
|
+
expect(isClientSideUrl("//www.google.com")).toEqual(false);
|
|
19
|
+
expect(isClientSideUrl("//")).toEqual(false);
|
|
20
|
+
|
|
21
|
+
// non-http(s) URLs
|
|
22
|
+
expect(isClientSideUrl("javascript:void(0);")).toEqual(false);
|
|
23
|
+
expect(isClientSideUrl("javascript:void(0)")).toEqual(false);
|
|
24
|
+
expect(isClientSideUrl("mailto:foo@example.com")).toEqual(false);
|
|
25
|
+
expect(isClientSideUrl("tel:+1234567890")).toEqual(false);
|
|
26
|
+
expect(isClientSideUrl("tel:+1234567890")).toEqual(false);
|
|
27
|
+
expect(isClientSideUrl("ms-help://kb12345.htm")).toEqual(false);
|
|
28
|
+
expect(isClientSideUrl("z39.50s://0.0.0.0")).toEqual(false);
|
|
29
|
+
|
|
30
|
+
// HREFs with anchors
|
|
31
|
+
expect(isClientSideUrl("#")).toEqual(false);
|
|
32
|
+
expect(isClientSideUrl("#foo")).toEqual(false);
|
|
33
|
+
expect(isClientSideUrl("/foo#bar")).toEqual(false);
|
|
34
|
+
expect(isClientSideUrl("foo/bar#baz")).toEqual(false);
|
|
35
|
+
|
|
36
|
+
// internal URLs
|
|
37
|
+
expect(isClientSideUrl("/foo//bar")).toEqual(true);
|
|
38
|
+
expect(isClientSideUrl("/coach/dashboard")).toEqual(true);
|
|
39
|
+
expect(isClientSideUrl("/math/early-math/modal/e/addition_1")).toEqual(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("invalid values for 'href' should return false", () => {
|
|
45
|
+
// $FlowIgnore: testing invalid input
|
|
46
|
+
expect(isClientSideUrl(null)).toEqual(false);
|
|
47
|
+
// $FlowIgnore: testing invalid input
|
|
48
|
+
expect(isClientSideUrl(undefined)).toEqual(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Returns either the default ClickableBehavior or a react-router aware version.
|
|
4
|
+
*
|
|
5
|
+
* The react-router aware version is returned if `router` is a react-router-dom
|
|
6
|
+
* router, `skipClientNav` is not `true`, and `href` is an internal URL.
|
|
7
|
+
*
|
|
8
|
+
* The `router` can be accessed via this.context.router from a component rendered
|
|
9
|
+
* as a descendant of a BrowserRouter.
|
|
10
|
+
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
11
|
+
*/
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import {withRouter} from "react-router-dom";
|
|
14
|
+
|
|
15
|
+
import ClickableBehavior from "../components/clickable-behavior.js";
|
|
16
|
+
import {isClientSideUrl} from "./is-client-side-url.js";
|
|
17
|
+
|
|
18
|
+
const ClickableBehaviorWithRouter = withRouter(ClickableBehavior);
|
|
19
|
+
|
|
20
|
+
export default function getClickableBehavior(
|
|
21
|
+
/**
|
|
22
|
+
* The URL to navigate to.
|
|
23
|
+
*/
|
|
24
|
+
href?: string,
|
|
25
|
+
/**
|
|
26
|
+
* Should we skip using the react router and go to the page directly.
|
|
27
|
+
*/
|
|
28
|
+
skipClientNav?: boolean,
|
|
29
|
+
/**
|
|
30
|
+
* router object added to the React context object by react-router-dom.
|
|
31
|
+
*/
|
|
32
|
+
router?: any,
|
|
33
|
+
): React.ComponentType<React.ElementConfig<typeof ClickableBehavior>> {
|
|
34
|
+
if (router && skipClientNav !== true && href && isClientSideUrl(href)) {
|
|
35
|
+
// We cast to `any` here since the type of ClickableBehaviorWithRouter
|
|
36
|
+
// is slightly different from the return type of this function.
|
|
37
|
+
// TODO(WB-1037): Always return the wrapped version once all routes have
|
|
38
|
+
// been ported to the app-shell in webapp.
|
|
39
|
+
return (ClickableBehaviorWithRouter: any);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ClickableBehavior;
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Returns:
|
|
4
|
+
* - false for hrefs staring with http://, https://, //.
|
|
5
|
+
* - false for '#', 'javascript:...', 'mailto:...', 'tel:...', etc.
|
|
6
|
+
* - true for all other values, e.g. /foo/bar
|
|
7
|
+
*/
|
|
8
|
+
export const isClientSideUrl = (href: string): boolean => {
|
|
9
|
+
if (typeof href !== "string") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
!/^(https?:)?\/\//i.test(href) &&
|
|
14
|
+
!/^([^#]*#[\w-]*|[\w\-.]+:)/.test(href)
|
|
15
|
+
);
|
|
16
|
+
};
|