@bunnix/components 0.9.2 → 0.9.4
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/@types/index.d.ts +203 -5
- package/README.md +73 -103
- package/package.json +1 -1
- package/src/components/ComboBox.mjs +8 -2
- package/src/components/DatePicker.mjs +234 -57
- package/src/components/Dialog.mjs +1 -1
- package/src/components/InputField.mjs +55 -6
- package/src/components/ProgressBar.mjs +81 -0
- package/src/components/TimePicker.mjs +255 -65
- package/src/index.mjs +4 -0
- package/src/styles/controls.css +72 -0
- package/src/utils/maskUtils.mjs +569 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import Bunnix, { ForEach, useMemo, useRef, useState } from "@bunnix/core";
|
|
1
|
+
import Bunnix, { ForEach, useMemo, useRef, useState, useEffect } from "@bunnix/core";
|
|
2
2
|
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
3
3
|
import Icon from "./Icon.mjs";
|
|
4
|
-
const { div, button, span, hr } = Bunnix;
|
|
4
|
+
const { div, label, input: inputEl, button, span, hr } = Bunnix;
|
|
5
5
|
|
|
6
6
|
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
7
7
|
|
|
@@ -10,7 +10,44 @@ const isSameDay = (a, b) =>
|
|
|
10
10
|
|
|
11
11
|
const toMidnight = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
12
12
|
|
|
13
|
-
const formatDate = (date,
|
|
13
|
+
const formatDate = (date, format = "DD/MM/YYYY") => {
|
|
14
|
+
if (!date) return "";
|
|
15
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
16
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
17
|
+
const year = date.getFullYear();
|
|
18
|
+
return `${day}/${month}/${year}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseDate = (str) => {
|
|
22
|
+
if (!str) return null;
|
|
23
|
+
const parts = str.split("/");
|
|
24
|
+
if (parts.length !== 3) return null;
|
|
25
|
+
const day = parseInt(parts[0], 10);
|
|
26
|
+
const month = parseInt(parts[1], 10) - 1;
|
|
27
|
+
const year = parseInt(parts[2], 10);
|
|
28
|
+
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
|
|
29
|
+
const date = new Date(year, month, day);
|
|
30
|
+
if (date.getDate() !== day || date.getMonth() !== month || date.getFullYear() !== year) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return date;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const applyDateMask = (value) => {
|
|
37
|
+
// Remove all non-digits
|
|
38
|
+
const digits = value.replace(/\D/g, "");
|
|
39
|
+
|
|
40
|
+
// Apply mask DD/MM/YYYY
|
|
41
|
+
let masked = "";
|
|
42
|
+
for (let i = 0; i < digits.length && i < 8; i++) {
|
|
43
|
+
if (i === 2 || i === 4) {
|
|
44
|
+
masked += "/";
|
|
45
|
+
}
|
|
46
|
+
masked += digits[i];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return masked;
|
|
50
|
+
};
|
|
14
51
|
|
|
15
52
|
const buildCalendar = (viewDate) => {
|
|
16
53
|
if (!viewDate) return [];
|
|
@@ -49,25 +86,33 @@ const buildCalendar = (viewDate) => {
|
|
|
49
86
|
|
|
50
87
|
export default function DatePicker({
|
|
51
88
|
id,
|
|
52
|
-
placeholder,
|
|
89
|
+
placeholder = "DD/MM/YYYY",
|
|
53
90
|
range = false,
|
|
54
91
|
variant = "regular",
|
|
55
92
|
size = "regular",
|
|
93
|
+
label: labelText,
|
|
94
|
+
disabled = false,
|
|
95
|
+
value,
|
|
96
|
+
onInput,
|
|
97
|
+
onChange,
|
|
98
|
+
onFocus,
|
|
99
|
+
onBlur,
|
|
100
|
+
input,
|
|
101
|
+
change,
|
|
102
|
+
focus,
|
|
103
|
+
blur,
|
|
56
104
|
class: className = ""
|
|
57
105
|
} = {}) {
|
|
58
106
|
const popoverRef = useRef(null);
|
|
107
|
+
const inputRef = useRef(null);
|
|
59
108
|
const pickerId = id || `datepicker-${Math.random().toString(36).slice(2, 8)}`;
|
|
60
109
|
const anchorName = `--${pickerId}`;
|
|
61
110
|
|
|
62
|
-
const
|
|
63
|
-
return new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const selectedStart = useState(null);
|
|
111
|
+
const selectedStart = useState(value || null);
|
|
67
112
|
const selectedEnd = useState(null);
|
|
68
|
-
const inputValue = useState("");
|
|
113
|
+
const inputValue = useState(value ? formatDate(value) : "");
|
|
69
114
|
|
|
70
|
-
const viewDate = useState(new Date());
|
|
115
|
+
const viewDate = useState(value || new Date());
|
|
71
116
|
|
|
72
117
|
const calendar = useMemo([viewDate], (value) => buildCalendar(value));
|
|
73
118
|
const monthLabel = useMemo([viewDate], (value) => {
|
|
@@ -92,15 +137,47 @@ export default function DatePicker({
|
|
|
92
137
|
}
|
|
93
138
|
};
|
|
94
139
|
|
|
140
|
+
// Handle click outside and escape key for manual popover
|
|
141
|
+
useEffect((popoverElement) => {
|
|
142
|
+
if (!popoverElement) return;
|
|
143
|
+
|
|
144
|
+
const handleClickOutside = (e) => {
|
|
145
|
+
if (!popoverElement.matches(":popover-open")) return;
|
|
146
|
+
|
|
147
|
+
const input = inputRef.current;
|
|
148
|
+
const isClickInside = popoverElement.contains(e.target);
|
|
149
|
+
const isClickOnInput = input && input.contains(e.target);
|
|
150
|
+
|
|
151
|
+
if (!isClickInside && !isClickOnInput) {
|
|
152
|
+
closePopover();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleEscape = (e) => {
|
|
157
|
+
if (e.key === "Escape" && popoverElement.matches(":popover-open")) {
|
|
158
|
+
closePopover();
|
|
159
|
+
inputRef.current?.focus();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
document.addEventListener("click", handleClickOutside, true);
|
|
164
|
+
document.addEventListener("keydown", handleEscape);
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
document.removeEventListener("click", handleClickOutside, true);
|
|
168
|
+
document.removeEventListener("keydown", handleEscape);
|
|
169
|
+
};
|
|
170
|
+
}, popoverRef);
|
|
171
|
+
|
|
95
172
|
const setSelection = (start, end = null) => {
|
|
96
173
|
selectedStart.set(start);
|
|
97
174
|
selectedEnd.set(end);
|
|
98
175
|
if (range) {
|
|
99
|
-
const startText = formatDate(start
|
|
100
|
-
const endText = end ? formatDate(end
|
|
176
|
+
const startText = formatDate(start);
|
|
177
|
+
const endText = end ? formatDate(end) : "";
|
|
101
178
|
inputValue.set(endText ? `${startText} - ${endText}` : startText);
|
|
102
179
|
} else {
|
|
103
|
-
inputValue.set(formatDate(start
|
|
180
|
+
inputValue.set(formatDate(start));
|
|
104
181
|
}
|
|
105
182
|
};
|
|
106
183
|
|
|
@@ -118,6 +195,12 @@ export default function DatePicker({
|
|
|
118
195
|
} else {
|
|
119
196
|
setSelection(date, null);
|
|
120
197
|
closePopover();
|
|
198
|
+
|
|
199
|
+
// Trigger change handlers
|
|
200
|
+
const handleChange = onChange ?? change;
|
|
201
|
+
if (handleChange) {
|
|
202
|
+
handleChange({ target: { value: formatDate(date) }, date });
|
|
203
|
+
}
|
|
121
204
|
}
|
|
122
205
|
viewDate.set(new Date(date.getFullYear(), date.getMonth(), 1));
|
|
123
206
|
};
|
|
@@ -143,66 +226,159 @@ export default function DatePicker({
|
|
|
143
226
|
selectedEnd.set(null);
|
|
144
227
|
inputValue.set("");
|
|
145
228
|
closePopover();
|
|
229
|
+
|
|
230
|
+
const handleChange = onChange ?? change;
|
|
231
|
+
if (handleChange) {
|
|
232
|
+
handleChange({ target: { value: "" }, date: null });
|
|
233
|
+
}
|
|
146
234
|
};
|
|
147
235
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const today = new Date();
|
|
154
|
-
const fmt = formatter.get();
|
|
236
|
+
const handleOK = () => {
|
|
237
|
+
// Update the main input with the currently selected date when OK is clicked
|
|
238
|
+
const start = selectedStart.get();
|
|
239
|
+
const end = selectedEnd.get();
|
|
240
|
+
|
|
155
241
|
if (range) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
242
|
+
if (start && end) {
|
|
243
|
+
const startText = formatDate(start);
|
|
244
|
+
const endText = formatDate(end);
|
|
245
|
+
const value = `${startText} - ${endText}`;
|
|
246
|
+
inputValue.set(value);
|
|
247
|
+
|
|
248
|
+
const handleChange = onChange ?? change;
|
|
249
|
+
if (handleChange) {
|
|
250
|
+
handleChange({ target: { value }, dateStart: start, dateEnd: end });
|
|
251
|
+
}
|
|
252
|
+
} else if (start) {
|
|
253
|
+
const value = formatDate(start);
|
|
254
|
+
inputValue.set(value);
|
|
255
|
+
|
|
256
|
+
const handleChange = onChange ?? change;
|
|
257
|
+
if (handleChange) {
|
|
258
|
+
handleChange({ target: { value }, dateStart: start, dateEnd: null });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
if (start) {
|
|
263
|
+
const value = formatDate(start);
|
|
264
|
+
inputValue.set(value);
|
|
265
|
+
|
|
266
|
+
const handleChange = onChange ?? change;
|
|
267
|
+
if (handleChange) {
|
|
268
|
+
handleChange({ target: { value }, date: start });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
159
271
|
}
|
|
160
|
-
|
|
161
|
-
|
|
272
|
+
|
|
273
|
+
closePopover();
|
|
274
|
+
};
|
|
162
275
|
|
|
163
|
-
const
|
|
276
|
+
const handleInputChange = (e) => {
|
|
277
|
+
const rawValue = e.target.value;
|
|
278
|
+
const maskedValue = applyDateMask(rawValue);
|
|
279
|
+
inputValue.set(maskedValue);
|
|
280
|
+
|
|
281
|
+
// Try to parse the date if complete
|
|
282
|
+
if (maskedValue.length === 10) {
|
|
283
|
+
const parsedDate = parseDate(maskedValue);
|
|
284
|
+
if (parsedDate) {
|
|
285
|
+
selectedStart.set(parsedDate);
|
|
286
|
+
viewDate.set(new Date(parsedDate.getFullYear(), parsedDate.getMonth(), 1));
|
|
287
|
+
|
|
288
|
+
const handleChange = onChange ?? change;
|
|
289
|
+
if (handleChange) {
|
|
290
|
+
handleChange({ target: { value: maskedValue }, date: parsedDate });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const handleInput = onInput ?? input;
|
|
296
|
+
if (handleInput) {
|
|
297
|
+
handleInput(e);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
164
300
|
|
|
165
|
-
|
|
166
|
-
|
|
301
|
+
const handleInputFocus = (e) => {
|
|
302
|
+
openPopover();
|
|
303
|
+
|
|
304
|
+
const handleFocus = onFocus ?? focus;
|
|
305
|
+
if (handleFocus) {
|
|
306
|
+
handleFocus(e);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleInputBlur = (e) => {
|
|
311
|
+
// Don't close popover on blur - let it handle its own dismissal
|
|
312
|
+
// The popover will close when clicking outside or pressing escape
|
|
313
|
+
|
|
314
|
+
const handleBlur = onBlur ?? blur;
|
|
315
|
+
if (handleBlur) {
|
|
316
|
+
handleBlur(e);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleCalendarIconClick = (e) => {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
e.stopPropagation();
|
|
323
|
+
const input = inputRef.current;
|
|
324
|
+
if (input) {
|
|
325
|
+
input.focus();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// DatePicker supports regular, large, xlarge (no xsmall, small)
|
|
330
|
+
const normalizeSize = (value) => clampSize(value, ["regular", "large", "xlarge"], "regular");
|
|
167
331
|
const normalizedSize = normalizeSize(size);
|
|
168
332
|
const sizeToken = toSizeToken(normalizedSize);
|
|
333
|
+
const sizeClass = sizeToken === "xl" ? "input-xl" : sizeToken === "lg" ? "input-lg" : "";
|
|
169
334
|
const variantClass = variant === "rounded" ? "rounded-full" : "";
|
|
170
|
-
const
|
|
171
|
-
? "
|
|
172
|
-
:
|
|
173
|
-
? "
|
|
174
|
-
:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
335
|
+
const iconSizeValue = normalizedSize === "large"
|
|
336
|
+
? "large"
|
|
337
|
+
: normalizedSize === "xlarge"
|
|
338
|
+
? "xlarge"
|
|
339
|
+
: undefined;
|
|
340
|
+
|
|
341
|
+
return div({ class: `column-container no-margin shrink-0 gap-0 ${className}`.trim() }, [
|
|
342
|
+
labelText && label({ class: "label select-none" }, labelText),
|
|
343
|
+
div({ class: "datepicker-input-wrapper w-full relative" }, [
|
|
344
|
+
inputEl({
|
|
345
|
+
ref: inputRef,
|
|
346
|
+
id: pickerId,
|
|
347
|
+
type: "text",
|
|
348
|
+
value: inputValue,
|
|
349
|
+
placeholder,
|
|
350
|
+
disabled,
|
|
351
|
+
autocomplete: "off",
|
|
352
|
+
class: `input ${sizeClass} ${variantClass} pr-xl`.trim(),
|
|
353
|
+
style: `anchor-name: ${anchorName}`,
|
|
354
|
+
input: handleInputChange,
|
|
355
|
+
focus: handleInputFocus,
|
|
356
|
+
blur: handleInputBlur,
|
|
357
|
+
maxlength: "10"
|
|
358
|
+
}),
|
|
359
|
+
button({
|
|
360
|
+
class: "datepicker-icon-button",
|
|
361
|
+
type: "button",
|
|
362
|
+
disabled,
|
|
363
|
+
click: handleCalendarIconClick,
|
|
364
|
+
tabindex: "-1"
|
|
365
|
+
}, [
|
|
366
|
+
Icon({ name: "calendar", fill: "quaternary", size: iconSizeValue })
|
|
367
|
+
])
|
|
192
368
|
]),
|
|
193
369
|
div({
|
|
194
370
|
ref: popoverRef,
|
|
195
|
-
popover: "
|
|
371
|
+
popover: "manual",
|
|
196
372
|
class: "datepicker-popover popover-base",
|
|
197
373
|
style: `--anchor-id: ${anchorName}`
|
|
198
374
|
}, [
|
|
199
375
|
div({ class: "card column-container shadow gap-0 p-0 bg-base datepicker-card" }, [
|
|
200
376
|
div({ class: "row-container items-center justify-between datepicker-header p-sm no-margin" }, [
|
|
201
|
-
button({ class: "btn btn-flat datepicker-nav", click: handlePrevMonth }, [
|
|
377
|
+
button({ type: "button", class: "btn btn-flat datepicker-nav", click: handlePrevMonth }, [
|
|
202
378
|
span({ class: "icon icon-chevron-left icon-base" })
|
|
203
379
|
]),
|
|
204
380
|
span({ class: "datepicker-title" }, monthLabel),
|
|
205
|
-
button({ class: "btn btn-flat datepicker-nav", click: handleNextMonth }, [
|
|
381
|
+
button({ type: "button", class: "btn btn-flat datepicker-nav", click: handleNextMonth }, [
|
|
206
382
|
span({ class: "icon icon-chevron-right icon-base" })
|
|
207
383
|
])
|
|
208
384
|
]),
|
|
@@ -231,6 +407,7 @@ export default function DatePicker({
|
|
|
231
407
|
].filter(Boolean).join(" ");
|
|
232
408
|
|
|
233
409
|
return button({
|
|
410
|
+
type: "button",
|
|
234
411
|
class: classNames,
|
|
235
412
|
click: () => handleDayClick(cell.date)
|
|
236
413
|
}, cell.date.getDate().toString());
|
|
@@ -239,9 +416,9 @@ export default function DatePicker({
|
|
|
239
416
|
]),
|
|
240
417
|
hr({ class: "no-margin" }),
|
|
241
418
|
div({ class: "row-container justify-center items-center gap-md p-base shrink-0 datepicker-footer" }, [
|
|
242
|
-
button({ class: "btn btn-flat", click: handleClear }, "Clear"),
|
|
243
|
-
button({ class: "btn btn-flat", click: handleToday }, "Today"),
|
|
244
|
-
button({ class: "btn", click:
|
|
419
|
+
button({ type: "button", class: "btn btn-flat", click: handleClear }, "Clear"),
|
|
420
|
+
button({ type: "button", class: "btn btn-flat", click: handleToday }, "Today"),
|
|
421
|
+
button({ type: "button", class: "btn", click: handleOK }, "OK")
|
|
245
422
|
])
|
|
246
423
|
])
|
|
247
424
|
])
|
|
@@ -136,7 +136,7 @@ export default function Dialog() {
|
|
|
136
136
|
VStack({
|
|
137
137
|
ref: panelRef,
|
|
138
138
|
gap: "regular",
|
|
139
|
-
class: "box-capsule dialog-panel shadow bg-base p-lg items-stretch dialog-appear"
|
|
139
|
+
class: "box-capsule dialog-panel shadow bg-base border-solid p-lg items-stretch dialog-appear"
|
|
140
140
|
}, [
|
|
141
141
|
HStack({ alignment: "leading", gap: "small", class: "items-center w-full" }, [
|
|
142
142
|
Text({ type: "heading4", class: "no-margin" }, dialogState.map((value) => value.title)),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import Bunnix, { useRef, useEffect } from "@bunnix/core";
|
|
1
|
+
import Bunnix, { useRef, useEffect, useState } from "@bunnix/core";
|
|
2
2
|
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
3
|
+
import { applyMask, getMaskMaxLength } from "../utils/maskUtils.mjs";
|
|
3
4
|
const { div, label, input: inputEl, datalist, option, span } = Bunnix;
|
|
4
5
|
|
|
5
6
|
export default function InputField({
|
|
@@ -11,6 +12,8 @@ export default function InputField({
|
|
|
11
12
|
placeholder,
|
|
12
13
|
label: labelText,
|
|
13
14
|
disabled = false,
|
|
15
|
+
autocomplete,
|
|
16
|
+
mask,
|
|
14
17
|
suggestions = [],
|
|
15
18
|
onInput,
|
|
16
19
|
onChange,
|
|
@@ -26,6 +29,10 @@ export default function InputField({
|
|
|
26
29
|
} = {}) {
|
|
27
30
|
const inputRef = useRef(null);
|
|
28
31
|
const listId = suggestions.length > 0 ? `list-${Math.random().toString(36).slice(2, 8)}` : null;
|
|
32
|
+
|
|
33
|
+
// Initialize masked value based on initial value and mask
|
|
34
|
+
const initialMaskedValue = mask && value ? applyMask(value, mask) : (value || "");
|
|
35
|
+
const maskedValue = useState(initialMaskedValue);
|
|
29
36
|
|
|
30
37
|
// InputField supports regular, large, xlarge (no xsmall, small)
|
|
31
38
|
const normalizeSize = (value) => clampSize(value, ["regular", "large", "xlarge"], "regular");
|
|
@@ -48,19 +55,61 @@ export default function InputField({
|
|
|
48
55
|
const handleBlur = onBlur ?? blur;
|
|
49
56
|
const handleKeyDown = onKeyDown ?? keydown;
|
|
50
57
|
|
|
58
|
+
const handleMaskedInput = (e) => {
|
|
59
|
+
if (mask) {
|
|
60
|
+
const rawValue = e.target.value;
|
|
61
|
+
const masked = applyMask(rawValue, mask);
|
|
62
|
+
maskedValue.set(masked);
|
|
63
|
+
|
|
64
|
+
// Update the input element value
|
|
65
|
+
e.target.value = masked;
|
|
66
|
+
|
|
67
|
+
// Create a new event with the masked value
|
|
68
|
+
const maskedEvent = {
|
|
69
|
+
...e,
|
|
70
|
+
target: { ...e.target, value: masked }
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (handleInput) {
|
|
74
|
+
handleInput(maskedEvent);
|
|
75
|
+
}
|
|
76
|
+
if (handleChange) {
|
|
77
|
+
handleChange(maskedEvent);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
if (handleInput) {
|
|
81
|
+
handleInput(e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleMaskedChange = (e) => {
|
|
87
|
+
if (!mask && handleChange) {
|
|
88
|
+
handleChange(e);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Determine maxlength based on mask
|
|
93
|
+
const maxLength = mask ? getMaskMaxLength(mask) : rest.maxlength;
|
|
94
|
+
|
|
95
|
+
// Remove maxlength from rest to avoid override
|
|
96
|
+
const { maxlength: _maxlength, ...restWithoutMaxLength } = rest;
|
|
97
|
+
|
|
51
98
|
const inputElement = inputEl({
|
|
52
99
|
ref: inputRef,
|
|
53
100
|
type,
|
|
54
|
-
value: value ?? "",
|
|
101
|
+
value: mask ? maskedValue : (value ?? ""),
|
|
55
102
|
placeholder: placeholder ?? "", // Ensure placeholder is never undefined to avoid "false" text
|
|
56
103
|
disabled,
|
|
104
|
+
autocomplete: autocomplete ?? "off", // Default to off to prevent browser autocomplete suggestions
|
|
105
|
+
maxlength: maxLength,
|
|
57
106
|
class: `input ${combinedClass}`.trim(),
|
|
58
|
-
input:
|
|
59
|
-
change:
|
|
107
|
+
input: handleMaskedInput,
|
|
108
|
+
change: handleMaskedChange,
|
|
60
109
|
focus: handleFocus,
|
|
61
110
|
blur: handleBlur,
|
|
62
111
|
keydown: handleKeyDown,
|
|
63
|
-
...
|
|
112
|
+
...restWithoutMaxLength
|
|
64
113
|
});
|
|
65
114
|
|
|
66
115
|
const iconSizeClass = sizeToken === "xl"
|
|
@@ -76,7 +125,7 @@ export default function InputField({
|
|
|
76
125
|
])
|
|
77
126
|
: inputElement;
|
|
78
127
|
|
|
79
|
-
return div({ class: "column-container no-margin shrink-0" }, [
|
|
128
|
+
return div({ class: "column-container no-margin shrink-0 gap-0" }, [
|
|
80
129
|
labelText && label({ class: "label select-none" }, labelText),
|
|
81
130
|
inputBlock,
|
|
82
131
|
listId && datalist({ id: listId },
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Bunnix from "@bunnix/core";
|
|
2
|
+
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
3
|
+
|
|
4
|
+
const { div } = Bunnix;
|
|
5
|
+
|
|
6
|
+
export default function ProgressBar({
|
|
7
|
+
value = 0,
|
|
8
|
+
min = 0,
|
|
9
|
+
max = 100,
|
|
10
|
+
size,
|
|
11
|
+
color = "default",
|
|
12
|
+
class: className = "",
|
|
13
|
+
...rest
|
|
14
|
+
} = {}) {
|
|
15
|
+
const isState = (val) => val && typeof val.map === "function";
|
|
16
|
+
const normalizeSize = (val) =>
|
|
17
|
+
clampSize(val, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
|
|
18
|
+
|
|
19
|
+
const sizeState = isState(size) ? size : null;
|
|
20
|
+
const classState = isState(className) ? className : null;
|
|
21
|
+
const colorState = isState(color) ? color : null;
|
|
22
|
+
const valueState = isState(value) ? value : null;
|
|
23
|
+
const minState = isState(min) ? min : null;
|
|
24
|
+
const maxState = isState(max) ? max : null;
|
|
25
|
+
|
|
26
|
+
const buildClass = (sizeValue, classValue) => {
|
|
27
|
+
const normalizedSize = normalizeSize(sizeValue);
|
|
28
|
+
const sizeToken = toSizeToken(normalizedSize);
|
|
29
|
+
const sizeClass = sizeToken ? `progress-bar-${sizeToken}` : "";
|
|
30
|
+
return `progress-bar ${sizeClass} ${classValue || ""}`.trim();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const buildFillClass = (colorValue) => {
|
|
34
|
+
const resolvedColor = colorValue || "default";
|
|
35
|
+
return `progress-bar-fill text-${resolvedColor}`.trim();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const clampPercent = (val, minValue, maxValue) => {
|
|
39
|
+
const safeMin = Number.isFinite(Number(minValue)) ? Number(minValue) : 0;
|
|
40
|
+
const safeMax = Number.isFinite(Number(maxValue)) ? Number(maxValue) : 100;
|
|
41
|
+
const safeValue = Number.isFinite(Number(val)) ? Number(val) : 0;
|
|
42
|
+
|
|
43
|
+
if (safeMax <= safeMin) return 0;
|
|
44
|
+
|
|
45
|
+
const rawPercent = ((safeValue - safeMin) / (safeMax - safeMin)) * 100;
|
|
46
|
+
return Math.min(100, Math.max(0, rawPercent));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildFillStyle = (val, minValue, maxValue) =>
|
|
50
|
+
`width: ${clampPercent(val, minValue, maxValue)}%;`;
|
|
51
|
+
|
|
52
|
+
const combinedClass = sizeState
|
|
53
|
+
? sizeState.map((value) => buildClass(value, classState ? classState.get() : className))
|
|
54
|
+
: classState
|
|
55
|
+
? classState.map((value) => buildClass(size, value))
|
|
56
|
+
: buildClass(size, className);
|
|
57
|
+
|
|
58
|
+
const fillClass = colorState ? colorState.map((value) => buildFillClass(value)) : buildFillClass(color);
|
|
59
|
+
|
|
60
|
+
const fillStyle = valueState
|
|
61
|
+
? valueState.map((val) =>
|
|
62
|
+
buildFillStyle(val, minState ? minState.get() : min, maxState ? maxState.get() : max)
|
|
63
|
+
)
|
|
64
|
+
: minState
|
|
65
|
+
? minState.map((val) => buildFillStyle(value, val, maxState ? maxState.get() : max))
|
|
66
|
+
: maxState
|
|
67
|
+
? maxState.map((val) => buildFillStyle(value, min, val))
|
|
68
|
+
: buildFillStyle(value, min, max);
|
|
69
|
+
|
|
70
|
+
return div(
|
|
71
|
+
{
|
|
72
|
+
class: combinedClass,
|
|
73
|
+
role: "progressbar",
|
|
74
|
+
"aria-valuemin": min,
|
|
75
|
+
"aria-valuemax": max,
|
|
76
|
+
"aria-valuenow": value,
|
|
77
|
+
...rest,
|
|
78
|
+
},
|
|
79
|
+
[div({ class: fillClass, style: fillStyle })],
|
|
80
|
+
);
|
|
81
|
+
}
|