@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4
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/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +7 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +58 -9
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +34 -54
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +13 -10
- package/src/TimeInput/TimeInput.test.tsx +139 -7
- package/src/TimeInput/TimeInput.tsx +53 -24
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
13
|
-
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
13
|
+
import {act, render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import * as stylex from '@stylexjs/stylex';
|
|
16
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
15
17
|
import {TimeInput} from './TimeInput';
|
|
16
18
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
17
19
|
import {InternationalizationProvider} from '../i18n';
|
|
@@ -65,6 +67,26 @@ describe('TimeInput', () => {
|
|
|
65
67
|
expect(onChange).toHaveBeenCalledWith('14:31');
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
it('does not step into a fractional minute when increment is fractional', () => {
|
|
71
|
+
const onChange = vi.fn();
|
|
72
|
+
render(
|
|
73
|
+
<TimeInput
|
|
74
|
+
label="Time"
|
|
75
|
+
value={'14:30' as ISOTimeString}
|
|
76
|
+
increment={0.5}
|
|
77
|
+
onChange={onChange}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
const input = screen.getByLabelText('Time');
|
|
81
|
+
|
|
82
|
+
// `increment` is documented as minutes and typed `number`. A fractional one
|
|
83
|
+
// used to reach formatISOTime unrounded, which pads "30.5" to two
|
|
84
|
+
// characters and leaves it, so the arrow key handed the consumer a value
|
|
85
|
+
// that is no longer a time at all.
|
|
86
|
+
fireEvent.keyDown(input, {key: 'ArrowUp'});
|
|
87
|
+
expect(onChange).toHaveBeenCalledWith('14:30');
|
|
88
|
+
});
|
|
89
|
+
|
|
68
90
|
it('displays formatted time in 12h format', () => {
|
|
69
91
|
render(
|
|
70
92
|
<TimeInput
|
|
@@ -121,9 +143,41 @@ describe('TimeInput', () => {
|
|
|
121
143
|
);
|
|
122
144
|
});
|
|
123
145
|
|
|
124
|
-
it('
|
|
146
|
+
it('keeps a disabled input focusable without native disabled (AST-085 FR1)', () => {
|
|
147
|
+
render(<TimeInput label="Time" isDisabled onChange={() => {}} />);
|
|
148
|
+
const input = screen.getByRole('textbox');
|
|
149
|
+
expect(input).not.toHaveAttribute('disabled');
|
|
150
|
+
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
151
|
+
expect(input).toHaveAttribute('readonly');
|
|
152
|
+
input.focus();
|
|
153
|
+
expect(input).toHaveFocus();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('restores the focus ring on a focused disabled field (AST-085 FR6)', () => {
|
|
157
|
+
// The same declarations as the field's ring style compile to the same
|
|
158
|
+
// atomic classes; the shared disabled style alone drops them.
|
|
159
|
+
const ringClasses = stylex
|
|
160
|
+
.props(
|
|
161
|
+
stylex.create({
|
|
162
|
+
ring: {
|
|
163
|
+
borderColor: {
|
|
164
|
+
default: colorVars['--color-border-emphasized'],
|
|
165
|
+
':focus-within': colorVars['--color-accent'],
|
|
166
|
+
},
|
|
167
|
+
boxShadow: {
|
|
168
|
+
default: 'none',
|
|
169
|
+
':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}).ring,
|
|
173
|
+
)
|
|
174
|
+
.className!.split(' ')
|
|
175
|
+
.filter(name => !name.includes('__'));
|
|
125
176
|
render(<TimeInput label="Time" isDisabled onChange={() => {}} />);
|
|
126
|
-
|
|
177
|
+
const wrapper = screen.getByRole('textbox').closest('.astryx-time-input')!;
|
|
178
|
+
for (const name of ringClasses) {
|
|
179
|
+
expect(wrapper).toHaveClass(name);
|
|
180
|
+
}
|
|
127
181
|
});
|
|
128
182
|
|
|
129
183
|
it('shows clear button when hasClear is true and value exists', () => {
|
|
@@ -625,11 +679,89 @@ describe('TimeInput', () => {
|
|
|
625
679
|
expect(onChange).not.toHaveBeenCalled();
|
|
626
680
|
});
|
|
627
681
|
|
|
628
|
-
it('
|
|
629
|
-
|
|
682
|
+
it('blocks typing, arrows and a caller onClick when disabled without a reason (AST-085 FR2)', async () => {
|
|
683
|
+
const user = userEvent.setup();
|
|
684
|
+
const onChange = vi.fn();
|
|
685
|
+
const onClick = vi.fn();
|
|
686
|
+
render(
|
|
687
|
+
<TimeInput
|
|
688
|
+
label="Time"
|
|
689
|
+
value={'09:00' as ISOTimeString}
|
|
690
|
+
onChange={onChange}
|
|
691
|
+
onClick={onClick}
|
|
692
|
+
isDisabled
|
|
693
|
+
/>,
|
|
694
|
+
);
|
|
695
|
+
const input = screen.getByLabelText('Time');
|
|
696
|
+
await user.click(input);
|
|
697
|
+
await user.type(input, '1');
|
|
698
|
+
await user.keyboard('{ArrowUp}');
|
|
699
|
+
await user.tab();
|
|
700
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
701
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it.each([
|
|
705
|
+
['text-input', false, {isDisabled: true}],
|
|
706
|
+
['native', true, {isDisabled: true}],
|
|
707
|
+
['bottom-sheet', true, {isDisabled: true}],
|
|
708
|
+
['bottom-sheet', true, {isLoading: true}],
|
|
709
|
+
] as const)(
|
|
710
|
+
'does not submit a surrounding form on Enter: %s (coarse %s) %o (AST-085 FR2)',
|
|
711
|
+
async (presentation, isCoarse, state) => {
|
|
712
|
+
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
713
|
+
matches: /pointer:\s*coarse/.test(query)
|
|
714
|
+
? isCoarse
|
|
715
|
+
: /pointer:\s*fine/.test(query) && !isCoarse,
|
|
716
|
+
media: query,
|
|
717
|
+
addEventListener: () => {},
|
|
718
|
+
removeEventListener: () => {},
|
|
719
|
+
addListener: () => {},
|
|
720
|
+
removeListener: () => {},
|
|
721
|
+
}));
|
|
722
|
+
const user = userEvent.setup();
|
|
723
|
+
const onSubmit = vi.fn((e: React.FormEvent) => e.preventDefault());
|
|
724
|
+
render(
|
|
725
|
+
<form onSubmit={onSubmit}>
|
|
726
|
+
<TimeInput
|
|
727
|
+
label="Time"
|
|
728
|
+
presentation={presentation}
|
|
729
|
+
value={'09:00' as ISOTimeString}
|
|
730
|
+
onChange={() => {}}
|
|
731
|
+
{...state}
|
|
732
|
+
/>
|
|
733
|
+
<button type="submit">Save</button>
|
|
734
|
+
</form>,
|
|
735
|
+
);
|
|
736
|
+
try {
|
|
737
|
+
const input = document.querySelector<HTMLInputElement>(
|
|
738
|
+
'.astryx-time-input input:not([type=hidden])',
|
|
739
|
+
)!;
|
|
740
|
+
act(() => input.focus());
|
|
741
|
+
await user.keyboard('{Enter}');
|
|
742
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
743
|
+
} finally {
|
|
744
|
+
vi.unstubAllGlobals();
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
it('drops a draft instead of committing it when the field becomes disabled (AST-085 FR2)', async () => {
|
|
750
|
+
const user = userEvent.setup();
|
|
751
|
+
const onChange = vi.fn();
|
|
752
|
+
const value = '09:00' as ISOTimeString;
|
|
753
|
+
const {rerender} = render(
|
|
754
|
+
<TimeInput label="Time" value={value} onChange={onChange} />,
|
|
755
|
+
);
|
|
630
756
|
const input = screen.getByLabelText('Time');
|
|
631
|
-
|
|
632
|
-
|
|
757
|
+
await user.click(input);
|
|
758
|
+
await user.clear(input);
|
|
759
|
+
onChange.mockClear();
|
|
760
|
+
rerender(
|
|
761
|
+
<TimeInput label="Time" value={value} onChange={onChange} isDisabled />,
|
|
762
|
+
);
|
|
763
|
+
await user.tab();
|
|
764
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
633
765
|
});
|
|
634
766
|
|
|
635
767
|
it('does not swap in the format-hint placeholder on focus while disabled', () => {
|
|
@@ -93,6 +93,18 @@ import {useDevWarning} from '../hooks/useDevWarning';
|
|
|
93
93
|
const TOUCH_POINTER_QUERY = '(pointer: coarse)';
|
|
94
94
|
|
|
95
95
|
const styles = stylex.create({
|
|
96
|
+
// The shared disabled style drops the focus ring; restore it while a
|
|
97
|
+
// control inside the field holds focus (spec:AST-085 FR6). Defaults repeat it.
|
|
98
|
+
disabledFocusRing: {
|
|
99
|
+
borderColor: {
|
|
100
|
+
default: colorVars['--color-border-emphasized'],
|
|
101
|
+
':focus-within': colorVars['--color-accent'],
|
|
102
|
+
},
|
|
103
|
+
boxShadow: {
|
|
104
|
+
default: 'none',
|
|
105
|
+
':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
96
108
|
icon: {
|
|
97
109
|
display: 'flex',
|
|
98
110
|
alignItems: 'center',
|
|
@@ -207,12 +219,12 @@ export interface TimeInputProps extends Omit<
|
|
|
207
219
|
/**
|
|
208
220
|
* Explains why the input is disabled. When set together with
|
|
209
221
|
* `isDisabled`, the input shows a tooltip with this text on hover and
|
|
210
|
-
* keyboard focus, and the
|
|
211
|
-
*
|
|
212
|
-
*
|
|
222
|
+
* keyboard focus, and the input is described by it, so the reason is
|
|
223
|
+
* discoverable by keyboard and assistive technology. Typing and arrow-key
|
|
224
|
+
* adjustment stay blocked.
|
|
213
225
|
*
|
|
214
|
-
* Use this instead of wrapping a disabled input in `Tooltip
|
|
215
|
-
*
|
|
226
|
+
* Use this instead of wrapping a disabled input in `Tooltip`, so the
|
|
227
|
+
* reason is tied to the field.
|
|
216
228
|
*
|
|
217
229
|
* @example
|
|
218
230
|
* ```
|
|
@@ -286,7 +298,8 @@ export interface TimeInputProps extends Omit<
|
|
|
286
298
|
hourFormat?: TimeInputHourFormat;
|
|
287
299
|
|
|
288
300
|
/**
|
|
289
|
-
* Increment in minutes when using arrow keys.
|
|
301
|
+
* Increment in minutes when using arrow keys. Steps are whole minutes: a
|
|
302
|
+
* fractional value leaves the time unchanged.
|
|
290
303
|
* @default 1
|
|
291
304
|
*/
|
|
292
305
|
increment?: number;
|
|
@@ -478,10 +491,10 @@ function TimeField({
|
|
|
478
491
|
}
|
|
479
492
|
}, [announce, inputGroup, status?.message, status?.type]);
|
|
480
493
|
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
494
|
+
// A disabled field never renders native `disabled` (spec:AST-085 FR1): the
|
|
495
|
+
// input stays focusable via aria-disabled, and readOnly plus the value
|
|
496
|
+
// mutation guards block typing. The disabled-reason tooltip's listeners
|
|
497
|
+
// attach to the input container.
|
|
485
498
|
const showsDisabledMessage = isDisabled && !!disabledMessage;
|
|
486
499
|
const disabledMessageTooltip = useTooltip({
|
|
487
500
|
placement: 'above',
|
|
@@ -576,8 +589,8 @@ function TimeField({
|
|
|
576
589
|
// Handle input text change - update immediately if valid
|
|
577
590
|
const handleInputChange = useCallback(
|
|
578
591
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
579
|
-
//
|
|
580
|
-
//
|
|
592
|
+
// The input is never natively disabled (spec:AST-085 FR1), so guard
|
|
593
|
+
// value mutation explicitly (readOnly also blocks typing).
|
|
581
594
|
if (isDisabled) {
|
|
582
595
|
return;
|
|
583
596
|
}
|
|
@@ -612,6 +625,11 @@ function TimeField({
|
|
|
612
625
|
if (pendingInput === null) {
|
|
613
626
|
return;
|
|
614
627
|
}
|
|
628
|
+
// Disabled: keep the value, drop the draft (spec:AST-085 FR2).
|
|
629
|
+
if (isDisabled) {
|
|
630
|
+
setPendingInput(null);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
615
633
|
|
|
616
634
|
if (!pendingInput.trim()) {
|
|
617
635
|
// Empty input clears the value
|
|
@@ -632,10 +650,17 @@ function TimeField({
|
|
|
632
650
|
// Clear pending input - display will revert to formatted value
|
|
633
651
|
setPendingInput(null);
|
|
634
652
|
},
|
|
635
|
-
[pendingInput, value, fireChange, hasSeconds, min, max],
|
|
653
|
+
[pendingInput, isDisabled, value, fireChange, hasSeconds, min, max],
|
|
636
654
|
);
|
|
637
655
|
|
|
638
656
|
// Handle keyboard navigation on input
|
|
657
|
+
// Enter in a focused disabled field would submit a surrounding form (FR2).
|
|
658
|
+
const preventDisabledEnter = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
659
|
+
if (isDisabled && e.key === 'Enter') {
|
|
660
|
+
e.preventDefault();
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
639
664
|
const handleInputKeyDown = useCallback(
|
|
640
665
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
641
666
|
// ArrowUp/ArrowDown step the time and preventDefault; an IME candidate
|
|
@@ -645,8 +670,7 @@ function TimeField({
|
|
|
645
670
|
if (isImeKeyEvent(e.nativeEvent)) {
|
|
646
671
|
return;
|
|
647
672
|
}
|
|
648
|
-
// Arrow-key adjustment mutates the value; block it while
|
|
649
|
-
// disabled reason (the input keeps focusability via aria-disabled).
|
|
673
|
+
// Arrow-key adjustment mutates the value; block it while disabled.
|
|
650
674
|
if (isDisabled) {
|
|
651
675
|
return;
|
|
652
676
|
}
|
|
@@ -743,6 +767,7 @@ function TimeField({
|
|
|
743
767
|
inputWrapperStyles.base,
|
|
744
768
|
sizeStyles[size],
|
|
745
769
|
isDisabled && inputWrapperStyles.disabled,
|
|
770
|
+
isDisabled && styles.disabledFocusRing,
|
|
746
771
|
status && inputStatusBorderStyles[status.type],
|
|
747
772
|
status && !isDisabled && inputStatusHoverShadowStyles[status.type],
|
|
748
773
|
status && inputStatusFocusWithinStyles[status.type],
|
|
@@ -777,7 +802,7 @@ function TimeField({
|
|
|
777
802
|
max={max}
|
|
778
803
|
hourFormat={hourFormat}
|
|
779
804
|
isEffectivelyDisabled={isDisabled}
|
|
780
|
-
|
|
805
|
+
isFocusableDisabled
|
|
781
806
|
isEffectivelyRequired={isEffectivelyRequired}
|
|
782
807
|
isBusy={isBusy}
|
|
783
808
|
statusType={status?.type}
|
|
@@ -786,7 +811,8 @@ function TimeField({
|
|
|
786
811
|
...controlProps,
|
|
787
812
|
onFocus: onFocusProp,
|
|
788
813
|
onBlur: onBlurProp,
|
|
789
|
-
onKeyDown: onKeyDownProp,
|
|
814
|
+
onKeyDown: chainEventHandlers(preventDisabledEnter, onKeyDownProp),
|
|
815
|
+
onClick: isDisabled ? undefined : controlProps.onClick,
|
|
790
816
|
}}
|
|
791
817
|
/>
|
|
792
818
|
) : (
|
|
@@ -803,14 +829,17 @@ function TimeField({
|
|
|
803
829
|
onChange={handleInputChange}
|
|
804
830
|
onFocus={chainEventHandlers(handleFocus, onFocusProp)}
|
|
805
831
|
onBlur={chainEventHandlers(handleBlur, onBlurProp)}
|
|
806
|
-
onKeyDown={chainEventHandlers(
|
|
832
|
+
onKeyDown={chainEventHandlers(
|
|
833
|
+
preventDisabledEnter,
|
|
834
|
+
handleInputKeyDown,
|
|
835
|
+
onKeyDownProp,
|
|
836
|
+
)}
|
|
807
837
|
placeholder={displayPlaceholder}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
readOnly={showsDisabledMessage || undefined}
|
|
838
|
+
aria-disabled={isDisabled ? 'true' : undefined}
|
|
839
|
+
readOnly={isDisabled || undefined}
|
|
840
|
+
// The browser still dispatches a click on an aria-disabled input,
|
|
841
|
+
// so a caller's onClick is skipped too (spec:AST-085 FR2).
|
|
842
|
+
onClick={isDisabled ? undefined : controlProps.onClick}
|
|
814
843
|
autoFocus={hasAutoFocus}
|
|
815
844
|
data-autofocus={hasAutoFocus || undefined}
|
|
816
845
|
aria-describedby={ariaDescribedBy}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
useRef,
|
|
25
25
|
useState,
|
|
26
26
|
useTransition,
|
|
27
|
+
type KeyboardEvent,
|
|
27
28
|
} from 'react';
|
|
28
29
|
import * as stylex from '@stylexjs/stylex';
|
|
29
30
|
import {BottomSheet} from '../BottomSheet';
|
|
@@ -45,7 +46,7 @@ import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
|
45
46
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
46
47
|
import {Icon} from '../Icon';
|
|
47
48
|
import {useTranslator} from '../i18n';
|
|
48
|
-
import {sizeVars, spacingVars} from '../theme/tokens.stylex';
|
|
49
|
+
import {colorVars, sizeVars, spacingVars} from '../theme/tokens.stylex';
|
|
49
50
|
import {useSize} from '../SizeContext/SizeContext';
|
|
50
51
|
import {Spinner} from '../Spinner';
|
|
51
52
|
import {useTooltip} from '../Tooltip';
|
|
@@ -74,6 +75,17 @@ const sizeStyles = stylex.create({
|
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
const styles = stylex.create({
|
|
78
|
+
// Same as TimeInput's: the focus ring for a focused disabled field.
|
|
79
|
+
disabledFocusRing: {
|
|
80
|
+
borderColor: {
|
|
81
|
+
default: colorVars['--color-border-emphasized'],
|
|
82
|
+
':focus-within': colorVars['--color-accent'],
|
|
83
|
+
},
|
|
84
|
+
boxShadow: {
|
|
85
|
+
default: 'none',
|
|
86
|
+
':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
77
89
|
iconButton: {
|
|
78
90
|
display: 'flex',
|
|
79
91
|
alignItems: 'center',
|
|
@@ -447,6 +459,7 @@ export function TouchTimeField({
|
|
|
447
459
|
inputWrapperStyles.base,
|
|
448
460
|
sizeStyles[size],
|
|
449
461
|
isDisabled && inputWrapperStyles.disabled,
|
|
462
|
+
isDisabled && styles.disabledFocusRing,
|
|
450
463
|
status && inputStatusBorderStyles[status.type],
|
|
451
464
|
status && !isDisabled && inputStatusHoverShadowStyles[status.type],
|
|
452
465
|
status && inputStatusFocusWithinStyles[status.type],
|
|
@@ -459,7 +472,7 @@ export function TouchTimeField({
|
|
|
459
472
|
<button
|
|
460
473
|
type="button"
|
|
461
474
|
onClick={openSheet}
|
|
462
|
-
disabled={isEffectivelyDisabled}
|
|
475
|
+
aria-disabled={isEffectivelyDisabled || undefined}
|
|
463
476
|
aria-label={t('@astryx.timeInput.openPicker', {label})}
|
|
464
477
|
tabIndex={-1}
|
|
465
478
|
{...stylex.props(focusOutlineStyles.focusVisible, styles.iconButton)}>
|
|
@@ -475,10 +488,19 @@ export function TouchTimeField({
|
|
|
475
488
|
readOnly
|
|
476
489
|
inputMode="none"
|
|
477
490
|
onChange={() => {}}
|
|
478
|
-
onClick={
|
|
491
|
+
onClick={
|
|
492
|
+
isEffectivelyDisabled
|
|
493
|
+
? undefined
|
|
494
|
+
: chainEventHandlers(openSheet, onClickProp)
|
|
495
|
+
}
|
|
496
|
+
// Enter would submit a surrounding form while disabled (FR2).
|
|
497
|
+
onKeyDown={chainEventHandlers((e: KeyboardEvent<HTMLInputElement>) => {
|
|
498
|
+
if (isEffectivelyDisabled && e.key === 'Enter') {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
}
|
|
501
|
+
}, controlProps.onKeyDown)}
|
|
479
502
|
placeholder={placeholder}
|
|
480
|
-
disabled={isEffectivelyDisabled
|
|
481
|
-
aria-disabled={showsDisabledMessage ? 'true' : undefined}
|
|
503
|
+
aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
|
|
482
504
|
aria-describedby={ariaDescribedBy}
|
|
483
505
|
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
484
506
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
@@ -28,7 +28,7 @@ export const docs = {
|
|
|
28
28
|
name: 'format',
|
|
29
29
|
type: "'relative' | 'relative_short' | 'auto' | 'date' | 'date_long' | 'date_weekday' | 'date_time' | 'time' | 'system_date' | 'system_date_time' | 'system_time' | 'unix_seconds'",
|
|
30
30
|
description:
|
|
31
|
-
"Display format. 'relative' uses the locale's native long relative-time wording ('2 hours ago' in English), 'relative_short' uses its narrow pattern ('2h ago' in English) for compact surfaces, 'date' shows 'Mar 21, 2025', 'date_long' shows 'March 21, 2025', 'date_weekday' shows 'Wed, Mar 21, 2025', 'date_time' shows 'Mar 21, 2025, 2:51 PM', 'time' shows '2:51 PM', 'system_*' variants use ISO-style formatting, 'unix_seconds' shows the Unix time in whole seconds since the epoch (an absolute, zone-independent value), 'auto' switches from relative to date_time based on recency.",
|
|
31
|
+
"Display format. 'relative' uses the locale's native long relative-time wording ('2 hours ago' in English), 'relative_short' uses its narrow pattern ('2h ago' in English) for compact surfaces, 'date' shows 'Mar 21, 2025', 'date_long' shows 'March 21, 2025', 'date_weekday' shows 'Wed, Mar 21, 2025', 'date_time' shows 'Mar 21, 2025, 2:51 PM', 'time' shows '2:51 PM', 'system_*' variants use ISO-style formatting, 'unix_seconds' shows the Unix time in whole seconds since the epoch (an absolute, zone-independent value), 'auto' switches from relative to date_time based on recency. Absolute formats never read the current time. 'relative', 'relative_short' and 'auto' read it on the client only: a server render or static prerender shows the full date and time in UTC, labelled, and the client switches to the format's normal output after hydration.",
|
|
32
32
|
default: "'auto'",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from 'vitest';
|
|
12
12
|
import {render, screen, act, waitFor, fireEvent} from '@testing-library/react';
|
|
13
13
|
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import {Profiler} from 'react';
|
|
15
|
+
import {hydrateRoot, type Root} from 'react-dom/client';
|
|
16
|
+
import {renderToString} from 'react-dom/server';
|
|
14
17
|
import {Timestamp} from './Timestamp';
|
|
15
18
|
import {formatInstant} from './formatInstant';
|
|
16
19
|
import {formatTooltipLines} from './tooltipEntries';
|
|
@@ -1466,3 +1469,168 @@ describe('Timestamp pass-through props', () => {
|
|
|
1466
1469
|
});
|
|
1467
1470
|
});
|
|
1468
1471
|
});
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Counts `new Date()` calls with no argument (reads of the current time) made
|
|
1475
|
+
* while `run` executes.
|
|
1476
|
+
*/
|
|
1477
|
+
function countClockReads(run: () => void): number {
|
|
1478
|
+
const ActiveDate = globalThis.Date;
|
|
1479
|
+
let reads = 0;
|
|
1480
|
+
class CountingDate extends ActiveDate {
|
|
1481
|
+
constructor(...args: [] | [string | number | Date]) {
|
|
1482
|
+
if (args.length === 0) {
|
|
1483
|
+
reads += 1;
|
|
1484
|
+
}
|
|
1485
|
+
super(...(args as [string | number | Date]));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
globalThis.Date = CountingDate as DateConstructor;
|
|
1489
|
+
try {
|
|
1490
|
+
run();
|
|
1491
|
+
} finally {
|
|
1492
|
+
globalThis.Date = ActiveDate;
|
|
1493
|
+
}
|
|
1494
|
+
return reads;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
describe('Timestamp server rendering', () => {
|
|
1498
|
+
beforeEach(() => {
|
|
1499
|
+
vi.useFakeTimers();
|
|
1500
|
+
vi.setSystemTime(new Date('2026-03-25T12:00:00Z'));
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
afterEach(() => {
|
|
1504
|
+
vi.useRealTimers();
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
it.each(['date', 'date_time', 'time', 'unix_seconds'] as const)(
|
|
1508
|
+
'never reads the current time for the absolute %s format',
|
|
1509
|
+
format => {
|
|
1510
|
+
const element = (
|
|
1511
|
+
<Timestamp value="2026-03-25T10:00:00Z" format={format} />
|
|
1512
|
+
);
|
|
1513
|
+
expect(countClockReads(() => renderToString(element))).toBe(0);
|
|
1514
|
+
expect(countClockReads(() => render(element))).toBe(0);
|
|
1515
|
+
},
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
it.each(['relative', 'relative_short', 'auto'] as const)(
|
|
1519
|
+
'renders the %s format as the full date and time in UTC on the server without reading the clock',
|
|
1520
|
+
format => {
|
|
1521
|
+
const value = '2026-03-25T10:00:00Z';
|
|
1522
|
+
let html = '';
|
|
1523
|
+
expect(
|
|
1524
|
+
countClockReads(() => {
|
|
1525
|
+
html = renderToString(<Timestamp value={value} format={format} />);
|
|
1526
|
+
}),
|
|
1527
|
+
).toBe(0);
|
|
1528
|
+
const host = document.createElement('div');
|
|
1529
|
+
host.innerHTML = html;
|
|
1530
|
+
expect(host.textContent).toBe(
|
|
1531
|
+
formatInstant(new Date(value), 'date_time', 'en-US', {
|
|
1532
|
+
timeZone: 'UTC',
|
|
1533
|
+
isTimezoneShown: true,
|
|
1534
|
+
}),
|
|
1535
|
+
);
|
|
1536
|
+
},
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
it('never commits the UTC placeholder when a client Timestamp switches to a relative format', () => {
|
|
1540
|
+
const value = Date.now() / 1000 - 7200;
|
|
1541
|
+
const committed: string[] = [];
|
|
1542
|
+
function Spy({format}: {format: 'date' | 'relative'}) {
|
|
1543
|
+
return (
|
|
1544
|
+
<Profiler
|
|
1545
|
+
id="ts"
|
|
1546
|
+
onRender={() => {
|
|
1547
|
+
committed.push(document.querySelector('time')?.textContent ?? '');
|
|
1548
|
+
}}>
|
|
1549
|
+
<Timestamp value={value} format={format} hasTooltip={false} />
|
|
1550
|
+
</Profiler>
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
const {rerender} = render(<Spy format="date" />);
|
|
1554
|
+
committed.length = 0;
|
|
1555
|
+
rerender(<Spy format="relative" />);
|
|
1556
|
+
expect(screen.getByText('2 hours ago')).toBeInTheDocument();
|
|
1557
|
+
expect(committed.some(text => text.includes('UTC'))).toBe(false);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
describe('across time zones', () => {
|
|
1561
|
+
const originalTZ = process.env.TZ;
|
|
1562
|
+
|
|
1563
|
+
afterEach(() => {
|
|
1564
|
+
if (originalTZ === undefined) {
|
|
1565
|
+
delete process.env.TZ;
|
|
1566
|
+
} else {
|
|
1567
|
+
process.env.TZ = originalTZ;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
it('writes the same server markup whatever the server time zone', () => {
|
|
1572
|
+
const element = (
|
|
1573
|
+
<Timestamp value="2026-03-25T10:00:00Z" format="relative" />
|
|
1574
|
+
);
|
|
1575
|
+
process.env.TZ = 'UTC';
|
|
1576
|
+
const utc = renderToString(element);
|
|
1577
|
+
process.env.TZ = 'Asia/Tokyo';
|
|
1578
|
+
expect(renderToString(element)).toBe(utc);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
it('hydrates server markup from another time zone without a mismatch', () => {
|
|
1582
|
+
const value = Date.now() / 1000 - 7200;
|
|
1583
|
+
const element = (
|
|
1584
|
+
<Timestamp value={value} format="relative" hasTooltip={false} />
|
|
1585
|
+
);
|
|
1586
|
+
process.env.TZ = 'UTC';
|
|
1587
|
+
const host = document.createElement('div');
|
|
1588
|
+
document.body.appendChild(host);
|
|
1589
|
+
host.innerHTML = renderToString(element);
|
|
1590
|
+
|
|
1591
|
+
process.env.TZ = 'America/Los_Angeles';
|
|
1592
|
+
const errors = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1593
|
+
let root: Root | undefined;
|
|
1594
|
+
try {
|
|
1595
|
+
act(() => {
|
|
1596
|
+
root = hydrateRoot(host, element, {
|
|
1597
|
+
onRecoverableError: error => {
|
|
1598
|
+
throw error;
|
|
1599
|
+
},
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
expect(host.textContent).toBe('2 hours ago');
|
|
1603
|
+
expect(errors).not.toHaveBeenCalled();
|
|
1604
|
+
} finally {
|
|
1605
|
+
errors.mockRestore();
|
|
1606
|
+
act(() => root?.unmount());
|
|
1607
|
+
host.remove();
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
it('hydrates the server markup without a mismatch, then shows the relative phrase', () => {
|
|
1613
|
+
const value = Date.now() / 1000 - 7200;
|
|
1614
|
+
const element = (
|
|
1615
|
+
<Timestamp value={value} format="relative" hasTooltip={false} />
|
|
1616
|
+
);
|
|
1617
|
+
const host = document.createElement('div');
|
|
1618
|
+
document.body.appendChild(host);
|
|
1619
|
+
host.innerHTML = renderToString(element);
|
|
1620
|
+
expect(host.textContent).not.toBe('2 hours ago');
|
|
1621
|
+
|
|
1622
|
+
const errors = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1623
|
+
let root: Root | undefined;
|
|
1624
|
+
try {
|
|
1625
|
+
act(() => {
|
|
1626
|
+
root = hydrateRoot(host, element);
|
|
1627
|
+
});
|
|
1628
|
+
expect(host.textContent).toBe('2 hours ago');
|
|
1629
|
+
expect(errors).not.toHaveBeenCalled();
|
|
1630
|
+
} finally {
|
|
1631
|
+
errors.mockRestore();
|
|
1632
|
+
act(() => root?.unmount());
|
|
1633
|
+
host.remove();
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
});
|