@inera/ids-react 9.2.1 → 9.3.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.
Files changed (80) hide show
  1. package/components/accordion/accordion-base.d.ts +2 -1
  2. package/components/accordion/accordion-base.js +3 -4
  3. package/components/accordion/accordion.js +5 -2
  4. package/components/alert/alert-base.js +2 -2
  5. package/components/breadcrumbs/breadcrumbs.js +6 -1
  6. package/components/button/button.d.ts +1 -0
  7. package/components/button/button.js +2 -2
  8. package/components/button/control-button.d.ts +6 -0
  9. package/components/button/control-button.js +9 -0
  10. package/components/carousel/carousel.js +1 -1
  11. package/components/dialog/dialog-base.js +1 -1
  12. package/components/form/checkbox/checkbox-base.js +2 -3
  13. package/components/form/checkbox/checkbox-group-base.d.ts +2 -1
  14. package/components/form/checkbox/checkbox-group-base.js +3 -3
  15. package/components/form/checkbox/checkbox-group.d.ts +2 -1
  16. package/components/form/checkbox/checkbox-group.js +2 -2
  17. package/components/form/checkbox/checkbox.js +3 -4
  18. package/components/form/datepicker/datepicker.d.ts +3 -1
  19. package/components/form/datepicker/datepicker.js +142 -110
  20. package/components/form/form-hooks/useInputValidity.d.ts +1 -1
  21. package/components/form/form-hooks/useInputValidity.js +28 -12
  22. package/components/form/form-props/form-props.d.ts +1 -0
  23. package/components/form/input/input-base.d.ts +4 -2
  24. package/components/form/input/input-base.js +14 -9
  25. package/components/form/input/input.d.ts +3 -0
  26. package/components/form/input/input.js +1 -1
  27. package/components/form/radio/radio-base.js +1 -2
  28. package/components/form/radio/radio-group-base.d.ts +2 -1
  29. package/components/form/radio/radio-group-base.js +3 -3
  30. package/components/form/radio/radio-group.d.ts +2 -1
  31. package/components/form/radio/radio-group.js +2 -2
  32. package/components/form/range/range-base.d.ts +1 -1
  33. package/components/form/range/range-base.js +2 -2
  34. package/components/form/select/select-base.d.ts +3 -3
  35. package/components/form/select/select-base.js +3 -5
  36. package/components/form/select/select.d.ts +2 -2
  37. package/components/form/select/select.js +1 -1
  38. package/components/form/select-multiple/select-multiple-base.d.ts +1 -1
  39. package/components/form/select-multiple/select-multiple-base.js +2 -2
  40. package/components/form/textarea/textarea-base.d.ts +1 -1
  41. package/components/form/textarea/textarea-base.js +3 -5
  42. package/components/form/textarea/textarea.js +1 -1
  43. package/components/form/time/time-base.d.ts +1 -1
  44. package/components/form/time/time-base.js +2 -4
  45. package/components/form/time/time.js +4 -5
  46. package/components/header-1177/header-1177-region-picker-base.d.ts +3 -1
  47. package/components/header-1177/header-1177-region-picker-base.js +8 -3
  48. package/components/header-1177/header-1177-region-picker-mobile-base.d.ts +3 -1
  49. package/components/header-1177/header-1177-region-picker-mobile-base.js +8 -3
  50. package/components/header-1177/header-1177-region-picker-mobile.d.ts +2 -0
  51. package/components/header-1177/header-1177-region-picker.d.ts +2 -0
  52. package/components/header-1177-pro/header-1177-pro-region-picker-base.d.ts +3 -1
  53. package/components/header-1177-pro/header-1177-pro-region-picker-base.js +8 -3
  54. package/components/header-1177-pro/header-1177-pro-region-picker-mobile-base.d.ts +3 -1
  55. package/components/header-1177-pro/header-1177-pro-region-picker-mobile-base.js +8 -3
  56. package/components/header-1177-pro/header-1177-pro-region-picker-mobile.d.ts +3 -1
  57. package/components/header-1177-pro/header-1177-pro-region-picker-mobile.js +1 -1
  58. package/components/header-1177-pro/header-1177-pro-region-picker.d.ts +3 -1
  59. package/components/header-1177-pro/header-1177-pro-region-picker.js +1 -1
  60. package/components/popover/popover-content.js +1 -1
  61. package/components/popover/popover.d.ts +2 -1
  62. package/components/popover/popover.js +28 -14
  63. package/components/puff-list/puff-list-item-header.d.ts +1 -1
  64. package/components/puff-list/puff-list-item.d.ts +2 -1
  65. package/components/puff-list/puff-list-item.js +2 -2
  66. package/components/side-panel/side-panel-base.d.ts +1 -0
  67. package/components/side-panel/side-panel-base.js +4 -2
  68. package/components/side-panel/side-panel.d.ts +2 -1
  69. package/components/side-panel/side-panel.js +2 -2
  70. package/components/stepper/step-base.d.ts +1 -1
  71. package/components/stepper/step-base.js +1 -1
  72. package/components/stepper/step.d.ts +1 -1
  73. package/components/stepper/step.js +2 -11
  74. package/components/tag/tag.js +8 -2
  75. package/components/tooltip/tooltip-base.d.ts +1 -3
  76. package/components/tooltip/tooltip-base.js +14 -10
  77. package/components/tooltip/tooltip.js +1 -41
  78. package/index.d.ts +1 -0
  79. package/index.js +1 -0
  80. package/package.json +4 -3
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useId, useRef, useState, useEffect } from 'react';
3
- import { isValid, getWeek, format, subMonths, addMonths } from 'date-fns';
3
+ import { getWeek, isValid, format, subMonths, addMonths } from 'date-fns';
4
4
  import { sv } from 'react-day-picker/locale';
5
5
  import clsx from 'clsx';
6
6
  import { IDSErrorMessage } from '../error-message/error-message.js';
@@ -10,6 +10,8 @@ import { useAriaDescribedBy } from '../form-hooks/useAriaDescribedBy.js';
10
10
  import { useClickOutside } from '../../utils/hooks/useClickOutside.js';
11
11
 
12
12
  const locale = { locale: sv };
13
+ const datePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
14
+ const datePatternString = "\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])";
13
15
  const createNewDate = (dateString) => {
14
16
  return new Date(dateString + "T00:00:00Z");
15
17
  };
@@ -25,7 +27,17 @@ const getPrevMonthYear = (date) => {
25
27
  const getNextMonthYear = (date) => {
26
28
  return `${getSweMonth(addMonths(date, 1))} ${getSweYear(addMonths(date, 1))}`;
27
29
  };
28
- function IDSDatePicker({ label, id, value, light = false, placeholder = "åååå-mm-dd", errorMsg = "", missingDateErrorMsg = "Datum saknas", invalidDateErrorMsg = "Ogiltigt datum", calendarHeader = "Välj datum", srOpenText = "Öppna kalendern", srCloseText = "Stäng kalendern", validationOnBlur = false, defaultMonth, startMonth = new Date(1900, 0, 1), endMonth = new Date(2050, 0, 1), noValidation = false, disabled = false, required = false, invalid = false, tooltip, disableNavigation = false, modifiers, focusedDay, onChange, onFocus, onBlur, onOpen, onClose, onDayClick, className, ...props }) {
30
+ const parseDateValue = (value) => {
31
+ if (!datePattern.test(value))
32
+ return undefined;
33
+ const parsedDate = createNewDate(value);
34
+ if (!isValid(parsedDate))
35
+ return undefined;
36
+ const [year, month, day] = value.split("-").map(Number);
37
+ const isSameDate = parsedDate.getUTCFullYear() === year && parsedDate.getUTCMonth() === month - 1 && parsedDate.getUTCDate() === day;
38
+ return isSameDate ? parsedDate : undefined;
39
+ };
40
+ function IDSDatePicker({ label, id, value, light = false, placeholder = "åååå-mm-dd", subtitle, dataTestId, errorMsg = "", missingDateErrorMsg = "Datum saknas", invalidDateErrorMsg = "Ogiltigt datum", calendarHeader = "Välj datum", srOpenText = "Öppna kalendern", srCloseText = "Stäng kalendern", validationOnBlur = false, defaultMonth, startMonth = new Date(1900, 0, 1), endMonth = new Date(2050, 0, 1), noValidation = false, disabled = false, required, invalid = false, readOnly = false, tooltip, disableNavigation = false, modifiers, focusedDay, onChange, onFocus, onBlur, onOpen, onClose, onDayClick, className, ...props }) {
29
41
  const reactId = useId();
30
42
  const dialogId = `datepicker-dialog-${reactId}`;
31
43
  const headerId = `datepicker-header-${reactId}`;
@@ -38,24 +50,82 @@ function IDSDatePicker({ label, id, value, light = false, placeholder = "ååå
38
50
  const headerRef = useRef(null);
39
51
  const prevMonthButtonRef = useRef(null);
40
52
  const nextMonthButtonRef = useRef(null);
53
+ const wasValidationEnabledRef = useRef(false);
54
+ const shouldRunRequiredValidation = required === true && !noValidation && !disabled;
55
+ const shouldShowCustomError = invalid === true && !!errorMsg && !noValidation && !disabled;
41
56
  const [isDialogOpen, setIsDialogOpen] = useState(false);
42
- const [inputValue, setInputValue] = useState(value);
43
- const initialSelectedDate = !!value ? createNewDate(value) : undefined;
57
+ const [inputValue, setInputValue] = useState(value ?? "");
58
+ const initialSelectedDate = value ? parseDateValue(value) : undefined;
44
59
  const [selectedDate, setSelectedDate] = useState(initialSelectedDate || defaultMonth);
45
60
  const [month, setMonth] = useState(initialSelectedDate || defaultMonth || new Date());
46
- const [hasDateError, setHasDateError] = useState(false);
47
- const [hasMissingError, setHasMissingError] = useState(false);
48
- const [hasOtherError, setHasOtherError] = useState(false);
49
- const hasError = hasMissingError || hasDateError || hasOtherError || invalid;
50
- const showInvalidError = !!hasDateError && !!invalidDateErrorMsg && !noValidation;
51
- const showMissingError = !!hasMissingError && !!missingDateErrorMsg && !noValidation;
52
- const showError = !!invalid && !!errorMsg && !isDialogOpen && !noValidation;
53
- useAriaDescribedBy(inputRef, errorMsgId, invalid, (showError || showMissingError || showInvalidError) && !noValidation);
61
+ const [showValidationErrors, setShowValidationErrors] = useState(false);
62
+ const [validationError, setValidationError] = useState(null);
63
+ const hasValidationError = showValidationErrors && validationError !== null;
64
+ const hasCustomError = shouldShowCustomError;
65
+ const hasError = hasValidationError || hasCustomError;
66
+ const getValidationError = (nextValue) => {
67
+ if (noValidation || disabled)
68
+ return null;
69
+ const trimmedValue = nextValue.trim();
70
+ if (shouldRunRequiredValidation) {
71
+ if (!trimmedValue)
72
+ return "missing";
73
+ if (!parseDateValue(trimmedValue))
74
+ return "invalid";
75
+ }
76
+ if (shouldShowCustomError)
77
+ return "custom";
78
+ return null;
79
+ };
80
+ const getErrorMessage = (error) => {
81
+ if (error === "missing")
82
+ return missingDateErrorMsg;
83
+ if (error === "invalid")
84
+ return invalidDateErrorMsg;
85
+ if (error === "custom")
86
+ return errorMsg;
87
+ return "";
88
+ };
89
+ const syncNativeValidity = (nextValue) => {
90
+ if (!shouldRunRequiredValidation || !inputRef.current)
91
+ return;
92
+ const nextError = getValidationError(nextValue);
93
+ inputRef.current.setCustomValidity(getErrorMessage(nextError));
94
+ };
95
+ const updateVisibleValidation = (nextValue, shouldShow) => {
96
+ if (!shouldRunRequiredValidation) {
97
+ setValidationError(null);
98
+ setShowValidationErrors(false);
99
+ return;
100
+ }
101
+ const nextError = getValidationError(nextValue);
102
+ setValidationError(nextError);
103
+ setShowValidationErrors(shouldShow);
104
+ };
105
+ const resetVisibleValidation = () => {
106
+ setValidationError(null);
107
+ setShowValidationErrors(false);
108
+ };
109
+ useAriaDescribedBy(inputRef, errorMsgId, hasError, hasError);
54
110
  useFocusTrap(dialogRef.current, isDialogOpen);
55
111
  useClickOutside(() => {
56
112
  setIsDialogOpen(false);
57
113
  onClose?.();
58
114
  }, [dialogRef, triggerRef], triggerRef, isDialogOpen);
115
+ useEffect(() => {
116
+ if (!inputRef.current)
117
+ return;
118
+ if (!shouldRunRequiredValidation) {
119
+ if (wasValidationEnabledRef.current) {
120
+ inputRef.current.setCustomValidity("");
121
+ }
122
+ wasValidationEnabledRef.current = false;
123
+ resetVisibleValidation();
124
+ return;
125
+ }
126
+ wasValidationEnabledRef.current = true;
127
+ syncNativeValidity(inputValue);
128
+ }, [shouldRunRequiredValidation, inputValue, invalid, errorMsg, missingDateErrorMsg, invalidDateErrorMsg]);
59
129
  const handleOpenDialog = (e) => {
60
130
  e.preventDefault();
61
131
  setIsDialogOpen(true);
@@ -63,7 +133,7 @@ function IDSDatePicker({ label, id, value, light = false, placeholder = "ååå
63
133
  };
64
134
  const closeDialog = () => {
65
135
  setIsDialogOpen(false);
66
- triggerRef.current.focus();
136
+ triggerRef.current?.focus();
67
137
  onClose?.();
68
138
  };
69
139
  useEffect(() => {
@@ -79,113 +149,78 @@ function IDSDatePicker({ label, id, value, light = false, placeholder = "ååå
79
149
  document.addEventListener("keydown", handleKeyDown);
80
150
  return () => document.removeEventListener("keydown", handleKeyDown);
81
151
  }, [isDialogOpen]);
152
+ const emitValue = (nextValue, parsedDate) => {
153
+ const trimmedValue = nextValue.trim();
154
+ const isMissing = shouldRunRequiredValidation && !trimmedValue;
155
+ const isInvalidDate = shouldRunRequiredValidation && !!trimmedValue && !parsedDate;
156
+ onChange?.({
157
+ value: nextValue,
158
+ valueAsDate: parsedDate,
159
+ invalidDate: isInvalidDate,
160
+ missingDate: isMissing
161
+ });
162
+ };
82
163
  const handleDayPickerSelect = (date) => {
83
- const dateString = format(date, "yyyy-MM-dd");
84
- resetErrors();
85
- if (!date) {
86
- setInputValue("");
87
- setSelectedDate(undefined);
88
- }
89
- else {
90
- setSelectedDate(date);
91
- setInputValue(dateString);
164
+ const nextValue = date ? format(date, "yyyy-MM-dd") : "";
165
+ resetVisibleValidation();
166
+ setInputValue(nextValue);
167
+ setSelectedDate(date);
168
+ if (date) {
169
+ setMonth(date);
92
170
  }
171
+ emitValue(nextValue, date);
93
172
  requestAnimationFrame(() => {
94
- if (inputRef.current) {
95
- inputRef.current.checkValidity();
96
- inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
97
- inputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
173
+ if (!inputRef.current)
174
+ return;
175
+ if (shouldRunRequiredValidation) {
176
+ syncNativeValidity(nextValue);
98
177
  }
178
+ inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
179
+ inputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
99
180
  });
100
- emitValue(dateString, date);
101
181
  closeDialog();
102
182
  };
103
- const updateErrors = (validity, isNotADate) => {
104
- if (!noValidation) {
105
- setHasMissingError(validity.valueMissing);
106
- setHasOtherError(!validity.valid);
107
- if (validity.valueMissing) {
108
- setHasDateError(false);
109
- }
110
- else {
111
- setHasDateError(!!isNotADate || validity.patternMismatch);
112
- }
113
- }
114
- };
115
- const resetErrors = () => {
116
- setHasMissingError(false);
117
- setHasDateError(false);
118
- setHasOtherError(false);
119
- };
120
183
  const handleInputChange = (e) => {
121
- resetErrors();
122
- setInputValue(e.currentTarget.value);
123
- const parsedDate = createNewDate(e.currentTarget.value);
124
- setHasDateError(false);
125
- if (isValid(parsedDate)) {
126
- setSelectedDate(parsedDate);
184
+ const nextValue = e.currentTarget.value;
185
+ const parsedDate = parseDateValue(nextValue.trim());
186
+ resetVisibleValidation();
187
+ setInputValue(nextValue);
188
+ setSelectedDate(parsedDate);
189
+ if (parsedDate) {
127
190
  setMonth(parsedDate);
128
191
  }
129
- else {
130
- setHasDateError(true);
131
- setSelectedDate(undefined);
192
+ if (shouldRunRequiredValidation) {
193
+ syncNativeValidity(nextValue);
132
194
  }
133
- emitValue(e.currentTarget.value, parsedDate);
195
+ emitValue(nextValue, parsedDate);
134
196
  };
135
- // if the input value is changed programmatically
136
197
  useEffect(() => {
137
- if (value !== inputValue && inputRef.current) {
138
- resetErrors();
139
- setInputValue(value);
140
- setHasDateError(false);
141
- if (value) {
142
- const parsedDate = createNewDate(value);
143
- if (isValid(parsedDate)) {
144
- setSelectedDate(parsedDate);
145
- setMonth(parsedDate);
146
- }
147
- else {
148
- setSelectedDate(undefined);
149
- setHasDateError(true);
150
- }
151
- }
198
+ if (value === inputValue)
199
+ return;
200
+ const nextValue = value ?? "";
201
+ const parsedDate = nextValue ? parseDateValue(nextValue.trim()) : undefined;
202
+ resetVisibleValidation();
203
+ setInputValue(nextValue);
204
+ setSelectedDate(parsedDate);
205
+ if (parsedDate) {
206
+ setMonth(parsedDate);
152
207
  }
153
- }, [value, inputRef]);
154
- const emitValue = (val, parsedDate) => {
155
- const isMissing = required && !val;
156
- const isValidDate = parsedDate && parsedDate instanceof Date && isValid(parsedDate);
157
- onChange?.({
158
- value: val,
159
- valueAsDate: isValidDate ? createNewDate(val) : undefined,
160
- invalidDate: !isValid(parsedDate),
161
- missingDate: isMissing
162
- });
163
- };
208
+ }, [value]);
164
209
  const handleInvalid = (e) => {
165
- updateErrors(e.currentTarget.validity);
210
+ if (!shouldRunRequiredValidation)
211
+ return;
212
+ e.preventDefault();
213
+ syncNativeValidity(e.currentTarget.value);
214
+ updateVisibleValidation(e.currentTarget.value, true);
166
215
  };
167
216
  const handleBlur = (e) => {
168
- if (validationOnBlur) {
169
- updateErrors(e.currentTarget.validity, !!e.currentTarget.value.trim().length && !isValid(selectedDate));
217
+ if (shouldRunRequiredValidation && validationOnBlur) {
218
+ syncNativeValidity(e.currentTarget.value);
219
+ updateVisibleValidation(e.currentTarget.value, true);
170
220
  }
171
221
  onBlur?.(e);
172
222
  };
173
- useEffect(() => {
174
- const form = inputRef.current?.form;
175
- if (!form)
176
- return;
177
- const handleSubmit = (_e) => {
178
- if (!noValidation) {
179
- requestAnimationFrame(() => {
180
- updateErrors(inputRef.current.validity, !isValid(createNewDate(inputRef.current.value)));
181
- });
182
- }
183
- };
184
- form.addEventListener("submit", handleSubmit);
185
- return () => form.removeEventListener("submit", handleSubmit);
186
- }, []);
187
223
  function CustomNav(props) {
188
- // Add the nav buttons after the dropdowns for correct tab order
189
224
  const { children } = props;
190
225
  const { goToMonth, previousMonth, nextMonth } = useDayPicker();
191
226
  const currentMonth = "aktuell månad";
@@ -193,28 +228,25 @@ function IDSDatePicker({ label, id, value, light = false, placeholder = "ååå
193
228
  const goToPrevMonth = () => {
194
229
  previousMonth && goToMonth(previousMonth);
195
230
  requestAnimationFrame(() => {
196
- if (prevMonthButtonRef.current) {
197
- prevMonthButtonRef.current.focus();
198
- }
231
+ prevMonthButtonRef.current?.focus();
199
232
  });
200
233
  };
201
234
  const goToNextMonth = () => {
202
235
  nextMonth && goToMonth(nextMonth);
203
236
  requestAnimationFrame(() => {
204
- if (nextMonthButtonRef.current) {
205
- nextMonthButtonRef.current.focus();
206
- }
237
+ nextMonthButtonRef.current?.focus();
207
238
  });
208
239
  };
209
240
  return (jsxs(DropdownNav, { className: "ids-datepicker__nav", ...props, children: [jsx("div", { className: "ids-datepicker__nav-dropdowns", children: children }), jsxs("div", { className: "ids-datepicker__nav-buttons", children: [jsx("button", { type: "button", ref: prevMonthButtonRef, className: "ids-datepicker__nav-prev", onClick: goToPrevMonth, disabled: !previousMonth || disableNavigation, "aria-label": `${currentMonth} ${getSweMonth(month)}. ${goTo} ${getPrevMonthYear(month)}` }), jsx("button", { type: "button", ref: nextMonthButtonRef, className: "ids-datepicker__nav-next", onClick: goToNextMonth, disabled: !nextMonth || disableNavigation, "aria-label": `${currentMonth} ${getSweMonth(month)}. ${goTo} ${getNextMonthYear(month)}` })] })] }));
210
241
  }
211
242
  useEffect(() => {
212
- const header = headerRef.current;
213
- if (!header)
243
+ if (!isDialogOpen)
214
244
  return;
215
- header?.focus();
245
+ headerRef.current?.focus();
216
246
  }, [isDialogOpen]);
217
- return (jsxs("div", { className: clsx("ids-datepicker", { "ids-datepicker--invalid": !noValidation && hasError }, className), ...props, children: [isDialogOpen && jsx("div", { className: "ids-datepicker__overlay" }), label && (jsxs("div", { className: "ids-label-wrapper ids-label-wrapper--margin-bottom", children: [jsx("label", { htmlFor: inputId, className: "ids-label", children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), jsxs("div", { className: "ids-datepicker__input-wrapper", children: [jsx("input", { className: clsx("ids-datepicker__input", { "ids-input--light": light }), ref: inputRef, style: { fontSize: "inherit" }, id: inputId, type: "text", value: inputValue, required: required, disabled: disabled, "aria-invalid": !noValidation && hasError, placeholder: placeholder, onChange: handleInputChange, onInvalid: handleInvalid, onBlur: handleBlur, pattern: "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" }), jsx("button", { ref: triggerRef, type: "button", className: "ids-datepicker__trigger", style: { fontSize: "inherit" }, disabled: disabled, onClick: handleOpenDialog, "aria-controls": "dialog", "aria-haspopup": "dialog", "aria-expanded": isDialogOpen, "aria-label": srOpenText }), jsxs("div", { className: clsx("ids-datepicker__dialog", { "ids-datepicker__dialog--show": isDialogOpen }), role: "dialog", ref: dialogRef, id: dialogId, "aria-modal": true, "aria-labelledby": headerId, children: [jsxs("div", { className: "ids-datepicker__dialog-bar", children: [jsx("div", { className: "ids-datepicker__dialog-header", id: headerId, ref: headerRef, tabIndex: -1, children: calendarHeader }), jsx("button", { className: "ids-datepicker__dialog-close-button", type: "button", onClick: closeDialog, "aria-label": srCloseText })] }), jsx(DayPicker, { mode: "single", locale: sv, labels: {
247
+ const visibleErrorType = hasValidationError ? validationError : hasCustomError ? "custom" : null;
248
+ const visibleErrorMessage = visibleErrorType ? getErrorMessage(visibleErrorType) : "";
249
+ return (jsxs("div", { className: clsx("ids-datepicker", { "ids-datepicker--invalid": hasError }, className), "data-testid": dataTestId, children: [isDialogOpen && jsx("div", { className: "ids-datepicker__overlay" }), label && (jsxs("div", { className: "ids-label-wrapper ids-label-wrapper--margin-bottom", children: [jsx("label", { htmlFor: inputId, className: clsx("ids-label", { "ids-label--disabled": disabled }), children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), subtitle && jsx("div", { className: clsx("ids-subtitle", { "ids-subtitle--disabled": disabled }), children: subtitle }), jsxs("div", { className: "ids-datepicker__input-wrapper", children: [jsx("input", { className: clsx("ids-datepicker__input", { "ids-input--light": light }), ref: inputRef, style: { fontSize: "inherit" }, id: inputId, type: "text", value: inputValue, required: shouldRunRequiredValidation || undefined, pattern: shouldRunRequiredValidation ? datePatternString : undefined, "aria-invalid": hasError || undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, onChange: handleInputChange, onFocus: onFocus, onInvalid: handleInvalid, onBlur: handleBlur, ...props }), jsx("button", { ref: triggerRef, type: "button", className: "ids-datepicker__trigger", style: { fontSize: "inherit" }, disabled: disabled || readOnly, onClick: handleOpenDialog, "aria-controls": dialogId, "aria-haspopup": "dialog", "aria-expanded": isDialogOpen, "aria-label": srOpenText }), jsxs("div", { className: clsx("ids-datepicker__dialog", { "ids-datepicker__dialog--show": isDialogOpen }), role: "dialog", ref: dialogRef, id: dialogId, "aria-modal": true, "aria-labelledby": headerId, children: [jsxs("div", { className: "ids-datepicker__dialog-bar", children: [jsx("div", { className: "ids-datepicker__dialog-header", id: headerId, ref: headerRef, tabIndex: -1, children: calendarHeader }), jsx("button", { className: "ids-datepicker__dialog-close-button", type: "button", onClick: closeDialog, "aria-label": srCloseText })] }), jsx(DayPicker, { mode: "single", locale: sv, labels: {
218
250
  labelWeekNumberHeader: () => "Veckonumer",
219
251
  labelWeekNumber: (_weekNumber) => `vecka`,
220
252
  labelDayButton(date, _modifiers, _options, dateLib) {
@@ -235,7 +267,7 @@ function IDSDatePicker({ label, id, value, light = false, placeholder = "ååå
235
267
  WeekNumberHeader: props => (jsx(WeekNumberHeader, { ...props, className: "ids-datepicker__week-number-header", children: shortWeek })),
236
268
  MonthsDropdown: props => (jsx(MonthsDropdown, { ...props, disabled: disableNavigation, className: "ids-datepicker__month-select" })),
237
269
  YearsDropdown: props => (jsx(YearsDropdown, { ...props, disabled: disableNavigation, className: "ids-datepicker__year-select" }))
238
- }, startMonth: startMonth, endMonth: endMonth, month: month, onMonthChange: setMonth, defaultMonth: defaultMonth, selected: selectedDate, onSelect: handleDayPickerSelect, onDayClick: onDayClick })] })] }), (showError || showInvalidError || showMissingError) && (jsxs(IDSErrorMessage, { id: errorMsgId, show: true, children: [showInvalidError && !showMissingError && !showError && invalidDateErrorMsg, showMissingError && !showInvalidError && !showError && missingDateErrorMsg, showError && errorMsg] }))] }));
270
+ }, startMonth: startMonth, endMonth: endMonth, month: month, onMonthChange: setMonth, defaultMonth: defaultMonth, selected: selectedDate, onSelect: handleDayPickerSelect, onDayClick: onDayClick })] })] }), visibleErrorMessage && (jsx(IDSErrorMessage, { id: errorMsgId, show: true, children: visibleErrorMessage }))] }));
239
271
  }
240
272
  IDSDatePicker.displayName = "IDSDatePicker";
241
273
 
@@ -1 +1 @@
1
- export declare function useInputValidity(ref: React.RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>, validateOnBlur?: boolean): boolean;
1
+ export declare function useInputValidity(ref: React.RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>, validateOnBlur?: boolean, skipValidation?: boolean): boolean;
@@ -1,30 +1,46 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useRef, useEffect } from 'react';
2
2
 
3
- function useInputValidity(ref, validateOnBlur) {
3
+ function useInputValidity(ref, validateOnBlur, skipValidation) {
4
4
  const [isValid, setIsValid] = useState(true);
5
+ const validationStartedRef = useRef(false);
5
6
  useEffect(() => {
7
+ if (skipValidation) {
8
+ validationStartedRef.current = false;
9
+ setIsValid(true);
10
+ return;
11
+ }
6
12
  const inputEl = ref.current;
7
13
  if (!inputEl)
8
14
  return;
9
15
  const updateValidity = () => {
10
- setIsValid(inputEl.validity.valid);
16
+ const isOptionalEmpty = !inputEl.required && inputEl.value === "";
17
+ setIsValid(isOptionalEmpty || inputEl.validity.valid);
18
+ };
19
+ const startValidation = () => {
20
+ validationStartedRef.current = true;
21
+ updateValidity();
22
+ };
23
+ const updateValidityAfterValidationStarted = () => {
24
+ if (validationStartedRef.current) {
25
+ updateValidity();
26
+ }
11
27
  };
12
28
  const form = inputEl.closest("form");
13
- form?.addEventListener("submit", updateValidity);
29
+ form?.addEventListener("submit", startValidation);
30
+ inputEl.addEventListener("invalid", startValidation);
31
+ inputEl.addEventListener("change", updateValidityAfterValidationStarted);
14
32
  if (validateOnBlur) {
15
- inputEl.addEventListener("blur", updateValidity);
33
+ inputEl.addEventListener("blur", startValidation);
16
34
  }
17
- inputEl.addEventListener("change", updateValidity);
18
- inputEl.addEventListener("invalid", updateValidity);
19
35
  return () => {
20
- form?.removeEventListener("submit", updateValidity);
36
+ form?.removeEventListener("submit", startValidation);
37
+ inputEl.removeEventListener("invalid", startValidation);
38
+ inputEl.removeEventListener("change", updateValidityAfterValidationStarted);
21
39
  if (validateOnBlur) {
22
- inputEl.removeEventListener("blur", updateValidity);
40
+ inputEl.removeEventListener("blur", startValidation);
23
41
  }
24
- inputEl.removeEventListener("change", updateValidity);
25
- inputEl.removeEventListener("invalid", updateValidity);
26
42
  };
27
- }, [ref]);
43
+ }, [ref, validateOnBlur, skipValidation]);
28
44
  return isValid;
29
45
  }
30
46
 
@@ -8,6 +8,7 @@ export interface CommonFormProps {
8
8
  label?: string;
9
9
  errorMsg?: string;
10
10
  hint?: string | ReactNode;
11
+ subtitle?: string | ReactNode;
11
12
  tooltip?: ReactNode;
12
13
  focusAnchor?: boolean;
13
14
  dataTestId?: string;
@@ -1,16 +1,18 @@
1
1
  import React, { ReactNode, CSSProperties } from "react";
2
2
  import { CommonFormPropsWithReadOnly } from "../form-props/form-props";
3
3
  export interface IDSInputBaseProps extends React.InputHTMLAttributes<HTMLInputElement>, CommonFormPropsWithReadOnly {
4
- icon?: string;
5
4
  unit?: string;
5
+ subtitle?: string;
6
6
  showSearchLabel?: boolean;
7
7
  submitButton?: ReactNode;
8
8
  hintId?: string;
9
9
  errorMsgId?: string;
10
10
  inputRef?: React.Ref<HTMLInputElement>;
11
11
  style?: CSSProperties;
12
+ icon?: string;
13
+ clearButton?: ReactNode;
12
14
  }
13
- export declare function IDSInputBase({ label, type, icon, hint, unit, showSearchLabel, errorMsg, dataTestId, disabled, invalid, required, focusAnchor, light, readOnly, tooltip, submitButton, className, id, hintId, errorMsgId, inputRef, ...props }: IDSInputBaseProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function IDSInputBase({ label, type, icon, clearButton, hint, unit, subtitle, showSearchLabel, errorMsg, dataTestId, disabled, invalid, required, focusAnchor, light, readOnly, tooltip, submitButton, className, id, hintId, errorMsgId, inputRef, ...props }: IDSInputBaseProps): import("react/jsx-runtime").JSX.Element;
14
16
  export declare namespace IDSInputBase {
15
17
  var displayName: string;
16
18
  }
@@ -3,7 +3,7 @@ import { useId } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { IDSErrorMessage } from '../error-message/error-message.js';
5
5
 
6
- function IDSInputBase({ label, type = "text", icon, hint, unit, showSearchLabel = false, errorMsg, dataTestId, disabled = false, invalid = false, required = false, focusAnchor = false, light = false, readOnly = false, tooltip, submitButton, className, id, hintId, errorMsgId, inputRef, ...props }) {
6
+ function IDSInputBase({ label, type = "text", icon, clearButton, hint, unit, subtitle, showSearchLabel = false, errorMsg, dataTestId, disabled = false, invalid = false, required = false, focusAnchor = false, light = false, readOnly = false, tooltip, submitButton, className, id, hintId, errorMsgId, inputRef, ...props }) {
7
7
  const reactId = useId();
8
8
  const inputId = id ?? `input-base-${reactId}`;
9
9
  const baseHintId = hintId ?? `input-base-hint-${reactId}`;
@@ -17,15 +17,20 @@ function IDSInputBase({ label, type = "text", icon, hint, unit, showSearchLabel
17
17
  if (type === "search" && !showSearchLabel && label) {
18
18
  ariaHandler["aria-label"] = label;
19
19
  }
20
- return (jsxs(Fragment, { children: [jsxs("div", { className: clsx("ids-input", { "ids-input--search": type === "search", "ids-input--icon": !!icon }, className), "data-testid": dataTestId, children: [jsxs("div", { className: "ids-input__wrapper", children: [label && (jsxs("div", { className: clsx("ids-label-wrapper", "ids-label-wrapper--margin-bottom", {
21
- "ids-hidden": type === "search" && !showSearchLabel
20
+ return (jsxs(Fragment, { children: [jsxs("div", { className: clsx("ids-input", {
21
+ "ids-input--search": type === "search",
22
+ "ids-input--icon": !!icon && !clearButton,
23
+ "ids-input--clear-button": !!clearButton,
24
+ "ids-input--unit": !!unit
25
+ }, className), "data-testid": dataTestId, children: [jsxs("div", { className: "ids-input__wrapper", children: [label && (jsxs("div", { className: clsx("ids-label-wrapper", "ids-label-wrapper--margin-bottom", {
26
+ "ids-label-wrapper--sr-only": type === "search" && !showSearchLabel
22
27
  }), children: [jsx("label", { className: clsx("ids-label", {
23
- "ids-label--disabled": disabled || readOnly
24
- }), htmlFor: inputId, children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), jsxs("div", { className: "ids-input__unit-wrapper", children: [jsxs("div", { className: "ids-input__input-wrapper", children: [type === "search" && jsx("span", { className: "ids-input__search-icon" }), jsx("input", { ref: inputRef, id: inputId, type: type, readOnly: readOnly, className: clsx("ids-input__input", {
25
- "ids-input--light": light,
26
- "ids-input--invalid": invalid,
27
- "ids-focus-anchor": focusAnchor
28
- }), "aria-invalid": invalid, required: required, disabled: disabled, ...ariaHandler, ...props }), icon && type !== "search" && jsx("span", { className: `ids-input__icon ids-icon-${icon}` })] }), unit && (jsx("div", { className: "ids-input__unit", "aria-hidden": "true", children: unit }))] }), hint && (jsx("div", { className: "ids-input__hint", id: baseHintId, children: hint }))] }), !!submitButton && submitButton] }), showErrorMsg && (jsx(IDSErrorMessage, { id: baseErrorMsgId, show: true, children: errorMsg }))] }));
28
+ "ids-label--sr-only": type === "search" && !showSearchLabel
29
+ }), htmlFor: inputId, children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), subtitle && jsx("div", { className: "ids-subtitle", children: subtitle }), jsxs("div", { className: "ids-input__input-wrapper", children: [type === "search" && jsx("span", { className: "ids-input__search-icon" }), jsx("input", { ref: inputRef, id: inputId, type: type, readOnly: readOnly, className: clsx("ids-input__input", {
30
+ "ids-input--light": light,
31
+ "ids-input--invalid": invalid,
32
+ "ids-focus-anchor": focusAnchor
33
+ }), "aria-invalid": invalid, required: required, disabled: disabled, ...ariaHandler, ...props }), icon && !clearButton && jsx("span", { className: `ids-input__icon ids-icon-${icon}` }), clearButton && jsx("span", { className: "ids-input__clear-button", children: clearButton }), unit && (jsx("div", { className: "ids-input__unit", "aria-hidden": "true", children: unit }))] }), hint && (jsx("div", { className: "ids-input__hint", id: baseHintId, children: hint }))] }), !!submitButton && submitButton] }), showErrorMsg && (jsx(IDSErrorMessage, { id: baseErrorMsgId, show: true, children: errorMsg }))] }));
29
34
  }
30
35
  IDSInputBase.displayName = "IDSInputBase";
31
36
 
@@ -2,7 +2,10 @@ import { ReactNode } from "react";
2
2
  import { CommonFormPropsWithReadOnly } from "../form-props/form-props";
3
3
  interface IDSInputProps extends React.InputHTMLAttributes<HTMLInputElement>, CommonFormPropsWithReadOnly {
4
4
  icon?: string;
5
+ clearButton?: ReactNode;
5
6
  unit?: string;
7
+ hint?: string | ReactNode;
8
+ subtitle?: string;
6
9
  showSearchLabel?: boolean;
7
10
  submitButton?: ReactNode;
8
11
  }
@@ -6,7 +6,7 @@ import { IDSInputBase } from './input-base.js';
6
6
 
7
7
  const IDSInput = forwardRef(({ invalid = false, noValidation = false, style, validationOnBlur = false, ...props }, ref) => {
8
8
  const inputRef = useRef(null);
9
- const hasValidValue = useInputValidity(inputRef, validationOnBlur);
9
+ const hasValidValue = useInputValidity(inputRef, validationOnBlur, noValidation);
10
10
  const computedInvalid = (invalid || !hasValidValue) && !noValidation;
11
11
  // Merge forwarded + local ref
12
12
  const mergedRef = (node) => {
@@ -13,8 +13,7 @@ function IDSRadioBase({ id, name, light, disabled, focusAnchor, tooltip, dataTes
13
13
  "ids-input--light": light,
14
14
  "ids-focus-anchor": focusAnchor
15
15
  }), name: name, disabled: disabled, "aria-invalid": invalid, ...ariaHandler, ...props }), !!children && (jsxs("div", { className: "ids-label-wrapper", children: [jsx("label", { htmlFor: inputId, className: clsx("ids-radio__label ids-label", {
16
- "ids-label--clickable": !disabled,
17
- "ids-label--disabled": disabled
16
+ "ids-label--clickable": !disabled
18
17
  }), children: children }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] }))] }));
19
18
  }
20
19
  IDSRadioBase.displayName = "IDSRadioBase";
@@ -7,10 +7,11 @@ interface IDSRadioGroupBaseProps extends FieldsetHTMLAttributes<HTMLFieldSetElem
7
7
  required?: boolean;
8
8
  tooltip?: ReactNode;
9
9
  invalid?: boolean;
10
+ subtitle?: string | ReactNode;
10
11
  errorMsgId?: string;
11
12
  groupRef?: React.Ref<HTMLFieldSetElement>;
12
13
  }
13
- export declare function IDSRadioGroupBase({ legend, hideLegend, errorMsg, errorMsgId, compact, required, tooltip, className, children, invalid, groupRef, ...props }: IDSRadioGroupBaseProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function IDSRadioGroupBase({ legend, hideLegend, errorMsg, errorMsgId, compact, required, tooltip, subtitle, className, children, invalid, groupRef, ...props }: IDSRadioGroupBaseProps): import("react/jsx-runtime").JSX.Element;
14
15
  export declare namespace IDSRadioGroupBase {
15
16
  var displayName: string;
16
17
  }
@@ -2,11 +2,11 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { IDSErrorMessage } from '../error-message/error-message.js';
3
3
  import clsx from 'clsx';
4
4
 
5
- function IDSRadioGroupBase({ legend, hideLegend, errorMsg, errorMsgId, compact = false, required = false, tooltip, className, children, invalid, groupRef, ...props }) {
5
+ function IDSRadioGroupBase({ legend, hideLegend, errorMsg, errorMsgId, compact = false, required = false, tooltip, subtitle, className, children, invalid, groupRef, ...props }) {
6
6
  const showErrorMsg = invalid && !!errorMsg && !!errorMsgId;
7
- return (jsxs("fieldset", { ref: groupRef, "aria-required": required, className: clsx("ids-form-group__fieldset", { "ids-form-group__fieldset--compact": compact }, className), ...props, children: [legend && (jsx("div", { className: clsx("ids-label-wrapper", {
7
+ return (jsxs("fieldset", { ref: groupRef, "aria-required": required, className: clsx("ids-form-group__fieldset", { "ids-form-group__fieldset--compact": compact }, className), ...props, children: [legend && (jsxs("div", { className: clsx("ids-label-wrapper", {
8
8
  "ids-label-wrapper--sr-only": hideLegend
9
- }, className), children: jsxs("legend", { children: [legend, tooltip && jsx("span", { className: "ids-legend__tooltip", children: tooltip })] }) })), children, showErrorMsg && (jsx(IDSErrorMessage, { id: errorMsgId, show: true, style: { marginTop: compact ? "0.75rem" : "auto" }, children: errorMsg }))] }));
9
+ }, className), children: [jsxs("legend", { children: [legend, tooltip && jsx("span", { className: "ids-legend__tooltip", children: tooltip })] }), subtitle && jsx("div", { className: "ids-subtitle ids-subtitle--group", children: subtitle })] })), children, showErrorMsg && (jsx(IDSErrorMessage, { id: errorMsgId, show: true, style: { marginTop: compact ? "0.75rem" : "auto" }, children: errorMsg }))] }));
10
10
  }
11
11
  IDSRadioGroupBase.displayName = "IDSRadioGroupBase";
12
12
 
@@ -7,11 +7,12 @@ interface IDSRadioGroupProps extends FieldsetHTMLAttributes<HTMLFieldSetElement>
7
7
  compact?: boolean;
8
8
  invalid?: boolean;
9
9
  required?: boolean;
10
+ subtitle?: string | ReactNode;
10
11
  tooltip?: ReactNode;
11
12
  noValidation?: boolean;
12
13
  onRadioChange?: (e: ChangeEvent<HTMLInputElement>) => void;
13
14
  }
14
- export declare function IDSRadioGroup({ name, required, noValidation, invalid, errorMsg, children, onRadioChange, ...props }: IDSRadioGroupProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function IDSRadioGroup({ name, required, noValidation, invalid, errorMsg, subtitle, children, onRadioChange, ...props }: IDSRadioGroupProps): import("react/jsx-runtime").JSX.Element;
15
16
  export declare namespace IDSRadioGroup {
16
17
  var displayName: string;
17
18
  }
@@ -5,7 +5,7 @@ import { IDSRadioGroupBase } from './radio-group-base.js';
5
5
  import { IDSRadio } from './radio.js';
6
6
  import { useGroupValidity } from '../form-hooks/useGroupValidity.js';
7
7
 
8
- function IDSRadioGroup({ name, required, noValidation, invalid, errorMsg, children, onRadioChange, ...props }) {
8
+ function IDSRadioGroup({ name, required, noValidation, invalid, errorMsg, subtitle, children, onRadioChange, ...props }) {
9
9
  const groupRef = useRef(null);
10
10
  const { isValid, hasInteracted } = useGroupValidity(groupRef, "radio");
11
11
  const reactId = useId();
@@ -31,7 +31,7 @@ function IDSRadioGroup({ name, required, noValidation, invalid, errorMsg, childr
31
31
  }
32
32
  return child;
33
33
  });
34
- return (jsx(IDSRadioGroupBase, { ...props, groupRef: groupRef, required: required, invalid: groupInvalid || invalid, errorMsg: !noValidation && errorMsg, errorMsgId: errorMsgId, children: clonedChildren }));
34
+ return (jsx(IDSRadioGroupBase, { ...props, groupRef: groupRef, required: required, subtitle: subtitle, invalid: groupInvalid || invalid, errorMsg: !noValidation && errorMsg, errorMsgId: errorMsgId, children: clonedChildren }));
35
35
  }
36
36
  IDSRadioGroup.displayName = "IDSRadioGroup";
37
37
 
@@ -10,7 +10,7 @@ export interface IDSRangeBaseProps extends React.InputHTMLAttributes<HTMLInputEl
10
10
  inputRef?: Ref<HTMLInputElement>;
11
11
  style?: CSSProperties;
12
12
  }
13
- export declare function IDSRangeBase({ id, label, showTicks, ticks, disabled, focusAnchor, dataTestId, tooltip, valueNow, min, max, step, inputRef, style, className, ...props }: IDSRangeBaseProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function IDSRangeBase({ id, label, showTicks, ticks, disabled, focusAnchor, dataTestId, tooltip, valueNow, min, max, step, inputRef, style, className, subtitle, ...props }: IDSRangeBaseProps): import("react/jsx-runtime").JSX.Element;
14
14
  export declare namespace IDSRangeBase {
15
15
  var displayName: string;
16
16
  }
@@ -2,9 +2,9 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useId } from 'react';
3
3
  import clsx from 'clsx';
4
4
 
5
- function IDSRangeBase({ id, label, showTicks, ticks = [], disabled, focusAnchor, dataTestId, tooltip, valueNow, min = 0, max = 10, step = 1, inputRef, style, className, ...props }) {
5
+ function IDSRangeBase({ id, label, showTicks, ticks = [], disabled, focusAnchor, dataTestId, tooltip, valueNow, min = 0, max = 10, step = 1, inputRef, style, className, subtitle, ...props }) {
6
6
  const inputId = !!id ? id : `range-${useId()}`;
7
- return (jsxs("div", { className: clsx("ids-range", className), "data-testid": dataTestId, style: style, children: [label && (jsxs("div", { className: "ids-label-wrapper ids-label-wrapper--margin-bottom", children: [jsx("label", { htmlFor: inputId, className: clsx("ids-label", { "ids-label--disabled": disabled }), children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), jsx("input", { id: inputId, ref: inputRef, type: "range", className: clsx("ids-range__input", {
7
+ return (jsxs("div", { className: clsx("ids-range", className), "data-testid": dataTestId, style: style, children: [label && (jsxs("div", { className: "ids-label-wrapper ids-label-wrapper--margin-bottom", children: [jsx("label", { htmlFor: inputId, className: "ids-label", children: label }), tooltip && jsx("span", { className: "ids-label__tooltip", children: tooltip })] })), subtitle && jsx("div", { className: "ids-subtitle", children: subtitle }), jsx("input", { id: inputId, ref: inputRef, type: "range", className: clsx("ids-range__input", {
8
8
  "ids-focus-anchor": focusAnchor
9
9
  }), min: min, "aria-valuemin": min, max: max, "aria-valuemax": max, "aria-valuenow": valueNow, "aria-disabled": disabled, disabled: disabled, step: step, style: {
10
10
  backgroundSize: ((valueNow - (min ?? 0)) * 100) / ((max ?? 100) - (min ?? 0)) + "% 100%"