@khanacademy/wonder-blocks-link 6.1.7 → 6.1.8

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,289 +0,0 @@
1
- import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
- import {Link} from "react-router-dom";
4
- import {__RouterContext} from "react-router";
5
-
6
- import {addStyle} from "@khanacademy/wonder-blocks-core";
7
- import {mix, fade, color, spacing} from "@khanacademy/wonder-blocks-tokens";
8
- import {isClientSideUrl} from "@khanacademy/wonder-blocks-clickable";
9
- import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
10
- import externalLinkIcon from "@phosphor-icons/core/bold/arrow-square-out-bold.svg";
11
-
12
- import type {
13
- ChildrenProps,
14
- ClickableState,
15
- } from "@khanacademy/wonder-blocks-clickable";
16
- import type {StyleDeclaration} from "aphrodite";
17
- import type {SharedProps} from "./link";
18
-
19
- type Props = SharedProps &
20
- ChildrenProps &
21
- ClickableState & {
22
- href: string;
23
- };
24
-
25
- const StyledAnchor = addStyle("a");
26
- const StyledLink = addStyle(Link);
27
-
28
- const LinkCore = React.forwardRef(function LinkCore(
29
- props: Props,
30
- ref: React.ForwardedRef<typeof Link | HTMLAnchorElement>,
31
- ) {
32
- const renderInner = (router: any): React.ReactNode => {
33
- const {
34
- children,
35
- skipClientNav,
36
- focused,
37
- hovered,
38
- href,
39
- inline = false,
40
- kind = "primary",
41
- light = false,
42
- visitable = false,
43
- pressed,
44
- style,
45
- testId,
46
- waiting: _,
47
- target,
48
- startIcon,
49
- endIcon,
50
- ...restProps
51
- } = props;
52
-
53
- const linkStyles = _generateStyles(inline, kind, light, visitable);
54
- const restingStyles = inline
55
- ? linkStyles.restingInline
56
- : linkStyles.resting;
57
-
58
- const defaultStyles = [
59
- sharedStyles.shared,
60
- restingStyles,
61
- pressed && linkStyles.active,
62
- // A11y: The focus ring should always be present when the
63
- // the link has focus, even the link is being hovered over.
64
- // TODO(WB-1498): Udpate ClickableBehavior so that focus doesn't
65
- // stop on mouseleave. We want the focus ring to remain on a
66
- // focused link even after hovering and un-hovering on it.
67
- !pressed && hovered && linkStyles.hover,
68
- !pressed && focused && linkStyles.focus,
69
- ];
70
-
71
- const commonProps = {
72
- "data-testid": testId,
73
- style: [defaultStyles, style],
74
- target,
75
- ...restProps,
76
- } as const;
77
-
78
- const linkUrl = new URL(href, window.location.origin);
79
-
80
- const isExternalLink = linkUrl.origin !== window.location.origin;
81
-
82
- const externalIcon = (
83
- <PhosphorIcon
84
- icon={externalLinkIcon}
85
- size="small"
86
- style={[linkContentStyles.endIcon, linkContentStyles.centered]}
87
- testId="external-icon"
88
- />
89
- );
90
-
91
- let startIconElement;
92
- let endIconElement;
93
-
94
- if (startIcon) {
95
- startIconElement = React.cloneElement(startIcon, {
96
- style: [
97
- linkContentStyles.startIcon,
98
- linkContentStyles.centered,
99
- ],
100
- testId: "start-icon",
101
- "aria-hidden": "true",
102
- ...startIcon.props,
103
- } as Partial<React.ReactElement<React.ComponentProps<typeof PhosphorIcon>>>);
104
- }
105
-
106
- if (endIcon) {
107
- endIconElement = React.cloneElement(endIcon, {
108
- style: [linkContentStyles.endIcon, linkContentStyles.centered],
109
- testId: "end-icon",
110
- "aria-hidden": "true",
111
- ...endIcon.props,
112
- } as Partial<React.ReactElement<React.ComponentProps<typeof PhosphorIcon>>>);
113
- }
114
-
115
- const linkContent = (
116
- <>
117
- {startIcon && startIconElement}
118
- {children}
119
- {endIcon
120
- ? endIconElement
121
- : isExternalLink && target === "_blank" && externalIcon}
122
- </>
123
- );
124
-
125
- return router && !skipClientNav && isClientSideUrl(href) ? (
126
- <StyledLink
127
- {...commonProps}
128
- to={href}
129
- ref={ref as React.ForwardedRef<typeof Link>}
130
- >
131
- {linkContent}
132
- </StyledLink>
133
- ) : (
134
- <StyledAnchor
135
- {...commonProps}
136
- href={href}
137
- ref={ref as React.ForwardedRef<HTMLAnchorElement>}
138
- >
139
- {linkContent}
140
- </StyledAnchor>
141
- );
142
- };
143
-
144
- return (
145
- <__RouterContext.Consumer>
146
- {(router) => renderInner(router)}
147
- </__RouterContext.Consumer>
148
- );
149
- });
150
-
151
- const styles: Record<string, any> = {};
152
-
153
- const linkContentStyles = StyleSheet.create({
154
- startIcon: {
155
- marginInlineEnd: spacing.xxxSmall_4,
156
- },
157
- endIcon: {
158
- marginInlineStart: spacing.xxxSmall_4,
159
- },
160
- centered: {
161
- // Manually align the bottom of start/end icons with the text baseline.
162
- verticalAlign: "-10%",
163
- },
164
- });
165
-
166
- const sharedStyles = StyleSheet.create({
167
- shared: {
168
- cursor: "pointer",
169
- textDecoration: "none",
170
- outline: "none",
171
- alignItems: "center",
172
- },
173
- });
174
-
175
- const _generateStyles = (
176
- inline: boolean,
177
- kind: "primary" | "secondary",
178
- light: boolean,
179
- visitable: boolean,
180
- ) => {
181
- const buttonType = `${kind}-${inline.toString()}-${light.toString()}-${visitable.toString()}`;
182
- if (styles[buttonType]) {
183
- return styles[buttonType];
184
- }
185
-
186
- if (kind === "secondary" && light) {
187
- throw new Error("Secondary Light links are not supported");
188
- }
189
-
190
- if (visitable && kind !== "primary") {
191
- throw new Error("Only primary link is visitable");
192
- }
193
-
194
- const {blue, purple, white, offBlack, offBlack32, offBlack64} = color;
195
-
196
- // NOTE: This color is only used here.
197
- const pink = "#fa50ae";
198
-
199
- // Standard purple
200
- const linkPurple = mix(fade(offBlack, 0.08), purple);
201
- // Light blue
202
- const fadedBlue = color.fadedBlue;
203
- // Light pink
204
- const activeLightVisited = mix(fade(white, 0.32), pink);
205
- // Dark blue
206
- const activeDefaultPrimary = color.activeBlue;
207
-
208
- const primaryDefaultTextColor = light ? white : blue;
209
- const secondaryDefaultTextColor = inline ? offBlack : offBlack64;
210
- const defaultTextColor =
211
- kind === "primary"
212
- ? primaryDefaultTextColor
213
- : secondaryDefaultTextColor;
214
-
215
- const primaryActiveColor = light ? fadedBlue : activeDefaultPrimary;
216
- const secondaryActiveColor = inline ? activeDefaultPrimary : offBlack;
217
- const activeColor =
218
- kind === "primary" ? primaryActiveColor : secondaryActiveColor;
219
-
220
- const defaultVisited = visitable
221
- ? {
222
- ":visited": {
223
- color: light ? pink : linkPurple,
224
- },
225
- }
226
- : Object.freeze({});
227
- const activeVisited = visitable
228
- ? {
229
- ":visited": {
230
- color: light
231
- ? activeLightVisited
232
- : mix(offBlack32, linkPurple),
233
- },
234
- }
235
- : Object.freeze({});
236
-
237
- const newStyles: StyleDeclaration = {
238
- resting: {
239
- color: defaultTextColor,
240
- ...defaultVisited,
241
- },
242
- restingInline: {
243
- color: defaultTextColor,
244
- // TODO(WB-1521): Update text decoration to the 1px dashed
245
- // underline after the Link audit.
246
- // textDecoration: "underline currentcolor solid 1px",
247
- textDecoration: "underline currentcolor solid",
248
- // TODO(WB-1521): Update the underline offset to be 4px after
249
- // the Link audit.
250
- // textUnderlineOffset: 4,
251
- textUnderlineOffset: 2,
252
- ...defaultVisited,
253
- },
254
- hover: {
255
- // TODO(WB-1521): Update text decoration to the 1px dashed
256
- // underline after the Link audit.
257
- // textDecoration: "underline currentcolor dashed 2px",
258
- textDecoration: "underline currentcolor solid",
259
- color: defaultTextColor,
260
- // TODO(WB-1521): Update the underline offset to be 4px after
261
- // the Link audit.
262
- // textUnderlineOffset: 4,
263
- ...defaultVisited,
264
- },
265
- focus: {
266
- // Focus styles only show up with keyboard navigation.
267
- // Mouse users don't see focus styles.
268
- ":focus-visible": {
269
- color: defaultTextColor,
270
- outline: `1px solid ${light ? white : blue}`,
271
- borderRadius: 3,
272
- ...defaultVisited,
273
- },
274
- },
275
- active: {
276
- color: activeColor,
277
- textDecoration: "underline currentcolor solid",
278
- // TODO(WB-1521): Update the underline offset to be 4px after
279
- // the Link audit.
280
- // textUnderlineOffset: 4,
281
- ...activeVisited,
282
- },
283
- };
284
-
285
- styles[buttonType] = StyleSheet.create(newStyles);
286
- return styles[buttonType];
287
- };
288
-
289
- export default LinkCore;
@@ -1,291 +0,0 @@
1
- import * as React from "react";
2
- import {__RouterContext} from "react-router";
3
- import {Link as ReactRouterLink} from "react-router-dom";
4
- import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
5
-
6
- import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
7
- import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
8
- import type {Typography} from "@khanacademy/wonder-blocks-typography";
9
- import LinkCore from "./link-core";
10
-
11
- type CommonProps = AriaProps & {
12
- /**
13
- * Text to appear on the link. It can be a plain text or a Typography element.
14
- */
15
- children: string | React.ReactElement<React.ComponentProps<Typography>>;
16
- /**
17
- * URL to navigate to.
18
- */
19
- href: string;
20
- /**
21
- * An optional id attribute.
22
- */
23
- id?: string;
24
- /**
25
- * Indicates that this link is used within a body of text.
26
- * This styles the link with an underline to distinguish it
27
- * from surrounding text.
28
- */
29
- inline?: boolean;
30
- /**
31
- * Kind of Link. Note: Secondary light Links are not supported.
32
- */
33
- kind?: "primary" | "secondary";
34
- /**
35
- * Whether the button is on a dark/colored background.
36
- */
37
- light?: boolean;
38
- /**
39
- * Whether the link should change color once it's visited.
40
- * secondary or primary (light) links are not allowed to be visitable.
41
- */
42
- visitable?: boolean;
43
- /**
44
- * Specifies the type of relationship between the current document and the
45
- * linked document. Should only be used when `href` is specified. This
46
- * defaults to "noopener noreferrer" when `target="_blank"`, but can be
47
- * overridden by setting this prop to something else.
48
- */
49
- rel?: string;
50
- /**
51
- * Set the tabindex attribute on the rendered element.
52
- */
53
- tabIndex?: number;
54
- /**
55
- * Test ID used for e2e testing.
56
- */
57
- testId?: string;
58
- /**
59
- * Whether to avoid using client-side navigation.
60
- *
61
- * If the URL passed to href is local to the client-side, e.g.
62
- * /math/algebra/eval-exprs, then it tries to use react-router-dom's Link
63
- * component which handles the client-side navigation. You can set
64
- * `skipClientNav` to true avoid using client-side nav entirely.
65
- *
66
- * NOTE: All URLs containing a protocol are considered external, e.g.
67
- * https://khanacademy.org/math/algebra/eval-exprs will trigger a full
68
- * page reload.
69
- */
70
- skipClientNav?: boolean;
71
- /**
72
- * Custom styles.
73
- */
74
- style?: StyleType;
75
- // TODO(yejia): use this if ADR #47 has been implemented
76
- /*
77
- style?: Style<Exact<{
78
- width?: number | string
79
- position: Position,
80
- ...MarginStyles,
81
- ...FlexItemStyles,
82
- }>>,
83
- */
84
-
85
- /**
86
- * Adds CSS classes to the Link.
87
- */
88
- className?: string;
89
- // NOTE(jeresig): Currently React Docgen (used by Styleguidist) doesn't
90
- // support ... inside of an exact object type. Thus we had to move the
91
- // following propers into this SharedProps, even though they should be
92
- // external. Once that's fixed we can split them back apart.
93
-
94
- /**
95
- * Function to call when button is clicked.
96
- *
97
- * This callback should be used for things like marking BigBingo
98
- * conversions. It should NOT be used to redirect to a different URL or to
99
- * prevent navigation via e.preventDefault(). The event passed to this
100
- * handler will have its preventDefault() and stopPropagation() methods
101
- * stubbed out.
102
- */
103
- onClick?: (e: React.SyntheticEvent) => unknown;
104
- /**
105
- * Run async code in the background while client-side navigating. If the
106
- * browser does a full page load navigation, the callback promise must be
107
- * settled before the navigation will occur. Errors are ignored so that
108
- * navigation is guaranteed to succeed.
109
- */
110
- safeWithNav?: () => Promise<unknown>;
111
- /**
112
- * Respond to raw "keydown" event.
113
- */
114
- onKeyDown?: (e: React.KeyboardEvent) => unknown;
115
- /**
116
- * Respond to raw "keyup" event.
117
- */
118
- onKeyUp?: (e: React.KeyboardEvent) => unknown;
119
- /**
120
- * An optional title attribute.
121
- */
122
- title?: string;
123
- /**
124
- * An optional icon displayed before the link label.
125
- */
126
- startIcon?: React.ReactElement<React.ComponentProps<typeof PhosphorIcon>>;
127
- /**
128
- * An optional icon displayed after the link label.
129
- * If `target="_blank"` and `endIcon` is passed in, `endIcon` will override
130
- * the default `externalIcon`.
131
- */
132
- endIcon?: React.ReactElement<React.ComponentProps<typeof PhosphorIcon>>;
133
- };
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
-
166
- /**
167
- * Reusable link component.
168
- *
169
- * Consisting of a [`ClickableBehavior`](#clickablebehavior) surrounding a
170
- * `LinkCore`. `ClickableBehavior` handles interactions and state changes.
171
- * `LinkCore` is a stateless component which displays the different states
172
- * the `Link` can take.
173
- *
174
- * ### Usage
175
- *
176
- * ```jsx
177
- * <Link
178
- * href="https://khanacademy.org/"
179
- * >
180
- * Label
181
- * </Link>
182
- * ```
183
- */
184
- const Link = React.forwardRef(function Link(
185
- props: SharedProps,
186
- ref: React.ForwardedRef<typeof ReactRouterLink | HTMLAnchorElement>,
187
- ) {
188
- const {
189
- onClick,
190
- beforeNav = undefined,
191
- safeWithNav,
192
- href,
193
- skipClientNav,
194
- children,
195
- tabIndex,
196
- onKeyDown,
197
- onKeyUp,
198
- target = undefined,
199
- inline = false,
200
- kind = "primary",
201
- light = false,
202
- visitable = false,
203
- ...sharedProps
204
- } = props;
205
-
206
- const renderClickableBehavior = (router: any): React.ReactNode => {
207
- const ClickableBehavior = getClickableBehavior(
208
- href,
209
- skipClientNav,
210
- router,
211
- );
212
-
213
- if (beforeNav) {
214
- return (
215
- <ClickableBehavior
216
- disabled={false}
217
- href={href}
218
- role="link"
219
- onClick={onClick}
220
- beforeNav={beforeNav}
221
- safeWithNav={safeWithNav}
222
- onKeyDown={onKeyDown}
223
- onKeyUp={onKeyUp}
224
- >
225
- {(state, {...childrenProps}) => {
226
- return (
227
- <LinkCore
228
- {...sharedProps}
229
- {...state}
230
- {...childrenProps}
231
- skipClientNav={skipClientNav}
232
- href={href}
233
- target={target}
234
- tabIndex={tabIndex}
235
- inline={inline}
236
- kind={kind}
237
- light={light}
238
- visitable={visitable}
239
- ref={ref}
240
- >
241
- {children}
242
- </LinkCore>
243
- );
244
- }}
245
- </ClickableBehavior>
246
- );
247
- } else {
248
- return (
249
- <ClickableBehavior
250
- disabled={false}
251
- href={href}
252
- role="link"
253
- onClick={onClick}
254
- safeWithNav={safeWithNav}
255
- target={target}
256
- onKeyDown={onKeyDown}
257
- onKeyUp={onKeyUp}
258
- >
259
- {(state, {...childrenProps}) => {
260
- return (
261
- <LinkCore
262
- {...sharedProps}
263
- {...state}
264
- {...childrenProps}
265
- skipClientNav={skipClientNav}
266
- href={href}
267
- target={target}
268
- tabIndex={tabIndex}
269
- inline={inline}
270
- kind={kind}
271
- light={light}
272
- visitable={visitable}
273
- ref={ref}
274
- >
275
- {children}
276
- </LinkCore>
277
- );
278
- }}
279
- </ClickableBehavior>
280
- );
281
- }
282
- };
283
-
284
- return (
285
- <__RouterContext.Consumer>
286
- {(router) => renderClickableBehavior(router)}
287
- </__RouterContext.Consumer>
288
- );
289
- });
290
-
291
- export default Link;
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- import Link from "./components/link";
2
-
3
- export {Link as default};
@@ -1,15 +0,0 @@
1
- {
2
- "exclude": ["dist"],
3
- "extends": "../tsconfig-shared.json",
4
- "compilerOptions": {
5
- "outDir": "./dist",
6
- "rootDir": "src",
7
- },
8
- "references": [
9
- {"path": "../wonder-blocks-clickable/tsconfig-build.json"},
10
- {"path": "../wonder-blocks-core/tsconfig-build.json"},
11
- {"path": "../wonder-blocks-typography/tsconfig-build.json"},
12
- {"path": "../wonder-blocks-icon/tsconfig-build.json"},
13
- {"path": "../wonder-blocks-tokens/tsconfig-build.json"},
14
- ]
15
- }