@astryxdesign/core 0.6.3-canary.bcf01fe → 0.6.3-canary.c13ef6b
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/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/theme/declarationBoundary.d.ts +17 -0
- package/dist/theme/declarationBoundary.d.ts.map +1 -0
- package/dist/theme/declarationBoundary.js +343 -0
- package/dist/theme/generateThemeRules.d.ts +13 -7
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +131 -35
- 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/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/theme/Theme.test.tsx +32 -0
- package/src/theme/declarationBoundary.test.ts +126 -0
- package/src/theme/declarationBoundary.ts +347 -0
- package/src/theme/generateThemeRules.test.ts +470 -2
- package/src/theme/generateThemeRules.ts +219 -51
- package/src/theme/tokenValueCompat.test.ts +4 -1
- 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,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,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why `value` cannot be emitted as the value of one declaration, or `null`
|
|
3
|
+
* when it can. Valid CSS a browser keeps inside one declaration always
|
|
4
|
+
* returns `null`; see the file header for the exact rules.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkDeclarationValue(input: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Why `name` cannot be emitted as a declaration's property name, or `null`
|
|
9
|
+
* when it can. A property name is one CSS ident sequence: a standard
|
|
10
|
+
* property (`color`), a vendor-prefixed one (`-webkit-line-clamp`), or a
|
|
11
|
+
* custom property (`--brand-accent`, `--_internal`). Escapes are allowed
|
|
12
|
+
* because an ident allows them. Whether the name means anything to the
|
|
13
|
+
* browser is not this check's concern; only that it cannot end the
|
|
14
|
+
* declaration or leak a second one.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkDeclarationName(input: string): string | null;
|
|
17
|
+
//# sourceMappingURL=declarationBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declarationBoundary.d.ts","sourceRoot":"","sources":["../../src/theme/declarationBoundary.ts"],"names":[],"mappings":"AAgOA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgGlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUjE"}
|