@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tui-cruises/mein-schiff-web-react-component-library",
3
- "version": "3.1.13",
3
+ "version": "3.1.14",
4
4
  "main": "./index.tsx",
5
5
  "types": "./index.tsx",
6
6
  "type": "module",
@@ -42,10 +42,17 @@ export type BirthdateFieldProps = InputAttributes & {
42
42
  className?: string;
43
43
  locale?: string;
44
44
  onChange?: (value: string) => void;
45
- defaultValue?: string; // YYYY-MM-JJ
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
- const parts = value.split('-');
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
- const defaultValueAsString = defaultValue?.toString() ?? '';
147
- const initials = parseInitialValue(defaultValueAsString);
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
- const prevValue = useRef(defaultValueAsString);
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={part === 'year' ? new Date().getFullYear() - 100 : 1} // Oldest passenger age: 100
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 { InputHTMLAttributes, useRef, useState, useId, ReactNode } from 'react';
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 = InputHTMLAttributes<HTMLInputElement> & {
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
- props.defaultValue,
119
+ initialValueAsString,
98
120
  );
99
121
 
100
122
  const [typedInNumber, setTypedInNumber] = useState(
101
- props.defaultValue
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,