@astryxdesign/core 0.1.0-canary.e0850b3 → 0.1.0-canary.e2d38fb
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/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +5 -1
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +15 -2
- package/package.json +2 -2
- package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
- package/src/DateInput/DateInput.test.tsx +68 -20
- package/src/utils/dateParser.test.ts +26 -0
- package/src/utils/dateParser.ts +16 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAwDD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,SAAS,EACT,KAAK,EACL,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,GACN,EAAE,2BAA2B,qBAwB7B;yBAhCe,sBAAsB"}
|
|
@@ -43,6 +43,10 @@ const styles = {
|
|
|
43
43
|
khDVqt: "xuxw1ft",
|
|
44
44
|
kg3NbH: "xf314gf",
|
|
45
45
|
$$css: true
|
|
46
|
+
},
|
|
47
|
+
buttonWithLabel: {
|
|
48
|
+
kwRFfy: "x1t818jl",
|
|
49
|
+
$$css: true
|
|
46
50
|
}
|
|
47
51
|
};
|
|
48
52
|
|
|
@@ -95,7 +99,7 @@ export function ChatLayoutScrollButton({
|
|
|
95
99
|
variant: "ghost",
|
|
96
100
|
size: "md",
|
|
97
101
|
onClick: onClick,
|
|
98
|
-
xstyle: styles.button,
|
|
102
|
+
xstyle: [styles.button, label ? styles.buttonWithLabel : null],
|
|
99
103
|
children: label ?? undefined
|
|
100
104
|
})
|
|
101
105
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsG9D"}
|
package/dist/utils/dateParser.js
CHANGED
|
@@ -112,10 +112,23 @@ export function parseDateInput(input) {
|
|
|
112
112
|
return parseNumericDate(+first, +second, currentYear);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// 6. Fall back to native Date parsing for other formats
|
|
115
|
+
// 6. Fall back to native Date parsing for other formats.
|
|
116
|
+
//
|
|
117
|
+
// Skip bare numeric input (e.g. "0", "1", "01", "2026"). These are
|
|
118
|
+
// in-progress values a user is still typing, not complete dates. Native
|
|
119
|
+
// `Date` parsing coerces them into arbitrary dates ("0" -> year 2000 in V8,
|
|
120
|
+
// year 0 in some engines), which is both surprising and — when the year
|
|
121
|
+
// resolves to 0 — produces an out-of-range date that throws downstream.
|
|
122
|
+
// Treat them as not-yet-a-valid-date instead.
|
|
123
|
+
if (/^\d+$/.test(trimmed)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
116
126
|
const parsed = new Date(trimmed);
|
|
117
127
|
if (!isNaN(parsed.getTime())) {
|
|
118
|
-
|
|
128
|
+
const fromDate = plainDateFromDate(parsed);
|
|
129
|
+
// Validate the result so we never return an out-of-range date (e.g. a
|
|
130
|
+
// year of 0), which would throw when later re-parsed.
|
|
131
|
+
return tryCreatePlainDate(fromDate.year, fromDate.month, fromDate.day);
|
|
119
132
|
}
|
|
120
133
|
return null;
|
|
121
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.e2d38fb",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"astryx",
|
|
19
19
|
"design-system",
|
|
20
20
|
"react",
|
|
21
21
|
"components",
|
|
@@ -90,6 +90,12 @@ const styles = stylex.create({
|
|
|
90
90
|
whiteSpace: 'nowrap',
|
|
91
91
|
paddingInline: spacingVars['--spacing-2'],
|
|
92
92
|
},
|
|
93
|
+
// When a label is shown, the icon sits on the leading edge and the text on
|
|
94
|
+
// the trailing edge. Symmetric padding leaves the text cramped against the
|
|
95
|
+
// pill's rounded edge, so give the trailing side extra breathing room.
|
|
96
|
+
buttonWithLabel: {
|
|
97
|
+
paddingInlineEnd: spacingVars['--spacing-3'],
|
|
98
|
+
},
|
|
93
99
|
});
|
|
94
100
|
|
|
95
101
|
// =============================================================================
|
|
@@ -130,7 +136,7 @@ export function ChatLayoutScrollButton({
|
|
|
130
136
|
variant="ghost"
|
|
131
137
|
size="md"
|
|
132
138
|
onClick={onClick}
|
|
133
|
-
xstyle={styles.button}>
|
|
139
|
+
xstyle={[styles.button, label ? styles.buttonWithLabel : null]}>
|
|
134
140
|
{label ?? undefined}
|
|
135
141
|
</Button>
|
|
136
142
|
</div>
|
|
@@ -21,19 +21,13 @@ describe('DateInput', () => {
|
|
|
21
21
|
|
|
22
22
|
it('renders with placeholder', () => {
|
|
23
23
|
render(
|
|
24
|
-
<DateInput
|
|
25
|
-
label="Date"
|
|
26
|
-
onChange={() => {}}
|
|
27
|
-
placeholder="Pick a date"
|
|
28
|
-
/>,
|
|
24
|
+
<DateInput label="Date" onChange={() => {}} placeholder="Pick a date" />,
|
|
29
25
|
);
|
|
30
26
|
expect(screen.getByPlaceholderText('Pick a date')).toBeInTheDocument();
|
|
31
27
|
});
|
|
32
28
|
|
|
33
29
|
it('displays formatted date when value is provided', () => {
|
|
34
|
-
render(
|
|
35
|
-
<DateInput label="Date" value="2026-01-25" onChange={() => {}} />,
|
|
36
|
-
);
|
|
30
|
+
render(<DateInput label="Date" value="2026-01-25" onChange={() => {}} />);
|
|
37
31
|
expect(screen.getByDisplayValue('January 25, 2026')).toBeInTheDocument();
|
|
38
32
|
});
|
|
39
33
|
|
|
@@ -118,9 +112,7 @@ describe('DateInput', () => {
|
|
|
118
112
|
|
|
119
113
|
it('reverts to previous value on blur when input is invalid', async () => {
|
|
120
114
|
const onChange = vi.fn();
|
|
121
|
-
render(
|
|
122
|
-
<DateInput label="Date" value="2026-01-25" onChange={onChange} />,
|
|
123
|
-
);
|
|
115
|
+
render(<DateInput label="Date" value="2026-01-25" onChange={onChange} />);
|
|
124
116
|
|
|
125
117
|
const input = screen.getByRole('combobox');
|
|
126
118
|
fireEvent.change(input, {target: {value: 'not a date'}});
|
|
@@ -299,9 +291,7 @@ describe('DateInput', () => {
|
|
|
299
291
|
// --- P1: Tab order: calendar button first, then input ---
|
|
300
292
|
|
|
301
293
|
it('renders calendar button before input in DOM order', () => {
|
|
302
|
-
const {container} = render(
|
|
303
|
-
<DateInput label="Date" onChange={() => {}} />,
|
|
304
|
-
);
|
|
294
|
+
const {container} = render(<DateInput label="Date" onChange={() => {}} />);
|
|
305
295
|
const input = container.querySelector('input');
|
|
306
296
|
const button = container.querySelector('button');
|
|
307
297
|
// Calendar button should come before input in the DOM
|
|
@@ -389,9 +379,7 @@ describe('DateInput', () => {
|
|
|
389
379
|
|
|
390
380
|
it('calls onChange with undefined when input is cleared and blurred', () => {
|
|
391
381
|
const onChange = vi.fn();
|
|
392
|
-
render(
|
|
393
|
-
<DateInput label="Date" value="2026-01-25" onChange={onChange} />,
|
|
394
|
-
);
|
|
382
|
+
render(<DateInput label="Date" value="2026-01-25" onChange={onChange} />);
|
|
395
383
|
|
|
396
384
|
const input = screen.getByRole('combobox');
|
|
397
385
|
fireEvent.change(input, {target: {value: ''}});
|
|
@@ -476,9 +464,7 @@ describe('DateInput', () => {
|
|
|
476
464
|
});
|
|
477
465
|
|
|
478
466
|
it('does not show clear button when hasClear is false', () => {
|
|
479
|
-
render(
|
|
480
|
-
<DateInput label="Date" value="2026-01-15" onChange={() => {}} />,
|
|
481
|
-
);
|
|
467
|
+
render(<DateInput label="Date" value="2026-01-15" onChange={() => {}} />);
|
|
482
468
|
expect(
|
|
483
469
|
screen.queryByRole('button', {name: 'Clear Date'}),
|
|
484
470
|
).not.toBeInTheDocument();
|
|
@@ -514,6 +500,68 @@ describe('DateInput', () => {
|
|
|
514
500
|
});
|
|
515
501
|
});
|
|
516
502
|
|
|
503
|
+
// --- Regression: in-progress / leading-zero input must not crash ---
|
|
504
|
+
|
|
505
|
+
describe('incomplete typed input', () => {
|
|
506
|
+
it('does not crash or fire onChange when first digit typed is 0', () => {
|
|
507
|
+
const onChange = vi.fn();
|
|
508
|
+
render(<DateInput label="Date" onChange={onChange} />);
|
|
509
|
+
|
|
510
|
+
const input = screen.getByRole('combobox');
|
|
511
|
+
// Typing a leading "0" (e.g. starting "01" for January) must be treated
|
|
512
|
+
// as incomplete input, not coerced into an (invalid) date that crashes.
|
|
513
|
+
expect(() =>
|
|
514
|
+
fireEvent.change(input, {target: {value: '0'}}),
|
|
515
|
+
).not.toThrow();
|
|
516
|
+
|
|
517
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
518
|
+
expect(input).toHaveValue('0');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('does not crash or fire onChange when first digit typed is 1', () => {
|
|
522
|
+
const onChange = vi.fn();
|
|
523
|
+
render(<DateInput label="Date" onChange={onChange} />);
|
|
524
|
+
|
|
525
|
+
const input = screen.getByRole('combobox');
|
|
526
|
+
expect(() =>
|
|
527
|
+
fireEvent.change(input, {target: {value: '1'}}),
|
|
528
|
+
).not.toThrow();
|
|
529
|
+
|
|
530
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
531
|
+
expect(input).toHaveValue('1');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('does not crash while progressively typing a numeric date', () => {
|
|
535
|
+
const onChange = vi.fn();
|
|
536
|
+
render(<DateInput label="Date" onChange={onChange} />);
|
|
537
|
+
|
|
538
|
+
const input = screen.getByRole('combobox');
|
|
539
|
+
// Simulate keystroke-by-keystroke entry of "01/15/2026". The leading
|
|
540
|
+
// single-digit keystrokes must not crash (the original bug).
|
|
541
|
+
for (const partial of ['0', '01', '01/', '01/1', '01/15', '01/15/']) {
|
|
542
|
+
expect(() =>
|
|
543
|
+
fireEvent.change(input, {target: {value: partial}}),
|
|
544
|
+
).not.toThrow();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Completing the date commits it without error.
|
|
548
|
+
expect(() =>
|
|
549
|
+
fireEvent.change(input, {target: {value: '01/15/2026'}}),
|
|
550
|
+
).not.toThrow();
|
|
551
|
+
expect(onChange).toHaveBeenCalledWith('2026-01-15');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('does not crash on blur after typing an incomplete value', () => {
|
|
555
|
+
const onChange = vi.fn();
|
|
556
|
+
render(<DateInput label="Date" onChange={onChange} />);
|
|
557
|
+
|
|
558
|
+
const input = screen.getByRole('combobox');
|
|
559
|
+
fireEvent.change(input, {target: {value: '0'}});
|
|
560
|
+
expect(() => fireEvent.blur(input)).not.toThrow();
|
|
561
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
517
565
|
describe('external value changes', () => {
|
|
518
566
|
it('clears pending input when value changes externally', () => {
|
|
519
567
|
const onChange = vi.fn();
|
|
@@ -320,5 +320,31 @@ describe('parseDateInput', () => {
|
|
|
320
320
|
it('rejects mixed separators', () => {
|
|
321
321
|
expect(parseDateInput('1/25.2026')).toBeNull();
|
|
322
322
|
});
|
|
323
|
+
|
|
324
|
+
it('treats a single typed digit as incomplete, not a date', () => {
|
|
325
|
+
// A user starting to type a month (e.g. "0" or "1" for January) should
|
|
326
|
+
// not produce a date. Native Date parsing would otherwise coerce these
|
|
327
|
+
// into arbitrary dates (and a year of 0 in some engines).
|
|
328
|
+
expect(parseDateInput('0')).toBeNull();
|
|
329
|
+
expect(parseDateInput('1')).toBeNull();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('treats bare numeric input as incomplete, not a date', () => {
|
|
333
|
+
expect(parseDateInput('00')).toBeNull();
|
|
334
|
+
expect(parseDateInput('01')).toBeNull();
|
|
335
|
+
expect(parseDateInput('12')).toBeNull();
|
|
336
|
+
expect(parseDateInput('2026')).toBeNull();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('never returns an out-of-range date for partial input', () => {
|
|
340
|
+
// Regression: partial input must never yield a date with year < 1,
|
|
341
|
+
// which would throw when later re-parsed and crash the page.
|
|
342
|
+
for (const input of ['0', '1', '01', '00', '9', '99']) {
|
|
343
|
+
const result = parseDateInput(input);
|
|
344
|
+
if (result !== null) {
|
|
345
|
+
expect(result.year).toBeGreaterThanOrEqual(1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
323
349
|
});
|
|
324
350
|
});
|
package/src/utils/dateParser.ts
CHANGED
|
@@ -124,10 +124,24 @@ export function parseDateInput(input: string): PlainDate | null {
|
|
|
124
124
|
return parseNumericDate(+first, +second, currentYear);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// 6. Fall back to native Date parsing for other formats
|
|
127
|
+
// 6. Fall back to native Date parsing for other formats.
|
|
128
|
+
//
|
|
129
|
+
// Skip bare numeric input (e.g. "0", "1", "01", "2026"). These are
|
|
130
|
+
// in-progress values a user is still typing, not complete dates. Native
|
|
131
|
+
// `Date` parsing coerces them into arbitrary dates ("0" -> year 2000 in V8,
|
|
132
|
+
// year 0 in some engines), which is both surprising and — when the year
|
|
133
|
+
// resolves to 0 — produces an out-of-range date that throws downstream.
|
|
134
|
+
// Treat them as not-yet-a-valid-date instead.
|
|
135
|
+
if (/^\d+$/.test(trimmed)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
const parsed = new Date(trimmed);
|
|
129
140
|
if (!isNaN(parsed.getTime())) {
|
|
130
|
-
|
|
141
|
+
const fromDate = plainDateFromDate(parsed);
|
|
142
|
+
// Validate the result so we never return an out-of-range date (e.g. a
|
|
143
|
+
// year of 0), which would throw when later re-parsed.
|
|
144
|
+
return tryCreatePlainDate(fromDate.year, fromDate.month, fromDate.day);
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
return null;
|