@tsiky/components-r19 1.0.0 → 1.1.0
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/index.ts +35 -33
- package/package.json +1 -1
- package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +123 -89
- package/src/components/Charts/bar-chart/BarChart.tsx +167 -132
- package/src/components/Charts/mixed-chart/MixedChart.tsx +65 -9
- package/src/components/Charts/sankey-chart/SankeyChart.tsx +183 -155
- package/src/components/Confirmationpopup/ConfirmationPopup.module.css +88 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.stories.tsx +94 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.tsx +47 -0
- package/src/components/Confirmationpopup/index.ts +6 -0
- package/src/components/Confirmationpopup/useConfirmationPopup.ts +48 -0
- package/src/components/DayStatCard/DayStatCard.tsx +96 -69
- package/src/components/DynamicTable/AdvancedFilters.tsx +196 -196
- package/src/components/DynamicTable/ColumnSorter.tsx +185 -185
- package/src/components/DynamicTable/Pagination.tsx +115 -115
- package/src/components/DynamicTable/TableauDynamique.module.css +1287 -1287
- package/src/components/DynamicTable/filters/SelectFilter.tsx +69 -69
- package/src/components/EntryControl/EntryControl.tsx +117 -117
- package/src/components/Grid/Grid.tsx +5 -0
- package/src/components/Header/Header.tsx +4 -2
- package/src/components/Header/header.css +61 -31
- package/src/components/MetricsPanel/MetricsPanel.module.css +688 -636
- package/src/components/MetricsPanel/MetricsPanel.tsx +220 -282
- package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +148 -125
- package/src/components/NavBar/NavBar.tsx +1 -1
- package/src/components/SelectFilter/SelectFilter.module.css +249 -0
- package/src/components/SelectFilter/SelectFilter.stories.tsx +321 -0
- package/src/components/SelectFilter/SelectFilter.tsx +219 -0
- package/src/components/SelectFilter/index.ts +2 -0
- package/src/components/SelectFilter/types.ts +19 -0
- package/src/components/TranslationKey/TranslationKey.tsx +265 -245
- package/src/components/TrendList/TrendList.tsx +72 -45
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import SelectFilter from './SelectFilter';
|
|
4
|
+
import type { SelectFilterProps, SelectOption } from './types';
|
|
5
|
+
|
|
6
|
+
const mockCountries: SelectOption[] = [
|
|
7
|
+
{ value: 'fr', label: 'France' },
|
|
8
|
+
{ value: 'us', label: 'United States' },
|
|
9
|
+
{ value: 'de', label: 'Germany' },
|
|
10
|
+
{ value: 'it', label: 'Italy' },
|
|
11
|
+
{ value: 'es', label: 'Spain' },
|
|
12
|
+
{ value: 'uk', label: 'United Kingdom' },
|
|
13
|
+
{ value: 'jp', label: 'Japan' },
|
|
14
|
+
{ value: 'ca', label: 'Canada' },
|
|
15
|
+
{ value: 'au', label: 'Australia' },
|
|
16
|
+
{ value: 'br', label: 'Brazil' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const mockUsers: SelectOption[] = [
|
|
20
|
+
{ value: 1, label: 'John Doe - john@example.com' },
|
|
21
|
+
{ value: 2, label: 'Jane Smith - jane@example.com' },
|
|
22
|
+
{ value: 3, label: 'Bob Johnson - bob@example.com' },
|
|
23
|
+
{ value: 4, label: 'Alice Brown - alice@example.com' },
|
|
24
|
+
{ value: 5, label: 'Charlie Wilson - charlie@example.com' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const mockLongLabels: SelectOption[] = [
|
|
28
|
+
{ value: 1041, label: 'MEDECINE VASCULAIRE HOSPITALISATION COMPLETE (1041)' },
|
|
29
|
+
{ value: 1042, label: 'CARDIOLOGIE HOSPITALISATION COMPLETE (1042)' },
|
|
30
|
+
{ value: 1043, label: 'CHIRURGIE THORACIQUE ET CARDIO-VASCULAIRE (1043)' },
|
|
31
|
+
{ value: 1044, label: 'NEUROLOGIE HOSPITALISATION COMPLETE (1044)' },
|
|
32
|
+
{ value: 1045, label: 'NEUROCHIRURGIE HOSPITALISATION COMPLETE (1045)' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
type SelectFilterStoryProps = Omit<SelectFilterProps, 'onChange'> & {
|
|
36
|
+
onChange?: (value: string | number) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const meta: Meta<SelectFilterStoryProps> = {
|
|
40
|
+
title: 'Components/SelectFilter',
|
|
41
|
+
component: SelectFilter,
|
|
42
|
+
parameters: {
|
|
43
|
+
layout: 'centered',
|
|
44
|
+
},
|
|
45
|
+
tags: ['autodocs'],
|
|
46
|
+
argTypes: {
|
|
47
|
+
size: {
|
|
48
|
+
control: { type: 'radio' },
|
|
49
|
+
options: ['sm', 'md', 'lg'],
|
|
50
|
+
},
|
|
51
|
+
variant: {
|
|
52
|
+
control: { type: 'select' },
|
|
53
|
+
options: ['default', 'outline', 'filled'],
|
|
54
|
+
},
|
|
55
|
+
disabled: {
|
|
56
|
+
control: { type: 'boolean' },
|
|
57
|
+
},
|
|
58
|
+
loading: {
|
|
59
|
+
control: { type: 'boolean' },
|
|
60
|
+
},
|
|
61
|
+
error: {
|
|
62
|
+
control: { type: 'boolean' },
|
|
63
|
+
},
|
|
64
|
+
width: {
|
|
65
|
+
control: { type: 'text' },
|
|
66
|
+
description: 'Classes Tailwind pour la largeur (ex: w-64, w-80, w-full)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
args: {
|
|
70
|
+
placeholder: 'Sélectionner une option...',
|
|
71
|
+
options: mockCountries,
|
|
72
|
+
width: 'w-80',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default meta;
|
|
77
|
+
type Story = StoryObj<SelectFilterStoryProps>;
|
|
78
|
+
|
|
79
|
+
const SelectWithState = (args: SelectFilterStoryProps) => {
|
|
80
|
+
const [value, setValue] = useState<string | number>('');
|
|
81
|
+
|
|
82
|
+
const handleChange = (newValue: string | number, _option: SelectOption) => {
|
|
83
|
+
setValue(newValue);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={args.width || 'w-80'}>
|
|
88
|
+
<SelectFilter {...args} value={value} onChange={handleChange} />
|
|
89
|
+
<div className='mt-4 text-sm text-gray-600'>Valeur sélectionnée: {value || 'Aucune'}</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const Default: Story = {
|
|
95
|
+
render: (args) => <SelectWithState {...args} />,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const WithCustomPlaceholder: Story = {
|
|
99
|
+
render: (args) => <SelectWithState {...args} />,
|
|
100
|
+
args: {
|
|
101
|
+
placeholder: 'Choisissez un pays...',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Small: Story = {
|
|
106
|
+
render: (args) => <SelectWithState {...args} />,
|
|
107
|
+
args: {
|
|
108
|
+
size: 'sm',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Medium: Story = {
|
|
113
|
+
render: (args) => <SelectWithState {...args} />,
|
|
114
|
+
args: {
|
|
115
|
+
size: 'md',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Large: Story = {
|
|
120
|
+
render: (args) => <SelectWithState {...args} />,
|
|
121
|
+
args: {
|
|
122
|
+
size: 'lg',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const Outline: Story = {
|
|
127
|
+
render: (args) => <SelectWithState {...args} />,
|
|
128
|
+
args: {
|
|
129
|
+
variant: 'outline',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const Filled: Story = {
|
|
134
|
+
render: (args) => <SelectWithState {...args} />,
|
|
135
|
+
args: {
|
|
136
|
+
variant: 'filled',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const Disabled: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
disabled: true,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const Loading: Story = {
|
|
147
|
+
args: {
|
|
148
|
+
loading: true,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const Error: Story = {
|
|
153
|
+
args: {
|
|
154
|
+
error: true,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const WithUserData: Story = {
|
|
159
|
+
render: (args) => <SelectWithState {...args} />,
|
|
160
|
+
args: {
|
|
161
|
+
options: mockUsers,
|
|
162
|
+
placeholder: 'Rechercher un utilisateur...',
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const WithDisabledOptions: Story = {
|
|
167
|
+
render: (args) => <SelectWithState {...args} />,
|
|
168
|
+
args: {
|
|
169
|
+
options: [
|
|
170
|
+
{ value: 'fr', label: 'France' },
|
|
171
|
+
{ value: 'us', label: 'United States' },
|
|
172
|
+
{ value: 'de', label: 'Germany', disabled: true },
|
|
173
|
+
{ value: 'it', label: 'Italy' },
|
|
174
|
+
{ value: 'es', label: 'Spain', disabled: true },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const WithLongLabels: Story = {
|
|
180
|
+
render: (args) => <SelectWithState {...args} />,
|
|
181
|
+
args: {
|
|
182
|
+
options: mockLongLabels,
|
|
183
|
+
placeholder: 'Sélectionnez un service médical...',
|
|
184
|
+
width: 'w-96',
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const DifferentWidths: Story = {
|
|
189
|
+
render: () => {
|
|
190
|
+
const [value1, setValue1] = useState('');
|
|
191
|
+
const [value2, setValue2] = useState('');
|
|
192
|
+
const [value3, setValue3] = useState('');
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className='space-y-6 max-w-2xl'>
|
|
196
|
+
<div>
|
|
197
|
+
<label className='block text-sm font-medium text-gray-700 mb-2'>
|
|
198
|
+
Largeur normale (w-64)
|
|
199
|
+
</label>
|
|
200
|
+
<SelectFilter
|
|
201
|
+
options={mockCountries}
|
|
202
|
+
value={value1}
|
|
203
|
+
onChange={(newValue, _option) => setValue1(newValue.toString())}
|
|
204
|
+
placeholder='Pays...'
|
|
205
|
+
width='w-64'
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div>
|
|
210
|
+
<label className='block text-sm font-medium text-gray-700 mb-2'>
|
|
211
|
+
Largeur moyenne (w-80)
|
|
212
|
+
</label>
|
|
213
|
+
<SelectFilter
|
|
214
|
+
options={mockUsers}
|
|
215
|
+
value={value2}
|
|
216
|
+
onChange={(newValue, _option) => setValue2(newValue.toString())}
|
|
217
|
+
placeholder='Utilisateurs...'
|
|
218
|
+
width='w-80'
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div>
|
|
223
|
+
<label className='block text-sm font-medium text-gray-700 mb-2'>
|
|
224
|
+
Largeur pour labels longs (w-96)
|
|
225
|
+
</label>
|
|
226
|
+
<SelectFilter
|
|
227
|
+
options={mockLongLabels}
|
|
228
|
+
value={value3}
|
|
229
|
+
onChange={(newValue, _option) => setValue3(newValue.toString())}
|
|
230
|
+
placeholder='Services médicaux...'
|
|
231
|
+
width='w-96'
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className='p-4 bg-gray-50 rounded-lg'>
|
|
236
|
+
<div className='text-sm space-y-2'>
|
|
237
|
+
<div>Pays sélectionné: {value1 || 'Aucun'}</div>
|
|
238
|
+
<div>Utilisateur sélectionné: {value2 || 'Aucun'}</div>
|
|
239
|
+
<div>Service sélectionné: {value3 || 'Aucun'}</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const MultipleSelects: Story = {
|
|
248
|
+
render: () => {
|
|
249
|
+
const [country, setCountry] = useState('');
|
|
250
|
+
const [user, setUser] = useState('');
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div className='space-y-4 w-80'>
|
|
254
|
+
<div>
|
|
255
|
+
<label className='block text-sm font-medium text-gray-700 mb-2'>Pays</label>
|
|
256
|
+
<SelectFilter
|
|
257
|
+
options={mockCountries}
|
|
258
|
+
value={country}
|
|
259
|
+
onChange={(newValue, _option) => setCountry(newValue.toString())}
|
|
260
|
+
placeholder='Choisir un pays'
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div>
|
|
265
|
+
<label className='block text-sm font-medium text-gray-700 mb-2'>Utilisateur</label>
|
|
266
|
+
<SelectFilter
|
|
267
|
+
options={mockUsers}
|
|
268
|
+
value={user}
|
|
269
|
+
onChange={(newValue, _option) => setUser(newValue.toString())}
|
|
270
|
+
placeholder='Choisir un utilisateur'
|
|
271
|
+
variant='outline'
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className='p-4 bg-gray-50 rounded-lg'>
|
|
276
|
+
<div className='text-sm'>
|
|
277
|
+
<div>Pays sélectionné: {country || 'Aucun'}</div>
|
|
278
|
+
<div>Utilisateur sélectionné: {user || 'Aucun'}</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const AsyncLoading: Story = {
|
|
287
|
+
render: () => {
|
|
288
|
+
const [options, setOptions] = useState<SelectOption[]>([]);
|
|
289
|
+
const [loading, setLoading] = useState(false);
|
|
290
|
+
const [value, setValue] = useState('');
|
|
291
|
+
|
|
292
|
+
const loadOptions = () => {
|
|
293
|
+
setLoading(true);
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
setOptions(mockCountries);
|
|
296
|
+
setLoading(false);
|
|
297
|
+
}, 2000);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className='w-80'>
|
|
302
|
+
<SelectFilter
|
|
303
|
+
options={options}
|
|
304
|
+
value={value}
|
|
305
|
+
onChange={(newValue, _option) => setValue(newValue.toString())}
|
|
306
|
+
loading={loading}
|
|
307
|
+
placeholder='Cliquez pour charger les options'
|
|
308
|
+
/>
|
|
309
|
+
<div className='mt-2 text-xs text-gray-500'>
|
|
310
|
+
<button
|
|
311
|
+
type='button'
|
|
312
|
+
onClick={loadOptions}
|
|
313
|
+
className='text-blue-600 hover:text-blue-800 underline'
|
|
314
|
+
>
|
|
315
|
+
Charger les options
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
|
2
|
+
import type { SelectFilterProps, SelectOption } from './types';
|
|
3
|
+
import styles from './SelectFilter.module.css';
|
|
4
|
+
|
|
5
|
+
const SelectFilter: React.FC<SelectFilterProps> = ({
|
|
6
|
+
options,
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
placeholder = 'Sélectionner une option...',
|
|
10
|
+
disabled = false,
|
|
11
|
+
loading = false,
|
|
12
|
+
error = false,
|
|
13
|
+
className = '',
|
|
14
|
+
size = 'md',
|
|
15
|
+
variant = 'default',
|
|
16
|
+
width = 'w-full',
|
|
17
|
+
}) => {
|
|
18
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
19
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
20
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
21
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
22
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
const filteredOptions = useMemo(() => {
|
|
25
|
+
if (!searchTerm) return options;
|
|
26
|
+
return options.filter((option) =>
|
|
27
|
+
option.label.toLowerCase().includes(searchTerm.toLowerCase())
|
|
28
|
+
);
|
|
29
|
+
}, [options, searchTerm]);
|
|
30
|
+
|
|
31
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
35
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
36
|
+
setIsOpen(false);
|
|
37
|
+
setSearchTerm('');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
42
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setHighlightedIndex(0);
|
|
47
|
+
}, [filteredOptions]);
|
|
48
|
+
|
|
49
|
+
const handleSelect = (option: SelectOption) => {
|
|
50
|
+
if (option.disabled) return;
|
|
51
|
+
onChange?.(option.value, option);
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
setSearchTerm('');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
setSearchTerm(e.target.value);
|
|
58
|
+
if (!isOpen) setIsOpen(true);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
62
|
+
if (!isOpen) {
|
|
63
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
|
64
|
+
setIsOpen(true);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (e.key) {
|
|
70
|
+
case 'ArrowDown':
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev));
|
|
73
|
+
break;
|
|
74
|
+
case 'ArrowUp':
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
77
|
+
break;
|
|
78
|
+
case 'Enter':
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
if (filteredOptions[highlightedIndex]) {
|
|
81
|
+
handleSelect(filteredOptions[highlightedIndex]);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'Escape':
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
setSearchTerm('');
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getButtonClasses = () => {
|
|
92
|
+
const baseClasses = [
|
|
93
|
+
styles.selectButton,
|
|
94
|
+
styles[size],
|
|
95
|
+
styles[variant],
|
|
96
|
+
disabled ? styles.disabled : '',
|
|
97
|
+
error ? styles.error : '',
|
|
98
|
+
className,
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(' ');
|
|
102
|
+
|
|
103
|
+
return baseClasses;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getWidthClass = () => {
|
|
107
|
+
switch (width) {
|
|
108
|
+
case 'w-64':
|
|
109
|
+
return styles.width64;
|
|
110
|
+
case 'w-80':
|
|
111
|
+
return styles.width80;
|
|
112
|
+
case 'w-96':
|
|
113
|
+
return styles.width96;
|
|
114
|
+
default:
|
|
115
|
+
return styles.widthFull;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const getOptionClasses = (option: SelectOption, index: number) => {
|
|
120
|
+
return [
|
|
121
|
+
styles.option,
|
|
122
|
+
index === highlightedIndex ? styles.optionHighlighted : '',
|
|
123
|
+
option.value === value ? styles.optionSelected : '',
|
|
124
|
+
option.disabled ? styles.optionDisabled : '',
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join(' ');
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className={`${styles.container} ${getWidthClass()}`} ref={dropdownRef}>
|
|
132
|
+
<button
|
|
133
|
+
className={getButtonClasses()}
|
|
134
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
135
|
+
onKeyDown={handleKeyDown}
|
|
136
|
+
tabIndex={disabled ? -1 : 0}
|
|
137
|
+
type='button'
|
|
138
|
+
aria-haspopup='listbox'
|
|
139
|
+
aria-expanded={isOpen}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
>
|
|
142
|
+
<div className={styles.buttonContent}>
|
|
143
|
+
<span className={`${styles.label} ${!selectedOption ? styles.placeholder : ''}`}>
|
|
144
|
+
{selectedOption ? selectedOption.label : placeholder}
|
|
145
|
+
</span>
|
|
146
|
+
<div className={styles.iconsContainer}>
|
|
147
|
+
{loading && <div className={styles.loadingSpinner} />}
|
|
148
|
+
<svg
|
|
149
|
+
className={`${styles.arrowIcon} ${isOpen ? styles.arrowIconOpen : ''}`}
|
|
150
|
+
fill='none'
|
|
151
|
+
stroke='currentColor'
|
|
152
|
+
viewBox='0 0 24 24'
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
strokeLinecap='round'
|
|
156
|
+
strokeLinejoin='round'
|
|
157
|
+
strokeWidth={2}
|
|
158
|
+
d='M19 9l-7 7-7-7'
|
|
159
|
+
/>
|
|
160
|
+
</svg>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</button>
|
|
164
|
+
|
|
165
|
+
{isOpen && (
|
|
166
|
+
<div className={`${styles.dropdown} ${getWidthClass()}`}>
|
|
167
|
+
<div className={styles.searchContainer}>
|
|
168
|
+
<input
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
type='text'
|
|
171
|
+
value={searchTerm}
|
|
172
|
+
onChange={handleInputChange}
|
|
173
|
+
placeholder='Rechercher...'
|
|
174
|
+
className={styles.searchInput}
|
|
175
|
+
autoFocus
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className={styles.optionsList}>
|
|
180
|
+
{filteredOptions.length === 0 ? (
|
|
181
|
+
<div className={styles.noResults}>Aucun résultat trouvé</div>
|
|
182
|
+
) : (
|
|
183
|
+
<ul role='listbox'>
|
|
184
|
+
{filteredOptions.map((option, index) => (
|
|
185
|
+
<li
|
|
186
|
+
key={option.value}
|
|
187
|
+
className={getOptionClasses(option, index)}
|
|
188
|
+
onClick={() => handleSelect(option)}
|
|
189
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
190
|
+
role='option'
|
|
191
|
+
aria-selected={option.value === value}
|
|
192
|
+
aria-disabled={option.disabled}
|
|
193
|
+
>
|
|
194
|
+
<div className={styles.buttonContent}>
|
|
195
|
+
<span className={styles.optionLabel} title={option.label}>
|
|
196
|
+
{option.label}
|
|
197
|
+
</span>
|
|
198
|
+
{option.value === value && (
|
|
199
|
+
<svg className={styles.checkIcon} fill='currentColor' viewBox='0 0 20 20'>
|
|
200
|
+
<path
|
|
201
|
+
fillRule='evenodd'
|
|
202
|
+
d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'
|
|
203
|
+
clipRule='evenodd'
|
|
204
|
+
/>
|
|
205
|
+
</svg>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</li>
|
|
209
|
+
))}
|
|
210
|
+
</ul>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export default SelectFilter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SelectOption {
|
|
2
|
+
value: string | number;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SelectFilterProps {
|
|
8
|
+
options: SelectOption[];
|
|
9
|
+
value?: string | number;
|
|
10
|
+
onChange?: (value: string | number, option: SelectOption) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
error?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
size?: 'sm' | 'md' | 'lg';
|
|
17
|
+
variant?: 'default' | 'outline' | 'filled';
|
|
18
|
+
width?: 'w-full' | 'w-64' | 'w-80' | 'w-96';
|
|
19
|
+
}
|