@tui-cruises/mein-schiff-web-react-component-library 3.1.13 → 3.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [3.1.14](https://bitbucket.org/yours_truly/tuic-mein-schiff-web-react-component-library/compare/v3.1.13...v3.1.14) (2026-04-15)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BirthdateField:** add controlled value, minYear/maxYear, ISO-8601 support ([ba03b4a](https://bitbucket.org/yours_truly/tuic-mein-schiff-web-react-component-library/commit/ba03b4a443fdfbcb90a7dd24efacdd7106545827))
|
|
11
|
+
* **PhoneNumberInput:** add controlled value prop ([c33f049](https://bitbucket.org/yours_truly/tuic-mein-schiff-web-react-component-library/commit/c33f049febe5ffe0304951b85a1953ecb4b5d409))
|
|
12
|
+
|
|
5
13
|
### [3.1.13](https://bitbucket.org/yours_truly/tuic-mein-schiff-web-react-component-library/compare/v3.1.12...v3.1.13) (2026-04-15)
|
|
6
14
|
|
|
7
15
|
|
package/package.json
CHANGED
|
@@ -42,10 +42,17 @@ export type BirthdateFieldProps = InputAttributes & {
|
|
|
42
42
|
className?: string;
|
|
43
43
|
locale?: string;
|
|
44
44
|
onChange?: (value: string) => void;
|
|
45
|
-
|
|
45
|
+
/** Uncontrolled initial value (YYYY-MM-DD). Ignored if `value` is provided. */
|
|
46
|
+
defaultValue?: string;
|
|
47
|
+
/** Controlled value (YYYY-MM-DD). When provided, internal state syncs to this prop. */
|
|
48
|
+
value?: string;
|
|
46
49
|
readOnly?: Boolean;
|
|
47
50
|
disabled?: Boolean;
|
|
48
51
|
legend?: string;
|
|
52
|
+
/** Minimum allowed year. Defaults to `currentYear - 100` (birthday mode). */
|
|
53
|
+
minYear?: number;
|
|
54
|
+
/** Maximum allowed year. Defaults to `currentYear` (birthday mode). */
|
|
55
|
+
maxYear?: number;
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
type SetPartFunction = {
|
|
@@ -99,7 +106,9 @@ const createCombinedValue = (day: string, month: string, year: string) => {
|
|
|
99
106
|
* @returns {object} An object containing day, month and year as strings.
|
|
100
107
|
*/
|
|
101
108
|
const parseInitialValue = (value: string) => {
|
|
102
|
-
|
|
109
|
+
// Strip time portion from ISO 8601 strings (e.g. "2020-01-05T00:00:00Z" → "2020-01-05")
|
|
110
|
+
const dateOnly = value.split('T')[0] ?? '';
|
|
111
|
+
const parts = dateOnly.split('-');
|
|
103
112
|
return {
|
|
104
113
|
year: normalizeInitialValueSubPart(parts[0] ?? ''),
|
|
105
114
|
month: normalizeInitialValueSubPart(parts[1] ?? ''),
|
|
@@ -135,16 +144,21 @@ const BirthdateField = forwardRef<HTMLInputElement, BirthdateFieldProps>(
|
|
|
135
144
|
locale = 'de-DE',
|
|
136
145
|
onChange,
|
|
137
146
|
defaultValue,
|
|
147
|
+
value,
|
|
138
148
|
readOnly,
|
|
139
149
|
disabled,
|
|
140
150
|
legend,
|
|
151
|
+
minYear,
|
|
152
|
+
maxYear,
|
|
141
153
|
...props
|
|
142
154
|
},
|
|
143
155
|
ref,
|
|
144
156
|
) => {
|
|
145
157
|
const format = getFormatBasedOnLocale(locale);
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
// When `value` is provided the component is controlled; otherwise we
|
|
159
|
+
// seed internal state from `defaultValue`.
|
|
160
|
+
const initialValueAsString = (value ?? defaultValue)?.toString() ?? '';
|
|
161
|
+
const initials = parseInitialValue(initialValueAsString);
|
|
148
162
|
const [day, setDay] = useState(initials.day);
|
|
149
163
|
const [month, setMonth] = useState(initials.month);
|
|
150
164
|
const [year, setYear] = useState(initials.year);
|
|
@@ -152,7 +166,11 @@ const BirthdateField = forwardRef<HTMLInputElement, BirthdateFieldProps>(
|
|
|
152
166
|
createCombinedValue(initials.day, initials.month, initials.year),
|
|
153
167
|
);
|
|
154
168
|
|
|
155
|
-
|
|
169
|
+
// Tracks the last combined value we either emitted via onChange or
|
|
170
|
+
// received via the `value` prop — used to avoid firing onChange in
|
|
171
|
+
// response to our own externally-echoed value, and to detect when
|
|
172
|
+
// an external `value` change needs to be synced into internal state.
|
|
173
|
+
const prevValue = useRef(combinedValue);
|
|
156
174
|
const dayRef = useRef<HTMLInputElement>(null);
|
|
157
175
|
const monthRef = useRef<HTMLInputElement>(null);
|
|
158
176
|
const yearRef = useRef<HTMLInputElement>(null);
|
|
@@ -175,6 +193,21 @@ const BirthdateField = forwardRef<HTMLInputElement, BirthdateFieldProps>(
|
|
|
175
193
|
}
|
|
176
194
|
}, [day, month, year, onChange]);
|
|
177
195
|
|
|
196
|
+
// Controlled mode: when the `value` prop changes from outside (i.e.
|
|
197
|
+
// differs from what we last emitted) re-seed internal state from it.
|
|
198
|
+
// Comparing against `prevValue` avoids a feedback loop when the parent
|
|
199
|
+
// echoes our own onChange value back as `value`.
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (value === undefined) return;
|
|
202
|
+
const next = value.toString();
|
|
203
|
+
if (next === prevValue.current) return;
|
|
204
|
+
const parts = parseInitialValue(next);
|
|
205
|
+
prevValue.current = createCombinedValue(parts.day, parts.month, parts.year);
|
|
206
|
+
setDay(parts.day);
|
|
207
|
+
setMonth(parts.month);
|
|
208
|
+
setYear(parts.year);
|
|
209
|
+
}, [value]);
|
|
210
|
+
|
|
178
211
|
const handleChange = (
|
|
179
212
|
part: PartKey,
|
|
180
213
|
value: string,
|
|
@@ -232,10 +265,14 @@ const BirthdateField = forwardRef<HTMLInputElement, BirthdateFieldProps>(
|
|
|
232
265
|
nextRef,
|
|
233
266
|
)
|
|
234
267
|
}
|
|
235
|
-
min={
|
|
268
|
+
min={
|
|
269
|
+
part === 'year'
|
|
270
|
+
? (minYear ?? new Date().getFullYear() - 100) // Oldest passenger age: 100
|
|
271
|
+
: 1
|
|
272
|
+
}
|
|
236
273
|
max={
|
|
237
274
|
part === 'year'
|
|
238
|
-
? new Date().getFullYear()
|
|
275
|
+
? (maxYear ?? new Date().getFullYear())
|
|
239
276
|
: part === 'month'
|
|
240
277
|
? 12
|
|
241
278
|
: 31
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
InputHTMLAttributes,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useId,
|
|
9
|
+
ReactNode,
|
|
10
|
+
} from 'react';
|
|
4
11
|
import { Icon } from '../../core/Icon';
|
|
5
12
|
import * as Form from '@radix-ui/react-form';
|
|
6
13
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
@@ -17,7 +24,10 @@ import { CountryCode, parsePhoneNumber } from 'libphonenumber-js';
|
|
|
17
24
|
import { InputFieldError } from '../InputField/InputFieldError';
|
|
18
25
|
import { twMerge } from 'tailwind-merge';
|
|
19
26
|
|
|
20
|
-
type PhoneNumberInputProps =
|
|
27
|
+
type PhoneNumberInputProps = Omit<
|
|
28
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
29
|
+
'value'
|
|
30
|
+
> & {
|
|
21
31
|
countries?: PhoneNumberCountry[];
|
|
22
32
|
defaultCountryCode?: string;
|
|
23
33
|
invalid?: boolean;
|
|
@@ -25,6 +35,12 @@ type PhoneNumberInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
|
25
35
|
error?: string;
|
|
26
36
|
ariaLabelCountryCode?: string;
|
|
27
37
|
ariaLabelPhoneNumber?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Controlled value as a combined string (e.g. `"+49 1234567"`). When
|
|
40
|
+
* provided, internal state syncs to this prop. Use together with
|
|
41
|
+
* `onValueChange` for full controlled behavior.
|
|
42
|
+
*/
|
|
43
|
+
value?: string;
|
|
28
44
|
/** Called whenever the value changes with the dial code and number separately. */
|
|
29
45
|
onValueChange?: (value: { dialCode: string; localNumber: string }) => void;
|
|
30
46
|
};
|
|
@@ -89,19 +105,22 @@ const PhoneNumberInputRenderFunction: ForwardRefRenderFunction<
|
|
|
89
105
|
onValueChange,
|
|
90
106
|
readOnly,
|
|
91
107
|
disabled,
|
|
108
|
+
defaultValue,
|
|
109
|
+
value,
|
|
92
110
|
...attrs
|
|
93
111
|
} = props;
|
|
94
112
|
|
|
113
|
+
// When `value` is provided the component is controlled; otherwise we
|
|
114
|
+
// seed internal state from `defaultValue`.
|
|
115
|
+
const initialValueAsString = (value ?? defaultValue)?.toString() ?? '';
|
|
116
|
+
|
|
95
117
|
const defaultCountry = getDefaultCountry(
|
|
96
118
|
defaultCountryCode,
|
|
97
|
-
|
|
119
|
+
initialValueAsString,
|
|
98
120
|
);
|
|
99
121
|
|
|
100
122
|
const [typedInNumber, setTypedInNumber] = useState(
|
|
101
|
-
|
|
102
|
-
?.toString()
|
|
103
|
-
.substring(defaultCountry.dialCode.length)
|
|
104
|
-
.trim() || '',
|
|
123
|
+
initialValueAsString.substring(defaultCountry.dialCode.length).trim() || '',
|
|
105
124
|
);
|
|
106
125
|
|
|
107
126
|
const [selectedCountryCode, setSelectedCountryCode] = useState<string>(
|
|
@@ -112,11 +131,31 @@ const PhoneNumberInputRenderFunction: ForwardRefRenderFunction<
|
|
|
112
131
|
countries.find(country => country.code === selectedCountryCode)?.dialCode ||
|
|
113
132
|
'';
|
|
114
133
|
|
|
134
|
+
// Tracks the last combined `${dialCode} ${localNumber}`.trim() value we
|
|
135
|
+
// either emitted via onValueChange or received via the `value` prop —
|
|
136
|
+
// used to avoid re-syncing internal state when the parent echoes our
|
|
137
|
+
// own value back, and to detect genuine external `value` changes.
|
|
138
|
+
const lastCombinedRef = useRef<string>(initialValueAsString);
|
|
139
|
+
|
|
140
|
+
// Controlled mode: re-seed internal state when `value` changes externally.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (value === undefined) return;
|
|
143
|
+
const asString = value.toString();
|
|
144
|
+
if (asString === lastCombinedRef.current) return;
|
|
145
|
+
lastCombinedRef.current = asString;
|
|
146
|
+
const country = getDefaultCountry(defaultCountryCode, asString);
|
|
147
|
+
const localNumber =
|
|
148
|
+
asString.substring(country.dialCode.length).trim() || '';
|
|
149
|
+
setSelectedCountryCode(country.code);
|
|
150
|
+
setTypedInNumber(localNumber);
|
|
151
|
+
}, [value, defaultCountryCode]);
|
|
152
|
+
|
|
115
153
|
const handleCountryChange = (value: string) => {
|
|
116
154
|
setSelectedCountryCode(value);
|
|
117
155
|
setTypedInNumber('');
|
|
118
156
|
const newDialCode =
|
|
119
157
|
countries.find(country => country.code === value)?.dialCode || '';
|
|
158
|
+
lastCombinedRef.current = newDialCode.trim();
|
|
120
159
|
onValueChange?.({ dialCode: newDialCode, localNumber: '' });
|
|
121
160
|
};
|
|
122
161
|
|
|
@@ -127,6 +166,7 @@ const PhoneNumberInputRenderFunction: ForwardRefRenderFunction<
|
|
|
127
166
|
.replace(/[^\d\s]/g, '')
|
|
128
167
|
.trim();
|
|
129
168
|
setTypedInNumber(justNumber);
|
|
169
|
+
lastCombinedRef.current = `${selectedCountryDialCode} ${justNumber}`.trim();
|
|
130
170
|
onValueChange?.({
|
|
131
171
|
dialCode: selectedCountryDialCode,
|
|
132
172
|
localNumber: justNumber,
|