@udixio/ui-react 2.9.23 → 2.10.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/CHANGELOG.md +32 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +2906 -2498
- package/dist/lib/components/AnchorPositioner.d.ts +2 -1
- package/dist/lib/components/AnchorPositioner.d.ts.map +1 -1
- package/dist/lib/components/ContextMenu.d.ts +15 -0
- package/dist/lib/components/ContextMenu.d.ts.map +1 -0
- package/dist/lib/components/Menu.d.ts +16 -0
- package/dist/lib/components/Menu.d.ts.map +1 -0
- package/dist/lib/components/MenuGroup.d.ts +12 -0
- package/dist/lib/components/MenuGroup.d.ts.map +1 -0
- package/dist/lib/components/MenuHeadline.d.ts +7 -0
- package/dist/lib/components/MenuHeadline.d.ts.map +1 -0
- package/dist/lib/components/MenuItem.d.ts +9 -0
- package/dist/lib/components/MenuItem.d.ts.map +1 -0
- package/dist/lib/components/TextField.d.ts +5 -1
- package/dist/lib/components/TextField.d.ts.map +1 -1
- package/dist/lib/components/index.d.ts +6 -2
- package/dist/lib/components/index.d.ts.map +1 -1
- package/dist/lib/icon/icon.d.ts.map +1 -1
- package/dist/lib/interfaces/index.d.ts +1 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -1
- package/dist/lib/interfaces/menu-group.interface.d.ts +13 -0
- package/dist/lib/interfaces/menu-group.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/menu-item.interface.d.ts +34 -0
- package/dist/lib/interfaces/menu-item.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/menu.interface.d.ts +17 -0
- package/dist/lib/interfaces/menu.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/text-field.interface.d.ts +9 -1
- package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
- package/dist/lib/styles/index.d.ts +3 -0
- package/dist/lib/styles/index.d.ts.map +1 -1
- package/dist/lib/styles/menu-group.style.d.ts +14 -0
- package/dist/lib/styles/menu-group.style.d.ts.map +1 -0
- package/dist/lib/styles/menu-headline.style.d.ts +19 -0
- package/dist/lib/styles/menu-headline.style.d.ts.map +1 -0
- package/dist/lib/styles/menu-item.style.d.ts +39 -0
- package/dist/lib/styles/menu-item.style.d.ts.map +1 -0
- package/dist/lib/styles/menu.style.d.ts +19 -0
- package/dist/lib/styles/menu.style.d.ts.map +1 -0
- package/dist/lib/styles/text-field.style.d.ts +19 -2
- package/dist/lib/styles/text-field.style.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/components/AnchorPositioner.tsx +61 -18
- package/src/lib/components/ContextMenu.tsx +111 -0
- package/src/lib/components/Menu.tsx +113 -0
- package/src/lib/components/MenuGroup.tsx +34 -0
- package/src/lib/components/MenuHeadline.tsx +9 -0
- package/src/lib/components/MenuItem.tsx +197 -0
- package/src/lib/components/TextField.tsx +154 -26
- package/src/lib/components/index.ts +7 -2
- package/src/lib/icon/icon.tsx +10 -6
- package/src/lib/interfaces/index.ts +1 -0
- package/src/lib/interfaces/menu-group.interface.ts +13 -0
- package/src/lib/interfaces/menu-item.interface.ts +35 -0
- package/src/lib/interfaces/menu.interface.ts +20 -0
- package/src/lib/interfaces/text-field.interface.ts +9 -1
- package/src/lib/styles/index.ts +3 -0
- package/src/lib/styles/menu-group.style.ts +34 -0
- package/src/lib/styles/menu-headline.style.ts +20 -0
- package/src/lib/styles/menu-item.style.ts +53 -0
- package/src/lib/styles/menu.style.ts +32 -0
|
@@ -3,10 +3,16 @@ import { Icon } from '../icon';
|
|
|
3
3
|
import {
|
|
4
4
|
faCalendarDays,
|
|
5
5
|
faCircleExclamation,
|
|
6
|
+
faChevronDown,
|
|
7
|
+
faChevronUp,
|
|
6
8
|
} from '@fortawesome/free-solid-svg-icons';
|
|
7
9
|
import { motion } from 'motion/react';
|
|
8
10
|
import { DatePicker } from './DatePicker';
|
|
9
11
|
import { Button } from './Button';
|
|
12
|
+
import { Menu } from './Menu';
|
|
13
|
+
import { MenuItem } from './MenuItem';
|
|
14
|
+
import { Divider } from './Divider';
|
|
15
|
+
import { MenuHeadline } from './MenuHeadline';
|
|
10
16
|
|
|
11
17
|
import TextareaAutosize from 'react-textarea-autosize';
|
|
12
18
|
import { useTextFieldStyle } from '../styles/text-field.style';
|
|
@@ -22,6 +28,7 @@ import { TextFieldInterface } from '../interfaces';
|
|
|
22
28
|
* @devx
|
|
23
29
|
* - Supports controlled (`value`) and uncontrolled (`defaultValue`) usage.
|
|
24
30
|
* - `multiline` switches to textarea mode.
|
|
31
|
+
* - `type="select" ` switches to select mode with `options`
|
|
25
32
|
* @a11y
|
|
26
33
|
* - `aria-describedby` links supporting text/error to input.
|
|
27
34
|
*/
|
|
@@ -50,8 +57,10 @@ export const TextField = ({
|
|
|
50
57
|
ref,
|
|
51
58
|
onFocus,
|
|
52
59
|
onBlur,
|
|
60
|
+
options,
|
|
61
|
+
children,
|
|
53
62
|
...restProps
|
|
54
|
-
}: ReactProps<TextFieldInterface>) => {
|
|
63
|
+
}: ReactProps<TextFieldInterface> & { children?: React.ReactNode }) => {
|
|
55
64
|
const generatedId = useId();
|
|
56
65
|
const id = idProp || generatedId;
|
|
57
66
|
const helperTextId = `${id}-helper`;
|
|
@@ -78,20 +87,23 @@ export const TextField = ({
|
|
|
78
87
|
}, [errorText]);
|
|
79
88
|
|
|
80
89
|
const focusInput = () => {
|
|
81
|
-
if (inputRef.current && !isFocused) {
|
|
82
|
-
|
|
90
|
+
if (inputRef.current && !isFocused && !disabled) {
|
|
91
|
+
if (type !== 'select') {
|
|
92
|
+
inputRef.current.focus();
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
};
|
|
85
96
|
|
|
86
97
|
useEffect(() => {
|
|
87
98
|
if (!autoFocus || disabled) return;
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
if (type !== 'select') {
|
|
101
|
+
const rafId = window.requestAnimationFrame(() => {
|
|
102
|
+
focusInput();
|
|
103
|
+
});
|
|
104
|
+
return () => window.cancelAnimationFrame(rafId);
|
|
105
|
+
}
|
|
106
|
+
}, [autoFocus, disabled, inputRef, type]);
|
|
95
107
|
|
|
96
108
|
useEffect(() => {
|
|
97
109
|
if (isFocused) {
|
|
@@ -147,7 +159,7 @@ export const TextField = ({
|
|
|
147
159
|
useEffect(() => {
|
|
148
160
|
if (showDatePicker) {
|
|
149
161
|
setIsFocused(true);
|
|
150
|
-
} else {
|
|
162
|
+
} else if (!isSelectInput || !showMenu) {
|
|
151
163
|
setIsFocused(false);
|
|
152
164
|
}
|
|
153
165
|
}, [showDatePicker]);
|
|
@@ -203,7 +215,6 @@ export const TextField = ({
|
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
if (onChange) {
|
|
206
|
-
// Create a synthetic event
|
|
207
218
|
const event = {
|
|
208
219
|
target: {
|
|
209
220
|
value: newValue,
|
|
@@ -216,13 +227,78 @@ export const TextField = ({
|
|
|
216
227
|
setShowDatePicker(false);
|
|
217
228
|
};
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
// Select Logic
|
|
231
|
+
const isSelectInput = type === 'select';
|
|
232
|
+
const [showMenu, setShowMenu] = useState(false);
|
|
233
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
234
|
+
|
|
235
|
+
const displayValue = useMemo(() => {
|
|
236
|
+
if (isSelectInput && options) {
|
|
237
|
+
const selectedOption = options.find(
|
|
238
|
+
(o) => String(o.value) === String(value),
|
|
239
|
+
);
|
|
240
|
+
return selectedOption ? selectedOption.label : value;
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}, [value, isSelectInput, options]);
|
|
221
244
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
const handleSelectToggle = () => {
|
|
246
|
+
if (disabled) return;
|
|
247
|
+
setShowMenu(!showMenu);
|
|
248
|
+
setIsFocused(!showMenu);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleSelectOption = (optionValue: string | number) => {
|
|
252
|
+
if (!isControlled) {
|
|
253
|
+
setInternalValue(String(optionValue));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (onChange) {
|
|
257
|
+
const event = {
|
|
258
|
+
target: {
|
|
259
|
+
value: String(optionValue),
|
|
260
|
+
name,
|
|
261
|
+
type,
|
|
262
|
+
},
|
|
263
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
264
|
+
onChange(event);
|
|
265
|
+
}
|
|
266
|
+
setShowMenu(false);
|
|
267
|
+
setIsFocused(false);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Close menu on outside click
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!showMenu) return;
|
|
273
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
274
|
+
if (
|
|
275
|
+
textFieldRef.current &&
|
|
276
|
+
!textFieldRef.current.contains(event.target as Node) &&
|
|
277
|
+
menuRef.current &&
|
|
278
|
+
!menuRef.current.contains(event.target as Node)
|
|
279
|
+
) {
|
|
280
|
+
setShowMenu(false);
|
|
281
|
+
setIsFocused(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
285
|
+
return () => {
|
|
286
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
287
|
+
};
|
|
288
|
+
}, [showMenu]);
|
|
289
|
+
|
|
290
|
+
const effectiveTrailingIcon = useMemo(() => {
|
|
291
|
+
if (trailingIcon) return trailingIcon;
|
|
292
|
+
if (isDateInput) return faCalendarDays;
|
|
293
|
+
if (isSelectInput) return showMenu ? faChevronUp : faChevronDown;
|
|
294
|
+
return undefined;
|
|
295
|
+
}, [trailingIcon, isDateInput, isSelectInput, showMenu]);
|
|
296
|
+
|
|
297
|
+
// Enhance styles for date input or select
|
|
298
|
+
const inputSpecialClass =
|
|
299
|
+
isDateInput || isSelectInput
|
|
300
|
+
? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer selection:bg-transparent'
|
|
301
|
+
: '';
|
|
226
302
|
|
|
227
303
|
const styles = useTextFieldStyle({
|
|
228
304
|
showSupportingText: hasSupportingText,
|
|
@@ -241,25 +317,37 @@ export const TextField = ({
|
|
|
241
317
|
trailingIcon: effectiveTrailingIcon,
|
|
242
318
|
variant,
|
|
243
319
|
errorText,
|
|
244
|
-
value: String(
|
|
320
|
+
value: String(displayValue),
|
|
245
321
|
suffix,
|
|
246
322
|
multiline,
|
|
247
323
|
});
|
|
248
324
|
|
|
249
325
|
const TextComponent = multiline ? TextareaAutosize : 'input';
|
|
250
|
-
|
|
326
|
+
// For select, we want the input to be readOnly but still focusable?
|
|
327
|
+
// Actually, for better UX, standard select inputs are often readOnly text fields.
|
|
328
|
+
const textComponentProps = multiline
|
|
329
|
+
? {}
|
|
330
|
+
: {
|
|
331
|
+
type: isSelectInput ? 'text' : type,
|
|
332
|
+
readOnly: isSelectInput,
|
|
333
|
+
};
|
|
251
334
|
|
|
252
335
|
const isFloating =
|
|
253
336
|
isFocused ||
|
|
254
337
|
(typeof value === 'string' && value.length > 0) ||
|
|
255
|
-
type == 'date'
|
|
338
|
+
type == 'date' ||
|
|
339
|
+
(isSelectInput && showMenu);
|
|
340
|
+
|
|
256
341
|
const showLegend = isFloating && variant === 'outlined';
|
|
257
342
|
const showLabel = !showLegend;
|
|
258
343
|
|
|
259
344
|
return (
|
|
260
345
|
<div ref={textFieldRef} className={styles.textField} style={style}>
|
|
261
346
|
<fieldset
|
|
262
|
-
onClick={
|
|
347
|
+
onClick={() => {
|
|
348
|
+
if (isSelectInput) handleSelectToggle();
|
|
349
|
+
else focusInput();
|
|
350
|
+
}}
|
|
263
351
|
className={styles.content}
|
|
264
352
|
role="presentation"
|
|
265
353
|
>
|
|
@@ -329,14 +417,19 @@ export const TextField = ({
|
|
|
329
417
|
<TextComponent
|
|
330
418
|
{...(restProps as any)}
|
|
331
419
|
ref={inputRef as any}
|
|
332
|
-
value={
|
|
420
|
+
value={displayValue} // Use displayValue for select
|
|
333
421
|
onChange={handleChange}
|
|
334
|
-
className={classNames(styles.input,
|
|
422
|
+
className={classNames(styles.input, inputSpecialClass)}
|
|
335
423
|
id={id}
|
|
336
424
|
name={name}
|
|
337
425
|
placeholder={isFocused ? (placeholder ?? undefined) : ''}
|
|
338
|
-
onFocus={() =>
|
|
339
|
-
|
|
426
|
+
onFocus={() => {
|
|
427
|
+
if(!isSelectInput) setIsFocused(true)
|
|
428
|
+
}}
|
|
429
|
+
onBlur={() => {
|
|
430
|
+
// For select, we manage focus manually with menu state usually
|
|
431
|
+
if(!isSelectInput) setIsFocused(false)
|
|
432
|
+
}}
|
|
340
433
|
disabled={disabled}
|
|
341
434
|
autoComplete={autoComplete}
|
|
342
435
|
aria-invalid={!!errorText?.length}
|
|
@@ -355,10 +448,11 @@ export const TextField = ({
|
|
|
355
448
|
onClick={(event) => {
|
|
356
449
|
event.stopPropagation();
|
|
357
450
|
if (isDateInput) handleDatePickerToggle();
|
|
451
|
+
if (isSelectInput) handleSelectToggle();
|
|
358
452
|
}}
|
|
359
453
|
className={classNames(
|
|
360
454
|
styles.trailingIcon,
|
|
361
|
-
isDateInput && 'cursor-pointer',
|
|
455
|
+
(isDateInput || isSelectInput) && 'cursor-pointer',
|
|
362
456
|
)}
|
|
363
457
|
>
|
|
364
458
|
<div className="flex items-center justify-center w-full h-full">
|
|
@@ -435,6 +529,40 @@ export const TextField = ({
|
|
|
435
529
|
</AnchorPositioner>
|
|
436
530
|
</>
|
|
437
531
|
)}
|
|
532
|
+
|
|
533
|
+
{isSelectInput && showMenu && (
|
|
534
|
+
<AnchorPositioner anchorRef={textFieldRef} position="bottom" style={{ width: textFieldRef.current?.offsetWidth }}>
|
|
535
|
+
<div ref={menuRef}>
|
|
536
|
+
<Menu
|
|
537
|
+
selected={value}
|
|
538
|
+
onItemSelect={handleSelectOption}
|
|
539
|
+
>
|
|
540
|
+
{children}
|
|
541
|
+
{!children && options?.map((opt, i) => {
|
|
542
|
+
if (opt.type === 'divider') {
|
|
543
|
+
return <Divider key={i} className="my-1" />
|
|
544
|
+
}
|
|
545
|
+
if (opt.type === 'headline') {
|
|
546
|
+
return <MenuHeadline key={i} label={opt.label} />
|
|
547
|
+
}
|
|
548
|
+
return (
|
|
549
|
+
<MenuItem
|
|
550
|
+
key={opt.value ?? i}
|
|
551
|
+
value={opt.value ?? ''}
|
|
552
|
+
label={opt.label}
|
|
553
|
+
leadingIcon={opt.leadingIcon}
|
|
554
|
+
trailingIcon={opt.trailingIcon}
|
|
555
|
+
disabled={opt.disabled}
|
|
556
|
+
>
|
|
557
|
+
{opt.label}
|
|
558
|
+
</MenuItem>
|
|
559
|
+
)
|
|
560
|
+
})}
|
|
561
|
+
</Menu>
|
|
562
|
+
</div>
|
|
563
|
+
</AnchorPositioner>
|
|
564
|
+
)}
|
|
438
565
|
</div>
|
|
439
566
|
);
|
|
440
567
|
};
|
|
568
|
+
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
|
|
1
2
|
export * from './AnchorPositioner';
|
|
2
3
|
export * from './Button';
|
|
3
4
|
export * from './Card';
|
|
5
|
+
export * from './Card';
|
|
4
6
|
export * from './Carousel';
|
|
5
7
|
export * from './CarouselItem';
|
|
6
|
-
export * from './CarouselItem';
|
|
7
8
|
export * from './Checkbox';
|
|
8
9
|
export * from './Chip';
|
|
10
|
+
export * from './ContextMenu';
|
|
9
11
|
export * from './Chips';
|
|
10
12
|
export * from './Divider';
|
|
11
13
|
export * from './Fab';
|
|
12
14
|
export * from './FabMenu';
|
|
13
15
|
export * from './IconButton';
|
|
14
|
-
export * from './
|
|
16
|
+
export * from './Menu';
|
|
17
|
+
export * from './MenuItem';
|
|
18
|
+
export * from './MenuGroup';
|
|
19
|
+
export * from './MenuHeadline';
|
|
15
20
|
export * from './ProgressIndicator';
|
|
16
21
|
export * from './Slider';
|
|
17
22
|
export * from './SideSheet';
|
package/src/lib/icon/icon.tsx
CHANGED
|
@@ -25,14 +25,12 @@ export const Icon: React.FC<Props> = ({
|
|
|
25
25
|
}) => {
|
|
26
26
|
// Si c'est une chaîne de caractères (SVG raw)
|
|
27
27
|
if (typeof icon === 'string') {
|
|
28
|
-
// Modifier la couleur du SVG en remplaçant les attributs fill/stroke
|
|
29
28
|
let svgContent = icon;
|
|
29
|
+
|
|
30
|
+
let additionalAttrs = ' width="100%" height="100%"';
|
|
31
|
+
|
|
30
32
|
if (colors[0]) {
|
|
31
|
-
|
|
32
|
-
svgContent = svgContent.replace(
|
|
33
|
-
/<svg([^>]*)>/,
|
|
34
|
-
`<svg$1 fill="${colors[0]}" color="${colors[0]}">`,
|
|
35
|
-
);
|
|
33
|
+
additionalAttrs += ` fill="${colors[0]}" color="${colors[0]}"`;
|
|
36
34
|
// Remplacer les paths existants pour utiliser currentColor
|
|
37
35
|
svgContent = svgContent.replace(
|
|
38
36
|
/<path([^>]*?)>/g,
|
|
@@ -40,6 +38,12 @@ export const Icon: React.FC<Props> = ({
|
|
|
40
38
|
);
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
// Injecter les attributs (taille + couleur éventuelle) dans la balise <svg>
|
|
42
|
+
svgContent = svgContent.replace(
|
|
43
|
+
/<svg([^>]*)>/,
|
|
44
|
+
`<svg$1${additionalAttrs}>`,
|
|
45
|
+
);
|
|
46
|
+
|
|
43
47
|
return (
|
|
44
48
|
<div
|
|
45
49
|
className={classNames(
|
|
@@ -9,6 +9,7 @@ export * from './divider.interface';
|
|
|
9
9
|
export * from './fab.interface';
|
|
10
10
|
export * from './fab-menu.interface';
|
|
11
11
|
export * from './icon-button.interface';
|
|
12
|
+
export * from './menu.interface';
|
|
12
13
|
export * from './progress-indicator.interface';
|
|
13
14
|
export * from './side-sheet.interface';
|
|
14
15
|
export * from './slider.interface';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MenuGroupProps {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
className?: string;
|
|
4
|
+
variant?: 'standard' | 'vibrant';
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MenuGroupInterface {
|
|
9
|
+
type: 'div';
|
|
10
|
+
props: MenuGroupProps;
|
|
11
|
+
states: object;
|
|
12
|
+
elements: ['menuGroup', 'groupLabel'];
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ComponentInterface } from '../utils/component';
|
|
2
|
+
|
|
3
|
+
export interface MenuItemInterface extends ComponentInterface {
|
|
4
|
+
value: string | number;
|
|
5
|
+
label?: string;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
leadingIcon?: any;
|
|
8
|
+
trailingIcon?: any;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
selected?: boolean; // Injected by parent
|
|
11
|
+
variant?: 'standard' | 'vibrant'; // Injected by parent
|
|
12
|
+
onClick?: (e?: React.MouseEvent) => void;
|
|
13
|
+
// ComponentInterface implementation
|
|
14
|
+
type: 'div';
|
|
15
|
+
props: {
|
|
16
|
+
label?: string;
|
|
17
|
+
value: string | number;
|
|
18
|
+
leadingIcon?: any;
|
|
19
|
+
trailingIcon?: any;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
selected?: boolean;
|
|
22
|
+
variant?: 'standard' | 'vibrant';
|
|
23
|
+
onItemSelect?: (value: string | number) => void; // Injected
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
states: Record<string, any>;
|
|
27
|
+
elements: [
|
|
28
|
+
'menuItem',
|
|
29
|
+
'selectedItem',
|
|
30
|
+
'itemLabel',
|
|
31
|
+
'itemIcon',
|
|
32
|
+
'leadingIcon',
|
|
33
|
+
'trailingIcon',
|
|
34
|
+
];
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type MenuStates = Record<string, any>;
|
|
2
|
+
|
|
3
|
+
export interface MenuProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
selected?: string | number | (string | number)[];
|
|
6
|
+
onItemSelect?: (value: string | number) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
variant?: 'standard' | 'vibrant';
|
|
9
|
+
// options prop REMOVED as requested by user ("options passed as children")
|
|
10
|
+
// However, for backward compat or data-driven, one might want it, but I will strictly follow "options as children"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MenuInterface {
|
|
14
|
+
type: 'div';
|
|
15
|
+
props: MenuProps;
|
|
16
|
+
states: {
|
|
17
|
+
hasGroups: boolean;
|
|
18
|
+
};
|
|
19
|
+
elements: ['menu'];
|
|
20
|
+
}
|
|
@@ -22,7 +22,15 @@ type Props = {
|
|
|
22
22
|
id?: string;
|
|
23
23
|
style?: React.CSSProperties;
|
|
24
24
|
variant?: TextFieldVariant;
|
|
25
|
-
|
|
25
|
+
options?: Array<{
|
|
26
|
+
label: string;
|
|
27
|
+
value: string | number;
|
|
28
|
+
leadingIcon?: any;
|
|
29
|
+
trailingIcon?: any;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
type?: 'divider' | 'headline';
|
|
32
|
+
}>;
|
|
33
|
+
type?: 'text' | 'password' | 'number' | 'date' | 'select';
|
|
26
34
|
autoComplete?: 'on' | 'off' | string;
|
|
27
35
|
autoFocus?: boolean;
|
|
28
36
|
multiline?: boolean;
|
package/src/lib/styles/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export * from './divider.style';
|
|
|
9
9
|
export * from './fab.style';
|
|
10
10
|
export * from './fab-menu.style';
|
|
11
11
|
export * from './icon-button.style';
|
|
12
|
+
export * from './menu.style';
|
|
13
|
+
export * from './menu-group.style';
|
|
12
14
|
export * from './progress-indicator.style';
|
|
13
15
|
export * from './side-sheet.style';
|
|
14
16
|
export * from './slider.style';
|
|
@@ -20,3 +22,4 @@ export * from './tab-panels.style';
|
|
|
20
22
|
export * from './text-field.style';
|
|
21
23
|
export * from './tooltip.style';
|
|
22
24
|
export { useButtonStyle } from './button.style';
|
|
25
|
+
export { useMenuStyle } from './menu.style';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClassNameComponent,
|
|
3
|
+
classNames,
|
|
4
|
+
createUseClassNames,
|
|
5
|
+
defaultClassNames,
|
|
6
|
+
} from '../utils';
|
|
7
|
+
import { MenuGroupInterface } from '../interfaces/menu-group.interface';
|
|
8
|
+
|
|
9
|
+
const menuGroupConfig: ClassNameComponent<MenuGroupInterface> = ({
|
|
10
|
+
variant,
|
|
11
|
+
}) => ({
|
|
12
|
+
menuGroup: classNames(
|
|
13
|
+
'flex flex-col gap-0.5 mb-0.5 last:mb-0',
|
|
14
|
+
'rounded-lg py-0.5 px-1 shadow-2 first:rounded-t-2xl last:rounded-b-2xl',
|
|
15
|
+
{
|
|
16
|
+
'bg-surface-container': variant === 'standard',
|
|
17
|
+
'bg-tertiary-container text-on-tertiary-container': variant === 'vibrant',
|
|
18
|
+
},
|
|
19
|
+
),
|
|
20
|
+
groupLabel: classNames('px-3 pt-2 text-label-small tracking-wide ', {
|
|
21
|
+
'text-on-surface-variant': variant === 'standard',
|
|
22
|
+
'text-on-tertiary-container opacity-80': variant === 'vibrant',
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const menuGroupStyle = defaultClassNames<MenuGroupInterface>(
|
|
27
|
+
'menuGroup',
|
|
28
|
+
menuGroupConfig,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const useMenuGroupStyle = createUseClassNames<MenuGroupInterface>(
|
|
32
|
+
'menuGroup',
|
|
33
|
+
menuGroupConfig,
|
|
34
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import { type ClassNameComponent, classNames, defaultClassNames, createUseClassNames } from '../utils';
|
|
3
|
+
|
|
4
|
+
export interface MenuHeadlineInterface {
|
|
5
|
+
label?: string;
|
|
6
|
+
variant?: 'standard' | 'vibrant';
|
|
7
|
+
type: 'div';
|
|
8
|
+
props: { label?: string; variant?: 'standard' | 'vibrant' };
|
|
9
|
+
states: Record<string, any>;
|
|
10
|
+
elements: ['headline'];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const menuHeadlineConfig: ClassNameComponent<MenuHeadlineInterface> = ({ props }) => ({
|
|
14
|
+
headline: classNames('px-3 py-1 text-label-small opacity-60 mt-1', {
|
|
15
|
+
'text-on-surface-variant': !props?.variant || props.variant === 'standard',
|
|
16
|
+
// Vibrant treatment if different
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const useMenuHeadlineStyle = createUseClassNames<MenuHeadlineInterface>('menu-headline', menuHeadlineConfig);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClassNameComponent,
|
|
3
|
+
classNames,
|
|
4
|
+
createUseClassNames,
|
|
5
|
+
defaultClassNames,
|
|
6
|
+
} from '../utils';
|
|
7
|
+
import { MenuItemInterface } from '../interfaces/menu-item.interface';
|
|
8
|
+
|
|
9
|
+
const menuItemConfig: ClassNameComponent<MenuItemInterface> = ({
|
|
10
|
+
variant,
|
|
11
|
+
disabled,
|
|
12
|
+
selected,
|
|
13
|
+
}) => ({
|
|
14
|
+
menuItem: classNames(
|
|
15
|
+
'group/menu-item overflow-hidden flex items-center h-12 px-3 cursor-pointer outline-none select-none shrink-0 ',
|
|
16
|
+
'text-label-large',
|
|
17
|
+
'transition-colors duration-200',
|
|
18
|
+
{
|
|
19
|
+
'rounded-sm': selected,
|
|
20
|
+
'rounded-xl': !selected,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
'text-on-surface': !variant || variant === 'standard',
|
|
24
|
+
// 'hover:bg-on-surface/[0.08] focus:bg-on-surface/[0.12]': !props?.variant || props.variant === 'standard', // Handled by State
|
|
25
|
+
// 'hover:bg-on-tertiary-container/[0.08] focus:bg-on-tertiary-container/[0.12]': props?.variant === 'vibrant', // Handled by State
|
|
26
|
+
'opacity-38 pointer-events-none': disabled,
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
selectedItem: classNames(
|
|
30
|
+
'bg-secondary-container text-on-secondary-container',
|
|
31
|
+
// 'hover:bg-secondary-container/[0.8]',
|
|
32
|
+
'[&_.menu-item-icon]:text-inherit',
|
|
33
|
+
{
|
|
34
|
+
// For vibrant, selected state
|
|
35
|
+
'!bg-on-tertiary-container/[0.12]': variant === 'vibrant',
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
itemLabel: classNames('flex-1 truncate'),
|
|
39
|
+
itemIcon: classNames(
|
|
40
|
+
'w-6 h-6 flex items-center justify-center menu-item-icon',
|
|
41
|
+
),
|
|
42
|
+
leadingIcon: classNames('mr-3'),
|
|
43
|
+
trailingIcon: classNames('ml-3'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const menuItemStyle = defaultClassNames<MenuItemInterface>(
|
|
47
|
+
'menuItem',
|
|
48
|
+
menuItemConfig,
|
|
49
|
+
);
|
|
50
|
+
export const useMenuItemStyle = createUseClassNames<MenuItemInterface>(
|
|
51
|
+
'menuItem',
|
|
52
|
+
menuItemConfig,
|
|
53
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClassNameComponent,
|
|
3
|
+
classNames,
|
|
4
|
+
createUseClassNames,
|
|
5
|
+
defaultClassNames,
|
|
6
|
+
} from '../utils';
|
|
7
|
+
import { MenuInterface } from '../interfaces';
|
|
8
|
+
|
|
9
|
+
const menuConfig: ClassNameComponent<MenuInterface> = ({
|
|
10
|
+
variant,
|
|
11
|
+
hasGroups,
|
|
12
|
+
}) => ({
|
|
13
|
+
menu: classNames(
|
|
14
|
+
'z-50 min-w-[112px] max-w-[280px] max-h-[300px] ',
|
|
15
|
+
'flex flex-col',
|
|
16
|
+
{ 'overflow-y-auto': !hasGroups },
|
|
17
|
+
{
|
|
18
|
+
'bg-surface-container': !variant || variant === 'standard',
|
|
19
|
+
// Vibrant uses tertiary-container (approximated) or just colored surface
|
|
20
|
+
'bg-tertiary-container text-on-tertiary-container': variant === 'vibrant',
|
|
21
|
+
'py-0.5 shadow-2 px-1 rounded-2xl': !hasGroups,
|
|
22
|
+
'bg-transparent ': hasGroups,
|
|
23
|
+
},
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const menuStyle = defaultClassNames<MenuInterface>('menu', menuConfig);
|
|
28
|
+
|
|
29
|
+
export const useMenuStyle = createUseClassNames<MenuInterface>(
|
|
30
|
+
'menu',
|
|
31
|
+
menuConfig,
|
|
32
|
+
);
|