@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/CHANGELOG.md +6 -0
- package/dist/components/clickable.d.ts +55 -68
- package/dist/components/clickable.js.flow +60 -67
- package/dist/es/index.js +44 -44
- package/dist/index.js +44 -44
- package/package.json +1 -1
- package/src/components/__tests__/clickable.test.tsx +78 -0
- package/src/components/clickable.tsx +54 -43
- package/tsconfig-build.tsbuildinfo +1 -1
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
renderClickableBehavior
|
|
427
|
-
const
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
@@ -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
|
|
50
|
+
light?: boolean;
|
|
51
51
|
/**
|
|
52
52
|
* Disables or enables the child; defaults to false
|
|
53
53
|
*/
|
|
54
|
-
disabled
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
178
|
+
if (activeHref && useClient && props.href) {
|
|
181
179
|
return (
|
|
182
180
|
<StyledLink
|
|
183
181
|
{...commonProps}
|
|
184
|
-
to={
|
|
185
|
-
role={
|
|
186
|
-
target={
|
|
187
|
-
aria-disabled={
|
|
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
|
-
{
|
|
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={
|
|
197
|
-
role={
|
|
198
|
-
target={
|
|
199
|
-
aria-disabled={
|
|
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
|
-
{
|
|
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={
|
|
209
|
+
aria-disabled={props.disabled}
|
|
210
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
210
211
|
>
|
|
211
|
-
{
|
|
212
|
+
{props.children(clickableState)}
|
|
212
213
|
</StyledButton>
|
|
213
214
|
);
|
|
214
215
|
}
|
|
215
216
|
};
|
|
216
217
|
|
|
217
|
-
renderClickableBehavior(router: any)
|
|
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
|
-
} =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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({
|