@astryxdesign/core 0.1.0 → 0.1.1-canary.129bf0e
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/CHANGELOG.md +66 -0
- package/README.md +68 -0
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Banner/Banner.d.ts +7 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +9 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -0
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +5 -1
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/DropdownMenu/DropdownMenu.js +2 -2
- package/dist/DropdownMenu/{renderXDSDropdownItems.d.ts → renderDropdownItems.d.ts} +3 -3
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -0
- package/dist/DropdownMenu/{renderXDSDropdownItems.js → renderDropdownItems.js} +2 -2
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +7 -1
- package/dist/HoverCard/HoverCard.d.ts +2 -2
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +18 -6
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -3
- package/dist/Layer/useLayer.d.ts +13 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +7 -2
- package/dist/Layout/Layout.d.ts +10 -1
- package/dist/Layout/Layout.d.ts.map +1 -1
- package/dist/Layout/Layout.js +5 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +13 -3
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +13 -0
- package/dist/Outline/Outline.d.ts +3 -2
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +23 -4
- package/dist/Outline/useScrollSpy.d.ts +14 -1
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +161 -50
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +31 -27
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +1 -5
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +1 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +26 -8
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +30 -7
- package/dist/Table/index.d.ts +3 -1
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
- package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
- package/dist/Table/plugins/stickyColumns/index.js +3 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +376 -0
- package/dist/Table/types.d.ts +90 -5
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +1 -1
- package/dist/ToggleButton/ToggleButton.d.ts +10 -3
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +64 -18
- package/dist/astryx.css +11 -0
- package/dist/astryx.umd.js +147 -0
- package/dist/astryx.umd.js.map +7 -0
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +1 -1
- package/dist/theme/tokens.d.ts +1 -1
- package/dist/theme/tokens.js +4 -4
- package/dist/theme/useTheme.d.ts +2 -2
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +15 -2
- package/package.json +7 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
- package/src/Banner/Banner.test.tsx +16 -7
- package/src/Banner/Banner.tsx +9 -2
- package/src/Button/Button.test.tsx +26 -11
- package/src/Button/Button.tsx +2 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
- package/src/Collapsible/useCollapsible.doc.mjs +2 -2
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +68 -20
- package/src/Divider/Divider.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenu.tsx +2 -2
- package/src/DropdownMenu/{renderXDSDropdownItems.tsx → renderDropdownItems.tsx} +2 -2
- package/src/EmptyState/EmptyState.test.tsx +4 -2
- package/src/EmptyState/EmptyState.tsx +6 -2
- package/src/FormLayout/FormLayout.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +3 -0
- package/src/HoverCard/HoverCard.test.tsx +178 -2
- package/src/HoverCard/HoverCard.tsx +20 -16
- package/src/HoverCard/useHoverCard.tsx +12 -10
- package/src/Icon/Icon.doc.mjs +4 -4
- package/src/Item/Item.doc.mjs +2 -2
- package/src/Layer/useLayer.doc.mjs +7 -2
- package/src/Layer/useLayer.tsx +19 -2
- package/src/Layout/Layout.doc.mjs +2 -1
- package/src/Layout/Layout.tsx +15 -1
- package/src/Layout/__tests__/childrenAsContent.test.tsx +59 -0
- package/src/Lightbox/Lightbox.doc.mjs +0 -2
- package/src/Link/Link.doc.mjs +3 -3
- package/src/Link/LinkProvider.doc.mjs +3 -3
- package/src/Markdown/Markdown.doc.mjs +6 -4
- package/src/Markdown/Markdown.test.tsx +17 -26
- package/src/Markdown/Markdown.tsx +16 -6
- package/src/MobileNav/MobileNav.doc.mjs +8 -8
- package/src/MobileNav/MobileNav.tsx +13 -0
- package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
- package/src/Outline/Outline.doc.mjs +1 -1
- package/src/Outline/Outline.test.tsx +76 -38
- package/src/Outline/Outline.tsx +23 -4
- package/src/Outline/useScrollSpy.ts +196 -63
- package/src/Pagination/Pagination.test.tsx +137 -13
- package/src/Pagination/Pagination.tsx +33 -28
- package/src/Resizable/Resizable.doc.mjs +3 -3
- package/src/Resizable/useResizable.ts +1 -7
- package/src/Selector/Selector.doc.mjs +4 -0
- package/src/Selector/Selector.tsx +5 -6
- package/src/Skeleton/Skeleton.doc.mjs +11 -1
- package/src/Table/BaseTable.tsx +50 -24
- package/src/Table/Table.doc.mjs +3 -3
- package/src/Table/Table.tsx +22 -1
- package/src/Table/index.ts +3 -0
- package/src/Table/plugins/stickyColumns/index.ts +4 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +414 -0
- package/src/Table/types.ts +96 -4
- package/src/Table/useBaseTablePlugins.ts +1 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +2 -2
- package/src/ToggleButton/ToggleButton.test.tsx +148 -6
- package/src/ToggleButton/ToggleButton.tsx +83 -20
- package/src/Toolbar/Toolbar.doc.mjs +1 -1
- package/src/hooks/useEntryAnimation.doc.mjs +3 -3
- package/src/hooks/useMediaQuery.doc.mjs +2 -2
- package/src/hooks/useStreamingText.doc.mjs +3 -3
- package/src/theme/Theme.doc.mjs +2 -2
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/index.ts +1 -1
- package/src/theme/syntax/defineSyntaxTheme.ts +1 -1
- package/src/theme/tokens.ts +4 -4
- package/src/theme/useTheme.ts +2 -2
- package/src/utils/dateParser.test.ts +26 -0
- package/src/utils/dateParser.ts +16 -2
- package/dist/DropdownMenu/renderXDSDropdownItems.d.ts.map +0 -1
|
@@ -30,7 +30,6 @@ export const docs = {
|
|
|
30
30
|
name: 'index',
|
|
31
31
|
type: 'number',
|
|
32
32
|
description: 'Current index in gallery mode (when media is an array).',
|
|
33
|
-
default: '0',
|
|
34
33
|
},
|
|
35
34
|
{
|
|
36
35
|
name: 'onIndexChange',
|
|
@@ -94,7 +93,6 @@ export const docsZh = {
|
|
|
94
93
|
name: 'index',
|
|
95
94
|
type: 'number',
|
|
96
95
|
description: '画廊模式中当前索引。',
|
|
97
|
-
default: '0',
|
|
98
96
|
},
|
|
99
97
|
{
|
|
100
98
|
name: 'onIndexChange',
|
package/src/Link/Link.doc.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export const docs = {
|
|
|
99
99
|
isHiddenFromOverview: true,
|
|
100
100
|
displayName: 'Link Provider',
|
|
101
101
|
description:
|
|
102
|
-
'Provider that sets the default link component for all
|
|
102
|
+
'Provider that sets the default link component for all Astryx link-rendering components in the subtree. ' +
|
|
103
103
|
'Wrap your app root to replace native <a> elements with your framework router (Next.js Link, React Router Link, etc.).',
|
|
104
104
|
props: [
|
|
105
105
|
{
|
|
@@ -226,7 +226,7 @@ export const docsZh = {
|
|
|
226
226
|
isHiddenFromOverview: true,
|
|
227
227
|
displayName: 'Link Provider',
|
|
228
228
|
description:
|
|
229
|
-
'为子树中所有
|
|
229
|
+
'为子树中所有 Astryx 链接组件设置默认链接组件的 Provider。',
|
|
230
230
|
props: [
|
|
231
231
|
{
|
|
232
232
|
name: 'component',
|
|
@@ -310,7 +310,7 @@ export const docsDense = {
|
|
|
310
310
|
isHiddenFromOverview: true,
|
|
311
311
|
displayName: 'Link Provider',
|
|
312
312
|
description:
|
|
313
|
-
'Provider setting default link component for all
|
|
313
|
+
'Provider setting default link component for all Astryx links in subtree.',
|
|
314
314
|
propDescriptions: {
|
|
315
315
|
component: 'Component for all link elements',
|
|
316
316
|
children: 'Subtree',
|
|
@@ -10,7 +10,7 @@ export const docs = {
|
|
|
10
10
|
isHiddenFromOverview: true,
|
|
11
11
|
keywords: ['link', 'provider', 'router', 'nextjs', 'client-side-routing'],
|
|
12
12
|
usage: {
|
|
13
|
-
description: 'Wraps your app to replace the default <a> tag with a framework-specific link component (e.g. Next.js Link) for client-side routing across all
|
|
13
|
+
description: 'Wraps your app to replace the default <a> tag with a framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
|
|
14
14
|
},
|
|
15
15
|
props: [
|
|
16
16
|
{name: 'component', type: 'LinkComponentType', required: true, description: 'Link component to use for all link elements in the subtree (e.g. Next.js Link).'},
|
|
@@ -20,9 +20,9 @@ export const docs = {
|
|
|
20
20
|
|
|
21
21
|
/** @type {import('../docs-types').TranslationDoc} */
|
|
22
22
|
export const docsDense = {
|
|
23
|
-
description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all
|
|
23
|
+
description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
|
|
24
24
|
usage: {
|
|
25
|
-
description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all
|
|
25
|
+
description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
|
|
26
26
|
},
|
|
27
27
|
propDescriptions: {
|
|
28
28
|
component: 'link component for all link elements in subtree (e.g. Next.js Link)',
|
|
@@ -75,6 +75,7 @@ export const docs = {
|
|
|
75
75
|
type: 'number | string',
|
|
76
76
|
description:
|
|
77
77
|
'Max width for prose content (paragraphs, headings, lists, blockquotes). Tables and code blocks are unconstrained and can expand to the full container width. Use for readable line lengths in wide layouts.',
|
|
78
|
+
default: '680',
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
81
|
name: 'contentAlign',
|
|
@@ -131,7 +132,7 @@ export const docs = {
|
|
|
131
132
|
},
|
|
132
133
|
usage: {
|
|
133
134
|
description:
|
|
134
|
-
'Renders a markdown string as
|
|
135
|
+
'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
|
|
135
136
|
bestPractices: [
|
|
136
137
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
137
138
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
@@ -245,6 +246,7 @@ export const docsZh = {
|
|
|
245
246
|
type: 'number | string',
|
|
246
247
|
description:
|
|
247
248
|
'正文内容的最大宽度(段落、标题、列表、引用块)。表格和代码块不受限制,可扩展到完整容器宽度。用于在宽布局中保持可读行长。',
|
|
249
|
+
default: '680',
|
|
248
250
|
},
|
|
249
251
|
{
|
|
250
252
|
name: 'contentAlign',
|
|
@@ -294,7 +296,7 @@ export const docsZh = {
|
|
|
294
296
|
},
|
|
295
297
|
usage: {
|
|
296
298
|
description:
|
|
297
|
-
'Renders a markdown string as
|
|
299
|
+
'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
|
|
298
300
|
bestPractices: [
|
|
299
301
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
300
302
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
@@ -306,10 +308,10 @@ export const docsZh = {
|
|
|
306
308
|
|
|
307
309
|
export const docsDense = {
|
|
308
310
|
description:
|
|
309
|
-
'Renders markdown string as
|
|
311
|
+
'Renders markdown string as Astryx-styled components. Use for user-generated content, AI responses, docs. Headings, lists, tables, code, citations w/ consistent styling.',
|
|
310
312
|
usage: {
|
|
311
313
|
description:
|
|
312
|
-
'Renders a markdown string as
|
|
314
|
+
'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
|
|
313
315
|
bestPractices: [
|
|
314
316
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
315
317
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
@@ -22,9 +22,16 @@ describe('Markdown', () => {
|
|
|
22
22
|
expect(screen.getByText('Heading 2').tagName).toBe('H2');
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('renders paragraphs', () => {
|
|
25
|
+
it('renders paragraphs as block <div> (never <p>) for composition safety', () => {
|
|
26
26
|
render(<Markdown>{'Hello world'}</Markdown>);
|
|
27
|
-
|
|
27
|
+
// Markdown paragraphs render as <div> so block-level inline content
|
|
28
|
+
// (images, custom inline components) never trips the phrasing-content
|
|
29
|
+
// trap that a <p> would impose. role="paragraph" re-exposes the paragraph
|
|
30
|
+
// role to assistive tech without the <p> hazard. Consumers who want a real
|
|
31
|
+
// <p> element can pass `components={{paragraph: 'p'}}`.
|
|
32
|
+
const para = screen.getByText('Hello world');
|
|
33
|
+
expect(para.tagName).toBe('DIV');
|
|
34
|
+
expect(para).toHaveAttribute('role', 'paragraph');
|
|
28
35
|
});
|
|
29
36
|
|
|
30
37
|
it('renders inline display without block wrappers', () => {
|
|
@@ -201,9 +208,7 @@ describe('Markdown', () => {
|
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
it('shows streaming cursor when isStreaming is true', () => {
|
|
204
|
-
const {container} = render(
|
|
205
|
-
<Markdown isStreaming>{'Hello'}</Markdown>,
|
|
206
|
-
);
|
|
211
|
+
const {container} = render(<Markdown isStreaming>{'Hello'}</Markdown>);
|
|
207
212
|
// Streaming mode parses incrementally but no cursor element
|
|
208
213
|
expect(container.querySelector('[role="document"]')).toBeInTheDocument();
|
|
209
214
|
});
|
|
@@ -243,9 +248,7 @@ describe('Markdown', () => {
|
|
|
243
248
|
|
|
244
249
|
it('sanitizes data: URLs in images', () => {
|
|
245
250
|
const {container} = render(
|
|
246
|
-
<Markdown>
|
|
247
|
-
{'</script>)'}
|
|
248
|
-
</Markdown>,
|
|
251
|
+
<Markdown>{'</script>)'}</Markdown>,
|
|
249
252
|
);
|
|
250
253
|
const img = container.querySelector('img');
|
|
251
254
|
expect(img).toBeNull();
|
|
@@ -435,9 +438,7 @@ describe('inlinePlugins', () => {
|
|
|
435
438
|
},
|
|
436
439
|
};
|
|
437
440
|
const {container} = render(
|
|
438
|
-
<Markdown inlinePlugins={[plugin]}>
|
|
439
|
-
{'See TAG:important here'}
|
|
440
|
-
</Markdown>,
|
|
441
|
+
<Markdown inlinePlugins={[plugin]}>{'See TAG:important here'}</Markdown>,
|
|
441
442
|
);
|
|
442
443
|
const tag = container.querySelector('[data-testid="tag-match"]');
|
|
443
444
|
expect(tag).toBeInTheDocument();
|
|
@@ -446,9 +447,7 @@ describe('inlinePlugins', () => {
|
|
|
446
447
|
|
|
447
448
|
it('renders identically when no inlinePlugins are provided', () => {
|
|
448
449
|
const withPlugins = render(
|
|
449
|
-
<Markdown inlinePlugins={[]}>
|
|
450
|
-
{'Hello **world** and `code`'}
|
|
451
|
-
</Markdown>,
|
|
450
|
+
<Markdown inlinePlugins={[]}>{'Hello **world** and `code`'}</Markdown>,
|
|
452
451
|
);
|
|
453
452
|
const withoutPlugins = render(
|
|
454
453
|
<Markdown>{'Hello **world** and `code`'}</Markdown>,
|
|
@@ -481,9 +480,7 @@ describe('inlinePlugins', () => {
|
|
|
481
480
|
|
|
482
481
|
it('renders bare https URLs as links when autolink="gfm"', () => {
|
|
483
482
|
const {container} = render(
|
|
484
|
-
<Markdown autolink="gfm">
|
|
485
|
-
{'see https://example.com here'}
|
|
486
|
-
</Markdown>,
|
|
483
|
+
<Markdown autolink="gfm">{'see https://example.com here'}</Markdown>,
|
|
487
484
|
);
|
|
488
485
|
const link = container.querySelector('a');
|
|
489
486
|
expect(link).not.toBeNull();
|
|
@@ -503,9 +500,7 @@ describe('inlinePlugins', () => {
|
|
|
503
500
|
|
|
504
501
|
it('renders bare emails with mailto: href', () => {
|
|
505
502
|
const {container} = render(
|
|
506
|
-
<Markdown autolink="gfm">
|
|
507
|
-
{'ping user@example.com please'}
|
|
508
|
-
</Markdown>,
|
|
503
|
+
<Markdown autolink="gfm">{'ping user@example.com please'}</Markdown>,
|
|
509
504
|
);
|
|
510
505
|
const link = container.querySelector('a');
|
|
511
506
|
expect(link).not.toBeNull();
|
|
@@ -515,9 +510,7 @@ describe('inlinePlugins', () => {
|
|
|
515
510
|
|
|
516
511
|
it('does not autolink URLs inside code spans', () => {
|
|
517
512
|
const {container} = render(
|
|
518
|
-
<Markdown autolink="gfm">
|
|
519
|
-
{'try `https://example.com` here'}
|
|
520
|
-
</Markdown>,
|
|
513
|
+
<Markdown autolink="gfm">{'try `https://example.com` here'}</Markdown>,
|
|
521
514
|
);
|
|
522
515
|
expect(container.querySelector('a')).toBeNull();
|
|
523
516
|
expect(container.querySelector('code')).not.toBeNull();
|
|
@@ -525,9 +518,7 @@ describe('inlinePlugins', () => {
|
|
|
525
518
|
|
|
526
519
|
it('does not autolink URLs inside code blocks', () => {
|
|
527
520
|
const {container} = render(
|
|
528
|
-
<Markdown autolink="gfm">
|
|
529
|
-
{'```\nhttps://example.com\n```'}
|
|
530
|
-
</Markdown>,
|
|
521
|
+
<Markdown autolink="gfm">{'```\nhttps://example.com\n```'}</Markdown>,
|
|
531
522
|
);
|
|
532
523
|
expect(container.querySelector('a')).toBeNull();
|
|
533
524
|
expect(container.querySelector('pre')).not.toBeNull();
|
|
@@ -1113,9 +1113,19 @@ function renderBlock(
|
|
|
1113
1113
|
if (ParagraphComp) {
|
|
1114
1114
|
return <ParagraphComp key={index}>{paraChildren}</ParagraphComp>;
|
|
1115
1115
|
}
|
|
1116
|
+
// Markdown paragraphs render as <div>, not <p>: inline content can
|
|
1117
|
+
// include block-level nodes (images, custom inline components), and a
|
|
1118
|
+
// <p> would reparent them, desyncing SSR markup from the hydrated DOM.
|
|
1119
|
+
// Block spacing comes from token-based StyleX margins, so the rendered
|
|
1120
|
+
// appearance is unchanged. role="paragraph" re-exposes the paragraph
|
|
1121
|
+
// role in the accessibility tree (a pure ARIA hint — it does not trigger
|
|
1122
|
+
// the parser's block-child reparenting) so prose semantics are preserved
|
|
1123
|
+
// without the <p> composition hazard. Consumers who want a real <p>
|
|
1124
|
+
// element can still pass components={{paragraph: 'p'}}.
|
|
1116
1125
|
return (
|
|
1117
|
-
<
|
|
1126
|
+
<div
|
|
1118
1127
|
key={index}
|
|
1128
|
+
role="paragraph"
|
|
1119
1129
|
{...stylex.props(
|
|
1120
1130
|
spacing,
|
|
1121
1131
|
contentWidthValue != null
|
|
@@ -1128,7 +1138,7 @@ function renderBlock(
|
|
|
1128
1138
|
isLast && styles.noMarginBlockEnd,
|
|
1129
1139
|
)}>
|
|
1130
1140
|
{paraChildren}
|
|
1131
|
-
</
|
|
1141
|
+
</div>
|
|
1132
1142
|
);
|
|
1133
1143
|
}
|
|
1134
1144
|
case 'codeblock': {
|
|
@@ -1487,7 +1497,7 @@ function renderBlock(
|
|
|
1487
1497
|
const safeSrc = sanitizeUrl(node.src);
|
|
1488
1498
|
if (safeSrc == null) {
|
|
1489
1499
|
return (
|
|
1490
|
-
<
|
|
1500
|
+
<div
|
|
1491
1501
|
key={index}
|
|
1492
1502
|
{...stylex.props(
|
|
1493
1503
|
spacing,
|
|
@@ -1495,11 +1505,11 @@ function renderBlock(
|
|
|
1495
1505
|
isLast && styles.noMarginBlockEnd,
|
|
1496
1506
|
)}>
|
|
1497
1507
|
[{node.alt}]
|
|
1498
|
-
</
|
|
1508
|
+
</div>
|
|
1499
1509
|
);
|
|
1500
1510
|
}
|
|
1501
1511
|
return (
|
|
1502
|
-
<
|
|
1512
|
+
<div
|
|
1503
1513
|
key={index}
|
|
1504
1514
|
{...stylex.props(
|
|
1505
1515
|
spacing,
|
|
@@ -1507,7 +1517,7 @@ function renderBlock(
|
|
|
1507
1517
|
isLast && styles.noMarginBlockEnd,
|
|
1508
1518
|
)}>
|
|
1509
1519
|
<img src={safeSrc} alt={node.alt} {...stylex.props(styles.image)} />
|
|
1510
|
-
</
|
|
1520
|
+
</div>
|
|
1511
1521
|
);
|
|
1512
1522
|
}
|
|
1513
1523
|
}
|
|
@@ -43,14 +43,14 @@ export const docs = {
|
|
|
43
43
|
type: 'number',
|
|
44
44
|
description:
|
|
45
45
|
'Drawer width in pixels. Capped at 85vw to prevent overflow on small screens.',
|
|
46
|
-
default: '
|
|
46
|
+
default: '320',
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
name: 'side',
|
|
50
|
-
type: "'start' | 'end'",
|
|
50
|
+
type: "'start' | 'end' | 'auto'",
|
|
51
51
|
description:
|
|
52
|
-
'Which side the drawer slides from. Start is left in LTR, right in RTL.',
|
|
53
|
-
default: "'
|
|
52
|
+
'Which side the drawer slides from. Start is left in LTR, right in RTL. Auto picks a side based on the trigger position.',
|
|
53
|
+
default: "'auto'",
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
},
|
|
@@ -124,14 +124,14 @@ export const docsZh = {
|
|
|
124
124
|
type: 'number',
|
|
125
125
|
description:
|
|
126
126
|
'抽屉宽度(像素)。上限为 85vw 以防止在小屏幕上溢出。',
|
|
127
|
-
default: '
|
|
127
|
+
default: '320',
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
name: 'side',
|
|
131
|
-
type: "'start' | 'end'",
|
|
131
|
+
type: "'start' | 'end' | 'auto'",
|
|
132
132
|
description:
|
|
133
|
-
'抽屉滑出的方向。在 LTR 布局中 start 为左侧,在 RTL 布局中为右侧。',
|
|
134
|
-
default: "'
|
|
133
|
+
'抽屉滑出的方向。在 LTR 布局中 start 为左侧,在 RTL 布局中为右侧。auto 根据触发元素的位置自动选择方向。',
|
|
134
|
+
default: "'auto'",
|
|
135
135
|
},
|
|
136
136
|
],
|
|
137
137
|
theming: {
|
|
@@ -363,8 +363,21 @@ export function MobileNav({
|
|
|
363
363
|
return () => {
|
|
364
364
|
if (closeTimeoutRef.current) {
|
|
365
365
|
clearTimeout(closeTimeoutRef.current);
|
|
366
|
+
closeTimeoutRef.current = null;
|
|
366
367
|
}
|
|
367
368
|
document.documentElement.style.overflow = '';
|
|
369
|
+
// Close the native dialog on teardown if it's still open. Inside AppShell
|
|
370
|
+
// the drawer is mounted in an <Activity> that switches to mode="hidden"
|
|
371
|
+
// when the drawer closes; React then runs this cleanup (with a stale
|
|
372
|
+
// isOpen) instead of re-running the effect with isOpen=false, so the
|
|
373
|
+
// close branch above never fires. If we leave the <dialog> `open` here,
|
|
374
|
+
// showModal() is skipped on the next open (the dialog is already open in
|
|
375
|
+
// the hidden tree) and the drawer can never be re-opened. Closing it
|
|
376
|
+
// unconditionally on teardown keeps the native dialog state in sync so a
|
|
377
|
+
// subsequent open cleanly calls showModal() again.
|
|
378
|
+
if (dialog.open) {
|
|
379
|
+
dialog.close();
|
|
380
|
+
}
|
|
368
381
|
};
|
|
369
382
|
}, [isOpen, side]);
|
|
370
383
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MobileNavReopen.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, AppShell + SideNav
|
|
6
|
+
* @output Regression test for mobile hamburger nav re-open after close
|
|
7
|
+
* @position Testing; validates the OOTB AppShell mobile drawer toggle cycle
|
|
8
|
+
*
|
|
9
|
+
* Repro for: mobile hamburger nav can be opened and closed once, but cannot
|
|
10
|
+
* be re-opened after closing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
describe,
|
|
15
|
+
it,
|
|
16
|
+
expect,
|
|
17
|
+
vi,
|
|
18
|
+
beforeAll,
|
|
19
|
+
beforeEach,
|
|
20
|
+
afterEach,
|
|
21
|
+
} from 'vitest';
|
|
22
|
+
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
23
|
+
import {AppShell} from '../AppShell/AppShell';
|
|
24
|
+
import {SideNav, SideNavItem, SideNavSection} from '../SideNav';
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
HTMLDialogElement.prototype.showModal =
|
|
28
|
+
HTMLDialogElement.prototype.showModal ||
|
|
29
|
+
function (this: HTMLDialogElement) {
|
|
30
|
+
this.setAttribute('open', '');
|
|
31
|
+
};
|
|
32
|
+
HTMLDialogElement.prototype.close =
|
|
33
|
+
HTMLDialogElement.prototype.close ||
|
|
34
|
+
function (this: HTMLDialogElement) {
|
|
35
|
+
this.removeAttribute('open');
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
class MockResizeObserver {
|
|
40
|
+
observe() {}
|
|
41
|
+
unobserve() {}
|
|
42
|
+
disconnect() {}
|
|
43
|
+
}
|
|
44
|
+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
|
|
45
|
+
|
|
46
|
+
function createMockMatchMedia(matches: boolean) {
|
|
47
|
+
return {
|
|
48
|
+
matches,
|
|
49
|
+
media: '',
|
|
50
|
+
onchange: null,
|
|
51
|
+
addEventListener: vi.fn(),
|
|
52
|
+
removeEventListener: vi.fn(),
|
|
53
|
+
addListener: vi.fn(),
|
|
54
|
+
removeListener: vi.fn(),
|
|
55
|
+
dispatchEvent: vi.fn(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.stubGlobal(
|
|
61
|
+
'matchMedia',
|
|
62
|
+
vi.fn().mockReturnValue(createMockMatchMedia(true)),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
vi.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function TestShell() {
|
|
71
|
+
return (
|
|
72
|
+
<AppShell
|
|
73
|
+
sideNav={
|
|
74
|
+
<SideNav>
|
|
75
|
+
<SideNavSection title="Test" isHeaderHidden>
|
|
76
|
+
<SideNavItem label="Home" />
|
|
77
|
+
</SideNavSection>
|
|
78
|
+
</SideNav>
|
|
79
|
+
}
|
|
80
|
+
mobileNav={{breakpoint: 'md'}}>
|
|
81
|
+
<div>Content</div>
|
|
82
|
+
</AppShell>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('Mobile nav re-open after close (uncontrolled OOTB)', () => {
|
|
87
|
+
it('can be opened, closed, then opened again', () => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
try {
|
|
90
|
+
render(<TestShell />);
|
|
91
|
+
|
|
92
|
+
const getDialog = () => screen.getAllByRole('dialog', {hidden: true})[0];
|
|
93
|
+
const openToggle = () =>
|
|
94
|
+
screen.getByRole('button', {name: /open navigation/i});
|
|
95
|
+
|
|
96
|
+
// 1. Open
|
|
97
|
+
fireEvent.click(openToggle());
|
|
98
|
+
expect(getDialog()).toHaveAttribute('open');
|
|
99
|
+
|
|
100
|
+
// 2. Close via the drawer's close button
|
|
101
|
+
fireEvent.click(screen.getByRole('button', {name: /close navigation/i}));
|
|
102
|
+
// Flush the delayed dialog.close() (slide-out transition)
|
|
103
|
+
act(() => {
|
|
104
|
+
vi.advanceTimersByTime(300);
|
|
105
|
+
});
|
|
106
|
+
expect(getDialog()).not.toHaveAttribute('open');
|
|
107
|
+
|
|
108
|
+
// 3. Open AGAIN — this is the bug: it should re-open
|
|
109
|
+
fireEvent.click(openToggle());
|
|
110
|
+
act(() => {
|
|
111
|
+
vi.advanceTimersByTime(300);
|
|
112
|
+
});
|
|
113
|
+
expect(getDialog()).toHaveAttribute('open');
|
|
114
|
+
} finally {
|
|
115
|
+
vi.useRealTimers();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -228,7 +228,7 @@ export const docsZh = {
|
|
|
228
228
|
/** @type {import('../docs-types').TranslationDoc} */
|
|
229
229
|
export const docsDense = {
|
|
230
230
|
description:
|
|
231
|
-
'Document outline/table-of-contents nav with sliding indicator track. Flat items array {id,label,level}; anchor links; density variant (default/compact); uncontrolled scroll-spy
|
|
231
|
+
'Document outline/table-of-contents nav with sliding indicator track. Flat items array {id,label,level}; anchor links; density variant (default/compact); uncontrolled scroll-spy by scroll position (last heading past its scroll-margin-top line; first item at top, last at bottom); controlled with activeId; smooth-scroll on click that pins the active item until the next manual scroll.',
|
|
232
232
|
usage: {
|
|
233
233
|
description:
|
|
234
234
|
'A table-of-contents sidebar for documentation pages, help centers, wikis, and long settings pages. Use it for navigation within a single page, not for app routes.',
|
|
@@ -37,7 +37,13 @@ describe('parseOutlineFromMarkdown', () => {
|
|
|
37
37
|
parseOutlineFromMarkdown(
|
|
38
38
|
'## **Install** `@astryxdesign/core`\n\n```\n# Not a heading\n```',
|
|
39
39
|
),
|
|
40
|
-
).toEqual([
|
|
40
|
+
).toEqual([
|
|
41
|
+
{
|
|
42
|
+
id: 'install-astryxdesign-core',
|
|
43
|
+
label: 'Install @astryxdesign/core',
|
|
44
|
+
level: 2,
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
41
47
|
});
|
|
42
48
|
|
|
43
49
|
it('deduplicates generated ids', () => {
|
|
@@ -87,7 +93,7 @@ describe('Outline', () => {
|
|
|
87
93
|
).not.toHaveAttribute('aria-current');
|
|
88
94
|
});
|
|
89
95
|
|
|
90
|
-
it('smooth-scrolls and
|
|
96
|
+
it('smooth-scrolls and defers the indicator until the scroll settles when uncontrolled', async () => {
|
|
91
97
|
const user = userEvent.setup();
|
|
92
98
|
const target = document.createElement('h2');
|
|
93
99
|
target.id = 'install';
|
|
@@ -101,6 +107,47 @@ describe('Outline', () => {
|
|
|
101
107
|
behavior: 'smooth',
|
|
102
108
|
block: 'start',
|
|
103
109
|
});
|
|
110
|
+
// Uncontrolled: the indicator is deferred during the programmatic scroll,
|
|
111
|
+
// so it has not moved to the clicked item yet.
|
|
112
|
+
expect(
|
|
113
|
+
screen.getByRole('link', {name: 'Installation'}),
|
|
114
|
+
).not.toHaveAttribute('aria-current', 'true');
|
|
115
|
+
|
|
116
|
+
// When the scroll settles, the indicator lands on the clicked item.
|
|
117
|
+
act(() => {
|
|
118
|
+
window.dispatchEvent(new Event('scrollend'));
|
|
119
|
+
});
|
|
120
|
+
expect(onActiveIdChange).toHaveBeenCalledWith('install');
|
|
121
|
+
expect(screen.getByRole('link', {name: 'Installation'})).toHaveAttribute(
|
|
122
|
+
'aria-current',
|
|
123
|
+
'true',
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
document.body.removeChild(target);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('reports active id on click when controlled', async () => {
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
const target = document.createElement('h2');
|
|
132
|
+
target.id = 'install';
|
|
133
|
+
document.body.appendChild(target);
|
|
134
|
+
const onActiveIdChange = vi.fn();
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<Outline
|
|
138
|
+
items={items}
|
|
139
|
+
activeId="intro"
|
|
140
|
+
onActiveIdChange={onActiveIdChange}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
await user.click(screen.getByRole('link', {name: 'Installation'}));
|
|
144
|
+
|
|
145
|
+
expect(target.scrollIntoView).toHaveBeenCalledWith({
|
|
146
|
+
behavior: 'smooth',
|
|
147
|
+
block: 'start',
|
|
148
|
+
});
|
|
149
|
+
// Controlled: there is no built-in scroll-spy, so the consumer owns the
|
|
150
|
+
// active state and must be notified on click.
|
|
104
151
|
expect(onActiveIdChange).toHaveBeenCalledWith('install');
|
|
105
152
|
|
|
106
153
|
document.body.removeChild(target);
|
|
@@ -122,11 +169,7 @@ describe('Outline', () => {
|
|
|
122
169
|
|
|
123
170
|
it('renders with density="compact"', () => {
|
|
124
171
|
render(
|
|
125
|
-
<Outline
|
|
126
|
-
items={items}
|
|
127
|
-
density="compact"
|
|
128
|
-
data-testid="outline-compact"
|
|
129
|
-
/>,
|
|
172
|
+
<Outline items={items} density="compact" data-testid="outline-compact" />,
|
|
130
173
|
);
|
|
131
174
|
expect(screen.getByTestId('outline-compact').className).toContain(
|
|
132
175
|
'compact',
|
|
@@ -201,50 +244,45 @@ describe('Outline', () => {
|
|
|
201
244
|
).not.toHaveAttribute('aria-current');
|
|
202
245
|
});
|
|
203
246
|
|
|
204
|
-
it('updates uncontrolled active id from
|
|
205
|
-
let observerCallback: IntersectionObserverCallback | undefined;
|
|
206
|
-
|
|
207
|
-
class MockIntersectionObserver {
|
|
208
|
-
observe = vi.fn();
|
|
209
|
-
disconnect = vi.fn();
|
|
210
|
-
|
|
211
|
-
constructor(callback: IntersectionObserverCallback) {
|
|
212
|
-
observerCallback = callback;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
|
217
|
-
|
|
247
|
+
it('updates uncontrolled active id from scroll position', () => {
|
|
218
248
|
const intro = document.createElement('h2');
|
|
219
249
|
intro.id = 'intro';
|
|
250
|
+
const install = document.createElement('h3');
|
|
251
|
+
install.id = 'install';
|
|
220
252
|
const api = document.createElement('h3');
|
|
221
253
|
api.id = 'api';
|
|
222
|
-
document.body.append(intro, api);
|
|
254
|
+
document.body.append(intro, install, api);
|
|
223
255
|
|
|
224
|
-
|
|
225
|
-
|
|
256
|
+
// Not at the bottom of the page.
|
|
257
|
+
Object.defineProperty(document.documentElement, 'scrollHeight', {
|
|
258
|
+
value: 4000,
|
|
259
|
+
configurable: true,
|
|
260
|
+
});
|
|
226
261
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
// intro + install have scrolled above the activation line (top <= 0);
|
|
263
|
+
// api is still below it, so install is the last passed heading.
|
|
264
|
+
vi.spyOn(intro, 'getBoundingClientRect').mockReturnValue({
|
|
265
|
+
top: -200,
|
|
266
|
+
} as DOMRect);
|
|
267
|
+
vi.spyOn(install, 'getBoundingClientRect').mockReturnValue({
|
|
268
|
+
top: -10,
|
|
269
|
+
} as DOMRect);
|
|
270
|
+
vi.spyOn(api, 'getBoundingClientRect').mockReturnValue({
|
|
271
|
+
top: 400,
|
|
272
|
+
} as DOMRect);
|
|
236
273
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
274
|
+
const onActiveIdChange = vi.fn();
|
|
275
|
+
// The hook resolves the active id from scroll position on mount.
|
|
276
|
+
render(<Outline items={items} onActiveIdChange={onActiveIdChange} />);
|
|
240
277
|
|
|
241
|
-
expect(screen.getByRole('link', {name: '
|
|
278
|
+
expect(screen.getByRole('link', {name: 'Installation'})).toHaveAttribute(
|
|
242
279
|
'aria-current',
|
|
243
280
|
'true',
|
|
244
281
|
);
|
|
245
|
-
expect(onActiveIdChange).toHaveBeenCalledWith('
|
|
282
|
+
expect(onActiveIdChange).toHaveBeenCalledWith('install');
|
|
246
283
|
|
|
247
284
|
document.body.removeChild(intro);
|
|
285
|
+
document.body.removeChild(install);
|
|
248
286
|
document.body.removeChild(api);
|
|
249
287
|
});
|
|
250
288
|
});
|