@shohojdhara/atomix 0.3.15 → 0.4.1
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/build-tools/index.d.ts +31 -30
- package/build-tools/package.json +4 -21
- package/dist/atomix.css +20234 -2027
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +76 -2
- package/dist/atomix.min.css.map +1 -1
- package/dist/build-tools/index.d.ts +31 -30
- package/dist/build-tools/package.json +4 -21
- package/dist/charts.js +4 -5
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +87 -10
- package/dist/core.js +673 -480
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +15 -3
- package/dist/forms.js +530 -97
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +5 -6
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +644 -277
- package/dist/index.esm.js +1948 -1347
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3333 -2728
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/layout.js.map +1 -1
- package/dist/theme.d.ts +9 -9
- package/dist/theme.js.map +1 -1
- package/package.json +2 -2
- package/scripts/atomix-cli.js +10 -1
- package/scripts/cli/__tests__/utils.test.js +6 -2
- package/scripts/cli/migration-tools.js +2 -2
- package/scripts/cli/theme-bridge.js +7 -9
- package/scripts/cli/utils.js +2 -1
- package/src/components/Accordion/Accordion.stories.tsx +72 -23
- package/src/components/Accordion/Accordion.test.tsx +70 -50
- package/src/components/Accordion/Accordion.tsx +219 -96
- package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +1 -1
- package/src/components/AtomixGlass/GlassFilter.tsx +9 -16
- package/src/components/AtomixGlass/glass-utils.ts +4 -3
- package/src/components/AtomixGlass/shader-utils.ts +128 -52
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +1 -1
- package/src/components/AtomixGlass/stories/Shaders.stories.tsx +1 -1
- package/src/components/Avatar/Avatar.stories.tsx +45 -62
- package/src/components/Avatar/Avatar.tsx +58 -56
- package/src/components/Badge/Badge.stories.tsx +20 -9
- package/src/components/Badge/Badge.test.tsx +41 -41
- package/src/components/Badge/Badge.tsx +64 -62
- package/src/components/Block/Block.stories.tsx +14 -4
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +9 -8
- package/src/components/Breadcrumb/Breadcrumb.tsx +173 -65
- package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
- package/src/components/Button/Button.stories.tsx +13 -22
- package/src/components/Button/Button.test.tsx +97 -81
- package/src/components/Button/Button.tsx +46 -14
- package/src/components/Button/ButtonGroup.stories.tsx +37 -32
- package/src/components/Button/ButtonGroup.tsx +4 -15
- package/src/components/Callout/Callout.stories.tsx +166 -918
- package/src/components/Callout/Callout.tsx +196 -84
- package/src/components/Callout/CalloutCompound.test.tsx +72 -0
- package/src/components/Card/Card.stories.tsx +67 -36
- package/src/components/Card/Card.tsx +30 -14
- package/src/components/Chart/AreaChart.tsx +1 -1
- package/src/components/Chart/CandlestickChart.tsx +23 -16
- package/src/components/Chart/Chart.stories.tsx +4 -9
- package/src/components/Chart/Chart.tsx +40 -44
- package/src/components/Chart/ChartRenderer.tsx +39 -12
- package/src/components/Chart/ChartToolbar.tsx +21 -5
- package/src/components/Chart/DonutChart.tsx +1 -1
- package/src/components/Chart/FunnelChart.tsx +4 -1
- package/src/components/Chart/GaugeChart.tsx +3 -1
- package/src/components/Chart/HeatmapChart.tsx +50 -37
- package/src/components/Chart/LineChart.tsx +3 -2
- package/src/components/Chart/MultiAxisChart.tsx +24 -16
- package/src/components/Chart/RadarChart.tsx +19 -17
- package/src/components/Chart/ScatterChart.tsx +29 -21
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +6 -2
- package/src/components/ColorModeToggle/ColorModeToggle.tsx +15 -3
- package/src/components/Countdown/Countdown.stories.tsx +7 -7
- package/src/components/DataTable/DataTable.stories.tsx +43 -38
- package/src/components/DataTable/DataTable.test.tsx +26 -148
- package/src/components/DataTable/DataTable.tsx +485 -456
- package/src/components/DatePicker/DatePicker.stories.tsx +32 -47
- package/src/components/DatePicker/DatePicker.tsx +31 -26
- package/src/components/Dropdown/Dropdown.stories.tsx +2 -5
- package/src/components/Dropdown/Dropdown.tsx +425 -298
- package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
- package/src/components/EdgePanel/EdgePanel.stories.tsx +6 -19
- package/src/components/EdgePanel/EdgePanel.tsx +163 -113
- package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
- package/src/components/Footer/Footer.stories.tsx +21 -16
- package/src/components/Footer/Footer.tsx +130 -128
- package/src/components/Footer/FooterLink.tsx +2 -2
- package/src/components/Form/Checkbox.test.tsx +49 -49
- package/src/components/Form/Checkbox.tsx +108 -100
- package/src/components/Form/Form.stories.tsx +2 -10
- package/src/components/Form/Input.stories.tsx +22 -39
- package/src/components/Form/Input.test.tsx +38 -44
- package/src/components/Form/Radio.stories.tsx +6 -12
- package/src/components/Form/Radio.tsx +68 -66
- package/src/components/Form/Select.stories.tsx +23 -0
- package/src/components/Form/Select.test.tsx +99 -0
- package/src/components/Form/Select.tsx +239 -186
- package/src/components/Form/SelectOption.tsx +88 -0
- package/src/components/Form/Textarea.test.tsx +27 -32
- package/src/components/Hero/Hero.stories.tsx +93 -23
- package/src/components/Hero/Hero.test.tsx +142 -0
- package/src/components/Hero/Hero.tsx +343 -58
- package/src/components/Icon/index.ts +7 -1
- package/src/components/List/List.test.tsx +62 -0
- package/src/components/List/List.tsx +32 -25
- package/src/components/List/ListItem.tsx +20 -0
- package/src/components/Modal/Modal.stories.tsx +67 -2
- package/src/components/Modal/Modal.tsx +208 -125
- package/src/components/Modal/ModalCompound.test.tsx +94 -0
- package/src/components/Navigation/Menu/MegaMenu.tsx +70 -70
- package/src/components/Navigation/Nav/NavDropdown.tsx +1 -5
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +128 -28
- package/src/components/Navigation/SideMenu/SideMenu.tsx +5 -7
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +4 -5
- package/src/components/Pagination/Pagination.stories.tsx +7 -4
- package/src/components/Pagination/Pagination.tsx +199 -202
- package/src/components/PhotoViewer/PhotoViewer.tsx +4 -1
- package/src/components/Popover/Popover.stories.tsx +99 -192
- package/src/components/Popover/Popover.tsx +41 -37
- package/src/components/Progress/Progress.stories.tsx +35 -44
- package/src/components/River/River.stories.tsx +2 -1
- package/src/components/SectionIntro/SectionIntro.stories.tsx +71 -71
- package/src/components/Slider/Slider.stories.tsx +12 -4
- package/src/components/Spinner/Spinner.stories.tsx +3 -1
- package/src/components/Spinner/Spinner.test.tsx +23 -23
- package/src/components/Spinner/Spinner.tsx +43 -46
- package/src/components/Steps/Steps.stories.tsx +8 -6
- package/src/components/Steps/Steps.tsx +124 -21
- package/src/components/Steps/StepsCompound.test.tsx +81 -0
- package/src/components/Tabs/Tabs.stories.tsx +12 -9
- package/src/components/Tabs/Tabs.tsx +230 -75
- package/src/components/Tabs/TabsCompound.test.tsx +64 -0
- package/src/components/Toggle/Toggle.stories.tsx +27 -13
- package/src/components/Toggle/Toggle.test.tsx +65 -70
- package/src/components/Toggle/Toggle.tsx +4 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +24 -20
- package/src/components/Tooltip/Tooltip.tsx +104 -106
- package/src/components/Upload/Upload.stories.tsx +129 -127
- package/src/components/Upload/Upload.tsx +287 -283
- package/src/components/VideoPlayer/VideoPlayer.tsx +6 -1
- package/src/components/index.ts +13 -2
- package/src/layouts/Grid/Grid.stories.tsx +9 -3
- package/src/layouts/MasonryGrid/MasonryGrid.tsx +5 -1
- package/src/lib/__tests__/theme-tools.test.ts +32 -6
- package/src/lib/composables/index.ts +0 -4
- package/src/lib/composables/shared-mouse-tracker.ts +13 -14
- package/src/lib/composables/useAtomixGlass.ts +102 -60
- package/src/lib/composables/useChartExport.ts +1 -1
- package/src/lib/composables/useDataTable.ts +29 -17
- package/src/lib/composables/useHero.ts +58 -14
- package/src/lib/composables/useHeroBackgroundSlider.ts +2 -9
- package/src/lib/composables/useInput.ts +10 -8
- package/src/lib/composables/useSideMenu.ts +6 -5
- package/src/lib/composables/useTooltip.ts +1 -2
- package/src/lib/composables/useVideoPlayer.ts +44 -35
- package/src/lib/config/index.ts +154 -154
- package/src/lib/constants/cssVariables.ts +29 -29
- package/src/lib/hooks/__tests__/useComponentCustomization.test.ts +2 -6
- package/src/lib/hooks/index.ts +1 -1
- package/src/lib/hooks/useComponentCustomization.ts +11 -17
- package/src/lib/hooks/usePerformanceMonitor.ts +6 -7
- package/src/lib/patterns/__tests__/slots.test.ts +1 -1
- package/src/lib/patterns/index.ts +1 -1
- package/src/lib/patterns/slots.tsx +8 -13
- package/src/lib/storybook/InteractiveDemo.tsx +13 -18
- package/src/lib/storybook/PreviewContainer.tsx +1 -1
- package/src/lib/storybook/VariantsGrid.tsx +3 -7
- package/src/lib/storybook/index.ts +1 -1
- package/src/lib/theme/adapters/cssVariableMapper.ts +47 -74
- package/src/lib/theme/adapters/index.ts +3 -9
- package/src/lib/theme/adapters/themeAdapter.ts +41 -26
- package/src/lib/theme/config/index.ts +1 -1
- package/src/lib/theme/config/types.ts +2 -2
- package/src/lib/theme/config/validator.ts +10 -5
- package/src/lib/theme/constants/constants.ts +2 -2
- package/src/lib/theme/constants/index.ts +1 -2
- package/src/lib/theme/core/__tests__/createTheme.test.ts +20 -22
- package/src/lib/theme/core/composeTheme.ts +32 -26
- package/src/lib/theme/core/createTheme.ts +1 -1
- package/src/lib/theme/core/createThemeObject.ts +308 -301
- package/src/lib/theme/core/index.ts +3 -3
- package/src/lib/theme/devtools/CLI.ts +105 -111
- package/src/lib/theme/devtools/Comparator.tsx +50 -32
- package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +50 -48
- package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +257 -63
- package/src/lib/theme/devtools/Inspector.tsx +75 -60
- package/src/lib/theme/devtools/LiveEditor.tsx +97 -76
- package/src/lib/theme/devtools/Preview.tsx +150 -106
- package/src/lib/theme/devtools/ThemeValidator.ts +29 -21
- package/src/lib/theme/devtools/index.ts +3 -9
- package/src/lib/theme/devtools/useHistory.ts +23 -21
- package/src/lib/theme/errors/errors.ts +12 -11
- package/src/lib/theme/errors/index.ts +2 -7
- package/src/lib/theme/generators/generateCSS.ts +9 -13
- package/src/lib/theme/generators/generateCSSNested.ts +1 -6
- package/src/lib/theme/generators/generateCSSVariables.ts +673 -630
- package/src/lib/theme/generators/index.ts +1 -4
- package/src/lib/theme/i18n/index.ts +1 -1
- package/src/lib/theme/i18n/rtl.ts +13 -13
- package/src/lib/theme/index.ts +7 -16
- package/src/lib/theme/runtime/ThemeApplicator.ts +4 -4
- package/src/lib/theme/runtime/ThemeContext.tsx +1 -1
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +19 -23
- package/src/lib/theme/runtime/ThemeProvider.tsx +230 -239
- package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +1 -1
- package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +24 -29
- package/src/lib/theme/runtime/index.ts +2 -5
- package/src/lib/theme/runtime/useTheme.ts +18 -18
- package/src/lib/theme/runtime/useThemeTokens.ts +22 -22
- package/src/lib/theme/test/testTheme.ts +15 -16
- package/src/lib/theme/tokens/index.ts +2 -7
- package/src/lib/theme/tokens/tokens.ts +25 -24
- package/src/lib/theme/types.ts +428 -411
- package/src/lib/theme/utils/__tests__/themeValidation.test.ts +3 -3
- package/src/lib/theme/utils/componentTheming.ts +18 -18
- package/src/lib/theme/utils/domUtils.ts +277 -289
- package/src/lib/theme/utils/index.ts +1 -2
- package/src/lib/theme/utils/injectCSS.ts +10 -14
- package/src/lib/theme/utils/naming.ts +20 -16
- package/src/lib/theme/utils/themeHelpers.ts +10 -12
- package/src/lib/theme/utils/themeUtils.ts +85 -86
- package/src/lib/theme/utils/themeValidation.ts +82 -33
- package/src/lib/theme-tools.ts +8 -6
- package/src/lib/types/components.ts +180 -73
- package/src/lib/types/partProps.ts +1 -1
- package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
- package/src/lib/utils/__tests__/csv.test.ts +1 -1
- package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
- package/src/lib/utils/componentUtils.ts +8 -12
- package/src/lib/utils/csv.ts +3 -1
- package/src/lib/utils/dataTableExport.ts +1 -5
- package/src/lib/utils/fontPreloader.ts +10 -19
- package/src/lib/utils/icons.ts +4 -1
- package/src/lib/utils/index.ts +2 -6
- package/src/lib/utils/memoryMonitor.ts +10 -8
- package/src/lib/utils/themeNaming.ts +3 -3
- package/src/styles/01-settings/_index.scss +0 -1
- package/src/styles/01-settings/_settings.colors.scss +8 -8
- package/src/styles/01-settings/_settings.design-tokens.scss +61 -50
- package/src/styles/01-settings/_settings.navbar.scss +1 -1
- package/src/styles/01-settings/_settings.spacing.scss +3 -4
- package/src/styles/01-settings/_settings.tooltip.scss +1 -1
- package/src/styles/01-settings/_settings.typography.scss +1 -1
- package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
- package/src/styles/02-tools/_tools.button.scss +51 -21
- package/src/styles/02-tools/_tools.utility-api.scss +36 -24
- package/src/styles/03-generic/_generic.root.scss +4 -3
- package/src/styles/06-components/_components.atomix-glass.scss +13 -9
- package/src/styles/06-components/_components.button.scss +16 -4
- package/src/styles/06-components/_components.callout.scss +27 -21
- package/src/styles/06-components/_components.card.scss +5 -14
- package/src/styles/06-components/_components.chart.scss +22 -19
- package/src/styles/06-components/_components.checkbox.scss +3 -1
- package/src/styles/06-components/_components.color-mode-toggle.scss +3 -1
- package/src/styles/06-components/_components.edge-panel.scss +9 -2
- package/src/styles/06-components/_components.footer.scss +1 -1
- package/src/styles/06-components/_components.side-menu.scss +5 -5
- package/src/styles/06-components/_components.toggle.scss +18 -0
- package/src/styles/06-components/_index.scss +1 -1
- package/src/styles/06-components/old.chart.styles.scss +0 -2
- package/src/styles/99-utilities/_utilities.border.scss +69 -27
- package/src/styles/99-utilities/_utilities.display.scss +1 -1
- package/src/styles/99-utilities/_utilities.opacity.scss +10 -0
- package/src/styles/99-utilities/_utilities.position.scss +16 -9
- package/src/styles/99-utilities/_utilities.scss +1 -1
- package/src/styles/99-utilities/_utilities.sizes.scss +47 -18
- package/src/styles/99-utilities/_utilities.spacing.scss +118 -66
- package/src/styles/99-utilities/_utilities.text-gradient.scss +30 -30
- package/src/styles/99-utilities/_utilities.text.scss +67 -47
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
import React, { memo } from 'react';
|
|
2
2
|
import { ListProps } from '../../lib/types/components';
|
|
3
3
|
import { LIST } from '../../lib/constants/components';
|
|
4
|
+
import { ListItem } from './ListItem';
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
children,
|
|
7
|
-
variant = 'default',
|
|
8
|
-
className = '',
|
|
9
|
-
style,
|
|
10
|
-
...props
|
|
11
|
-
}) => {
|
|
12
|
-
// Generate CSS classes
|
|
13
|
-
const listClasses = [LIST.BASE_CLASS, variant !== 'default' && `c-list--${variant}`, className]
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
.join(' ');
|
|
6
|
+
export type { ListProps };
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
export type ListComponent = React.FC<ListProps> & {
|
|
9
|
+
Item: typeof ListItem;
|
|
10
|
+
};
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return <li className="c-list__item">{child}</li>;
|
|
27
|
-
})}
|
|
28
|
-
</ListElement>
|
|
29
|
-
);
|
|
30
|
-
});
|
|
12
|
+
export const List: ListComponent = memo(
|
|
13
|
+
({ children, variant = 'default', className = '', style, ...props }: ListProps) => {
|
|
14
|
+
// Generate CSS classes
|
|
15
|
+
const listClasses = [LIST.BASE_CLASS, variant !== 'default' && `c-list--${variant}`, className]
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.join(' ');
|
|
31
18
|
|
|
32
|
-
|
|
19
|
+
// Determine the HTML element based on variant
|
|
20
|
+
const ListElement = ['number', 'text'].includes(variant) ? 'ol' : 'ul';
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ListElement className={listClasses} style={style} {...props}>
|
|
24
|
+
{React.Children.map(children, child => {
|
|
25
|
+
if (React.isValidElement(child)) {
|
|
26
|
+
// Check if child is a ListItem
|
|
27
|
+
if (child.type === ListItem) {
|
|
28
|
+
return child;
|
|
29
|
+
}
|
|
30
|
+
// Legacy behavior: wrap in li
|
|
31
|
+
return <li className="c-list__item">{child}</li>;
|
|
32
|
+
}
|
|
33
|
+
return <li className="c-list__item">{child}</li>;
|
|
34
|
+
})}
|
|
35
|
+
</ListElement>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
) as unknown as ListComponent;
|
|
33
39
|
|
|
34
40
|
List.displayName = 'List';
|
|
41
|
+
List.Item = ListItem;
|
|
35
42
|
|
|
36
43
|
export default List;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { LIST } from '../../lib/constants/components';
|
|
3
|
+
|
|
4
|
+
export interface ListItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
|
|
9
|
+
({ children, className = '', ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<li ref={ref} className={`${LIST.ITEM_CLASS} ${className}`.trim()} {...props}>
|
|
12
|
+
{children}
|
|
13
|
+
</li>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
ListItem.displayName = 'ListItem';
|
|
19
|
+
|
|
20
|
+
export default ListItem;
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
import { fn } from '@storybook/test';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import type { AtomixGlassProps } from '../../lib/types/components';
|
|
5
|
-
import Modal from './Modal';
|
|
5
|
+
import { Modal } from './Modal';
|
|
6
6
|
|
|
7
7
|
// Helper type for glass props in stories (without children requirement)
|
|
8
8
|
type GlassProps = boolean | Omit<AtomixGlassProps, 'children'>;
|
|
@@ -31,6 +31,7 @@ Modal displays content in a focused overlay dialog. It provides a way to present
|
|
|
31
31
|
- Header and footer sections
|
|
32
32
|
- Accessible design
|
|
33
33
|
- Responsive behavior
|
|
34
|
+
- **Compound Component Pattern** (new)
|
|
34
35
|
|
|
35
36
|
## Accessibility
|
|
36
37
|
|
|
@@ -53,6 +54,20 @@ Modal displays content in a focused overlay dialog. It provides a way to present
|
|
|
53
54
|
</Modal>
|
|
54
55
|
\`\`\`
|
|
55
56
|
|
|
57
|
+
### Compound Component Usage
|
|
58
|
+
|
|
59
|
+
\`\`\`tsx
|
|
60
|
+
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
61
|
+
<Modal.Header closeButton title="Custom Header" />
|
|
62
|
+
<Modal.Body>
|
|
63
|
+
<p>Flexible body content</p>
|
|
64
|
+
</Modal.Body>
|
|
65
|
+
<Modal.Footer>
|
|
66
|
+
<button>Action</button>
|
|
67
|
+
</Modal.Footer>
|
|
68
|
+
</Modal>
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
56
71
|
### With Glass Effect
|
|
57
72
|
|
|
58
73
|
\`\`\`tsx
|
|
@@ -284,6 +299,55 @@ export const WithGlassEffect: Story = {
|
|
|
284
299
|
},
|
|
285
300
|
};
|
|
286
301
|
|
|
302
|
+
export const CompoundUsage: Story = {
|
|
303
|
+
render: args => {
|
|
304
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<>
|
|
308
|
+
<div
|
|
309
|
+
className="c-btn c-btn--primary"
|
|
310
|
+
onClick={() => setIsOpen(true)}
|
|
311
|
+
style={{ cursor: 'pointer', padding: '8px 16px', display: 'inline-block' }}
|
|
312
|
+
>
|
|
313
|
+
Open Compound Modal
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<Modal
|
|
317
|
+
{...args}
|
|
318
|
+
isOpen={isOpen}
|
|
319
|
+
onOpenChange={setIsOpen}
|
|
320
|
+
>
|
|
321
|
+
<Modal.Header
|
|
322
|
+
title="Compound Component Pattern"
|
|
323
|
+
subtitle="Fully customizable header"
|
|
324
|
+
closeButton
|
|
325
|
+
/>
|
|
326
|
+
<Modal.Body>
|
|
327
|
+
<p>
|
|
328
|
+
This modal uses the Compound Component pattern (Modal.Header, Modal.Body, Modal.Footer).
|
|
329
|
+
This allows for greater flexibility in content arrangement.
|
|
330
|
+
</p>
|
|
331
|
+
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
332
|
+
Custom content structure inside Body
|
|
333
|
+
</div>
|
|
334
|
+
</Modal.Body>
|
|
335
|
+
<Modal.Footer>
|
|
336
|
+
<button className="c-btn c-btn--outline-secondary" onClick={() => setIsOpen(false)}>Custom Footer Button</button>
|
|
337
|
+
</Modal.Footer>
|
|
338
|
+
</Modal>
|
|
339
|
+
</>
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
parameters: {
|
|
343
|
+
docs: {
|
|
344
|
+
description: {
|
|
345
|
+
story: 'Demonstrates the Compound Component usage pattern.',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
287
351
|
/**
|
|
288
352
|
* Small size modal variant.
|
|
289
353
|
*/
|
|
@@ -649,7 +713,8 @@ export const GlassModalSizes: Story = {
|
|
|
649
713
|
The glass effect adapts to different modal sizes while maintaining its visual appeal.
|
|
650
714
|
</p>
|
|
651
715
|
<p>
|
|
652
|
-
The glass effect enhances the modal's appearance, making it visually appealing and
|
|
716
|
+
The glass effect enhances the modal's appearance, making it visually appealing and
|
|
717
|
+
easier to read.
|
|
653
718
|
</p>
|
|
654
719
|
</Modal>
|
|
655
720
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState, useCallback, memo } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback, memo, forwardRef, ReactNode } from 'react';
|
|
2
2
|
import { ModalProps } from '../../lib/types/components';
|
|
3
3
|
import { MODAL } from '../../lib/constants/components';
|
|
4
4
|
import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
|
|
@@ -73,149 +73,232 @@ function useModal({
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Modal Subcomponents
|
|
77
|
+
|
|
78
|
+
export interface ModalHeaderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
79
|
+
title?: ReactNode;
|
|
80
|
+
subtitle?: ReactNode;
|
|
81
|
+
closeButton?: boolean;
|
|
82
|
+
onClose?: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
|
|
86
|
+
({ title, subtitle, closeButton, onClose, children, className = '', ...props }, ref) => {
|
|
87
|
+
return (
|
|
88
|
+
<div ref={ref} className={`c-modal__header ${className}`.trim()} {...props}>
|
|
89
|
+
<div className="c-modal__header-content">
|
|
90
|
+
{title && <h3 className="c-modal__title">{title}</h3>}
|
|
91
|
+
{subtitle && <p className="c-modal__sub">{subtitle}</p>}
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
{closeButton && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="c-modal__close c-btn js-modal-close"
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
aria-label="Close modal"
|
|
100
|
+
>
|
|
101
|
+
<svg
|
|
102
|
+
width="20"
|
|
103
|
+
height="20"
|
|
104
|
+
viewBox="0 0 20 20"
|
|
105
|
+
fill="none"
|
|
106
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
d="M16.0672 15.1828C16.1253 15.2409 16.1713 15.3098 16.2028 15.3857C16.2342 15.4615 16.2504 15.5429 16.2504 15.625C16.2504 15.7071 16.2342 15.7884 16.2028 15.8643C16.1713 15.9402 16.1253 16.0091 16.0672 16.0672C16.0091 16.1252 15.9402 16.1713 15.8643 16.2027C15.7885 16.2342 15.7071 16.2503 15.625 16.2503C15.5429 16.2503 15.4616 16.2342 15.3857 16.2027C15.3098 16.1713 15.2409 16.1252 15.1828 16.0672L10 10.8836L4.8172 16.0672C4.69992 16.1844 4.54086 16.2503 4.37501 16.2503C4.20916 16.2503 4.0501 16.1844 3.93282 16.0672C3.81555 15.9499 3.74966 15.7908 3.74966 15.625C3.74966 15.4591 3.81555 15.3001 3.93282 15.1828L9.11642 9.99998L3.93282 4.81717C3.81555 4.69989 3.74966 4.54083 3.74966 4.37498C3.74966 4.20913 3.81555 4.05007 3.93282 3.93279C4.0501 3.81552 4.20916 3.74963 4.37501 3.74963C4.54086 3.74963 4.69992 3.81552 4.8172 3.93279L10 9.11639L15.1828 3.93279C15.3001 3.81552 15.4592 3.74963 15.625 3.74963C15.7909 3.74963 15.9499 3.81552 16.0672 3.93279C16.1845 4.05007 16.2504 4.20913 16.2504 4.37498C16.2504 4.54083 16.1845 4.69989 16.0672 4.81717L10.8836 9.99998L16.0672 15.1828Z"
|
|
110
|
+
fill="#141414"
|
|
111
|
+
/>
|
|
112
|
+
</svg>
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
ModalHeader.displayName = 'ModalHeader';
|
|
120
|
+
|
|
121
|
+
export const ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
122
|
+
({ children, className = '', ...props }, ref) => {
|
|
123
|
+
return (
|
|
124
|
+
<div ref={ref} className={`c-modal__body ${className}`.trim()} {...props}>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
ModalBody.displayName = 'ModalBody';
|
|
131
|
+
|
|
132
|
+
export const ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
133
|
+
({ children, className = '', ...props }, ref) => {
|
|
134
|
+
return (
|
|
135
|
+
<div ref={ref} className={`c-modal__footer ${className}`.trim()} {...props}>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
ModalFooter.displayName = 'ModalFooter';
|
|
142
|
+
|
|
76
143
|
/**
|
|
77
144
|
* Modal component for displaying overlay content
|
|
78
145
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
keyboard = true,
|
|
90
|
-
className = '',
|
|
91
|
-
style,
|
|
92
|
-
closeButton = true,
|
|
93
|
-
footer,
|
|
94
|
-
glass,
|
|
95
|
-
...props
|
|
96
|
-
}) => {
|
|
97
|
-
const modalRef = useRef<HTMLDivElement>(null);
|
|
98
|
-
const dialogRef = useRef<HTMLDivElement>(null);
|
|
99
|
-
const backdropRef = useRef<HTMLDivElement>(null);
|
|
100
|
-
|
|
101
|
-
const {
|
|
102
|
-
isOpen: isOpenState,
|
|
103
|
-
open,
|
|
104
|
-
close,
|
|
105
|
-
} = useModal({
|
|
106
|
-
isOpen,
|
|
146
|
+
type ModalComponent = React.FC<ModalProps> & {
|
|
147
|
+
Header: typeof ModalHeader;
|
|
148
|
+
Body: typeof ModalBody;
|
|
149
|
+
Footer: typeof ModalFooter;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const ModalImpl = memo(
|
|
153
|
+
({
|
|
154
|
+
children,
|
|
155
|
+
isOpen = false,
|
|
107
156
|
onOpenChange,
|
|
108
157
|
onClose,
|
|
109
158
|
onOpen,
|
|
110
|
-
|
|
159
|
+
title,
|
|
160
|
+
subtitle,
|
|
161
|
+
size = 'md',
|
|
162
|
+
backdrop = true,
|
|
163
|
+
keyboard = true,
|
|
164
|
+
className = '',
|
|
165
|
+
style,
|
|
166
|
+
closeButton = true,
|
|
167
|
+
footer,
|
|
168
|
+
glass,
|
|
169
|
+
...props
|
|
170
|
+
}: ModalProps) => {
|
|
171
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
172
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
173
|
+
const backdropRef = useRef<HTMLDivElement>(null);
|
|
111
174
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
175
|
+
const {
|
|
176
|
+
isOpen: isOpenState,
|
|
177
|
+
open,
|
|
178
|
+
close,
|
|
179
|
+
} = useModal({
|
|
180
|
+
isOpen,
|
|
181
|
+
onOpenChange,
|
|
182
|
+
onClose,
|
|
183
|
+
onOpen,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Handle keyboard events for Escape key
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!keyboard) return undefined;
|
|
189
|
+
|
|
190
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
191
|
+
if (event.key === 'Escape' && isOpenState) {
|
|
192
|
+
close();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
115
195
|
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
document.addEventListener('keydown', handleKeydown);
|
|
197
|
+
return () => {
|
|
198
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
199
|
+
};
|
|
200
|
+
}, [isOpenState, close, keyboard]);
|
|
201
|
+
|
|
202
|
+
// Handle backdrop click
|
|
203
|
+
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
204
|
+
if (backdrop && event.target === event.currentTarget) {
|
|
118
205
|
close();
|
|
119
206
|
}
|
|
120
207
|
};
|
|
121
208
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
209
|
+
// Assemble classes
|
|
210
|
+
const modalClasses = [
|
|
211
|
+
'c-modal',
|
|
212
|
+
isOpenState ? MODAL.CLASSES.IS_OPEN : '',
|
|
213
|
+
size ? `c-modal--${size}` : '',
|
|
214
|
+
glass ? 'c-modal--glass' : '',
|
|
215
|
+
className,
|
|
216
|
+
]
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.join(' ');
|
|
127
219
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
220
|
+
// Check for compound components usage
|
|
221
|
+
const hasCompoundComponents = React.Children.toArray(children).some((child) =>
|
|
222
|
+
React.isValidElement(child) &&
|
|
223
|
+
['ModalHeader', 'ModalBody', 'ModalFooter'].includes((child.type as any).displayName)
|
|
224
|
+
);
|
|
134
225
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{title &&
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
fill="none"
|
|
166
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
167
|
-
>
|
|
168
|
-
<path
|
|
169
|
-
d="M16.0672 15.1828C16.1253 15.2409 16.1713 15.3098 16.2028 15.3857C16.2342 15.4615 16.2504 15.5429 16.2504 15.625C16.2504 15.7071 16.2342 15.7884 16.2028 15.8643C16.1713 15.9402 16.1253 16.0091 16.0672 16.0672C16.0091 16.1252 15.9402 16.1713 15.8643 16.2027C15.7885 16.2342 15.7071 16.2503 15.625 16.2503C15.5429 16.2503 15.4616 16.2342 15.3857 16.2027C15.3098 16.1713 15.2409 16.1252 15.1828 16.0672L10 10.8836L4.8172 16.0672C4.69992 16.1844 4.54086 16.2503 4.37501 16.2503C4.20916 16.2503 4.0501 16.1844 3.93282 16.0672C3.81555 15.9499 3.74966 15.7908 3.74966 15.625C3.74966 15.4591 3.81555 15.3001 3.93282 15.1828L9.11642 9.99998L3.93282 4.81717C3.81555 4.69989 3.74966 4.54083 3.74966 4.37498C3.74966 4.20913 3.81555 4.05007 3.93282 3.93279C4.0501 3.81552 4.20916 3.74963 4.37501 3.74963C4.54086 3.74963 4.69992 3.81552 4.8172 3.93279L10 9.11639L15.1828 3.93279C15.3001 3.81552 15.4592 3.74963 15.625 3.74963C15.7909 3.74963 15.9499 3.81552 16.0672 3.93279C16.1845 4.05007 16.2504 4.20913 16.2504 4.37498C16.2504 4.54083 16.1845 4.69989 16.0672 4.81717L10.8836 9.99998L16.0672 15.1828Z"
|
|
170
|
-
fill="#141414"
|
|
171
|
-
/>
|
|
172
|
-
</svg>
|
|
173
|
-
</button>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
226
|
+
const modalContent = (
|
|
227
|
+
<div className="c-modal__content">
|
|
228
|
+
{hasCompoundComponents ? (
|
|
229
|
+
React.Children.map(children, child => {
|
|
230
|
+
if (
|
|
231
|
+
React.isValidElement(child) &&
|
|
232
|
+
(child.type as any).displayName === 'ModalHeader'
|
|
233
|
+
) {
|
|
234
|
+
return React.cloneElement(child, {
|
|
235
|
+
onClose: (child.props as any).onClose || close,
|
|
236
|
+
} as any);
|
|
237
|
+
}
|
|
238
|
+
return child;
|
|
239
|
+
})
|
|
240
|
+
) : (
|
|
241
|
+
<>
|
|
242
|
+
{(title || closeButton) && (
|
|
243
|
+
<ModalHeader
|
|
244
|
+
title={title}
|
|
245
|
+
subtitle={subtitle}
|
|
246
|
+
closeButton={closeButton}
|
|
247
|
+
onClose={close}
|
|
248
|
+
/>
|
|
249
|
+
)}
|
|
250
|
+
<ModalBody>{children}</ModalBody>
|
|
251
|
+
{footer && <ModalFooter>{footer}</ModalFooter>}
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
177
256
|
|
|
178
|
-
|
|
257
|
+
return (
|
|
258
|
+
<div
|
|
259
|
+
ref={modalRef}
|
|
260
|
+
className={modalClasses}
|
|
261
|
+
style={{ display: isOpenState ? 'block' : 'none', ...style }}
|
|
262
|
+
role="dialog"
|
|
263
|
+
aria-modal="true"
|
|
264
|
+
aria-hidden={!isOpenState}
|
|
265
|
+
{...props}
|
|
266
|
+
>
|
|
267
|
+
<div ref={backdropRef} className="c-modal__backdrop" onClick={handleBackdropClick} />
|
|
268
|
+
<div ref={dialogRef} className="c-modal__dialog">
|
|
269
|
+
{glass
|
|
270
|
+
? // Default glass settings for modals
|
|
271
|
+
(() => {
|
|
272
|
+
const defaultGlassProps = {
|
|
273
|
+
displacementScale: document.querySelector('.c-modal---glass .c-modal__content')
|
|
274
|
+
?.clientHeight,
|
|
275
|
+
blurAmount: 2.2,
|
|
276
|
+
elasticity: 0,
|
|
277
|
+
mode: 'shader' as const,
|
|
278
|
+
shaderMode: 'premiumGlass',
|
|
279
|
+
};
|
|
179
280
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
281
|
+
const glassProps =
|
|
282
|
+
glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
|
|
183
283
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
style={{ display: isOpenState ? 'block' : 'none', ...style }}
|
|
189
|
-
role="dialog"
|
|
190
|
-
aria-modal="true"
|
|
191
|
-
aria-hidden={!isOpenState}
|
|
192
|
-
{...props}
|
|
193
|
-
>
|
|
194
|
-
<div ref={backdropRef} className="c-modal__backdrop" onClick={handleBackdropClick} />
|
|
195
|
-
<div ref={dialogRef} className="c-modal__dialog">
|
|
196
|
-
{glass
|
|
197
|
-
? // Default glass settings for modals
|
|
198
|
-
(() => {
|
|
199
|
-
const defaultGlassProps = {
|
|
200
|
-
displacementScale: document.querySelector('.c-modal---glass .c-modal__content')?.clientHeight,
|
|
201
|
-
blurAmount: 2.2,
|
|
202
|
-
elasticity: 0,
|
|
203
|
-
mode: 'shader' as const,
|
|
204
|
-
shaderMode: 'premiumGlass'
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const glassProps =
|
|
208
|
-
glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
|
|
209
|
-
|
|
210
|
-
return <AtomixGlass {...glassProps}>{modalContent}</AtomixGlass>;
|
|
211
|
-
})()
|
|
212
|
-
: modalContent}
|
|
284
|
+
return <AtomixGlass {...glassProps}>{modalContent}</AtomixGlass>;
|
|
285
|
+
})()
|
|
286
|
+
: modalContent}
|
|
287
|
+
</div>
|
|
213
288
|
</div>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
ModalImpl.displayName = 'Modal';
|
|
294
|
+
|
|
295
|
+
// Attach subcomponents
|
|
296
|
+
const ModalWithSubcomponents = ModalImpl as unknown as ModalComponent;
|
|
297
|
+
ModalWithSubcomponents.Header = ModalHeader;
|
|
298
|
+
ModalWithSubcomponents.Body = ModalBody;
|
|
299
|
+
ModalWithSubcomponents.Footer = ModalFooter;
|
|
217
300
|
|
|
218
|
-
Modal
|
|
301
|
+
export const Modal = ModalWithSubcomponents;
|
|
219
302
|
|
|
220
303
|
export type { ModalProps };
|
|
221
304
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { Modal } from './Modal';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
describe('Modal Component', () => {
|
|
7
|
+
it('renders correctly with legacy props', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Modal isOpen={true} title="Legacy Title" footer="Legacy Footer">
|
|
10
|
+
Legacy Content
|
|
11
|
+
</Modal>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByText('Legacy Title')).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByText('Legacy Content')).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText('Legacy Footer')).toBeInTheDocument();
|
|
17
|
+
|
|
18
|
+
// Check structure classes
|
|
19
|
+
expect(document.querySelector('.c-modal__header')).toBeInTheDocument();
|
|
20
|
+
expect(document.querySelector('.c-modal__body')).toBeInTheDocument();
|
|
21
|
+
expect(document.querySelector('.c-modal__footer')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders correctly with compound components', () => {
|
|
25
|
+
render(
|
|
26
|
+
<Modal isOpen={true}>
|
|
27
|
+
<Modal.Header title="Compound Header" />
|
|
28
|
+
<Modal.Body>Compound Body</Modal.Body>
|
|
29
|
+
<Modal.Footer>Compound Footer</Modal.Footer>
|
|
30
|
+
</Modal>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('Compound Header')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Compound Body')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Compound Footer')).toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
// Verify no double wrapping
|
|
38
|
+
// If double wrapping occurred, we might see nested .c-modal__body or similar issues,
|
|
39
|
+
// or the header inside the body if logic failed.
|
|
40
|
+
|
|
41
|
+
const header = document.querySelector('.c-modal__header');
|
|
42
|
+
const body = document.querySelector('.c-modal__body');
|
|
43
|
+
const footer = document.querySelector('.c-modal__footer');
|
|
44
|
+
|
|
45
|
+
// Header should be a direct child of .c-modal__content (or close to it)
|
|
46
|
+
expect(header?.parentElement).toHaveClass('c-modal__content');
|
|
47
|
+
expect(body?.parentElement).toHaveClass('c-modal__content');
|
|
48
|
+
expect(footer?.parentElement).toHaveClass('c-modal__content');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('injects onClose into Modal.Header when used in compound mode', () => {
|
|
52
|
+
const onClose = vi.fn();
|
|
53
|
+
render(
|
|
54
|
+
<Modal isOpen={true} onClose={onClose}>
|
|
55
|
+
<Modal.Header closeButton data-testid="header" />
|
|
56
|
+
<Modal.Body>Content</Modal.Body>
|
|
57
|
+
</Modal>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const closeBtn = screen.getByLabelText('Close modal');
|
|
61
|
+
fireEvent.click(closeBtn);
|
|
62
|
+
expect(onClose).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('allows custom onClose in Modal.Header', () => {
|
|
66
|
+
const modalOnClose = vi.fn();
|
|
67
|
+
const headerOnClose = vi.fn();
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<Modal isOpen={true} onClose={modalOnClose}>
|
|
71
|
+
<Modal.Header closeButton onClose={headerOnClose} />
|
|
72
|
+
<Modal.Body>Content</Modal.Body>
|
|
73
|
+
</Modal>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const closeBtn = screen.getByLabelText('Close modal');
|
|
77
|
+
fireEvent.click(closeBtn);
|
|
78
|
+
|
|
79
|
+
expect(headerOnClose).toHaveBeenCalled();
|
|
80
|
+
expect(modalOnClose).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('prioritizes compound components over legacy props', () => {
|
|
84
|
+
render(
|
|
85
|
+
<Modal isOpen={true} title="Legacy Title">
|
|
86
|
+
<Modal.Header title="Compound Header" />
|
|
87
|
+
<Modal.Body>Compound Body</Modal.Body>
|
|
88
|
+
</Modal>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(screen.getByText('Compound Header')).toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByText('Legacy Title')).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|