@astryxdesign/core 0.1.1 → 0.1.2-canary.3893cd5
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 +105 -0
- package/README.md +119 -1
- 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 +12 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +51 -13
- 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 +7 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +1 -1
- package/dist/Layer/anchorName.d.ts +20 -0
- package/dist/Layer/anchorName.d.ts.map +1 -0
- package/dist/Layer/anchorName.js +36 -0
- package/dist/Layer/useLayer.d.ts +19 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +15 -6
- package/dist/Link/Link.d.ts +1 -1
- package/dist/Link/Link.js +2 -2
- package/dist/Link/LinkProvider.d.ts +1 -0
- package/dist/Link/LinkProvider.d.ts.map +1 -1
- package/dist/Link/LinkProvider.js +1 -0
- 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/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +31 -27
- package/dist/Slider/Slider.js +2 -2
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +27 -8
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +30 -7
- package/dist/Table/TableRow.d.ts +6 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +15 -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 +369 -0
- package/dist/Table/types.d.ts +95 -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/Text/text.stylex.d.ts +2 -2
- package/dist/Text/text.stylex.js +2 -2
- package/dist/Timestamp/Timestamp.d.ts +1 -1
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +28 -3
- package/dist/ToggleButton/ToggleButton.d.ts +7 -9
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +34 -57
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +1 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +1 -0
- package/dist/astryx.css +30 -1
- package/dist/astryx.umd.js +147 -0
- package/dist/astryx.umd.js.map +7 -0
- package/dist/theme/generateThemeRules.js +1 -1
- package/dist/theme/syntax/SyntaxTheme.d.ts +1 -1
- package/dist/theme/syntax/SyntaxTheme.js +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +2 -2
- package/dist/theme/syntax/presets.d.ts +1 -1
- package/dist/theme/syntax/presets.d.ts.map +1 -1
- package/dist/theme/syntax/presets.js +1 -1
- package/dist/theme/syntax/tokens.d.ts +1 -1
- package/dist/theme/syntax/tokens.js +1 -1
- package/dist/theme/types.d.ts +1 -1
- package/package.json +6 -2
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
- package/src/Banner/Banner.doc.mjs +8 -0
- package/src/Banner/Banner.test.tsx +16 -7
- package/src/Banner/Banner.tsx +9 -2
- package/src/Button/Button.doc.mjs +6 -0
- package/src/Button/Button.test.tsx +105 -11
- package/src/Button/Button.tsx +73 -5
- package/src/Chat/ChatComposer.doc.mjs +3 -0
- package/src/Chat/ChatComposerDrawer.doc.mjs +12 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +8 -0
- package/src/ClickableCard/ClickableCard.doc.mjs +15 -0
- package/src/Dialog/Dialog.doc.mjs +2 -2
- package/src/EmptyState/EmptyState.test.tsx +4 -2
- package/src/EmptyState/EmptyState.tsx +6 -2
- package/src/HoverCard/HoverCard.doc.mjs +3 -0
- package/src/HoverCard/HoverCard.test.tsx +189 -2
- package/src/HoverCard/HoverCard.tsx +20 -16
- package/src/HoverCard/useHoverCard.tsx +13 -10
- package/src/Icon/Icon.doc.mjs +8 -0
- package/src/Kbd/Kbd.tsx +2 -9
- package/src/Layer/anchorName.test.ts +146 -0
- package/src/Layer/anchorName.ts +46 -0
- package/src/Layer/useLayer.doc.mjs +7 -2
- package/src/Layer/useLayer.tsx +33 -9
- package/src/Layout/Layout.doc.mjs +1 -1
- package/src/Lightbox/Lightbox.doc.mjs +0 -2
- package/src/Link/Link.test.tsx +2 -2
- package/src/Link/Link.tsx +3 -3
- package/src/Link/LinkProvider.tsx +1 -0
- package/src/List/List.doc.mjs +7 -0
- package/src/Markdown/Markdown.doc.mjs +2 -0
- 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/OverflowList/OverflowList.doc.mjs +17 -0
- package/src/Pagination/Pagination.test.tsx +137 -13
- package/src/Pagination/Pagination.tsx +33 -28
- package/src/ProgressBar/ProgressBar.test.tsx +13 -3
- package/src/Resizable/Resizable.doc.mjs +1 -1
- package/src/Section/Section.doc.mjs +11 -0
- package/src/SelectableCard/SelectableCard.doc.mjs +15 -0
- package/src/Selector/Selector.doc.mjs +4 -0
- package/src/Skeleton/Skeleton.doc.mjs +11 -1
- package/src/Slider/Slider.doc.mjs +4 -0
- package/src/Slider/Slider.test.tsx +3 -16
- package/src/Slider/Slider.tsx +1 -1
- package/src/TabList/TabList.doc.mjs +6 -0
- package/src/Table/BaseTable.tsx +51 -24
- package/src/Table/Table.doc.mjs +12 -0
- package/src/Table/Table.test.tsx +29 -30
- package/src/Table/Table.tsx +22 -1
- package/src/Table/TableRow.tsx +37 -7
- 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 +407 -0
- package/src/Table/types.ts +101 -4
- package/src/Table/useBaseTablePlugins.ts +1 -0
- package/src/Table/useTableColumnResize.doc.mjs +96 -0
- package/src/Table/useTableStickyColumns.doc.mjs +59 -0
- package/src/Text/Heading.doc.mjs +15 -2
- package/src/Text/Text.doc.mjs +7 -1
- package/src/Text/text.stylex.ts +2 -2
- package/src/Timestamp/Timestamp.doc.mjs +9 -0
- package/src/Timestamp/Timestamp.test.tsx +44 -18
- package/src/Timestamp/Timestamp.tsx +30 -10
- package/src/ToggleButton/ToggleButton.doc.mjs +9 -3
- package/src/ToggleButton/ToggleButton.test.tsx +39 -18
- package/src/ToggleButton/ToggleButton.tsx +46 -83
- package/src/Tokenizer/Tokenizer.tsx +4 -1
- package/src/Toolbar/Toolbar.doc.mjs +65 -5
- package/src/Tooltip/Tooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +2 -3
- package/src/hooks/useImageMode.test.ts +133 -0
- package/src/theme/derivedVarRegistry.test.ts +4 -0
- package/src/theme/generateThemeRules.test.ts +7 -8
- package/src/theme/generateThemeRules.ts +1 -1
- package/src/theme/syntax/SyntaxTheme.tsx +1 -1
- package/src/theme/syntax/defineSyntaxTheme.ts +20 -11
- package/src/theme/syntax/presets.ts +5 -2
- package/src/theme/syntax/tokens.ts +1 -1
- package/src/theme/types.ts +1 -1
package/src/Layer/useLayer.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import React, {
|
|
|
23
23
|
} from 'react';
|
|
24
24
|
import * as stylex from '@stylexjs/stylex';
|
|
25
25
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
26
|
+
import {addAnchorName, removeAnchorName} from './anchorName';
|
|
26
27
|
import {typographyVars} from '../theme/tokens.stylex';
|
|
27
28
|
|
|
28
29
|
const styles = stylex.create({
|
|
@@ -65,6 +66,12 @@ export type LayerAlignment = 'start' | 'center' | 'end';
|
|
|
65
66
|
export interface ContextRenderProps {
|
|
66
67
|
placement?: LayerPlacement;
|
|
67
68
|
alignment?: LayerAlignment;
|
|
69
|
+
/**
|
|
70
|
+
* ARIA role applied to the popover container (e.g. `'tooltip'`). Lets
|
|
71
|
+
* consumers complete the ARIA pattern and gives test tooling a stable,
|
|
72
|
+
* non-hashed selector for the layer.
|
|
73
|
+
*/
|
|
74
|
+
role?: string;
|
|
68
75
|
/**
|
|
69
76
|
* StyleX styles for the popover container.
|
|
70
77
|
*/
|
|
@@ -79,6 +86,19 @@ export interface ContextRenderProps {
|
|
|
79
86
|
* Merged after StyleX and anchor positioning styles.
|
|
80
87
|
*/
|
|
81
88
|
style?: React.CSSProperties;
|
|
89
|
+
/**
|
|
90
|
+
* HTML tag to render the popover container as.
|
|
91
|
+
*
|
|
92
|
+
* Defaults to `'div'`. Pass `'span'` when the layer must render inline-safe
|
|
93
|
+
* markup — e.g. a `HoverCard` wrapping inline text inside a `<p>`. A `<span>`
|
|
94
|
+
* is phrasing content, so it stays put in the DOM tree instead of being
|
|
95
|
+
* reparented out of a paragraph by the HTML parser, which keeps server and
|
|
96
|
+
* client markup identical. The Popover API and CSS anchor positioning work
|
|
97
|
+
* the same on either tag.
|
|
98
|
+
*
|
|
99
|
+
* @default 'div'
|
|
100
|
+
*/
|
|
101
|
+
as?: 'div' | 'span';
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
/**
|
|
@@ -311,16 +331,14 @@ export function useLayer(
|
|
|
311
331
|
const ref: RefCallback<HTMLElement> | undefined =
|
|
312
332
|
mode === 'context'
|
|
313
333
|
? (el: HTMLElement | null) => {
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
).anchorName = '';
|
|
334
|
+
// Remove only THIS layer's anchor name from the previous element so
|
|
335
|
+
// other layers sharing the same trigger keep their anchors.
|
|
336
|
+
if (triggerRef.current && triggerRef.current !== el) {
|
|
337
|
+
removeAnchorName(triggerRef.current, anchorId);
|
|
319
338
|
}
|
|
320
339
|
|
|
321
340
|
if (el) {
|
|
322
|
-
(el
|
|
323
|
-
anchorId;
|
|
341
|
+
addAnchorName(el, anchorId);
|
|
324
342
|
}
|
|
325
343
|
|
|
326
344
|
triggerRef.current = el;
|
|
@@ -366,9 +384,11 @@ export function useLayer(
|
|
|
366
384
|
const {
|
|
367
385
|
placement = 'above',
|
|
368
386
|
alignment = 'center',
|
|
387
|
+
role,
|
|
369
388
|
xstyle,
|
|
370
389
|
className: extraClassName,
|
|
371
390
|
style: extraStyle,
|
|
391
|
+
as: Container = 'div',
|
|
372
392
|
} = props || {};
|
|
373
393
|
|
|
374
394
|
// CSS anchor positioning (dynamic, not in StyleX)
|
|
@@ -383,15 +403,19 @@ export function useLayer(
|
|
|
383
403
|
? `${extraClassName} ${stylexResult.className ?? ''}`
|
|
384
404
|
: stylexResult.className;
|
|
385
405
|
|
|
406
|
+
// Render as the requested tag. A `span` keeps the layer phrasing content
|
|
407
|
+
// so it is valid (and stays put on hydration) inside inline contexts like
|
|
408
|
+
// a `<p>`; `div` remains the default for block layers.
|
|
386
409
|
return (
|
|
387
|
-
<
|
|
410
|
+
<Container
|
|
388
411
|
ref={popoverRefCallback}
|
|
389
412
|
id={id}
|
|
413
|
+
role={role}
|
|
390
414
|
popover={lightDismiss ? 'auto' : 'manual'}
|
|
391
415
|
className={combinedClassName}
|
|
392
416
|
style={{...stylexResult.style, ...anchorStyle, ...extraStyle}}>
|
|
393
417
|
{children}
|
|
394
|
-
</
|
|
418
|
+
</Container>
|
|
395
419
|
);
|
|
396
420
|
},
|
|
397
421
|
[anchorId, id, lightDismiss, popoverRefCallback],
|
|
@@ -30,7 +30,7 @@ export const docs = {
|
|
|
30
30
|
name: 'content',
|
|
31
31
|
type: 'ReactNode',
|
|
32
32
|
description:
|
|
33
|
-
'Main content area (center). Children passed to `<Layout>` render here too
|
|
33
|
+
'Main content area (center). Children passed to `<Layout>` render here too: `<Layout>{main}</Layout>` is shorthand for `<Layout content={main} />`.',
|
|
34
34
|
slotElements: [
|
|
35
35
|
{
|
|
36
36
|
__element: 'LayoutContent',
|
|
@@ -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.test.tsx
CHANGED
|
@@ -99,8 +99,8 @@ describe('Link', () => {
|
|
|
99
99
|
|
|
100
100
|
it('renders with different color values', () => {
|
|
101
101
|
const {rerender} = render(
|
|
102
|
-
<Link href="/test" color="
|
|
103
|
-
|
|
102
|
+
<Link href="/test" color="accent">
|
|
103
|
+
Accent
|
|
104
104
|
</Link>,
|
|
105
105
|
);
|
|
106
106
|
expect(screen.getByRole('link')).toBeInTheDocument();
|
package/src/Link/Link.tsx
CHANGED
|
@@ -117,7 +117,7 @@ const linkColorStyles = stylex.create({
|
|
|
117
117
|
placeholder: {
|
|
118
118
|
color: colorVars['--color-text-secondary'],
|
|
119
119
|
},
|
|
120
|
-
|
|
120
|
+
accent: {
|
|
121
121
|
color: colorVars['--color-text-accent'],
|
|
122
122
|
},
|
|
123
123
|
inherit: {
|
|
@@ -216,7 +216,7 @@ export interface LinkProps extends BaseProps<
|
|
|
216
216
|
weight?: TextWeight;
|
|
217
217
|
/**
|
|
218
218
|
* Text color. Forwarded to Text.
|
|
219
|
-
* @default '
|
|
219
|
+
* @default 'accent'
|
|
220
220
|
*/
|
|
221
221
|
color?: TextColor;
|
|
222
222
|
/**
|
|
@@ -264,7 +264,7 @@ export function Link({
|
|
|
264
264
|
type = 'body',
|
|
265
265
|
size,
|
|
266
266
|
weight,
|
|
267
|
-
color = '
|
|
267
|
+
color = 'accent',
|
|
268
268
|
display = 'inline',
|
|
269
269
|
maxLines = 0,
|
|
270
270
|
children,
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* - /packages/core/src/Link/index.ts
|
|
24
24
|
* - /packages/core/src/Link/Link.doc.mjs
|
|
25
25
|
* - /packages/cli/templates/blocks/components/Link/ (showcase blocks)
|
|
26
|
+
* - /packages/cli/templates/blocks/components/LinkProvider/ (example blocks)
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
29
|
import {useMemo, type ReactNode} from 'react';
|
package/src/List/List.doc.mjs
CHANGED
|
@@ -61,6 +61,13 @@ export const docs = {
|
|
|
61
61
|
description: "List marker style. 'decimal' renders an <ol> element instead of <ul>.",
|
|
62
62
|
default: "'none'",
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'start',
|
|
66
|
+
type: 'number',
|
|
67
|
+
description:
|
|
68
|
+
"Starting number for ordered lists (listStyle='decimal'). Sets the CSS counter to begin at this value.",
|
|
69
|
+
default: '1',
|
|
70
|
+
},
|
|
64
71
|
{
|
|
65
72
|
name: 'xstyle',
|
|
66
73
|
type: 'StyleXStyles',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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
|
+
});
|
|
@@ -18,6 +18,23 @@ export const docs = {
|
|
|
18
18
|
'clamp',
|
|
19
19
|
'responsive',
|
|
20
20
|
],
|
|
21
|
+
playground: {
|
|
22
|
+
// children is required; without seeded items the properties-tab preview
|
|
23
|
+
// renders an empty list. Provide a few items so the preview is meaningful.
|
|
24
|
+
// `observeParent` measures the (full-width) preview container instead of
|
|
25
|
+
// the list's own collapsed content box, so the row shows all items rather
|
|
26
|
+
// than collapsing to one.
|
|
27
|
+
defaults: {
|
|
28
|
+
behavior: 'observeParent',
|
|
29
|
+
children: [
|
|
30
|
+
{__element: 'Button', props: {label: 'Overview', variant: 'secondary'}},
|
|
31
|
+
{__element: 'Button', props: {label: 'Activity', variant: 'secondary'}},
|
|
32
|
+
{__element: 'Button', props: {label: 'Settings', variant: 'secondary'}},
|
|
33
|
+
{__element: 'Button', props: {label: 'Members', variant: 'secondary'}},
|
|
34
|
+
{__element: 'Button', props: {label: 'Billing', variant: 'secondary'}},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
21
38
|
props: [
|
|
22
39
|
{
|
|
23
40
|
name: 'children',
|