@shohojdhara/atomix 0.4.0 → 0.4.2
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 +0 -14
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +12 -19
- package/dist/charts.js +555 -359
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +98 -28
- package/dist/core.js +1082 -733
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +26 -21
- package/dist/forms.js +937 -350
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +14 -21
- package/dist/heavy.js +409 -256
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +518 -284
- package/dist/index.esm.js +1993 -1237
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1994 -1237
- 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 +43 -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/AtomixGlass/AtomixGlass.tsx +82 -54
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
- package/src/components/AtomixGlass/README.md +5 -5
- package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
- package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
- package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
- package/src/components/Badge/Badge.stories.tsx +1 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.tsx +185 -65
- package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
- package/src/components/Breadcrumb/index.ts +2 -2
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/Button/README.md +2 -2
- package/src/components/Callout/Callout.stories.tsx +166 -1011
- package/src/components/Callout/Callout.test.tsx +3 -3
- package/src/components/Callout/Callout.tsx +196 -84
- package/src/components/Callout/CalloutCompound.test.tsx +72 -0
- package/src/components/Callout/README.md +2 -2
- package/src/components/Chart/Chart.stories.tsx +1 -1
- package/src/components/Chart/Chart.tsx +5 -5
- package/src/components/Chart/TreemapChart.tsx +37 -29
- package/src/components/DatePicker/readme.md +3 -3
- package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +133 -20
- package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
- package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
- package/src/components/EdgePanel/EdgePanel.tsx +164 -112
- package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
- package/src/components/Form/Checkbox.stories.tsx +1 -1
- package/src/components/Form/Checkbox.tsx +1 -1
- package/src/components/Form/Input.stories.tsx +1 -1
- package/src/components/Form/Input.tsx +1 -1
- package/src/components/Form/Radio.stories.tsx +1 -1
- package/src/components/Form/Radio.tsx +1 -1
- package/src/components/Form/Select.stories.tsx +24 -1
- package/src/components/Form/Select.test.tsx +99 -0
- package/src/components/Form/Select.tsx +145 -94
- package/src/components/Form/SelectOption.tsx +88 -0
- package/src/components/Form/Textarea.stories.tsx +1 -1
- package/src/components/Form/Textarea.tsx +1 -1
- package/src/components/Hero/Hero.stories.tsx +39 -2
- package/src/components/Hero/Hero.test.tsx +142 -0
- package/src/components/Hero/Hero.tsx +143 -4
- 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/Messages/Messages.stories.tsx +1 -1
- package/src/components/Messages/Messages.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +66 -2
- package/src/components/Modal/Modal.tsx +115 -35
- package/src/components/Modal/ModalCompound.test.tsx +94 -0
- package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
- package/src/components/Navigation/Nav/Nav.tsx +1 -1
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
- package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/Popover/Popover.stories.tsx +1 -1
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/Rating/Rating.stories.tsx +1 -1
- package/src/components/Rating/Rating.test.tsx +73 -0
- package/src/components/Rating/Rating.tsx +25 -37
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Steps/Steps.stories.tsx +1 -1
- package/src/components/Steps/Steps.tsx +125 -22
- package/src/components/Steps/StepsCompound.test.tsx +81 -0
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +198 -45
- package/src/components/Tabs/TabsCompound.test.tsx +64 -0
- package/src/components/Todo/Todo.tsx +0 -1
- package/src/components/Toggle/Toggle.stories.tsx +1 -1
- package/src/components/Toggle/Toggle.tsx +1 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
- package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
- package/src/lib/composables/__tests__/useChart.test.ts +50 -0
- package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
- package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
- package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
- package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
- package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
- package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
- package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
- package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
- package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
- package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
- package/src/lib/composables/glass-styles.ts +302 -0
- package/src/lib/composables/index.ts +0 -8
- package/src/lib/composables/useAtomixGlass.ts +331 -537
- package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
- package/src/lib/composables/useBarChart.ts +1 -1
- package/src/lib/composables/useBreadcrumb.ts +6 -6
- package/src/lib/composables/useChart.ts +104 -21
- package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
- package/src/lib/composables/useSlider.ts +66 -34
- package/src/lib/theme/devtools/CLI.ts +2 -10
- package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
- package/src/lib/types/components.ts +21 -23
- package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
- package/src/lib/utils/__tests__/dom.test.ts +100 -0
- package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
- package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
- package/src/lib/utils/themeNaming.ts +1 -1
- package/src/styles/06-components/_components.accordion.scss +0 -2
- package/src/styles/06-components/_components.chart.scss +0 -1
- package/src/styles/06-components/_components.dropdown.scss +0 -1
- package/src/styles/06-components/_components.edge-panel.scss +0 -2
- package/src/styles/06-components/_components.photoviewer.scss +0 -1
- package/src/styles/06-components/_components.river.scss +0 -1
- package/src/styles/06-components/_components.slider.scss +0 -3
- package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { EdgePanel } from './EdgePanel';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
describe('EdgePanel Component', () => {
|
|
7
|
+
it('renders correctly with legacy props', () => {
|
|
8
|
+
render(
|
|
9
|
+
<EdgePanel isOpen={true} title="Legacy Title">
|
|
10
|
+
Legacy Content
|
|
11
|
+
</EdgePanel>
|
|
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
|
+
<EdgePanel isOpen={true}>
|
|
21
|
+
<EdgePanel.Header>
|
|
22
|
+
<h4>Compound Title</h4>
|
|
23
|
+
</EdgePanel.Header>
|
|
24
|
+
<EdgePanel.Body>
|
|
25
|
+
Compound Content
|
|
26
|
+
</EdgePanel.Body>
|
|
27
|
+
<EdgePanel.Footer>
|
|
28
|
+
Footer
|
|
29
|
+
</EdgePanel.Footer>
|
|
30
|
+
</EdgePanel>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('Compound Title')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Compound Content')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Footer')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('uses close button in compound mode', () => {
|
|
39
|
+
const onClose = vi.fn();
|
|
40
|
+
render(
|
|
41
|
+
<EdgePanel isOpen={true} onOpenChange={onClose}>
|
|
42
|
+
<EdgePanel.Header>
|
|
43
|
+
<EdgePanel.CloseButton onClick={() => onClose(false)} />
|
|
44
|
+
</EdgePanel.Header>
|
|
45
|
+
<EdgePanel.Body>Content</EdgePanel.Body>
|
|
46
|
+
</EdgePanel>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const closeBtn = screen.getByLabelText('Close panel');
|
|
50
|
+
fireEvent.click(closeBtn);
|
|
51
|
+
expect(onClose).toHaveBeenCalledWith(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -117,7 +117,7 @@ export const Checkbox = React.memo(
|
|
|
117
117
|
blurAmount: 1,
|
|
118
118
|
saturation: 160,
|
|
119
119
|
aberrationIntensity: 0.3,
|
|
120
|
-
|
|
120
|
+
borderRadius: 6,
|
|
121
121
|
mode: 'shader' as const,
|
|
122
122
|
};
|
|
123
123
|
const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
|
|
@@ -256,6 +256,29 @@ export const Sizes: Story = {
|
|
|
256
256
|
),
|
|
257
257
|
};
|
|
258
258
|
|
|
259
|
+
// Compound usage
|
|
260
|
+
export const Compound: Story = {
|
|
261
|
+
render: () => (
|
|
262
|
+
<div style={{ width: '300px' }}>
|
|
263
|
+
<Select placeholder="Select a framework">
|
|
264
|
+
<Select.Option value="react">React</Select.Option>
|
|
265
|
+
<Select.Option value="vue">Vue</Select.Option>
|
|
266
|
+
<Select.Option value="angular">Angular</Select.Option>
|
|
267
|
+
<Select.Option value="svelte" disabled>
|
|
268
|
+
Svelte (Disabled)
|
|
269
|
+
</Select.Option>
|
|
270
|
+
</Select>
|
|
271
|
+
</div>
|
|
272
|
+
),
|
|
273
|
+
parameters: {
|
|
274
|
+
docs: {
|
|
275
|
+
description: {
|
|
276
|
+
story: 'Select component using Compound Component Pattern.',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
259
282
|
// Select states
|
|
260
283
|
export const States: Story = {
|
|
261
284
|
args: {
|
|
@@ -330,7 +353,7 @@ export const GlassCustom: Story = {
|
|
|
330
353
|
blurAmount: 2,
|
|
331
354
|
saturation: 200,
|
|
332
355
|
aberrationIntensity: 0.8,
|
|
333
|
-
|
|
356
|
+
borderRadius: 12,
|
|
334
357
|
},
|
|
335
358
|
},
|
|
336
359
|
render: (args: any) => (
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Select } from './Select';
|
|
5
|
+
|
|
6
|
+
describe('Select Component', () => {
|
|
7
|
+
it('renders legacy options correctly', () => {
|
|
8
|
+
const options = [
|
|
9
|
+
{ value: '1', label: 'Option 1' },
|
|
10
|
+
{ value: '2', label: 'Option 2' },
|
|
11
|
+
];
|
|
12
|
+
render(<Select options={options} value="" onChange={() => {}} />);
|
|
13
|
+
|
|
14
|
+
// Check custom UI items
|
|
15
|
+
const items = document.querySelectorAll('.c-select__item');
|
|
16
|
+
expect(items).toHaveLength(2);
|
|
17
|
+
expect(items[0]).toHaveTextContent('Option 1');
|
|
18
|
+
expect(items[1]).toHaveTextContent('Option 2');
|
|
19
|
+
|
|
20
|
+
// Check native select options
|
|
21
|
+
const select = document.querySelector('select');
|
|
22
|
+
expect(select).not.toBeNull();
|
|
23
|
+
expect(select?.options).toHaveLength(3); // Placeholder + 2
|
|
24
|
+
expect(select?.options[1].value).toBe('1');
|
|
25
|
+
expect(select?.options[2].value).toBe('2');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders compound options correctly', async () => {
|
|
29
|
+
render(
|
|
30
|
+
<Select value="" onChange={() => {}}>
|
|
31
|
+
<Select.Option value="1">Compound Option 1</Select.Option>
|
|
32
|
+
<Select.Option value="2">Compound Option 2</Select.Option>
|
|
33
|
+
</Select>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Check custom UI items
|
|
37
|
+
const items = document.querySelectorAll('.c-select__item');
|
|
38
|
+
expect(items).toHaveLength(2);
|
|
39
|
+
expect(items[0]).toHaveTextContent('Compound Option 1');
|
|
40
|
+
expect(items[1]).toHaveTextContent('Compound Option 2');
|
|
41
|
+
|
|
42
|
+
// Check native select options
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
const select = document.querySelector('select');
|
|
45
|
+
expect(select).not.toBeNull();
|
|
46
|
+
expect(select?.options).toHaveLength(3); // Placeholder + 2
|
|
47
|
+
expect(select?.options[1].value).toBe('1');
|
|
48
|
+
expect(select?.options[2].value).toBe('2');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('handles selection in legacy mode', () => {
|
|
53
|
+
const handleChange = vi.fn();
|
|
54
|
+
const options = [
|
|
55
|
+
{ value: '1', label: 'Option 1' },
|
|
56
|
+
{ value: '2', label: 'Option 2' },
|
|
57
|
+
];
|
|
58
|
+
render(<Select options={options} value="" onChange={handleChange} />);
|
|
59
|
+
|
|
60
|
+
// Open dropdown
|
|
61
|
+
const trigger = document.querySelector('.c-select__selected');
|
|
62
|
+
fireEvent.click(trigger!);
|
|
63
|
+
|
|
64
|
+
// Click item
|
|
65
|
+
const item = document.querySelector('.c-select__item[data-value="1"]');
|
|
66
|
+
fireEvent.click(item!);
|
|
67
|
+
|
|
68
|
+
expect(handleChange).toHaveBeenCalled();
|
|
69
|
+
// Check event value
|
|
70
|
+
expect(handleChange.mock.calls[0][0].target.value).toBe('1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles selection in compound mode', async () => {
|
|
74
|
+
const handleChange = vi.fn();
|
|
75
|
+
render(
|
|
76
|
+
<Select value="" onChange={handleChange}>
|
|
77
|
+
<Select.Option value="1">Option 1</Select.Option>
|
|
78
|
+
<Select.Option value="2">Option 2</Select.Option>
|
|
79
|
+
</Select>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Wait for options to be registered
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
const select = document.querySelector('select');
|
|
85
|
+
expect(select?.options).toHaveLength(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Open dropdown
|
|
89
|
+
const trigger = document.querySelector('.c-select__selected');
|
|
90
|
+
fireEvent.click(trigger!);
|
|
91
|
+
|
|
92
|
+
// Click item
|
|
93
|
+
const item = document.querySelector('.c-select__item[data-value="2"]');
|
|
94
|
+
fireEvent.click(item!);
|
|
95
|
+
|
|
96
|
+
expect(handleChange).toHaveBeenCalled();
|
|
97
|
+
expect(handleChange.mock.calls[0][0].target.value).toBe('2');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState, memo } from 'react';
|
|
2
|
-
import { SelectProps } from '../../lib/types/components';
|
|
1
|
+
import React, { useRef, useEffect, useState, memo, useCallback } from 'react';
|
|
2
|
+
import { SelectProps, SelectOption as SelectOptionType } from '../../lib/types/components';
|
|
3
3
|
import { useSelect } from '../../lib/composables';
|
|
4
4
|
import { SELECT } from '../../lib/constants/components';
|
|
5
5
|
import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
|
|
6
|
+
import { SelectContext, SelectOption } from './SelectOption';
|
|
7
|
+
|
|
8
|
+
export type SelectComponent = React.FC<SelectProps> & {
|
|
9
|
+
Option: typeof SelectOption;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Select - A component for dropdown selection
|
|
9
14
|
*/
|
|
10
|
-
export const Select:
|
|
15
|
+
export const Select: SelectComponent = memo(
|
|
11
16
|
({
|
|
12
|
-
options
|
|
17
|
+
options,
|
|
13
18
|
value,
|
|
14
19
|
onChange,
|
|
15
20
|
onBlur,
|
|
@@ -28,7 +33,8 @@ export const Select: React.FC<SelectProps> = memo(
|
|
|
28
33
|
'aria-label': ariaLabel,
|
|
29
34
|
'aria-describedby': ariaDescribedBy,
|
|
30
35
|
glass,
|
|
31
|
-
|
|
36
|
+
children,
|
|
37
|
+
}: SelectProps) => {
|
|
32
38
|
const { generateSelectClass } = useSelect({
|
|
33
39
|
size,
|
|
34
40
|
disabled,
|
|
@@ -51,17 +57,35 @@ export const Select: React.FC<SelectProps> = memo(
|
|
|
51
57
|
const bodyRef = useRef<HTMLDivElement>(null);
|
|
52
58
|
const nativeSelectRef = useRef<HTMLSelectElement>(null);
|
|
53
59
|
|
|
60
|
+
// State for registered options (Compound mode)
|
|
61
|
+
const [registeredOptions, setRegisteredOptions] = useState<SelectOptionType[]>([]);
|
|
62
|
+
|
|
63
|
+
const registerOption = useCallback((option: SelectOptionType) => {
|
|
64
|
+
setRegisteredOptions((prev) => {
|
|
65
|
+
if (prev.some(o => o.value === option.value)) return prev;
|
|
66
|
+
return [...prev, option];
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const unregisterOption = useCallback((value: string) => {
|
|
71
|
+
setRegisteredOptions((prev) => prev.filter(o => o.value !== value));
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// Determine active options
|
|
75
|
+
const hasOptionsProp = options && options.length > 0;
|
|
76
|
+
const activeOptions = hasOptionsProp ? options : registeredOptions;
|
|
77
|
+
|
|
54
78
|
// Update selected label when value changes
|
|
55
79
|
useEffect(() => {
|
|
56
80
|
if (value) {
|
|
57
|
-
const selectedOption =
|
|
81
|
+
const selectedOption = activeOptions.find(opt => opt.value === value);
|
|
58
82
|
if (selectedOption) {
|
|
59
83
|
setSelectedLabel(selectedOption.label);
|
|
60
84
|
}
|
|
61
85
|
} else {
|
|
62
86
|
setSelectedLabel(placeholder);
|
|
63
87
|
}
|
|
64
|
-
}, [value,
|
|
88
|
+
}, [value, activeOptions, placeholder]);
|
|
65
89
|
|
|
66
90
|
// Handle click outside to close dropdown
|
|
67
91
|
useEffect(() => {
|
|
@@ -93,99 +117,125 @@ export const Select: React.FC<SelectProps> = memo(
|
|
|
93
117
|
};
|
|
94
118
|
|
|
95
119
|
// Handle item selection
|
|
96
|
-
const handleItemClick = (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
bodyRef.current
|
|
101
|
-
|
|
120
|
+
const handleItemClick = useCallback(
|
|
121
|
+
(option: { value: string; label: string }) => {
|
|
122
|
+
setSelectedLabel(option.label);
|
|
123
|
+
setIsOpen(false);
|
|
124
|
+
if (bodyRef.current) {
|
|
125
|
+
bodyRef.current.style.height = '0px';
|
|
126
|
+
}
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
if (nativeSelectRef.current) {
|
|
129
|
+
nativeSelectRef.current.value = option.value;
|
|
130
|
+
}
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
if (onChange) {
|
|
133
|
+
// Create a synthetic event
|
|
134
|
+
const event = {
|
|
135
|
+
target: {
|
|
136
|
+
name,
|
|
137
|
+
value: option.value,
|
|
138
|
+
},
|
|
139
|
+
} as React.ChangeEvent<HTMLSelectElement>;
|
|
140
|
+
onChange(event);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
[onChange, name]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const onSelect = useCallback(
|
|
147
|
+
(val: string, label: string) => {
|
|
148
|
+
handleItemClick({ value: val, label });
|
|
149
|
+
},
|
|
150
|
+
[handleItemClick]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const contextValue = React.useMemo(
|
|
154
|
+
() => ({
|
|
155
|
+
registerOption,
|
|
156
|
+
unregisterOption,
|
|
157
|
+
selectedValue: value,
|
|
158
|
+
onSelect,
|
|
159
|
+
}),
|
|
160
|
+
[registerOption, unregisterOption, value, onSelect]
|
|
161
|
+
);
|
|
118
162
|
|
|
119
163
|
const selectContent = (
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{/* Native select for accessibility and form submission */}
|
|
127
|
-
<select
|
|
128
|
-
ref={nativeSelectRef}
|
|
129
|
-
value={value}
|
|
130
|
-
onChange={onChange}
|
|
131
|
-
onBlur={onBlur}
|
|
132
|
-
onFocus={onFocus}
|
|
133
|
-
disabled={disabled}
|
|
134
|
-
required={required}
|
|
135
|
-
id={id}
|
|
136
|
-
name={name}
|
|
137
|
-
multiple={multiple}
|
|
138
|
-
aria-label={ariaLabel}
|
|
139
|
-
aria-describedby={ariaDescribedBy}
|
|
140
|
-
aria-invalid={invalid}
|
|
141
|
-
style={{ display: 'none' }}
|
|
164
|
+
<SelectContext.Provider value={contextValue}>
|
|
165
|
+
<div
|
|
166
|
+
className={`${selectClass} ${isOpen ? SELECT.CLASSES.IS_OPEN : ''}`}
|
|
167
|
+
ref={dropdownRef}
|
|
168
|
+
style={style}
|
|
169
|
+
aria-expanded={isOpen}
|
|
142
170
|
>
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
171
|
+
{/* Native select for accessibility and form submission */}
|
|
172
|
+
<select
|
|
173
|
+
ref={nativeSelectRef}
|
|
174
|
+
value={value}
|
|
175
|
+
onChange={onChange}
|
|
176
|
+
onBlur={onBlur}
|
|
177
|
+
onFocus={onFocus}
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
required={required}
|
|
180
|
+
id={id}
|
|
181
|
+
name={name}
|
|
182
|
+
multiple={multiple}
|
|
183
|
+
aria-label={ariaLabel}
|
|
184
|
+
aria-describedby={ariaDescribedBy}
|
|
185
|
+
aria-invalid={invalid}
|
|
186
|
+
style={{ display: 'none' }}
|
|
187
|
+
>
|
|
188
|
+
{placeholder && (
|
|
189
|
+
<option value="" disabled>
|
|
190
|
+
{placeholder}
|
|
191
|
+
</option>
|
|
192
|
+
)}
|
|
193
|
+
{activeOptions.map(option => (
|
|
194
|
+
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
195
|
+
{option.label}
|
|
196
|
+
</option>
|
|
197
|
+
))}
|
|
198
|
+
</select>
|
|
199
|
+
|
|
200
|
+
{/* Custom Select UI */}
|
|
201
|
+
<div className={SELECT.CLASSES.SELECTED} onClick={handleToggle} aria-disabled={disabled}>
|
|
202
|
+
{selectedLabel}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<i className={`${SELECT.CLASSES.ICON_CARET} ${SELECT.CLASSES.TOGGLE_ICON}`} />
|
|
159
206
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
<div className={SELECT.CLASSES.SELECT_BODY} ref={bodyRef} style={{ height: 0 }}>
|
|
208
|
+
<div className={SELECT.CLASSES.SELECT_PANEL} ref={panelRef}>
|
|
209
|
+
<ul className={SELECT.CLASSES.SELECT_ITEMS}>
|
|
210
|
+
{hasOptionsProp ? (
|
|
211
|
+
options.map((option, index) => (
|
|
212
|
+
<li
|
|
213
|
+
key={option.value}
|
|
214
|
+
className={SELECT.CLASSES.SELECT_ITEM}
|
|
215
|
+
data-value={option.value}
|
|
216
|
+
onClick={() => !option.disabled && handleItemClick(option)}
|
|
217
|
+
>
|
|
218
|
+
<label htmlFor={`SelectItem${index}`} className="c-checkbox">
|
|
219
|
+
<input
|
|
220
|
+
type="checkbox"
|
|
221
|
+
id={`SelectItem${index}`}
|
|
222
|
+
className="c-checkbox__input c-select__item-input"
|
|
223
|
+
checked={value === option.value}
|
|
224
|
+
readOnly
|
|
225
|
+
disabled={option.disabled}
|
|
226
|
+
/>
|
|
227
|
+
<div className="c-select__item-label">{option.label}</div>
|
|
228
|
+
</label>
|
|
229
|
+
</li>
|
|
230
|
+
))
|
|
231
|
+
) : (
|
|
232
|
+
children
|
|
233
|
+
)}
|
|
234
|
+
</ul>
|
|
235
|
+
</div>
|
|
186
236
|
</div>
|
|
187
237
|
</div>
|
|
188
|
-
</
|
|
238
|
+
</SelectContext.Provider>
|
|
189
239
|
);
|
|
190
240
|
|
|
191
241
|
if (glass) {
|
|
@@ -195,7 +245,7 @@ export const Select: React.FC<SelectProps> = memo(
|
|
|
195
245
|
blurAmount: 1,
|
|
196
246
|
saturation: 180,
|
|
197
247
|
aberrationIntensity: 0.2,
|
|
198
|
-
|
|
248
|
+
borderRadius: 12,
|
|
199
249
|
mode: 'shader' as const,
|
|
200
250
|
};
|
|
201
251
|
|
|
@@ -206,10 +256,11 @@ export const Select: React.FC<SelectProps> = memo(
|
|
|
206
256
|
|
|
207
257
|
return selectContent;
|
|
208
258
|
}
|
|
209
|
-
);
|
|
259
|
+
) as unknown as SelectComponent;
|
|
210
260
|
|
|
211
261
|
export type { SelectProps };
|
|
212
262
|
|
|
213
263
|
Select.displayName = 'Select';
|
|
264
|
+
Select.Option = SelectOption;
|
|
214
265
|
|
|
215
266
|
export default Select;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, memo, ReactNode } from 'react';
|
|
2
|
+
import { SelectOption as SelectOptionType } from '../../lib/types/components';
|
|
3
|
+
import { SELECT } from '../../lib/constants/components';
|
|
4
|
+
|
|
5
|
+
// Context for managing options registration and selection
|
|
6
|
+
export interface SelectContextType {
|
|
7
|
+
registerOption: (option: SelectOptionType) => void;
|
|
8
|
+
unregisterOption: (value: string) => void;
|
|
9
|
+
selectedValue?: string | string[];
|
|
10
|
+
onSelect: (value: string, label: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SelectContext = createContext<SelectContextType | null>(null);
|
|
14
|
+
|
|
15
|
+
export interface SelectOptionProps {
|
|
16
|
+
value: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: React.CSSProperties;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SelectOption: React.FC<SelectOptionProps> = memo(
|
|
24
|
+
({ value, children, disabled = false, className = '', style }) => {
|
|
25
|
+
const context = useContext(SelectContext);
|
|
26
|
+
|
|
27
|
+
// We assume children is the label if it's a string, or we need a way to get label.
|
|
28
|
+
// For simplicity, we use children as label for registration if it's a string.
|
|
29
|
+
const label = typeof children === 'string' ? children : value;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (context) {
|
|
33
|
+
context.registerOption({ value, label, disabled });
|
|
34
|
+
return () => {
|
|
35
|
+
context.unregisterOption(value);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}, [context, value, label, disabled]);
|
|
40
|
+
|
|
41
|
+
if (!context) {
|
|
42
|
+
console.warn('SelectOption must be used within a Select component');
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { selectedValue, onSelect } = context;
|
|
47
|
+
|
|
48
|
+
const isSelected = Array.isArray(selectedValue)
|
|
49
|
+
? selectedValue.includes(value)
|
|
50
|
+
: selectedValue === value;
|
|
51
|
+
|
|
52
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
if (!disabled) {
|
|
56
|
+
onSelect(value, label);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<li
|
|
62
|
+
className={`${SELECT.CLASSES.SELECT_ITEM} ${className}`.trim()}
|
|
63
|
+
data-value={value}
|
|
64
|
+
onClick={handleClick}
|
|
65
|
+
style={style}
|
|
66
|
+
role="option"
|
|
67
|
+
aria-selected={isSelected}
|
|
68
|
+
aria-disabled={disabled}
|
|
69
|
+
>
|
|
70
|
+
<label className="c-checkbox" style={{ pointerEvents: 'none' }}>
|
|
71
|
+
<input
|
|
72
|
+
type="checkbox"
|
|
73
|
+
className="c-checkbox__input c-select__item-input"
|
|
74
|
+
checked={isSelected}
|
|
75
|
+
readOnly
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
tabIndex={-1}
|
|
78
|
+
/>
|
|
79
|
+
<div className="c-select__item-label">{children}</div>
|
|
80
|
+
</label>
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
SelectOption.displayName = 'SelectOption';
|
|
87
|
+
|
|
88
|
+
export default SelectOption;
|