@astryxdesign/core 0.4.6 → 0.4.7-canary.29be96d
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/CHANGELOG.md +35 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +54 -15
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +10 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +23 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -5
- package/dist/Calendar/Calendar.d.ts +3 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +13 -14
- package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
- package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
- package/dist/Calendar/getInitialFocusDate.js +62 -0
- package/dist/Chat/ChatMessageList.d.ts +22 -3
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +6 -3
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +34 -1
- package/dist/Dialog/Dialog.d.ts +7 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +48 -22
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +6 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +10 -3
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +38 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +149 -23
- package/dist/MultiSelector/MultiSelector.d.ts +15 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +17 -6
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +5 -0
- package/dist/Stepper/Step.d.ts +140 -0
- package/dist/Stepper/Step.d.ts.map +1 -0
- package/dist/Stepper/Step.js +1045 -0
- package/dist/Stepper/StepStatus.d.ts +24 -0
- package/dist/Stepper/StepStatus.d.ts.map +1 -0
- package/dist/Stepper/StepStatus.js +1 -0
- package/dist/Stepper/Stepper.d.ts +93 -0
- package/dist/Stepper/Stepper.d.ts.map +1 -0
- package/dist/Stepper/Stepper.js +184 -0
- package/dist/Stepper/StepperContext.d.ts +41 -0
- package/dist/Stepper/StepperContext.d.ts.map +1 -0
- package/dist/Stepper/StepperContext.js +34 -0
- package/dist/Stepper/index.d.ts +8 -0
- package/dist/Stepper/index.d.ts.map +1 -0
- package/dist/Stepper/index.js +7 -0
- package/dist/Stepper/stepper.stylex.d.ts +15 -0
- package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
- package/dist/Stepper/stepper.stylex.js +20 -0
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +5 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +4 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
- package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +19 -5
- package/dist/Table/types.d.ts +22 -4
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +5 -0
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +6 -2
- package/dist/astryx.css +41 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +10 -3
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/dist/hooks/useMergedRefs.d.ts +18 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/timeParser.d.ts +1 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +26 -12
- package/locales/en.json +14 -6
- package/locales/pseudo.json +6 -0
- package/package.json +8 -3
- package/src/Avatar/Avatar.doc.mjs +2 -1
- package/src/BottomSheet/BottomSheet.test.tsx +123 -0
- package/src/BottomSheet/BottomSheet.tsx +36 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +13 -0
- package/src/BottomSheet/useSheetGestures.test.ts +27 -0
- package/src/BottomSheet/useSheetGestures.ts +25 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +2 -1
- package/src/Button/Button.doc.mjs +22 -0
- package/src/Calendar/Calendar.doc.mjs +4 -3
- package/src/Calendar/Calendar.test.tsx +52 -0
- package/src/Calendar/Calendar.tsx +18 -15
- package/src/Calendar/getInitialFocusDate.test.ts +94 -0
- package/src/Calendar/getInitialFocusDate.ts +93 -0
- package/src/Chat/ChatMessageList.doc.mjs +9 -1
- package/src/Chat/ChatMessageList.test.tsx +46 -0
- package/src/Chat/ChatMessageList.tsx +28 -4
- package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
- package/src/DateInput/DateInputTouch.test.tsx +36 -0
- package/src/DateInput/TouchDateField.tsx +35 -1
- package/src/Dialog/Dialog.doc.mjs +8 -3
- package/src/Dialog/Dialog.test.tsx +71 -0
- package/src/Dialog/Dialog.tsx +72 -20
- package/src/Field/Field.doc.mjs +11 -0
- package/src/Heading/Heading.test.tsx +23 -1
- package/src/Heading/Heading.tsx +6 -2
- package/src/Kbd/Kbd.test.tsx +17 -0
- package/src/Kbd/Kbd.tsx +10 -3
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Markdown/index.ts +1 -0
- package/src/Markdown/parser.perf.test.ts +71 -1
- package/src/Markdown/parser.test.ts +145 -2
- package/src/Markdown/parser.ts +208 -22
- package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +10 -0
- package/src/MultiSelector/MultiSelector.test.tsx +105 -0
- package/src/MultiSelector/MultiSelector.tsx +50 -6
- package/src/MultiSelector/index.ts +1 -0
- package/src/NumberInput/NumberInput.doc.mjs +5 -0
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/PowerSearch/PowerSearch.doc.mjs +10 -0
- package/src/Selector/Selector.doc.mjs +11 -0
- package/src/Selector/Selector.test.tsx +21 -0
- package/src/Selector/Selector.tsx +11 -1
- package/src/Stepper/Step.doc.mjs +98 -0
- package/src/Stepper/Step.tsx +1638 -0
- package/src/Stepper/StepStatus.ts +26 -0
- package/src/Stepper/Stepper.doc.mjs +370 -0
- package/src/Stepper/Stepper.test.tsx +1024 -0
- package/src/Stepper/Stepper.tsx +258 -0
- package/src/Stepper/StepperContext.ts +71 -0
- package/src/Stepper/index.ts +18 -0
- package/src/Stepper/stepper.stylex.ts +19 -0
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.test.tsx +21 -4
- package/src/Table/BaseTable.tsx +6 -3
- package/src/Table/Table.test.tsx +35 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
- package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
- package/src/Table/types.ts +22 -4
- package/src/Table/useBaseTablePlugins.ts +5 -0
- package/src/Table/useTableGroupedRows.doc.mjs +5 -4
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Table/useTableSelection.doc.mjs +31 -0
- package/src/Text/Text.test.tsx +23 -1
- package/src/Text/Text.tsx +6 -2
- package/src/TextInput/TextInput.doc.mjs +10 -0
- package/src/Toast/Toast.doc.mjs +6 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +10 -0
- package/src/Toolbar/Toolbar.doc.mjs +5 -0
- package/src/__tests__/apiContractDrift.test.tsx +80 -0
- package/src/__tests__/fieldContract.test.tsx +24 -0
- package/src/__tests__/structuralComponentContract.test.tsx +39 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useHotkeys.test.ts +18 -0
- package/src/hooks/useHotkeys.ts +10 -3
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
- package/src/hooks/useMergedRefs.doc.mjs +79 -0
- package/src/hooks/useMergedRefs.test.tsx +62 -0
- package/src/hooks/useMergedRefs.ts +36 -0
- package/src/index.ts +1 -0
- package/src/theme/MediaTheme.doc.mjs +5 -5
- package/src/utils/timeParser.test.ts +6 -1
- package/src/utils/timeParser.ts +35 -13
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* SYNC: When Dialog.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {readFileSync} from 'node:fs';
|
|
12
13
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
13
14
|
import {render, screen} from '@testing-library/react';
|
|
14
15
|
import {Dialog, resolveDialogPositionOffsets} from './Dialog';
|
|
@@ -184,6 +185,76 @@ describe('Dialog', () => {
|
|
|
184
185
|
});
|
|
185
186
|
});
|
|
186
187
|
|
|
188
|
+
describe('responsive sizing', () => {
|
|
189
|
+
it('keeps the requested width but clamps standard dialogs to container and dynamic viewport gutters', () => {
|
|
190
|
+
render(
|
|
191
|
+
<Dialog
|
|
192
|
+
isOpen={true}
|
|
193
|
+
onOpenChange={() => {}}
|
|
194
|
+
width={600}
|
|
195
|
+
maxHeight="70dvh"
|
|
196
|
+
aria-label="Sized dialog">
|
|
197
|
+
Content
|
|
198
|
+
</Dialog>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const dialog = screen.getByRole('dialog');
|
|
202
|
+
const inlineStyle = dialog.getAttribute('style') ?? '';
|
|
203
|
+
expect(inlineStyle).toContain('--x-width: 600px');
|
|
204
|
+
expect(inlineStyle).toContain(
|
|
205
|
+
'--x-maxWidth: min(100%, calc(100dvw - var(--spacing-4) - var(--spacing-4)))',
|
|
206
|
+
);
|
|
207
|
+
expect(inlineStyle).toContain('--x-maxHeight: 70dvh');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('uses a fullscreen-specific fade animation instead of centered dialog movement', () => {
|
|
211
|
+
const source = readFileSync(
|
|
212
|
+
'packages/core/src/Dialog/Dialog.tsx',
|
|
213
|
+
'utf8',
|
|
214
|
+
);
|
|
215
|
+
const standardOpen = source.slice(
|
|
216
|
+
source.indexOf(' open: {'),
|
|
217
|
+
source.indexOf(' // Backdrop using ::backdrop'),
|
|
218
|
+
);
|
|
219
|
+
const fullscreenOpen = source.slice(
|
|
220
|
+
source.indexOf(' fullscreenOpen: {'),
|
|
221
|
+
source.indexOf(' fullscreenSafeArea: {'),
|
|
222
|
+
);
|
|
223
|
+
const modalStyleOrder = source.slice(
|
|
224
|
+
source.indexOf('focusOutlineProps.focusVisible('),
|
|
225
|
+
source.indexOf(
|
|
226
|
+
' xstyle,',
|
|
227
|
+
source.indexOf('focusOutlineProps.focusVisible('),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(standardOpen).toContain('enterDirectional');
|
|
232
|
+
expect(fullscreenOpen).toContain('enterFullscreen');
|
|
233
|
+
expect(fullscreenOpen).not.toContain('enterDirectional');
|
|
234
|
+
expect(modalStyleOrder.indexOf('styles.open')).toBeLessThan(
|
|
235
|
+
modalStyleOrder.indexOf('styles.fullscreenOpen'),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('protects fullscreen content with safe-area padding', () => {
|
|
240
|
+
render(
|
|
241
|
+
<Dialog
|
|
242
|
+
isOpen={true}
|
|
243
|
+
onOpenChange={() => {}}
|
|
244
|
+
variant="fullscreen"
|
|
245
|
+
aria-label="Fullscreen dialog">
|
|
246
|
+
<div data-testid="child">Content</div>
|
|
247
|
+
</Dialog>,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const wrapper = screen.getByTestId('child').parentElement!;
|
|
251
|
+
const computed = window.getComputedStyle(wrapper);
|
|
252
|
+
expect(computed.paddingInlineStart).toContain('safe-area-inset-left');
|
|
253
|
+
expect(computed.paddingInlineEnd).toContain('safe-area-inset-right');
|
|
254
|
+
expect(wrapper.parentElement!.tagName).toBe('DIALOG');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
187
258
|
describe('position prop', () => {
|
|
188
259
|
it('accepts position configuration', () => {
|
|
189
260
|
render(
|
package/src/Dialog/Dialog.tsx
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* @output Exports Dialog component, DialogProps, DialogVariant, DialogPurpose types
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by Dialog.test.tsx
|
|
10
10
|
*
|
|
11
|
+
* The standard variant treats `width` as the preferred surface width, then
|
|
12
|
+
* clamps it to the dynamic viewport with spacing-token gutters so narrow
|
|
13
|
+
* viewports keep content and controls on screen without changing the public API.
|
|
14
|
+
* Fullscreen dialogs preserve the same padding floor while honoring safe-area
|
|
15
|
+
* insets, and fade in without the centered-dialog translate/scale motion.
|
|
16
|
+
*
|
|
11
17
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
18
|
* - /packages/core/src/Dialog/Dialog.doc.mjs (props table, features, implementation notes)
|
|
13
19
|
* - /packages/core/src/Dialog/Dialog.test.tsx (tests for new/changed behavior)
|
|
@@ -35,6 +41,7 @@ import {
|
|
|
35
41
|
durationVars,
|
|
36
42
|
easeVars,
|
|
37
43
|
shadowVars,
|
|
44
|
+
spacingVars,
|
|
38
45
|
} from '../theme/tokens.stylex';
|
|
39
46
|
import {container} from '../Layout/container.stylex';
|
|
40
47
|
import type {SpacingToken} from '../Layout/container.stylex';
|
|
@@ -118,6 +125,11 @@ const enterDirectional = stylex.keyframes({
|
|
|
118
125
|
to: {opacity: 1, transform: 'translate(0, 0) scale(1)'},
|
|
119
126
|
});
|
|
120
127
|
|
|
128
|
+
const enterFullscreen = stylex.keyframes({
|
|
129
|
+
from: {opacity: 0},
|
|
130
|
+
to: {opacity: 1},
|
|
131
|
+
});
|
|
132
|
+
|
|
121
133
|
/**
|
|
122
134
|
* Dialog styles using native <dialog> element
|
|
123
135
|
* Uses ::backdrop pseudo-element for overlay
|
|
@@ -171,6 +183,22 @@ const styles = stylex.create({
|
|
|
171
183
|
margin: 0,
|
|
172
184
|
inset: 0,
|
|
173
185
|
},
|
|
186
|
+
fullscreenOpen: {
|
|
187
|
+
animationName: {
|
|
188
|
+
default: enterFullscreen,
|
|
189
|
+
'@media (prefers-reduced-motion: reduce)': 'none',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
fullscreenSafeArea: {
|
|
193
|
+
paddingBlockStart:
|
|
194
|
+
'max(var(--container-padding-block-start), env(safe-area-inset-top, 0px))',
|
|
195
|
+
paddingBlockEnd:
|
|
196
|
+
'max(var(--container-padding-block-end), env(safe-area-inset-bottom, 0px))',
|
|
197
|
+
paddingInlineStart:
|
|
198
|
+
'max(var(--container-padding-inline-start), env(safe-area-inset-left, 0px))',
|
|
199
|
+
paddingInlineEnd:
|
|
200
|
+
'max(var(--container-padding-inline-end), env(safe-area-inset-right, 0px))',
|
|
201
|
+
},
|
|
174
202
|
inner: {
|
|
175
203
|
display: 'flex',
|
|
176
204
|
flexDirection: 'column',
|
|
@@ -194,12 +222,30 @@ const styles = stylex.create({
|
|
|
194
222
|
},
|
|
195
223
|
});
|
|
196
224
|
|
|
197
|
-
|
|
225
|
+
const STANDARD_DIALOG_VIEWPORT_GUTTER = spacingVars['--spacing-4'];
|
|
226
|
+
const STANDARD_DIALOG_VIEWPORT_MAX_WIDTH = `calc(100dvw - ${STANDARD_DIALOG_VIEWPORT_GUTTER} - ${STANDARD_DIALOG_VIEWPORT_GUTTER})`;
|
|
227
|
+
const STANDARD_DIALOG_MAX_WIDTH = `min(100%, ${STANDARD_DIALOG_VIEWPORT_MAX_WIDTH})`;
|
|
228
|
+
|
|
229
|
+
function formatSizeValue(value: number | string): string {
|
|
230
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveDialogSizing(
|
|
234
|
+
width: number | string,
|
|
235
|
+
maxHeight: number | string,
|
|
236
|
+
) {
|
|
237
|
+
return {
|
|
238
|
+
width: formatSizeValue(width),
|
|
239
|
+
maxWidth: STANDARD_DIALOG_MAX_WIDTH,
|
|
240
|
+
maxHeight: formatSizeValue(maxHeight),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
198
244
|
const dynamicStyles = stylex.create({
|
|
199
|
-
sizing: (width:
|
|
200
|
-
width
|
|
201
|
-
maxWidth
|
|
202
|
-
maxHeight
|
|
245
|
+
sizing: (width: string, maxWidth: string, maxHeight: string) => ({
|
|
246
|
+
width,
|
|
247
|
+
maxWidth,
|
|
248
|
+
maxHeight,
|
|
203
249
|
}),
|
|
204
250
|
position: (
|
|
205
251
|
top: string,
|
|
@@ -288,7 +334,7 @@ export interface DialogProps extends BaseProps<HTMLDialogElement> {
|
|
|
288
334
|
* The actual height will be the height of its content.
|
|
289
335
|
* Numbers are treated as pixels, strings are used as-is.
|
|
290
336
|
* Ignored when variant is 'fullscreen'.
|
|
291
|
-
* @default '
|
|
337
|
+
* @default '75dvh'
|
|
292
338
|
*/
|
|
293
339
|
maxHeight?: number | string;
|
|
294
340
|
|
|
@@ -356,7 +402,7 @@ export function Dialog({
|
|
|
356
402
|
isInline = false,
|
|
357
403
|
onOpenChange,
|
|
358
404
|
width = 400,
|
|
359
|
-
maxHeight = '
|
|
405
|
+
maxHeight = '75dvh',
|
|
360
406
|
position,
|
|
361
407
|
variant = 'standard',
|
|
362
408
|
purpose = 'info',
|
|
@@ -374,6 +420,9 @@ export function Dialog({
|
|
|
374
420
|
const paddingToken = spacingStepToToken[effectivePadding] as SpacingToken;
|
|
375
421
|
|
|
376
422
|
const isFullscreen = variant === 'fullscreen';
|
|
423
|
+
const standardSizing = isFullscreen
|
|
424
|
+
? null
|
|
425
|
+
: resolveDialogSizing(width, maxHeight);
|
|
377
426
|
|
|
378
427
|
// Default accessible name: publish a title id through DialogContext so a
|
|
379
428
|
// DialogHeader applies it to its heading (mirrors AlertDialog's explicit
|
|
@@ -550,7 +599,6 @@ export function Dialog({
|
|
|
550
599
|
}
|
|
551
600
|
};
|
|
552
601
|
|
|
553
|
-
// Shared inner content wrapper
|
|
554
602
|
const innerContent = (
|
|
555
603
|
<div
|
|
556
604
|
{...stylex.props(
|
|
@@ -559,22 +607,14 @@ export function Dialog({
|
|
|
559
607
|
useThemeDefault
|
|
560
608
|
? {
|
|
561
609
|
useThemeDefault: 'dialog',
|
|
562
|
-
maxHeight:
|
|
563
|
-
? undefined
|
|
564
|
-
: typeof maxHeight === 'number'
|
|
565
|
-
? `${maxHeight}px`
|
|
566
|
-
: maxHeight,
|
|
610
|
+
maxHeight: standardSizing?.maxHeight,
|
|
567
611
|
}
|
|
568
612
|
: {
|
|
569
613
|
paddingInnerX: paddingToken,
|
|
570
614
|
paddingInnerY: paddingToken,
|
|
571
615
|
paddingOuterX: paddingToken,
|
|
572
616
|
paddingOuterY: paddingToken,
|
|
573
|
-
maxHeight:
|
|
574
|
-
? undefined
|
|
575
|
-
: typeof maxHeight === 'number'
|
|
576
|
-
? `${maxHeight}px`
|
|
577
|
-
: maxHeight,
|
|
617
|
+
maxHeight: standardSizing?.maxHeight,
|
|
578
618
|
},
|
|
579
619
|
),
|
|
580
620
|
!useThemeDefault &&
|
|
@@ -589,6 +629,7 @@ export function Dialog({
|
|
|
589
629
|
!useThemeDefault &&
|
|
590
630
|
effectivePadding !== 4 &&
|
|
591
631
|
containerPaddingBlockEndVarStyles[effectivePadding],
|
|
632
|
+
isFullscreen && styles.fullscreenSafeArea,
|
|
592
633
|
)}>
|
|
593
634
|
<DialogContext value={dialogContextValue}>{children}</DialogContext>
|
|
594
635
|
</div>
|
|
@@ -612,7 +653,12 @@ export function Dialog({
|
|
|
612
653
|
stylex.props(
|
|
613
654
|
styles.inlineWrapper,
|
|
614
655
|
overlayPaddingReset.reset,
|
|
615
|
-
|
|
656
|
+
standardSizing &&
|
|
657
|
+
dynamicStyles.sizing(
|
|
658
|
+
standardSizing.width,
|
|
659
|
+
standardSizing.maxWidth,
|
|
660
|
+
standardSizing.maxHeight,
|
|
661
|
+
),
|
|
616
662
|
isFullscreen && styles.fullscreen,
|
|
617
663
|
xstyle,
|
|
618
664
|
),
|
|
@@ -641,7 +687,12 @@ export function Dialog({
|
|
|
641
687
|
overlayPaddingReset.reset,
|
|
642
688
|
isOpen && styles.open,
|
|
643
689
|
styles.backdrop,
|
|
644
|
-
|
|
690
|
+
standardSizing &&
|
|
691
|
+
dynamicStyles.sizing(
|
|
692
|
+
standardSizing.width,
|
|
693
|
+
standardSizing.maxWidth,
|
|
694
|
+
standardSizing.maxHeight,
|
|
695
|
+
),
|
|
645
696
|
hasPosition &&
|
|
646
697
|
(() => {
|
|
647
698
|
const o = resolveDialogPositionOffsets(position);
|
|
@@ -653,6 +704,7 @@ export function Dialog({
|
|
|
653
704
|
);
|
|
654
705
|
})(),
|
|
655
706
|
isFullscreen && styles.fullscreen,
|
|
707
|
+
isFullscreen && isOpen && styles.fullscreenOpen,
|
|
656
708
|
xstyle,
|
|
657
709
|
),
|
|
658
710
|
className,
|
package/src/Field/Field.doc.mjs
CHANGED
|
@@ -60,6 +60,17 @@ export const docs = {
|
|
|
60
60
|
description: 'ID for the input element (used for the label htmlFor attribute).',
|
|
61
61
|
required: true,
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: 'labelID',
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'ID applied to the label element itself for group accessibility.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'isGroupLabel',
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: 'Renders the label as a span for control groups (radiogroup, checkbox list).',
|
|
72
|
+
default: 'false',
|
|
73
|
+
},
|
|
63
74
|
{
|
|
64
75
|
name: 'children',
|
|
65
76
|
type: 'ReactNode',
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {render, screen} from '@testing-library/react';
|
|
9
|
-
import {describe, it, expect} from 'vitest';
|
|
9
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
10
10
|
import {Heading} from './Heading';
|
|
11
11
|
|
|
12
12
|
describe('Heading', () => {
|
|
@@ -16,6 +16,28 @@ describe('Heading', () => {
|
|
|
16
16
|
expect(screen.getByText('Page Title')).toBeInTheDocument();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
it('keeps a forwarded ref attached across rerenders', () => {
|
|
20
|
+
const ref = vi.fn();
|
|
21
|
+
const {rerender} = render(
|
|
22
|
+
<Heading ref={ref} level={1}>
|
|
23
|
+
Before
|
|
24
|
+
</Heading>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const element = screen.getByText('Before');
|
|
28
|
+
expect(ref).toHaveBeenLastCalledWith(element);
|
|
29
|
+
ref.mockClear();
|
|
30
|
+
|
|
31
|
+
rerender(
|
|
32
|
+
<Heading ref={ref} level={1}>
|
|
33
|
+
After
|
|
34
|
+
</Heading>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(ref).not.toHaveBeenCalled();
|
|
38
|
+
expect(screen.getByText('After')).toBe(element);
|
|
39
|
+
});
|
|
40
|
+
|
|
19
41
|
it('renders h1 for level 1', () => {
|
|
20
42
|
render(<Heading level={1}>H1</Heading>);
|
|
21
43
|
const element = screen.getByText('H1');
|
package/src/Heading/Heading.tsx
CHANGED
|
@@ -43,7 +43,8 @@ import {
|
|
|
43
43
|
import {useTruncation} from '../Text/useTruncation';
|
|
44
44
|
import {resolveStyleColor} from '../Text/Text';
|
|
45
45
|
import type {LayerPlacement} from '../Layer';
|
|
46
|
-
import {mergeProps
|
|
46
|
+
import {mergeProps} from '../utils';
|
|
47
|
+
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
47
48
|
import type {BaseProps} from '../BaseProps';
|
|
48
49
|
import {themeProps} from '../utils/themeProps';
|
|
49
50
|
|
|
@@ -242,13 +243,16 @@ export function Heading({
|
|
|
242
243
|
// Ref for the heading element (used as tooltip anchor)
|
|
243
244
|
const headingRef = useRef<HTMLHeadingElement>(null);
|
|
244
245
|
|
|
246
|
+
// Keep the merged ref stable across rerenders.
|
|
247
|
+
const mergedRef = useMergedRefs(ref, truncation.ref, headingRef);
|
|
248
|
+
|
|
245
249
|
// Build inline style for -webkit-line-clamp (dynamic value)
|
|
246
250
|
const inlineStyle = maxLines > 1 ? {WebkitLineClamp: maxLines} : undefined;
|
|
247
251
|
|
|
248
252
|
return (
|
|
249
253
|
<>
|
|
250
254
|
<Component
|
|
251
|
-
ref={
|
|
255
|
+
ref={mergedRef}
|
|
252
256
|
{...mergeProps(
|
|
253
257
|
themeProps('heading', {level, color, ...(type && {type})}),
|
|
254
258
|
stylex.props(
|
package/src/Kbd/Kbd.test.tsx
CHANGED
|
@@ -23,6 +23,7 @@ describe('Kbd', () => {
|
|
|
23
23
|
value: originalPlatform,
|
|
24
24
|
configurable: true,
|
|
25
25
|
});
|
|
26
|
+
delete (navigator as {userAgentData?: unknown}).userAgentData;
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
it('renders a single key', () => {
|
|
@@ -54,6 +55,22 @@ describe('Kbd', () => {
|
|
|
54
55
|
expect(screen.getByText('\u2318')).toBeInTheDocument(); // \u2318
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it('reads a blank userAgentData.platform as unknown, not as non-Mac', () => {
|
|
59
|
+
// Builds that rewrite their client-hints identity expose the key with an
|
|
60
|
+
// empty value; navigator.platform is the only surface left that answers.
|
|
61
|
+
Object.defineProperty(navigator, 'userAgentData', {
|
|
62
|
+
value: {platform: ''},
|
|
63
|
+
configurable: true,
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(navigator, 'platform', {
|
|
66
|
+
value: 'MacIntel',
|
|
67
|
+
configurable: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
render(<Kbd keys="mod" />);
|
|
71
|
+
expect(screen.getByText('\u2318')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
57
74
|
it('maps modifier keys to symbols', () => {
|
|
58
75
|
render(<Kbd keys="ctrl+alt+shift+k" />);
|
|
59
76
|
expect(screen.getByText('\u2303')).toBeInTheDocument(); // \u2303
|
package/src/Kbd/Kbd.tsx
CHANGED
|
@@ -124,8 +124,9 @@ function getServerPlatformSnapshot(): boolean {
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Detects whether the current platform is macOS/iOS.
|
|
127
|
-
* Prefers the User-Agent Client Hints API when
|
|
128
|
-
* falls back to navigator.platform (deprecated but universally
|
|
127
|
+
* Prefers the User-Agent Client Hints API when it names a platform (modern
|
|
128
|
+
* Chrome/Edge), falls back to navigator.platform (deprecated but universally
|
|
129
|
+
* supported) when it is absent or blank.
|
|
129
130
|
*/
|
|
130
131
|
function detectMac(): boolean {
|
|
131
132
|
if (typeof navigator === 'undefined') {
|
|
@@ -134,7 +135,13 @@ function detectMac(): boolean {
|
|
|
134
135
|
// Prefer User-Agent Client Hints API (not deprecated)
|
|
135
136
|
const uaData = 'userAgentData' in navigator ? navigator.userAgentData : null;
|
|
136
137
|
if (uaData && typeof uaData === 'object' && 'platform' in uaData) {
|
|
137
|
-
|
|
138
|
+
const uaPlatform = (uaData as {platform?: unknown}).platform;
|
|
139
|
+
// A blank platform is no answer, not a negative one. Builds that rewrite
|
|
140
|
+
// their client-hints identity ship '', so fall through rather than
|
|
141
|
+
// reading it as "not Apple".
|
|
142
|
+
if (typeof uaPlatform === 'string' && uaPlatform.trim() !== '') {
|
|
143
|
+
return /mac/i.test(uaPlatform);
|
|
144
|
+
}
|
|
138
145
|
}
|
|
139
146
|
// Fallback: navigator.platform (deprecated but still shipped everywhere)
|
|
140
147
|
return /Mac|iPhone|iPad|iPod/.test(navigator.platform ?? '');
|
package/src/Link/Link.doc.mjs
CHANGED
|
@@ -78,6 +78,17 @@ export const docs = {
|
|
|
78
78
|
description:
|
|
79
79
|
'Link relationship tokens. noopener noreferrer are merged automatically for target="_blank".',
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'download',
|
|
83
|
+
type: 'string | boolean',
|
|
84
|
+
description:
|
|
85
|
+
'Causes the browser to download the linked URL. A string specifies the suggested filename.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'referrerPolicy',
|
|
89
|
+
type: 'HTMLAttributeReferrerPolicy',
|
|
90
|
+
description: 'Referrer policy for the link.',
|
|
91
|
+
},
|
|
81
92
|
{
|
|
82
93
|
name: 'onClick',
|
|
83
94
|
type: 'MouseEventHandler',
|
package/src/Markdown/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseMarkdownIncremental,
|
|
7
7
|
createIncrementalState,
|
|
8
8
|
} from './parser';
|
|
9
|
-
import type {BlockNode} from './parser';
|
|
9
|
+
import type {BlockNode, ParseOptions} from './parser';
|
|
10
10
|
|
|
11
11
|
function generateAIResponse(paragraphs: number): string {
|
|
12
12
|
const sections: string[] = [];
|
|
@@ -69,6 +69,42 @@ function simulateStreamingIncremental(
|
|
|
69
69
|
return parseMarkdownIncremental(fullText, state);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* How many blocks each chunk of a streamed document had to build.
|
|
74
|
+
*
|
|
75
|
+
* A block the parser kept in its settled cache is handed back as the very same
|
|
76
|
+
* object, so a returned block that is not one of the previous chunk's is one
|
|
77
|
+
* this chunk built. That makes the cache countable without reaching into the
|
|
78
|
+
* parser's internals.
|
|
79
|
+
*/
|
|
80
|
+
function blocksBuiltPerChunk(
|
|
81
|
+
fullText: string,
|
|
82
|
+
chunkSize: number,
|
|
83
|
+
options?: ParseOptions,
|
|
84
|
+
): number[] {
|
|
85
|
+
const state = createIncrementalState();
|
|
86
|
+
let previous: ReadonlySet<BlockNode> = new Set();
|
|
87
|
+
const built: number[] = [];
|
|
88
|
+
const step = (text: string) => {
|
|
89
|
+
const blocks =
|
|
90
|
+
options == null
|
|
91
|
+
? parseMarkdownIncremental(text, state)
|
|
92
|
+
: parseMarkdownIncremental(text, state, options);
|
|
93
|
+
built.push(blocks.reduce((n, b) => (previous.has(b) ? n : n + 1), 0));
|
|
94
|
+
previous = new Set(blocks);
|
|
95
|
+
};
|
|
96
|
+
for (let i = chunkSize; i <= fullText.length; i += chunkSize) {
|
|
97
|
+
step(fullText.slice(0, i));
|
|
98
|
+
}
|
|
99
|
+
step(fullText);
|
|
100
|
+
return built;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const median = (values: number[]): number =>
|
|
104
|
+
[...values].sort((a, b) => a - b)[Math.floor((values.length - 1) / 2)];
|
|
105
|
+
|
|
106
|
+
const sum = (values: number[]): number => values.reduce((a, b) => a + b, 0);
|
|
107
|
+
|
|
72
108
|
// A single wall-clock measurement is flaky when the whole suite runs in
|
|
73
109
|
// parallel forks: scheduler contention can inflate a millisecond-scale
|
|
74
110
|
// sample well past a tight budget (observed 30.9ms against the 20ms Small
|
|
@@ -204,3 +240,37 @@ describe('parseMarkdown performance', () => {
|
|
|
204
240
|
expect(incrElapsed).toBeLessThanOrEqual(fullElapsed * 1.1);
|
|
205
241
|
});
|
|
206
242
|
});
|
|
243
|
+
|
|
244
|
+
// What the incremental parser exists for is a cache, so these assert the cache:
|
|
245
|
+
// integers that come out the same on a loaded CI box as on an idle laptop,
|
|
246
|
+
// unlike the wall-clock budgets above.
|
|
247
|
+
describe('parseMarkdownIncremental cache', () => {
|
|
248
|
+
it('builds a bounded number of blocks per chunk however long the document is', () => {
|
|
249
|
+
const chunkSize = 50;
|
|
250
|
+
const medians = [50, 200, 500].map(paragraphs =>
|
|
251
|
+
median(blocksBuiltPerChunk(generateAIResponse(paragraphs), chunkSize)),
|
|
252
|
+
);
|
|
253
|
+
console.log(
|
|
254
|
+
` median blocks built per chunk (50/200/500 paragraphs): ${medians.join('/')}`,
|
|
255
|
+
);
|
|
256
|
+
// A chunk re-parses its unsettled tail, not the document — so the typical
|
|
257
|
+
// chunk's cost is a constant, and a 10x longer document does not move it.
|
|
258
|
+
expect(medians).toEqual([2, 2, 2]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('keeps the cache when source ranges are asked for', () => {
|
|
262
|
+
const text = generateAIResponse(200);
|
|
263
|
+
const chunkSize = 50;
|
|
264
|
+
const without = sum(blocksBuiltPerChunk(text, chunkSize));
|
|
265
|
+
const withRanges = sum(
|
|
266
|
+
blocksBuiltPerChunk(text, chunkSize, {sourceRanges: true}),
|
|
267
|
+
);
|
|
268
|
+
console.log(
|
|
269
|
+
` blocks built streaming ${text.length} chars: ${without} without ranges, ${withRanges} with`,
|
|
270
|
+
);
|
|
271
|
+
// Stamping offsets must not cost a cache entry. An implementation that
|
|
272
|
+
// tracked positions by invalidating the settled cache each chunk would
|
|
273
|
+
// still pass any time budget generous enough not to flake; it fails here.
|
|
274
|
+
expect(withRanges).toBe(without);
|
|
275
|
+
});
|
|
276
|
+
});
|