@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
package/src/Dialog/Dialog.tsx
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
containerPaddingBlockStartVarStyles,
|
|
45
45
|
containerPaddingBlockEndVarStyles,
|
|
46
46
|
spacingStepToToken,
|
|
47
|
+
overlayPaddingReset,
|
|
47
48
|
} from '../Layout/padding.stylex';
|
|
48
49
|
import type {SpacingStep} from '../utils/types';
|
|
49
50
|
import {mergeProps, mergeRefs} from '../utils';
|
|
@@ -610,6 +611,7 @@ export function Dialog({
|
|
|
610
611
|
themeProps('dialog', {variant}),
|
|
611
612
|
stylex.props(
|
|
612
613
|
styles.inlineWrapper,
|
|
614
|
+
overlayPaddingReset.reset,
|
|
613
615
|
!isFullscreen && dynamicStyles.sizing(width, maxHeight),
|
|
614
616
|
isFullscreen && styles.fullscreen,
|
|
615
617
|
xstyle,
|
|
@@ -636,6 +638,7 @@ export function Dialog({
|
|
|
636
638
|
themeProps('dialog', {variant}),
|
|
637
639
|
focusOutlineProps.focusVisible(
|
|
638
640
|
styles.dialog,
|
|
641
|
+
overlayPaddingReset.reset,
|
|
639
642
|
isOpen && styles.open,
|
|
640
643
|
styles.backdrop,
|
|
641
644
|
!isFullscreen && dynamicStyles.sizing(width, maxHeight),
|
package/src/Field/Field.doc.mjs
CHANGED
package/src/Field/FieldLabel.tsx
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file FieldLabel.tsx
|
|
7
|
-
* @input Uses React, Icon, IconType, useTranslator
|
|
7
|
+
* @input Uses React, Icon, IconType, useTranslator, FormLayoutContext
|
|
8
8
|
* @output Exports FieldLabel component, FieldLabelProps
|
|
9
9
|
* @position Core label implementation; used by Field, CheckboxInput, Switch
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
12
|
* - /packages/core/src/Field/Field.doc.mjs (props table, features, implementation notes)
|
|
13
13
|
* - /packages/core/src/Field/index.ts (exports if types change)
|
|
14
|
+
* - /packages/core/src/FormLayout/FormLayoutContext.ts (defaultOptionality drives the indicator)
|
|
14
15
|
* - /packages/cli/assets/templates/blocks/components/Field/ (showcase blocks)
|
|
15
16
|
* - /packages/core/locales/en.json (@astryx.field.required / @astryx.field.optional)
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import {useMemo, useRef, type ReactNode, type RefObject} from 'react';
|
|
19
|
+
import {use, useMemo, useRef, type ReactNode, type RefObject} from 'react';
|
|
19
20
|
import * as stylex from '@stylexjs/stylex';
|
|
20
21
|
import type {BaseProps} from '../BaseProps';
|
|
21
22
|
import {mergeProps} from '../utils';
|
|
@@ -32,6 +33,7 @@ import {Tooltip} from '../Tooltip';
|
|
|
32
33
|
import {useTranslator} from '../i18n';
|
|
33
34
|
import {themeProps} from '../utils/themeProps';
|
|
34
35
|
import {useInputContainer} from '../hooks';
|
|
36
|
+
import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
|
|
35
37
|
|
|
36
38
|
const styles = stylex.create({
|
|
37
39
|
label: {
|
|
@@ -188,9 +190,23 @@ export function FieldLabel({
|
|
|
188
190
|
...rest
|
|
189
191
|
}: FieldLabelProps) {
|
|
190
192
|
const t = useTranslator();
|
|
191
|
-
const
|
|
193
|
+
const {defaultOptionality} = use(FormLayoutContext);
|
|
194
|
+
|
|
195
|
+
// A form-level `defaultOptionality` means "only the exception is marked": a
|
|
196
|
+
// field that merely restates the form's default shows no indicator, and only
|
|
197
|
+
// a deviation from it does. This is the *visible indicator* only; the
|
|
198
|
+
// matching `aria-required` is resolved on each control (see
|
|
199
|
+
// useResolvedRequired) so the unmarked majority is still announced.
|
|
200
|
+
//
|
|
201
|
+
// defaultOptionality isRequired isOptional
|
|
202
|
+
// 'optional' → required indicator → (matches default, hidden)
|
|
203
|
+
// 'required' → (matches, hidden) → optional indicator
|
|
204
|
+
// unset → required indicator → optional indicator
|
|
205
|
+
const showRequired = isRequired && defaultOptionality !== 'required';
|
|
206
|
+
const showOptional = isOptional && defaultOptionality !== 'optional';
|
|
207
|
+
const statusText = showOptional
|
|
192
208
|
? t('@astryx.field.optional')
|
|
193
|
-
:
|
|
209
|
+
: showRequired
|
|
194
210
|
? t('@astryx.field.required')
|
|
195
211
|
: null;
|
|
196
212
|
|
|
@@ -114,4 +114,29 @@ describe('InputClearButton', () => {
|
|
|
114
114
|
expect(css).toContain('.astryx-input-clear-icon:hover {');
|
|
115
115
|
expect(css).toContain('color: var(--color-icon-primary)');
|
|
116
116
|
});
|
|
117
|
+
|
|
118
|
+
it('renders the astryx-input-clear-button target on the button wrapper', () => {
|
|
119
|
+
render(<InputClearButton label="Clear" onClick={() => {}} />);
|
|
120
|
+
const button = screen.getByRole('button', {name: 'Clear'});
|
|
121
|
+
expect(button).toHaveClass('astryx-input-clear-button');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('exposes input-clear-button so a theme controls the button size and hover', () => {
|
|
125
|
+
const theme = defineTheme({
|
|
126
|
+
name: 'input-clear-button-test',
|
|
127
|
+
components: {
|
|
128
|
+
'input-clear-button': {
|
|
129
|
+
base: {
|
|
130
|
+
height: '28px',
|
|
131
|
+
':hover': {backgroundImage: 'none'},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const css = generateThemeTestCSS(theme);
|
|
137
|
+
expect(css).toContain('.astryx-input-clear-button {');
|
|
138
|
+
expect(css).toContain('height: 28px');
|
|
139
|
+
expect(css).toContain('.astryx-input-clear-button:hover {');
|
|
140
|
+
expect(css).toContain('background-image: none');
|
|
141
|
+
});
|
|
117
142
|
});
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* TextInput, NumberInput, TimeInput, DateInput, DateTimeInput,
|
|
12
12
|
* DateRangeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput —
|
|
13
13
|
* routes it through here, so the glyph is themed in one place via the
|
|
14
|
-
* `astryx-input-clear-icon` target
|
|
14
|
+
* `astryx-input-clear-icon` target and the button wrapper is themed via the
|
|
15
|
+
* `astryx-input-clear-button` target.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import type {ReactNode} from 'react';
|
|
@@ -48,11 +49,13 @@ export function InputClearButton({
|
|
|
48
49
|
iconClassName,
|
|
49
50
|
}: InputClearButtonProps): ReactNode {
|
|
50
51
|
const {className: iconTargetClassName} = themeProps('input-clear-icon');
|
|
52
|
+
const {className: buttonTargetClassName} = themeProps('input-clear-button');
|
|
51
53
|
return (
|
|
52
54
|
<Button
|
|
53
55
|
variant="ghost"
|
|
54
56
|
size="sm"
|
|
55
57
|
label={label}
|
|
58
|
+
className={buttonTargetClassName}
|
|
56
59
|
icon={
|
|
57
60
|
<Icon
|
|
58
61
|
icon="close"
|
|
@@ -16,6 +16,12 @@ export const docs = {
|
|
|
16
16
|
'Controls field arrangement. Vertical stacks top-to-bottom, horizontal arranges left-to-right with equal flex-grow, and horizontal-labels uses CSS Grid with labels to the left of inputs (collapses to vertical on narrow viewports <=480px).',
|
|
17
17
|
default: "'vertical'",
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
name: 'defaultOptionality',
|
|
21
|
+
type: "'optional' | 'required'",
|
|
22
|
+
description:
|
|
23
|
+
'The state the form treats as its default, so only the exception shows an optional/required indicator. With "optional", only fields marked isRequired show an indicator; with "required", only fields marked isOptional do. A field that restates the default shows nothing. Under "required" the unmarked fields also expose aria-required so screen readers match the visual default — aria-required only, never the native required attribute. Leave unset for today\'s per-field behavior.',
|
|
24
|
+
},
|
|
19
25
|
{
|
|
20
26
|
name: 'children',
|
|
21
27
|
type: 'ReactNode',
|
|
@@ -64,6 +70,12 @@ export const docsZh = {
|
|
|
64
70
|
'控制字段排列方式。vertical 从上到下堆叠,horizontal 从左到右排列且等比弹性增长,horizontal-labels 使用 CSS Grid 将标签放在输入框左侧(在窄视口 <=480px 时折叠为垂直布局)。',
|
|
65
71
|
default: "'vertical'",
|
|
66
72
|
},
|
|
73
|
+
{
|
|
74
|
+
name: 'defaultOptionality',
|
|
75
|
+
type: "'optional' | 'required'",
|
|
76
|
+
description:
|
|
77
|
+
'表单视为默认的状态,因此仅例外字段显示可选/必填指示器。设为 "optional" 时,仅标记 isRequired 的字段显示指示器;设为 "required" 时,仅标记 isOptional 的字段显示。与默认一致的字段不显示任何内容。设为 "required" 时,未标记的字段仍会暴露 aria-required,使屏幕阅读器与视觉默认一致——仅作用于 aria-required,不改变原生 required 属性。不设置则保持当前逐字段行为。',
|
|
78
|
+
},
|
|
67
79
|
{
|
|
68
80
|
name: 'children',
|
|
69
81
|
type: 'ReactNode',
|
|
@@ -122,6 +134,7 @@ export const docsDense = {
|
|
|
122
134
|
},
|
|
123
135
|
propDescriptions: {
|
|
124
136
|
direction: 'Field arrangement. Vertical stacks top-to-bottom, horizontal arranges left-to-right w/ equal flex-grow, horizontal-labels uses CSS Grid w/ labels left of inputs (collapses <=480px).',
|
|
137
|
+
defaultOptionality: 'State the form treats as default, so only the exception is marked. "optional" → only isRequired fields show an indicator; "required" → only isOptional fields do. Under "required" unmarked fields also expose aria-required (aria only, not native required). Unset = per-field behavior.',
|
|
125
138
|
children: 'Form fields to arrange. Accepts Astryx inputs + Field-wrapped custom controls.',
|
|
126
139
|
xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
|
|
127
140
|
},
|
|
@@ -16,6 +16,8 @@ import {FormLayout} from './FormLayout';
|
|
|
16
16
|
import {FormLayoutContext} from './FormLayoutContext';
|
|
17
17
|
import type {FormLayoutDirection} from './FormLayoutContext';
|
|
18
18
|
import {Field} from '../Field';
|
|
19
|
+
import {TextInput} from '../TextInput';
|
|
20
|
+
import {CheckboxInput} from '../CheckboxInput';
|
|
19
21
|
|
|
20
22
|
// Helper component to read context
|
|
21
23
|
function DirectionReader() {
|
|
@@ -23,6 +25,12 @@ function DirectionReader() {
|
|
|
23
25
|
return <span data-testid="direction">{direction}</span>;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// Helper component to read the defaultOptionality context value
|
|
29
|
+
function OptionalityReader() {
|
|
30
|
+
const {defaultOptionality} = use(FormLayoutContext);
|
|
31
|
+
return <span data-testid="optionality">{defaultOptionality ?? 'unset'}</span>;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
describe('FormLayout', () => {
|
|
27
35
|
// ─── Basic rendering ────────────────────────────────────────────────────
|
|
28
36
|
|
|
@@ -154,6 +162,178 @@ describe('FormLayout', () => {
|
|
|
154
162
|
expect(screen.getByTestId('inner-child-2')).toBeInTheDocument();
|
|
155
163
|
});
|
|
156
164
|
|
|
165
|
+
// ─── defaultOptionality context propagation ─────────────────────────────
|
|
166
|
+
|
|
167
|
+
it('leaves defaultOptionality unset by default', () => {
|
|
168
|
+
render(
|
|
169
|
+
<FormLayout>
|
|
170
|
+
<OptionalityReader />
|
|
171
|
+
</FormLayout>,
|
|
172
|
+
);
|
|
173
|
+
expect(screen.getByTestId('optionality')).toHaveTextContent('unset');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('provides defaultOptionality="optional" to children', () => {
|
|
177
|
+
render(
|
|
178
|
+
<FormLayout defaultOptionality="optional">
|
|
179
|
+
<OptionalityReader />
|
|
180
|
+
</FormLayout>,
|
|
181
|
+
);
|
|
182
|
+
expect(screen.getByTestId('optionality')).toHaveTextContent('optional');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('provides defaultOptionality="required" to children', () => {
|
|
186
|
+
render(
|
|
187
|
+
<FormLayout defaultOptionality="required">
|
|
188
|
+
<OptionalityReader />
|
|
189
|
+
</FormLayout>,
|
|
190
|
+
);
|
|
191
|
+
expect(screen.getByTestId('optionality')).toHaveTextContent('required');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('an inner layout shadows the outer defaultOptionality', () => {
|
|
195
|
+
render(
|
|
196
|
+
<FormLayout defaultOptionality="optional">
|
|
197
|
+
<FormLayout defaultOptionality="required">
|
|
198
|
+
<OptionalityReader />
|
|
199
|
+
</FormLayout>
|
|
200
|
+
</FormLayout>,
|
|
201
|
+
);
|
|
202
|
+
expect(screen.getByTestId('optionality')).toHaveTextContent('required');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─── defaultOptionality indicator behavior (through Field) ───────────────
|
|
206
|
+
//
|
|
207
|
+
// The rule: only the *exception* is marked. A field that restates the form
|
|
208
|
+
// default shows nothing; a deviation shows its indicator.
|
|
209
|
+
|
|
210
|
+
it('optional default: only isRequired fields show an indicator', () => {
|
|
211
|
+
render(
|
|
212
|
+
<FormLayout defaultOptionality="optional">
|
|
213
|
+
<Field label="Bio" inputID="bio">
|
|
214
|
+
<input id="bio" />
|
|
215
|
+
</Field>
|
|
216
|
+
<Field label="Nickname" inputID="nick" isOptional>
|
|
217
|
+
<input id="nick" />
|
|
218
|
+
</Field>
|
|
219
|
+
<Field label="Email" inputID="email" isRequired>
|
|
220
|
+
<input id="email" />
|
|
221
|
+
</Field>
|
|
222
|
+
</FormLayout>,
|
|
223
|
+
);
|
|
224
|
+
// Plain + isOptional match the default → nothing shown.
|
|
225
|
+
expect(screen.queryByText(/Optional/)).not.toBeInTheDocument();
|
|
226
|
+
// isRequired deviates → the required indicator shows.
|
|
227
|
+
expect(screen.getByText(/Required/)).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('required default: only isOptional fields show an indicator', () => {
|
|
231
|
+
render(
|
|
232
|
+
<FormLayout defaultOptionality="required">
|
|
233
|
+
<Field label="Name" inputID="name">
|
|
234
|
+
<input id="name" />
|
|
235
|
+
</Field>
|
|
236
|
+
<Field label="Email" inputID="email" isRequired>
|
|
237
|
+
<input id="email" />
|
|
238
|
+
</Field>
|
|
239
|
+
<Field label="Nickname" inputID="nick" isOptional>
|
|
240
|
+
<input id="nick" />
|
|
241
|
+
</Field>
|
|
242
|
+
</FormLayout>,
|
|
243
|
+
);
|
|
244
|
+
// Plain + isRequired match the default → nothing shown.
|
|
245
|
+
expect(screen.queryByText(/Required/)).not.toBeInTheDocument();
|
|
246
|
+
// isOptional deviates → the optional indicator shows.
|
|
247
|
+
expect(screen.getByText(/Optional/)).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('unset default preserves per-field indicators (backwards compatible)', () => {
|
|
251
|
+
render(
|
|
252
|
+
<FormLayout>
|
|
253
|
+
<Field label="Email" inputID="email" isRequired>
|
|
254
|
+
<input id="email" />
|
|
255
|
+
</Field>
|
|
256
|
+
<Field label="Nickname" inputID="nick" isOptional>
|
|
257
|
+
<input id="nick" />
|
|
258
|
+
</Field>
|
|
259
|
+
</FormLayout>,
|
|
260
|
+
);
|
|
261
|
+
expect(screen.getByText(/Required/)).toBeInTheDocument();
|
|
262
|
+
expect(screen.getByText(/Optional/)).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── defaultOptionality aria-required resolution ─────────────────────────
|
|
266
|
+
//
|
|
267
|
+
// The indicator is suppressed for the unmarked majority, so the matching
|
|
268
|
+
// `aria-required` must still be exposed — otherwise a sighted user reads a
|
|
269
|
+
// field as required (form default, no indicator) while a screen reader hears
|
|
270
|
+
// "not required". Native `required` stays bound to the explicit prop so a
|
|
271
|
+
// layout default never switches on browser validation.
|
|
272
|
+
|
|
273
|
+
it('required default: an unmarked input still exposes aria-required', () => {
|
|
274
|
+
render(
|
|
275
|
+
<FormLayout defaultOptionality="required">
|
|
276
|
+
<TextInput label="Name" value="" onChange={() => {}} />
|
|
277
|
+
</FormLayout>,
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByLabelText('Name')).toHaveAttribute(
|
|
280
|
+
'aria-required',
|
|
281
|
+
'true',
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('required default: an isOptional input is not aria-required', () => {
|
|
286
|
+
render(
|
|
287
|
+
<FormLayout defaultOptionality="required">
|
|
288
|
+
<TextInput label="Nickname" value="" onChange={() => {}} isOptional />
|
|
289
|
+
</FormLayout>,
|
|
290
|
+
);
|
|
291
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-required');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('optional default: an unmarked input is not aria-required', () => {
|
|
295
|
+
render(
|
|
296
|
+
<FormLayout defaultOptionality="optional">
|
|
297
|
+
<TextInput label="Bio" value="" onChange={() => {}} />
|
|
298
|
+
</FormLayout>,
|
|
299
|
+
);
|
|
300
|
+
expect(screen.getByLabelText('Bio')).not.toHaveAttribute('aria-required');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('no layout: an unmarked input is not aria-required (backwards compatible)', () => {
|
|
304
|
+
render(<TextInput label="Solo" value="" onChange={() => {}} />);
|
|
305
|
+
expect(screen.getByLabelText('Solo')).not.toHaveAttribute('aria-required');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('required default resolves aria-required without native required', () => {
|
|
309
|
+
render(
|
|
310
|
+
<FormLayout defaultOptionality="required">
|
|
311
|
+
<CheckboxInput label="Terms" value={false} onChange={() => {}} />
|
|
312
|
+
</FormLayout>,
|
|
313
|
+
);
|
|
314
|
+
const checkbox = screen.getByRole('checkbox', {name: 'Terms'});
|
|
315
|
+
// Announced as required (form default)…
|
|
316
|
+
expect(checkbox).toHaveAttribute('aria-required', 'true');
|
|
317
|
+
// …but the native `required` attribute is not switched on by the layout.
|
|
318
|
+
expect(checkbox).not.toHaveAttribute('required');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('explicit isRequired still drives native required under a layout', () => {
|
|
322
|
+
render(
|
|
323
|
+
<FormLayout defaultOptionality="required">
|
|
324
|
+
<CheckboxInput
|
|
325
|
+
label="Consent"
|
|
326
|
+
value={false}
|
|
327
|
+
onChange={() => {}}
|
|
328
|
+
isRequired
|
|
329
|
+
/>
|
|
330
|
+
</FormLayout>,
|
|
331
|
+
);
|
|
332
|
+
const checkbox = screen.getByRole('checkbox', {name: 'Consent'});
|
|
333
|
+
expect(checkbox).toHaveAttribute('aria-required', 'true');
|
|
334
|
+
expect(checkbox).toHaveAttribute('required');
|
|
335
|
+
});
|
|
336
|
+
|
|
157
337
|
// ─── Snapshot tests ─────────────────────────────────────────────────────
|
|
158
338
|
|
|
159
339
|
it('matches snapshot for vertical direction', () => {
|
|
@@ -223,10 +403,7 @@ describe('FormLayout', () => {
|
|
|
223
403
|
it('horizontal-labels with Field: label and input wrapper are siblings under display:contents', () => {
|
|
224
404
|
render(
|
|
225
405
|
<FormLayout direction="horizontal-labels" data-testid="layout">
|
|
226
|
-
<Field
|
|
227
|
-
label="Username"
|
|
228
|
-
inputID="username"
|
|
229
|
-
data-testid="username-field">
|
|
406
|
+
<Field label="Username" inputID="username" data-testid="username-field">
|
|
230
407
|
<input id="username" data-testid="username-input" />
|
|
231
408
|
</Field>
|
|
232
409
|
</FormLayout>,
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
12
|
* - /packages/core/src/FormLayout/FormLayout.test.tsx (tests for new/changed behavior)
|
|
13
|
+
* - /packages/core/src/FormLayout/FormLayoutContext.ts (context value shape)
|
|
14
|
+
* - /packages/core/src/FormLayout/FormLayout.doc.mjs (props table)
|
|
13
15
|
* - /packages/core/src/FormLayout/index.ts (exports if types change)
|
|
16
|
+
* - /packages/core/src/Field/FieldLabel.tsx (defaultOptionality indicator resolution)
|
|
14
17
|
* - /apps/storybook/stories/FormLayout.stories.tsx (storybook stories)
|
|
15
18
|
* - /packages/cli/assets/templates/blocks/components/FormLayout/ (showcase blocks)
|
|
16
19
|
*/
|
|
@@ -19,7 +22,11 @@ import {useMemo, type ReactNode} from 'react';
|
|
|
19
22
|
import type {BaseProps} from '../BaseProps';
|
|
20
23
|
import * as stylex from '@stylexjs/stylex';
|
|
21
24
|
import {spacingVars} from '../theme/tokens.stylex';
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
FormLayoutContext,
|
|
27
|
+
type FormLayoutDirection,
|
|
28
|
+
type FormOptionality,
|
|
29
|
+
} from './FormLayoutContext';
|
|
23
30
|
import {mergeProps} from '../utils';
|
|
24
31
|
import {themeProps} from '../utils/themeProps';
|
|
25
32
|
|
|
@@ -82,6 +89,26 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
82
89
|
* @default 'vertical'
|
|
83
90
|
*/
|
|
84
91
|
direction?: FormLayoutDirection;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Which state the form treats as its default, so only the *exception*
|
|
95
|
+
* carries a visible optional/required indicator. It also resolves each
|
|
96
|
+
* field's `aria-required` so the unmarked majority is still announced
|
|
97
|
+
* correctly — but only `aria-required`, never the native `required`
|
|
98
|
+
* attribute, so a layout default can't switch on browser validation.
|
|
99
|
+
*
|
|
100
|
+
* - `'optional'` — fields read as optional; only a field with `isRequired`
|
|
101
|
+
* shows an indicator (the "required" one).
|
|
102
|
+
* - `'required'` — fields read as required; only a field with `isOptional`
|
|
103
|
+
* shows an indicator (the "optional" one). Fields without `isOptional`
|
|
104
|
+
* expose `aria-required` even though they show no indicator.
|
|
105
|
+
* - unset — today's behavior: `isRequired` and `isOptional` each show their
|
|
106
|
+
* own indicator independently.
|
|
107
|
+
*
|
|
108
|
+
* A field that merely restates the default (e.g. `isOptional` under
|
|
109
|
+
* `'optional'`) shows nothing. An inner `FormLayout` shadows an outer one.
|
|
110
|
+
*/
|
|
111
|
+
defaultOptionality?: FormOptionality;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
// =============================================================================
|
|
@@ -109,13 +136,17 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
109
136
|
export function FormLayout({
|
|
110
137
|
children,
|
|
111
138
|
direction = 'vertical',
|
|
139
|
+
defaultOptionality,
|
|
112
140
|
xstyle,
|
|
113
141
|
className,
|
|
114
142
|
style,
|
|
115
143
|
ref,
|
|
116
144
|
...props
|
|
117
145
|
}: FormLayoutProps) {
|
|
118
|
-
const contextValue = useMemo(
|
|
146
|
+
const contextValue = useMemo(
|
|
147
|
+
() => ({direction, defaultOptionality}),
|
|
148
|
+
[direction, defaultOptionality],
|
|
149
|
+
);
|
|
119
150
|
|
|
120
151
|
return (
|
|
121
152
|
<FormLayoutContext value={contextValue}>
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file FormLayoutContext.ts
|
|
7
7
|
* @input Uses React createContext
|
|
8
|
-
* @output Exports FormLayoutContext and
|
|
9
|
-
* @position Context for form layout direction detection
|
|
8
|
+
* @output Exports FormLayoutContext, FormLayoutDirection, and FormOptionality types
|
|
9
|
+
* @position Context for form layout direction + default-optionality detection
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
|
+
* - /packages/core/src/FormLayout/FormLayout.tsx (prop + context value)
|
|
13
|
+
* - /packages/core/src/Field/FieldLabel.tsx (indicator resolution)
|
|
14
|
+
* - /packages/core/src/FormLayout/index.ts (exports if types change)
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import {createContext} from 'react';
|
|
@@ -22,15 +25,27 @@ import {createContext} from 'react';
|
|
|
22
25
|
* of their inputs (settings/admin panel pattern).
|
|
23
26
|
*/
|
|
24
27
|
export type FormLayoutDirection =
|
|
25
|
-
| '
|
|
26
|
-
| 'horizontal'
|
|
27
|
-
| 'horizontal-labels';
|
|
28
|
+
'vertical' | 'horizontal' | 'horizontal-labels';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Which state a form treats as its default, so only the *exception* carries a
|
|
32
|
+
* visible optional/required indicator.
|
|
33
|
+
*
|
|
34
|
+
* - `'optional'` — fields are optional unless a field opts into `isRequired`;
|
|
35
|
+
* only required fields show an indicator.
|
|
36
|
+
* - `'required'` — fields are required unless a field opts into `isOptional`;
|
|
37
|
+
* only optional fields show an indicator.
|
|
38
|
+
*/
|
|
39
|
+
export type FormOptionality = 'optional' | 'required';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Context for detecting which form layout a component is rendered in. Children
|
|
43
|
+
* can use this to adapt their rendering based on the parent layout — direction
|
|
44
|
+
* for spatial arrangement, and `defaultOptionality` so a field can suppress the
|
|
45
|
+
* indicator that merely restates the form-wide default.
|
|
32
46
|
*/
|
|
33
47
|
export const FormLayoutContext = createContext<{
|
|
34
48
|
direction: FormLayoutDirection;
|
|
49
|
+
defaultOptionality?: FormOptionality;
|
|
35
50
|
}>({direction: 'vertical'});
|
|
36
51
|
FormLayoutContext.displayName = 'FormLayoutContext';
|
package/src/FormLayout/index.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
|
|
14
14
|
export {FormLayout} from './FormLayout';
|
|
15
15
|
export type {FormLayoutProps} from './FormLayout';
|
|
16
|
-
export type {FormLayoutDirection} from './FormLayoutContext';
|
|
16
|
+
export type {FormLayoutDirection, FormOptionality} from './FormLayoutContext';
|
|
17
17
|
export {FormLayoutContext} from './FormLayoutContext';
|
package/src/Item/Item.doc.mjs
CHANGED
|
@@ -40,6 +40,7 @@ export const docs = {
|
|
|
40
40
|
{name: 'density', type: "'compact' | 'balanced' | 'spacious'", description: 'Spacing density. "compact" uses 4px block padding, "balanced" uses 8px, and "spacious" uses 12px block and inline padding.', default: "'balanced'"},
|
|
41
41
|
{name: 'labelLines', type: 'number', description: 'Max lines before label truncates with ellipsis.'},
|
|
42
42
|
{name: 'descriptionLines', type: 'number', description: 'Max lines before description truncates with ellipsis.'},
|
|
43
|
+
{name: 'layout', type: "'stacked' | 'inline'", description: 'How the label and description sit together. stacked puts the description on its own line below the label; inline keeps both on one line, description ellipsizing first, so the row fits a fixed-height host.', default: "'stacked'"},
|
|
43
44
|
{name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler. Makes the item clickable with button semantics.'},
|
|
44
45
|
{name: 'interactiveRef', type: 'RefObject<HTMLElement | null>', description: 'Ref to a nested control (e.g. a checkbox in startContent) that owns the item\'s keyboard access and action. The row becomes an enlarged click/tap target that delegates surface clicks to it (useClickableContainer) and renders no invisible button/anchor, so the row adds no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href; those are ignored when set.'},
|
|
45
46
|
{name: 'href', type: 'string', description: 'Link URL. Makes the item a link via an invisible anchor element.'},
|
package/src/Item/Item.test.tsx
CHANGED
|
@@ -508,4 +508,54 @@ describe('Item', () => {
|
|
|
508
508
|
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
509
509
|
expect(screen.getByText(/commented/)).toBeInTheDocument();
|
|
510
510
|
});
|
|
511
|
+
it('puts the label and description in one row when layout is inline', () => {
|
|
512
|
+
const stacked = render(
|
|
513
|
+
<Item label="Private" description="Only members can access" />,
|
|
514
|
+
);
|
|
515
|
+
const stackedRow = screen.getByText('Private').parentElement;
|
|
516
|
+
stacked.unmount();
|
|
517
|
+
|
|
518
|
+
render(
|
|
519
|
+
<Item
|
|
520
|
+
label="Private"
|
|
521
|
+
description="Only members can access"
|
|
522
|
+
layout="inline"
|
|
523
|
+
/>,
|
|
524
|
+
);
|
|
525
|
+
const inlineRow = screen.getByText('Private').parentElement;
|
|
526
|
+
|
|
527
|
+
// Same container, different styling: the shared content box switches from
|
|
528
|
+
// a column to a row, so its class list must differ from the stacked one.
|
|
529
|
+
expect(inlineRow?.className).not.toBe(stackedRow?.className);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('ellipsizes a ReactNode description when layout is inline', () => {
|
|
533
|
+
// A stacked ReactNode description is left alone (it may wrap); an inline
|
|
534
|
+
// one is one line by definition, so it truncates like a string does.
|
|
535
|
+
const stacked = render(
|
|
536
|
+
<Item
|
|
537
|
+
label="Private"
|
|
538
|
+
description={<span>Only members can access</span>}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
const stackedDescription = screen.getByText('Only members can access')
|
|
542
|
+
.parentElement?.className;
|
|
543
|
+
stacked.unmount();
|
|
544
|
+
|
|
545
|
+
render(
|
|
546
|
+
<Item
|
|
547
|
+
label="Private"
|
|
548
|
+
description={<span>Only members can access</span>}
|
|
549
|
+
layout="inline"
|
|
550
|
+
/>,
|
|
551
|
+
);
|
|
552
|
+
expect(
|
|
553
|
+
screen.getByText('Only members can access').parentElement?.className,
|
|
554
|
+
).not.toBe(stackedDescription);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('ignores inline layout when there is no description', () => {
|
|
558
|
+
render(<Item label="Private" layout="inline" />);
|
|
559
|
+
expect(screen.getByText('Private')).toBeInTheDocument();
|
|
560
|
+
});
|
|
511
561
|
});
|