@khanacademy/wonder-blocks-clickable 4.2.7 → 4.2.8

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.
@@ -1,429 +0,0 @@
1
- import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
- import {Link} from "react-router-dom";
4
- import {__RouterContext} from "react-router";
5
-
6
- import {addStyle} from "@khanacademy/wonder-blocks-core";
7
- import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
8
- import {color} from "@khanacademy/wonder-blocks-tokens";
9
-
10
- import getClickableBehavior from "../util/get-clickable-behavior";
11
- import type {ClickableRole, ClickableState} from "./clickable-behavior";
12
- import {isClientSideUrl} from "../util/is-client-side-url";
13
-
14
- type CommonProps =
15
- /**
16
- * aria-label should be used when `spinner={true}` to let people using screen
17
- * readers that the action taken by clicking the button will take some
18
- * time to complete.
19
- */
20
- Partial<Omit<AriaProps, "aria-disabled">> & {
21
- /**
22
- * The child of Clickable must be a function which returns the component
23
- * which should be made Clickable. The function is passed an object with
24
- * three boolean properties: hovered, focused, and pressed.
25
- */
26
- children: (clickableState: ClickableState) => React.ReactNode;
27
- /**
28
- * An onClick function which Clickable can execute when clicked
29
- */
30
- onClick?: (e: React.SyntheticEvent) => unknown;
31
- /**
32
- * An onFocus function which Clickable can execute when focused
33
- */
34
- onFocus?: (e: React.FocusEvent) => unknown;
35
- /**
36
- * Optional href which Clickable should direct to, uses client-side routing
37
- * by default if react-router is present
38
- */
39
- href?: string;
40
- /**
41
- * Styles to apply to the Clickable component
42
- */
43
- style?: StyleType;
44
- /**
45
- * Adds CSS classes to the Clickable.
46
- */
47
- className?: string;
48
- /**
49
- * Whether the Clickable is on a dark colored background.
50
- * Sets the default focus ring color to white, instead of blue.
51
- * Defaults to false.
52
- */
53
- light?: boolean;
54
- /**
55
- * Disables or enables the child; defaults to false
56
- */
57
- disabled?: boolean;
58
- /**
59
- * An optional id attribute.
60
- */
61
- id?: string;
62
- /**
63
- * Specifies the type of relationship between the current document and the
64
- * linked document. Should only be used when `href` is specified. This
65
- * defaults to "noopener noreferrer" when `target="_blank"`, but can be
66
- * overridden by setting this prop to something else.
67
- */
68
- rel?: string;
69
- /**
70
- * The role of the component, can be a role of type ClickableRole
71
- */
72
- role?: ClickableRole;
73
- /**
74
- * Avoids client-side routing in the presence of the href prop
75
- */
76
- skipClientNav?: boolean;
77
- /**
78
- * Test ID used for e2e testing.
79
- */
80
- testId?: string;
81
- /**
82
- * Respond to raw "keydown" event.
83
- */
84
- onKeyDown?: (e: React.KeyboardEvent) => unknown;
85
- /**
86
- * Respond to raw "keyup" event.
87
- */
88
- onKeyUp?: (e: React.KeyboardEvent) => unknown;
89
- /**
90
- * Respond to raw "mousedown" event.
91
- */
92
- onMouseDown?: (e: React.MouseEvent) => unknown;
93
- /**
94
- * Respond to raw "mouseup" event.
95
- */
96
- onMouseUp?: (e: React.MouseEvent) => unknown;
97
- /**
98
- * Don't show the default focus ring. This should be used when implementing
99
- * a custom focus ring within your own component that uses Clickable.
100
- */
101
- hideDefaultFocusRing?: boolean;
102
- /**
103
- * Set the tabindex attribute on the rendered element.
104
- */
105
- tabIndex?: number;
106
- /**
107
- * An optional title attribute.
108
- */
109
- title?: string;
110
- /**
111
- * Run async code before navigating. If the promise returned rejects then
112
- * navigation will not occur.
113
- *
114
- * If both safeWithNav and beforeNav are provided, beforeNav will be run
115
- * first and safeWithNav will only be run if beforeNav does not reject.
116
- *
117
- * WARNING: This prop must be used with `href` and should not be used with
118
- * `target="blank"`.
119
- */
120
- beforeNav?: () => Promise<unknown>;
121
- /**
122
- * Run async code in the background while client-side navigating. If the
123
- * browser does a full page load navigation, the callback promise must be
124
- * settled before the navigation will occur. Errors are ignored so that
125
- * navigation is guaranteed to succeed.
126
- */
127
- safeWithNav?: () => Promise<unknown>;
128
- };
129
-
130
- type Props =
131
- | (CommonProps & {
132
- href: string;
133
-
134
- /**
135
- * Run async code in the background while client-side navigating. If the
136
- * browser does a full page load navigation, the callback promise must be
137
- * settled before the navigation will occur. Errors are ignored so that
138
- * navigation is guaranteed to succeed.
139
- */
140
- safeWithNav?: () => Promise<unknown>;
141
-
142
- /**
143
- * A target destination window for a link to open in.
144
- */
145
- target?: "_blank";
146
-
147
- beforeNav?: never;
148
- })
149
- | (CommonProps & {
150
- href?: string;
151
-
152
- /**
153
- * Run async code before navigating. If the promise returned rejects then
154
- * navigation will not occur.
155
- *
156
- * If both safeWithNav and beforeNav are provided, beforeNav will be run
157
- * first and safeWithNav will only be run if beforeNav does not reject.
158
- */
159
- beforeNav?: () => Promise<unknown>;
160
-
161
- /**
162
- * Run async code in the background while client-side navigating. If the
163
- * browser does a full page load navigation, the callback promise must be
164
- * settled before the navigation will occur. Errors are ignored so that
165
- * navigation is guaranteed to succeed.
166
- */
167
- safeWithNav?: () => Promise<unknown>;
168
-
169
- target?: never;
170
- });
171
-
172
- const StyledAnchor = addStyle("a");
173
- const StyledButton = addStyle("button");
174
- const StyledLink = addStyle(Link);
175
-
176
- /**
177
- * A component to turn any custom component into a clickable one.
178
- *
179
- * Works by wrapping `ClickableBehavior` around the child element and styling
180
- * the child appropriately and encapsulates routing logic which can be
181
- * customized. Expects a function which returns an element as its child.
182
- *
183
- * Clickable allows your components to:
184
- *
185
- * - Handle mouse / touch / keyboard events
186
- * - Match the standard behavior of the given role
187
- * - Apply custom styles based on pressed / focused / hovered state
188
- * - Perform Client Side Navigation when href is passed and the component is a
189
- * descendent of a react-router Router.
190
- *
191
- * ### Usage
192
- *
193
- * ```jsx
194
- * <Clickable onClick={() => alert("You clicked me!")}>
195
- * {({hovered, focused, pressed}) =>
196
- * <div
197
- * style={[
198
- * hovered && styles.hovered,
199
- * focused && styles.focused,
200
- * pressed && styles.pressed,
201
- * ]}
202
- * >
203
- * Click Me!
204
- * </div>
205
- * }
206
- * </Clickable>
207
- * ```
208
- */
209
-
210
- const Clickable = React.forwardRef(function Clickable(
211
- props: Props,
212
- ref: React.ForwardedRef<
213
- typeof Link | HTMLAnchorElement | HTMLButtonElement
214
- >,
215
- ) {
216
- const getCorrectTag: (
217
- clickableState: ClickableState,
218
- router: any,
219
- commonProps: {
220
- [key: string]: any;
221
- },
222
- ) => React.ReactElement = (clickableState, router, commonProps) => {
223
- const activeHref = props.href && !props.disabled;
224
- const useClient =
225
- router && !props.skipClientNav && isClientSideUrl(props.href || "");
226
-
227
- // NOTE: checking this.props.href here is redundant, but TypeScript
228
- // needs it to refine this.props.href to a string.
229
- if (activeHref && useClient && props.href) {
230
- return (
231
- <StyledLink
232
- {...commonProps}
233
- to={props.href}
234
- role={props.role}
235
- target={props.target || undefined}
236
- aria-disabled={props.disabled ? "true" : "false"}
237
- ref={ref as React.Ref<typeof Link>}
238
- >
239
- {props.children(clickableState)}
240
- </StyledLink>
241
- );
242
- } else if (activeHref && !useClient) {
243
- return (
244
- <StyledAnchor
245
- {...commonProps}
246
- href={props.href}
247
- role={props.role}
248
- target={props.target || undefined}
249
- aria-disabled={props.disabled ? "true" : "false"}
250
- ref={ref as React.Ref<HTMLAnchorElement>}
251
- >
252
- {props.children(clickableState)}
253
- </StyledAnchor>
254
- );
255
- } else {
256
- return (
257
- <StyledButton
258
- {...commonProps}
259
- type="button"
260
- aria-disabled={props.disabled}
261
- ref={ref as React.Ref<HTMLButtonElement>}
262
- >
263
- {props.children(clickableState)}
264
- </StyledButton>
265
- );
266
- }
267
- };
268
-
269
- const renderClickableBehavior: (router: any) => React.ReactNode = (
270
- router: any,
271
- ) => {
272
- const {
273
- href,
274
- onClick,
275
- skipClientNav,
276
- beforeNav = undefined,
277
- safeWithNav = undefined,
278
- style,
279
- target = undefined,
280
- testId,
281
- onFocus,
282
- onKeyDown,
283
- onKeyUp,
284
- onMouseDown,
285
- onMouseUp,
286
- hideDefaultFocusRing,
287
- light,
288
- disabled,
289
- tabIndex,
290
- ...restProps
291
- } = props;
292
- const ClickableBehavior = getClickableBehavior(
293
- href,
294
- skipClientNav,
295
- router,
296
- );
297
-
298
- const getStyle = (state: ClickableState): StyleType => [
299
- styles.reset,
300
- styles.link,
301
- !hideDefaultFocusRing &&
302
- state.focused &&
303
- (light ? styles.focusedLight : styles.focused),
304
- disabled && styles.disabled,
305
- style,
306
- ];
307
-
308
- if (beforeNav) {
309
- return (
310
- <ClickableBehavior
311
- href={href}
312
- onClick={onClick}
313
- beforeNav={beforeNav}
314
- safeWithNav={safeWithNav}
315
- onFocus={onFocus}
316
- onKeyDown={onKeyDown}
317
- onKeyUp={onKeyUp}
318
- onMouseDown={onMouseDown}
319
- onMouseUp={onMouseUp}
320
- disabled={disabled}
321
- tabIndex={tabIndex}
322
- >
323
- {(state, childrenProps) =>
324
- getCorrectTag(state, router, {
325
- ...restProps,
326
- "data-testid": testId,
327
- style: getStyle(state),
328
- ...childrenProps,
329
- })
330
- }
331
- </ClickableBehavior>
332
- );
333
- } else {
334
- return (
335
- <ClickableBehavior
336
- href={href}
337
- onClick={onClick}
338
- safeWithNav={safeWithNav}
339
- onFocus={onFocus}
340
- onKeyDown={onKeyDown}
341
- onKeyUp={onKeyUp}
342
- onMouseDown={onMouseDown}
343
- onMouseUp={onMouseUp}
344
- target={target}
345
- disabled={disabled}
346
- tabIndex={tabIndex}
347
- >
348
- {(state, childrenProps) =>
349
- getCorrectTag(state, router, {
350
- ...restProps,
351
- "data-testid": testId,
352
- style: getStyle(state),
353
- ...childrenProps,
354
- })
355
- }
356
- </ClickableBehavior>
357
- );
358
- }
359
- };
360
-
361
- return (
362
- <__RouterContext.Consumer>
363
- {(router) => renderClickableBehavior(router)}
364
- </__RouterContext.Consumer>
365
- );
366
- });
367
-
368
- Clickable.defaultProps = {
369
- light: false,
370
- disabled: false,
371
- };
372
-
373
- export default Clickable;
374
-
375
- // Source: https://gist.github.com/MoOx/9137295
376
- const styles = StyleSheet.create({
377
- reset: {
378
- border: "none",
379
- margin: 0,
380
- padding: 0,
381
- width: "auto",
382
- overflow: "visible",
383
-
384
- background: "transparent",
385
- textDecoration: "none",
386
-
387
- /* inherit font & color from ancestor */
388
- color: "inherit",
389
- font: "inherit",
390
-
391
- boxSizing: "border-box",
392
- // This removes the 300ms click delay on mobile browsers by indicating that
393
- // "double-tap to zoom" shouldn't be used on this element.
394
- touchAction: "manipulation",
395
- userSelect: "none",
396
-
397
- // This is usual frowned upon b/c of accessibility. We expect users of Clickable
398
- // to define their own focus styles.
399
- outline: "none",
400
-
401
- /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
402
- lineHeight: "normal",
403
-
404
- /* Corrects font smoothing for webkit */
405
- WebkitFontSmoothing: "inherit",
406
- MozOsxFontSmoothing: "inherit",
407
- },
408
- link: {
409
- cursor: "pointer",
410
- },
411
- focused: {
412
- ":focus": {
413
- outline: `solid 2px ${color.blue}`,
414
- },
415
- },
416
- focusedLight: {
417
- outline: `solid 2px ${color.white}`,
418
- },
419
- disabled: {
420
- color: color.offBlack32,
421
- cursor: "not-allowed",
422
- ":focus": {
423
- outline: "none",
424
- },
425
- ":focus-visible": {
426
- outline: `solid 2px ${color.blue}`,
427
- },
428
- },
429
- });
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- import type {
2
- ChildrenProps,
3
- ClickableState,
4
- ClickableRole,
5
- } from "./components/clickable-behavior";
6
- import Clickable from "./components/clickable";
7
-
8
- export {default as ClickableBehavior} from "./components/clickable-behavior";
9
- export {default as getClickableBehavior} from "./util/get-clickable-behavior";
10
- export {isClientSideUrl} from "./util/is-client-side-url";
11
-
12
- export {Clickable as default};
13
-
14
- export type {ChildrenProps, ClickableState, ClickableRole};
@@ -1,104 +0,0 @@
1
- import * as React from "react";
2
- import {MemoryRouter} from "react-router-dom";
3
- import ClickableBehavior from "../../components/clickable-behavior";
4
- import getClickableBehavior from "../get-clickable-behavior";
5
-
6
- describe("getClickableBehavior", () => {
7
- test("Without href, returns ClickableBehavior", () => {
8
- // Arrange
9
- const url = undefined;
10
- const skipClientNav = undefined;
11
- const router = undefined;
12
- const expectation = ClickableBehavior;
13
-
14
- // Act
15
- const result = getClickableBehavior(url, skipClientNav, router);
16
-
17
- // Assert
18
- expect(result).toBe(expectation);
19
- });
20
-
21
- describe("with href", () => {
22
- test("External URL, returns ClickableBehavior", () => {
23
- // Arrange
24
- const url = "http://google.com";
25
- const skipClientNav = undefined;
26
- const router = <MemoryRouter />;
27
- const expectation = ClickableBehavior;
28
-
29
- // Act
30
- const result = getClickableBehavior(url, skipClientNav, router);
31
-
32
- // Assert
33
- expect(result).toBe(expectation);
34
- });
35
-
36
- describe("Internal URL", () => {
37
- describe("skipClientNav is undefined", () => {
38
- test("No router, returns ClickableBehavior", () => {
39
- // Arrange
40
- const url = "/prep/lsat";
41
- const skipClientNav = undefined;
42
- const router = undefined;
43
- const expectation = ClickableBehavior;
44
-
45
- // Act
46
- const result = getClickableBehavior(
47
- url,
48
- skipClientNav,
49
- router,
50
- );
51
-
52
- // Assert
53
- expect(result).toBe(expectation);
54
- });
55
-
56
- test("Router, returns ClickableBehaviorWithRouter", () => {
57
- // Arrange
58
- const url = "/prep/lsat";
59
- const skipClientNav = undefined;
60
- const router = <MemoryRouter />;
61
- const expectation = "withRouter(ClickableBehavior)";
62
-
63
- // Act
64
- const result = getClickableBehavior(
65
- url,
66
- skipClientNav,
67
- router,
68
- );
69
-
70
- // Assert
71
- expect(result.displayName).toBe(expectation);
72
- });
73
- });
74
-
75
- test("skipClientNav is false, returns ClickableBehaviorWithRouter", () => {
76
- // Arrange
77
- const url = "/prep/lsat";
78
- const skipClientNav = false;
79
- const router = <MemoryRouter />;
80
- const expectation = "withRouter(ClickableBehavior)";
81
-
82
- // Act
83
- const result = getClickableBehavior(url, skipClientNav, router);
84
-
85
- // Assert
86
- expect(result.displayName).toBe(expectation);
87
- });
88
-
89
- test("skipClientNav is true, returns ClickableBehavior", () => {
90
- // Arrange
91
- const url = "/prep/lsat";
92
- const skipClientNav = true;
93
- const router = <MemoryRouter />;
94
- const expectation = ClickableBehavior;
95
-
96
- // Act
97
- const result = getClickableBehavior(url, skipClientNav, router);
98
-
99
- // Assert
100
- expect(result).toBe(expectation);
101
- });
102
- });
103
- });
104
- });
@@ -1,49 +0,0 @@
1
- import {isClientSideUrl} from "../is-client-side-url";
2
-
3
- describe("isClientSideUrl", () => {
4
- test("returns boolean based on the url", () => {
5
- // external URLs
6
- expect(
7
- isClientSideUrl(
8
- "//khanacademy.zendesk.com/hc/en-us/articles/236355907",
9
- ),
10
- ).toEqual(false);
11
- expect(
12
- isClientSideUrl(
13
- "https://khanacademy.zendesk.com/hc/en-us/articles/360007253831",
14
- ),
15
- ).toEqual(false);
16
- expect(isClientSideUrl("http://external.com")).toEqual(false);
17
- expect(isClientSideUrl("//www.google.com")).toEqual(false);
18
- expect(isClientSideUrl("//")).toEqual(false);
19
-
20
- // non-http(s) URLs
21
- expect(isClientSideUrl("javascript:void(0);")).toEqual(false);
22
- expect(isClientSideUrl("javascript:void(0)")).toEqual(false);
23
- expect(isClientSideUrl("mailto:foo@example.com")).toEqual(false);
24
- expect(isClientSideUrl("tel:+1234567890")).toEqual(false);
25
- expect(isClientSideUrl("tel:+1234567890")).toEqual(false);
26
- expect(isClientSideUrl("ms-help://kb12345.htm")).toEqual(false);
27
- expect(isClientSideUrl("z39.50s://0.0.0.0")).toEqual(false);
28
-
29
- // HREFs with anchors
30
- expect(isClientSideUrl("#")).toEqual(false);
31
- expect(isClientSideUrl("#foo")).toEqual(false);
32
- expect(isClientSideUrl("/foo#bar")).toEqual(false);
33
- expect(isClientSideUrl("foo/bar#baz")).toEqual(false);
34
-
35
- // internal URLs
36
- expect(isClientSideUrl("/foo//bar")).toEqual(true);
37
- expect(isClientSideUrl("/coach/dashboard")).toEqual(true);
38
- expect(isClientSideUrl("/math/early-math/modal/e/addition_1")).toEqual(
39
- true,
40
- );
41
- });
42
-
43
- test("invalid values for 'href' should return false", () => {
44
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'null' is not assignable to parameter of type 'string'.
45
- expect(isClientSideUrl(null)).toEqual(false);
46
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'undefined' is not assignable to parameter of type 'string'.
47
- expect(isClientSideUrl(undefined)).toEqual(false);
48
- });
49
- });
@@ -1,46 +0,0 @@
1
- /**
2
- * Returns either the default ClickableBehavior or a react-router aware version.
3
- *
4
- * The react-router aware version is returned if `router` is a react-router-dom
5
- * router, `skipClientNav` is not `true`, and `href` is an internal URL.
6
- *
7
- * The `router` can be accessed via __RouterContext (imported from 'react-router')
8
- * from a component rendered as a descendant of a BrowserRouter.
9
- * See https://reacttraining.com/react-router/web/guides/basic-components.
10
- */
11
- import * as React from "react";
12
- import {withRouter} from "react-router-dom";
13
-
14
- import {PropsFor} from "@khanacademy/wonder-blocks-core";
15
-
16
- import ClickableBehavior from "../components/clickable-behavior";
17
- import {isClientSideUrl} from "./is-client-side-url";
18
-
19
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'typeof ClickableBehavior' is not assignable to parameter of type 'ComponentType<RouteComponentProps<any, StaticContext, unknown>>'.
20
- const ClickableBehaviorWithRouter = withRouter(ClickableBehavior);
21
-
22
- export default function getClickableBehavior(
23
- /**
24
- * The URL to navigate to.
25
- */
26
- href?: string,
27
- /**
28
- * Should we skip using the react router and go to the page directly.
29
- */
30
- skipClientNav?: boolean,
31
- /**
32
- * router object added to the React context object by react-router-dom.
33
- */
34
- router?: any,
35
- ): React.ComponentType<PropsFor<typeof ClickableBehavior>> {
36
- if (router && skipClientNav !== true && href && isClientSideUrl(href)) {
37
- // We cast to `any` here since the type of ClickableBehaviorWithRouter
38
- // is slightly different from the return type of this function.
39
- // TODO(WB-1037): Always return the wrapped version once all routes have
40
- // been ported to the app-shell in webapp.
41
- return ClickableBehaviorWithRouter as any;
42
- }
43
-
44
- // @ts-expect-error [FEI-5019] - TS2322 - Type 'typeof ClickableBehavior' is not assignable to type 'ComponentType<Pick<Props, "children" | "href" | "tabIndex" | "role" | "target" | "type" | "rel" | "onKeyDown" | "onKeyUp" | "onClick" | "history" | "skipClientNav" | "safeWithNav" | "beforeNav"> & InexactPartial<...> & InexactPartial<...>>'.
45
- return ClickableBehavior;
46
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Returns:
3
- * - false for hrefs staring with http://, https://, //.
4
- * - false for '#', 'javascript:...', 'mailto:...', 'tel:...', etc.
5
- * - true for all other values, e.g. /foo/bar
6
- */
7
- export const isClientSideUrl = (href: string): boolean => {
8
- if (typeof href !== "string") {
9
- return false;
10
- }
11
- return (
12
- !/^(https?:)?\/\//i.test(href) &&
13
- !/^([^#]*#[\w-]*|[\w\-.]+:)/.test(href)
14
- );
15
- };
@@ -1,12 +0,0 @@
1
- {
2
- "exclude": ["dist"],
3
- "extends": "../tsconfig-shared.json",
4
- "compilerOptions": {
5
- "outDir": "./dist",
6
- "rootDir": "src",
7
- },
8
- "references": [
9
- {"path": "../wonder-blocks-core/tsconfig-build.json"},
10
- {"path": "../wonder-blocks-tokens/tsconfig-build.json"},
11
- ]
12
- }