@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
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
import * as PropTypes from "prop-types";
|
|
5
|
+
import {Link} from "react-router-dom";
|
|
6
|
+
|
|
7
|
+
import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
8
|
+
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
9
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
10
|
+
|
|
11
|
+
import getClickableBehavior from "../util/get-clickable-behavior.js";
|
|
12
|
+
import type {ClickableRole, ClickableState} from "./clickable-behavior.js";
|
|
13
|
+
import {isClientSideUrl} from "../util/is-client-side-url.js";
|
|
14
|
+
|
|
15
|
+
type CommonProps = {|
|
|
16
|
+
/**
|
|
17
|
+
* aria-label should be used when `spinner={true}` to let people using screen
|
|
18
|
+
* readers that the action taken by clicking the button will take some
|
|
19
|
+
* time to complete.
|
|
20
|
+
*/
|
|
21
|
+
...$Rest<AriaProps, {|"aria-disabled": "true" | "false" | void|}>,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The child of Clickable must be a function which returns the component
|
|
25
|
+
* which should be made Clickable. The function is passed an object with
|
|
26
|
+
* three boolean properties: hovered, focused, and pressed.
|
|
27
|
+
*/
|
|
28
|
+
children: (ClickableState) => React.Node,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An onClick function which Clickable can execute when clicked
|
|
32
|
+
*/
|
|
33
|
+
onClick?: (e: SyntheticEvent<>) => mixed,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optinal href which Clickable should direct to, uses client-side routing
|
|
37
|
+
* by default if react-router is present
|
|
38
|
+
*/
|
|
39
|
+
href?: string,
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Styles to apply to the Clickable compoenent
|
|
43
|
+
*/
|
|
44
|
+
style?: StyleType,
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds CSS classes to the Clickable.
|
|
48
|
+
*/
|
|
49
|
+
className?: string,
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the Clickable is on a dark colored background.
|
|
53
|
+
* Sets the default focus ring color to white, instead of blue.
|
|
54
|
+
* Defaults to false.
|
|
55
|
+
*/
|
|
56
|
+
light: boolean,
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Disables or enables the child; defaults to false
|
|
60
|
+
*/
|
|
61
|
+
disabled: boolean,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An optional id attribute.
|
|
65
|
+
*/
|
|
66
|
+
id?: string,
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Specifies the type of relationship between the current document and the
|
|
70
|
+
* linked document. Should only be used when `href` is specified. This
|
|
71
|
+
* defaults to "noopener noreferrer" when `target="_blank"`, but can be
|
|
72
|
+
* overridden by setting this prop to something else.
|
|
73
|
+
*/
|
|
74
|
+
rel?: string,
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The role of the component, can be a role of type ClickableRole
|
|
78
|
+
*/
|
|
79
|
+
role?: ClickableRole,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Avoids client-side routing in the presence of the href prop
|
|
83
|
+
*/
|
|
84
|
+
skipClientNav?: boolean,
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Test ID used for e2e testing.
|
|
88
|
+
*/
|
|
89
|
+
testId?: string,
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Respond to raw "keydown" event.
|
|
93
|
+
*/
|
|
94
|
+
onKeyDown?: (e: SyntheticKeyboardEvent<>) => mixed,
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Respond to raw "keyup" event.
|
|
98
|
+
*/
|
|
99
|
+
onKeyUp?: (e: SyntheticKeyboardEvent<>) => mixed,
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Don't show the default focus ring. This should be used when implementing
|
|
103
|
+
* a custom focus ring within your own component that uses Clickable.
|
|
104
|
+
*/
|
|
105
|
+
hideDefaultFocusRing?: boolean,
|
|
106
|
+
|};
|
|
107
|
+
|
|
108
|
+
type Props =
|
|
109
|
+
| {|
|
|
110
|
+
...CommonProps,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A target destination window for a link to open in.
|
|
114
|
+
*/
|
|
115
|
+
target?: "_blank",
|
|
116
|
+
|}
|
|
117
|
+
| {|
|
|
118
|
+
...CommonProps,
|
|
119
|
+
|
|
120
|
+
href: string,
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run async code before navigating. If the promise returned rejects then
|
|
124
|
+
* navigation will not occur.
|
|
125
|
+
*
|
|
126
|
+
* If both safeWithNav and beforeNav are provided, beforeNav will be run
|
|
127
|
+
* first and safeWithNav will only be run if beforeNav does not reject.
|
|
128
|
+
*/
|
|
129
|
+
beforeNav: () => Promise<mixed>,
|
|
130
|
+
|}
|
|
131
|
+
| {|
|
|
132
|
+
...CommonProps,
|
|
133
|
+
|
|
134
|
+
href: string,
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run async code in the background while client-side navigating. If the
|
|
138
|
+
* browser does a full page load navigation, the callback promise must be
|
|
139
|
+
* settled before the navigation will occur. Errors are ignored so that
|
|
140
|
+
* navigation is guaranteed to succeed.
|
|
141
|
+
*/
|
|
142
|
+
safeWithNav: () => Promise<mixed>,
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A target destination window for a link to open in.
|
|
146
|
+
*/
|
|
147
|
+
target?: "_blank",
|
|
148
|
+
|}
|
|
149
|
+
| {|
|
|
150
|
+
...CommonProps,
|
|
151
|
+
|
|
152
|
+
href: string,
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Run async code before navigating. If the promise returned rejects then
|
|
156
|
+
* navigation will not occur.
|
|
157
|
+
*
|
|
158
|
+
* If both safeWithNav and beforeNav are provided, beforeNav will be run
|
|
159
|
+
* first and safeWithNav will only be run if beforeNav does not reject.
|
|
160
|
+
*/
|
|
161
|
+
beforeNav: () => Promise<mixed>,
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Run async code in the background while client-side navigating. If the
|
|
165
|
+
* browser does a full page load navigation, the callback promise must be
|
|
166
|
+
* settled before the navigation will occur. Errors are ignored so that
|
|
167
|
+
* navigation is guaranteed to succeed.
|
|
168
|
+
*/
|
|
169
|
+
safeWithNav: () => Promise<mixed>,
|
|
170
|
+
|};
|
|
171
|
+
|
|
172
|
+
type ContextTypes = {|
|
|
173
|
+
router: $FlowFixMe,
|
|
174
|
+
|};
|
|
175
|
+
|
|
176
|
+
type DefaultProps = {|
|
|
177
|
+
light: $PropertyType<Props, "light">,
|
|
178
|
+
disabled: $PropertyType<Props, "disabled">,
|
|
179
|
+
"aria-label": $PropertyType<Props, "aria-label">,
|
|
180
|
+
|};
|
|
181
|
+
|
|
182
|
+
const StyledAnchor = addStyle<"a">("a");
|
|
183
|
+
const StyledButton = addStyle<"button">("button");
|
|
184
|
+
const StyledLink = addStyle<typeof Link>(Link);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A component to turn any custom component into a clickable one.
|
|
188
|
+
*
|
|
189
|
+
* Works by wrapping ClickableBehavior around the child element and styling the
|
|
190
|
+
* child appropriately and encapsulates routing logic which can be customized.
|
|
191
|
+
* Expects a function which returns an element as it's child.
|
|
192
|
+
*
|
|
193
|
+
* Example usage:
|
|
194
|
+
* ```jsx
|
|
195
|
+
* <Clickable onClick={() => alert("You clicked me!")}>
|
|
196
|
+
* {({hovered, focused, pressed}) =>
|
|
197
|
+
* <div
|
|
198
|
+
* style={[
|
|
199
|
+
* hovered && styles.hovered,
|
|
200
|
+
* focused && styles.focused,
|
|
201
|
+
* pressed && styles.pressed,
|
|
202
|
+
* ]}
|
|
203
|
+
* >
|
|
204
|
+
* Click Me!
|
|
205
|
+
* </div>
|
|
206
|
+
* }
|
|
207
|
+
* </Clickable>
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export default class Clickable extends React.Component<Props> {
|
|
211
|
+
static contextTypes: ContextTypes = {router: PropTypes.any};
|
|
212
|
+
|
|
213
|
+
static defaultProps: DefaultProps = {
|
|
214
|
+
light: false,
|
|
215
|
+
disabled: false,
|
|
216
|
+
"aria-label": "",
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
getCorrectTag: (
|
|
220
|
+
clickableState: ClickableState,
|
|
221
|
+
commonProps: {[string]: any, ...},
|
|
222
|
+
) => React.Node = (clickableState, commonProps) => {
|
|
223
|
+
const activeHref = this.props.href && !this.props.disabled;
|
|
224
|
+
const useClient =
|
|
225
|
+
this.context.router &&
|
|
226
|
+
!this.props.skipClientNav &&
|
|
227
|
+
isClientSideUrl(this.props.href || "");
|
|
228
|
+
|
|
229
|
+
// NOTE: checking this.props.href here is redundant, but flow
|
|
230
|
+
// needs it to refine this.props.href to a string.
|
|
231
|
+
if (activeHref && useClient && this.props.href) {
|
|
232
|
+
return (
|
|
233
|
+
<StyledLink
|
|
234
|
+
{...commonProps}
|
|
235
|
+
to={this.props.href}
|
|
236
|
+
role={this.props.role}
|
|
237
|
+
target={this.props.target || undefined}
|
|
238
|
+
aria-disabled={this.props.disabled ? "true" : undefined}
|
|
239
|
+
>
|
|
240
|
+
{this.props.children(clickableState)}
|
|
241
|
+
</StyledLink>
|
|
242
|
+
);
|
|
243
|
+
} else if (activeHref && !useClient) {
|
|
244
|
+
return (
|
|
245
|
+
<StyledAnchor
|
|
246
|
+
{...commonProps}
|
|
247
|
+
href={this.props.href}
|
|
248
|
+
role={this.props.role}
|
|
249
|
+
target={this.props.target || undefined}
|
|
250
|
+
aria-disabled={this.props.disabled ? "true" : undefined}
|
|
251
|
+
>
|
|
252
|
+
{this.props.children(clickableState)}
|
|
253
|
+
</StyledAnchor>
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
return (
|
|
257
|
+
<StyledButton
|
|
258
|
+
{...commonProps}
|
|
259
|
+
type="button"
|
|
260
|
+
disabled={this.props.disabled}
|
|
261
|
+
>
|
|
262
|
+
{this.props.children(clickableState)}
|
|
263
|
+
</StyledButton>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
render(): React.Node {
|
|
269
|
+
const {
|
|
270
|
+
href,
|
|
271
|
+
onClick,
|
|
272
|
+
skipClientNav,
|
|
273
|
+
beforeNav = undefined,
|
|
274
|
+
safeWithNav = undefined,
|
|
275
|
+
style,
|
|
276
|
+
target = undefined,
|
|
277
|
+
testId,
|
|
278
|
+
onKeyDown,
|
|
279
|
+
onKeyUp,
|
|
280
|
+
hideDefaultFocusRing,
|
|
281
|
+
light,
|
|
282
|
+
disabled,
|
|
283
|
+
...restProps
|
|
284
|
+
} = this.props;
|
|
285
|
+
const ClickableBehavior = getClickableBehavior(
|
|
286
|
+
href,
|
|
287
|
+
skipClientNav,
|
|
288
|
+
this.context.router,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const getStyle = (state: ClickableState): StyleType => [
|
|
292
|
+
styles.reset,
|
|
293
|
+
styles.link,
|
|
294
|
+
!hideDefaultFocusRing &&
|
|
295
|
+
state.focused &&
|
|
296
|
+
(light ? styles.focusedLight : styles.focused),
|
|
297
|
+
style,
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
if (beforeNav) {
|
|
301
|
+
return (
|
|
302
|
+
<ClickableBehavior
|
|
303
|
+
href={href}
|
|
304
|
+
onClick={onClick}
|
|
305
|
+
beforeNav={beforeNav}
|
|
306
|
+
safeWithNav={safeWithNav}
|
|
307
|
+
onKeyDown={onKeyDown}
|
|
308
|
+
onKeyUp={onKeyUp}
|
|
309
|
+
disabled={disabled}
|
|
310
|
+
>
|
|
311
|
+
{(state, childrenProps) =>
|
|
312
|
+
this.getCorrectTag(state, {
|
|
313
|
+
...restProps,
|
|
314
|
+
"data-test-id": testId,
|
|
315
|
+
style: getStyle(state),
|
|
316
|
+
...childrenProps,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
</ClickableBehavior>
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
return (
|
|
323
|
+
<ClickableBehavior
|
|
324
|
+
href={href}
|
|
325
|
+
onClick={onClick}
|
|
326
|
+
safeWithNav={safeWithNav}
|
|
327
|
+
onKeyDown={onKeyDown}
|
|
328
|
+
onKeyUp={onKeyUp}
|
|
329
|
+
target={target}
|
|
330
|
+
disabled={disabled}
|
|
331
|
+
>
|
|
332
|
+
{(state, childrenProps) =>
|
|
333
|
+
this.getCorrectTag(state, {
|
|
334
|
+
...restProps,
|
|
335
|
+
"data-test-id": testId,
|
|
336
|
+
style: getStyle(state),
|
|
337
|
+
...childrenProps,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
</ClickableBehavior>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Source: https://gist.github.com/MoOx/9137295
|
|
347
|
+
const styles = StyleSheet.create({
|
|
348
|
+
reset: {
|
|
349
|
+
border: "none",
|
|
350
|
+
margin: 0,
|
|
351
|
+
padding: 0,
|
|
352
|
+
width: "auto",
|
|
353
|
+
overflow: "visible",
|
|
354
|
+
|
|
355
|
+
background: "transparent",
|
|
356
|
+
textDecoration: "none",
|
|
357
|
+
|
|
358
|
+
/* inherit font & color from ancestor */
|
|
359
|
+
color: "inherit",
|
|
360
|
+
font: "inherit",
|
|
361
|
+
|
|
362
|
+
boxSizing: "border-box",
|
|
363
|
+
// This removes the 300ms click delay on mobile browsers by indicating that
|
|
364
|
+
// "double-tap to zoom" shouldn't be used on this element.
|
|
365
|
+
touchAction: "manipulation",
|
|
366
|
+
userSelect: "none",
|
|
367
|
+
|
|
368
|
+
// This is usual frowned upon b/c of accessibility. We expect users of Clickable
|
|
369
|
+
// to define their own focus styles.
|
|
370
|
+
outline: "none",
|
|
371
|
+
|
|
372
|
+
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
|
373
|
+
lineHeight: "normal",
|
|
374
|
+
|
|
375
|
+
/* Corrects font smoothing for webkit */
|
|
376
|
+
WebkitFontSmoothing: "inherit",
|
|
377
|
+
MozOsxFontSmoothing: "inherit",
|
|
378
|
+
},
|
|
379
|
+
link: {
|
|
380
|
+
cursor: "pointer",
|
|
381
|
+
},
|
|
382
|
+
focused: {
|
|
383
|
+
outline: `solid 2px ${Color.blue}`,
|
|
384
|
+
},
|
|
385
|
+
focusedLight: {
|
|
386
|
+
outline: `solid 2px ${Color.white}`,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
### Creating a Clickable component
|
|
2
|
+
|
|
3
|
+
You can make custom components `Clickable` by returning them in a function of the
|
|
4
|
+
`Clickable` child. The eventState parameter the function takes allows access to states
|
|
5
|
+
pressed, hovered and clicked, which you may use to create custom styles.
|
|
6
|
+
|
|
7
|
+
Clickable has a default focus ring style built-in. If you are creating your own
|
|
8
|
+
custom focus ring it should be disabled using by setting `hideDefaultFocusRing={true}`
|
|
9
|
+
in the props passed to `Clickable`.
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import {StyleSheet} from "aphrodite";
|
|
13
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
14
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
15
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
16
|
+
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
17
|
+
|
|
18
|
+
const styles = StyleSheet.create({
|
|
19
|
+
hovered: {
|
|
20
|
+
textDecoration: "underline",
|
|
21
|
+
backgroundColor: Color.teal,
|
|
22
|
+
},
|
|
23
|
+
pressed: {
|
|
24
|
+
color: Color.blue,
|
|
25
|
+
},
|
|
26
|
+
focused: {
|
|
27
|
+
outline: `solid 4px ${Color.offBlack64}`,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
<View>
|
|
32
|
+
<Clickable
|
|
33
|
+
onClick={() => alert("You clicked some text!")}
|
|
34
|
+
hideDefaultFocusRing={true}
|
|
35
|
+
role="tab"
|
|
36
|
+
>
|
|
37
|
+
{
|
|
38
|
+
({hovered, focused, pressed}) =>
|
|
39
|
+
<View style={[
|
|
40
|
+
hovered && styles.hovered,
|
|
41
|
+
focused && styles.focused,
|
|
42
|
+
pressed && styles.pressed,
|
|
43
|
+
]}>
|
|
44
|
+
<Body>
|
|
45
|
+
This text is clickable!
|
|
46
|
+
</Body>
|
|
47
|
+
</View>
|
|
48
|
+
}
|
|
49
|
+
</Clickable>
|
|
50
|
+
</View>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Clickable has a `light` prop which changes the default focus ring color to fit a dark background.
|
|
54
|
+
|
|
55
|
+
```jsx
|
|
56
|
+
import {StyleSheet} from "aphrodite";
|
|
57
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
58
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
59
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
60
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
61
|
+
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
background: {
|
|
65
|
+
backgroundColor: Color.darkBlue,
|
|
66
|
+
color: Color.white,
|
|
67
|
+
padding: Spacing.small_12,
|
|
68
|
+
},
|
|
69
|
+
hovered: {
|
|
70
|
+
textDecoration: "underline",
|
|
71
|
+
backgroundColor: Color.purple,
|
|
72
|
+
},
|
|
73
|
+
pressed: {
|
|
74
|
+
color: Color.blue,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
<View style={styles.background}>
|
|
79
|
+
<Clickable
|
|
80
|
+
onClick={() => alert("You clicked some text!")}
|
|
81
|
+
role="tab"
|
|
82
|
+
light={true}
|
|
83
|
+
>
|
|
84
|
+
{
|
|
85
|
+
({hovered, focused, pressed}) =>
|
|
86
|
+
<View style={[
|
|
87
|
+
hovered && styles.hovered,
|
|
88
|
+
pressed && styles.pressed,
|
|
89
|
+
]}>
|
|
90
|
+
<Body>
|
|
91
|
+
This text is clickable!
|
|
92
|
+
</Body>
|
|
93
|
+
</View>
|
|
94
|
+
}
|
|
95
|
+
</Clickable>
|
|
96
|
+
</View>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Client-Side routing with Clickable
|
|
100
|
+
|
|
101
|
+
If your Clickable component is within a React-Router enviroment, your component will automatically default to client-side routing with the `href` prop is set. This behavior can be toggeled by passing the `skipClientNav` prop. In this example we see two Clickable h1 tags, one which employs client-side routing, and the other uses skipClientNav to avoid this default behavior.
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
import {StyleSheet} from "aphrodite";
|
|
105
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
106
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
107
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
108
|
+
import {MemoryRouter, Route, Switch} from "react-router-dom";
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
row: {
|
|
112
|
+
flexDirection: "row",
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
},
|
|
115
|
+
h1: {
|
|
116
|
+
marginRight: Spacing.large_24,
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// NOTE: In actual code you would use BrowserRouter instead
|
|
121
|
+
<MemoryRouter>
|
|
122
|
+
<View style={styles.row}>
|
|
123
|
+
<Clickable href="/foo" style={styles.h1} onClick={() => console.log("I'm still on the same page!")}>
|
|
124
|
+
{
|
|
125
|
+
eventState =>
|
|
126
|
+
<h1>
|
|
127
|
+
Uses Client-side Nav
|
|
128
|
+
</h1>
|
|
129
|
+
}
|
|
130
|
+
</Clickable>
|
|
131
|
+
<Clickable href="/foo" style={styles.h1} skipClientNav>
|
|
132
|
+
{
|
|
133
|
+
eventState =>
|
|
134
|
+
<h1>
|
|
135
|
+
Avoids Client-side Nav
|
|
136
|
+
</h1>
|
|
137
|
+
}
|
|
138
|
+
</Clickable>
|
|
139
|
+
<Switch>
|
|
140
|
+
<Route path="/foo">
|
|
141
|
+
<View id="foo">Hello, world!</View>
|
|
142
|
+
</Route>
|
|
143
|
+
</Switch>
|
|
144
|
+
</View>
|
|
145
|
+
</MemoryRouter>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Navigating with the Keyboard
|
|
149
|
+
|
|
150
|
+
Clickable adds support to keyboard navigation. This way, your components are
|
|
151
|
+
accessible and emulate better the browser's behavior.
|
|
152
|
+
|
|
153
|
+
*NOTE:* If you want to navigate to an external URL and/or reload the window, make
|
|
154
|
+
sure to use `href` and `skipClientNav={true}`.
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
import {StyleSheet} from "aphrodite";
|
|
158
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
159
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
160
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
161
|
+
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
162
|
+
|
|
163
|
+
const styles = StyleSheet.create({
|
|
164
|
+
hovered: {
|
|
165
|
+
textDecoration: "underline",
|
|
166
|
+
backgroundColor: Color.teal,
|
|
167
|
+
},
|
|
168
|
+
pressed: {
|
|
169
|
+
color: Color.blue,
|
|
170
|
+
},
|
|
171
|
+
focused: {
|
|
172
|
+
outline: `solid 4px ${Color.lightBlue}`,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
<View>
|
|
177
|
+
<Clickable
|
|
178
|
+
href="https://www.khanacademy.org/about/tos"
|
|
179
|
+
skipClientNav={true}
|
|
180
|
+
hideDefaultFocusRing={true}
|
|
181
|
+
>
|
|
182
|
+
{
|
|
183
|
+
({hovered, focused, pressed}) =>
|
|
184
|
+
<View style={[
|
|
185
|
+
hovered && styles.hovered,
|
|
186
|
+
focused && styles.focused,
|
|
187
|
+
pressed && styles.pressed,
|
|
188
|
+
]}>
|
|
189
|
+
<Body>
|
|
190
|
+
This text should navigate using the keyboard
|
|
191
|
+
</Body>
|
|
192
|
+
</View>
|
|
193
|
+
}
|
|
194
|
+
</Clickable>
|
|
195
|
+
</View>
|
|
196
|
+
```
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {StyleSheet} from "aphrodite";
|
|
5
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
6
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
8
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
9
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
10
|
+
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
11
|
+
|
|
12
|
+
import type {StoryComponentType} from "@storybook/react";
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
title: "Clickable",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const basic: StoryComponentType = () => (
|
|
19
|
+
<View>
|
|
20
|
+
<View style={styles.centerText}>
|
|
21
|
+
<Clickable
|
|
22
|
+
href="https://www.khanacademy.org/about/tos"
|
|
23
|
+
skipClientNav={true}
|
|
24
|
+
>
|
|
25
|
+
{({hovered, focused, pressed}) => (
|
|
26
|
+
<View
|
|
27
|
+
style={[
|
|
28
|
+
hovered && styles.hovered,
|
|
29
|
+
pressed && styles.pressed,
|
|
30
|
+
]}
|
|
31
|
+
>
|
|
32
|
+
<Body>This text is clickable!</Body>
|
|
33
|
+
</View>
|
|
34
|
+
)}
|
|
35
|
+
</Clickable>
|
|
36
|
+
</View>
|
|
37
|
+
<Strut size={Spacing.xLarge_32} />
|
|
38
|
+
<View style={[styles.centerText, styles.dark]}>
|
|
39
|
+
<Clickable
|
|
40
|
+
href="https://www.khanacademy.org/about/tos"
|
|
41
|
+
skipClientNav={true}
|
|
42
|
+
light={true}
|
|
43
|
+
>
|
|
44
|
+
{({hovered, focused, pressed}) => (
|
|
45
|
+
<View
|
|
46
|
+
style={[
|
|
47
|
+
hovered && styles.hovered,
|
|
48
|
+
pressed && styles.pressed,
|
|
49
|
+
]}
|
|
50
|
+
>
|
|
51
|
+
<Body>This text is clickable!</Body>
|
|
52
|
+
</View>
|
|
53
|
+
)}
|
|
54
|
+
</Clickable>
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export const keyboardNavigation: StoryComponentType = () => (
|
|
60
|
+
<View>
|
|
61
|
+
<Clickable
|
|
62
|
+
href="https://www.khanacademy.org/about/tos"
|
|
63
|
+
skipClientNav={true}
|
|
64
|
+
>
|
|
65
|
+
{({hovered, focused, pressed}) => (
|
|
66
|
+
<View
|
|
67
|
+
style={[
|
|
68
|
+
hovered && styles.hovered,
|
|
69
|
+
focused && styles.focused,
|
|
70
|
+
pressed && styles.pressed,
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<Body>This text should navigate using the keyboard</Body>
|
|
74
|
+
</View>
|
|
75
|
+
)}
|
|
76
|
+
</Clickable>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
keyboardNavigation.story = {
|
|
81
|
+
parameters: {
|
|
82
|
+
chromatic: {
|
|
83
|
+
// we don't need screenshots because this story only tests behavior.
|
|
84
|
+
disable: true,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const keyboardNavigationTab: StoryComponentType = () => (
|
|
90
|
+
<View>
|
|
91
|
+
<Clickable role="tab" aria-controls="panel-1" id="tab-1">
|
|
92
|
+
{({hovered, focused, pressed}) => (
|
|
93
|
+
<View
|
|
94
|
+
style={[
|
|
95
|
+
hovered && styles.hovered,
|
|
96
|
+
focused && styles.focused,
|
|
97
|
+
pressed && styles.pressed,
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
<Body>Open School Info</Body>
|
|
101
|
+
</View>
|
|
102
|
+
)}
|
|
103
|
+
</Clickable>
|
|
104
|
+
<View id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
|
|
105
|
+
This is the information for the school.
|
|
106
|
+
</View>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
hovered: {
|
|
112
|
+
textDecoration: "underline",
|
|
113
|
+
backgroundColor: Color.teal,
|
|
114
|
+
},
|
|
115
|
+
pressed: {
|
|
116
|
+
color: Color.blue,
|
|
117
|
+
},
|
|
118
|
+
focused: {
|
|
119
|
+
outline: `solid 4px ${Color.lightBlue}`,
|
|
120
|
+
},
|
|
121
|
+
centerText: {
|
|
122
|
+
textAlign: "center",
|
|
123
|
+
},
|
|
124
|
+
dark: {
|
|
125
|
+
backgroundColor: Color.darkBlue,
|
|
126
|
+
color: Color.white,
|
|
127
|
+
padding: Spacing.xSmall_8,
|
|
128
|
+
},
|
|
129
|
+
});
|