@khanacademy/wonder-blocks-link 4.3.1 → 5.0.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.
@@ -1,9 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { Link as ReactRouterLink } from "react-router-dom";
3
3
  import type { AriaProps, StyleType } from "@khanacademy/wonder-blocks-core";
4
+ import Icon from "@khanacademy/wonder-blocks-icon";
4
5
  import type { Typography } from "@khanacademy/wonder-blocks-typography";
5
- import type { IconAsset } from "@khanacademy/wonder-blocks-icon";
6
- export type SharedProps = AriaProps & {
6
+ type CommonProps = AriaProps & {
7
7
  /**
8
8
  * Text to appear on the link. It can be a plain text or a Typography element.
9
9
  */
@@ -96,6 +96,22 @@ export type SharedProps = AriaProps & {
96
96
  * Respond to raw "keyup" event.
97
97
  */
98
98
  onKeyUp?: (e: React.KeyboardEvent) => unknown;
99
+ /**
100
+ * An optional title attribute.
101
+ */
102
+ title?: string;
103
+ /**
104
+ * An optional icon displayed before the link label.
105
+ */
106
+ startIcon?: React.ReactElement<typeof Icon>;
107
+ /**
108
+ * An optional icon displayed after the link label.
109
+ * If `target="_blank"` and `endIcon` is passed in, `endIcon` will override
110
+ * the default `externalIcon`.
111
+ */
112
+ endIcon?: React.ReactElement<typeof Icon>;
113
+ };
114
+ export type SharedProps = (CommonProps & {
99
115
  /**
100
116
  * A target destination window for a link to open in. We only support
101
117
  * "_blank" which opens the URL in a new tab.
@@ -103,6 +119,8 @@ export type SharedProps = AriaProps & {
103
119
  * TODO(WB-1262): only allow this prop when `href` is also set.t
104
120
  */
105
121
  target?: "_blank";
122
+ beforeNav?: never;
123
+ }) | (CommonProps & {
106
124
  /**
107
125
  * Run async code before navigating to the URL passed to `href`. If the
108
126
  * promise returned rejects then navigation will not occur.
@@ -117,21 +135,8 @@ export type SharedProps = AriaProps & {
117
135
  * navigation.
118
136
  */
119
137
  beforeNav?: () => Promise<unknown>;
120
- /**
121
- * An optional title attribute.
122
- */
123
- title?: string;
124
- /**
125
- * An optional icon displayed before the link label.
126
- */
127
- startIcon?: IconAsset;
128
- /**
129
- * An optional icon displayed after the link label.
130
- * If `target="_blank"` and `endIcon` is passed in, `endIcon` will override
131
- * the default `externalIcon`.
132
- */
133
- endIcon?: IconAsset;
134
- };
138
+ target?: never;
139
+ });
135
140
  /**
136
141
  * Reusable link component.
137
142
  *
@@ -150,135 +155,5 @@ export type SharedProps = AriaProps & {
150
155
  * </Link>
151
156
  * ```
152
157
  */
153
- declare const Link: React.ForwardRefExoticComponent<Readonly<import("../../../wonder-blocks-core/src/util/aria-types").AriaAttributes> & Readonly<{
154
- role?: import("../../../wonder-blocks-core/src/util/aria-types").AriaRole | undefined;
155
- }> & {
156
- /**
157
- * Text to appear on the link. It can be a plain text or a Typography element.
158
- */
159
- children: string | React.ReactElement<React.ComponentProps<Typography>>;
160
- /**
161
- * URL to navigate to.
162
- */
163
- href: string;
164
- /**
165
- * An optional id attribute.
166
- */
167
- id?: string | undefined;
168
- /**
169
- * Indicates that this link is used within a body of text.
170
- * This styles the link with an underline to distinguish it
171
- * from surrounding text.
172
- */
173
- inline?: boolean | undefined;
174
- /**
175
- * Kind of Link. Note: Secondary light Links are not supported.
176
- */
177
- kind?: "primary" | "secondary" | undefined;
178
- /**
179
- * Whether the button is on a dark/colored background.
180
- */
181
- light?: boolean | undefined;
182
- /**
183
- * Whether the link should change color once it's visited.
184
- * secondary or primary (light) links are not allowed to be visitable.
185
- */
186
- visitable?: boolean | undefined;
187
- /**
188
- * Specifies the type of relationship between the current document and the
189
- * linked document. Should only be used when `href` is specified. This
190
- * defaults to "noopener noreferrer" when `target="_blank"`, but can be
191
- * overridden by setting this prop to something else.
192
- */
193
- rel?: string | undefined;
194
- /**
195
- * Set the tabindex attribute on the rendered element.
196
- */
197
- tabIndex?: number | undefined;
198
- /**
199
- * Test ID used for e2e testing.
200
- */
201
- testId?: string | undefined;
202
- /**
203
- * Whether to avoid using client-side navigation.
204
- *
205
- * If the URL passed to href is local to the client-side, e.g.
206
- * /math/algebra/eval-exprs, then it tries to use react-router-dom's Link
207
- * component which handles the client-side navigation. You can set
208
- * `skipClientNav` to true avoid using client-side nav entirely.
209
- *
210
- * NOTE: All URLs containing a protocol are considered external, e.g.
211
- * https://khanacademy.org/math/algebra/eval-exprs will trigger a full
212
- * page reload.
213
- */
214
- skipClientNav?: boolean | undefined;
215
- /**
216
- * Custom styles.
217
- */
218
- style?: StyleType;
219
- /**
220
- * Adds CSS classes to the Link.
221
- */
222
- className?: string | undefined;
223
- /**
224
- * Function to call when button is clicked.
225
- *
226
- * This callback should be used for things like marking BigBingo
227
- * conversions. It should NOT be used to redirect to a different URL or to
228
- * prevent navigation via e.preventDefault(). The event passed to this
229
- * handler will have its preventDefault() and stopPropagation() methods
230
- * stubbed out.
231
- */
232
- onClick?: ((e: React.SyntheticEvent) => unknown) | undefined;
233
- /**
234
- * Run async code in the background while client-side navigating. If the
235
- * browser does a full page load navigation, the callback promise must be
236
- * settled before the navigation will occur. Errors are ignored so that
237
- * navigation is guaranteed to succeed.
238
- */
239
- safeWithNav?: (() => Promise<unknown>) | undefined;
240
- /**
241
- * Respond to raw "keydown" event.
242
- */
243
- onKeyDown?: ((e: React.KeyboardEvent) => unknown) | undefined;
244
- /**
245
- * Respond to raw "keyup" event.
246
- */
247
- onKeyUp?: ((e: React.KeyboardEvent) => unknown) | undefined;
248
- /**
249
- * A target destination window for a link to open in. We only support
250
- * "_blank" which opens the URL in a new tab.
251
- *
252
- * TODO(WB-1262): only allow this prop when `href` is also set.t
253
- */
254
- target?: "_blank" | undefined;
255
- /**
256
- * Run async code before navigating to the URL passed to `href`. If the
257
- * promise returned rejects then navigation will not occur.
258
- *
259
- * If both safeWithNav and beforeNav are provided, beforeNav will be run
260
- * first and safeWithNav will only be run if beforeNav does not reject.
261
- *
262
- * WARNING: Using this with `target="_blank"` will trigger built-in popup
263
- * blockers in Firefox and Safari. This is because we do navigation
264
- * programmatically and `beforeNav` causes a delay which means that the
265
- * browser can't make a directly link between a user action and the
266
- * navigation.
267
- */
268
- beforeNav?: (() => Promise<unknown>) | undefined;
269
- /**
270
- * An optional title attribute.
271
- */
272
- title?: string | undefined;
273
- /**
274
- * An optional icon displayed before the link label.
275
- */
276
- startIcon?: IconAsset | undefined;
277
- /**
278
- * An optional icon displayed after the link label.
279
- * If `target="_blank"` and `endIcon` is passed in, `endIcon` will override
280
- * the default `externalIcon`.
281
- */
282
- endIcon?: IconAsset | undefined;
283
- } & React.RefAttributes<HTMLAnchorElement | typeof ReactRouterLink>>;
158
+ declare const Link: React.ForwardRefExoticComponent<SharedProps & React.RefAttributes<HTMLAnchorElement | typeof ReactRouterLink>>;
284
159
  export default Link;
package/dist/es/index.js CHANGED
@@ -82,19 +82,23 @@ const LinkCore = React.forwardRef(function LinkCore(props, ref) {
82
82
  style: [linkContentStyles.endIcon, linkContentStyles.centered],
83
83
  testId: "external-icon"
84
84
  });
85
- const linkContent = React.createElement(React.Fragment, null, startIcon && React.createElement(Icon, {
86
- icon: startIcon,
87
- size: "small",
88
- style: [linkContentStyles.startIcon, linkContentStyles.centered],
89
- testId: "start-icon",
90
- "aria-hidden": "true"
91
- }), children, endIcon ? React.createElement(Icon, {
92
- icon: endIcon,
93
- size: "small",
94
- style: [linkContentStyles.endIcon, linkContentStyles.centered],
95
- testId: "end-icon",
96
- "aria-hidden": "true"
97
- }) : isExternalLink && target === "_blank" && externalIcon);
85
+ let startIconElement;
86
+ let endIconElement;
87
+ if (startIcon) {
88
+ startIconElement = React.cloneElement(startIcon, _extends({
89
+ style: [linkContentStyles.startIcon, linkContentStyles.centered],
90
+ testId: "start-icon",
91
+ "aria-hidden": "true"
92
+ }, startIcon.props));
93
+ }
94
+ if (endIcon) {
95
+ endIconElement = React.cloneElement(endIcon, _extends({
96
+ style: [linkContentStyles.endIcon, linkContentStyles.centered],
97
+ testId: "end-icon",
98
+ "aria-hidden": "true"
99
+ }, endIcon.props));
100
+ }
101
+ const linkContent = React.createElement(React.Fragment, null, startIcon && startIconElement, children, endIcon ? endIconElement : isExternalLink && target === "_blank" && externalIcon);
98
102
  return router && !skipClientNav && isClientSideUrl(href) ? React.createElement(StyledLink, _extends({}, commonProps, {
99
103
  to: href,
100
104
  ref: ref
package/dist/index.js CHANGED
@@ -109,19 +109,23 @@ const LinkCore = React__namespace.forwardRef(function LinkCore(props, ref) {
109
109
  style: [linkContentStyles.endIcon, linkContentStyles.centered],
110
110
  testId: "external-icon"
111
111
  });
112
- const linkContent = React__namespace.createElement(React__namespace.Fragment, null, startIcon && React__namespace.createElement(Icon__default["default"], {
113
- icon: startIcon,
114
- size: "small",
115
- style: [linkContentStyles.startIcon, linkContentStyles.centered],
116
- testId: "start-icon",
117
- "aria-hidden": "true"
118
- }), children, endIcon ? React__namespace.createElement(Icon__default["default"], {
119
- icon: endIcon,
120
- size: "small",
121
- style: [linkContentStyles.endIcon, linkContentStyles.centered],
122
- testId: "end-icon",
123
- "aria-hidden": "true"
124
- }) : isExternalLink && target === "_blank" && externalIcon);
112
+ let startIconElement;
113
+ let endIconElement;
114
+ if (startIcon) {
115
+ startIconElement = React__namespace.cloneElement(startIcon, _extends({
116
+ style: [linkContentStyles.startIcon, linkContentStyles.centered],
117
+ testId: "start-icon",
118
+ "aria-hidden": "true"
119
+ }, startIcon.props));
120
+ }
121
+ if (endIcon) {
122
+ endIconElement = React__namespace.cloneElement(endIcon, _extends({
123
+ style: [linkContentStyles.endIcon, linkContentStyles.centered],
124
+ testId: "end-icon",
125
+ "aria-hidden": "true"
126
+ }, endIcon.props));
127
+ }
128
+ const linkContent = React__namespace.createElement(React__namespace.Fragment, null, startIcon && startIconElement, children, endIcon ? endIconElement : isExternalLink && target === "_blank" && externalIcon);
125
129
  return router && !skipClientNav && wonderBlocksClickable.isClientSideUrl(href) ? React__namespace.createElement(StyledLink, _extends({}, commonProps, {
126
130
  to: href,
127
131
  ref: ref
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-link",
3
- "version": "4.3.1",
3
+ "version": "5.0.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,10 +16,10 @@
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
18
  "@babel/runtime": "^7.18.6",
19
- "@khanacademy/wonder-blocks-clickable": "^3.1.2",
19
+ "@khanacademy/wonder-blocks-clickable": "^4.0.0",
20
20
  "@khanacademy/wonder-blocks-color": "^2.0.1",
21
- "@khanacademy/wonder-blocks-core": "^5.3.1",
22
- "@khanacademy/wonder-blocks-icon": "^2.0.15",
21
+ "@khanacademy/wonder-blocks-core": "^6.0.0",
22
+ "@khanacademy/wonder-blocks-icon": "^2.1.1",
23
23
  "@khanacademy/wonder-blocks-spacing": "^4.0.1"
24
24
  },
25
25
  "peerDependencies": {
@@ -29,6 +29,6 @@
29
29
  "react-router-dom": "5.3.0"
30
30
  },
31
31
  "devDependencies": {
32
- "wb-dev-build-settings": "^0.9.7"
32
+ "@khanacademy/wb-dev-build-settings": "^1.0.0"
33
33
  }
34
- }
34
+ }
@@ -3,7 +3,7 @@ import {MemoryRouter, Route, Switch} from "react-router-dom";
3
3
  import {fireEvent, render, screen, waitFor} from "@testing-library/react";
4
4
  import userEvent from "@testing-library/user-event";
5
5
 
6
- import {icons} from "@khanacademy/wonder-blocks-icon";
6
+ import Icon, {icons} from "@khanacademy/wonder-blocks-icon";
7
7
 
8
8
  import Link from "../link";
9
9
 
@@ -412,7 +412,10 @@ describe("Link", () => {
412
412
  test("render icon with link when startIcon prop is passed in", () => {
413
413
  // Arrange
414
414
  render(
415
- <Link href="https://www.khanacademy.org/" startIcon={icons.add}>
415
+ <Link
416
+ href="https://www.khanacademy.org/"
417
+ startIcon={<Icon icon={icons.add} />}
418
+ >
416
419
  Add new item
417
420
  </Link>,
418
421
  );
@@ -440,7 +443,10 @@ describe("Link", () => {
440
443
  test("startIcon prop passed down correctly", () => {
441
444
  // Arrange
442
445
  render(
443
- <Link href="https://www.khanacademy.org/" startIcon={icons.add}>
446
+ <Link
447
+ href="https://www.khanacademy.org/"
448
+ startIcon={<Icon icon={icons.add} />}
449
+ >
444
450
  Add new item
445
451
  </Link>,
446
452
  );
@@ -461,7 +467,7 @@ describe("Link", () => {
461
467
  render(
462
468
  <Link
463
469
  href="https://www.khanacademy.org/"
464
- endIcon={icons.caretRight}
470
+ endIcon={<Icon icon={icons.caretRight} />}
465
471
  >
466
472
  Click to go back
467
473
  </Link>,
@@ -492,7 +498,7 @@ describe("Link", () => {
492
498
  render(
493
499
  <Link
494
500
  href="https://www.google.com/"
495
- endIcon={icons.caretRight}
501
+ endIcon={<Icon icon={icons.caretRight} />}
496
502
  target="_blank"
497
503
  >
498
504
  Open a new tab
@@ -511,7 +517,7 @@ describe("Link", () => {
511
517
  render(
512
518
  <Link
513
519
  href="https://www.google.com/"
514
- endIcon={icons.caretRight}
520
+ endIcon={<Icon icon={icons.caretRight} />}
515
521
  target="_blank"
516
522
  >
517
523
  Open a new tab
@@ -530,7 +536,7 @@ describe("Link", () => {
530
536
  test("endIcon prop passed down correctly", () => {
531
537
  // Arrange
532
538
  render(
533
- <Link href="/" endIcon={icons.caretRight}>
539
+ <Link href="/" endIcon={<Icon icon={icons.caretRight} />}>
534
540
  Click to go back
535
541
  </Link>,
536
542
  );
@@ -2,11 +2,11 @@ import * as React from "react";
2
2
 
3
3
  import Link from "../link";
4
4
 
5
- // TODO(FEI-5000): Re-enable test after updating props to be conditional.
6
- // <Link beforeNav={() => Promise.resolve()}>Hello, world!</Link>;
5
+ // @ts-expect-error - href must be used with safeWithNav
6
+ <Link beforeNav={() => Promise.resolve()}>Hello, world!</Link>;
7
7
 
8
- // TODO(FEI-5000): Re-enable test after updating props to be conditional.
9
- // <Link safeWithNav={() => Promise.resolve()}>Hello, world!</Link>;
8
+ // @ts-expect-error - href must be used with safeWithNav
9
+ <Link safeWithNav={() => Promise.resolve()}>Hello, world!</Link>;
10
10
 
11
11
  // It's okay to use onClick with href
12
12
  <Link href="/foo" onClick={() => {}}>
@@ -21,6 +21,11 @@ import Link from "../link";
21
21
  Hello, world!
22
22
  </Link>;
23
23
 
24
+ // @ts-expect-error - `target="_blank"` cannot beused with `beforeNav`
25
+ <Link href="/foo" target="_blank" beforeNav={() => Promise.resolve()}>
26
+ Hello, world!
27
+ </Link>;
28
+
24
29
  // All three of these props can be used together
25
30
  <Link
26
31
  href="/foo"
@@ -93,35 +93,37 @@ const LinkCore = React.forwardRef(function LinkCore(
93
93
  />
94
94
  );
95
95
 
96
+ let startIconElement;
97
+ let endIconElement;
98
+
99
+ if (startIcon) {
100
+ startIconElement = React.cloneElement(startIcon, {
101
+ style: [
102
+ linkContentStyles.startIcon,
103
+ linkContentStyles.centered,
104
+ ],
105
+ testId: "start-icon",
106
+ "aria-hidden": "true",
107
+ ...startIcon.props,
108
+ } as Partial<React.ComponentProps<typeof Icon>>);
109
+ }
110
+
111
+ if (endIcon) {
112
+ endIconElement = React.cloneElement(endIcon, {
113
+ style: [linkContentStyles.endIcon, linkContentStyles.centered],
114
+ testId: "end-icon",
115
+ "aria-hidden": "true",
116
+ ...endIcon.props,
117
+ } as Partial<React.ComponentProps<typeof Icon>>);
118
+ }
119
+
96
120
  const linkContent = (
97
121
  <>
98
- {startIcon && (
99
- <Icon
100
- icon={startIcon}
101
- size="small"
102
- style={[
103
- linkContentStyles.startIcon,
104
- linkContentStyles.centered,
105
- ]}
106
- testId="start-icon"
107
- aria-hidden="true"
108
- />
109
- )}
122
+ {startIcon && startIconElement}
110
123
  {children}
111
- {endIcon ? (
112
- <Icon
113
- icon={endIcon}
114
- size="small"
115
- style={[
116
- linkContentStyles.endIcon,
117
- linkContentStyles.centered,
118
- ]}
119
- testId="end-icon"
120
- aria-hidden="true"
121
- />
122
- ) : (
123
- isExternalLink && target === "_blank" && externalIcon
124
- )}
124
+ {endIcon
125
+ ? endIconElement
126
+ : isExternalLink && target === "_blank" && externalIcon}
125
127
  </>
126
128
  );
127
129
 
@@ -4,12 +4,11 @@ import {Link as ReactRouterLink} from "react-router-dom";
4
4
  import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
5
5
 
6
6
  import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
7
+ import Icon from "@khanacademy/wonder-blocks-icon";
7
8
  import type {Typography} from "@khanacademy/wonder-blocks-typography";
8
- import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
9
9
  import LinkCore from "./link-core";
10
10
 
11
- // TODO(FEI-5000): Convert back to conditional props after TS migration is complete.
12
- export type SharedProps = AriaProps & {
11
+ type CommonProps = AriaProps & {
13
12
  /**
14
13
  * Text to appear on the link. It can be a plain text or a Typography element.
15
14
  */
@@ -117,27 +116,6 @@ export type SharedProps = AriaProps & {
117
116
  * Respond to raw "keyup" event.
118
117
  */
119
118
  onKeyUp?: (e: React.KeyboardEvent) => unknown;
120
- /**
121
- * A target destination window for a link to open in. We only support
122
- * "_blank" which opens the URL in a new tab.
123
- *
124
- * TODO(WB-1262): only allow this prop when `href` is also set.t
125
- */
126
- target?: "_blank";
127
- /**
128
- * Run async code before navigating to the URL passed to `href`. If the
129
- * promise returned rejects then navigation will not occur.
130
- *
131
- * If both safeWithNav and beforeNav are provided, beforeNav will be run
132
- * first and safeWithNav will only be run if beforeNav does not reject.
133
- *
134
- * WARNING: Using this with `target="_blank"` will trigger built-in popup
135
- * blockers in Firefox and Safari. This is because we do navigation
136
- * programmatically and `beforeNav` causes a delay which means that the
137
- * browser can't make a directly link between a user action and the
138
- * navigation.
139
- */
140
- beforeNav?: () => Promise<unknown>;
141
119
  /**
142
120
  * An optional title attribute.
143
121
  */
@@ -145,15 +123,46 @@ export type SharedProps = AriaProps & {
145
123
  /**
146
124
  * An optional icon displayed before the link label.
147
125
  */
148
- startIcon?: IconAsset;
126
+ startIcon?: React.ReactElement<typeof Icon>;
149
127
  /**
150
128
  * An optional icon displayed after the link label.
151
129
  * If `target="_blank"` and `endIcon` is passed in, `endIcon` will override
152
130
  * the default `externalIcon`.
153
131
  */
154
- endIcon?: IconAsset;
132
+ endIcon?: React.ReactElement<typeof Icon>;
155
133
  };
156
134
 
135
+ export type SharedProps =
136
+ | (CommonProps & {
137
+ /**
138
+ * A target destination window for a link to open in. We only support
139
+ * "_blank" which opens the URL in a new tab.
140
+ *
141
+ * TODO(WB-1262): only allow this prop when `href` is also set.t
142
+ */
143
+ target?: "_blank";
144
+
145
+ beforeNav?: never; // disallow beforeNav when target="_blank"
146
+ })
147
+ | (CommonProps & {
148
+ /**
149
+ * Run async code before navigating to the URL passed to `href`. If the
150
+ * promise returned rejects then navigation will not occur.
151
+ *
152
+ * If both safeWithNav and beforeNav are provided, beforeNav will be run
153
+ * first and safeWithNav will only be run if beforeNav does not reject.
154
+ *
155
+ * WARNING: Using this with `target="_blank"` will trigger built-in popup
156
+ * blockers in Firefox and Safari. This is because we do navigation
157
+ * programmatically and `beforeNav` causes a delay which means that the
158
+ * browser can't make a directly link between a user action and the
159
+ * navigation.
160
+ */
161
+ beforeNav?: () => Promise<unknown>;
162
+
163
+ target?: never; // disallow target="_blank" when using beforeNav
164
+ });
165
+
157
166
  /**
158
167
  * Reusable link component.
159
168
  *