@astryxdesign/core 0.7.2-canary.caec7ca → 0.7.2-canary.d52db36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +4 -3
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +19 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/astryx.css +5 -2
- package/package.json +3 -3
- package/src/Item/Item.doc.mjs +1 -1
- package/src/Item/Item.tsx +1 -1
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +144 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.tsx +46 -26
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/TabList/TabList.spec.md +57 -44
- package/src/theme/derivedVarRegistry.test.ts +3 -0
package/src/List/List.test.tsx
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file List.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, List, ListItem
|
|
6
|
-
* @output Unit tests for List and ListItem, including
|
|
6
|
+
* @output Unit tests for List and ListItem, including the row edge treatment
|
|
7
7
|
* @position Testing; validates List.tsx and ListItem.tsx implementation
|
|
8
8
|
*
|
|
9
9
|
* SYNC: When modified, update this header
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen, waitFor} from '@testing-library/react';
|
|
13
|
+
import {cleanup, render, screen, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {rulesDeclaredFor} from '../__tests__/pressState';
|
|
16
16
|
import {List} from './List';
|
|
@@ -277,121 +277,171 @@ describe('List', () => {
|
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
// ===========================================================================
|
|
280
|
-
//
|
|
280
|
+
// Edge treatment
|
|
281
281
|
// ===========================================================================
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
/** Every injected rule that names one of the element's classes. */
|
|
284
|
+
function declarationsFor(el: Element): string {
|
|
285
|
+
return rulesDeclaredFor(el).join('\n').replace(/\s+/g, '');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The declarations of the row inset variable that reach the row. */
|
|
289
|
+
function insetDeclarations(item: Element): string[] {
|
|
290
|
+
return rulesDeclaredFor(item).filter(rule =>
|
|
291
|
+
rule.includes('--_item-inset-inline:'),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const ZERO_INSET = /--_item-inset-inline:\s*0(px)?\s*;/;
|
|
296
|
+
const LIST_REACH = [
|
|
297
|
+
'margin-inline-start:calc(-1*var(--container-padding-inline-start,0px))',
|
|
298
|
+
'margin-inline-end:calc(-1*var(--container-padding-inline-end,0px))',
|
|
299
|
+
'width:calc(100%+var(--container-padding-inline-start,0px)+var(--container-padding-inline-end,0px))',
|
|
300
|
+
];
|
|
301
|
+
const OUTSET_ROW = [
|
|
302
|
+
'margin-inline-start:max(var(--container-padding-inline-start,0px)-var(--_item-inset-inline),0px)',
|
|
303
|
+
'margin-inline-end:max(var(--container-padding-inline-end,0px)-var(--_item-inset-inline),0px)',
|
|
304
|
+
];
|
|
305
|
+
const BLEED_ROW = [
|
|
306
|
+
'padding-inline-start:max(var(--container-padding-inline-start,0px),var(--_item-inset-inline))',
|
|
307
|
+
'padding-inline-end:max(var(--container-padding-inline-end,0px),var(--_item-inset-inline))',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
function renderEdge(props: Partial<React.ComponentProps<typeof List>>) {
|
|
284
311
|
const {container} = render(
|
|
285
|
-
<List>
|
|
286
|
-
<ListItem label="
|
|
312
|
+
<List header={<span>Items</span>} {...props}>
|
|
313
|
+
<ListItem label="Static" />
|
|
314
|
+
<ListItem label="Button" onClick={() => {}} />
|
|
315
|
+
<ListItem label="Link" href="/docs" />
|
|
287
316
|
</List>,
|
|
288
317
|
);
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
318
|
+
const ul = container.querySelector('ul')!;
|
|
319
|
+
return {
|
|
320
|
+
ul,
|
|
321
|
+
header: ul.parentElement!,
|
|
322
|
+
items: [...ul.querySelectorAll('li')],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
292
325
|
|
|
293
|
-
it('
|
|
294
|
-
|
|
295
|
-
// read --_item-inset-inline, which Item sets on the same element (custom
|
|
296
|
-
// properties cascade downward, so the <ul> cannot read it).
|
|
297
|
-
const {container} = render(
|
|
298
|
-
<List edgeCompensation="inline">
|
|
299
|
-
<ListItem label="Item 1" />
|
|
300
|
-
<ListItem label="Item 2" />
|
|
301
|
-
</List>,
|
|
302
|
-
);
|
|
303
|
-
const items = container.querySelectorAll('li');
|
|
304
|
-
expect(items).toHaveLength(2);
|
|
326
|
+
it('keeps the row padding and leaves the list and rows in place by default', () => {
|
|
327
|
+
const {ul, items} = renderEdge({});
|
|
305
328
|
for (const item of items) {
|
|
306
|
-
expect(item.className).
|
|
329
|
+
expect(item.className).not.toMatch(/edge(Flush|Outset|Bleed)/);
|
|
330
|
+
expect(insetDeclarations(item)).toHaveLength(1);
|
|
331
|
+
expect(insetDeclarations(item)[0]).not.toMatch(ZERO_INSET);
|
|
307
332
|
}
|
|
308
|
-
expect(
|
|
309
|
-
|
|
310
|
-
);
|
|
333
|
+
expect(ul.className).not.toContain('reachesContainerPadding');
|
|
334
|
+
expect(edgeContext(ul)).toBe('padded');
|
|
311
335
|
});
|
|
312
336
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
337
|
+
/** The edge each row resolved, read from the row's own edge class. */
|
|
338
|
+
function edgeContext(ul: Element): string {
|
|
339
|
+
const edges = [...ul.querySelectorAll('li')].map(li => {
|
|
340
|
+
const match = /edge(Flush|Outset|Bleed)/.exec(li.className);
|
|
341
|
+
return match ? match[1].toLowerCase() : 'padded';
|
|
342
|
+
});
|
|
343
|
+
expect(new Set(edges).size).toBe(1);
|
|
344
|
+
return edges[0];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
it('applies one treatment to every row, static or interactive, and never to the header', () => {
|
|
348
|
+
for (const edge of ['flush', 'outset', 'bleed'] as const) {
|
|
349
|
+
const {ul, header, items} = renderEdge({edge});
|
|
350
|
+
expect(items).toHaveLength(3);
|
|
351
|
+
expect(edgeContext(ul)).toBe(edge);
|
|
352
|
+
expect(header.className).not.toMatch(/edge|reachesContainerPadding/);
|
|
353
|
+
cleanup();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('flush: zeroes the shared inset at every density and leaves the list in place', () => {
|
|
317
358
|
for (const density of ['compact', 'balanced', 'spacious'] as const) {
|
|
318
|
-
const {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
359
|
+
const {ul, items} = renderEdge({edge: 'flush', density});
|
|
360
|
+
expect(ul.className).not.toContain('reachesContainerPadding');
|
|
361
|
+
for (const item of items) {
|
|
362
|
+
expect(item).toHaveAttribute('data-density', density);
|
|
363
|
+
const insets = insetDeclarations(item);
|
|
364
|
+
expect(insets).toHaveLength(1);
|
|
365
|
+
expect(insets[0]).toMatch(ZERO_INSET);
|
|
366
|
+
}
|
|
367
|
+
cleanup();
|
|
327
368
|
}
|
|
328
369
|
});
|
|
329
370
|
|
|
330
|
-
it('
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
371
|
+
it('outset: the list reaches the container padding and each row steps back in by what its inset does not fill, per edge', () => {
|
|
372
|
+
// Each edge reads its own container padding var: one var on both edges
|
|
373
|
+
// over-reaches the other edge under asymmetric padding (16px start /
|
|
374
|
+
// 4px end), so a selected row would paint past the outer border.
|
|
375
|
+
const {ul, items} = renderEdge({edge: 'outset'});
|
|
376
|
+
const listCss = declarationsFor(ul);
|
|
377
|
+
for (const declaration of LIST_REACH) {
|
|
378
|
+
expect(listCss).toContain(declaration);
|
|
379
|
+
}
|
|
380
|
+
for (const item of items) {
|
|
381
|
+
const css = declarationsFor(item);
|
|
382
|
+
for (const declaration of OUTSET_ROW) {
|
|
383
|
+
expect(css).toContain(declaration);
|
|
384
|
+
}
|
|
385
|
+
expect(css).not.toContain(
|
|
386
|
+
'margin-inline-end:max(var(--container-padding-inline-start',
|
|
387
|
+
);
|
|
388
|
+
expect(insetDeclarations(item)[0]).not.toMatch(ZERO_INSET);
|
|
389
|
+
}
|
|
344
390
|
});
|
|
345
391
|
|
|
346
|
-
it('
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<ListItem label="Item" />
|
|
357
|
-
</List>,
|
|
358
|
-
);
|
|
359
|
-
expect(container.querySelector('li')!.className).toContain(
|
|
360
|
-
'inlineEdgeCompensation',
|
|
361
|
-
);
|
|
362
|
-
let out = '';
|
|
363
|
-
for (const sheet of Array.from(document.styleSheets)) {
|
|
364
|
-
try {
|
|
365
|
-
for (const rule of Array.from(sheet.cssRules)) {
|
|
366
|
-
out += rule.cssText + '\n';
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
// ignore cross-origin sheets
|
|
392
|
+
it('bleed: the list reaches the container padding and rows fill it with text on the padding line, per edge', () => {
|
|
393
|
+
const {ul, items} = renderEdge({edge: 'bleed'});
|
|
394
|
+
const listCss = declarationsFor(ul);
|
|
395
|
+
for (const declaration of LIST_REACH) {
|
|
396
|
+
expect(listCss).toContain(declaration);
|
|
397
|
+
}
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
const css = declarationsFor(item);
|
|
400
|
+
for (const declaration of BLEED_ROW) {
|
|
401
|
+
expect(css).toContain(declaration);
|
|
370
402
|
}
|
|
403
|
+
expect(css).toContain('border-radius:0');
|
|
404
|
+
expect(css).not.toMatch(/margin-inline-(start|end):max/);
|
|
371
405
|
}
|
|
372
|
-
out += Array.from(document.querySelectorAll('style'))
|
|
373
|
-
.map(s => s.textContent || '')
|
|
374
|
-
.join('\n');
|
|
375
|
-
const css = out.replace(/\s+/g, '');
|
|
376
|
-
expect(css).toContain(
|
|
377
|
-
'margin-inline-start:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-start,0px)))',
|
|
378
|
-
);
|
|
379
|
-
expect(css).toContain(
|
|
380
|
-
'margin-inline-end:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-end,0px)))',
|
|
381
|
-
);
|
|
382
|
-
// The end edge must not clamp against the start var (the over-cancel).
|
|
383
|
-
expect(css).not.toContain(
|
|
384
|
-
'margin-inline-end:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-start,0px)))',
|
|
385
|
-
);
|
|
386
406
|
});
|
|
387
407
|
|
|
388
|
-
it('
|
|
408
|
+
it('keeps the inline clip on the list for every treatment', () => {
|
|
409
|
+
for (const edge of ['padded', 'flush', 'outset', 'bleed'] as const) {
|
|
410
|
+
const {ul} = renderEdge({edge});
|
|
411
|
+
expect(declarationsFor(ul)).toContain('overflow-x:clip');
|
|
412
|
+
cleanup();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('treats the deprecated edgeCompensation="inline" as edge="outset" without a runtime warning', () => {
|
|
417
|
+
// Plain use is reported statically by the @deprecated type; rendering
|
|
418
|
+
// never warns merely because the old name is used (spec:AST-017 FR29a).
|
|
419
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
420
|
+
const {ul} = renderEdge({edgeCompensation: 'inline'});
|
|
421
|
+
expect(edgeContext(ul)).toBe('outset');
|
|
422
|
+
expect(ul.className).toContain('reachesContainerPadding');
|
|
423
|
+
expect(warn).not.toHaveBeenCalled();
|
|
424
|
+
warn.mockRestore();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('lets edge win over the deprecated edgeCompensation and warns about the conflict', () => {
|
|
428
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
429
|
+
const {ul} = renderEdge({edge: 'flush', edgeCompensation: 'inline'});
|
|
430
|
+
expect(edgeContext(ul)).toBe('flush');
|
|
431
|
+
expect(ul.className).not.toContain('reachesContainerPadding');
|
|
432
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
433
|
+
const message = String(warn.mock.calls[0][0]);
|
|
434
|
+
expect(message).toContain('`edge` wins');
|
|
435
|
+
expect(message).toContain('astryx upgrade');
|
|
436
|
+
warn.mockRestore();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('keeps the inset of a ListItem outside a List', () => {
|
|
389
440
|
const {container} = render(<ListItem label="Standalone" />);
|
|
390
|
-
expect(container.querySelector('li')!.className).not.
|
|
391
|
-
|
|
441
|
+
expect(container.querySelector('li')!.className).not.toMatch(
|
|
442
|
+
/edge(Flush|Outset|Bleed)/,
|
|
392
443
|
);
|
|
393
444
|
});
|
|
394
|
-
|
|
395
445
|
// ===========================================================================
|
|
396
446
|
// Dividers
|
|
397
447
|
// ===========================================================================
|
package/src/List/List.tsx
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file List.tsx
|
|
7
|
-
* @input Uses React, ReactNode, StyleXStyles, theme tokens,
|
|
8
|
-
* with
|
|
9
|
-
* @output Exports List component, ListProps, ListDensity, ListStyle
|
|
7
|
+
* @input Uses React, ReactNode, StyleXStyles, theme tokens, useDevWarning,
|
|
8
|
+
* and ListContext with the row edge treatment
|
|
9
|
+
* @output Exports List component, ListProps, ListDensity, ListEdge, ListStyle
|
|
10
|
+
* types
|
|
10
11
|
* @position Core implementation; consumed by index.ts, tested by List.test.tsx
|
|
11
12
|
*
|
|
12
13
|
* SYNC: When modified, update these files to stay in sync:
|
|
@@ -24,13 +25,16 @@ import type {BaseProps} from '../BaseProps';
|
|
|
24
25
|
import {
|
|
25
26
|
ListContext,
|
|
26
27
|
type ListDensity,
|
|
28
|
+
type ListEdge,
|
|
27
29
|
type ListMarkerStyle,
|
|
28
30
|
} from './ListContext';
|
|
29
31
|
import {mergeProps} from '../utils';
|
|
30
32
|
import {themeProps} from '../utils/themeProps';
|
|
33
|
+
import {useDevWarning} from '../hooks/useDevWarning';
|
|
31
34
|
|
|
32
35
|
export {
|
|
33
36
|
type ListDensity,
|
|
37
|
+
type ListEdge,
|
|
34
38
|
type ListMarkerStyle as ListStyle,
|
|
35
39
|
} from './ListContext';
|
|
36
40
|
|
|
@@ -60,16 +64,39 @@ export interface ListProps extends BaseProps<
|
|
|
60
64
|
hasDividers?: boolean;
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* Where each row's text, dividers, and hover and selection backgrounds sit,
|
|
68
|
+
* relative to the list's edges and the container around it.
|
|
69
|
+
* - 'padded': text sits inside each row's padding, so it starts a little in
|
|
70
|
+
* from a heading above the list and backgrounds have room around it
|
|
71
|
+
* - 'flush': rows drop their side padding, so text starts on the heading's
|
|
72
|
+
* line and dividers and backgrounds stop at the row's content, with no
|
|
73
|
+
* room beyond it on either side. Works anywhere. Suits static rows
|
|
74
|
+
* - 'outset': text sits on the heading's line, and dividers and backgrounds
|
|
75
|
+
* reach a little past it into the container's padding. Suits interactive
|
|
76
|
+
* rows in a container
|
|
77
|
+
* - 'bleed': text sits on the heading's line, and dividers and backgrounds
|
|
78
|
+
* reach the container's edges with square corners, like Table rows in a
|
|
79
|
+
* Card
|
|
80
|
+
*
|
|
81
|
+
* A row's first content is what lines up with the heading: its marker,
|
|
82
|
+
* icon, or avatar when it has one, otherwise its label. 'outset' and
|
|
83
|
+
* 'bleed' reach into the padding of the nearest container that shares its
|
|
84
|
+
* padding: a Card, Section, Dialog, or BottomSheet, or a transparent
|
|
85
|
+
* container such as a Layout region with explicit padding or a padded
|
|
86
|
+
* ScrollableArea. Place the List directly in that container's padding:
|
|
87
|
+
* padding that Stack, Center, or your own styles add in between is not
|
|
88
|
+
* counted. Drawers and the automatic padding of LayoutContent and
|
|
89
|
+
* LayoutPanel aren't supported yet. With no such container, including in a Popover, they look
|
|
90
|
+
* like 'padded'. In a container with less padding than the rows' own, text
|
|
91
|
+
* starts a little after the heading's line. A theme's inline padding on `item` replaces the row
|
|
92
|
+
* padding of every value, including the zero padding of 'flush'.
|
|
93
|
+
* @default 'padded'
|
|
94
|
+
*/
|
|
95
|
+
edge?: ListEdge;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated Use `edge="outset"`, which this sets. Still works; `edge`
|
|
99
|
+
* wins when both are set. `astryx upgrade` rewrites existing usage.
|
|
73
100
|
*/
|
|
74
101
|
edgeCompensation?: 'inline';
|
|
75
102
|
|
|
@@ -121,6 +148,18 @@ const styles = stylex.create({
|
|
|
121
148
|
// vertical scroller. (StyleX emits the physical longhand of this axis.)
|
|
122
149
|
overflowInline: 'clip',
|
|
123
150
|
},
|
|
151
|
+
// 'outset' and 'bleed' rows reach into the container's padding. The list
|
|
152
|
+
// reaches the container's padding edge on each inline edge first, so its
|
|
153
|
+
// inline clip (above) moves out with the rows instead of cutting them; each
|
|
154
|
+
// row then places itself inside it. Container padding vars are inherited,
|
|
155
|
+
// so the list can read them, while the rows' own inset is scoped to the
|
|
156
|
+
// rows. Missing or reset container padding resolves to zero.
|
|
157
|
+
reachesContainerPadding: {
|
|
158
|
+
marginInlineStart: 'calc(-1 * var(--container-padding-inline-start, 0px))',
|
|
159
|
+
marginInlineEnd: 'calc(-1 * var(--container-padding-inline-end, 0px))',
|
|
160
|
+
width:
|
|
161
|
+
'calc(100% + var(--container-padding-inline-start, 0px) + var(--container-padding-inline-end, 0px))',
|
|
162
|
+
},
|
|
124
163
|
withDividers: {
|
|
125
164
|
gap: 0,
|
|
126
165
|
},
|
|
@@ -148,9 +187,10 @@ const dynamicStyles = stylex.create({
|
|
|
148
187
|
* Renders semantic `<ul>` or `<ol>` elements with configurable density,
|
|
149
188
|
* dividers, marker styles, and an optional header.
|
|
150
189
|
*
|
|
151
|
-
* Set `
|
|
152
|
-
*
|
|
153
|
-
*
|
|
190
|
+
* Set `edge` to choose where row text, dividers, and backgrounds sit: inside
|
|
191
|
+
* each row's padding (`padded`, the default), at the list's edges (`flush`),
|
|
192
|
+
* on a heading's line with backgrounds reaching into the container's padding
|
|
193
|
+
* (`outset`), or reaching the container's edges like Table rows (`bleed`).
|
|
154
194
|
*
|
|
155
195
|
* @example
|
|
156
196
|
* ```
|
|
@@ -162,12 +202,16 @@ const dynamicStyles = stylex.create({
|
|
|
162
202
|
* <ListItem label="First step" />
|
|
163
203
|
* <ListItem label="Second step" />
|
|
164
204
|
* </List>
|
|
205
|
+
* <List edge="flush" hasDividers>
|
|
206
|
+
* <ListItem label="Solstice Mug" description="Ceramic, 12 oz" />
|
|
207
|
+
* </List>
|
|
165
208
|
* ```
|
|
166
209
|
*/
|
|
167
210
|
export function List({
|
|
168
211
|
children,
|
|
169
212
|
density = 'balanced',
|
|
170
213
|
hasDividers = false,
|
|
214
|
+
edge: edgeProp,
|
|
171
215
|
edgeCompensation,
|
|
172
216
|
header,
|
|
173
217
|
listStyle = 'none',
|
|
@@ -182,10 +226,21 @@ export function List({
|
|
|
182
226
|
const headerId = useId();
|
|
183
227
|
const isOrdered = listStyle === 'decimal';
|
|
184
228
|
const Tag = isOrdered ? 'ol' : 'ul';
|
|
229
|
+
// Plain use of the deprecated prop is reported by its @deprecated type;
|
|
230
|
+
// development warns only when the two names conflict (spec:AST-017 FR29).
|
|
231
|
+
useDevWarning(
|
|
232
|
+
'List',
|
|
233
|
+
'`edge` and the deprecated `edgeCompensation` are both set; `edge` wins. ' +
|
|
234
|
+
'Remove `edgeCompensation`. `astryx upgrade` rewrites existing usage.',
|
|
235
|
+
edgeProp != null && edgeCompensation != null,
|
|
236
|
+
);
|
|
237
|
+
const edge: ListEdge =
|
|
238
|
+
edgeProp ?? (edgeCompensation === 'inline' ? 'outset' : 'padded');
|
|
239
|
+
const reachesContainerPadding = edge === 'outset' || edge === 'bleed';
|
|
185
240
|
|
|
186
241
|
const contextValue = useMemo(
|
|
187
|
-
() => ({density, hasDividers,
|
|
188
|
-
[density, hasDividers,
|
|
242
|
+
() => ({density, hasDividers, edge, listStyle}),
|
|
243
|
+
[density, hasDividers, edge, listStyle],
|
|
189
244
|
);
|
|
190
245
|
|
|
191
246
|
const listElement = (
|
|
@@ -211,6 +266,7 @@ export function List({
|
|
|
211
266
|
stylex.props(
|
|
212
267
|
styles.list,
|
|
213
268
|
hasDividers && styles.withDividers,
|
|
269
|
+
reachesContainerPadding && styles.reachesContainerPadding,
|
|
214
270
|
listStyle !== 'none' &&
|
|
215
271
|
(start != null && start !== 1
|
|
216
272
|
? dynamicStyles.counterStart(start - 1)
|
package/src/List/ListContext.tsx
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file ListContext.tsx
|
|
7
7
|
* @input Layer-scoped React context
|
|
8
|
-
* @output Exports ListContext for sharing density, dividers,
|
|
9
|
-
* and
|
|
8
|
+
* @output Exports ListContext for sharing density, dividers, the row edge
|
|
9
|
+
* treatment, and marker style between List and ListItem, and
|
|
10
10
|
* ListMarkerScope, which sets the marker for the items inside it, or a
|
|
11
11
|
* task item's checkbox in its place
|
|
12
12
|
* @position Internal context; consumed by List.tsx and ListItem.tsx
|
|
@@ -18,6 +18,12 @@ import {createLayerScopedContext as createContext} from '../Layer/layerScopedCon
|
|
|
18
18
|
export type ListDensity = 'compact' | 'balanced' | 'spacious';
|
|
19
19
|
export type ListMarkerStyle = 'none' | 'disc' | 'decimal' | 'circle';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Where each row's content and its lines and backgrounds sit relative to the
|
|
23
|
+
* list's edges and the padding of the container around the list.
|
|
24
|
+
*/
|
|
25
|
+
export type ListEdge = 'padded' | 'flush' | 'outset' | 'bleed';
|
|
26
|
+
|
|
21
27
|
/**
|
|
22
28
|
* Every marker ListItem draws: the public styles, and the square, letter,
|
|
23
29
|
* and roman markers Markdown's nested lists use (spec:AST-061 DEC-6).
|
|
@@ -35,8 +41,8 @@ export interface ListTaskMarker {
|
|
|
35
41
|
export interface ListContextValue {
|
|
36
42
|
density: ListDensity;
|
|
37
43
|
hasDividers: boolean;
|
|
44
|
+
edge: ListEdge;
|
|
38
45
|
listStyle: ListMarkerStyle;
|
|
39
|
-
edgeCompensation?: 'inline';
|
|
40
46
|
/**
|
|
41
47
|
* The marker items draw instead of the one `listStyle` names, set by
|
|
42
48
|
* ListMarkerScope; internal. Ignored when `listStyle` is `'none'`.
|
package/src/List/ListItem.tsx
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file ListItem.tsx
|
|
7
|
-
* @input Uses React, ReactNode, StyleXStyles, theme tokens, List edge
|
|
8
|
-
* CheckboxInput, and the marker or task checkbox a
|
|
9
|
-
* (Markdown's nested lists and the task items of mixed
|
|
7
|
+
* @input Uses React, ReactNode, StyleXStyles, theme tokens, the List row edge
|
|
8
|
+
* treatment, CheckboxInput, and the marker or task checkbox a
|
|
9
|
+
* ListMarkerScope sets (Markdown's nested lists and the task items of mixed
|
|
10
|
+
* lists)
|
|
10
11
|
* @output Exports ListItem component, ListItemProps type
|
|
11
12
|
* @position Core implementation; consumed by List, index.ts, tested by List.test.tsx
|
|
12
13
|
*
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
|
|
24
25
|
import {use, type ReactNode} from 'react';
|
|
25
26
|
import * as stylex from '@stylexjs/stylex';
|
|
27
|
+
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
26
28
|
import {
|
|
27
29
|
colorVars,
|
|
28
30
|
spacingVars,
|
|
@@ -149,23 +151,37 @@ const styles = stylex.create({
|
|
|
149
151
|
withCounter: {
|
|
150
152
|
counterIncrement: 'astryx-list',
|
|
151
153
|
},
|
|
152
|
-
// List's
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// and
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// the
|
|
164
|
-
|
|
154
|
+
// List's edge treatments. Each reads --_item-inset-inline, the variable
|
|
155
|
+
// Item derives its paddingInline from, on the row itself (custom properties
|
|
156
|
+
// only cascade downward, so the <ul> cannot read it); density and theme
|
|
157
|
+
// paddingInline overrides on `item` move it. Each edge resolves against ITS
|
|
158
|
+
// OWN container padding var, so asymmetric padding never over-reaches an
|
|
159
|
+
// edge, and logical properties keep RTL correct.
|
|
160
|
+
//
|
|
161
|
+
// 'flush': no inset, so content meets the row's edges.
|
|
162
|
+
edgeFlush: {
|
|
163
|
+
'--_item-inset-inline': '0px',
|
|
164
|
+
},
|
|
165
|
+
// 'outset': the list reaches the container's padding edge (List.tsx); the
|
|
166
|
+
// row steps back in by whatever padding its inset does not fill, so the row
|
|
167
|
+
// reaches min(inset, container padding) past the content line and its text
|
|
168
|
+
// lands on that line when the padding is at least the inset.
|
|
169
|
+
edgeOutset: {
|
|
165
170
|
marginInlineStart:
|
|
166
|
-
'
|
|
171
|
+
'max(var(--container-padding-inline-start, 0px) - var(--_item-inset-inline), 0px)',
|
|
167
172
|
marginInlineEnd:
|
|
168
|
-
'
|
|
173
|
+
'max(var(--container-padding-inline-end, 0px) - var(--_item-inset-inline), 0px)',
|
|
174
|
+
},
|
|
175
|
+
// 'bleed': the row fills the list, which reaches the container's padding
|
|
176
|
+
// edge (List.tsx); its text sits on the container's padding line, never
|
|
177
|
+
// closer to the row's edge than its own inset. Square corners: the row
|
|
178
|
+
// meets the container's straight sides.
|
|
179
|
+
edgeBleed: {
|
|
180
|
+
paddingInlineStart:
|
|
181
|
+
'max(var(--container-padding-inline-start, 0px), var(--_item-inset-inline))',
|
|
182
|
+
paddingInlineEnd:
|
|
183
|
+
'max(var(--container-padding-inline-end, 0px), var(--_item-inset-inline))',
|
|
184
|
+
borderRadius: 0,
|
|
169
185
|
},
|
|
170
186
|
withDivider: {
|
|
171
187
|
borderBlockEndWidth: borderVars['--border-width'],
|
|
@@ -313,8 +329,8 @@ export function ListItem({
|
|
|
313
329
|
const ctx = use(ListContext);
|
|
314
330
|
const density = ctx?.density ?? 'balanced';
|
|
315
331
|
const hasDividers = ctx?.hasDividers ?? false;
|
|
332
|
+
const edge = ctx?.edge ?? 'padded';
|
|
316
333
|
const listStyle = ctx?.listStyle ?? 'none';
|
|
317
|
-
const edgeCompensation = ctx?.edgeCompensation;
|
|
318
334
|
const hasMarkers = listStyle !== 'none';
|
|
319
335
|
// ListMarkerScope may name another marker for this item.
|
|
320
336
|
const markerKind: ListMarker | null =
|
|
@@ -370,13 +386,17 @@ export function ListItem({
|
|
|
370
386
|
isSelected={isSelected}
|
|
371
387
|
align={align}
|
|
372
388
|
density={density}
|
|
373
|
-
xstyle={
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
389
|
+
xstyle={
|
|
390
|
+
[
|
|
391
|
+
hasMarkers && styles.withCounter,
|
|
392
|
+
hasDividers && styles.withDivider,
|
|
393
|
+
hasDividers && embeddedStyles.noRadius,
|
|
394
|
+
edge === 'flush' && styles.edgeFlush,
|
|
395
|
+
edge === 'outset' && styles.edgeOutset,
|
|
396
|
+
edge === 'bleed' && styles.edgeBleed,
|
|
397
|
+
xstyle,
|
|
398
|
+
] as StyleXStyles
|
|
399
|
+
}
|
|
380
400
|
{...mergeProps(themeProps('list-item'), {className, style})}
|
|
381
401
|
{...restProps}
|
|
382
402
|
/>
|
package/src/List/index.ts
CHANGED
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export {List} from './List';
|
|
15
|
-
export type {ListProps, ListStyle, ListDensity} from './List';
|
|
15
|
+
export type {ListProps, ListStyle, ListDensity, ListEdge} from './List';
|
|
16
16
|
export {ListItem} from './ListItem';
|
|
17
17
|
export type {ListItemProps} from './ListItem';
|
|
@@ -405,6 +405,16 @@ const markdownTableCellStyles = stylex.create({
|
|
|
405
405
|
code: {
|
|
406
406
|
wordBreak: 'normal',
|
|
407
407
|
},
|
|
408
|
+
// The grid's outer edge. `dividers="grid"` draws the lines *between* cells
|
|
409
|
+
// and stops at the table's rim, so without this the top, bottom, and sides
|
|
410
|
+
// stay open and the grid reads as unfinished. On the table element, not the
|
|
411
|
+
// wrapper, so `border-collapse: collapse` merges it with the cell lines
|
|
412
|
+
// into one 1px rule rather than doubling up at the edge.
|
|
413
|
+
tableOuterEdge: {
|
|
414
|
+
borderWidth: borderVars['--border-width'],
|
|
415
|
+
borderStyle: 'solid',
|
|
416
|
+
borderColor: colorVars['--color-border'],
|
|
417
|
+
},
|
|
408
418
|
});
|
|
409
419
|
|
|
410
420
|
/**
|
|
@@ -1905,7 +1915,10 @@ function renderBlock(
|
|
|
1905
1915
|
),
|
|
1906
1916
|
)}
|
|
1907
1917
|
{...lineStamps}>
|
|
1908
|
-
<Table
|
|
1918
|
+
<Table
|
|
1919
|
+
dividers="grid"
|
|
1920
|
+
textOverflow="wrap"
|
|
1921
|
+
xstyle={markdownTableCellStyles.tableOuterEdge}>
|
|
1909
1922
|
<TableHeader>
|
|
1910
1923
|
<TableRow>
|
|
1911
1924
|
{header?.children.map((h, i) => (
|