@shohojdhara/atomix 0.4.0 → 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/dist/atomix.css +9231 -9337
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +2 -2
- package/dist/atomix.min.css.map +1 -1
- 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 +495 -254
- package/dist/index.esm.js +1269 -723
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1273 -723
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.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 +40 -0
- package/src/components/Accordion/Accordion.tsx +174 -56
- package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +156 -50
- package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
- package/src/components/Callout/Callout.stories.tsx +166 -1011
- package/src/components/Callout/Callout.tsx +196 -84
- package/src/components/Callout/CalloutCompound.test.tsx +72 -0
- package/src/components/Dropdown/Dropdown.tsx +133 -20
- package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
- package/src/components/EdgePanel/EdgePanel.tsx +164 -112
- package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
- package/src/components/Form/Select.stories.tsx +23 -0
- package/src/components/Form/Select.test.tsx +99 -0
- package/src/components/Form/Select.tsx +144 -93
- package/src/components/Form/SelectOption.tsx +88 -0
- package/src/components/Hero/Hero.stories.tsx +37 -0
- package/src/components/Hero/Hero.test.tsx +142 -0
- package/src/components/Hero/Hero.tsx +142 -3
- package/src/components/List/List.test.tsx +62 -0
- package/src/components/List/List.tsx +16 -5
- package/src/components/List/ListItem.tsx +20 -0
- package/src/components/Modal/Modal.stories.tsx +65 -1
- package/src/components/Modal/Modal.tsx +115 -35
- package/src/components/Modal/ModalCompound.test.tsx +94 -0
- package/src/components/Steps/Steps.tsx +124 -21
- package/src/components/Steps/StepsCompound.test.tsx +81 -0
- package/src/components/Tabs/Tabs.tsx +197 -44
- package/src/components/Tabs/TabsCompound.test.tsx +64 -0
- package/src/lib/composables/index.ts +0 -4
- package/src/lib/composables/useAtomixGlass.ts +0 -15
- package/src/lib/theme/devtools/CLI.ts +2 -10
- package/src/lib/types/components.ts +8 -2
- package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
- package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
- package/src/lib/utils/themeNaming.ts +1 -1
- package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
- package/src/styles/02-tools/_tools.utility-api.scss +6 -6
- package/src/styles/99-utilities/_utilities.text.scss +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shohojdhara/atomix",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Atomix Design System - A modern component library for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -302,4 +302,4 @@
|
|
|
302
302
|
"publishConfig": {
|
|
303
303
|
"access": "public"
|
|
304
304
|
}
|
|
305
|
-
}
|
|
305
|
+
}
|
package/scripts/atomix-cli.js
CHANGED
|
@@ -78,6 +78,15 @@ const packageJson = JSON.parse(
|
|
|
78
78
|
// CLI Configuration
|
|
79
79
|
const DEBUG = process.env.ATOMIX_DEBUG === 'true' || process.argv.includes('--debug');
|
|
80
80
|
|
|
81
|
+
const SENSITIVE_KEYS = /password|secret|token|api[-_]?key|access[-_]?key|auth[-_]?token|authorization|credential/i;
|
|
82
|
+
|
|
83
|
+
function sensitiveDataReplacer(key, value) {
|
|
84
|
+
if (key && SENSITIVE_KEYS.test(key)) {
|
|
85
|
+
return '***REDACTED***';
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
/**
|
|
82
91
|
* Debug logger
|
|
83
92
|
*/
|
|
@@ -85,7 +94,7 @@ function debug(message, data = null) {
|
|
|
85
94
|
if (DEBUG) {
|
|
86
95
|
console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
87
96
|
if (data) {
|
|
88
|
-
console.log(chalk.gray(JSON.stringify(data,
|
|
97
|
+
console.log(chalk.gray(JSON.stringify(data, sensitiveDataReplacer, 2)));
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -115,12 +115,16 @@ describe('CLI Utils', () => {
|
|
|
115
115
|
'command | pipe',
|
|
116
116
|
'`substitution`',
|
|
117
117
|
'$(command)',
|
|
118
|
-
'input > /dev/null'
|
|
118
|
+
'input > /dev/null',
|
|
119
|
+
'"quoted"',
|
|
120
|
+
"'singlequoted'",
|
|
121
|
+
'" $(whoami) "',
|
|
122
|
+
"' ; rm -rf / '"
|
|
119
123
|
];
|
|
120
124
|
|
|
121
125
|
dangerousInputs.forEach(input => {
|
|
122
126
|
const sanitized = sanitizeInput(input);
|
|
123
|
-
expect(sanitized).not.toMatch(/[
|
|
127
|
+
expect(sanitized).not.toMatch(/[;&|`$<>\\"']/);
|
|
124
128
|
});
|
|
125
129
|
});
|
|
126
130
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Helps migrate from other design systems and CSS frameworks
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFile, writeFile, readdir,
|
|
6
|
+
import { readFile, writeFile, readdir, lstat } from 'fs/promises';
|
|
7
7
|
import { join, extname, relative } from 'path';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import ora from 'ora';
|
|
@@ -329,7 +329,7 @@ async function getAllFiles(dir, extensions = []) {
|
|
|
329
329
|
|
|
330
330
|
for (const entry of entries) {
|
|
331
331
|
const fullPath = join(currentPath, entry);
|
|
332
|
-
const stats = await
|
|
332
|
+
const stats = await lstat(fullPath);
|
|
333
333
|
|
|
334
334
|
if (stats.isDirectory()) {
|
|
335
335
|
// Skip node_modules and hidden directories
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bridges the TypeScript theme devtools CLI (src/lib/theme/devtools/CLI.ts) with the main JavaScript CLI.
|
|
5
5
|
*
|
|
6
|
-
* @dependency
|
|
6
|
+
* @dependency tsx
|
|
7
7
|
* Theme subcommands (atomix theme validate|list|inspect|compare|export|create) run the TypeScript
|
|
8
|
-
* theme CLI via
|
|
9
|
-
* npm install --save-dev
|
|
10
|
-
* If
|
|
8
|
+
* theme CLI via tsx. Ensure tsx is installed in the project when using these commands:
|
|
9
|
+
* npm install --save-dev tsx
|
|
10
|
+
* If tsx is missing, theme subcommands will fail; run `atomix doctor` to check availability.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawn } from 'child_process';
|
|
@@ -29,13 +29,11 @@ export async function executeThemeCommand(command, args = [], options = {}) {
|
|
|
29
29
|
// Path to the theme CLI
|
|
30
30
|
const themeCliPath = join(__dirname, '../../src/lib/theme/devtools/CLI.ts');
|
|
31
31
|
|
|
32
|
-
// Use
|
|
33
|
-
const
|
|
32
|
+
// Use tsx to execute TypeScript CLI
|
|
33
|
+
const tsxPath = join(__dirname, '../../node_modules/.bin/tsx');
|
|
34
34
|
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
|
-
const child = spawn(
|
|
37
|
-
'--esm',
|
|
38
|
-
'--experimental-specifier-resolution=node',
|
|
36
|
+
const child = spawn(tsxPath, [
|
|
39
37
|
themeCliPath,
|
|
40
38
|
command,
|
|
41
39
|
...args
|
package/scripts/cli/utils.js
CHANGED
|
@@ -175,8 +175,9 @@ export function sanitizeInput(input) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Remove any shell metacharacters that could be dangerous
|
|
178
|
+
// Added single and double quotes to the blacklist to prevent shell injection
|
|
178
179
|
return input
|
|
179
|
-
.replace(/[;&|`$<>\\]/g, '')
|
|
180
|
+
.replace(/[;&|`$<>\\"']/g, '')
|
|
180
181
|
.replace(/\0/g, '') // Remove null bytes
|
|
181
182
|
.trim();
|
|
182
183
|
}
|
|
@@ -63,6 +63,7 @@ The Accordion component provides an expandable/collapsible container for content
|
|
|
63
63
|
- Glass morphism effect support
|
|
64
64
|
- Keyboard navigation support
|
|
65
65
|
- Disabled state handling
|
|
66
|
+
- **Compound Component Pattern** (new)
|
|
66
67
|
|
|
67
68
|
## Accessibility
|
|
68
69
|
|
|
@@ -81,6 +82,19 @@ The Accordion component provides an expandable/collapsible container for content
|
|
|
81
82
|
</Accordion>
|
|
82
83
|
\`\`\`
|
|
83
84
|
|
|
85
|
+
### Compound Component Usage
|
|
86
|
+
|
|
87
|
+
\`\`\`tsx
|
|
88
|
+
<Accordion>
|
|
89
|
+
<Accordion.Header>
|
|
90
|
+
<span>Custom Header Content</span>
|
|
91
|
+
</Accordion.Header>
|
|
92
|
+
<Accordion.Body>
|
|
93
|
+
<p>Content goes here</p>
|
|
94
|
+
</Accordion.Body>
|
|
95
|
+
</Accordion>
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
84
98
|
### With Custom Icon
|
|
85
99
|
|
|
86
100
|
\`\`\`tsx
|
|
@@ -266,6 +280,32 @@ export const WithAllProps: Story = {
|
|
|
266
280
|
},
|
|
267
281
|
};
|
|
268
282
|
|
|
283
|
+
export const CompoundUsage: Story = {
|
|
284
|
+
render: args => (
|
|
285
|
+
<Accordion {...args}>
|
|
286
|
+
<Accordion.Header>
|
|
287
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
288
|
+
<strong>Custom Header Layout</strong>
|
|
289
|
+
<span style={{ fontSize: '0.8em', color: '#666' }}>(with subtitle)</span>
|
|
290
|
+
</div>
|
|
291
|
+
</Accordion.Header>
|
|
292
|
+
<Accordion.Body>
|
|
293
|
+
<p>
|
|
294
|
+
This accordion uses the Compound Component pattern (Accordion.Header and Accordion.Body).
|
|
295
|
+
This allows for complete control over the header layout and content structure.
|
|
296
|
+
</p>
|
|
297
|
+
</Accordion.Body>
|
|
298
|
+
</Accordion>
|
|
299
|
+
),
|
|
300
|
+
parameters: {
|
|
301
|
+
docs: {
|
|
302
|
+
description: {
|
|
303
|
+
story: 'Demonstrates the Compound Component usage pattern for custom header layouts.',
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
269
309
|
// ============================================================================
|
|
270
310
|
// VARIANTS & STATES STORIES
|
|
271
311
|
// ============================================================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { ReactNode, useId, memo } from 'react';
|
|
1
|
+
import React, { ReactNode, useId, memo, forwardRef } from 'react';
|
|
2
2
|
import { ACCORDION } from '../../lib/constants/components';
|
|
3
3
|
import { useAccordion } from '../../lib/composables/useAccordion';
|
|
4
4
|
import type {
|
|
@@ -9,7 +9,105 @@ import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
|
|
|
9
9
|
|
|
10
10
|
export type AccordionProps = AccordionPropsType;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Default icon
|
|
13
|
+
const DefaultIcon = () => (
|
|
14
|
+
<i className="c-accordion__icon" aria-hidden="true">
|
|
15
|
+
<svg
|
|
16
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
+
width="24"
|
|
18
|
+
height="24"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="2"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
focusable="false"
|
|
27
|
+
>
|
|
28
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
29
|
+
</svg>
|
|
30
|
+
</i>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export interface AccordionHeaderProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
|
|
34
|
+
title?: ReactNode;
|
|
35
|
+
icon?: ReactNode;
|
|
36
|
+
iconPosition?: 'left' | 'right';
|
|
37
|
+
isOpen?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
|
|
41
|
+
(
|
|
42
|
+
{ title, icon, iconPosition = 'right', isOpen, children, className = '', ...props },
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
// Determine icon to render. Explicit check for undefined to allow null/false to hide icon.
|
|
46
|
+
const iconElement = icon === undefined ? <DefaultIcon /> : icon;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
ref={ref}
|
|
51
|
+
type="button"
|
|
52
|
+
className={className} // Parent injects the class names
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{title && <span className="c-accordion__title">{title}</span>}
|
|
56
|
+
{children}
|
|
57
|
+
{iconElement}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
AccordionHeader.displayName = 'AccordionHeader';
|
|
63
|
+
|
|
64
|
+
export interface AccordionBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
65
|
+
panelRef?: React.RefObject<HTMLDivElement>;
|
|
66
|
+
contentRef?: React.RefObject<HTMLDivElement>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Helper to merge refs
|
|
70
|
+
function mergeRefs<T = any>(...refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null)[]) {
|
|
71
|
+
return (node: T) => {
|
|
72
|
+
refs.forEach((ref) => {
|
|
73
|
+
if (typeof ref === 'function') {
|
|
74
|
+
ref(node);
|
|
75
|
+
} else if (ref != null) {
|
|
76
|
+
(ref as React.MutableRefObject<T | null>).current = node;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const AccordionBody = forwardRef<HTMLDivElement, AccordionBodyProps>(
|
|
83
|
+
({ children, className = '', panelRef, contentRef, ...props }, ref) => {
|
|
84
|
+
const mergedPanelRef = React.useMemo(() => mergeRefs(ref, panelRef), [ref, panelRef]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
ref={mergedPanelRef}
|
|
89
|
+
className={className} // Parent injects class names
|
|
90
|
+
role="region"
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
<div
|
|
94
|
+
className={ACCORDION.SELECTORS.BODY.replace('.', '')}
|
|
95
|
+
ref={contentRef}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
AccordionBody.displayName = 'AccordionBody';
|
|
104
|
+
|
|
105
|
+
type AccordionComponent = React.FC<AccordionProps> & {
|
|
106
|
+
Header: typeof AccordionHeader;
|
|
107
|
+
Body: typeof AccordionBody;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const AccordionImpl = memo(
|
|
13
111
|
({
|
|
14
112
|
title,
|
|
15
113
|
children,
|
|
@@ -24,7 +122,7 @@ export const Accordion: React.FC<AccordionProps> = memo(
|
|
|
24
122
|
className = '',
|
|
25
123
|
style,
|
|
26
124
|
glass,
|
|
27
|
-
}) => {
|
|
125
|
+
}: AccordionProps) => {
|
|
28
126
|
// Generate unique IDs for accessibility
|
|
29
127
|
const instanceId = useId();
|
|
30
128
|
const buttonId = `accordion-header-${instanceId}`;
|
|
@@ -49,59 +147,75 @@ export const Accordion: React.FC<AccordionProps> = memo(
|
|
|
49
147
|
onClose,
|
|
50
148
|
});
|
|
51
149
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
viewBox="0 0 24 24"
|
|
60
|
-
fill="none"
|
|
61
|
-
stroke="currentColor"
|
|
62
|
-
strokeWidth="2"
|
|
63
|
-
strokeLinecap="round"
|
|
64
|
-
strokeLinejoin="round"
|
|
65
|
-
aria-hidden="true"
|
|
66
|
-
focusable="false"
|
|
67
|
-
>
|
|
68
|
-
<polyline points="6 9 12 15 18 9"></polyline>
|
|
69
|
-
</svg>
|
|
70
|
-
</i>
|
|
150
|
+
const headerClassNames = generateHeaderClassNames();
|
|
151
|
+
const panelClassNames = ACCORDION.SELECTORS.PANEL.replace('.', '');
|
|
152
|
+
|
|
153
|
+
// Check for compound usage
|
|
154
|
+
const hasCompoundComponents = React.Children.toArray(children).some((child) =>
|
|
155
|
+
React.isValidElement(child) &&
|
|
156
|
+
['AccordionHeader', 'AccordionBody'].includes((child.type as any).displayName)
|
|
71
157
|
);
|
|
72
158
|
|
|
73
|
-
const
|
|
159
|
+
const content = (
|
|
74
160
|
<div
|
|
75
161
|
className={generateClassNames(className) + (glass ? ' c-accordion--glass' : '')}
|
|
76
162
|
style={style}
|
|
77
163
|
>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
164
|
+
{hasCompoundComponents ? (
|
|
165
|
+
React.Children.map(children, child => {
|
|
166
|
+
if (React.isValidElement(child)) {
|
|
167
|
+
if ((child.type as any).displayName === 'AccordionHeader') {
|
|
168
|
+
return React.cloneElement(child, {
|
|
169
|
+
id: buttonId,
|
|
170
|
+
className: `${headerClassNames} ${(child.props as any).className || ''}`.trim(),
|
|
171
|
+
onClick: (e: React.MouseEvent) => {
|
|
172
|
+
toggle();
|
|
173
|
+
(child.props as any).onClick?.(e);
|
|
174
|
+
},
|
|
175
|
+
'aria-expanded': state.isOpen,
|
|
176
|
+
'aria-controls': panelId,
|
|
177
|
+
'aria-disabled': disabled,
|
|
178
|
+
disabled: disabled,
|
|
179
|
+
iconPosition: (child.props as any).iconPosition || iconPosition,
|
|
180
|
+
} as any);
|
|
181
|
+
}
|
|
182
|
+
if ((child.type as any).displayName === 'AccordionBody') {
|
|
183
|
+
return React.cloneElement(child, {
|
|
184
|
+
id: panelId,
|
|
185
|
+
className: `${panelClassNames} ${(child.props as any).className || ''}`.trim(),
|
|
186
|
+
'aria-labelledby': buttonId,
|
|
187
|
+
panelRef: panelRef,
|
|
188
|
+
contentRef: contentRef,
|
|
189
|
+
} as any);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return child;
|
|
193
|
+
})
|
|
194
|
+
) : (
|
|
195
|
+
<>
|
|
196
|
+
<AccordionHeader
|
|
197
|
+
id={buttonId}
|
|
198
|
+
className={headerClassNames}
|
|
199
|
+
onClick={toggle}
|
|
200
|
+
aria-expanded={state.isOpen}
|
|
201
|
+
aria-controls={panelId}
|
|
202
|
+
aria-disabled={disabled}
|
|
203
|
+
disabled={disabled}
|
|
204
|
+
title={title}
|
|
205
|
+
icon={icon}
|
|
206
|
+
iconPosition={iconPosition}
|
|
207
|
+
/>
|
|
208
|
+
<AccordionBody
|
|
209
|
+
id={panelId}
|
|
210
|
+
className={panelClassNames}
|
|
211
|
+
aria-labelledby={buttonId}
|
|
212
|
+
panelRef={panelRef as React.RefObject<HTMLDivElement>}
|
|
213
|
+
contentRef={contentRef as React.RefObject<HTMLDivElement>}
|
|
214
|
+
>
|
|
215
|
+
{children}
|
|
216
|
+
</AccordionBody>
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
105
219
|
</div>
|
|
106
220
|
);
|
|
107
221
|
|
|
@@ -114,15 +228,19 @@ export const Accordion: React.FC<AccordionProps> = memo(
|
|
|
114
228
|
|
|
115
229
|
const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
|
|
116
230
|
|
|
117
|
-
return <AtomixGlass {...glassProps}>{
|
|
231
|
+
return <AtomixGlass {...glassProps}>{content}</AtomixGlass>;
|
|
118
232
|
}
|
|
119
233
|
|
|
120
|
-
return
|
|
234
|
+
return content;
|
|
121
235
|
}
|
|
122
236
|
);
|
|
123
237
|
|
|
124
|
-
|
|
125
|
-
|
|
238
|
+
AccordionImpl.displayName = 'Accordion';
|
|
239
|
+
|
|
240
|
+
// Attach subcomponents
|
|
241
|
+
const AccordionWithSubcomponents = AccordionImpl as unknown as AccordionComponent;
|
|
242
|
+
AccordionWithSubcomponents.Header = AccordionHeader;
|
|
243
|
+
AccordionWithSubcomponents.Body = AccordionBody;
|
|
126
244
|
|
|
127
|
-
|
|
128
|
-
export default Accordion;
|
|
245
|
+
export const Accordion = AccordionWithSubcomponents;
|
|
246
|
+
export default Accordion as unknown as AccordionComponent;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { Accordion } from './Accordion';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
describe('Accordion Component', () => {
|
|
7
|
+
it('renders correctly with legacy props', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Accordion title="Legacy Title" defaultOpen>
|
|
10
|
+
Legacy Content
|
|
11
|
+
</Accordion>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByText('Legacy Title')).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByText('Legacy Content')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders correctly with compound components', () => {
|
|
19
|
+
render(
|
|
20
|
+
<Accordion defaultOpen>
|
|
21
|
+
<Accordion.Header>
|
|
22
|
+
<span>Compound Header</span>
|
|
23
|
+
</Accordion.Header>
|
|
24
|
+
<Accordion.Body>
|
|
25
|
+
<p>Compound Body</p>
|
|
26
|
+
</Accordion.Body>
|
|
27
|
+
</Accordion>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText('Compound Header')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText('Compound Body')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('toggles visibility in compound mode', () => {
|
|
35
|
+
render(
|
|
36
|
+
<Accordion>
|
|
37
|
+
<Accordion.Header>Header</Accordion.Header>
|
|
38
|
+
<Accordion.Body>Body</Accordion.Body>
|
|
39
|
+
</Accordion>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const button = screen.getByRole('button');
|
|
43
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
44
|
+
|
|
45
|
+
fireEvent.click(button);
|
|
46
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('injects props into compound components', () => {
|
|
50
|
+
// We want to verify that aria-controls and aria-labelledby are correctly linked
|
|
51
|
+
// even in compound mode (since the parent injects IDs)
|
|
52
|
+
render(
|
|
53
|
+
<Accordion>
|
|
54
|
+
<Accordion.Header>Header</Accordion.Header>
|
|
55
|
+
<Accordion.Body>Body</Accordion.Body>
|
|
56
|
+
</Accordion>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const button = screen.getByRole('button');
|
|
60
|
+
const region = screen.getByRole('region');
|
|
61
|
+
|
|
62
|
+
const controlsId = button.getAttribute('aria-controls');
|
|
63
|
+
const labelledById = region.getAttribute('aria-labelledby');
|
|
64
|
+
const buttonId = button.getAttribute('id');
|
|
65
|
+
const regionId = region.getAttribute('id');
|
|
66
|
+
|
|
67
|
+
expect(controlsId).toBe(regionId);
|
|
68
|
+
expect(labelledById).toBe(buttonId);
|
|
69
|
+
});
|
|
70
|
+
});
|