@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.
- package/CHANGELOG.md +9 -0
- package/package.json +3 -3
- package/src/components/__tests__/clickable-behavior.test.tsx +0 -1438
- package/src/components/__tests__/clickable-behavior.typestest.tsx +0 -20
- package/src/components/__tests__/clickable.test.tsx +0 -661
- package/src/components/__tests__/clickable.typestest.tsx +0 -64
- package/src/components/clickable-behavior.ts +0 -659
- package/src/components/clickable.tsx +0 -429
- package/src/index.ts +0 -14
- package/src/util/__tests__/get-clickable-behavior.test.tsx +0 -104
- package/src/util/__tests__/is-client-side-url.js.test.ts +0 -49
- package/src/util/get-clickable-behavior.ts +0 -46
- package/src/util/is-client-side-url.ts +0 -15
- package/tsconfig-build.json +0 -12
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -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
|
-
};
|
package/tsconfig-build.json
DELETED
|
@@ -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
|
-
}
|