@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +2967 -2553
  4. package/dist/lib/components/AnchorPositioner.d.ts +2 -1
  5. package/dist/lib/components/AnchorPositioner.d.ts.map +1 -1
  6. package/dist/lib/components/ContextMenu.d.ts +15 -0
  7. package/dist/lib/components/ContextMenu.d.ts.map +1 -0
  8. package/dist/lib/components/Menu.d.ts +16 -0
  9. package/dist/lib/components/Menu.d.ts.map +1 -0
  10. package/dist/lib/components/MenuGroup.d.ts +12 -0
  11. package/dist/lib/components/MenuGroup.d.ts.map +1 -0
  12. package/dist/lib/components/MenuHeadline.d.ts +7 -0
  13. package/dist/lib/components/MenuHeadline.d.ts.map +1 -0
  14. package/dist/lib/components/MenuItem.d.ts +9 -0
  15. package/dist/lib/components/MenuItem.d.ts.map +1 -0
  16. package/dist/lib/components/TextField.d.ts +5 -1
  17. package/dist/lib/components/TextField.d.ts.map +1 -1
  18. package/dist/lib/components/index.d.ts +6 -2
  19. package/dist/lib/components/index.d.ts.map +1 -1
  20. package/dist/lib/interfaces/index.d.ts +1 -0
  21. package/dist/lib/interfaces/index.d.ts.map +1 -1
  22. package/dist/lib/interfaces/menu-group.interface.d.ts +13 -0
  23. package/dist/lib/interfaces/menu-group.interface.d.ts.map +1 -0
  24. package/dist/lib/interfaces/menu-item.interface.d.ts +34 -0
  25. package/dist/lib/interfaces/menu-item.interface.d.ts.map +1 -0
  26. package/dist/lib/interfaces/menu.interface.d.ts +17 -0
  27. package/dist/lib/interfaces/menu.interface.d.ts.map +1 -0
  28. package/dist/lib/interfaces/text-field.interface.d.ts +9 -1
  29. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  30. package/dist/lib/styles/index.d.ts +3 -0
  31. package/dist/lib/styles/index.d.ts.map +1 -1
  32. package/dist/lib/styles/menu-group.style.d.ts +14 -0
  33. package/dist/lib/styles/menu-group.style.d.ts.map +1 -0
  34. package/dist/lib/styles/menu-headline.style.d.ts +19 -0
  35. package/dist/lib/styles/menu-headline.style.d.ts.map +1 -0
  36. package/dist/lib/styles/menu-item.style.d.ts +39 -0
  37. package/dist/lib/styles/menu-item.style.d.ts.map +1 -0
  38. package/dist/lib/styles/menu.style.d.ts +19 -0
  39. package/dist/lib/styles/menu.style.d.ts.map +1 -0
  40. package/dist/lib/styles/text-field.style.d.ts +19 -2
  41. package/dist/lib/styles/text-field.style.d.ts.map +1 -1
  42. package/package.json +3 -3
  43. package/src/lib/components/AnchorPositioner.tsx +61 -18
  44. package/src/lib/components/ContextMenu.tsx +111 -0
  45. package/src/lib/components/Menu.tsx +113 -0
  46. package/src/lib/components/MenuGroup.tsx +34 -0
  47. package/src/lib/components/MenuHeadline.tsx +9 -0
  48. package/src/lib/components/MenuItem.tsx +197 -0
  49. package/src/lib/components/TextField.tsx +206 -42
  50. package/src/lib/components/index.ts +7 -2
  51. package/src/lib/interfaces/index.ts +1 -0
  52. package/src/lib/interfaces/menu-group.interface.ts +13 -0
  53. package/src/lib/interfaces/menu-item.interface.ts +35 -0
  54. package/src/lib/interfaces/menu.interface.ts +20 -0
  55. package/src/lib/interfaces/text-field.interface.ts +9 -1
  56. package/src/lib/styles/index.ts +3 -0
  57. package/src/lib/styles/menu-group.style.ts +34 -0
  58. package/src/lib/styles/menu-headline.style.ts +20 -0
  59. package/src/lib/styles/menu-item.style.ts +53 -0
  60. package/src/lib/styles/menu.style.ts +32 -0
@@ -0,0 +1,111 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { AnchorPositioner } from './AnchorPositioner';
3
+ import { Menu } from './Menu';
4
+ // import { MenuProps } from '../interfaces/menu.interface'; // MenuProps is not exported from interface file usually, check file content
5
+ import { MenuInterface } from '../interfaces';
6
+ import { ReactProps } from '../utils';
7
+
8
+ // MenuInterface has props: MenuProps.
9
+ // But MenuProps might not be exported directly from the package index, so accessing it via MenuInterface['props'] is safer if we can't import it.
10
+ // Actually checking Step 1271, MenuProps IS exported.
11
+
12
+ export type ContextMenuProps = {
13
+ props: { trigger: React.ReactNode } & MenuInterface['props'];
14
+ type: 'div';
15
+ states: {
16
+ hasGroups: boolean;
17
+ };
18
+ elements: [''];
19
+ };
20
+
21
+ export const ContextMenu = ({
22
+ trigger,
23
+ children,
24
+ onItemSelect,
25
+ ...menuProps
26
+ }: ReactProps<ContextMenuProps>) => {
27
+ const [contextMenu, setContextMenu] = useState<{
28
+ mouseX: number;
29
+ mouseY: number;
30
+ } | null>(null);
31
+ const anchorRef = useRef<HTMLDivElement>(null);
32
+
33
+ const handleContextMenu = (event: React.MouseEvent) => {
34
+ event.preventDefault();
35
+ setContextMenu({
36
+ mouseX: event.clientX,
37
+ mouseY: event.clientY,
38
+ });
39
+ };
40
+
41
+ const handleClose = () => {
42
+ setContextMenu(null);
43
+ };
44
+
45
+ const handleSelect = (val: string | number) => {
46
+ handleClose();
47
+ onItemSelect?.(val);
48
+ };
49
+
50
+ useEffect(() => {
51
+ if (!contextMenu) return;
52
+ const handleOutsideInteraction = () => setContextMenu(null);
53
+ window.addEventListener('click', handleOutsideInteraction);
54
+ window.addEventListener('scroll', handleOutsideInteraction, true);
55
+
56
+ return () => {
57
+ window.removeEventListener('click', handleOutsideInteraction);
58
+ window.removeEventListener('scroll', handleOutsideInteraction, true);
59
+ };
60
+ }, [contextMenu]);
61
+
62
+ // Clone trigger if valid element to attach onContextMenu, otherwise wrap
63
+ const triggerElement = React.isValidElement(trigger) ? (
64
+ React.cloneElement(
65
+ trigger as React.ReactElement,
66
+ {
67
+ onContextMenu: (e: React.MouseEvent) => {
68
+ handleContextMenu(e);
69
+ // Call original handler if exists
70
+ (trigger as React.ReactElement).props.onContextMenu?.(e);
71
+ },
72
+ } as any,
73
+ )
74
+ ) : (
75
+ <div onContextMenu={handleContextMenu} className="inline-block">
76
+ {trigger}
77
+ </div>
78
+ );
79
+
80
+ return (
81
+ <>
82
+ {triggerElement}
83
+
84
+ {/* Invisible anchor element positioned at cursor */}
85
+ <div
86
+ ref={anchorRef}
87
+ style={{
88
+ position: 'fixed',
89
+ top: contextMenu?.mouseY ?? 0,
90
+ left: contextMenu?.mouseX ?? 0,
91
+ width: 1,
92
+ height: 1,
93
+ pointerEvents: 'none',
94
+ visibility: 'hidden',
95
+ }}
96
+ />
97
+
98
+ {contextMenu && (
99
+ <AnchorPositioner
100
+ anchorRef={anchorRef}
101
+ position="bottom right"
102
+ onClick={(e) => e.stopPropagation()}
103
+ >
104
+ <Menu onItemSelect={handleSelect} {...menuProps}>
105
+ {children}
106
+ </Menu>
107
+ </AnchorPositioner>
108
+ )}
109
+ </>
110
+ );
111
+ };
@@ -0,0 +1,113 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { MenuInterface } from '../interfaces/menu.interface';
3
+ import { useMenuStyle } from '../styles/menu.style';
4
+ import { classNames } from '../utils';
5
+ import { ReactProps } from '../utils/component';
6
+ import { MenuItem } from './MenuItem';
7
+ import { Divider } from './Divider';
8
+ import { MenuHeadline } from './MenuHeadline';
9
+ import { MenuGroup } from './MenuGroup';
10
+
11
+ /**
12
+ * Menu displays a list of choices on a temporary surface.
13
+ * @status beta
14
+ * @category Selection
15
+ * @limitations
16
+ * - Don’t use MenuGroup in scrollable menus
17
+ * @devx
18
+ * - Used internally by `TextField` for `type="select"`.
19
+ * - Supports keyboard navigation and auto-scrolling to selected item.
20
+ * @a11y
21
+ * - `role="listbox"` with `aria-selected` management.
22
+ */
23
+ export const Menu = ({
24
+ children,
25
+ selected,
26
+ onItemSelect,
27
+ className,
28
+ variant = 'standard',
29
+ ...restProps
30
+ }: ReactProps<MenuInterface>) => {
31
+ /* pass restProps to include key such as variant */
32
+ const hasGroups = React.Children.toArray(children).some(
33
+ (child) => React.isValidElement(child) && child.type === MenuGroup,
34
+ );
35
+ const styles = useMenuStyle({
36
+ children,
37
+ selected,
38
+ onItemSelect,
39
+ className,
40
+ variant,
41
+ hasGroups,
42
+ ...restProps,
43
+ });
44
+
45
+ const listRef = useRef<HTMLDivElement>(null);
46
+
47
+ // Scroll to selected item on open
48
+ useEffect(() => {
49
+ if (listRef.current) {
50
+ const selectedEl = listRef.current.querySelector(
51
+ '[aria-selected="true"]',
52
+ ) as HTMLElement;
53
+ if (selectedEl) {
54
+ selectedEl.scrollIntoView({ block: 'nearest' });
55
+ }
56
+ }
57
+ }, []);
58
+
59
+ const renderChildren = (nodes: React.ReactNode): React.ReactNode => {
60
+ return React.Children.map(nodes, (child) => {
61
+ if (!React.isValidElement(child)) return child;
62
+
63
+ // Handle MenuGroup: add surface styles if grouped
64
+ if (child.type === MenuGroup) {
65
+ const groupChildren = renderChildren((child.props as any).children);
66
+ return React.cloneElement(child, {
67
+ children: groupChildren,
68
+ variant: variant,
69
+ } as any);
70
+ }
71
+
72
+ if (child.type === MenuItem) {
73
+ const childValue = (child.props as any).value;
74
+ const isSelected = Array.isArray(selected)
75
+ ? selected.includes(childValue)
76
+ : selected === childValue;
77
+
78
+ return React.cloneElement(child, {
79
+ selected: isSelected,
80
+ variant: variant,
81
+ onItemSelect: (val: string | number) => {
82
+ onItemSelect?.(val);
83
+ },
84
+ } as any);
85
+ }
86
+
87
+ if (child.type === MenuHeadline) {
88
+ return React.cloneElement(child, {
89
+ variant: variant,
90
+ } as any);
91
+ }
92
+
93
+ if (child.type === Divider) {
94
+ return React.cloneElement(child, {
95
+ className: classNames('my-1', (child.props as any).className),
96
+ } as any);
97
+ }
98
+
99
+ return child;
100
+ });
101
+ };
102
+
103
+ return (
104
+ <div ref={listRef} className={styles.menu} role="listbox" {...restProps}>
105
+ {renderChildren(children)}
106
+ {React.Children.count(children) === 0 && (
107
+ <div className="px-4 py-3 text-on-surface-variant opacity-60 italic text-body-medium">
108
+ No options
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ };
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { ReactProps } from '../utils/component';
3
+ import { useMenuGroupStyle } from '../styles/menu-group.style';
4
+ import { MenuGroupInterface } from '../interfaces/menu-group.interface';
5
+
6
+ /**
7
+ * MenuGroup renders a group of menu items with persistent styling.
8
+ * It is primarily used to apply grouping logic or context to its children.
9
+ *
10
+ * @status beta
11
+ * @category Selection
12
+ */
13
+ export const MenuGroup = ({
14
+ children,
15
+ className,
16
+ variant,
17
+ label,
18
+ ...restProps
19
+ }: ReactProps<MenuGroupInterface> & React.HTMLAttributes<HTMLDivElement>) => {
20
+ const styles = useMenuGroupStyle({
21
+ children,
22
+ className,
23
+ variant,
24
+ label,
25
+ ...restProps,
26
+ });
27
+
28
+ return (
29
+ <div className={styles.menuGroup} role="group" {...restProps}>
30
+ {label && <div className={styles.groupLabel}>{label}</div>}
31
+ {children}
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1,9 @@
1
+
2
+ import React from 'react';
3
+ import { useMenuHeadlineStyle, MenuHeadlineInterface } from '../styles/menu-headline.style';
4
+ import { ReactProps } from '../utils';
5
+
6
+ export const MenuHeadline = ({ label, children, variant, className, ...restProps }: ReactProps<MenuHeadlineInterface> & { children?: React.ReactNode }) => {
7
+ const styles = useMenuHeadlineStyle({ variant, className });
8
+ return <div className={styles.headline} role="group" aria-label={label} {...restProps}>{children ?? label}</div>;
9
+ };
@@ -0,0 +1,197 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { Icon } from '../icon';
3
+ import { classNames, ReactProps } from '../utils';
4
+ import { MenuItemInterface } from '../interfaces/menu-item.interface';
5
+ import { useMenuItemStyle } from '../styles/menu-item.style';
6
+ import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
7
+ import { AnchorPositioner } from './AnchorPositioner';
8
+ import { State } from '../effects';
9
+
10
+ /**
11
+ * Single item within a Menu.
12
+ * @status beta
13
+ * @category Selection
14
+ */
15
+ export const MenuItem = ({
16
+ label,
17
+ children,
18
+ value,
19
+ leadingIcon,
20
+ trailingIcon,
21
+ disabled,
22
+ selected,
23
+ variant,
24
+ onClick,
25
+ onItemSelect, // Injected by Menu
26
+ className,
27
+ ...restProps
28
+ }: ReactProps<MenuItemInterface>) => {
29
+ /* Extract subMenu from children if present */
30
+ let subMenuElement: React.ReactNode = null;
31
+ const contentChildren: React.ReactNode[] = [];
32
+
33
+ React.Children.forEach(children, (child) => {
34
+ // Check for Menu component via name (development) or if it has displayName 'Menu'
35
+ // We accept a child that is valid element and looks like a Menu.
36
+ if (
37
+ React.isValidElement(child) &&
38
+ ((child.type as any)?.name === 'Menu' ||
39
+ (child.type as any)?.displayName === 'Menu')
40
+ ) {
41
+ subMenuElement = child;
42
+ } else {
43
+ contentChildren.push(child);
44
+ }
45
+ });
46
+
47
+ const labelContent = contentChildren.length > 0 ? contentChildren : label;
48
+
49
+ const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
50
+ const itemRef = useRef<HTMLDivElement>(null);
51
+ const closeTimerRef = useRef<any>(null);
52
+
53
+ const styles = useMenuItemStyle({
54
+ variant,
55
+ disabled,
56
+ selected,
57
+ className,
58
+ value,
59
+ });
60
+
61
+ const handleClick = (e: React.MouseEvent) => {
62
+ if (disabled) {
63
+ e.preventDefault();
64
+ return;
65
+ }
66
+
67
+ if (subMenuElement) {
68
+ e.stopPropagation();
69
+ return;
70
+ }
71
+
72
+ onClick?.(e);
73
+ if (onItemSelect) {
74
+ onItemSelect(value);
75
+ }
76
+ };
77
+
78
+ const handleMouseEnter = () => {
79
+ if (closeTimerRef.current) {
80
+ clearTimeout(closeTimerRef.current);
81
+ closeTimerRef.current = null;
82
+ }
83
+ if (!disabled && subMenuElement) {
84
+ setIsSubMenuOpen(true);
85
+ }
86
+ };
87
+
88
+ const handleMouseLeave = () => {
89
+ if (subMenuElement) {
90
+ closeTimerRef.current = setTimeout(() => {
91
+ setIsSubMenuOpen(false);
92
+ }, 150); // Delay to allow diagonal movement
93
+ }
94
+ };
95
+
96
+ React.useEffect(() => {
97
+ return () => {
98
+ if (closeTimerRef.current) {
99
+ clearTimeout(closeTimerRef.current);
100
+ }
101
+ };
102
+ }, []);
103
+
104
+ const effectiveTrailingIcon =
105
+ trailingIcon ?? (subMenuElement ? faChevronRight : undefined);
106
+
107
+ return (
108
+ <div
109
+ ref={itemRef}
110
+ className={styles.menuItem} // Added relative and overflow-hidden for State
111
+ onClick={handleClick}
112
+ onMouseEnter={handleMouseEnter}
113
+ onMouseLeave={handleMouseLeave}
114
+ role="option"
115
+ aria-haspopup={!!subMenuElement}
116
+ aria-expanded={isSubMenuOpen}
117
+ aria-selected={selected}
118
+ tabIndex={disabled ? -1 : 0}
119
+ onKeyDown={(e) => {
120
+ // ... key handlers ...
121
+ if (e.key === 'Enter' || e.key === ' ') {
122
+ e.preventDefault();
123
+ if (subMenuElement) {
124
+ setIsSubMenuOpen(!isSubMenuOpen);
125
+ } else {
126
+ handleClick(e as any);
127
+ }
128
+ }
129
+ if (e.key === 'ArrowRight' && subMenuElement) {
130
+ setIsSubMenuOpen(true);
131
+ e.stopPropagation();
132
+ }
133
+ if (e.key === 'ArrowLeft' && isSubMenuOpen) {
134
+ setIsSubMenuOpen(false);
135
+ e.stopPropagation();
136
+ }
137
+ }}
138
+ {...restProps}
139
+ >
140
+ <State
141
+ className="absolute inset-0 pointer-events-none"
142
+ colorName={classNames(
143
+ // Match text color for state layer usually
144
+ variant === 'vibrant' ? 'on-tertiary-container' : 'on-surface',
145
+ selected && 'on-secondary-container',
146
+ )}
147
+ stateClassName={'state-ripple-group-[menu-item]'}
148
+ />
149
+
150
+ {leadingIcon && (
151
+ <div
152
+ className={classNames(
153
+ styles.itemIcon,
154
+ styles.leadingIcon,
155
+ 'z-10 relative',
156
+ )}
157
+ >
158
+ {React.isValidElement(leadingIcon) ? (
159
+ leadingIcon
160
+ ) : (
161
+ <Icon icon={leadingIcon} />
162
+ )}
163
+ </div>
164
+ )}
165
+ <span className={classNames(styles.itemLabel, 'z-10 relative')}>
166
+ {labelContent}
167
+ </span>
168
+ {effectiveTrailingIcon && (
169
+ <div
170
+ className={classNames(
171
+ styles.itemIcon,
172
+ styles.trailingIcon,
173
+ 'z-10 relative',
174
+ )}
175
+ >
176
+ {React.isValidElement(effectiveTrailingIcon) ? (
177
+ effectiveTrailingIcon
178
+ ) : (
179
+ <Icon icon={effectiveTrailingIcon} />
180
+ )}
181
+ </div>
182
+ )}
183
+
184
+ {subMenuElement && isSubMenuOpen && (
185
+ <AnchorPositioner
186
+ anchorRef={itemRef}
187
+ position="inline-end span-block-end"
188
+ hoverOpen={true}
189
+ >
190
+ <div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
191
+ {subMenuElement}
192
+ </div>
193
+ </AnchorPositioner>
194
+ )}
195
+ </div>
196
+ );
197
+ };