@astryxdesign/core 0.4.4 → 0.4.5-canary.1fdecb1
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 +40 -0
- package/dist/BottomSheet/BottomSheet.d.ts +11 -2
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +5 -2
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +65 -15
- package/dist/BottomSheet/index.d.ts +1 -1
- package/dist/BottomSheet/index.d.ts.map +1 -1
- package/dist/BottomSheet/snapOffsets.d.ts +57 -20
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -1
- package/dist/BottomSheet/snapOffsets.js +106 -30
- package/dist/BottomSheet/useSheetGestures.d.ts +13 -8
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +160 -64
- package/dist/Calendar/Calendar.d.ts +17 -0
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +34 -3
- package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts +14 -0
- package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts.map +1 -0
- package/dist/Calendar/getStandaloneShortWeekdayNames.js +26 -0
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
- package/dist/Calendar/hooks/useCalendarDays.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarDays.js +14 -10
- package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts +47 -0
- package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts.map +1 -0
- package/dist/Calendar/standaloneShortWeekdayNames.generated.js +40 -0
- package/dist/Center/Center.d.ts +23 -1
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +12 -6
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +9 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +6 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +38 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +7 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -3
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +21 -3
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/FormLayout/FormLayout.d.ts +24 -2
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +7 -2
- package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
- package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
- package/dist/FormLayout/FormLayoutContext.js +19 -4
- package/dist/FormLayout/index.d.ts +1 -1
- package/dist/FormLayout/index.d.ts.map +1 -1
- package/dist/Item/Item.d.ts +9 -1
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +45 -7
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +3 -2
- package/dist/Layout/container.stylex.d.ts +4 -2
- package/dist/Layout/container.stylex.d.ts.map +1 -1
- package/dist/Layout/container.stylex.js +19 -11
- package/dist/Layout/index.d.ts +1 -0
- package/dist/Layout/index.d.ts.map +1 -1
- package/dist/Layout/index.js +4 -0
- package/dist/Layout/padding.stylex.d.ts +315 -12
- package/dist/Layout/padding.stylex.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.js +389 -12
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +2 -1
- package/dist/Markdown/Markdown.d.ts +8 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +31 -3
- package/dist/Markdown/parser.d.ts +14 -2
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +50 -2
- package/dist/MobileNav/MobileNav.d.ts +1 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +24 -6
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +6 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +36 -3
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +2 -0
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +5 -31
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +12 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +3 -3
- package/dist/Section/Section.d.ts +36 -1
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +12 -2
- package/dist/Selector/Selector.d.ts +26 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +93 -15
- package/dist/Selector/SelectorOption.d.ts +14 -1
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +6 -0
- package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
- package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
- package/dist/Selector/SelectorRowLayoutContext.js +27 -0
- package/dist/Selector/types.d.ts +1 -0
- package/dist/Selector/types.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -2
- package/dist/Stack/Stack.d.ts +23 -1
- package/dist/Stack/Stack.d.ts.map +1 -1
- package/dist/Stack/Stack.js +12 -6
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +9 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/astryx.css +40 -21
- package/dist/hooks/scrollbarGutter.d.ts +28 -0
- package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
- package/dist/hooks/scrollbarGutter.js +103 -0
- package/dist/hooks/useResolvedRequired.d.ts +19 -0
- package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
- package/dist/hooks/useResolvedRequired.js +40 -0
- package/dist/hooks/useScrollLock.d.ts +4 -0
- package/dist/hooks/useScrollLock.d.ts.map +1 -1
- package/dist/hooks/useScrollLock.js +13 -1
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +7 -0
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +37 -4
- package/dist/utils/plainDate.d.ts +6 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +12 -0
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +8 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +6 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.doc.mjs +36 -3
- package/src/BottomSheet/BottomSheet.test.tsx +177 -2
- package/src/BottomSheet/BottomSheet.tsx +16 -2
- package/src/BottomSheet/BottomSheetPanel.test.tsx +41 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +95 -15
- package/src/BottomSheet/index.ts +5 -1
- package/src/BottomSheet/snapOffsets.test.ts +114 -47
- package/src/BottomSheet/snapOffsets.ts +119 -33
- package/src/BottomSheet/useSheetGestures.test.ts +113 -15
- package/src/BottomSheet/useSheetGestures.ts +196 -100
- package/src/Calendar/Calendar.doc.mjs +16 -0
- package/src/Calendar/Calendar.test.tsx +137 -0
- package/src/Calendar/Calendar.tsx +65 -2
- package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +54 -0
- package/src/Calendar/getStandaloneShortWeekdayNames.ts +38 -0
- package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
- package/src/Calendar/hooks/useCalendarDays.ts +13 -10
- package/src/Calendar/standaloneShortWeekdayNames.generated.ts +50 -0
- package/src/Center/Center.doc.mjs +48 -0
- package/src/Center/Center.test.tsx +158 -0
- package/src/Center/Center.tsx +50 -9
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/ComplexSelector/ComplexSelector.tsx +3 -1
- package/src/DateInput/DateInput.tsx +3 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
- package/src/DateRangeInput/DateRangeInput.tsx +72 -1
- package/src/DateTimeInput/DateTimeInput.tsx +4 -2
- package/src/Dialog/Dialog.test.tsx +24 -0
- package/src/Dialog/Dialog.tsx +3 -0
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/FieldLabel.tsx +20 -4
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FormLayout/FormLayout.doc.mjs +13 -0
- package/src/FormLayout/FormLayout.test.tsx +181 -4
- package/src/FormLayout/FormLayout.tsx +33 -2
- package/src/FormLayout/FormLayoutContext.ts +22 -7
- package/src/FormLayout/index.ts +1 -1
- package/src/Item/Item.doc.mjs +1 -0
- package/src/Item/Item.test.tsx +50 -0
- package/src/Item/Item.tsx +40 -1
- package/src/Layer/useLayer.tsx +13 -2
- package/src/Layout/Layout.doc.mjs +18 -0
- package/src/Layout/container.stylex.ts +11 -3
- package/src/Layout/index.ts +4 -0
- package/src/Layout/overlayPaddingReset.test.tsx +222 -0
- package/src/Layout/padding.stylex.ts +192 -12
- package/src/Lightbox/Lightbox.tsx +2 -1
- package/src/Markdown/Markdown.doc.mjs +3 -0
- package/src/Markdown/Markdown.test.tsx +78 -0
- package/src/Markdown/Markdown.tsx +41 -1
- package/src/Markdown/parser.ts +60 -2
- package/src/MobileNav/MobileNav.tsx +61 -5
- package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
- package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
- package/src/MultiSelector/MultiSelector.tsx +3 -1
- package/src/NumberInput/NumberInput.doc.mjs +24 -0
- package/src/NumberInput/NumberInput.test.tsx +138 -0
- package/src/NumberInput/NumberInput.tsx +43 -5
- package/src/Outline/Outline.doc.mjs +2 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +10 -40
- package/src/RadioList/RadioList.tsx +9 -1
- package/src/Resizable/ResizeHandle.test.tsx +22 -0
- package/src/Resizable/ResizeHandle.tsx +9 -3
- package/src/Section/Section.doc.mjs +55 -0
- package/src/Section/Section.test.tsx +201 -4
- package/src/Section/Section.tsx +69 -0
- package/src/Selector/Selector.doc.mjs +16 -2
- package/src/Selector/Selector.test.tsx +524 -0
- package/src/Selector/Selector.tsx +161 -17
- package/src/Selector/SelectorOption.doc.mjs +7 -0
- package/src/Selector/SelectorOption.tsx +21 -0
- package/src/Selector/SelectorRowLayoutContext.ts +32 -0
- package/src/Selector/types.ts +4 -4
- package/src/Slider/Slider.tsx +10 -0
- package/src/Stack/Stack.doc.mjs +72 -0
- package/src/Stack/Stack.test.tsx +156 -0
- package/src/Stack/Stack.tsx +50 -8
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.tsx +6 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TextArea/TextArea.tsx +3 -1
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/TimeInput.tsx +3 -1
- package/src/hooks/scrollbarGutter.test.ts +167 -0
- package/src/hooks/scrollbarGutter.ts +121 -0
- package/src/hooks/useResolvedRequired.ts +42 -0
- package/src/hooks/useScrollLock.doc.mjs +4 -4
- package/src/hooks/useScrollLock.test.ts +73 -0
- package/src/hooks/useScrollLock.ts +14 -0
- package/src/theme/derivedVarRegistry.test.ts +14 -3
- package/src/theme/derivedVarRegistry.ts +4 -0
- package/src/theme/generateThemeRules.test.ts +79 -0
- package/src/theme/generateThemeRules.ts +49 -4
- package/src/utils/plainDate.test.ts +50 -0
- package/src/utils/plainDate.ts +12 -0
|
@@ -23,6 +23,7 @@ import {spacingVars, colorVars, radiusVars} from '../../../theme/tokens.stylex';
|
|
|
23
23
|
import {Icon} from '../../../Icon';
|
|
24
24
|
import {resolveContextActions} from '../../tableContextMenu';
|
|
25
25
|
import {useTranslator} from '../../../i18n';
|
|
26
|
+
import {rtlStyles} from '../../../utils';
|
|
26
27
|
import type {
|
|
27
28
|
TablePlugin,
|
|
28
29
|
TableColumn,
|
|
@@ -92,8 +93,20 @@ const expansionStyles = stylex.create({
|
|
|
92
93
|
color: colorVars['--color-icon-primary'],
|
|
93
94
|
},
|
|
94
95
|
},
|
|
96
|
+
// The RTL mirror is folded into each state's transform rather than living
|
|
97
|
+
// on a parent span, matching TreeListItem's chevron (both are `transform`,
|
|
98
|
+
// so on one element the later value would win).
|
|
95
99
|
chevronExpanded: {
|
|
96
|
-
transform:
|
|
100
|
+
transform: {
|
|
101
|
+
default: 'rotate(90deg)',
|
|
102
|
+
':is([dir="rtl"] *)': 'scaleX(-1) rotate(90deg)',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
chevronCollapsed: {
|
|
106
|
+
transform: {
|
|
107
|
+
default: 'rotate(0deg)',
|
|
108
|
+
':is([dir="rtl"] *)': 'scaleX(-1) rotate(0deg)',
|
|
109
|
+
},
|
|
97
110
|
},
|
|
98
111
|
expandedRow: {
|
|
99
112
|
backgroundColor: colorVars['--color-background-muted'],
|
|
@@ -121,7 +134,9 @@ function ExpansionChevron({
|
|
|
121
134
|
type="button"
|
|
122
135
|
{...stylex.props(
|
|
123
136
|
expansionStyles.chevronButton,
|
|
124
|
-
isExpanded
|
|
137
|
+
isExpanded
|
|
138
|
+
? expansionStyles.chevronExpanded
|
|
139
|
+
: expansionStyles.chevronCollapsed,
|
|
125
140
|
)}
|
|
126
141
|
onClick={e => {
|
|
127
142
|
e.stopPropagation();
|
|
@@ -249,6 +264,9 @@ export function useTableRowExpansion<T extends Record<string, unknown>>(
|
|
|
249
264
|
icon={isExpanded ? 'chevronDown' : 'chevronRight'}
|
|
250
265
|
size="xsm"
|
|
251
266
|
aria-hidden
|
|
267
|
+
// chevronDown needs no mirroring; chevronRight (collapsed,
|
|
268
|
+
// pointing toward the reveal direction) does.
|
|
269
|
+
xstyle={!isExpanded && rtlStyles.mirror}
|
|
252
270
|
/>
|
|
253
271
|
),
|
|
254
272
|
onSelect: () => onToggle(key),
|
|
@@ -52,6 +52,7 @@ import type {SizeValue} from '../utils/types';
|
|
|
52
52
|
import {useInputContainer} from '../hooks/useInputContainer';
|
|
53
53
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
54
54
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
55
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
55
56
|
import {useSize} from '../SizeContext/SizeContext';
|
|
56
57
|
import {themeProps} from '../utils/themeProps';
|
|
57
58
|
import {characterCount} from '../utils/characters';
|
|
@@ -394,6 +395,7 @@ export function TextArea({
|
|
|
394
395
|
}: TextAreaProps) {
|
|
395
396
|
const size = useSize(sizeProp, 'md');
|
|
396
397
|
const t = useTranslator();
|
|
398
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
397
399
|
const announce = useAnnounce();
|
|
398
400
|
const id = useId();
|
|
399
401
|
const descriptionID = useId();
|
|
@@ -598,7 +600,7 @@ export function TextArea({
|
|
|
598
600
|
autoFocus={hasAutoFocus}
|
|
599
601
|
data-autofocus={hasAutoFocus || undefined}
|
|
600
602
|
aria-describedby={ariaDescribedBy}
|
|
601
|
-
aria-required={
|
|
603
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
602
604
|
aria-invalid={
|
|
603
605
|
status?.type === 'error' ||
|
|
604
606
|
(maxLength != null && valueLength > maxLength)
|
|
@@ -101,6 +101,7 @@ import {mergeProps, mergeRefs} from '../utils';
|
|
|
101
101
|
import {useSize} from '../SizeContext/SizeContext';
|
|
102
102
|
import {useInputContainer} from '../hooks/useInputContainer';
|
|
103
103
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
104
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
104
105
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
105
106
|
import type {BaseProps} from '../BaseProps';
|
|
106
107
|
import type {SizeValue} from '../utils/types';
|
|
@@ -301,6 +302,7 @@ export function TextInput({
|
|
|
301
302
|
...rest
|
|
302
303
|
}: TextInputProps) {
|
|
303
304
|
const t = useTranslator();
|
|
305
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
304
306
|
const size = useSize(sizeProp, 'md');
|
|
305
307
|
|
|
306
308
|
const id = useId();
|
|
@@ -445,7 +447,7 @@ export function TextInput({
|
|
|
445
447
|
autoFocus={hasAutoFocus}
|
|
446
448
|
data-autofocus={hasAutoFocus || undefined}
|
|
447
449
|
aria-describedby={ariaDescribedBy}
|
|
448
|
-
aria-required={
|
|
450
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
449
451
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
450
452
|
aria-busy={isBusy || undefined}
|
|
451
453
|
aria-labelledby={ariaLabelledBy}
|
|
@@ -67,6 +67,7 @@ import {useSize} from '../SizeContext/SizeContext';
|
|
|
67
67
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
68
68
|
import {useInputContainer} from '../hooks/useInputContainer';
|
|
69
69
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
70
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
70
71
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
71
72
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
72
73
|
import {useTooltip} from '../Tooltip';
|
|
@@ -347,6 +348,7 @@ export function TimeInput({
|
|
|
347
348
|
ref,
|
|
348
349
|
}: TimeInputProps) {
|
|
349
350
|
const t = useTranslator();
|
|
351
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
350
352
|
const placeholder =
|
|
351
353
|
placeholderFromProps ?? t('@astryx.timeInput.placeholder');
|
|
352
354
|
const size = useSize(sizeProp, 'md');
|
|
@@ -664,7 +666,7 @@ export function TimeInput({
|
|
|
664
666
|
autoFocus={hasAutoFocus}
|
|
665
667
|
data-autofocus={hasAutoFocus || undefined}
|
|
666
668
|
aria-describedby={ariaDescribedBy}
|
|
667
|
-
aria-required={
|
|
669
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
668
670
|
aria-invalid={
|
|
669
671
|
status?.type === 'error' || !isInputValid ? 'true' : undefined
|
|
670
672
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file scrollbarGutter.test.ts
|
|
5
|
+
* @input Uses vitest and a stubbed viewport + element box (jsdom does no layout)
|
|
6
|
+
* @output Unit tests for holdScrollbarGutter
|
|
7
|
+
* @position Testing; validates scrollbarGutter.ts implementation
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When scrollbarGutter.ts changes, update tests to match new behavior
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {afterEach, describe, expect, it} from 'vitest';
|
|
13
|
+
import {holdScrollbarGutter} from './scrollbarGutter';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* jsdom does no layout, so both halves of the measurement are stubbed: the
|
|
17
|
+
* viewport (`innerWidth` vs `documentElement.clientWidth`) and the element's
|
|
18
|
+
* own box, which `widths` reads from and a test flips to simulate the lock
|
|
19
|
+
* widening it.
|
|
20
|
+
*/
|
|
21
|
+
function stub({
|
|
22
|
+
innerWidth,
|
|
23
|
+
clientWidth,
|
|
24
|
+
widths,
|
|
25
|
+
}: {
|
|
26
|
+
innerWidth: number;
|
|
27
|
+
clientWidth: number;
|
|
28
|
+
widths?: () => number;
|
|
29
|
+
}) {
|
|
30
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
31
|
+
value: innerWidth,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
36
|
+
value: clientWidth,
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
if (widths) {
|
|
40
|
+
document.body.getBoundingClientRect = () => ({width: widths()}) as DOMRect;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rootGutter = () => document.documentElement.style.scrollbarGutter;
|
|
45
|
+
|
|
46
|
+
describe('holdScrollbarGutter', () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
document.body.style.cssText = '';
|
|
49
|
+
document.documentElement.style.cssText = '';
|
|
50
|
+
// @ts-expect-error -- drop the stubs so jsdom's own values come back
|
|
51
|
+
delete document.documentElement.clientWidth;
|
|
52
|
+
// @ts-expect-error -- restore the prototype's implementation
|
|
53
|
+
delete document.body.getBoundingClientRect;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('holds the gutter open when a space-taking scrollbar is about to be hidden', () => {
|
|
57
|
+
// A 1024px window over a 1009px layout viewport = a 15px classic scrollbar.
|
|
58
|
+
stub({innerWidth: 1024, clientWidth: 1009});
|
|
59
|
+
|
|
60
|
+
const hold = holdScrollbarGutter(document.body);
|
|
61
|
+
|
|
62
|
+
expect(rootGutter()).toBe('stable');
|
|
63
|
+
|
|
64
|
+
hold.settle();
|
|
65
|
+
hold.release();
|
|
66
|
+
|
|
67
|
+
expect(rootGutter()).toBe('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('leaves an overlay scrollbar alone — there is no gutter to hold', () => {
|
|
71
|
+
stub({innerWidth: 1024, clientWidth: 1024});
|
|
72
|
+
|
|
73
|
+
const hold = holdScrollbarGutter(document.body);
|
|
74
|
+
hold.settle();
|
|
75
|
+
|
|
76
|
+
expect(rootGutter()).toBe('');
|
|
77
|
+
expect(document.body.style.paddingRight).toBe('');
|
|
78
|
+
|
|
79
|
+
hold.release();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not pad when holding the gutter kept the element still', () => {
|
|
83
|
+
stub({innerWidth: 1024, clientWidth: 1009, widths: () => 1009});
|
|
84
|
+
|
|
85
|
+
const hold = holdScrollbarGutter(document.body);
|
|
86
|
+
hold.settle();
|
|
87
|
+
|
|
88
|
+
expect(rootGutter()).toBe('stable');
|
|
89
|
+
expect(document.body.style.paddingRight).toBe('');
|
|
90
|
+
|
|
91
|
+
hold.release();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('falls back to padding when the element grew anyway', () => {
|
|
95
|
+
// What an engine without scrollbar-gutter support does: the gutter request
|
|
96
|
+
// is ignored, the scrollbar goes away, and the element widens by 15px.
|
|
97
|
+
let width = 1009;
|
|
98
|
+
stub({innerWidth: 1024, clientWidth: 1009, widths: () => width});
|
|
99
|
+
|
|
100
|
+
const hold = holdScrollbarGutter(document.body);
|
|
101
|
+
width = 1024;
|
|
102
|
+
hold.settle();
|
|
103
|
+
|
|
104
|
+
expect(document.body.style.paddingRight).toBe('15px');
|
|
105
|
+
|
|
106
|
+
hold.release();
|
|
107
|
+
|
|
108
|
+
expect(document.body.style.paddingRight).toBe('');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("adds to the page's own padding instead of replacing it", () => {
|
|
112
|
+
let width = 1009;
|
|
113
|
+
stub({innerWidth: 1024, clientWidth: 1009, widths: () => width});
|
|
114
|
+
document.body.style.paddingRight = '24px';
|
|
115
|
+
|
|
116
|
+
const hold = holdScrollbarGutter(document.body);
|
|
117
|
+
width = 1024;
|
|
118
|
+
hold.settle();
|
|
119
|
+
|
|
120
|
+
expect(document.body.style.paddingRight).toBe('39px');
|
|
121
|
+
|
|
122
|
+
hold.release();
|
|
123
|
+
|
|
124
|
+
expect(document.body.style.paddingRight).toBe('24px');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('restores the page\u2019s own scrollbar-gutter rather than clearing it', () => {
|
|
128
|
+
stub({innerWidth: 1024, clientWidth: 1009});
|
|
129
|
+
document.documentElement.style.scrollbarGutter = 'stable both-edges';
|
|
130
|
+
|
|
131
|
+
const hold = holdScrollbarGutter(document.body);
|
|
132
|
+
|
|
133
|
+
expect(rootGutter()).toBe('stable');
|
|
134
|
+
|
|
135
|
+
hold.settle();
|
|
136
|
+
hold.release();
|
|
137
|
+
|
|
138
|
+
expect(rootGutter()).toBe('stable both-edges');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('settles once, however many times it is called', () => {
|
|
142
|
+
let width = 1009;
|
|
143
|
+
stub({innerWidth: 1024, clientWidth: 1009, widths: () => width});
|
|
144
|
+
|
|
145
|
+
const hold = holdScrollbarGutter(document.body);
|
|
146
|
+
width = 1024;
|
|
147
|
+
hold.settle();
|
|
148
|
+
hold.settle();
|
|
149
|
+
hold.settle();
|
|
150
|
+
|
|
151
|
+
expect(document.body.style.paddingRight).toBe('15px');
|
|
152
|
+
|
|
153
|
+
hold.release();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does nothing where there is no layout to measure', () => {
|
|
157
|
+
stub({innerWidth: 1024, clientWidth: 0});
|
|
158
|
+
|
|
159
|
+
const hold = holdScrollbarGutter(document.body);
|
|
160
|
+
hold.settle();
|
|
161
|
+
|
|
162
|
+
expect(rootGutter()).toBe('');
|
|
163
|
+
expect(document.body.style.paddingRight).toBe('');
|
|
164
|
+
|
|
165
|
+
hold.release();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file scrollbarGutter.ts
|
|
5
|
+
* @input The element a scroll lock pins, plus live layout measurements
|
|
6
|
+
* @output Exports holdScrollbarGutter and the ScrollbarGutterHold it returns
|
|
7
|
+
* @position Internal hook utility; used by useScrollLock and MobileNav so that
|
|
8
|
+
* locking background scroll does not resize the layout viewport.
|
|
9
|
+
*
|
|
10
|
+
* Locking scroll hides the document's scrollbar. Where that scrollbar is a
|
|
11
|
+
* classic (space-taking) one — Windows/Linux desktop, and macOS set to
|
|
12
|
+
* "Always" show scroll bars — hiding it widens the layout viewport by the
|
|
13
|
+
* scrollbar's width and the whole page jumps sideways behind the overlay.
|
|
14
|
+
*
|
|
15
|
+
* The fix is `scrollbar-gutter: stable`, which holds the gutter open once the
|
|
16
|
+
* scrollbar goes away. Being a real layout change it holds `position: fixed`
|
|
17
|
+
* chrome (sticky headers, toast viewports) as well as in-flow content, which
|
|
18
|
+
* padding fundamentally cannot: fixed elements resolve against the viewport,
|
|
19
|
+
* not against the padded element.
|
|
20
|
+
*
|
|
21
|
+
* Padding is kept only as a fallback for engines without `scrollbar-gutter`
|
|
22
|
+
* (Safari shipped it in 18.2), and it is applied by measuring whether the
|
|
23
|
+
* element actually moved rather than by assuming it did.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NOOP: ScrollbarGutterHold = {
|
|
27
|
+
settle() {},
|
|
28
|
+
release() {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface ScrollbarGutterHold {
|
|
32
|
+
/**
|
|
33
|
+
* Call once the lock's own styles are applied: measures whether the element
|
|
34
|
+
* grew anyway and, only then, pads the difference away.
|
|
35
|
+
*/
|
|
36
|
+
settle(): void;
|
|
37
|
+
/** Call on unlock: restores everything this hold changed. */
|
|
38
|
+
release(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Keeps `element`'s content box the width it is right now, across a scroll
|
|
43
|
+
* lock that is about to hide the document's scrollbar.
|
|
44
|
+
*
|
|
45
|
+
* Call it *before* applying the lock's styles, then {@link
|
|
46
|
+
* ScrollbarGutterHold.settle} immediately after, and {@link
|
|
47
|
+
* ScrollbarGutterHold.release} on unlock:
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* const hold = holdScrollbarGutter(document.body);
|
|
51
|
+
* body.style.position = 'fixed';
|
|
52
|
+
* hold.settle();
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* Overlay scrollbars (mobile, default macOS) take no layout space, so there is
|
|
56
|
+
* nothing to hold and nothing is touched.
|
|
57
|
+
*/
|
|
58
|
+
export function holdScrollbarGutter(element: HTMLElement): ScrollbarGutterHold {
|
|
59
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
60
|
+
return NOOP;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const root = document.documentElement;
|
|
64
|
+
const viewportWidth = root.clientWidth;
|
|
65
|
+
|
|
66
|
+
// 0 means "no layout to measure" (jsdom, and any non-visual environment),
|
|
67
|
+
// not "the scrollbar is as wide as the window".
|
|
68
|
+
if (viewportWidth === 0) {
|
|
69
|
+
return NOOP;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const previousGutter = root.style.scrollbarGutter;
|
|
73
|
+
const previousPadding = element.style.paddingRight;
|
|
74
|
+
const widthBefore = element.getBoundingClientRect().width;
|
|
75
|
+
|
|
76
|
+
let heldGutter = false;
|
|
77
|
+
let padded = false;
|
|
78
|
+
let settled = false;
|
|
79
|
+
|
|
80
|
+
// Only when a space-taking scrollbar is actually there. Holding the gutter
|
|
81
|
+
// open on a page that never had one would carve out 15px that was never
|
|
82
|
+
// reserved — the same shift, in the other direction.
|
|
83
|
+
if (window.innerWidth > viewportWidth) {
|
|
84
|
+
root.style.scrollbarGutter = 'stable';
|
|
85
|
+
heldGutter = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
settle() {
|
|
90
|
+
if (settled) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
settled = true;
|
|
94
|
+
|
|
95
|
+
// What the lock actually did to this element, rather than what we
|
|
96
|
+
// assumed it would do. 0 on any engine where the gutter held — and on a
|
|
97
|
+
// page that already sets `scrollbar-gutter: stable` itself, where the
|
|
98
|
+
// scrollbar was never taking the space back.
|
|
99
|
+
const grew = element.getBoundingClientRect().width - widthBefore;
|
|
100
|
+
if (grew <= 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const existing =
|
|
105
|
+
Number.parseFloat(window.getComputedStyle(element).paddingRight) || 0;
|
|
106
|
+
element.style.paddingRight = `${existing + grew}px`;
|
|
107
|
+
padded = true;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
release() {
|
|
111
|
+
if (padded) {
|
|
112
|
+
element.style.paddingRight = previousPadding;
|
|
113
|
+
padded = false;
|
|
114
|
+
}
|
|
115
|
+
if (heldGutter) {
|
|
116
|
+
root.style.scrollbarGutter = previousGutter;
|
|
117
|
+
heldGutter = false;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useResolvedRequired.ts
|
|
7
|
+
* @input Uses React, FormLayoutContext
|
|
8
|
+
* @output Exports useResolvedRequired
|
|
9
|
+
* @position Resolves a field's effective aria-required against the form default
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
12
|
+
* - /packages/core/src/FormLayout/FormLayoutContext.ts (defaultOptionality source)
|
|
13
|
+
* - /packages/core/src/FormLayout/FormLayout.tsx (prop doc: aria-required behavior)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {use} from 'react';
|
|
17
|
+
import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a field's effective *required* state for `aria-required`, honoring a
|
|
21
|
+
* surrounding `FormLayout`'s `defaultOptionality`.
|
|
22
|
+
*
|
|
23
|
+
* Under `defaultOptionality="required"` a field is required unless it opts out
|
|
24
|
+
* with `isOptional`. The visible indicator is suppressed for the unmarked
|
|
25
|
+
* majority, so those fields must still expose `aria-required` — otherwise a
|
|
26
|
+
* sighted user reads them as required (form-wide default, no indicator) while a
|
|
27
|
+
* screen reader hears "not required". This closes that mismatch.
|
|
28
|
+
*
|
|
29
|
+
* Semantics only: this drives `aria-required`, never the native `required`
|
|
30
|
+
* attribute, so a layout-level default can't silently switch on browser
|
|
31
|
+
* validation bubbles. `isOptional` takes precedence, matching `FieldLabel`.
|
|
32
|
+
*/
|
|
33
|
+
export function useResolvedRequired({
|
|
34
|
+
isRequired = false,
|
|
35
|
+
isOptional = false,
|
|
36
|
+
}: {
|
|
37
|
+
isRequired?: boolean;
|
|
38
|
+
isOptional?: boolean;
|
|
39
|
+
}): boolean {
|
|
40
|
+
const {defaultOptionality} = use(FormLayoutContext);
|
|
41
|
+
return !isOptional && (isRequired || defaultOptionality === 'required');
|
|
42
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export const docs = {
|
|
5
5
|
name: 'useScrollLock',
|
|
6
6
|
displayName: 'useScrollLock',
|
|
7
|
-
keywords: ['scroll', 'lock', 'modal', 'dialog', 'body', 'prevent', 'background', 'ios', 'safari', 'fixed'],
|
|
7
|
+
keywords: ['scroll', 'lock', 'modal', 'dialog', 'body', 'prevent', 'background', 'ios', 'safari', 'fixed', 'scrollbar', 'gutter', 'layout shift', 'reflow'],
|
|
8
8
|
params: [
|
|
9
9
|
{
|
|
10
10
|
name: 'isLocked',
|
|
@@ -16,7 +16,7 @@ export const docs = {
|
|
|
16
16
|
returns: [],
|
|
17
17
|
usage: {
|
|
18
18
|
description:
|
|
19
|
-
'Locks body scroll when active by pinning the body with position: fixed. This prevents background scrolling behind modals and dialogs, which is necessary for iOS Safari where overscroll-behavior: contain does not work. Restores the original scroll position when unlocked.',
|
|
19
|
+
'Locks body scroll when active by pinning the body with position: fixed. This prevents background scrolling behind modals and dialogs, which is necessary for iOS Safari where overscroll-behavior: contain does not work. Restores the original scroll position when unlocked. Pinning hides the document scrollbar, so where that scrollbar takes layout space (desktop) the hook holds its gutter open with scrollbar-gutter: stable for the duration of the lock — the page, including any position: fixed chrome, does not shift sideways.',
|
|
20
20
|
bestPractices: [
|
|
21
21
|
{ guidance: true, description: 'Use when opening full-screen modals or dialogs to prevent background content from scrolling.' },
|
|
22
22
|
{ guidance: true, description: 'Pass the same boolean that controls dialog visibility (e.g., isOpen) as the isLocked parameter.' },
|
|
@@ -32,13 +32,13 @@ export const docs = {
|
|
|
32
32
|
/** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
|
|
33
33
|
export const docsDense = {
|
|
34
34
|
description:
|
|
35
|
-
'Locks body scroll when active by pinning body w/ position: fixed. Prevents background scrolling behind modals + dialogs, necessary for iOS Safari where overscroll-behavior: contain does not work. Restores original scroll position when unlocked.',
|
|
35
|
+
'Locks body scroll when active by pinning body w/ position: fixed. Prevents background scrolling behind modals + dialogs, necessary for iOS Safari where overscroll-behavior: contain does not work. Restores original scroll position when unlocked. Holds the hidden scrollbar\'s gutter open (scrollbar-gutter: stable) so the page — incl. position: fixed chrome — does not shift sideways.',
|
|
36
36
|
paramDescriptions: {
|
|
37
37
|
isLocked: 'whether body scroll should be locked.',
|
|
38
38
|
},
|
|
39
39
|
usage: {
|
|
40
40
|
description:
|
|
41
|
-
'Locks body scroll when active by pinning body w/ position: fixed. Prevents background scrolling behind modals + dialogs, necessary for iOS Safari where overscroll-behavior: contain does not work. Restores original scroll position when unlocked.',
|
|
41
|
+
'Locks body scroll when active by pinning body w/ position: fixed. Prevents background scrolling behind modals + dialogs, necessary for iOS Safari where overscroll-behavior: contain does not work. Restores original scroll position when unlocked. Holds the hidden scrollbar\'s gutter open (scrollbar-gutter: stable) so the page — incl. position: fixed chrome — does not shift sideways.',
|
|
42
42
|
bestPractices: [
|
|
43
43
|
{ guidance: true, description: 'Use when opening full-screen modals / dialogs to prevent background content from scrolling.' },
|
|
44
44
|
{ guidance: true, description: 'Pass same boolean that controls dialog visibility (e.g. isOpen) as isLocked parameter.' },
|
|
@@ -17,6 +17,9 @@ describe('useScrollLock', () => {
|
|
|
17
17
|
afterEach(() => {
|
|
18
18
|
cleanup();
|
|
19
19
|
document.body.style.cssText = '';
|
|
20
|
+
document.documentElement.style.cssText = '';
|
|
21
|
+
// @ts-expect-error -- drop the viewport stub so jsdom's own value comes back
|
|
22
|
+
delete document.documentElement.clientWidth;
|
|
20
23
|
vi.restoreAllMocks();
|
|
21
24
|
});
|
|
22
25
|
|
|
@@ -69,4 +72,74 @@ describe('useScrollLock', () => {
|
|
|
69
72
|
expect(document.body.style.left).toBe('');
|
|
70
73
|
expect(document.body.style.right).toBe('');
|
|
71
74
|
});
|
|
75
|
+
|
|
76
|
+
it('holds the page still across the scrollbar it hides', () => {
|
|
77
|
+
// A 1024px window over a 1009px layout viewport = a 15px classic
|
|
78
|
+
// scrollbar. Pinning the body hides it, which would widen the page by
|
|
79
|
+
// those 15px and reflow everything sideways.
|
|
80
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
81
|
+
value: 1024,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
86
|
+
value: 1009,
|
|
87
|
+
configurable: true,
|
|
88
|
+
});
|
|
89
|
+
vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
|
90
|
+
|
|
91
|
+
const lock = renderHook(() => useScrollLock(true));
|
|
92
|
+
|
|
93
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('stable');
|
|
94
|
+
|
|
95
|
+
lock.unmount();
|
|
96
|
+
|
|
97
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('leaves the page alone when the scrollbar is an overlay one', () => {
|
|
101
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
102
|
+
value: 1024,
|
|
103
|
+
configurable: true,
|
|
104
|
+
writable: true,
|
|
105
|
+
});
|
|
106
|
+
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
107
|
+
value: 1024,
|
|
108
|
+
configurable: true,
|
|
109
|
+
});
|
|
110
|
+
vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
|
111
|
+
|
|
112
|
+
const lock = renderHook(() => useScrollLock(true));
|
|
113
|
+
|
|
114
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('');
|
|
115
|
+
expect(document.body.style.paddingRight).toBe('');
|
|
116
|
+
|
|
117
|
+
lock.unmount();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('holds the gutter for the outermost overlay only, and gives it back once', () => {
|
|
121
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
122
|
+
value: 1024,
|
|
123
|
+
configurable: true,
|
|
124
|
+
writable: true,
|
|
125
|
+
});
|
|
126
|
+
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
127
|
+
value: 1009,
|
|
128
|
+
configurable: true,
|
|
129
|
+
});
|
|
130
|
+
vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
|
131
|
+
|
|
132
|
+
const first = renderHook(() => useScrollLock(true));
|
|
133
|
+
const second = renderHook(() => useScrollLock(true));
|
|
134
|
+
|
|
135
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('stable');
|
|
136
|
+
|
|
137
|
+
first.unmount();
|
|
138
|
+
|
|
139
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('stable');
|
|
140
|
+
|
|
141
|
+
second.unmount();
|
|
142
|
+
|
|
143
|
+
expect(document.documentElement.style.scrollbarGutter).toBe('');
|
|
144
|
+
});
|
|
72
145
|
});
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* SYNC: When modified, update:
|
|
15
15
|
* - /packages/core/src/hooks/index.ts
|
|
16
|
+
* - /packages/core/src/hooks/scrollbarGutter.ts
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import {useEffect} from 'react';
|
|
20
|
+
import {holdScrollbarGutter, type ScrollbarGutterHold} from './scrollbarGutter';
|
|
19
21
|
|
|
20
22
|
interface ScrollLockSnapshot {
|
|
21
23
|
scrollX: number;
|
|
@@ -25,6 +27,7 @@ interface ScrollLockSnapshot {
|
|
|
25
27
|
top: string;
|
|
26
28
|
left: string;
|
|
27
29
|
right: string;
|
|
30
|
+
gutter: ScrollbarGutterHold;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
let lockCount = 0;
|
|
@@ -38,6 +41,10 @@ let originalBodyState: ScrollLockSnapshot | null = null;
|
|
|
38
41
|
* does not prevent body scroll behind modals. Restores scroll position
|
|
39
42
|
* on unlock.
|
|
40
43
|
*
|
|
44
|
+
* Pinning also hides the document's scrollbar, so the gutter that scrollbar
|
|
45
|
+
* occupied is held open for the duration of the lock — without it the page
|
|
46
|
+
* reflows sideways by ~15px the moment an overlay opens.
|
|
47
|
+
*
|
|
41
48
|
* @example
|
|
42
49
|
* ```
|
|
43
50
|
* useScrollLock(isOpen);
|
|
@@ -55,6 +62,9 @@ export function useScrollLock(isLocked: boolean): void {
|
|
|
55
62
|
const scrollX = window.scrollX;
|
|
56
63
|
const scrollY = window.scrollY;
|
|
57
64
|
|
|
65
|
+
// Taken before the pinning styles below hide the scrollbar.
|
|
66
|
+
const gutter = holdScrollbarGutter(body);
|
|
67
|
+
|
|
58
68
|
originalBodyState = {
|
|
59
69
|
scrollX,
|
|
60
70
|
scrollY,
|
|
@@ -63,6 +73,7 @@ export function useScrollLock(isLocked: boolean): void {
|
|
|
63
73
|
top: body.style.top,
|
|
64
74
|
left: body.style.left,
|
|
65
75
|
right: body.style.right,
|
|
76
|
+
gutter,
|
|
66
77
|
};
|
|
67
78
|
|
|
68
79
|
body.style.overflow = 'hidden';
|
|
@@ -70,6 +81,8 @@ export function useScrollLock(isLocked: boolean): void {
|
|
|
70
81
|
body.style.top = `-${scrollY}px`;
|
|
71
82
|
body.style.left = '0';
|
|
72
83
|
body.style.right = '0';
|
|
84
|
+
|
|
85
|
+
gutter.settle();
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
lockCount += 1;
|
|
@@ -89,6 +102,7 @@ export function useScrollLock(isLocked: boolean): void {
|
|
|
89
102
|
body.style.top = state.top;
|
|
90
103
|
body.style.left = state.left;
|
|
91
104
|
body.style.right = state.right;
|
|
105
|
+
state.gutter.release();
|
|
92
106
|
window.scrollTo(state.scrollX, state.scrollY);
|
|
93
107
|
};
|
|
94
108
|
}, [isLocked]);
|
|
@@ -85,6 +85,9 @@ const STRUCTURAL_VARS = new Set([
|
|
|
85
85
|
'--table-row-overlay',
|
|
86
86
|
'--separator-display',
|
|
87
87
|
'--astryx-section-padding',
|
|
88
|
+
// Private counterpart of the public token above: one ancestor Section's
|
|
89
|
+
// padding, propagated down the tree. Structural, never authored by a theme.
|
|
90
|
+
'--_section-padding-propagated',
|
|
88
91
|
]);
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -164,9 +167,6 @@ function discoverComponents(): ComponentInfo[] {
|
|
|
164
167
|
allVars.add(v);
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
|
-
if (allVars.size === 0) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
170
|
|
|
171
171
|
// Only check component directories (those with a doc file named after the
|
|
172
172
|
// directory). Match against the on-disk listing rather than existsSync so
|
|
@@ -188,6 +188,16 @@ function discoverComponents(): ComponentInfo[] {
|
|
|
188
188
|
/* skip */
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// A directory earns a check by declaring a var OR by documenting a
|
|
192
|
+
// derived[] entry. Bailing on the var count alone (as this did) hid every
|
|
193
|
+
// component that is themeable purely through an expansion strategy —
|
|
194
|
+
// `{property: 'padding', expand: 'container'}` names no var, so such a
|
|
195
|
+
// component declares nothing and its registry↔doc consistency check
|
|
196
|
+
// silently never ran.
|
|
197
|
+
if (allVars.size === 0 && docDerived.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
191
201
|
results.push({
|
|
192
202
|
dir,
|
|
193
203
|
sourceVars: [...allVars],
|
|
@@ -212,6 +222,7 @@ const DIR_TO_REGISTRY_KEY: Record<string, string> = {
|
|
|
212
222
|
DropdownMenu: 'dropdown-menu',
|
|
213
223
|
Field: 'field',
|
|
214
224
|
HoverCard: 'hovercard',
|
|
225
|
+
NumberInput: 'number-input',
|
|
215
226
|
Popover: 'popover',
|
|
216
227
|
ProgressBar: 'progressbar-mark',
|
|
217
228
|
Section: 'section',
|