@udixio/ui-react 2.9.22 → 2.9.24
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 +2967 -2553
- 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/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 +3 -3
- 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 +206 -42
- package/src/lib/components/index.ts +7 -2
- 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) {
|
|
@@ -134,20 +146,67 @@ export const TextField = ({
|
|
|
134
146
|
return null;
|
|
135
147
|
}, [value]);
|
|
136
148
|
|
|
137
|
-
const
|
|
149
|
+
const handleDatePickerToggle = () => {
|
|
138
150
|
if (disabled) return;
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
if (showDatePicker) {
|
|
152
|
+
setShowDatePicker(false);
|
|
153
|
+
} else {
|
|
154
|
+
setTempDate(initialDateValue);
|
|
155
|
+
setShowDatePicker(true);
|
|
156
|
+
}
|
|
141
157
|
};
|
|
142
158
|
|
|
143
159
|
useEffect(() => {
|
|
144
160
|
if (showDatePicker) {
|
|
145
161
|
setIsFocused(true);
|
|
146
|
-
} else {
|
|
162
|
+
} else if (!isSelectInput || !showMenu) {
|
|
147
163
|
setIsFocused(false);
|
|
148
164
|
}
|
|
149
165
|
}, [showDatePicker]);
|
|
150
166
|
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!showDatePicker) return;
|
|
169
|
+
|
|
170
|
+
const isInside = (target: Node | null) => {
|
|
171
|
+
if (!target) return false;
|
|
172
|
+
return (
|
|
173
|
+
textFieldRef.current?.contains(target) ||
|
|
174
|
+
datePickerRef.current?.contains(target)
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handlePointerDown = (e: PointerEvent) => {
|
|
179
|
+
const target = e.target as Node;
|
|
180
|
+
const inDatePicker = datePickerRef.current?.contains(target);
|
|
181
|
+
const inCalendarTrigger = calendarTriggerRef.current?.contains(target);
|
|
182
|
+
if (!inDatePicker && !inCalendarTrigger) {
|
|
183
|
+
setShowDatePicker(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleFocusIn = (e: FocusEvent) => {
|
|
188
|
+
if (!isInside(e.target as Node)) {
|
|
189
|
+
setShowDatePicker(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
194
|
+
if (e.key === 'Escape') {
|
|
195
|
+
setShowDatePicker(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
200
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
201
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
205
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
206
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
207
|
+
};
|
|
208
|
+
}, [showDatePicker]);
|
|
209
|
+
|
|
151
210
|
const handleDateConfirm = () => {
|
|
152
211
|
const newValue = tempDate ? tempDate.toLocaleDateString('en-CA') : '';
|
|
153
212
|
|
|
@@ -156,7 +215,6 @@ export const TextField = ({
|
|
|
156
215
|
}
|
|
157
216
|
|
|
158
217
|
if (onChange) {
|
|
159
|
-
// Create a synthetic event
|
|
160
218
|
const event = {
|
|
161
219
|
target: {
|
|
162
220
|
value: newValue,
|
|
@@ -169,13 +227,78 @@ export const TextField = ({
|
|
|
169
227
|
setShowDatePicker(false);
|
|
170
228
|
};
|
|
171
229
|
|
|
172
|
-
|
|
173
|
-
|
|
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]);
|
|
244
|
+
|
|
245
|
+
const handleSelectToggle = () => {
|
|
246
|
+
if (disabled) return;
|
|
247
|
+
setShowMenu(!showMenu);
|
|
248
|
+
setIsFocused(!showMenu);
|
|
249
|
+
};
|
|
174
250
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
: '';
|
|
179
302
|
|
|
180
303
|
const styles = useTextFieldStyle({
|
|
181
304
|
showSupportingText: hasSupportingText,
|
|
@@ -194,25 +317,37 @@ export const TextField = ({
|
|
|
194
317
|
trailingIcon: effectiveTrailingIcon,
|
|
195
318
|
variant,
|
|
196
319
|
errorText,
|
|
197
|
-
value: String(
|
|
320
|
+
value: String(displayValue),
|
|
198
321
|
suffix,
|
|
199
322
|
multiline,
|
|
200
323
|
});
|
|
201
324
|
|
|
202
325
|
const TextComponent = multiline ? TextareaAutosize : 'input';
|
|
203
|
-
|
|
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
|
+
};
|
|
204
334
|
|
|
205
335
|
const isFloating =
|
|
206
336
|
isFocused ||
|
|
207
337
|
(typeof value === 'string' && value.length > 0) ||
|
|
208
|
-
type == 'date'
|
|
338
|
+
type == 'date' ||
|
|
339
|
+
(isSelectInput && showMenu);
|
|
340
|
+
|
|
209
341
|
const showLegend = isFloating && variant === 'outlined';
|
|
210
342
|
const showLabel = !showLegend;
|
|
211
343
|
|
|
212
344
|
return (
|
|
213
345
|
<div ref={textFieldRef} className={styles.textField} style={style}>
|
|
214
346
|
<fieldset
|
|
215
|
-
onClick={
|
|
347
|
+
onClick={() => {
|
|
348
|
+
if (isSelectInput) handleSelectToggle();
|
|
349
|
+
else focusInput();
|
|
350
|
+
}}
|
|
216
351
|
className={styles.content}
|
|
217
352
|
role="presentation"
|
|
218
353
|
>
|
|
@@ -282,14 +417,19 @@ export const TextField = ({
|
|
|
282
417
|
<TextComponent
|
|
283
418
|
{...(restProps as any)}
|
|
284
419
|
ref={inputRef as any}
|
|
285
|
-
value={
|
|
420
|
+
value={displayValue} // Use displayValue for select
|
|
286
421
|
onChange={handleChange}
|
|
287
|
-
className={classNames(styles.input,
|
|
422
|
+
className={classNames(styles.input, inputSpecialClass)}
|
|
288
423
|
id={id}
|
|
289
424
|
name={name}
|
|
290
425
|
placeholder={isFocused ? (placeholder ?? undefined) : ''}
|
|
291
|
-
onFocus={() =>
|
|
292
|
-
|
|
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
|
+
}}
|
|
293
433
|
disabled={disabled}
|
|
294
434
|
autoComplete={autoComplete}
|
|
295
435
|
aria-invalid={!!errorText?.length}
|
|
@@ -307,11 +447,12 @@ export const TextField = ({
|
|
|
307
447
|
ref={isDateInput ? calendarTriggerRef : undefined}
|
|
308
448
|
onClick={(event) => {
|
|
309
449
|
event.stopPropagation();
|
|
310
|
-
if (isDateInput)
|
|
450
|
+
if (isDateInput) handleDatePickerToggle();
|
|
451
|
+
if (isSelectInput) handleSelectToggle();
|
|
311
452
|
}}
|
|
312
453
|
className={classNames(
|
|
313
454
|
styles.trailingIcon,
|
|
314
|
-
isDateInput && 'cursor-pointer',
|
|
455
|
+
(isDateInput || isSelectInput) && 'cursor-pointer',
|
|
315
456
|
)}
|
|
316
457
|
>
|
|
317
458
|
<div className="flex items-center justify-center w-full h-full">
|
|
@@ -358,18 +499,7 @@ export const TextField = ({
|
|
|
358
499
|
|
|
359
500
|
{isDateInput && showDatePicker && (
|
|
360
501
|
<>
|
|
361
|
-
<AnchorPositioner
|
|
362
|
-
onBlur={(e) => {
|
|
363
|
-
if (
|
|
364
|
-
datePickerRef.current &&
|
|
365
|
-
!datePickerRef.current?.contains(e.relatedTarget)
|
|
366
|
-
) {
|
|
367
|
-
setShowDatePicker(false);
|
|
368
|
-
}
|
|
369
|
-
}}
|
|
370
|
-
anchorRef={textFieldRef}
|
|
371
|
-
position="bottom"
|
|
372
|
-
>
|
|
502
|
+
<AnchorPositioner anchorRef={textFieldRef} position="bottom">
|
|
373
503
|
<div
|
|
374
504
|
ref={datePickerRef}
|
|
375
505
|
className="z-50 shadow-xl rounded-[28px] bg-surface-container-high overflow-hidden"
|
|
@@ -399,6 +529,40 @@ export const TextField = ({
|
|
|
399
529
|
</AnchorPositioner>
|
|
400
530
|
</>
|
|
401
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
|
+
)}
|
|
402
565
|
</div>
|
|
403
566
|
);
|
|
404
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';
|
|
@@ -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
|
+
);
|