@ews-admin/global-design-system 1.1.13 → 1.1.14
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/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts +22 -0
- package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts.map +1 -0
- package/dist/components/DropdownMultiSelect/index.d.ts +3 -0
- package/dist/components/DropdownMultiSelect/index.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +130 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +130 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +4 -1
- package/src/components/DropdownMultiSelect/DropdownMultiSelect.tsx +271 -0
- package/src/components/DropdownMultiSelect/index.ts +2 -0
- package/src/index.ts +3 -0
- package/src/utils/index.ts +1 -1
package/package.json
CHANGED
|
@@ -12,7 +12,8 @@ export interface ButtonProps
|
|
|
12
12
|
| "success"
|
|
13
13
|
| "warning"
|
|
14
14
|
| "error"
|
|
15
|
-
| "outline"
|
|
15
|
+
| "outline"
|
|
16
|
+
| "ghost";
|
|
16
17
|
/**
|
|
17
18
|
* Button size
|
|
18
19
|
*/
|
|
@@ -63,6 +64,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
63
64
|
error: "bg-ews-error text-white hover:bg-ews-error-hover",
|
|
64
65
|
outline:
|
|
65
66
|
"bg-transparent text-sm font-medium text-ews-primary hover:text-ews-primary/80",
|
|
67
|
+
ghost:
|
|
68
|
+
"border border-ews-primary text-ews-primary hover:bg-ews-primary hover:text-white disabled:border-gray-400 disabled:text-gray-400 focus:ring-2 focus:ring-offset-2 focus:ring-ews-primary",
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const sizes = {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Control, Controller, FieldValues, Path } from "react-hook-form";
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
import { Input } from "../Input";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMultiSelectProps<T extends FieldValues, TValue> {
|
|
7
|
+
options: { value: TValue; label: string }[];
|
|
8
|
+
name: Path<T>;
|
|
9
|
+
control?: Control<T>;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
onChange?: (value: TValue[]) => void;
|
|
12
|
+
value?: TValue[];
|
|
13
|
+
defaultValue?: TValue[];
|
|
14
|
+
onValidate?: (value: TValue[]) => boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DropdownMultiSelect = <
|
|
23
|
+
T extends FieldValues,
|
|
24
|
+
TValue extends string | number | object
|
|
25
|
+
>({
|
|
26
|
+
options,
|
|
27
|
+
name,
|
|
28
|
+
control,
|
|
29
|
+
placeholder = "Select options",
|
|
30
|
+
searchPlaceholder = "Search...",
|
|
31
|
+
onChange,
|
|
32
|
+
value: controlledValue,
|
|
33
|
+
defaultValue,
|
|
34
|
+
onValidate,
|
|
35
|
+
disabled = false,
|
|
36
|
+
error,
|
|
37
|
+
label,
|
|
38
|
+
className,
|
|
39
|
+
}: DropdownMultiSelectProps<T, TValue>) => {
|
|
40
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
41
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
42
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<
|
|
43
|
+
TValue[] | undefined
|
|
44
|
+
>(defaultValue);
|
|
45
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
const handleToggle = () => {
|
|
48
|
+
if (!disabled) {
|
|
49
|
+
setIsOpen(!isOpen);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
54
|
+
if (
|
|
55
|
+
dropdownRef.current &&
|
|
56
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
57
|
+
) {
|
|
58
|
+
setIsOpen(false);
|
|
59
|
+
setSearchTerm("");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
65
|
+
return () => {
|
|
66
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const removeAccents = (str: string) =>
|
|
71
|
+
str
|
|
72
|
+
.normalize("NFD")
|
|
73
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
74
|
+
.replace(/ç/g, "c")
|
|
75
|
+
.replace(/é|è|ê|ë/g, "e")
|
|
76
|
+
.replace(/à|á|â|ã|ä/g, "a")
|
|
77
|
+
.replace(/î|ï/g, "i")
|
|
78
|
+
.replace(/ô|ö/g, "o")
|
|
79
|
+
.replace(/ù|ú|û|ü/g, "u");
|
|
80
|
+
|
|
81
|
+
const getDisplayValue = (value: TValue) => {
|
|
82
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
83
|
+
return String(value);
|
|
84
|
+
}
|
|
85
|
+
return options.find((opt) => opt.value === value)?.label || "";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const filteredOptions = options.filter((option) =>
|
|
89
|
+
removeAccents(option.label.toLowerCase()).includes(
|
|
90
|
+
removeAccents(searchTerm.toLowerCase())
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const renderDropdown = ({
|
|
95
|
+
value = [] as TValue[],
|
|
96
|
+
onChange: fieldOnChange,
|
|
97
|
+
}: {
|
|
98
|
+
value: TValue[] | undefined;
|
|
99
|
+
onChange: (value: TValue[]) => void;
|
|
100
|
+
}) => (
|
|
101
|
+
<div className={cn("relative", className)} ref={dropdownRef}>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={handleToggle}
|
|
105
|
+
aria-label={name}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex w-full items-center justify-between rounded-md border border-ews-gray-300 bg-white px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-0 disabled:bg-ews-gray-50 disabled:text-ews-gray-500",
|
|
109
|
+
isOpen ? "rounded-b-none border-b-0" : "rounded-md",
|
|
110
|
+
error && "border-ews-error focus:ring-ews-error"
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<span className={cn("truncate", !value?.length && "text-ews-gray-500")}>
|
|
114
|
+
{value?.length > 0
|
|
115
|
+
? value.map((v) => getDisplayValue(v)).join(", ")
|
|
116
|
+
: placeholder}
|
|
117
|
+
</span>
|
|
118
|
+
<span
|
|
119
|
+
className={cn(
|
|
120
|
+
"ml-2 w-4 h-4 transition-transform transform",
|
|
121
|
+
isOpen ? "rotate-180" : "rotate-0"
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<svg
|
|
125
|
+
className="w-4 h-4"
|
|
126
|
+
fill="none"
|
|
127
|
+
stroke="currentColor"
|
|
128
|
+
viewBox="0 0 24 24"
|
|
129
|
+
>
|
|
130
|
+
<path
|
|
131
|
+
strokeLinecap="round"
|
|
132
|
+
strokeLinejoin="round"
|
|
133
|
+
strokeWidth={2}
|
|
134
|
+
d="M19 9l-7 7-7-7"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
</span>
|
|
138
|
+
</button>
|
|
139
|
+
|
|
140
|
+
{isOpen && (
|
|
141
|
+
<div className="absolute z-50 w-full bg-white rounded-b-md border border-t-0 shadow-lg border-ews-gray-300">
|
|
142
|
+
{/* Search Input */}
|
|
143
|
+
<div className="p-2 border-b border-ews-gray-200">
|
|
144
|
+
<Input
|
|
145
|
+
type="text"
|
|
146
|
+
placeholder={searchPlaceholder}
|
|
147
|
+
value={searchTerm}
|
|
148
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
149
|
+
className="p-0 border-0 shadow-none focus:ring-0"
|
|
150
|
+
size="sm"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Scrollable Options List */}
|
|
155
|
+
<div className="overflow-y-auto max-h-48">
|
|
156
|
+
{filteredOptions.length > 0 ? (
|
|
157
|
+
filteredOptions.map((option) => (
|
|
158
|
+
<div
|
|
159
|
+
key={getDisplayValue(option.value)}
|
|
160
|
+
className="flex items-center p-2 cursor-pointer hover:bg-ews-gray-100"
|
|
161
|
+
onClick={() => {
|
|
162
|
+
const currentValue = value ?? [];
|
|
163
|
+
const isSelected = currentValue.some(
|
|
164
|
+
(item: TValue) =>
|
|
165
|
+
JSON.stringify(item) === JSON.stringify(option.value)
|
|
166
|
+
);
|
|
167
|
+
const newValue = isSelected
|
|
168
|
+
? currentValue.filter(
|
|
169
|
+
(item: TValue) =>
|
|
170
|
+
JSON.stringify(item) !==
|
|
171
|
+
JSON.stringify(option.value)
|
|
172
|
+
)
|
|
173
|
+
: [...currentValue, option.value];
|
|
174
|
+
|
|
175
|
+
if (onValidate && !onValidate(newValue)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
fieldOnChange(newValue);
|
|
179
|
+
onChange?.(newValue);
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<input
|
|
183
|
+
type="checkbox"
|
|
184
|
+
checked={(value ?? []).some(
|
|
185
|
+
(item: TValue) =>
|
|
186
|
+
JSON.stringify(item) === JSON.stringify(option.value)
|
|
187
|
+
)}
|
|
188
|
+
onChange={(e) => {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
const currentValue = value ?? [];
|
|
191
|
+
const isSelected = currentValue.some(
|
|
192
|
+
(item: TValue) =>
|
|
193
|
+
JSON.stringify(item) === JSON.stringify(option.value)
|
|
194
|
+
);
|
|
195
|
+
const newValue = isSelected
|
|
196
|
+
? currentValue.filter(
|
|
197
|
+
(item: TValue) =>
|
|
198
|
+
JSON.stringify(item) !==
|
|
199
|
+
JSON.stringify(option.value)
|
|
200
|
+
)
|
|
201
|
+
: [...currentValue, option.value];
|
|
202
|
+
|
|
203
|
+
if (onValidate && !onValidate(newValue)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
fieldOnChange(newValue);
|
|
207
|
+
onChange?.(newValue);
|
|
208
|
+
}}
|
|
209
|
+
onClick={(e) => e.stopPropagation()}
|
|
210
|
+
className="mr-3 w-4 h-4 rounded border-ews-gray-300 text-ews-primary focus:ring-ews-primary"
|
|
211
|
+
/>
|
|
212
|
+
<label className="text-sm cursor-pointer text-ews-gray-700">
|
|
213
|
+
{option.label}
|
|
214
|
+
</label>
|
|
215
|
+
</div>
|
|
216
|
+
))
|
|
217
|
+
) : (
|
|
218
|
+
<div className="p-2 text-sm text-ews-gray-500">
|
|
219
|
+
No options found
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Render controlled version with react-hook-form
|
|
229
|
+
if (control) {
|
|
230
|
+
return (
|
|
231
|
+
<Controller
|
|
232
|
+
name={name}
|
|
233
|
+
control={control}
|
|
234
|
+
render={({ field: { value, onChange } }) => (
|
|
235
|
+
<div>
|
|
236
|
+
{label && (
|
|
237
|
+
<label className="block mb-1 text-sm font-medium text-ews-gray-700">
|
|
238
|
+
{label}
|
|
239
|
+
</label>
|
|
240
|
+
)}
|
|
241
|
+
{renderDropdown({ value, onChange })}
|
|
242
|
+
{error && <p className="mt-1 text-sm text-ews-error">{error}</p>}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Render uncontrolled version
|
|
250
|
+
return (
|
|
251
|
+
<div>
|
|
252
|
+
{label && (
|
|
253
|
+
<label className="block mb-1 text-sm font-medium text-ews-gray-700">
|
|
254
|
+
{label}
|
|
255
|
+
</label>
|
|
256
|
+
)}
|
|
257
|
+
{renderDropdown({
|
|
258
|
+
value: controlledValue ?? uncontrolledValue,
|
|
259
|
+
onChange: (newValue) => {
|
|
260
|
+
if (controlledValue === undefined) {
|
|
261
|
+
setUncontrolledValue(newValue);
|
|
262
|
+
}
|
|
263
|
+
onChange?.(newValue);
|
|
264
|
+
},
|
|
265
|
+
})}
|
|
266
|
+
{error && <p className="mt-1 text-sm text-ews-error">{error}</p>}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export { DropdownMultiSelect };
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export { MultiSearchAutocomplete } from "./components/MultiSearchAutocomplete";
|
|
|
16
16
|
export { Modal } from "./components/Modal";
|
|
17
17
|
export type { ErrorField, ErrorObject, ModalProps } from "./components/Modal";
|
|
18
18
|
|
|
19
|
+
export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
|
|
20
|
+
export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
|
|
21
|
+
|
|
19
22
|
export { Logo } from "./components/Logo";
|
|
20
23
|
export type { LogoProps } from "./components/Logo";
|
|
21
24
|
|
package/src/utils/index.ts
CHANGED
|
@@ -89,6 +89,6 @@ export const formatNumeric = (value: string): string => {
|
|
|
89
89
|
*/
|
|
90
90
|
export function isValidPhoneNumber(value: string): boolean {
|
|
91
91
|
const trimmedValue = value.trim();
|
|
92
|
-
const phoneRegex = /^[0-9]\d{1,
|
|
92
|
+
const phoneRegex = /^[0-9]\d{1,17}$/;
|
|
93
93
|
return phoneRegex.test(trimmedValue);
|
|
94
94
|
}
|