@astryxdesign/core 0.6.3-canary.98a2e4e → 0.6.3-canary.9e4c545
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/dist/Citation/Citation.d.ts +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +3 -2
- package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
- package/dist/CodeBlock/highlightRanges.js +14 -3
- package/dist/Link/useLinkComponent.d.ts +5 -15
- package/dist/Link/useLinkComponent.d.ts.map +1 -1
- package/dist/Link/useLinkComponent.js +34 -51
- package/dist/Markdown/Markdown.js +2 -2
- package/dist/Markdown/url.d.ts +2 -0
- package/dist/Markdown/url.d.ts.map +1 -1
- package/dist/Markdown/url.js +8 -2
- package/dist/TopNav/TopNavMenu.d.ts +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +3 -2
- package/dist/hooks/useClickableContainer.d.ts +10 -1
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +15 -4
- package/dist/utils/safeUrl.d.ts +11 -0
- package/dist/utils/safeUrl.d.ts.map +1 -0
- package/dist/utils/safeUrl.js +78 -0
- package/package.json +3 -3
- package/src/Button/Button.doc.mjs +1 -1
- package/src/Citation/Citation.doc.mjs +2 -2
- package/src/Citation/Citation.test.tsx +17 -0
- package/src/Citation/Citation.tsx +4 -2
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/CodeBlock/highlightRanges.test.ts +143 -1
- package/src/CodeBlock/highlightRanges.ts +15 -5
- package/src/Item/Item.doc.mjs +2 -2
- package/src/Link/Link.doc.mjs +6 -4
- package/src/Link/LinkProvider.doc.mjs +2 -2
- package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
- package/src/Link/useLinkComponent.test.tsx +333 -0
- package/src/Link/useLinkComponent.ts +29 -57
- package/src/List/ListItem.doc.mjs +2 -2
- package/src/Markdown/Markdown.doc.mjs +2 -2
- package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
- package/src/Markdown/Markdown.spec.md +19 -15
- package/src/Markdown/Markdown.test.tsx +52 -0
- package/src/Markdown/Markdown.tsx +2 -2
- package/src/Markdown/parser.test.ts +20 -0
- package/src/Markdown/url.ts +9 -3
- package/src/Token/Token.doc.mjs +2 -2
- package/src/TopNav/TopNavMenu.doc.mjs +2 -2
- package/src/TopNav/TopNavMenu.test.tsx +30 -0
- package/src/TopNav/TopNavMenu.tsx +7 -2
- package/src/hooks/useClickableContainer.test.tsx +236 -0
- package/src/hooks/useClickableContainer.ts +20 -4
- package/src/utils/safeUrl.test.ts +88 -0
- package/src/utils/safeUrl.ts +85 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Citation.tsx
|
|
3
|
-
* @input Uses React, StyleX, theme tokens
|
|
3
|
+
* @input Uses React, StyleX, theme tokens, and the shared navigation policy
|
|
4
4
|
* @output Exports Citation component for inline citation references
|
|
5
5
|
* @position Core implementation; consumed by index.ts
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Citation.d.ts","sourceRoot":"","sources":["../../src/Citation/Citation.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Citation.d.ts","sourceRoot":"","sources":["../../src/Citation/Citation.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAcrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC3D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC9B;AAiHD,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,MAAM,EACN,MAAM,EACN,OAAiB,EACjB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,KAAK,CAAC,YAAY,CA4FpC;yBAtGe,QAAQ"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Citation.tsx
|
|
7
|
-
* @input Uses React, StyleX, theme tokens
|
|
7
|
+
* @input Uses React, StyleX, theme tokens, and the shared navigation policy
|
|
8
8
|
* @output Exports Citation component for inline citation references
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
@@ -19,6 +19,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
19
19
|
import "../theme/tokens.stylex.js";
|
|
20
20
|
import { colorVars, spacingVars, radiusVars, typeScaleVars, fontWeightVars, borderVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
|
|
21
21
|
import { mergeProps } from "../utils/index.js";
|
|
22
|
+
import { isSafeUrl } from "../utils/safeUrl.js";
|
|
22
23
|
import { themeProps } from "../utils/themeProps.js";
|
|
23
24
|
import { useTranslator } from "../i18n/index.js";
|
|
24
25
|
import { renderIconSlot } from "../Icon/index.js";
|
|
@@ -103,7 +104,7 @@ export function Citation({
|
|
|
103
104
|
}) {
|
|
104
105
|
const t = useTranslator();
|
|
105
106
|
const title = source.title ?? String(number);
|
|
106
|
-
const href = source.url;
|
|
107
|
+
const href = source.url != null && isSafeUrl(source.url) ? source.url : undefined;
|
|
107
108
|
|
|
108
109
|
// Resolve the source icon. A non-string `icon` node renders as-is (an Astryx
|
|
109
110
|
// <Icon>, SVG, avatar, etc.). Otherwise fall back to an image URL: `src`, or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"highlightRanges.d.ts","sourceRoot":"","sources":["../../src/CodeBlock/highlightRanges.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAc,SAAS,EAAC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"highlightRanges.d.ts","sourceRoot":"","sources":["../../src/CodeBlock/highlightRanges.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAc,SAAS,EAAC,MAAM,aAAa,CAAC;AAwGxD,UAAU,UAAU;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;CACtB;AAgDD,iBAAS,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAIjD;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,SAAS,EAAE,GACtB,MAAM,IAAI,CAsHZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAChB,UAAU,EAAE,CAqBd;AAMD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,WAAW,EACf,UAAU,EAAE,SAAS,EAAE,GACtB,MAAM,IAAI,CA2DZ;AAeD;;GAEG;AACH,OAAO,EAAC,aAAa,EAAC,CAAC"}
|
|
@@ -52,9 +52,20 @@ function ensureDynamicHighlightType(tokenType) {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
// CSS.escape both generated names before they land in rule text: the
|
|
57
|
+
// registry key stays the raw string (a ::highlight() selector matches by
|
|
58
|
+
// ident VALUE, escapes and all), so dotted, non-ASCII, `_private`, and
|
|
59
|
+
// digit-led token types keep their colours — while nothing a token stream
|
|
60
|
+
// carries can step outside its ident and shape the rule.
|
|
61
|
+
const name = CSS.escape(`astryx-${tokenType}`);
|
|
62
|
+
const colorVar = `var(${CSS.escape(`--color-syntax-${tokenType}`)}, currentColor)`;
|
|
63
|
+
try {
|
|
64
|
+
dynamicStyleSheet.insertRule(`.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`);
|
|
65
|
+
} catch {
|
|
66
|
+
// An engine that still refuses the rule costs that type its colour
|
|
67
|
+
// (ranges paint with currentColor) — never the whole code block.
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
// ---------------------------------------------------------------------------
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import type { LinkComponentType } from './types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* When the resolved component is a custom component (not the native `<a>`),
|
|
8
|
-
* it is wrapped to receive both `href` and `to` props set to the same value.
|
|
9
|
-
* This allows `to`-based routers (React Router, TanStack Router) to work
|
|
10
|
-
* out of the box without a manual adapter.
|
|
11
|
-
*
|
|
12
|
-
* @param as - Per-component override. If provided, takes highest priority.
|
|
13
|
-
* @returns The resolved link component (with `to` injection for custom components).
|
|
3
|
+
* Resolve `as`, then LinkProvider, then a native anchor. Both destination props
|
|
4
|
+
* are checked before rendering; accepted structured values retain identity.
|
|
5
|
+
* Custom components receive `to={href}` unless an explicit `to` is supplied.
|
|
14
6
|
*
|
|
15
7
|
* @example
|
|
16
8
|
* ```
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* return <LinkComponent href="/foo">Click me</LinkComponent>;
|
|
20
|
-
* }
|
|
9
|
+
* const LinkComponent = useLinkComponent(as);
|
|
10
|
+
* return <LinkComponent href="/docs">Documentation</LinkComponent>;
|
|
21
11
|
* ```
|
|
22
12
|
*/
|
|
23
13
|
export declare function useLinkComponent(as?: LinkComponentType): LinkComponentType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLinkComponent.d.ts","sourceRoot":"","sources":["../../src/Link/useLinkComponent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useLinkComponent.d.ts","sourceRoot":"","sources":["../../src/Link/useLinkComponent.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,SAAS,CAAC;AA+B/C;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAI1E"}
|
|
@@ -4,79 +4,62 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useLinkComponent.ts
|
|
7
|
-
* @input
|
|
8
|
-
* @output
|
|
9
|
-
* @position
|
|
7
|
+
* @input Per-component override, LinkProvider context, and navigation props
|
|
8
|
+
* @output Stable link component with destination checks and custom-router `to` injection
|
|
9
|
+
* @position Shared native and custom link boundary; rejected destinations render
|
|
10
|
+
* as destination-less anchors without invoking a router.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* When the resolved component is a custom component (not native `<a>`),
|
|
14
|
-
* wraps it to pass `to={href}` alongside `href`. This enables compatibility
|
|
15
|
-
* with routers that use `to` (React Router, TanStack Router)
|
|
16
|
-
* without requiring an adapter component.
|
|
17
|
-
*
|
|
18
|
-
* SYNC: When modified, update these files to stay in sync:
|
|
19
|
-
* - /packages/core/src/Link/index.ts
|
|
20
|
-
* - /packages/core/src/Link/Link.doc.mjs
|
|
12
|
+
* SYNC: Link.doc.mjs and LinkProvider.doc.mjs describe this boundary.
|
|
21
13
|
*/
|
|
22
14
|
import { use, useMemo, createElement } from 'react';
|
|
23
15
|
import { LinkContext } from "./LinkContext.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* (React Router, TanStack Router) to work without an adapter.
|
|
28
|
-
*
|
|
29
|
-
* The wrapper is transparent: it forwards refs and all other props unchanged.
|
|
30
|
-
* Native `<a>` elements ignore the unknown `to` prop harmlessly.
|
|
31
|
-
*/
|
|
32
|
-
function createLinkWithTo(Component) {
|
|
33
|
-
function LinkWithTo({
|
|
16
|
+
import { isSafeDestination } from "../utils/safeUrl.js";
|
|
17
|
+
function createSafeLink(Component) {
|
|
18
|
+
function SafeLink({
|
|
34
19
|
href,
|
|
20
|
+
to,
|
|
35
21
|
ref,
|
|
36
22
|
...rest
|
|
37
23
|
}) {
|
|
24
|
+
// An undefined destination can crash a router or resolve to the current
|
|
25
|
+
// route. Reject the whole handoff, not just one prop or a fallback value.
|
|
26
|
+
if (!isSafeDestination(href) || !isSafeDestination(to)) {
|
|
27
|
+
return /*#__PURE__*/createElement('a', {
|
|
28
|
+
ref,
|
|
29
|
+
...rest
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (Component === 'a') {
|
|
33
|
+
return /*#__PURE__*/createElement('a', {
|
|
34
|
+
ref,
|
|
35
|
+
...rest,
|
|
36
|
+
href
|
|
37
|
+
});
|
|
38
|
+
}
|
|
38
39
|
return /*#__PURE__*/createElement(Component, {
|
|
39
40
|
ref,
|
|
41
|
+
...rest,
|
|
40
42
|
href,
|
|
41
|
-
to: href
|
|
42
|
-
...rest
|
|
43
|
+
to: to ?? href
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
return
|
|
46
|
+
SafeLink.displayName = `SafeLink(${typeof Component === 'string' ? Component : Component.displayName || Component.name || 'Component'})`;
|
|
47
|
+
return SafeLink;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* When the resolved component is a custom component (not the native `<a>`),
|
|
55
|
-
* it is wrapped to receive both `href` and `to` props set to the same value.
|
|
56
|
-
* This allows `to`-based routers (React Router, TanStack Router) to work
|
|
57
|
-
* out of the box without a manual adapter.
|
|
58
|
-
*
|
|
59
|
-
* @param as - Per-component override. If provided, takes highest priority.
|
|
60
|
-
* @returns The resolved link component (with `to` injection for custom components).
|
|
51
|
+
* Resolve `as`, then LinkProvider, then a native anchor. Both destination props
|
|
52
|
+
* are checked before rendering; accepted structured values retain identity.
|
|
53
|
+
* Custom components receive `to={href}` unless an explicit `to` is supplied.
|
|
61
54
|
*
|
|
62
55
|
* @example
|
|
63
56
|
* ```
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* return <LinkComponent href="/foo">Click me</LinkComponent>;
|
|
67
|
-
* }
|
|
57
|
+
* const LinkComponent = useLinkComponent(as);
|
|
58
|
+
* return <LinkComponent href="/docs">Documentation</LinkComponent>;
|
|
68
59
|
* ```
|
|
69
60
|
*/
|
|
70
61
|
export function useLinkComponent(as) {
|
|
71
62
|
const ctx = use(LinkContext);
|
|
72
63
|
const resolved = as ?? ctx?.component ?? 'a';
|
|
73
|
-
|
|
74
|
-
// Memoize the wrapper to maintain referential stability.
|
|
75
|
-
// The wrapper is only created for custom components (not native <a>).
|
|
76
|
-
return useMemo(() => {
|
|
77
|
-
if (resolved === 'a') {
|
|
78
|
-
return 'a';
|
|
79
|
-
}
|
|
80
|
-
return createLinkWithTo(resolved);
|
|
81
|
-
}, [resolved]);
|
|
64
|
+
return useMemo(() => createSafeLink(resolved), [resolved]);
|
|
82
65
|
}
|
|
@@ -35,7 +35,7 @@ import { parseInlineAst, parseMarkdownAst, parseMarkdownAstIncremental, createIn
|
|
|
35
35
|
import { getMarkdownAstLegacyCodeLanguage, markdownAstText } from "./ast.js";
|
|
36
36
|
import { applyMarkdownTransforms, getMarkdownExtensionRenderer, markdownExtensionText, prepareMarkdownPlugins, reportMarkdownPluginFailure } from "./plugins/protocol.js";
|
|
37
37
|
import { getMarkdownFenceProposal } from "./plugins/semanticFence.js";
|
|
38
|
-
import { sanitizeMarkdownUrl } from "./url.js";
|
|
38
|
+
import { sanitizeMarkdownLinkUrl, sanitizeMarkdownUrl } from "./url.js";
|
|
39
39
|
import { themeProps } from "../utils/themeProps.js";
|
|
40
40
|
import { useTranslator } from "../i18n/index.js";
|
|
41
41
|
|
|
@@ -668,7 +668,7 @@ function renderInline(node, index, onLinkClick, cursor, citationCtx, linkCompone
|
|
|
668
668
|
}
|
|
669
669
|
case 'link':
|
|
670
670
|
{
|
|
671
|
-
const safeHref =
|
|
671
|
+
const safeHref = sanitizeMarkdownLinkUrl(node.url);
|
|
672
672
|
if (safeHref == null) {
|
|
673
673
|
// Unsafe URL — render as plain text
|
|
674
674
|
return /*#__PURE__*/_jsx("span", {
|
package/dist/Markdown/url.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Preserve the released parser policy for no-plugin output compatibility. */
|
|
2
2
|
export declare function isSafeMarkdownParserUrl(url: string): boolean;
|
|
3
|
+
/** Navigation and image rendering deliberately have different policies. */
|
|
4
|
+
export declare function sanitizeMarkdownLinkUrl(url: string): string | null;
|
|
3
5
|
export declare function sanitizeMarkdownUrl(url: string): string | null;
|
|
4
6
|
//# sourceMappingURL=url.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/Markdown/url.ts"],"names":[],"mappings":"AAmBA,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D"}
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/Markdown/url.ts"],"names":[],"mappings":"AAmBA,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,2EAA2E;AAC3E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D"}
|
package/dist/Markdown/url.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @position Shared URL policy for parser, transforms, and renderer
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import { isSafeUrl, sanitizeUrl } from "../utils/safeUrl.js";
|
|
11
11
|
const RENDER_DANGEROUS_URL_PATTERN = /^(?:javascript:|data:|vbscript:)/i;
|
|
12
12
|
function normalizeMarkdownUrl(url) {
|
|
13
13
|
// Browsers ignore embedded controls in schemes, so normalize before checking.
|
|
@@ -17,7 +17,13 @@ function normalizeMarkdownUrl(url) {
|
|
|
17
17
|
|
|
18
18
|
/** Preserve the released parser policy for no-plugin output compatibility. */
|
|
19
19
|
export function isSafeMarkdownParserUrl(url) {
|
|
20
|
-
return
|
|
20
|
+
return isSafeUrl(url);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Navigation and image rendering deliberately have different policies. */
|
|
24
|
+
export function sanitizeMarkdownLinkUrl(url) {
|
|
25
|
+
const normalized = sanitizeUrl(url);
|
|
26
|
+
return normalized === '' ? null : normalized;
|
|
21
27
|
}
|
|
22
28
|
export function sanitizeMarkdownUrl(url) {
|
|
23
29
|
const normalized = normalizeMarkdownUrl(url);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file TopNavMenu.tsx
|
|
3
3
|
* @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
|
|
4
|
-
* useTypeahead, TopNavItem tokens
|
|
4
|
+
* useTypeahead, TopNavItem tokens, and the shared navigation policy
|
|
5
5
|
* @output Exports TopNavMenu component and related types
|
|
6
6
|
* @position Navigation item with hover-triggered overflow menu for TopNav
|
|
7
7
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAsM5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAMD,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,KAAK,EACL,KAAK,EACL,KAAW,EACX,SAAe,EACf,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,gBAAgB,EAC9B,YAAY,EAAE,gBAAgB,EAC9B,GAAG,IAAI,EACR,EAAE,eAAe,4BAyQjB;yBAtRe,UAAU"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file TopNavMenu.tsx
|
|
7
7
|
* @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
|
|
8
|
-
* useTypeahead, TopNavItem tokens
|
|
8
|
+
* useTypeahead, TopNavItem tokens, and the shared navigation policy
|
|
9
9
|
* @output Exports TopNavMenu component and related types
|
|
10
10
|
* @position Navigation item with hover-triggered overflow menu for TopNav
|
|
11
11
|
*
|
|
@@ -29,6 +29,7 @@ import { useTopNavSlot } from "./TopNavContext.js";
|
|
|
29
29
|
import { useTopNavRenderMode } from "./TopNavRenderContext.js";
|
|
30
30
|
import { useAppShellMobile } from "../AppShell/AppShellMobileContext.js";
|
|
31
31
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
32
|
+
import { isSafeUrl } from "../utils/safeUrl.js";
|
|
32
33
|
import { themeProps } from "../utils/themeProps.js";
|
|
33
34
|
import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
|
|
34
35
|
import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
|
|
@@ -378,7 +379,7 @@ export function TopNavMenu({
|
|
|
378
379
|
// promotes exactly one item to 0.
|
|
379
380
|
,
|
|
380
381
|
tabIndex: -1,
|
|
381
|
-
href: item.href,
|
|
382
|
+
href: item.href != null && isSafeUrl(item.href) ? item.href : undefined,
|
|
382
383
|
onClick: item.onClick,
|
|
383
384
|
...focusOutlineProps.focusVisible(styles.menuItem),
|
|
384
385
|
children: [/*#__PURE__*/_jsx("div", {
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* Solves the "nested interactive elements" problem: when a card is clickable
|
|
8
8
|
* but contains buttons/links, clicking those should NOT trigger the card's action.
|
|
9
9
|
*
|
|
10
|
+
* Every navigation exit here (window.open, location.href, and the delegated
|
|
11
|
+
* click on the interactive ref) runs after the shared navigation-destination
|
|
12
|
+
* rule in utils/safeUrl.ts; a blocked href does not navigate by any
|
|
13
|
+
* activation method.
|
|
14
|
+
*
|
|
10
15
|
* SYNC: When modified, update:
|
|
11
16
|
* - /packages/core/src/hooks/index.ts (export)
|
|
12
17
|
*/
|
|
@@ -30,7 +35,11 @@ export interface UseClickableContainerOptions {
|
|
|
30
35
|
interactiveRef?: RefObject<HTMLElement | null>;
|
|
31
36
|
/** Click handler */
|
|
32
37
|
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
33
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Navigation URL — when provided, clicking the container navigates. Checked
|
|
40
|
+
* with the shared navigation rule (utils/safeUrl.ts) before any activation
|
|
41
|
+
* method (plain, new-tab, Cmd/Ctrl-click, middle-click, delegated click).
|
|
42
|
+
*/
|
|
34
43
|
href?: string;
|
|
35
44
|
/** Link target */
|
|
36
45
|
target?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAC,MAAM,OAAO,CAAC;AAG9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAmBvB,CAAC;AAmCZ,MAAM,WAAW,4BAA4B;IAC3C,yCAAyC;IACzC,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC/C,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,cAAc,EACd,OAAO,EAAE,WAAW,EACpB,IAAI,EACJ,MAAM,EACN,QAAgB,GACjB,EAAE,4BAA4B,GAAG,wBAAwB,CAmHzD"}
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
* Solves the "nested interactive elements" problem: when a card is clickable
|
|
12
12
|
* but contains buttons/links, clicking those should NOT trigger the card's action.
|
|
13
13
|
*
|
|
14
|
+
* Every navigation exit here (window.open, location.href, and the delegated
|
|
15
|
+
* click on the interactive ref) runs after the shared navigation-destination
|
|
16
|
+
* rule in utils/safeUrl.ts; a blocked href does not navigate by any
|
|
17
|
+
* activation method.
|
|
18
|
+
*
|
|
14
19
|
* SYNC: When modified, update:
|
|
15
20
|
* - /packages/core/src/hooks/index.ts (export)
|
|
16
21
|
*/
|
|
17
22
|
import { useCallback, useEffect } from 'react';
|
|
23
|
+
import { isSafeUrl } from "../utils/safeUrl.js";
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Canonical list of interactive element selectors — native controls plus
|
|
@@ -128,8 +134,12 @@ export function useClickableContainer({
|
|
|
128
134
|
return;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
// Navigate if href is provided
|
|
132
|
-
|
|
137
|
+
// Navigate if href is provided. React DOM vets the hrefs it writes as
|
|
138
|
+
// attributes; nothing below passes through React, so the shared
|
|
139
|
+
// navigation rule decides once, before target, modifier key, or the
|
|
140
|
+
// delegated anchor can select a sink. A blocked href simply does not
|
|
141
|
+
// navigate — the click handler above still ran.
|
|
142
|
+
if (href != null && isSafeUrl(href)) {
|
|
133
143
|
const shouldOpenNewTab = target === '_blank' || event.ctrlKey || event.metaKey;
|
|
134
144
|
if (shouldOpenNewTab) {
|
|
135
145
|
window.open(href, '_blank', 'noopener');
|
|
@@ -171,9 +181,10 @@ export function useClickableContainer({
|
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
// Middle-click on href opens in new tab
|
|
184
|
+
// Middle-click on href opens in new tab (same scheme rule as onClick —
|
|
185
|
+
// this navigation never passes through React either).
|
|
175
186
|
const isMiddleClick = event.button === 1;
|
|
176
|
-
if (isMiddleClick && href != null && (eventTarget === event.currentTarget || !hasInteractiveAncestor(eventTarget, containerEl))) {
|
|
187
|
+
if (isMiddleClick && href != null && isSafeUrl(href) && (eventTarget === event.currentTarget || !hasInteractiveAncestor(eventTarget, containerEl))) {
|
|
177
188
|
window.open(href, '_blank', 'noopener');
|
|
178
189
|
}
|
|
179
190
|
}, [containerRef, href, disabled]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Return a normalized navigation URL, or null for a blocked scheme. */
|
|
2
|
+
export declare function sanitizeUrl(url: string): string | null;
|
|
3
|
+
/** Inspect a navigation string without changing the value used by its sink. */
|
|
4
|
+
export declare function isSafeUrl(url: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Inspect React Router paths, Next.js URL objects, and URL instances without
|
|
7
|
+
* serializing or replacing the object handed to the router. Search/query/hash
|
|
8
|
+
* are route-relative data, not schemes. Absent destinations remain absent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSafeDestination(destination: unknown): boolean;
|
|
11
|
+
//# sourceMappingURL=safeUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safeUrl.d.ts","sourceRoot":"","sources":["../../src/utils/safeUrl.ts"],"names":[],"mappings":"AAcA,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWtD;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CA8C/D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file safeUrl.ts
|
|
5
|
+
* @input Navigation strings or supported structured router destinations
|
|
6
|
+
* @output The shared blocked-scheme decision, without rewriting accepted inputs
|
|
7
|
+
* @position Core navigation policy; embedded-resource policy is separate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function normalizeUrl(url) {
|
|
11
|
+
// eslint-disable-next-line no-control-regex -- inspect schemes without browser-ignored controls
|
|
12
|
+
return url.replace(/[\x00-\x1f\x7f]/g, '').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Return a normalized navigation URL, or null for a blocked scheme. */
|
|
16
|
+
export function sanitizeUrl(url) {
|
|
17
|
+
const normalized = normalizeUrl(url);
|
|
18
|
+
const lower = normalized.toLowerCase();
|
|
19
|
+
if (lower.startsWith('javascript:') || lower.startsWith('vbscript:') || lower.startsWith('data:text/html')) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Inspect a navigation string without changing the value used by its sink. */
|
|
26
|
+
export function isSafeUrl(url) {
|
|
27
|
+
return sanitizeUrl(url) !== null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inspect React Router paths, Next.js URL objects, and URL instances without
|
|
32
|
+
* serializing or replacing the object handed to the router. Search/query/hash
|
|
33
|
+
* are route-relative data, not schemes. Absent destinations remain absent.
|
|
34
|
+
*/
|
|
35
|
+
export function isSafeDestination(destination) {
|
|
36
|
+
if (destination == null) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (typeof destination === 'string') {
|
|
40
|
+
return isSafeUrl(destination);
|
|
41
|
+
}
|
|
42
|
+
if (typeof destination !== 'object') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const {
|
|
46
|
+
pathname,
|
|
47
|
+
href,
|
|
48
|
+
protocol,
|
|
49
|
+
host,
|
|
50
|
+
hostname
|
|
51
|
+
} = destination;
|
|
52
|
+
for (const field of [pathname, href, protocol, host, hostname]) {
|
|
53
|
+
if (field != null && typeof field !== 'string') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof pathname === 'string' && !isSafeUrl(pathname)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (typeof href === 'string' && !isSafeUrl(href)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof protocol === 'string' && protocol !== '') {
|
|
64
|
+
// URL-object formatters accept a protocol with or without its colon and
|
|
65
|
+
// can assemble a data media type from the host and pathname fields.
|
|
66
|
+
const normalizedProtocol = normalizeUrl(protocol);
|
|
67
|
+
const prefix = normalizedProtocol.endsWith(':') ? normalizedProtocol : `${normalizedProtocol}:`;
|
|
68
|
+
if (!isSafeUrl(prefix)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const body = host || hostname || '';
|
|
72
|
+
const path = typeof pathname === 'string' ? pathname : '';
|
|
73
|
+
if (!isSafeUrl(`${prefix}${body}${path}`)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.6.3-canary.
|
|
3
|
+
"version": "0.6.3-canary.9e4c545",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -692,8 +692,8 @@
|
|
|
692
692
|
"react-dom": ">=19.0.0"
|
|
693
693
|
},
|
|
694
694
|
"devDependencies": {
|
|
695
|
-
"@astryxdesign/a11y-spec": "0.6.3-canary.
|
|
696
|
-
"@astryxdesign/cli": "0.6.3-canary.
|
|
695
|
+
"@astryxdesign/a11y-spec": "0.6.3-canary.9e4c545",
|
|
696
|
+
"@astryxdesign/cli": "0.6.3-canary.9e4c545",
|
|
697
697
|
"@babel/cli": "^7.29.7",
|
|
698
698
|
"@babel/core": "^7.29.7",
|
|
699
699
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -210,7 +210,7 @@ export const docs = {
|
|
|
210
210
|
name: 'href',
|
|
211
211
|
type: 'string',
|
|
212
212
|
description:
|
|
213
|
-
'When provided, renders the button as a link element (<a> or custom link component).',
|
|
213
|
+
'When provided, renders the button as a link element (<a> or custom link component). The destination follows the shared navigation rule described on the Link `href` prop.',
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
name: 'as',
|
|
@@ -71,7 +71,7 @@ export const docs = {
|
|
|
71
71
|
name: 'source',
|
|
72
72
|
type: 'CitationSource',
|
|
73
73
|
description:
|
|
74
|
-
'The citation source object containing title, url, an optional image src, and an optional icon node.',
|
|
74
|
+
'The citation source object containing title, url, an optional image src, and an optional icon node. The url follows the shared navigation rule described on the Link `href` prop; rejected destinations leave the citation visible without navigation. Image src uses separate resource handling.',
|
|
75
75
|
required: true,
|
|
76
76
|
},
|
|
77
77
|
{
|
|
@@ -122,7 +122,7 @@ export const docsDense = {
|
|
|
122
122
|
},
|
|
123
123
|
propDescriptions: {
|
|
124
124
|
source:
|
|
125
|
-
'citation source
|
|
125
|
+
'citation source with title, url, optional image src, and optional icon. url follows the Link href navigation rule; rejected destinations stay visible without navigation. Image src handling is separate.',
|
|
126
126
|
number: 'display index for this citation.',
|
|
127
127
|
variant: 'display style: label chip with source title or compact numbered badge.',
|
|
128
128
|
},
|
|
@@ -36,6 +36,23 @@ function atomicClasses(style: (typeof probe)[keyof typeof probe]): string[] {
|
|
|
36
36
|
describe('Citation', () => {
|
|
37
37
|
const source = {title: 'Example Source', url: 'https://example.com'};
|
|
38
38
|
|
|
39
|
+
it.each([
|
|
40
|
+
'javascript:alert(1)',
|
|
41
|
+
'vbscript:MsgBox(1)',
|
|
42
|
+
'data:text/html,<b>x</b>',
|
|
43
|
+
'java\nscript:alert(1)',
|
|
44
|
+
])('renders rejected citation URL %s without navigation', url => {
|
|
45
|
+
const {container} = render(
|
|
46
|
+
<>
|
|
47
|
+
<Citation source={{title: 'Source', url}} number={1} />
|
|
48
|
+
<Citation source={{title: 'Source', url}} number={1} variant="number" />
|
|
49
|
+
</>,
|
|
50
|
+
);
|
|
51
|
+
expect(container.querySelector('a')).toBeNull();
|
|
52
|
+
expect(container.querySelector('[href]')).toBeNull();
|
|
53
|
+
expect(container.textContent).toBe('Source1');
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
it('renders the source title as a link in the label variant', () => {
|
|
40
57
|
render(<Citation source={source} number={1} data-testid="citation" />);
|
|
41
58
|
const el = screen.getByTestId('citation');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Citation.tsx
|
|
7
|
-
* @input Uses React, StyleX, theme tokens
|
|
7
|
+
* @input Uses React, StyleX, theme tokens, and the shared navigation policy
|
|
8
8
|
* @output Exports Citation component for inline citation references
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
easeVars,
|
|
31
31
|
} from '../theme/tokens.stylex';
|
|
32
32
|
import {mergeProps} from '../utils';
|
|
33
|
+
import {isSafeUrl} from '../utils/safeUrl';
|
|
33
34
|
import type {BaseProps} from '../BaseProps';
|
|
34
35
|
import {themeProps} from '../utils/themeProps';
|
|
35
36
|
import {useTranslator} from '../i18n';
|
|
@@ -189,7 +190,8 @@ export function Citation({
|
|
|
189
190
|
}: CitationProps): React.ReactElement {
|
|
190
191
|
const t = useTranslator();
|
|
191
192
|
const title = source.title ?? String(number);
|
|
192
|
-
const href =
|
|
193
|
+
const href =
|
|
194
|
+
source.url != null && isSafeUrl(source.url) ? source.url : undefined;
|
|
193
195
|
|
|
194
196
|
// Resolve the source icon. A non-string `icon` node renders as-is (an Astryx
|
|
195
197
|
// <Icon>, SVG, avatar, etc.). Otherwise fall back to an image URL: `src`, or
|
|
@@ -22,7 +22,7 @@ export const docs = {
|
|
|
22
22
|
props: [
|
|
23
23
|
{name: 'label', type: 'string', description: 'Accessibility label.', required: true},
|
|
24
24
|
{name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler: fires on card surface only.'},
|
|
25
|
-
{name: 'href', type: 'string', description: 'Navigation URL.'},
|
|
25
|
+
{name: 'href', type: 'string', description: 'Navigation URL. Plain, new-tab, Cmd/Ctrl-click, and middle-click activation all follow the shared navigation rule described on the Link `href` prop.'},
|
|
26
26
|
{name: 'target', type: 'string', description: 'Link target.', default: "'_self'"},
|
|
27
27
|
{name: 'isDisabled', type: 'boolean', description: 'Disables the card.', default: 'false'},
|
|
28
28
|
{name: 'children', type: 'ReactNode', description: 'Card content.'},
|
|
@@ -69,7 +69,7 @@ export const docsDense = {
|
|
|
69
69
|
propDescriptions: {
|
|
70
70
|
label: 'accessibility label',
|
|
71
71
|
onClick: 'click handler: fires on card surface only',
|
|
72
|
-
href: 'navigation URL',
|
|
72
|
+
href: 'navigation URL; every activation follows the shared navigation rule (see Link href)',
|
|
73
73
|
target: 'link target',
|
|
74
74
|
isDisabled: 'disables card',
|
|
75
75
|
padding: 'inner padding',
|