@astryxdesign/core 0.4.1 → 0.4.2-canary.356d2f9
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 +55 -0
- package/dist/Avatar/Avatar.d.ts +4 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +19 -23
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +9 -7
- package/dist/Chat/ChatMessage.d.ts +7 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.d.ts +13 -1
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +19 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +5 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +158 -30
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -0
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +89 -32
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +119 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +56 -15
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -0
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +5 -0
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +14 -6
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +25 -93
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +10 -5
- package/dist/astryx.css +40 -8
- package/dist/astryx.umd.js +50 -50
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.stylex.d.ts +71 -4
- package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
- package/dist/hooks/containerReveal.stylex.js +57 -5
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useContainerReveal.d.ts +60 -3
- package/dist/hooks/useContainerReveal.d.ts.map +1 -1
- package/dist/hooks/useContainerReveal.js +27 -5
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useMenuHover.d.ts +50 -5
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +171 -51
- package/dist/theme/defineTheme.d.ts +16 -5
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +44 -48
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +7 -0
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +1 -0
- package/dist/theme/expandMotionScale.d.ts +1 -0
- package/dist/theme/expandMotionScale.d.ts.map +1 -1
- package/dist/theme/expandMotionScale.js +1 -0
- package/dist/theme/expandRadiusScale.d.ts +1 -0
- package/dist/theme/expandRadiusScale.d.ts.map +1 -1
- package/dist/theme/expandRadiusScale.js +1 -0
- package/dist/theme/expandTypeScale.d.ts +1 -0
- package/dist/theme/expandTypeScale.d.ts.map +1 -1
- package/dist/theme/expandTypeScale.js +1 -0
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/theme/tokens.stylex.d.ts +3 -1
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +3 -1
- package/locales/en.json +84 -0
- package/locales/pseudo.json +63 -0
- package/package.json +3 -3
- package/src/Avatar/Avatar.doc.mjs +5 -2
- package/src/Avatar/Avatar.test.tsx +51 -0
- package/src/Avatar/Avatar.tsx +30 -20
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
- package/src/Button/Button.tsx +5 -3
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.doc.mjs +2 -0
- package/src/Chat/Chat.doc.mjs +4 -1
- package/src/Chat/ChatMessage.doc.mjs +3 -3
- package/src/Chat/ChatMessage.tsx +7 -0
- package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
- package/src/Chat/ChatMessageBubble.test.tsx +95 -6
- package/src/Chat/ChatMessageBubble.tsx +25 -0
- package/src/CheckboxInput/CheckboxInput.tsx +20 -0
- package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
- package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
- package/src/DateTimeInput/DateTimeInput.tsx +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Item/Item.doc.mjs +4 -0
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +332 -7
- package/src/Layer/useLayer.tsx +235 -36
- package/src/MobileNav/MobileNav.doc.mjs +7 -0
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/RadioList/RadioListItem.tsx +20 -0
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +858 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +53 -25
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +97 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Slider/Slider.test.tsx +69 -33
- package/src/Slider/Slider.tsx +65 -13
- package/src/Switch/Switch.tsx +20 -0
- package/src/TabList/TabList.doc.mjs +3 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
- package/src/Thumbnail/Thumbnail.tsx +10 -0
- package/src/Timestamp/Timestamp.test.tsx +40 -22
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNav.test.tsx +86 -1
- package/src/TopNav/TopNavHeading.tsx +21 -14
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +29 -105
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +14 -4
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/hooks/containerReveal.stylex.ts +140 -9
- package/src/hooks/index.ts +6 -1
- package/src/hooks/useContainerReveal.doc.mjs +11 -5
- package/src/hooks/useContainerReveal.test.tsx +70 -3
- package/src/hooks/useContainerReveal.ts +86 -7
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useMenuHover.test.tsx +364 -0
- package/src/hooks/useMenuHover.ts +243 -47
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +57 -51
- package/src/theme/derivedVarRegistry.test.ts +78 -12
- package/src/theme/derivedVarRegistry.ts +4 -0
- package/src/theme/expandColorScale.ts +1 -0
- package/src/theme/expandMotionScale.ts +1 -0
- package/src/theme/expandRadiusScale.ts +1 -0
- package/src/theme/expandTypeScale.ts +1 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +3 -1
- package/src/theme/tokens.test.ts +12 -0
- package/src/theme/useTheme.test.tsx +18 -0
|
@@ -114,7 +114,7 @@ export const docs = {
|
|
|
114
114
|
{ guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
|
|
115
115
|
{ guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
|
|
116
116
|
{ guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
|
|
117
|
-
{ guidance:
|
|
117
|
+
{ guidance: true, description: 'Prefer placing HoverCard in a block context rather than directly in a <p>, heading, or link. Those placements are supported when necessary through a corrective portal, but the DOM and tab order may differ.' },
|
|
118
118
|
],
|
|
119
119
|
anatomy: [
|
|
120
120
|
{name: 'Trigger', required: true, description: 'The element that opens the hover card on hover or focus: a button, link, or inline text.'},
|
|
@@ -229,7 +229,7 @@ export const docsZh = {
|
|
|
229
229
|
{ guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
|
|
230
230
|
{ guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
|
|
231
231
|
{ guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
|
|
232
|
-
{ guidance:
|
|
232
|
+
{ guidance: true, description: 'Prefer placing HoverCard in a block context rather than directly in a <p>, heading, or link. Those placements are supported when necessary through a corrective portal, but the DOM and tab order may differ.' },
|
|
233
233
|
],
|
|
234
234
|
},
|
|
235
235
|
};
|
|
@@ -247,7 +247,7 @@ export const docsDense = {
|
|
|
247
247
|
{ guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
|
|
248
248
|
{ guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
|
|
249
249
|
{ guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
|
|
250
|
-
{ guidance:
|
|
250
|
+
{ guidance: true, description: 'Prefer a block context over a direct <p>, heading, or link placement. Those placements work when necessary through a corrective portal, but DOM/tab order may differ.' },
|
|
251
251
|
],
|
|
252
252
|
},
|
|
253
253
|
components: [
|
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
* SYNC: When HoverCard.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
describe,
|
|
14
|
+
it,
|
|
15
|
+
expect,
|
|
16
|
+
vi,
|
|
17
|
+
beforeAll,
|
|
18
|
+
beforeEach,
|
|
19
|
+
afterAll,
|
|
20
|
+
} from 'vitest';
|
|
13
21
|
import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
|
|
14
22
|
import {renderToString} from 'react-dom/server';
|
|
15
23
|
import {hydrateRoot} from 'react-dom/client';
|
|
16
24
|
import {StrictMode} from 'react';
|
|
25
|
+
import {Button} from '../Button/Button';
|
|
26
|
+
import {Theme, defineTheme} from '../theme';
|
|
17
27
|
import {HoverCard} from './HoverCard';
|
|
18
28
|
|
|
19
29
|
// Store original matches to restore later
|
|
@@ -48,6 +58,11 @@ afterAll(() => {
|
|
|
48
58
|
(HTMLElement.prototype as any).matches = originalMatches;
|
|
49
59
|
});
|
|
50
60
|
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.mocked(HTMLElement.prototype.showPopover).mockClear();
|
|
63
|
+
vi.mocked(HTMLElement.prototype.hidePopover).mockClear();
|
|
64
|
+
});
|
|
65
|
+
|
|
51
66
|
describe('HoverCard', () => {
|
|
52
67
|
it('renders trigger element', () => {
|
|
53
68
|
render(
|
|
@@ -58,31 +73,40 @@ describe('HoverCard', () => {
|
|
|
58
73
|
expect(screen.getByRole('button', {name: 'Trigger'})).toBeInTheDocument();
|
|
59
74
|
});
|
|
60
75
|
|
|
61
|
-
it('exposes the floating layer as role="group" when no label is provided', () => {
|
|
76
|
+
it('exposes the floating layer as role="group" when no label is provided', async () => {
|
|
62
77
|
render(
|
|
63
|
-
<HoverCard content={<span>Card content</span>}>
|
|
78
|
+
<HoverCard content={<span>Card content</span>} delay={0}>
|
|
64
79
|
<button type="button">Trigger</button>
|
|
65
80
|
</HoverCard>,
|
|
66
81
|
);
|
|
67
82
|
// A group may validly be unnamed; an unnamed dialog may not. Without a
|
|
68
83
|
// label the layer must not claim the dialog role.
|
|
69
|
-
expect(screen.
|
|
70
|
-
'Card content',
|
|
71
|
-
);
|
|
84
|
+
expect(screen.queryByRole('group', {hidden: true})).toBeNull();
|
|
72
85
|
expect(screen.queryByRole('dialog', {hidden: true})).toBeNull();
|
|
86
|
+
|
|
87
|
+
fireEvent.mouseEnter(screen.getByRole('button', {name: 'Trigger'}));
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByRole('group', {hidden: true})).toHaveTextContent(
|
|
91
|
+
'Card content',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
73
94
|
});
|
|
74
95
|
|
|
75
|
-
it('exposes the floating layer as a named dialog when label is provided', () => {
|
|
96
|
+
it('exposes the floating layer as a named dialog when label is provided', async () => {
|
|
76
97
|
render(
|
|
77
|
-
<HoverCard
|
|
98
|
+
<HoverCard
|
|
99
|
+
content={<span>Card content</span>}
|
|
100
|
+
label="Profile preview"
|
|
101
|
+
delay={0}>
|
|
78
102
|
<button type="button">Trigger</button>
|
|
79
103
|
</HoverCard>,
|
|
80
104
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const dialog = screen.
|
|
105
|
+
expect(screen.queryByRole('dialog', {hidden: true})).toBeNull();
|
|
106
|
+
|
|
107
|
+
fireEvent.mouseEnter(screen.getByRole('button', {name: 'Trigger'}));
|
|
108
|
+
|
|
109
|
+
const dialog = await screen.findByRole('dialog', {hidden: true});
|
|
86
110
|
expect(dialog).toHaveAttribute('aria-label', 'Profile preview');
|
|
87
111
|
expect(dialog).toHaveTextContent('Card content');
|
|
88
112
|
expect(screen.queryByRole('group', {hidden: true})).toBeNull();
|
|
@@ -104,52 +128,236 @@ describe('HoverCard', () => {
|
|
|
104
128
|
|
|
105
129
|
expect(trigger.parentElement?.tagName).toBe('SPAN');
|
|
106
130
|
expect(paragraph?.querySelector('div')).toBeNull();
|
|
131
|
+
expect(paragraph?.querySelector('template')).not.toBeNull();
|
|
107
132
|
});
|
|
108
133
|
|
|
109
|
-
it('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
it('portals block content before showing and restores the marker after hiding', async () => {
|
|
135
|
+
let contentWasPresentAtShow = false;
|
|
136
|
+
vi.mocked(HTMLElement.prototype.showPopover).mockImplementationOnce(
|
|
137
|
+
function (this: HTMLElement) {
|
|
138
|
+
contentWasPresentAtShow =
|
|
139
|
+
this.textContent?.includes('Block card content') ?? false;
|
|
140
|
+
popoverOpenState.set(this, true);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
const consoleErrorSpy = vi
|
|
144
|
+
.spyOn(console, 'error')
|
|
145
|
+
.mockImplementation(() => {});
|
|
146
|
+
|
|
114
147
|
const {container} = render(
|
|
115
148
|
<p>
|
|
116
149
|
Before{' '}
|
|
117
|
-
<HoverCard
|
|
118
|
-
<
|
|
150
|
+
<HoverCard
|
|
151
|
+
content={<div>Block card content</div>}
|
|
152
|
+
delay={0}
|
|
153
|
+
hideDelay={0}>
|
|
154
|
+
Trigger
|
|
119
155
|
</HoverCard>{' '}
|
|
120
156
|
after
|
|
121
157
|
</p>,
|
|
122
158
|
);
|
|
123
159
|
|
|
124
160
|
const paragraph = container.querySelector('p');
|
|
125
|
-
const
|
|
161
|
+
const trigger = screen.getByText('Trigger');
|
|
126
162
|
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
129
|
-
// The whole layer subtree lives inside the paragraph with no block boxes.
|
|
130
|
-
expect(paragraph?.contains(layer as Node)).toBe(true);
|
|
163
|
+
expect(screen.queryByText('Block card content')).toBeNull();
|
|
164
|
+
expect(paragraph?.querySelector('template')).not.toBeNull();
|
|
131
165
|
expect(paragraph?.querySelector('div')).toBeNull();
|
|
166
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
167
|
+
|
|
168
|
+
fireEvent.mouseEnter(trigger);
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
const content = screen.getByText('Block card content');
|
|
172
|
+
const layer = content.closest('[popover]');
|
|
173
|
+
|
|
174
|
+
expect(layer?.tagName).toBe('DIV');
|
|
175
|
+
expect(layer?.parentElement).toBe(container);
|
|
176
|
+
expect(paragraph?.contains(content)).toBe(false);
|
|
177
|
+
expect(contentWasPresentAtShow).toBe(true);
|
|
178
|
+
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
179
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
fireEvent.mouseLeave(trigger);
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
expect(screen.queryByText('Block card content')).toBeNull();
|
|
186
|
+
expect(paragraph?.querySelector('template')).not.toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
consoleErrorSpy.mockRestore();
|
|
132
190
|
});
|
|
133
191
|
|
|
134
|
-
it('
|
|
192
|
+
it('hosts the floating layer outside a wrapping link', async () => {
|
|
193
|
+
// Interactive ancestors capture the layer's own interactions: a card left
|
|
194
|
+
// inside an <a> puts its links and buttons inside that link, so clicking
|
|
195
|
+
// one navigates.
|
|
135
196
|
render(
|
|
197
|
+
<a href="#profile">
|
|
198
|
+
<HoverCard
|
|
199
|
+
content={
|
|
200
|
+
<span>
|
|
201
|
+
<a href="#inner">Inner link</a>
|
|
202
|
+
</span>
|
|
203
|
+
}
|
|
204
|
+
delay={0}>
|
|
205
|
+
Trigger
|
|
206
|
+
</HoverCard>
|
|
207
|
+
</a>,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const link = screen.getByRole('link', {name: /Trigger/});
|
|
211
|
+
expect(screen.queryByText('Inner link')).toBeNull();
|
|
212
|
+
fireEvent.mouseEnter(screen.getByText('Trigger'));
|
|
213
|
+
|
|
214
|
+
const layer = (await screen.findByText('Inner link')).closest('[popover]');
|
|
215
|
+
|
|
216
|
+
expect(layer).not.toBeNull();
|
|
217
|
+
expect(link.contains(layer as Node)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('keeps a safe layer inline at its JSX position', async () => {
|
|
221
|
+
const {container} = render(
|
|
222
|
+
<>
|
|
223
|
+
<HoverCard content={<div>Block card content</div>} delay={0}>
|
|
224
|
+
<button type="button">Trigger</button>
|
|
225
|
+
</HoverCard>
|
|
226
|
+
<button type="button">Following control</button>
|
|
227
|
+
</>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(screen.queryByText('Block card content')).toBeNull();
|
|
231
|
+
fireEvent.mouseEnter(screen.getByRole('button', {name: 'Trigger'}));
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
const layer = screen.getByText('Block card content').closest('[popover]');
|
|
235
|
+
const following = screen.getByRole('button', {name: 'Following control'});
|
|
236
|
+
|
|
237
|
+
expect(layer?.parentElement).toBe(container);
|
|
238
|
+
expect(layer?.nextElementSibling).toBe(following);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('does not render content initially', () => {
|
|
243
|
+
const {container} = render(
|
|
136
244
|
<HoverCard content={<span>Card content</span>}>
|
|
137
245
|
<button type="button">Trigger</button>
|
|
138
246
|
</HoverCard>,
|
|
139
247
|
);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
expect(
|
|
248
|
+
expect(screen.queryByText('Card content')).toBeNull();
|
|
249
|
+
expect(container.querySelector('[popover]')).toBeNull();
|
|
250
|
+
expect(container.querySelector('template')).not.toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('keeps a paragraph portal inside the nearest nested theme scope', async () => {
|
|
254
|
+
const outerTheme = defineTheme({name: 'hovercard-outer-test'});
|
|
255
|
+
const innerTheme = defineTheme({
|
|
256
|
+
name: 'hovercard-inner-test',
|
|
257
|
+
components: {
|
|
258
|
+
hovercard: {base: {borderWidth: '7px'}},
|
|
259
|
+
button: {base: {fontWeight: '700'}},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const {container} = render(
|
|
264
|
+
<Theme theme={outerTheme}>
|
|
265
|
+
<Theme theme={innerTheme}>
|
|
266
|
+
<p>
|
|
267
|
+
<HoverCard
|
|
268
|
+
content={<Button label="View profile">View profile</Button>}
|
|
269
|
+
delay={0}>
|
|
270
|
+
Trigger
|
|
271
|
+
</HoverCard>
|
|
272
|
+
</p>
|
|
273
|
+
</Theme>
|
|
274
|
+
</Theme>,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
fireEvent.mouseEnter(screen.getByText('Trigger'));
|
|
278
|
+
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
const button = screen.getByText('View profile').closest('button');
|
|
281
|
+
const layer = button?.closest('[popover]') ?? null;
|
|
282
|
+
const innerThemeScope = container.querySelector(
|
|
283
|
+
'[data-astryx-theme="hovercard-inner-test"]',
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(innerThemeScope).not.toBeNull();
|
|
287
|
+
expect(button).not.toBeNull();
|
|
288
|
+
expect(layer?.parentElement).toBe(innerThemeScope);
|
|
289
|
+
expect(innerThemeScope?.contains(layer)).toBe(true);
|
|
290
|
+
expect(container.querySelector('p')?.contains(layer)).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('does not freeze computed CSS variables on a paragraph portal', async () => {
|
|
295
|
+
const variables = new Map([
|
|
296
|
+
['--color-neutral', 'rgb(1, 2, 3)'],
|
|
297
|
+
['--color-text-primary', 'rgb(250, 251, 252)'],
|
|
298
|
+
]);
|
|
299
|
+
const getComputedStyleSpy = vi
|
|
300
|
+
.spyOn(window, 'getComputedStyle')
|
|
301
|
+
.mockImplementation(
|
|
302
|
+
element =>
|
|
303
|
+
({
|
|
304
|
+
length: variables.size,
|
|
305
|
+
item: (index: number) => Array.from(variables.keys())[index] ?? '',
|
|
306
|
+
getPropertyValue: (property: string) =>
|
|
307
|
+
variables.get(property) ?? '',
|
|
308
|
+
direction:
|
|
309
|
+
element.tagName.toLowerCase() === 'template' ? 'rtl' : 'ltr',
|
|
310
|
+
writingMode:
|
|
311
|
+
element.tagName.toLowerCase() === 'template'
|
|
312
|
+
? 'vertical-rl'
|
|
313
|
+
: 'horizontal-tb',
|
|
314
|
+
}) as CSSStyleDeclaration,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const {container} = render(
|
|
318
|
+
<p>
|
|
319
|
+
<HoverCard
|
|
320
|
+
content={<Button label="View profile">View profile</Button>}
|
|
321
|
+
delay={0}>
|
|
322
|
+
Trigger
|
|
323
|
+
</HoverCard>
|
|
324
|
+
</p>,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(screen.queryByRole('button', {name: 'View profile'})).toBeNull();
|
|
328
|
+
fireEvent.mouseEnter(screen.getByText('Trigger'));
|
|
329
|
+
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
const button = screen.getByRole('button', {name: 'View profile'});
|
|
332
|
+
const layer = button.closest('[popover]');
|
|
333
|
+
|
|
334
|
+
expect(layer?.parentElement).toBe(container);
|
|
335
|
+
expect(container.querySelector('p')?.contains(button)).toBe(false);
|
|
336
|
+
expect(layer).not.toBeNull();
|
|
337
|
+
expect(
|
|
338
|
+
(layer as HTMLElement).style.getPropertyValue('--color-neutral'),
|
|
339
|
+
).toBe('');
|
|
340
|
+
expect(
|
|
341
|
+
(layer as HTMLElement).style.getPropertyValue('--color-text-primary'),
|
|
342
|
+
).toBe('');
|
|
343
|
+
expect((layer as HTMLElement).style.direction).toBe('rtl');
|
|
344
|
+
expect((layer as HTMLElement).style.writingMode).toBe('vertical-rl');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
getComputedStyleSpy.mockRestore();
|
|
143
348
|
});
|
|
144
349
|
|
|
145
|
-
it('applies the theme body font to the floating layer', () => {
|
|
350
|
+
it('applies the theme body font to the floating layer', async () => {
|
|
146
351
|
render(
|
|
147
|
-
<HoverCard content={<span>Card content</span>}>
|
|
352
|
+
<HoverCard content={<span>Card content</span>} delay={0}>
|
|
148
353
|
<button type="button">Trigger</button>
|
|
149
354
|
</HoverCard>,
|
|
150
355
|
);
|
|
151
356
|
|
|
152
|
-
|
|
357
|
+
fireEvent.mouseEnter(screen.getByRole('button', {name: 'Trigger'}));
|
|
358
|
+
const layer = (await screen.findByText('Card content')).closest(
|
|
359
|
+
'[popover]',
|
|
360
|
+
);
|
|
153
361
|
expect(layer).not.toBeNull();
|
|
154
362
|
expect(getComputedStyle(layer as Element).fontFamily).toBe(
|
|
155
363
|
'var(--font-family-body)',
|
|
@@ -435,41 +643,57 @@ describe('HoverCard', () => {
|
|
|
435
643
|
// `typeof document !== 'undefined'` gate: the server rendered nothing while
|
|
436
644
|
// the first client render emitted the portal, so the two trees disagreed.
|
|
437
645
|
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
646
|
+
// Context layers now render the same inert <template> marker on the server
|
|
647
|
+
// and the first client render. Arbitrary content mounts only when the card
|
|
648
|
+
// is requested and its final inline/portal position is known.
|
|
441
649
|
|
|
442
|
-
it('renders the
|
|
443
|
-
const
|
|
650
|
+
it('renders only the inert marker before and after hydration', async () => {
|
|
651
|
+
const tree = (
|
|
444
652
|
<HoverCard content={<span>Card content</span>}>
|
|
445
653
|
<button type="button">Trigger</button>
|
|
446
|
-
</HoverCard
|
|
654
|
+
</HoverCard>
|
|
447
655
|
);
|
|
448
656
|
|
|
449
|
-
|
|
450
|
-
expect(
|
|
451
|
-
expect(
|
|
452
|
-
|
|
453
|
-
|
|
657
|
+
const serverHTML = renderToString(tree);
|
|
658
|
+
expect(serverHTML).not.toContain('popover=');
|
|
659
|
+
expect(serverHTML).not.toContain('Card content');
|
|
660
|
+
expect(serverHTML).toContain('<template');
|
|
661
|
+
|
|
662
|
+
const container = document.createElement('div');
|
|
663
|
+
container.innerHTML = serverHTML;
|
|
664
|
+
document.body.appendChild(container);
|
|
665
|
+
|
|
666
|
+
let root: ReturnType<typeof hydrateRoot>;
|
|
667
|
+
await act(async () => {
|
|
668
|
+
root = hydrateRoot(container, tree);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
expect(container.querySelector('[popover]')).toBeNull();
|
|
672
|
+
expect(container.querySelector('template')).not.toBeNull();
|
|
673
|
+
|
|
674
|
+
await act(async () => {
|
|
675
|
+
root.unmount();
|
|
676
|
+
});
|
|
677
|
+
container.remove();
|
|
454
678
|
});
|
|
455
679
|
|
|
456
|
-
it('
|
|
680
|
+
it('emits only a valid marker inside a paragraph', () => {
|
|
457
681
|
const html = renderToString(
|
|
458
682
|
<p>
|
|
459
683
|
Before{' '}
|
|
460
|
-
<HoverCard content={<
|
|
684
|
+
<HoverCard content={<div>Block card content</div>}>
|
|
461
685
|
<a href="#trigger">Trigger</a>
|
|
462
686
|
</HoverCard>{' '}
|
|
463
687
|
after
|
|
464
688
|
</p>,
|
|
465
689
|
);
|
|
466
690
|
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
// the browser parser will not reparent (which would itself desync
|
|
470
|
-
// hydration).
|
|
691
|
+
// <template> is inert phrasing/script-supporting content, so the parser
|
|
692
|
+
// has no block layer or consumer content to reparent out of the <p>.
|
|
471
693
|
expect(html).not.toContain('<div');
|
|
472
|
-
expect(html).
|
|
694
|
+
expect(html).not.toContain('popover=');
|
|
695
|
+
expect(html).not.toContain('Block card content');
|
|
696
|
+
expect(html).toContain('<template');
|
|
473
697
|
});
|
|
474
698
|
|
|
475
699
|
it('server markup matches the first client render (no hydration mismatch)', async () => {
|
|
@@ -477,7 +701,7 @@ describe('HoverCard', () => {
|
|
|
477
701
|
<StrictMode>
|
|
478
702
|
<p>
|
|
479
703
|
Glossary:{' '}
|
|
480
|
-
<HoverCard content={<
|
|
704
|
+
<HoverCard content={<div>Definition</div>}>
|
|
481
705
|
<a href="#term">term</a>
|
|
482
706
|
</HoverCard>
|
|
483
707
|
.
|
|
@@ -515,6 +739,8 @@ describe('HoverCard', () => {
|
|
|
515
739
|
|
|
516
740
|
expect(hydrationErrors).toEqual([]);
|
|
517
741
|
expect(recoverableErrors).toEqual([]);
|
|
742
|
+
expect(container.querySelector('[popover]')).toBeNull();
|
|
743
|
+
expect(container.querySelector('template')).not.toBeNull();
|
|
518
744
|
|
|
519
745
|
await act(async () => {
|
|
520
746
|
root.unmount();
|
|
@@ -533,10 +759,10 @@ describe('HoverCard', () => {
|
|
|
533
759
|
);
|
|
534
760
|
|
|
535
761
|
const serverHTML = renderToString(tree);
|
|
536
|
-
// isDefaultOpen must not leak the
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
expect(serverHTML).toContain('
|
|
762
|
+
// isDefaultOpen must not leak the final layer into SSR markup. The marker
|
|
763
|
+
// hydrates first; the effect then requests and opens the real popover.
|
|
764
|
+
expect(serverHTML).not.toContain('popover=');
|
|
765
|
+
expect(serverHTML).toContain('<template');
|
|
540
766
|
|
|
541
767
|
const container = document.createElement('div');
|
|
542
768
|
container.innerHTML = serverHTML;
|
|
@@ -568,6 +794,8 @@ describe('HoverCard', () => {
|
|
|
568
794
|
await waitFor(() => {
|
|
569
795
|
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
570
796
|
});
|
|
797
|
+
expect(container.querySelector('[popover]')).not.toBeNull();
|
|
798
|
+
expect(container).toHaveTextContent('Default open');
|
|
571
799
|
|
|
572
800
|
await act(async () => {
|
|
573
801
|
root.unmount();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @file HoverCard.tsx
|
|
7
7
|
* @input Uses React, useHoverCard hook
|
|
8
8
|
* @output Exports HoverCard component for hover/focus triggered layers
|
|
9
|
-
* @position Layer component;
|
|
9
|
+
* @position Layer component; inline-safe trigger wrapper, floating layer hosted by useLayer
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
12
|
* - /packages/core/src/HoverCard/HoverCard.test.tsx
|
|
@@ -250,18 +250,10 @@ export function HoverCard({
|
|
|
250
250
|
};
|
|
251
251
|
}, [textOnly, hoverCard.ref, hoverCard.describedBy]);
|
|
252
252
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// anchor positioning resolves the trigger reference regardless of where the
|
|
258
|
-
// element sits in the DOM, so no portal is needed to "escape" layout.
|
|
259
|
-
//
|
|
260
|
-
// The layer renders as inline-safe phrasing markup (a `<span>`, see
|
|
261
|
-
// useHoverCard), which stays put inside a `<p>` instead of being reparented
|
|
262
|
-
// by the HTML parser. That keeps the server markup and the first client
|
|
263
|
-
// render identical, so there is no hydration mismatch — and it preserves the
|
|
264
|
-
// inline-safety guarantee (no block elements injected into a paragraph).
|
|
253
|
+
// While closed, useLayer leaves only an inert <template> marker at this JSX
|
|
254
|
+
// position. When the card needs to open, it uses that marker to keep the
|
|
255
|
+
// final layer inline when the parent is safe or portal it outside a <p>,
|
|
256
|
+
// link, or other ancestor that cannot contain it safely.
|
|
265
257
|
const renderedHoverCard = hoverCard.renderHoverCard(content, {
|
|
266
258
|
xstyle,
|
|
267
259
|
className,
|
|
@@ -43,11 +43,7 @@ const styles = stylex.create({
|
|
|
43
43
|
borderRadius: 'var(--_hovercard-radius)',
|
|
44
44
|
boxShadow: shadowVars['--shadow-med'],
|
|
45
45
|
},
|
|
46
|
-
//
|
|
47
|
-
// Content wrapper for padding and mouse events.
|
|
48
|
-
// `display: block` keeps the wrapper a block box even though it renders as a
|
|
49
|
-
// `span` (the layer uses inline-safe phrasing markup so it is valid inside a
|
|
50
|
-
// paragraph and produces identical server/client markup).
|
|
46
|
+
// Content wrapper for padding and interaction events.
|
|
51
47
|
content: {
|
|
52
48
|
display: 'block',
|
|
53
49
|
paddingBlockStart: spacingVars['--spacing-3'],
|
|
@@ -254,6 +250,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
254
250
|
|
|
255
251
|
const layer = useLayer({
|
|
256
252
|
mode: 'context',
|
|
253
|
+
lazyMount: true,
|
|
257
254
|
onShow,
|
|
258
255
|
onHide,
|
|
259
256
|
});
|
|
@@ -459,7 +456,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
459
456
|
// Consumer surface style props land on the layer container — the
|
|
460
457
|
// themed surface (bg/radius/shadow) where the theme class lives — so
|
|
461
458
|
// customizing the card targets the same element as the theme. The inner
|
|
462
|
-
//
|
|
459
|
+
// div keeps `styles.content` for padding.
|
|
463
460
|
xstyle: [
|
|
464
461
|
popoverXstyle,
|
|
465
462
|
layerAnimations[renderPlacement],
|
|
@@ -469,13 +466,13 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
469
466
|
? `${themeClassName} ${props.className}`
|
|
470
467
|
: themeClassName,
|
|
471
468
|
style: props?.style,
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
as: '
|
|
469
|
+
// useLayer mounts only after it has verified or corrected the parent,
|
|
470
|
+
// so rich HoverCard content can use block-safe markup.
|
|
471
|
+
as: 'div' as const,
|
|
475
472
|
};
|
|
476
473
|
|
|
477
474
|
return layer.render(
|
|
478
|
-
<
|
|
475
|
+
<div
|
|
479
476
|
{...stylex.props(styles.content)}
|
|
480
477
|
onMouseEnter={() => {
|
|
481
478
|
isHoveringContentRef.current = true;
|
|
@@ -517,7 +514,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
517
514
|
scheduleHide();
|
|
518
515
|
}}>
|
|
519
516
|
{children}
|
|
520
|
-
</
|
|
517
|
+
</div>,
|
|
521
518
|
renderProps,
|
|
522
519
|
);
|
|
523
520
|
},
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import * as stylex from '@stylexjs/stylex';
|
|
11
11
|
import {radiusVars, borderVars} from '../theme/tokens.stylex';
|
|
12
12
|
|
|
13
|
+
// A grouped control may be followed by context-layer infrastructure rather
|
|
14
|
+
// than another control. Neither the inert marker nor the popover is a visual
|
|
15
|
+
// group member, so skip both when finding the trailing edge.
|
|
16
|
+
const IS_LAST_ITEM = ':not(:has(~ *:not([popover]):not(template)))';
|
|
17
|
+
|
|
13
18
|
export const groupStyles = stylex.create({
|
|
14
19
|
inGroup: {
|
|
15
20
|
flex: 1,
|
|
@@ -29,17 +34,11 @@ export const groupStyles = stylex.create({
|
|
|
29
34
|
},
|
|
30
35
|
borderStartEndRadius: {
|
|
31
36
|
default: 0,
|
|
32
|
-
|
|
33
|
-
':has(+ [popover]:last-child)': radiusVars['--radius-element'],
|
|
34
|
-
':has(+ [popover] + [popover]:last-child)':
|
|
35
|
-
radiusVars['--radius-element'],
|
|
37
|
+
[IS_LAST_ITEM]: radiusVars['--radius-element'],
|
|
36
38
|
},
|
|
37
39
|
borderEndEndRadius: {
|
|
38
40
|
default: 0,
|
|
39
|
-
|
|
40
|
-
':has(+ [popover]:last-child)': radiusVars['--radius-element'],
|
|
41
|
-
':has(+ [popover] + [popover]:last-child)':
|
|
42
|
-
radiusVars['--radius-element'],
|
|
41
|
+
[IS_LAST_ITEM]: radiusVars['--radius-element'],
|
|
43
42
|
},
|
|
44
43
|
':focus-within': {
|
|
45
44
|
zIndex: 1,
|
package/src/Item/Item.doc.mjs
CHANGED
|
@@ -19,6 +19,10 @@ export const docs = {
|
|
|
19
19
|
targets: [
|
|
20
20
|
{className: 'astryx-item', visualProps: ['density', 'align']},
|
|
21
21
|
],
|
|
22
|
+
vars: [
|
|
23
|
+
{name: '--_item-label-color', description: 'Color of the label line. Unset by default (the label uses the primary text token); a parent sets it to recolor the label it renders, as the destructive dropdown/context menu item does.', default: 'var(--color-text-primary)', private: true},
|
|
24
|
+
{name: '--_item-description-color', description: 'Companion to --_item-label-color for the secondary description line.', default: 'var(--color-text-secondary)', private: true},
|
|
25
|
+
],
|
|
22
26
|
},
|
|
23
27
|
components: [
|
|
24
28
|
{
|