@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3b5f42e
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/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +6 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +6 -0
- package/dist/astryx.umd.js +11 -11
- package/dist/astryx.umd.js.map +3 -3
- package/package.json +1 -1
- package/src/Calendar/Calendar.test.tsx +27 -0
- package/src/Calendar/Calendar.tsx +6 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/NavMenu/NavMenu.doc.mjs +9 -0
- package/src/Table/BaseTable.tsx +6 -0
- package/src/Table/Table.test.tsx +23 -0
- package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.4-canary.
|
|
3
|
+
"version": "0.1.4-canary.3b5f42e",
|
|
4
4
|
"displayName": "XDS Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -215,6 +215,33 @@ describe('Calendar', () => {
|
|
|
215
215
|
expect(screen.getByText(/February 2026.*March 2026/)).toBeInTheDocument();
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
it('clamps out-of-range numberOfMonths to a single month', () => {
|
|
219
|
+
// 1000 would otherwise try to render 1000 month grids and lock the page up.
|
|
220
|
+
render(
|
|
221
|
+
<Calendar
|
|
222
|
+
numberOfMonths={1000 as unknown as 1 | 2}
|
|
223
|
+
focusDate="2026-01-01"
|
|
224
|
+
/>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Only the focused month renders — no second month, no range separator.
|
|
228
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
229
|
+
expect(screen.queryByText(/February 2026/)).not.toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('clamps numberOfMonths={0} to a single month', () => {
|
|
233
|
+
render(
|
|
234
|
+
<Calendar
|
|
235
|
+
numberOfMonths={0 as unknown as 1 | 2}
|
|
236
|
+
focusDate="2026-01-01"
|
|
237
|
+
/>,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// 0 previously rendered no months at all; now falls back to one.
|
|
241
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
242
|
+
expect(screen.getAllByRole('gridcell').length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
218
245
|
// ─── Display Options ─────────────────────────────────────────
|
|
219
246
|
|
|
220
247
|
it('shows week numbers when hasWeekNumbers is true', () => {
|
|
@@ -203,7 +203,7 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
203
203
|
value,
|
|
204
204
|
defaultValue,
|
|
205
205
|
onChange,
|
|
206
|
-
numberOfMonths = 1,
|
|
206
|
+
numberOfMonths: numberOfMonthsProp = 1,
|
|
207
207
|
min,
|
|
208
208
|
max,
|
|
209
209
|
dateConstraints,
|
|
@@ -223,6 +223,11 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
223
223
|
// DayOfWeek so all downstream date math keeps working with an index.
|
|
224
224
|
const weekStartsOn = normalizeDayOfWeek(weekStartsOnProp);
|
|
225
225
|
|
|
226
|
+
// `numberOfMonths` is typed `1 | 2`; defensively clamp anything else that
|
|
227
|
+
// slips through at runtime to 1 so `Array.from({length})` can't render an
|
|
228
|
+
// absurd number of month grids (e.g. `numberOfMonths={1000}`).
|
|
229
|
+
const numberOfMonths = numberOfMonthsProp === 2 ? 2 : 1;
|
|
230
|
+
|
|
226
231
|
// Today's date (memoized)
|
|
227
232
|
const today = useMemo(() => plainDateToday(), []);
|
|
228
233
|
|
|
@@ -4,7 +4,6 @@ exports[`FormLayout > matches snapshot for horizontal direction 1`] = `
|
|
|
4
4
|
<div
|
|
5
5
|
class="astryx-form-layout horizontal FormLayout__styles.base xdt5ytf x18g69wz FormLayout__styles.horizontal xrvj5dj x1mt1orb xu6a5m6"
|
|
6
6
|
data-direction="horizontal"
|
|
7
|
-
data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40; @astryxdesign/core:src/FormLayout/FormLayout.tsx:45"
|
|
8
7
|
data-testid="layout"
|
|
9
8
|
>
|
|
10
9
|
<input
|
|
@@ -20,7 +19,6 @@ exports[`FormLayout > matches snapshot for horizontal-labels direction 1`] = `
|
|
|
20
19
|
<div
|
|
21
20
|
class="astryx-form-layout horizontal-labels FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff"
|
|
22
21
|
data-direction="horizontal-labels"
|
|
23
|
-
data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40; @astryxdesign/core:src/FormLayout/FormLayout.tsx:50"
|
|
24
22
|
data-testid="layout"
|
|
25
23
|
>
|
|
26
24
|
<label>
|
|
@@ -36,7 +34,6 @@ exports[`FormLayout > matches snapshot for vertical direction 1`] = `
|
|
|
36
34
|
<div
|
|
37
35
|
class="astryx-form-layout vertical FormLayout__styles.base x78zum5 xdt5ytf x18g69wz"
|
|
38
36
|
data-direction="vertical"
|
|
39
|
-
data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40"
|
|
40
37
|
data-testid="layout"
|
|
41
38
|
>
|
|
42
39
|
<input
|
|
@@ -17,6 +17,15 @@ export const docs = {
|
|
|
17
17
|
'NavHeadingMenuItem renders individual selectable items. ' +
|
|
18
18
|
'Pass as the menu prop of SideNavHeading or TopNavHeading.',
|
|
19
19
|
},
|
|
20
|
+
playground: {
|
|
21
|
+
defaults: {
|
|
22
|
+
children: [
|
|
23
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Dashboard', href: '#'}},
|
|
24
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Analytics', href: '#'}},
|
|
25
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Settings', href: '#'}},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
20
29
|
props: [
|
|
21
30
|
{name: 'children', type: 'ReactNode', required: true, description: 'Menu items.'},
|
|
22
31
|
{name: 'size', type: "'sm' | 'md' | 'lg'", default: "'md'", description: 'Controls min-width and item padding.'},
|
package/src/Table/BaseTable.tsx
CHANGED
|
@@ -284,6 +284,12 @@ function areRowPropsEqual<T extends Record<string, unknown>>(
|
|
|
284
284
|
const nextItem = nextProps.item;
|
|
285
285
|
const keys = Object.keys(nextItem);
|
|
286
286
|
|
|
287
|
+
// A key deleted from nextItem would never be visited by the loop below,
|
|
288
|
+
// and the row would keep rendering the removed field's stale value.
|
|
289
|
+
if (Object.keys(prevItem).length !== keys.length) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
287
293
|
for (const key of keys) {
|
|
288
294
|
if (prevItem[key] !== nextItem[key]) {
|
|
289
295
|
return false;
|
package/src/Table/Table.test.tsx
CHANGED
|
@@ -617,6 +617,29 @@ describe('BaseTable', () => {
|
|
|
617
617
|
// =============================================================================
|
|
618
618
|
|
|
619
619
|
describe('Table', () => {
|
|
620
|
+
it('clears a cell when the field is removed from the row object (#3595)', () => {
|
|
621
|
+
const cols: TableColumn<Record<string, unknown>>[] = [
|
|
622
|
+
{key: 'name', header: 'Name'},
|
|
623
|
+
{key: 'status', header: 'Status'},
|
|
624
|
+
];
|
|
625
|
+
const {rerender} = render(
|
|
626
|
+
<Table
|
|
627
|
+
data={[{id: '1', name: 'Alice', status: 'active'}]}
|
|
628
|
+
columns={cols}
|
|
629
|
+
idKey="id"
|
|
630
|
+
/>,
|
|
631
|
+
);
|
|
632
|
+
expect(screen.getByText('active')).toBeInTheDocument();
|
|
633
|
+
|
|
634
|
+
// Clearing a field by omitting it (optimistic update / server response)
|
|
635
|
+
// must re-render the row — the memo previously only compared the keys of
|
|
636
|
+
// the NEW item, so the deleted field's stale value kept rendering.
|
|
637
|
+
rerender(
|
|
638
|
+
<Table data={[{id: '1', name: 'Alice'}]} columns={cols} idKey="id" />,
|
|
639
|
+
);
|
|
640
|
+
expect(screen.queryByText('active')).not.toBeInTheDocument();
|
|
641
|
+
});
|
|
642
|
+
|
|
620
643
|
it('renders a table with correct structure', () => {
|
|
621
644
|
render(<Table data={users} columns={columns} />);
|
|
622
645
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
@@ -8,6 +8,16 @@ export const docs = {
|
|
|
8
8
|
displayName: 'Base Typeahead',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
10
|
description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
|
|
11
|
+
usage: {
|
|
12
|
+
description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
|
|
13
|
+
bestPractices: [
|
|
14
|
+
{ guidance: true, description: 'Use Typeahead or Tokenizer for standard fields — they wrap BaseTypeahead with the wrapper div, border styling, and token rendering it intentionally omits.' },
|
|
15
|
+
{ guidance: true, description: 'Provide your own wrapper div with border and layout when composing directly — BaseTypeahead renders no visual chrome of its own.' },
|
|
16
|
+
{ guidance: true, description: 'Pass anchorRef pointing to your wrapper so the dropdown positions against your custom input chrome, not just the bare input element.' },
|
|
17
|
+
{ guidance: false, description: 'Expect a wrapper div, border, or token rendering — BaseTypeahead is an engine only; all visual chrome is the caller\'s responsibility.' },
|
|
18
|
+
{ guidance: false, description: 'Use BaseTypeahead when Typeahead or Tokenizer would suffice — the extra wrapper and styling work is only justified for truly custom compositions.' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
11
21
|
props: [
|
|
12
22
|
{
|
|
13
23
|
name: 'searchSource',
|