@khanacademy/wonder-blocks-clickable 2.2.4 → 2.2.7

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,237 @@
1
+ export default {
2
+ children: {
3
+ description:
4
+ "The child of Clickable must be a function which returns the component which should be made Clickable. The function is passed an object with three boolean properties: hovered, focused, and pressed.",
5
+ control: {
6
+ type: "text",
7
+ },
8
+ type: {
9
+ required: true,
10
+ },
11
+ table: {
12
+ type: {
13
+ summary: "(ClickableState) => React.Node",
14
+ },
15
+ },
16
+ },
17
+ id: {
18
+ description: "An optional id attribute.",
19
+ table: {
20
+ type: {
21
+ summary: "string",
22
+ },
23
+ },
24
+ control: {
25
+ type: "text",
26
+ },
27
+ },
28
+ /**
29
+ * States
30
+ */
31
+ light: {
32
+ description:
33
+ "Whether the Clickable is on a dark colored background. Sets the default focus ring color to white, instead of blue. Defaults to false.",
34
+ defaultValue: false,
35
+ type: {
36
+ required: true,
37
+ },
38
+ table: {
39
+ category: "States",
40
+ type: {
41
+ summary: "boolean",
42
+ },
43
+ },
44
+ },
45
+ disabled: {
46
+ description: "Disables or enables the child; defaults to false",
47
+ defaultValue: false,
48
+ type: {
49
+ required: true,
50
+ },
51
+ table: {
52
+ category: "States",
53
+ type: {
54
+ summary: "boolean",
55
+ },
56
+ },
57
+ },
58
+ hideDefaultFocusRing: {
59
+ description:
60
+ "Don't show the default focus ring. This should be used when implementing a custom focus ring within your own component that uses Clickable.",
61
+ table: {
62
+ category: "States",
63
+ type: {
64
+ summary: "boolean",
65
+ },
66
+ },
67
+ },
68
+ /**
69
+ * Styling
70
+ */
71
+ className: {
72
+ description: "Adds CSS classes to the Clickable.",
73
+ control: {
74
+ type: "text",
75
+ },
76
+ table: {
77
+ category: "Styling",
78
+ },
79
+ type: {
80
+ summary: "string",
81
+ },
82
+ },
83
+ style: {
84
+ description: "Optional custom styles.",
85
+ table: {
86
+ category: "Styling",
87
+ type: {
88
+ summary: "StyleType",
89
+ },
90
+ },
91
+ },
92
+ /**
93
+ * Events
94
+ */
95
+ onClick: {
96
+ description:
97
+ "An onClick function which Clickable can execute when clicked.",
98
+ table: {
99
+ category: "Events",
100
+ type: {
101
+ summary: "(e: SyntheticEvent<>) => mixed",
102
+ detail: "`onClick` is optional if `href` is present, but must be defined if `href` is not",
103
+ },
104
+ },
105
+ action: "clicked",
106
+ },
107
+ onkeyDown: {
108
+ description: "Respond to raw `keydown` event.",
109
+ table: {
110
+ category: "Events",
111
+ type: {
112
+ summary: "(e: SyntheticKeyboardEvent<>) => mixed",
113
+ },
114
+ },
115
+ },
116
+ onKeyUp: {
117
+ description: "Respond to raw `keyup` event.",
118
+ table: {
119
+ category: "Events",
120
+ type: {
121
+ summary: "(e: SyntheticKeyboardEvent<>) => mixed",
122
+ },
123
+ },
124
+ },
125
+ /**
126
+ * Navigation
127
+ */
128
+ skipClientNav: {
129
+ description:
130
+ "Avoids client-side routing in the presence of the `href` prop",
131
+ defaultValue: false,
132
+ control: {
133
+ type: "boolean",
134
+ },
135
+ table: {
136
+ category: "Navigation",
137
+ type: {
138
+ summary: "boolean",
139
+ },
140
+ },
141
+ },
142
+ rel: {
143
+ description:
144
+ 'Specifies the type of relationship between the current document and the linked document. Should only be used when `href` is specified. This defaults to `noopener noreferrer` when `target="_blank"`, but can be overridden by setting this prop to something else.',
145
+ control: {type: "text"},
146
+ table: {
147
+ category: "Navigation",
148
+ type: {
149
+ summary: "string",
150
+ },
151
+ },
152
+ },
153
+ target: {
154
+ description:
155
+ "A target destination window for a link to open in. Should only be used when `href` is specified.",
156
+ control: {type: "text"},
157
+ table: {
158
+ category: "Navigation",
159
+ type: {
160
+ summary: "string",
161
+ },
162
+ },
163
+ },
164
+ href: {
165
+ description:
166
+ "Optional `href` which `Clickable` should direct to, uses client-side routing by default if react-router is present",
167
+ control: {type: "text"},
168
+ table: {
169
+ category: "Navigation",
170
+ type: {
171
+ summary: "string",
172
+ detail: "URL is required when we use `safeWithNav`",
173
+ },
174
+ },
175
+ },
176
+ beforeNav: {
177
+ description:
178
+ "Run async code before navigating. If the promise returned rejects then navigation will not occur. If both `safeWithNav` and `beforeNav` are provided, `beforeNav` will be run first and `safeWithNav` will only be run if `beforeNav` does not reject.",
179
+ table: {
180
+ category: "Navigation",
181
+ type: {
182
+ summary: "() => Promise<mixed>",
183
+ },
184
+ },
185
+ },
186
+ safeWithNav: {
187
+ description: `Run async code in the background while client-side navigating. If the browser does a full page load navigation, the callback promise must be settled before the navigation will occur. Errors are ignored so that navigation is guaranteed to succeed.`,
188
+ table: {
189
+ category: "Navigation",
190
+ type: {
191
+ summary: "() => Promise<mixed>",
192
+ },
193
+ },
194
+ },
195
+
196
+ /**
197
+ * Accessibility
198
+ */
199
+ "aria-label": {
200
+ description:
201
+ "A label for the clickable element read by a screen reader.",
202
+ control: {
203
+ type: "text",
204
+ },
205
+ table: {
206
+ category: "Accessibility",
207
+ type: {
208
+ summary: "string",
209
+ detail: `aria-label should be used when using
210
+ graphical elements to let people using screen readers the purpose of the
211
+ clickable element.`,
212
+ },
213
+ },
214
+ },
215
+ role: {
216
+ description:
217
+ "The role of the component, can be a role of type `ClickableRole`",
218
+ control: {type: "select"},
219
+ options: [
220
+ "button",
221
+ "checkbox",
222
+ "link",
223
+ "listbox",
224
+ "menu",
225
+ "menuitem",
226
+ "radio",
227
+ "tab",
228
+ ],
229
+ table: {
230
+ category: "Accessibility",
231
+ type: {
232
+ summary: "ClickableRole",
233
+ detail: `"button" | "link" | "checkbox" | "radio" | "listbox" | "option" | "menuitem" | "menu" | "tab"`,
234
+ },
235
+ },
236
+ },
237
+ };
@@ -0,0 +1,300 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {StyleSheet} from "aphrodite";
5
+ import {MemoryRouter, Route, Switch} from "react-router-dom";
6
+
7
+ import Clickable from "@khanacademy/wonder-blocks-clickable";
8
+ import {View} from "@khanacademy/wonder-blocks-core";
9
+ import Color from "@khanacademy/wonder-blocks-color";
10
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
11
+ import {Body, LabelLarge} from "@khanacademy/wonder-blocks-typography";
12
+
13
+ import type {StoryComponentType} from "@storybook/react";
14
+ import ComponentInfo from "../../../../../.storybook/components/component-info.js";
15
+ import {name, version} from "../../../package.json";
16
+ import argTypes from "./clickable.argtypes.js";
17
+
18
+ export default {
19
+ title: "Clickable / Clickable",
20
+ component: Clickable,
21
+ argTypes: argTypes,
22
+ args: {
23
+ testId: "",
24
+ disabled: false,
25
+ light: false,
26
+ hideDefaultFocusRing: false,
27
+ },
28
+ decorators: [
29
+ (Story: StoryComponentType): React.Element<typeof View> => (
30
+ <View style={styles.centerText}>
31
+ <Story />
32
+ </View>
33
+ ),
34
+ ],
35
+ parameters: {
36
+ componentSubtitle: ((
37
+ <ComponentInfo name={name} version={version} />
38
+ ): any),
39
+ docs: {
40
+ description: {
41
+ component: null,
42
+ },
43
+ source: {
44
+ // See https://github.com/storybookjs/storybook/issues/12596
45
+ excludeDecorators: true,
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ export const Default: StoryComponentType = (args) => (
52
+ <Clickable {...args}>
53
+ {({hovered, focused, pressed}) => {
54
+ return (
55
+ <View
56
+ style={[
57
+ styles.clickable,
58
+ hovered && styles.hovered,
59
+ pressed && styles.pressed,
60
+ ]}
61
+ >
62
+ <Body>This text is clickable!</Body>
63
+ </View>
64
+ );
65
+ }}
66
+ </Clickable>
67
+ );
68
+
69
+ export const Basic: StoryComponentType = () => (
70
+ <View style={styles.centerText}>
71
+ <Clickable
72
+ href="https://www.khanacademy.org/about/tos"
73
+ skipClientNav={true}
74
+ >
75
+ {({hovered, focused, pressed}) => (
76
+ <View
77
+ style={[
78
+ hovered && styles.hovered,
79
+ pressed && styles.pressed,
80
+ ]}
81
+ >
82
+ <Body>This text is clickable!</Body>
83
+ </View>
84
+ )}
85
+ </Clickable>
86
+ </View>
87
+ );
88
+
89
+ Basic.parameters = {
90
+ docs: {
91
+ description: {
92
+ story: "You can make custom components Clickable by returning them in a function of the Clickable child. The eventState parameter the function takes allows access to states pressed, hovered and clicked, which you may use to create custom styles.\n\nClickable has a default focus ring style built-in. If you are creating your own custom focus ring it should be disabled using by setting `hideDefaultFocusRing={true}` in the props passed to `Clickable`.",
93
+ },
94
+ },
95
+ chromatic: {
96
+ // we don't need screenshots because this story is already covered in
97
+ // `Default`. We add this story to the `Docs` tab to present the
98
+ // description above along with the example.
99
+ disableSnapshot: true,
100
+ },
101
+ };
102
+
103
+ /**
104
+ * Clickable usage on dark backgrounds
105
+ */
106
+ export const Light: StoryComponentType = () => (
107
+ <View style={styles.dark}>
108
+ <Clickable
109
+ href="https://www.khanacademy.org/about/tos"
110
+ skipClientNav={true}
111
+ light={true}
112
+ >
113
+ {({hovered, focused, pressed}) => (
114
+ <View
115
+ style={[
116
+ styles.clickable,
117
+ hovered && styles.hovered,
118
+ pressed && styles.pressed,
119
+ ]}
120
+ >
121
+ <Body>This text is clickable!</Body>
122
+ </View>
123
+ )}
124
+ </Clickable>
125
+ </View>
126
+ );
127
+
128
+ Light.parameters = {
129
+ chromatic: {
130
+ // Not needed because the default state doesn't test the disabled
131
+ // clickable behavior.
132
+ disableSnapshot: true,
133
+ },
134
+ docs: {
135
+ description: {
136
+ story: "Clickable has a `light` prop which changes the default focus ring color to fit a dark background.",
137
+ },
138
+ },
139
+ backgrounds: {
140
+ default: "darkBlue",
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Disabled state
146
+ */
147
+ export const Disabled: StoryComponentType = (args) => (
148
+ <>
149
+ <Clickable onClick={() => {}} {...args}>
150
+ {({hovered, focused, pressed}) => (
151
+ <View
152
+ style={[
153
+ styles.clickable,
154
+ hovered && styles.hovered,
155
+ pressed && styles.pressed,
156
+ ]}
157
+ >
158
+ <Body>
159
+ Disabled clickable using the default disabled style
160
+ </Body>
161
+ </View>
162
+ )}
163
+ </Clickable>
164
+ <Clickable onClick={() => {}} {...args}>
165
+ {({hovered, focused, pressed}) => (
166
+ <View
167
+ style={[
168
+ styles.clickable,
169
+ hovered && styles.hovered,
170
+ pressed && styles.pressed,
171
+ args.disabled && styles.disabled,
172
+ ]}
173
+ >
174
+ <Body>
175
+ Disabled clickable passing custom disabled styles
176
+ </Body>
177
+ </View>
178
+ )}
179
+ </Clickable>
180
+ </>
181
+ );
182
+
183
+ Disabled.args = {
184
+ disabled: true,
185
+ };
186
+
187
+ Disabled.parameters = {
188
+ docs: {
189
+ description: {
190
+ story: "Clickable has a `disabled` prop which prevents the element from being operable. Note that the default disabled style is applied to the element, but you can also pass custom styles to the children element by passing any `disabled` styles (see the second clickable element in the example below).",
191
+ },
192
+ },
193
+ };
194
+
195
+ export const ClientSideNavigation: StoryComponentType = () => (
196
+ <MemoryRouter>
197
+ <View>
198
+ <View style={styles.row}>
199
+ <Clickable
200
+ href="/foo"
201
+ style={styles.heading}
202
+ onClick={() => {
203
+ // eslint-disable-next-line no-console
204
+ console.log("I'm still on the same page!");
205
+ }}
206
+ >
207
+ {(eventState) => (
208
+ <LabelLarge>Uses Client-side Nav</LabelLarge>
209
+ )}
210
+ </Clickable>
211
+ <Clickable
212
+ href="/iframe.html?id=clickable-clickable--default&viewMode=story"
213
+ style={styles.heading}
214
+ skipClientNav
215
+ >
216
+ {(eventState) => (
217
+ <LabelLarge>Avoids Client-side Nav</LabelLarge>
218
+ )}
219
+ </Clickable>
220
+ </View>
221
+ <View style={styles.navigation}>
222
+ <Switch>
223
+ <Route path="/foo">
224
+ <View id="foo">
225
+ The first clickable element does client-side
226
+ navigation here.
227
+ </View>
228
+ </Route>
229
+ <Route path="*">See navigation changes here</Route>
230
+ </Switch>
231
+ </View>
232
+ </View>
233
+ </MemoryRouter>
234
+ );
235
+
236
+ ClientSideNavigation.storyName = "Client-side Navigation";
237
+
238
+ ClientSideNavigation.parameters = {
239
+ docs: {
240
+ description: {
241
+ story: "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.",
242
+ },
243
+ },
244
+ };
245
+
246
+ ClientSideNavigation.parameters = {
247
+ chromatic: {
248
+ // we don't need screenshots because this story only tests behavior.
249
+ disableSnapshot: true,
250
+ },
251
+ docs: {
252
+ description: {
253
+ story:
254
+ "Clickable adds support to keyboard navigation. This way, your components are accessible and emulate better the browser's behavior.\n\n" +
255
+ "**NOTE:** If you want to navigate to an external URL and/or reload the window, make sure to use `href` and `skipClientNav={true}`",
256
+ },
257
+ },
258
+ };
259
+
260
+ const styles = StyleSheet.create({
261
+ clickable: {
262
+ borderWidth: 1,
263
+ padding: Spacing.medium_16,
264
+ },
265
+ hovered: {
266
+ textDecoration: "underline",
267
+ backgroundColor: Color.teal,
268
+ },
269
+ pressed: {
270
+ color: Color.blue,
271
+ },
272
+ focused: {
273
+ outline: `solid 4px ${Color.lightBlue}`,
274
+ },
275
+ centerText: {
276
+ gap: Spacing.medium_16,
277
+ textAlign: "center",
278
+ },
279
+ dark: {
280
+ backgroundColor: Color.darkBlue,
281
+ color: Color.white,
282
+ padding: Spacing.xSmall_8,
283
+ },
284
+ row: {
285
+ flexDirection: "row",
286
+ alignItems: "center",
287
+ },
288
+ heading: {
289
+ marginRight: Spacing.large_24,
290
+ },
291
+ navigation: {
292
+ border: `1px dashed ${Color.lightBlue}`,
293
+ marginTop: Spacing.large_24,
294
+ padding: Spacing.large_24,
295
+ },
296
+ disabled: {
297
+ color: Color.white,
298
+ backgroundColor: Color.offBlack64,
299
+ },
300
+ });
@@ -166,6 +166,7 @@ type Props =
166
166
  * A target destination window for a link to open in. Should only be used
167
167
  * when `href` is specified.
168
168
  */
169
+ // TODO(WB-1262): only allow this prop when `href` is also set.
169
170
  target?: "_blank",
170
171
  |}
171
172
  | {|
@@ -260,18 +261,19 @@ const startState: ClickableState = {
260
261
  * 3. Keyup (spacebar/enter) -> focus state
261
262
  *
262
263
  * Warning: The event handlers returned (onClick, onMouseEnter, onMouseLeave,
263
- * onMouseDown, onMouseUp, onDragStart, onTouchStart, onTouchEnd, onTouchCancel, onKeyDown,
264
- * onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the component
265
- * that has the ClickableBehavior. You cannot override these handlers without
266
- * potentially breaking the functionality of ClickableBehavior.
264
+ * onMouseDown, onMouseUp, onDragStart, onTouchStart, onTouchEnd, onTouchCancel,
265
+ * onKeyDown, onKeyUp, onFocus, onBlur, tabIndex) should be passed on to the
266
+ * component that has the ClickableBehavior. You cannot override these handlers
267
+ * without potentially breaking the functionality of ClickableBehavior.
267
268
  *
268
- * There are internal props triggerOnEnter and triggerOnSpace that can be set
269
- * to false if one of those keys shouldn't count as a click on this component.
270
- * Be careful about setting those to false -- make certain that the component
269
+ * There are internal props triggerOnEnter and triggerOnSpace that can be set to
270
+ * false if one of those keys shouldn't count as a click on this component. Be
271
+ * careful about setting those to false -- make certain that the component
271
272
  * shouldn't process that key.
272
273
  *
273
- * See [this document](https://docs.google.com/document/d/1DG5Rg2f0cawIL5R8UqnPQpd7pbdObk8OyjO5ryYQmBM/edit#)
274
- * for a more thorough explanation of expected behaviors and potential cavaets.
274
+ * See [this
275
+ document](https://docs.google.com/document/d/1DG5Rg2f0cawIL5R8UqnPQpd7pbdObk8OyjO5ryYQmBM/edit#)
276
+ for a more thorough explanation of expected behaviors and potential cavaets.
275
277
  *
276
278
  * `ClickableBehavior` accepts a function as `children` which is passed state
277
279
  * and an object containing event handlers and some other props. The `children`
@@ -279,32 +281,30 @@ const startState: ClickableState = {
279
281
  *
280
282
  * Example:
281
283
  *
282
- * ```js
283
- * class MyClickableComponent extends React.Component<Props> {
284
- * render(): React.Node {
285
- * const ClickableBehavior = getClickableBehavior();
286
- * return <ClickableBehavior
287
- * disabled={this.props.disabled}
288
- * onClick={this.props.onClick}
289
- * >
290
- * {({hovered}, childrenProps) =>
291
- * <RoundRect
292
- * textcolor='white'
293
- * backgroundColor={hovered ? 'red' : 'blue'}}
294
- * {...childrenProps}
295
- * >
296
- * {this.props.children}
297
- * </RoundRect>
298
- * }
299
- * </ClickableBehavior>
300
- * }
284
+ * ```jsx
285
+ * function MyClickableComponent(props: Props) {
286
+ * const ClickableBehavior = getClickableBehavior();
287
+ *
288
+ * return (
289
+ * <ClickableBehavior disabled={props.disabled} onClick={props.onClick}>
290
+ * {({hovered}, childrenProps) => (
291
+ * <RoundRect
292
+ * textcolor="white"
293
+ * backgroundColor={hovered ? "red" : "blue"}
294
+ * {...childrenProps}
295
+ * >
296
+ * {props.children}
297
+ * </RoundRect>
298
+ * )}
299
+ * </ClickableBehavior>
300
+ * );
301
301
  * }
302
302
  * ```
303
303
  *
304
- * This follows a pattern called [Function as Child Components]
305
- * (https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9).
304
+ * This follows a pattern called [Function as Child
305
+ * Components](https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9).
306
306
  *
307
- * WARNING: Do not use this component directly, use getClickableBehavior
307
+ * **WARNING:** Do not use this component directly, use getClickableBehavior
308
308
  * instead. getClickableBehavior takes three arguments (href, directtNav, and
309
309
  * router) and returns either the default ClickableBehavior or a react-router
310
310
  * aware version.
@@ -312,9 +312,9 @@ const startState: ClickableState = {
312
312
  * The react-router aware version is returned if `router` is a react-router-dom
313
313
  * router, `skipClientNav` is not `true`, and `href` is an internal URL.
314
314
  *
315
- * The `router` can be accessed via __RouterContext (imported from 'react-router')
316
- * from a component rendered as a descendant of a BrowserRouter.
317
- * See https://reacttraining.com/react-router/web/guides/basic-components.
315
+ * The `router` can be accessed via __RouterContext (imported from
316
+ 'react-router') from a component rendered as a descendant of a BrowserRouter.
317
+ See https://reacttraining.com/react-router/web/guides/basic-components.
318
318
  */
319
319
  export default class ClickableBehavior extends React.Component<
320
320
  Props,