Skip to content

Commit 5a28ac1

Browse files
feat(ui): Introduce mosaic recipes and conditions (#8830)
1 parent e35b104 commit 5a28ac1

47 files changed

Lines changed: 1896 additions & 1353 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/mosaic-slot-recipes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/headless/src/primitives/dialog/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ No additional props beyond standard HTML attributes and the `render` prop.
9797

9898
## Data Attributes
9999

100-
| Attribute | Applies To | Description |
101-
| --------------------------------- | ---------------------------------- | --------------------------------------- |
102-
| `data-cl-slot` | All parts | Part identifier (e.g. `"dialog-popup"`) |
103-
| `data-cl-open` / `data-cl-closed` | Trigger, Backdrop, Viewport, Popup | Open state |
100+
| Attribute | Applies To | Description |
101+
| --------------------------------- | ---------------------------------- | ----------- |
102+
| `data-cl-open` / `data-cl-closed` | Trigger, Backdrop, Viewport, Popup | Open state |
103+
104+
Slot identity (`data-cl-slot`) is applied by the styled (mosaic) layer, not by the headless parts.
104105

105106
## Important Notes
106107

packages/headless/src/primitives/dialog/dialog-backdrop.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export const DialogBackdrop = React.forwardRef<HTMLDivElement, DialogBackdropPro
2121
const state = { open };
2222

2323
const defaultProps = {
24-
'data-cl-slot': 'dialog-backdrop',
2524
ref,
2625
...transitionProps,
2726
} satisfies DefaultProps<'div'>;

packages/headless/src/primitives/dialog/dialog-close.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>
1515

1616
const defaultProps = {
1717
type: 'button' as const,
18-
'data-cl-slot': 'dialog-close',
1918
ref,
2019
onClick() {
2120
setOpen(false);

packages/headless/src/primitives/dialog/dialog-description.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export const DialogDescription = React.forwardRef<HTMLParagraphElement, DialogDe
1515
const { descriptionId } = useDialogContext();
1616

1717
const defaultProps = {
18-
'data-cl-slot': 'dialog-description',
1918
id: descriptionId,
2019
ref,
2120
} satisfies DefaultProps<'p'>;

packages/headless/src/primitives/dialog/dialog-popup.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const DialogPopup = React.forwardRef<HTMLDivElement, DialogPopupProps>(fu
2626
}
2727

2828
const ownProps = {
29-
'data-cl-slot': 'dialog-popup',
3029
ref: combinedRef,
3130
'aria-labelledby': labelId,
3231
'aria-describedby': descriptionId,

packages/headless/src/primitives/dialog/dialog-title.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export const DialogTitle = React.forwardRef<HTMLHeadingElement, DialogTitleProps
1414
const { labelId } = useDialogContext();
1515

1616
const defaultProps = {
17-
'data-cl-slot': 'dialog-title',
1817
id: labelId,
1918
ref,
2019
} satisfies DefaultProps<'h2'>;

packages/headless/src/primitives/dialog/dialog-trigger.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerPr
2525

2626
const ownProps = {
2727
type: 'button',
28-
'data-cl-slot': 'dialog-trigger',
2928
ref: combinedRef,
3029
} satisfies DefaultProps<'button'>;
3130

packages/headless/src/primitives/dialog/dialog-viewport.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export const DialogViewport = React.forwardRef<HTMLDivElement, DialogViewportPro
3232
const state = { open };
3333

3434
const defaultProps = {
35-
'data-cl-slot': 'dialog-viewport',
3635
ref,
3736
...transitionProps,
3837
style: modal ? undefined : { pointerEvents: 'auto' as const },

packages/headless/src/primitives/dialog/dialog.test.tsx

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import { Dialog } from './index';
77

88
afterEach(() => cleanup());
99

10+
// Headless parts no longer emit `data-cl-slot` — slot identity is applied by the styled
11+
// (mosaic) layer. Tests locate the surface-only parts (backdrop, viewport, trigger) via
12+
// `data-testid` and everything else via its accessible role or text.
1013
function renderDialog(props: Partial<React.ComponentProps<typeof Dialog.Root>> = {}) {
1114
return render(
1215
<Dialog.Root {...props}>
13-
<Dialog.Trigger>Open dialog</Dialog.Trigger>
14-
<Dialog.Backdrop />
15-
<Dialog.Viewport>
16+
<Dialog.Trigger data-testid='dialog-trigger'>Open dialog</Dialog.Trigger>
17+
<Dialog.Backdrop data-testid='dialog-backdrop' />
18+
<Dialog.Viewport data-testid='dialog-viewport'>
1619
<Dialog.Popup>
1720
<Dialog.Title>Dialog Title</Dialog.Title>
1821
<Dialog.Description>Some dialog description</Dialog.Description>
@@ -25,25 +28,6 @@ function renderDialog(props: Partial<React.ComponentProps<typeof Dialog.Root>> =
2528
}
2629

2730
describe('Dialog', () => {
28-
describe('slot attributes', () => {
29-
it('renders trigger with data-cl-slot', () => {
30-
renderDialog();
31-
const trigger = screen.getByRole('button', { name: 'Open dialog' });
32-
expect(trigger).toHaveAttribute('data-cl-slot', 'dialog-trigger');
33-
});
34-
35-
it('renders all parts with correct slot attributes when open', () => {
36-
renderDialog({ defaultOpen: true });
37-
38-
expect(document.querySelector('[data-cl-slot="dialog-backdrop"]')).toBeInTheDocument();
39-
expect(document.querySelector('[data-cl-slot="dialog-viewport"]')).toBeInTheDocument();
40-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).toBeInTheDocument();
41-
expect(document.querySelector('[data-cl-slot="dialog-title"]')).toBeInTheDocument();
42-
expect(document.querySelector('[data-cl-slot="dialog-description"]')).toBeInTheDocument();
43-
expect(document.querySelector('[data-cl-slot="dialog-close"]')).toBeInTheDocument();
44-
});
45-
});
46-
4731
describe('open/close', () => {
4832
it('opens on trigger click', async () => {
4933
const user = userEvent.setup();
@@ -53,7 +37,7 @@ describe('Dialog', () => {
5337
await user.click(trigger);
5438

5539
expect(trigger).toHaveAttribute('data-cl-open', '');
56-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).toBeInTheDocument();
40+
expect(screen.getByRole('dialog')).toBeInTheDocument();
5741
});
5842

5943
it('closes on Escape', async () => {
@@ -93,7 +77,7 @@ describe('Dialog', () => {
9377
it('respects controlled open prop', () => {
9478
renderDialog({ open: true });
9579

96-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).toBeInTheDocument();
80+
expect(screen.getByRole('dialog')).toBeInTheDocument();
9781
});
9882

9983
it('does not open when controlled open is false', async () => {
@@ -102,29 +86,29 @@ describe('Dialog', () => {
10286

10387
await user.click(screen.getByRole('button', { name: 'Open dialog' }));
10488

105-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).not.toBeInTheDocument();
89+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
10690
});
10791
});
10892

10993
describe('ARIA attributes', () => {
11094
it('popup has aria-labelledby linked to title', () => {
11195
renderDialog({ defaultOpen: true });
11296

113-
const title = document.querySelector('[data-cl-slot="dialog-title"]');
114-
const popup = document.querySelector('[data-cl-slot="dialog-popup"]');
97+
const title = screen.getByText('Dialog Title');
98+
const popup = screen.getByRole('dialog');
11599

116100
expect(title).toHaveAttribute('id');
117-
expect(popup).toHaveAttribute('aria-labelledby', title?.getAttribute('id'));
101+
expect(popup).toHaveAttribute('aria-labelledby', title.getAttribute('id'));
118102
});
119103

120104
it('popup has aria-describedby linked to description', () => {
121105
renderDialog({ defaultOpen: true });
122106

123-
const desc = document.querySelector('[data-cl-slot="dialog-description"]');
124-
const popup = document.querySelector('[data-cl-slot="dialog-popup"]');
107+
const desc = screen.getByText('Some dialog description');
108+
const popup = screen.getByRole('dialog');
125109

126110
expect(desc).toHaveAttribute('id');
127-
expect(popup).toHaveAttribute('aria-describedby', desc?.getAttribute('id'));
111+
expect(popup).toHaveAttribute('aria-describedby', desc.getAttribute('id'));
128112
});
129113

130114
it('popup has role=dialog', () => {
@@ -137,7 +121,7 @@ describe('Dialog', () => {
137121
describe('animation lifecycle', () => {
138122
it('backdrop is not rendered when closed', () => {
139123
renderDialog();
140-
expect(document.querySelector('[data-cl-slot="dialog-backdrop"]')).not.toBeInTheDocument();
124+
expect(screen.queryByTestId('dialog-backdrop')).not.toBeInTheDocument();
141125
});
142126

143127
it('applies data-cl-open on popup when open', async () => {
@@ -146,8 +130,7 @@ describe('Dialog', () => {
146130

147131
await user.click(screen.getByRole('button', { name: 'Open dialog' }));
148132

149-
const popup = document.querySelector('[data-cl-slot="dialog-popup"]');
150-
expect(popup).toHaveAttribute('data-cl-open', '');
133+
expect(screen.getByRole('dialog')).toHaveAttribute('data-cl-open', '');
151134
});
152135

153136
it('applies data-cl-open on backdrop when open', async () => {
@@ -156,8 +139,7 @@ describe('Dialog', () => {
156139

157140
await user.click(screen.getByRole('button', { name: 'Open dialog' }));
158141

159-
const backdrop = document.querySelector('[data-cl-slot="dialog-backdrop"]');
160-
expect(backdrop).toHaveAttribute('data-cl-open', '');
142+
expect(screen.getByTestId('dialog-backdrop')).toHaveAttribute('data-cl-open', '');
161143
});
162144

163145
it('applies data-cl-open on viewport when open', async () => {
@@ -166,13 +148,12 @@ describe('Dialog', () => {
166148

167149
await user.click(screen.getByRole('button', { name: 'Open dialog' }));
168150

169-
const viewport = document.querySelector('[data-cl-slot="dialog-viewport"]');
170-
expect(viewport).toHaveAttribute('data-cl-open', '');
151+
expect(screen.getByTestId('dialog-viewport')).toHaveAttribute('data-cl-open', '');
171152
});
172153

173154
it('viewport is not rendered when closed', () => {
174155
renderDialog();
175-
expect(document.querySelector('[data-cl-slot="dialog-viewport"]')).not.toBeInTheDocument();
156+
expect(screen.queryByTestId('dialog-viewport')).not.toBeInTheDocument();
176157
});
177158
});
178159

@@ -204,12 +185,12 @@ describe('Dialog', () => {
204185
</Dialog.Root>,
205186
);
206187

207-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).not.toBeInTheDocument();
188+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
208189
expect(screen.queryByText('Popup content')).not.toBeInTheDocument();
209190

210191
await user.click(screen.getByRole('button', { name: 'Open' }));
211192

212-
expect(document.querySelector('[data-cl-slot="dialog-popup"]')).toBeInTheDocument();
193+
expect(screen.getByRole('dialog')).toBeInTheDocument();
213194
expect(screen.getByText('Popup content')).toBeInTheDocument();
214195
});
215196
});
@@ -223,8 +204,8 @@ describe('Dialog', () => {
223204

224205
it('trigger has data-cl-open when dialog is visible', () => {
225206
renderDialog({ defaultOpen: true });
226-
// When modal is open, trigger's container gets aria-hidden, so use querySelector
227-
const trigger = document.querySelector('[data-cl-slot="dialog-trigger"]');
207+
// When modal is open, the trigger's container gets aria-hidden, so query by test id.
208+
const trigger = screen.getByTestId('dialog-trigger');
228209
expect(trigger).toHaveAttribute('data-cl-open', '');
229210
});
230211
});
@@ -246,7 +227,7 @@ describe('Dialog', () => {
246227
<Dialog.Root modal={false}>
247228
<Dialog.Trigger>Open dialog</Dialog.Trigger>
248229
<Dialog.Backdrop />
249-
<Dialog.Viewport>
230+
<Dialog.Viewport data-testid='dialog-viewport'>
250231
<Dialog.Popup>
251232
<Dialog.Title>Dialog Title</Dialog.Title>
252233
<Dialog.Close>Close</Dialog.Close>
@@ -258,9 +239,9 @@ describe('Dialog', () => {
258239

259240
await user.click(screen.getByRole('button', { name: 'Open dialog' }));
260241

261-
const viewport = document.querySelector('[data-cl-slot="dialog-viewport"]');
242+
const viewport = screen.getByTestId('dialog-viewport');
262243
expect(viewport).toHaveStyle({ pointerEvents: 'auto' });
263-
expect(viewport?.parentElement).toHaveStyle({ pointerEvents: 'none' });
244+
expect(viewport.parentElement).toHaveStyle({ pointerEvents: 'none' });
264245

265246
await user.click(screen.getByRole('button', { name: 'Background button' }));
266247
expect(onBackgroundClick).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)