@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.
@@ -0,0 +1,712 @@
1
+ import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
2
+ import _extends from '@babel/runtime/helpers/extends';
3
+ import { Component, createElement } from 'react';
4
+ import { StyleSheet } from 'aphrodite';
5
+ import { any } from 'prop-types';
6
+ import { withRouter, Link } from 'react-router-dom';
7
+ import { addStyle } from '@khanacademy/wonder-blocks-core';
8
+ import Color from '@khanacademy/wonder-blocks-color';
9
+
10
+ const getAppropriateTriggersForRole = role => {
11
+ switch (role) {
12
+ // Triggers on ENTER, but not SPACE
13
+ case "link":
14
+ return {
15
+ triggerOnEnter: true,
16
+ triggerOnSpace: false
17
+ };
18
+ // Triggers on SPACE, but not ENTER
19
+
20
+ case "checkbox":
21
+ case "radio":
22
+ case "listbox":
23
+ case "option":
24
+ return {
25
+ triggerOnEnter: false,
26
+ triggerOnSpace: true
27
+ };
28
+ // Triggers on both ENTER and SPACE
29
+
30
+ case "button":
31
+ case "menuitem":
32
+ case "menu":
33
+ default:
34
+ return {
35
+ triggerOnEnter: true,
36
+ triggerOnSpace: true
37
+ };
38
+ }
39
+ };
40
+
41
+ const disabledHandlers = {
42
+ onClick: () => void 0,
43
+ onMouseEnter: () => void 0,
44
+ onMouseLeave: () => void 0,
45
+ onMouseDown: () => void 0,
46
+ onMouseUp: () => void 0,
47
+ onDragStart: () => void 0,
48
+ onTouchStart: () => void 0,
49
+ onTouchEnd: () => void 0,
50
+ onTouchCancel: () => void 0,
51
+ onKeyDown: () => void 0,
52
+ onKeyUp: () => void 0,
53
+ onFocus: () => void 0,
54
+ onBlur: () => void 0,
55
+ tabIndex: -1
56
+ };
57
+ const keyCodes = {
58
+ enter: 13,
59
+ space: 32
60
+ };
61
+ const startState = {
62
+ hovered: false,
63
+ focused: false,
64
+ pressed: false,
65
+ waiting: false
66
+ };
67
+ /**
68
+ * Add hover, focus, and active status updates to a clickable component.
69
+ *
70
+ * Via mouse:
71
+ *
72
+ * 1. Hover over button -> hover state
73
+ * 2. Mouse down -> active state
74
+ * 3. Mouse up -> default state
75
+ * 4. Press tab -> focus state
76
+ *
77
+ * Via touch:
78
+ *
79
+ * 1. Touch down -> press state
80
+ * 2. Touch up -> default state
81
+ *
82
+ * Via keyboard:
83
+ *
84
+ * 1. Tab to focus -> focus state
85
+ * 2. Keydown (spacebar/enter) -> active state
86
+ * 3. Keyup (spacebar/enter) -> focus state
87
+ *
88
+ * Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
89
+ * onMouseDown, onMouseUp, onDragStart, onTouchStart, onTouchEnd, onTouchCancel, onKeyDown,
90
+ * onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the component
91
+ * that has the ClickableBehavior. You cannot override these handlers without
92
+ * potentially breaking the functionality of ClickableBehavior.
93
+ *
94
+ * There are internal props triggerOnEnter and triggerOnSpace that can be set
95
+ * to false if one of those keys shouldn't count as a click on this component.
96
+ * Be careful about setting those to false -- make certain that the component
97
+ * shouldn't process that key.
98
+ *
99
+ * See [this document](https://docs.google.com/document/d/1DG5Rg2f0cawIL5R8UqnPQpd7pbdObk8OyjO5ryYQmBM/edit#)
100
+ * for a more thorough explanation of expected behaviors and potential cavaets.
101
+ *
102
+ * `ClickableBehavior` accepts a function as `children` which is passed state
103
+ * and an object containing event handlers and some other props. The `children`
104
+ * function should return a clickable React Element of some sort.
105
+ *
106
+ * Example:
107
+ *
108
+ * ```js
109
+ * class MyClickableComponent extends React.Component<Props> {
110
+ * render(): React.Node {
111
+ * const ClickableBehavior = getClickableBehavior();
112
+ * return <ClickableBehavior
113
+ * disabled={this.props.disabled}
114
+ * onClick={this.props.onClick}
115
+ * >
116
+ * {({hovered}, childrenProps) =>
117
+ * <RoundRect
118
+ * textcolor='white'
119
+ * backgroundColor={hovered ? 'red' : 'blue'}}
120
+ * {...childrenProps}
121
+ * >
122
+ * {this.props.children}
123
+ * </RoundRect>
124
+ * }
125
+ * </ClickableBehavior>
126
+ * }
127
+ * }
128
+ * ```
129
+ *
130
+ * This follows a pattern called [Function as Child Components]
131
+ * (https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9).
132
+ *
133
+ * WARNING: Do not use this component directly, use getClickableBehavior
134
+ * instead. getClickableBehavior takes three arguments (href, directtNav, and
135
+ * router) and returns either the default ClickableBehavior or a react-router
136
+ * aware version.
137
+ *
138
+ * The react-router aware version is returned if `router` is a react-router-dom
139
+ * router, `skipClientNav` is not `true`, and `href` is an internal URL.
140
+ *
141
+ * The `router` can be accessed via this.context.router from a component
142
+ * rendered as a descendant of a BrowserRouter.
143
+ * See https://reacttraining.com/react-router/web/guides/basic-components.
144
+ */
145
+
146
+ class ClickableBehavior extends Component {
147
+ static getDerivedStateFromProps(props, state) {
148
+ // If new props are disabled, reset the hovered/focused/pressed states
149
+ if (props.disabled) {
150
+ return startState;
151
+ } else {
152
+ // Cannot return undefined
153
+ return null;
154
+ }
155
+ }
156
+
157
+ constructor(props) {
158
+ super(props);
159
+
160
+ this.handleClick = e => {
161
+ const {
162
+ onClick = undefined,
163
+ beforeNav = undefined,
164
+ safeWithNav = undefined
165
+ } = this.props;
166
+
167
+ if (this.enterClick) {
168
+ return;
169
+ }
170
+
171
+ if (onClick || beforeNav || safeWithNav) {
172
+ this.waitingForClick = false;
173
+ }
174
+
175
+ this.runCallbackAndMaybeNavigate(e);
176
+ };
177
+
178
+ this.handleMouseEnter = e => {
179
+ // When the left button is pressed already, we want it to be pressed
180
+ if (e.buttons === 1) {
181
+ this.dragging = true;
182
+ this.setState({
183
+ pressed: true
184
+ });
185
+ } else if (!this.waitingForClick) {
186
+ this.setState({
187
+ hovered: true
188
+ });
189
+ }
190
+ };
191
+
192
+ this.handleMouseLeave = () => {
193
+ if (!this.waitingForClick) {
194
+ this.dragging = false;
195
+ this.setState({
196
+ hovered: false,
197
+ pressed: false,
198
+ focused: false
199
+ });
200
+ }
201
+ };
202
+
203
+ this.handleMouseDown = () => {
204
+ this.setState({
205
+ pressed: true
206
+ });
207
+ };
208
+
209
+ this.handleMouseUp = e => {
210
+ if (this.dragging) {
211
+ this.dragging = false;
212
+ this.handleClick(e);
213
+ }
214
+
215
+ this.setState({
216
+ pressed: false,
217
+ focused: false
218
+ });
219
+ };
220
+
221
+ this.handleDragStart = e => {
222
+ this.dragging = true;
223
+ e.preventDefault();
224
+ };
225
+
226
+ this.handleTouchStart = () => {
227
+ this.setState({
228
+ pressed: true
229
+ });
230
+ };
231
+
232
+ this.handleTouchEnd = () => {
233
+ this.setState({
234
+ pressed: false
235
+ });
236
+ this.waitingForClick = true;
237
+ };
238
+
239
+ this.handleTouchCancel = () => {
240
+ this.setState({
241
+ pressed: false
242
+ });
243
+ this.waitingForClick = true;
244
+ };
245
+
246
+ this.handleKeyDown = e => {
247
+ const {
248
+ onKeyDown,
249
+ role
250
+ } = this.props;
251
+
252
+ if (onKeyDown) {
253
+ onKeyDown(e);
254
+ }
255
+
256
+ const keyCode = e.which || e.keyCode;
257
+ const {
258
+ triggerOnEnter,
259
+ triggerOnSpace
260
+ } = getAppropriateTriggersForRole(role);
261
+
262
+ if (triggerOnEnter && keyCode === keyCodes.enter || triggerOnSpace && keyCode === keyCodes.space) {
263
+ // This prevents space from scrolling down. It also prevents the
264
+ // space and enter keys from triggering click events. We manually
265
+ // call the supplied onClick and handle potential navigation in
266
+ // handleKeyUp instead.
267
+ e.preventDefault();
268
+ this.setState({
269
+ pressed: true
270
+ });
271
+ } else if (!triggerOnEnter && keyCode === keyCodes.enter) {
272
+ // If the component isn't supposed to trigger on enter, we have to
273
+ // keep track of the enter keydown to negate the onClick callback
274
+ this.enterClick = true;
275
+ }
276
+ };
277
+
278
+ this.handleKeyUp = e => {
279
+ const {
280
+ onKeyUp,
281
+ role
282
+ } = this.props;
283
+
284
+ if (onKeyUp) {
285
+ onKeyUp(e);
286
+ }
287
+
288
+ const keyCode = e.which || e.keyCode;
289
+ const {
290
+ triggerOnEnter,
291
+ triggerOnSpace
292
+ } = getAppropriateTriggersForRole(role);
293
+
294
+ if (triggerOnEnter && keyCode === keyCodes.enter || triggerOnSpace && keyCode === keyCodes.space) {
295
+ this.setState({
296
+ pressed: false,
297
+ focused: true
298
+ });
299
+ this.runCallbackAndMaybeNavigate(e);
300
+ } else if (!triggerOnEnter && keyCode === keyCodes.enter) {
301
+ this.enterClick = false;
302
+ }
303
+ };
304
+
305
+ this.handleFocus = e => {
306
+ this.setState({
307
+ focused: true
308
+ });
309
+ };
310
+
311
+ this.handleBlur = e => {
312
+ this.setState({
313
+ focused: false,
314
+ pressed: false
315
+ });
316
+ };
317
+
318
+ this.state = startState;
319
+ this.waitingForClick = false;
320
+ this.enterClick = false;
321
+ this.dragging = false;
322
+ }
323
+
324
+ navigateOrReset(shouldNavigate) {
325
+ if (shouldNavigate) {
326
+ const {
327
+ history,
328
+ href,
329
+ skipClientNav,
330
+ target = undefined
331
+ } = this.props;
332
+
333
+ if (href) {
334
+ if (target === "_blank") {
335
+ window.open(href, "_blank");
336
+ this.setState({
337
+ waiting: false
338
+ });
339
+ } else if (history && !skipClientNav) {
340
+ history.push(href);
341
+ this.setState({
342
+ waiting: false
343
+ });
344
+ } else {
345
+ window.location.assign(href); // We don't bother clearing the waiting state, the full page
346
+ // load navigation will do that for us by loading a new page.
347
+ }
348
+ }
349
+ } else {
350
+ this.setState({
351
+ waiting: false
352
+ });
353
+ }
354
+ }
355
+
356
+ handleSafeWithNav(safeWithNav, shouldNavigate) {
357
+ const {
358
+ skipClientNav,
359
+ history
360
+ } = this.props;
361
+
362
+ if (history && !skipClientNav || this.props.target === "_blank") {
363
+ // client-side nav
364
+ safeWithNav();
365
+ this.navigateOrReset(shouldNavigate);
366
+ return Promise.resolve();
367
+ } else {
368
+ if (!this.state.waiting) {
369
+ // We only show the spinner for safeWithNav when doing
370
+ // a full page load navigation since since the spinner is
371
+ // indicating that we're waiting for navigation to occur.
372
+ this.setState({
373
+ waiting: true
374
+ });
375
+ }
376
+
377
+ return safeWithNav().then(() => {
378
+ if (!this.state.waiting) {
379
+ // We only show the spinner for safeWithNav when doing
380
+ // a full page load navigation since since the spinner is
381
+ // indicating that we're waiting for navigation to occur.
382
+ this.setState({
383
+ waiting: true
384
+ });
385
+ }
386
+
387
+ return;
388
+ }).catch(error => {// We ignore the error here so that we always
389
+ // navigate when using safeWithNav regardless of
390
+ // whether we're doing a client-side nav or not.
391
+ }).finally(() => {
392
+ this.navigateOrReset(shouldNavigate);
393
+ });
394
+ }
395
+ }
396
+
397
+ runCallbackAndMaybeNavigate(e) {
398
+ const {
399
+ onClick = undefined,
400
+ beforeNav = undefined,
401
+ safeWithNav = undefined,
402
+ href,
403
+ type
404
+ } = this.props;
405
+ let shouldNavigate = true;
406
+ let canSubmit = true;
407
+
408
+ if (onClick) {
409
+ onClick(e);
410
+ } // If onClick() has called e.preventDefault() then we shouldn't
411
+ // navigate.
412
+
413
+
414
+ if (e.defaultPrevented) {
415
+ shouldNavigate = false;
416
+ canSubmit = false;
417
+ }
418
+
419
+ e.preventDefault();
420
+
421
+ if (!href && type === "submit" && canSubmit) {
422
+ let target = e.currentTarget;
423
+
424
+ while (target) {
425
+ if (target instanceof window.HTMLFormElement) {
426
+ // This event must be marked as cancelable otherwise calling
427
+ // e.preventDefault() on it won't do anything in Firefox.
428
+ // Chrome and Safari allow calling e.preventDefault() on
429
+ // non-cancelable events, but really they shouldn't.
430
+ const event = new window.Event("submit", {
431
+ cancelable: true
432
+ });
433
+ target.dispatchEvent(event);
434
+ break;
435
+ } // All events should be typed as SyntheticEvent<HTMLElement>.
436
+ // Updating all of the places will take some time so I'll do
437
+ // this later
438
+ // $FlowFixMe[prop-missing]
439
+
440
+
441
+ target = target.parentElement;
442
+ }
443
+ }
444
+
445
+ if (beforeNav) {
446
+ this.setState({
447
+ waiting: true
448
+ });
449
+ beforeNav().then(() => {
450
+ if (safeWithNav) {
451
+ return this.handleSafeWithNav(safeWithNav, shouldNavigate);
452
+ } else {
453
+ return this.navigateOrReset(shouldNavigate);
454
+ }
455
+ }).catch(() => {});
456
+ } else if (safeWithNav) {
457
+ return this.handleSafeWithNav(safeWithNav, shouldNavigate);
458
+ } else {
459
+ this.navigateOrReset(shouldNavigate);
460
+ }
461
+ }
462
+
463
+ render() {
464
+ const childrenProps = this.props.disabled ? disabledHandlers : {
465
+ onClick: this.handleClick,
466
+ onMouseEnter: this.handleMouseEnter,
467
+ onMouseLeave: this.handleMouseLeave,
468
+ onMouseDown: this.handleMouseDown,
469
+ onMouseUp: this.handleMouseUp,
470
+ onDragStart: this.handleDragStart,
471
+ onTouchStart: this.handleTouchStart,
472
+ onTouchEnd: this.handleTouchEnd,
473
+ onTouchCancel: this.handleTouchCancel,
474
+ onKeyDown: this.handleKeyDown,
475
+ onKeyUp: this.handleKeyUp,
476
+ onFocus: this.handleFocus,
477
+ onBlur: this.handleBlur,
478
+ // We set tabIndex to 0 so that users can tab to clickable
479
+ // things that aren't buttons or anchors.
480
+ tabIndex: 0
481
+ }; // When the link is set to open in a new window, we want to set some
482
+ // `rel` attributes. This is to ensure that the links we're sending folks
483
+ // to can't hijack the existing page. These defaults can be overriden
484
+ // by passing in a different value for the `rel` prop.
485
+ // More info: https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
486
+
487
+ childrenProps.rel = this.props.rel || (this.props.target === "_blank" ? "noopener noreferrer" : undefined);
488
+ const {
489
+ children
490
+ } = this.props;
491
+ return children && children(this.state, childrenProps);
492
+ }
493
+
494
+ }
495
+ ClickableBehavior.defaultProps = {
496
+ disabled: false
497
+ };
498
+
499
+ /**
500
+ * Returns:
501
+ * - false for hrefs staring with http://, https://, //.
502
+ * - false for '#', 'javascript:...', 'mailto:...', 'tel:...', etc.
503
+ * - true for all other values, e.g. /foo/bar
504
+ */
505
+ const isClientSideUrl = href => {
506
+ if (typeof href !== "string") {
507
+ return false;
508
+ }
509
+
510
+ return !/^(https?:)?\/\//i.test(href) && !/^([^#]*#[\w-]*|[\w\-.]+:)/.test(href);
511
+ };
512
+
513
+ /**
514
+ * Returns either the default ClickableBehavior or a react-router aware version.
515
+ *
516
+ * The react-router aware version is returned if `router` is a react-router-dom
517
+ * router, `skipClientNav` is not `true`, and `href` is an internal URL.
518
+ *
519
+ * The `router` can be accessed via this.context.router from a component rendered
520
+ * as a descendant of a BrowserRouter.
521
+ * See https://reacttraining.com/react-router/web/guides/basic-components.
522
+ */
523
+ const ClickableBehaviorWithRouter = withRouter(ClickableBehavior);
524
+ function getClickableBehavior(
525
+ /**
526
+ * The URL to navigate to.
527
+ */
528
+ href,
529
+ /**
530
+ * Should we skip using the react router and go to the page directly.
531
+ */
532
+ skipClientNav,
533
+ /**
534
+ * router object added to the React context object by react-router-dom.
535
+ */
536
+ router) {
537
+ if (router && skipClientNav !== true && href && isClientSideUrl(href)) {
538
+ // We cast to `any` here since the type of ClickableBehaviorWithRouter
539
+ // is slightly different from the return type of this function.
540
+ // TODO(WB-1037): Always return the wrapped version once all routes have
541
+ // been ported to the app-shell in webapp.
542
+ return ClickableBehaviorWithRouter;
543
+ }
544
+
545
+ return ClickableBehavior;
546
+ }
547
+
548
+ const _excluded = ["href", "onClick", "skipClientNav", "beforeNav", "safeWithNav", "style", "target", "testId", "onKeyDown", "onKeyUp", "hideDefaultFocusRing", "light", "disabled"];
549
+ const StyledAnchor = addStyle("a");
550
+ const StyledButton = addStyle("button");
551
+ const StyledLink = addStyle(Link);
552
+ /**
553
+ * A component to turn any custom component into a clickable one.
554
+ *
555
+ * Works by wrapping ClickableBehavior around the child element and styling the
556
+ * child appropriately and encapsulates routing logic which can be customized.
557
+ * Expects a function which returns an element as it's child.
558
+ *
559
+ * Example usage:
560
+ * ```jsx
561
+ * <Clickable onClick={() => alert("You clicked me!")}>
562
+ * {({hovered, focused, pressed}) =>
563
+ * <div
564
+ * style={[
565
+ * hovered && styles.hovered,
566
+ * focused && styles.focused,
567
+ * pressed && styles.pressed,
568
+ * ]}
569
+ * >
570
+ * Click Me!
571
+ * </div>
572
+ * }
573
+ * </Clickable>
574
+ * ```
575
+ */
576
+
577
+ class Clickable extends Component {
578
+ constructor(...args) {
579
+ super(...args);
580
+
581
+ this.getCorrectTag = (clickableState, commonProps) => {
582
+ const activeHref = this.props.href && !this.props.disabled;
583
+ const useClient = this.context.router && !this.props.skipClientNav && isClientSideUrl(this.props.href || ""); // NOTE: checking this.props.href here is redundant, but flow
584
+ // needs it to refine this.props.href to a string.
585
+
586
+ if (activeHref && useClient && this.props.href) {
587
+ return /*#__PURE__*/createElement(StyledLink, _extends({}, commonProps, {
588
+ to: this.props.href,
589
+ role: this.props.role,
590
+ target: this.props.target || undefined,
591
+ "aria-disabled": this.props.disabled ? "true" : undefined
592
+ }), this.props.children(clickableState));
593
+ } else if (activeHref && !useClient) {
594
+ return /*#__PURE__*/createElement(StyledAnchor, _extends({}, commonProps, {
595
+ href: this.props.href,
596
+ role: this.props.role,
597
+ target: this.props.target || undefined,
598
+ "aria-disabled": this.props.disabled ? "true" : undefined
599
+ }), this.props.children(clickableState));
600
+ } else {
601
+ return /*#__PURE__*/createElement(StyledButton, _extends({}, commonProps, {
602
+ type: "button",
603
+ disabled: this.props.disabled
604
+ }), this.props.children(clickableState));
605
+ }
606
+ };
607
+ }
608
+
609
+ render() {
610
+ const _this$props = this.props,
611
+ {
612
+ href,
613
+ onClick,
614
+ skipClientNav,
615
+ beforeNav = undefined,
616
+ safeWithNav = undefined,
617
+ style,
618
+ target = undefined,
619
+ testId,
620
+ onKeyDown,
621
+ onKeyUp,
622
+ hideDefaultFocusRing,
623
+ light,
624
+ disabled
625
+ } = _this$props,
626
+ restProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
627
+
628
+ const ClickableBehavior = getClickableBehavior(href, skipClientNav, this.context.router);
629
+
630
+ const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), style];
631
+
632
+ if (beforeNav) {
633
+ return /*#__PURE__*/createElement(ClickableBehavior, {
634
+ href: href,
635
+ onClick: onClick,
636
+ beforeNav: beforeNav,
637
+ safeWithNav: safeWithNav,
638
+ onKeyDown: onKeyDown,
639
+ onKeyUp: onKeyUp,
640
+ disabled: disabled
641
+ }, (state, childrenProps) => this.getCorrectTag(state, _extends({}, restProps, {
642
+ "data-test-id": testId,
643
+ style: getStyle(state)
644
+ }, childrenProps)));
645
+ } else {
646
+ return /*#__PURE__*/createElement(ClickableBehavior, {
647
+ href: href,
648
+ onClick: onClick,
649
+ safeWithNav: safeWithNav,
650
+ onKeyDown: onKeyDown,
651
+ onKeyUp: onKeyUp,
652
+ target: target,
653
+ disabled: disabled
654
+ }, (state, childrenProps) => this.getCorrectTag(state, _extends({}, restProps, {
655
+ "data-test-id": testId,
656
+ style: getStyle(state)
657
+ }, childrenProps)));
658
+ }
659
+ }
660
+
661
+ } // Source: https://gist.github.com/MoOx/9137295
662
+
663
+ Clickable.contextTypes = {
664
+ router: any
665
+ };
666
+ Clickable.defaultProps = {
667
+ light: false,
668
+ disabled: false,
669
+ "aria-label": ""
670
+ };
671
+ const styles = StyleSheet.create({
672
+ reset: {
673
+ border: "none",
674
+ margin: 0,
675
+ padding: 0,
676
+ width: "auto",
677
+ overflow: "visible",
678
+ background: "transparent",
679
+ textDecoration: "none",
680
+
681
+ /* inherit font & color from ancestor */
682
+ color: "inherit",
683
+ font: "inherit",
684
+ boxSizing: "border-box",
685
+ // This removes the 300ms click delay on mobile browsers by indicating that
686
+ // "double-tap to zoom" shouldn't be used on this element.
687
+ touchAction: "manipulation",
688
+ userSelect: "none",
689
+ // This is usual frowned upon b/c of accessibility. We expect users of Clickable
690
+ // to define their own focus styles.
691
+ outline: "none",
692
+
693
+ /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
694
+ lineHeight: "normal",
695
+
696
+ /* Corrects font smoothing for webkit */
697
+ WebkitFontSmoothing: "inherit",
698
+ MozOsxFontSmoothing: "inherit"
699
+ },
700
+ link: {
701
+ cursor: "pointer"
702
+ },
703
+ focused: {
704
+ outline: `solid 2px ${Color.blue}`
705
+ },
706
+ focusedLight: {
707
+ outline: `solid 2px ${Color.white}`
708
+ }
709
+ });
710
+
711
+ export default Clickable;
712
+ export { ClickableBehavior, getClickableBehavior, isClientSideUrl };