@gooddata/sdk-ui-kit 11.52.0-alpha.5 → 11.52.0-alpha.6

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2020-2026 GoodData Corporation
3
- import { PureComponent, createRef, } from "react";
3
+ import { PureComponent, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
4
  import classNames from "classnames";
5
5
  import { format, isSameDay, isValid, parse } from "date-fns";
6
6
  import { de, enAU, enGB, enUS, es, fi, fr, frCA, id, it, ja, ko, nl, pl, pt, ptBR, ru, sl, th, tr, uk, vi, zhCN, } from "date-fns/locale";
@@ -81,191 +81,147 @@ function convertWeekStart(weekStart) {
81
81
  throw new Error(`Unknown week start ${weekStart}`);
82
82
  }
83
83
  }
84
- export class WrappedDatePicker extends PureComponent {
85
- rootRef = null;
86
- datePickerContainerRef = createRef();
87
- inputRef = createRef();
88
- datePickerId = uuid();
89
- static defaultProps = {
90
- className: "",
91
- date: new Date(),
92
- placeholder: "",
93
- onChange: () => { },
94
- onBlur: () => { },
95
- resetOnInvalidValue: false,
96
- size: "",
97
- tabIndex: 0,
98
- alignPoints: [{ align: "bl tl" }, { align: "br tr" }, { align: "tl bl" }, { align: "tr br" }],
99
- onAlign: () => { },
100
- dateFormat: DEFAULT_DATE_FORMAT,
101
- weekStart: "Sunday",
102
- };
103
- constructor(props) {
104
- super(props);
105
- const { alignPoints, date, dateFormat } = props;
106
- this.state = {
107
- align: alignPoints?.[0]?.align ?? "bl tl",
108
- selectedDate: date,
109
- monthDate: date,
110
- inputValue: formatDate(date || new Date(), dateFormat ?? DEFAULT_DATE_FORMAT),
111
- isOpen: false,
112
- };
113
- this.handleDayChanged = this.handleDayChanged.bind(this);
114
- this.handleMonthChanged = this.handleMonthChanged.bind(this);
115
- this.handleInputChanged = this.handleInputChanged.bind(this);
116
- this.handleInputBlur = this.handleInputBlur.bind(this);
117
- this.alignDatePicker = this.alignDatePicker.bind(this);
118
- this.setComponentRef = this.setComponentRef.bind(this);
119
- this.handleWrapperClick = this.handleWrapperClick.bind(this);
120
- this.handleClickOutside = this.handleClickOutside.bind(this);
121
- this.onKeyDown = this.onKeyDown.bind(this);
122
- this.handleCustomDayClick = this.handleCustomDayClick.bind(this);
123
- }
124
- componentDidMount() {
125
- const { date, dateFormat } = this.props;
126
- this.setState({ selectedDate: this.updateDate(date || new Date()) });
127
- this.setState({ inputValue: formatDate(date || new Date(), dateFormat ?? DEFAULT_DATE_FORMAT) });
128
- window.addEventListener("resize", this.resizeHandler);
129
- document.addEventListener("mousedown", this.handleClickOutside);
130
- }
131
- UNSAFE_componentWillReceiveProps(nextProps) {
132
- const { props } = this;
133
- const propsDate = props.date?.getTime() ?? 0;
134
- const nextPropsDate = nextProps.date?.getTime() ?? 0;
135
- if (propsDate !== nextPropsDate) {
136
- const selectedDate = this.updateDate(nextProps.date || new Date());
137
- this.setState({ selectedDate });
138
- this.setState({ monthDate: selectedDate });
139
- this.setState({ inputValue: formatDate(selectedDate, props.dateFormat ?? DEFAULT_DATE_FORMAT) });
140
- }
141
- }
142
- componentWillUnmount() {
143
- window.removeEventListener("resize", this.resizeHandler);
144
- document.removeEventListener("mousedown", this.handleClickOutside);
84
+ function normalizeDate(date) {
85
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
86
+ }
87
+ const DEFAULT_ALIGN_POINTS = [
88
+ { align: "bl tl" },
89
+ { align: "br tr" },
90
+ { align: "tl bl" },
91
+ { align: "tr br" },
92
+ ];
93
+ // evaluated once per module load, mirroring the original static defaultProps
94
+ const DEFAULT_DATE = new Date();
95
+ function WrappedDatePickerCore({ accessibilityConfig, date: dateProp, className = "", placeholder = "", onChange, onBlur, onValidateInput, resetOnInvalidValue = false, size = "", tabIndex = 0, alignPoints: alignPointsProp, onAlign, dateFormat: dateFormatProp, weekStart: weekStartProp, onDateInputKeyDown, intl, }) {
96
+ // these props are nullable at runtime (untyped callers do pass null explicitly), so plain default
97
+ // values are not enough - the class component guarded every usage with `||`, `??` or optional calls
98
+ const date = dateProp || DEFAULT_DATE;
99
+ const alignPoints = alignPointsProp ?? DEFAULT_ALIGN_POINTS;
100
+ const dateFormat = dateFormatProp ?? DEFAULT_DATE_FORMAT;
101
+ const weekStart = weekStartProp ?? "Sunday";
102
+ const rootRef = useRef(null);
103
+ const datePickerContainerRef = useRef(null);
104
+ const inputRef = useRef(null);
105
+ const datePickerId = useMemo(() => uuid(), []);
106
+ const [align, setAlign] = useState(alignPoints[0]?.align ?? "bl tl");
107
+ const [selectedDate, setSelectedDate] = useState(() => normalizeDate(date));
108
+ const [monthDate, setMonthDate] = useState(date);
109
+ const [inputValue, setInputValue] = useState(() => formatDate(date, dateFormat));
110
+ const [isOpen, setIsOpen] = useState(false);
111
+ // resync the derived state whenever the date coming from the props changes
112
+ const [prevDateTime, setPrevDateTime] = useState(date.getTime());
113
+ if (date.getTime() !== prevDateTime) {
114
+ const newlySelectedDate = normalizeDate(date);
115
+ setPrevDateTime(date.getTime());
116
+ setSelectedDate(newlySelectedDate);
117
+ setMonthDate(newlySelectedDate);
118
+ setInputValue(formatDate(newlySelectedDate, dateFormat));
145
119
  }
146
- handleClickOutside(event) {
147
- if (this.datePickerContainerRef.current &&
148
- !this.datePickerContainerRef.current.contains(event.target) &&
149
- this.inputRef.current &&
150
- !this.inputRef.current.contains(event.target)) {
151
- this.setState({ isOpen: false });
120
+ const alignDatePicker = useCallback(() => {
121
+ const container = datePickerContainerRef.current?.parentElement;
122
+ if (!alignPoints || !container || !rootRef.current) {
123
+ return;
152
124
  }
153
- }
154
- componentDidUpdate(_prevProps, prevState) {
155
- if (this.state.isOpen && !prevState.isOpen) {
156
- this.alignDatePicker();
125
+ const optimalAlignment = getOptimalAlignment({
126
+ targetRegion: elementRegion(rootRef.current),
127
+ selfRegion: elementRegion(container),
128
+ alignPoints,
129
+ });
130
+ const { align: optimalAlign } = optimalAlignment.alignment;
131
+ setAlign(optimalAlign);
132
+ onAlign?.(optimalAlign);
133
+ }, [alignPoints, onAlign]);
134
+ // keeps the debounced resize handler and the "just opened" effect stable while still
135
+ // calling the up-to-date alignment logic, the same way the class instance method did
136
+ const alignDatePickerRef = useRef(alignDatePicker);
137
+ // must run before the "just opened" layout effect below, hence useLayoutEffect
138
+ useLayoutEffect(() => {
139
+ alignDatePickerRef.current = alignDatePicker;
140
+ }, [alignDatePicker]);
141
+ const resizeHandler = useMemo(() => debounce(() => alignDatePickerRef.current(), 100), []);
142
+ useEffect(() => {
143
+ window.addEventListener("resize", resizeHandler);
144
+ return () => {
145
+ resizeHandler.cancel();
146
+ window.removeEventListener("resize", resizeHandler);
147
+ };
148
+ }, [resizeHandler]);
149
+ useEffect(() => {
150
+ const handleClickOutside = (event) => {
151
+ if (datePickerContainerRef.current &&
152
+ !datePickerContainerRef.current.contains(event.target) &&
153
+ inputRef.current &&
154
+ !inputRef.current.contains(event.target)) {
155
+ setIsOpen(false);
156
+ }
157
+ };
158
+ document.addEventListener("mousedown", handleClickOutside);
159
+ return () => {
160
+ document.removeEventListener("mousedown", handleClickOutside);
161
+ };
162
+ }, []);
163
+ useLayoutEffect(() => {
164
+ if (isOpen) {
165
+ alignDatePickerRef.current();
157
166
  }
158
- }
159
- setComponentRef(ref) {
160
- this.rootRef = ref;
161
- }
162
- getInputClasses() {
163
- return classNames("input-text", "small-12", this.props.size, `gd-datepicker-input-${this.datePickerId}`);
164
- }
165
- getComponentClasses() {
166
- return classNames("gd-datepicker", this.props.className, this.props.size, "gd-datepicker-input", this.state.isOpen ? "gd-datepicker-focused" : "");
167
- }
168
- getOverlayWrapperClasses() {
169
- const [inputAnchorPoint, pickerAnchorPoint] = this.state.align.split(" ");
170
- return classNames("gd-datepicker-picker", "gd-datepicker-OverlayWrapper", `gd-datepicker-OverlayWrapper-${inputAnchorPoint}-xx`, `gd-datepicker-OverlayWrapper-xx-${pickerAnchorPoint}`);
171
- }
172
- resizeHandler = debounce(() => this.alignDatePicker(), 100);
173
- updateDate(date) {
174
- return this.normalizeDate(date);
175
- }
176
- handleInputBlur(e) {
177
- this.props.onBlur?.(e.target.value);
178
- }
179
- handleInputChanged(e) {
167
+ }, [isOpen]);
168
+ const handleInputBlur = useCallback((e) => {
169
+ onBlur?.(e.target.value);
170
+ }, [onBlur]);
171
+ const handleInputChanged = useCallback((e) => {
180
172
  const { value } = e.target;
181
- const parsedDate = parseDate(value, this.props.dateFormat ?? DEFAULT_DATE_FORMAT);
182
- this.props.onValidateInput?.(value);
183
- this.setState({ inputValue: value });
173
+ const parsedDate = parseDate(value, dateFormat);
174
+ onValidateInput?.(value);
175
+ setInputValue(value);
184
176
  if (parsedDate) {
185
- this.setState({
186
- selectedDate: parsedDate,
187
- monthDate: parsedDate,
188
- }, () => {
189
- if (this.state.selectedDate) {
190
- this.props.onChange?.(this.state.selectedDate);
191
- }
192
- });
177
+ setSelectedDate(parsedDate);
178
+ setMonthDate(parsedDate);
179
+ onChange?.(parsedDate);
180
+ return;
193
181
  }
194
- else {
195
- if (this.props.resetOnInvalidValue) {
196
- this.setState({
197
- selectedDate: this.state.selectedDate,
198
- monthDate: this.state.selectedDate,
199
- });
200
- return;
201
- }
202
- this.setState({
203
- selectedDate: undefined,
204
- monthDate: undefined,
205
- }, () => {
206
- // Signal invalid state by passing null
207
- this.props.onChange?.(null);
208
- });
182
+ if (resetOnInvalidValue) {
183
+ setMonthDate(selectedDate);
184
+ return;
209
185
  }
210
- }
211
- handleDayChanged(newlySelectedDate) {
186
+ setSelectedDate(undefined);
187
+ setMonthDate(undefined);
188
+ // Signal invalid state by passing null
189
+ onChange?.(null);
190
+ }, [dateFormat, onChange, onValidateInput, resetOnInvalidValue, selectedDate]);
191
+ const handleDayChanged = useCallback((newlySelectedDate) => {
212
192
  if (!newlySelectedDate) {
213
- this.setState({ isOpen: false });
193
+ setIsOpen(false);
214
194
  return;
215
195
  }
216
- if (this.state.selectedDate && isSameDay(this.state.selectedDate, newlySelectedDate)) {
217
- this.setState({ isOpen: false });
196
+ if (selectedDate && isSameDay(selectedDate, newlySelectedDate)) {
197
+ setIsOpen(false);
218
198
  return;
219
199
  }
220
- this.inputRef.current?.focus();
221
- this.props.onValidateInput?.(formatDate(newlySelectedDate, this.props.dateFormat ?? DEFAULT_DATE_FORMAT));
222
- this.setState({
223
- selectedDate: newlySelectedDate,
224
- monthDate: newlySelectedDate,
225
- inputValue: formatDate(newlySelectedDate, this.props.dateFormat ?? DEFAULT_DATE_FORMAT),
226
- isOpen: false,
227
- }, () => {
228
- this.props.onChange?.(newlySelectedDate);
229
- });
230
- }
231
- handleMonthChanged(month) {
232
- this.inputRef.current?.focus();
233
- this.setState({ monthDate: month });
234
- }
235
- handleCustomDayClick = (day, _modifiers) => {
200
+ inputRef.current?.focus();
201
+ onValidateInput?.(formatDate(newlySelectedDate, dateFormat));
202
+ setSelectedDate(newlySelectedDate);
203
+ setMonthDate(newlySelectedDate);
204
+ setInputValue(formatDate(newlySelectedDate, dateFormat));
205
+ setIsOpen(false);
206
+ onChange?.(newlySelectedDate);
207
+ }, [dateFormat, onChange, onValidateInput, selectedDate]);
208
+ const handleMonthChanged = useCallback((month) => {
209
+ inputRef.current?.focus();
210
+ setMonthDate(month);
211
+ }, []);
212
+ const handleCustomDayClick = useCallback((day, _modifiers) => {
236
213
  // Handle all day clicks, including outside days
237
- this.handleDayChanged(day);
238
- };
239
- normalizeDate(date) {
240
- return new Date(date.getFullYear(), date.getMonth(), date.getDate());
241
- }
242
- alignDatePicker() {
243
- const { alignPoints } = this.props;
244
- const container = this.datePickerContainerRef.current?.parentElement;
245
- if (!alignPoints || !container || !this.rootRef) {
246
- return;
247
- }
248
- const optimalAlignment = getOptimalAlignment({
249
- targetRegion: elementRegion(this.rootRef),
250
- selfRegion: elementRegion(container),
251
- alignPoints,
252
- });
253
- const { align } = optimalAlignment.alignment;
254
- this.setState({
255
- align,
256
- }, () => {
257
- this.props.onAlign?.(align);
258
- });
259
- }
260
- onKeyDown(e) {
214
+ handleDayChanged(day);
215
+ }, [handleDayChanged]);
216
+ const handleKeyDown = useCallback((e) => {
261
217
  if (e.key === "Escape" || e.key === "Tab") {
262
- this.setState({ isOpen: false });
218
+ setIsOpen(false);
263
219
  }
264
220
  if (isEnterKey(e)) {
265
- this.props.onDateInputKeyDown?.(e);
221
+ onDateInputKeyDown?.(e);
266
222
  }
267
- }
268
- handleWrapperClick(e) {
223
+ }, [onDateInputKeyDown]);
224
+ const handleWrapperClick = useCallback((e) => {
269
225
  const { classList } = e.target;
270
226
  /**
271
227
  * Prevent default fixes bug BB-332 but prevents in closing other dropdowns (Bug BB-1102)
@@ -274,19 +230,25 @@ export class WrappedDatePicker extends PureComponent {
274
230
  if (e.target && classList?.contains(DATEPICKER_OUTSIDE_DAY_SELECTOR)) {
275
231
  e.preventDefault();
276
232
  }
277
- }
278
- render() {
279
- const { inputValue, selectedDate, monthDate, isOpen } = this.state;
280
- const { accessibilityConfig, placeholder, intl, tabIndex } = this.props;
281
- const classNamesProps = {
282
- root: this.getOverlayWrapperClasses(),
233
+ }, []);
234
+ const handleInputClick = useCallback(() => {
235
+ setIsOpen(true);
236
+ }, []);
237
+ const dayPickerClassNames = useMemo(() => {
238
+ const [inputAnchorPoint, pickerAnchorPoint] = align.split(" ");
239
+ return {
240
+ root: classNames("gd-datepicker-picker", "gd-datepicker-OverlayWrapper", `gd-datepicker-OverlayWrapper-${inputAnchorPoint}-xx`, `gd-datepicker-OverlayWrapper-xx-${pickerAnchorPoint}`),
283
241
  };
284
- return (_jsxs("div", { "data-testid": "datepicker", className: this.getComponentClasses(), ref: this.setComponentRef, onClick: this.handleWrapperClick, children: [
285
- _jsx("input", { autoComplete: "off", "aria-labelledby": accessibilityConfig?.ariaLabelledBy, "aria-label": accessibilityConfig?.ariaLabel ||
286
- intl.formatMessage({ id: "datePicker.accessibility.label" }), "aria-describedby": accessibilityConfig?.ariaDescribedBy, onKeyDown: this.onKeyDown, tabIndex: tabIndex, onClick: () => this.setState({ isOpen: true }), ref: this.inputRef, value: inputValue, className: this.getInputClasses(), placeholder: placeholder, onChange: this.handleInputChanged, onBlur: this.handleInputBlur }), isOpen ? (_jsx("div", { id: `datepicker-popup-${this.datePickerId}`, role: "dialog", ref: this.datePickerContainerRef, children: _jsx(DayPicker, { classNames: classNamesProps, locale: convertLocale(intl.locale), showOutsideDays: true, mode: "single", selected: selectedDate, month: monthDate, onMonthChange: this.handleMonthChanged, weekStartsOn: convertWeekStart(this.props.weekStart ?? "Sunday"), onDayClick: this.handleCustomDayClick }) })) : null, _jsx("span", { className: "gd-datepicker-icon gd-icon-calendar" })
287
- ] }));
288
- }
242
+ }, [align]);
243
+ const componentClasses = classNames("gd-datepicker", className, size, "gd-datepicker-input", isOpen ? "gd-datepicker-focused" : "");
244
+ const inputClasses = classNames("input-text", "small-12", size, `gd-datepicker-input-${datePickerId}`);
245
+ return (_jsxs("div", { "data-testid": "datepicker", className: componentClasses, ref: rootRef, onClick: handleWrapperClick, children: [
246
+ _jsx("input", { autoComplete: "off", "aria-labelledby": accessibilityConfig?.ariaLabelledBy, "aria-label": accessibilityConfig?.ariaLabel ||
247
+ intl.formatMessage({ id: "datePicker.accessibility.label" }), "aria-describedby": accessibilityConfig?.ariaDescribedBy, onKeyDown: handleKeyDown, tabIndex: tabIndex, onClick: handleInputClick, ref: inputRef, value: inputValue, className: inputClasses, placeholder: placeholder, onChange: handleInputChanged, onBlur: handleInputBlur }), isOpen ? (_jsx("div", { id: `datepicker-popup-${datePickerId}`, role: "dialog", ref: datePickerContainerRef, children: _jsx(DayPicker, { classNames: dayPickerClassNames, locale: convertLocale(intl.locale), showOutsideDays: true, mode: "single", selected: selectedDate, month: monthDate, onMonthChange: handleMonthChanged, weekStartsOn: convertWeekStart(weekStart), onDayClick: handleCustomDayClick }) })) : null, _jsx("span", { className: "gd-datepicker-icon gd-icon-calendar" })
248
+ ] }));
289
249
  }
250
+ export const WrappedDatePicker = memo(WrappedDatePickerCore);
251
+ WrappedDatePicker.displayName = "WrappedDatePicker";
290
252
  const DatePickerWithIntl = injectIntl(WrappedDatePicker);
291
253
  /**
292
254
  * @internal
@@ -1,7 +1,6 @@
1
1
  import { type ChangeEvent } from "react";
2
2
  import { type IInputPureProps } from "./InputPure.js";
3
3
  import { type Separators } from "./typings.js";
4
- export declare const MAX_NUMBER: number;
5
4
  /**
6
5
  * @internal
7
6
  */
@@ -1 +1 @@
1
- {"version":3,"file":"InputWithNumberFormat.d.ts","sourceRoot":"","sources":["../../src/Form/InputWithNumberFormat.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAsD,MAAM,OAAO,CAAC;AAI7F,OAAO,EAAE,KAAK,eAAe,EAAa,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,eAAO,MAAM,UAAU,QAAW,CAAC;AA6DnC;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED;;GAEG;AAEH,MAAM,WAAW,2BAA2B;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AAEH,MAAM,WAAW,2BACb,SAAQ,8BAA8B,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC;IACzE,qGAAqG;IACrG,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;CAChF;AAUD;;GAEG;AACH,eAAO,MAAM,qBAAqB,mEAsEhC,CAAC"}
1
+ {"version":3,"file":"InputWithNumberFormat.d.ts","sourceRoot":"","sources":["../../src/Form/InputWithNumberFormat.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAsD,MAAM,OAAO,CAAC;AAI7F,OAAO,EAAE,KAAK,eAAe,EAAa,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAmD/C;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED;;GAEG;AAEH,MAAM,WAAW,2BAA2B;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AAEH,MAAM,WAAW,2BACb,SAAQ,8BAA8B,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC;IACzE,qGAAqG;IACrG,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;CAChF;AAUD;;GAEG;AACH,eAAO,MAAM,qBAAqB,mEAsEhC,CAAC"}
@@ -4,20 +4,9 @@ import { memo, useEffect, useRef, useState } from "react";
4
4
  import { memoize } from "lodash-es";
5
5
  import { InputPure } from "./InputPure.js";
6
6
  import { DEFAULT_SEPARATORS, formatNumberWithSeparators } from "./numberFormat.js";
7
- // Highest number (BIGINT) according to gooddata documentation help.gooddata.com object-datatypes
8
- export const MAX_NUMBER = 10 ** 15;
9
- // Max number of digits right to decimal point according to gooddata documentation help.gooddata.com object-datatypes
10
- const MAX_DECIMAL_POINT_NUMBERS = 6;
11
7
  const getDanglingDecimalPointRegExp = memoize((decimal) => new RegExp(`\\${decimal}$`));
12
- const getFormatValidationRegExp = memoize(({ thousand, decimal }) => new RegExp(`^-?(\\d|\\${thousand})*(\\${decimal}\\d*)?$`));
13
- const parseStandardNumberString = (numberString) => {
14
- const belowDecimal = numberString.split(".")[1];
15
- const roundedNumberString = belowDecimal && belowDecimal.length >= MAX_DECIMAL_POINT_NUMBERS
16
- ? parseFloat(numberString).toFixed(MAX_DECIMAL_POINT_NUMBERS)
17
- : numberString;
18
- const number = parseFloat(roundedNumberString);
19
- return number === 0 ? 0 : number;
20
- };
8
+ // A half-typed exponent ("1e", "1e+") stays valid so it can be entered one character at a time.
9
+ const buildValidationRegExp = ({ thousand, decimal }) => new RegExp(`^-?(\\d|\\${thousand})*(\\${decimal}\\d*)?([eE][-+]?\\d*)?$`);
21
10
  // Removes thousand separators while keeping the decimal separator intact, so the value can be
22
11
  // edited as plain digits (no separators popping in/out) while the input is focused.
23
12
  const removeThousandSeparators = (value, { thousand } = DEFAULT_SEPARATORS) => value.split(thousand).join("");
@@ -27,7 +16,7 @@ const convertFormattedStringToStandard = (formattedString, { thousand, decimal }
27
16
  const withStandardDecimalPoint = withoutDanglingDecimalPoint.split(decimal).join(".");
28
17
  return withStandardDecimalPoint.length > 0 ? withStandardDecimalPoint : null;
29
18
  };
30
- const parse = (value, separators = DEFAULT_SEPARATORS) => {
19
+ const parse = (value, separators) => {
31
20
  if (value === null || value === "" || value === "-") {
32
21
  return null;
33
22
  }
@@ -35,11 +24,16 @@ const parse = (value, separators = DEFAULT_SEPARATORS) => {
35
24
  if (numberString === null) {
36
25
  return null;
37
26
  }
38
- return parseStandardNumberString(numberString);
27
+ const number = parseFloat(numberString);
28
+ return number === 0 ? 0 : number;
39
29
  };
40
- const isValid = (value, separators = DEFAULT_SEPARATORS) => {
30
+ // The wire layer draws the same line: it throws on Infinity/NaN.
31
+ const isValid = (value, separators) => {
32
+ if (!buildValidationRegExp(separators).test(value)) {
33
+ return false;
34
+ }
41
35
  const parsed = parse(value, separators);
42
- return getFormatValidationRegExp(separators).test(value) && Math.abs(parsed ?? 0) <= MAX_NUMBER;
36
+ return parsed === null || Number.isFinite(parsed);
43
37
  };
44
38
  // Coerces input value to number for formatting (handles string/number/null/undefined)
45
39
  const toNumberValue = (value) => {
@@ -51,7 +45,7 @@ const toNumberValue = (value) => {
51
45
  /**
52
46
  * @internal
53
47
  */
54
- export const InputWithNumberFormat = memo(function InputWithNumberFormat({ separators, value: propValue, onChange, onFocus, onBlur, ...restProps }) {
48
+ export const InputWithNumberFormat = memo(function InputWithNumberFormat({ separators = DEFAULT_SEPARATORS, value: propValue, onChange, onFocus, onBlur, ...restProps }) {
55
49
  const inputRef = useRef(null);
56
50
  const [value, setValue] = useState(() => formatNumberWithSeparators(toNumberValue(propValue), separators));
57
51
  const [isFocused, setIsFocused] = useState(false);
@@ -7017,6 +7017,8 @@ export declare interface IUiComboboxInputProps {
7017
7017
  accessibilityConfig?: IAccessibilityConfigBase;
7018
7018
  /** Visible placeholder. */
7019
7019
  placeholder?: string;
7020
+ /** Marks the field invalid: error-colored border plus `aria-invalid`. */
7021
+ isError?: boolean;
7020
7022
  /** Form field name forwarded to the underlying input. */
7021
7023
  name?: string;
7022
7024
  autoFocus?: boolean;
@@ -8116,6 +8118,7 @@ export declare type IUiMenuInteractiveItem<T extends IUiMenuItemData = object> =
8116
8118
  stringTitle: string;
8117
8119
  isDisabled?: boolean;
8118
8120
  isSelected?: boolean;
8121
+ selectionRole?: "radio" | "checkbox";
8119
8122
  isDestructive?: boolean;
8120
8123
  tooltip?: ReactNode;
8121
8124
  tooltipWidth?: number;
@@ -9004,6 +9007,8 @@ export declare interface IUiTextInputProps {
9004
9007
  onIconAfter?: IUiTextInputIconAfterButton;
9005
9008
  /** Accessibility config forwarded to the input element. */
9006
9009
  accessibilityConfig?: IAccessibilityConfigBase;
9010
+ /** Marks the field invalid: error-colored border plus `aria-invalid`. */
9011
+ isError?: boolean;
9007
9012
  disabled?: boolean;
9008
9013
  autoFocus?: boolean;
9009
9014
  /** Forwarded to the input element. Use for autocomplete / combobox patterns. */
@@ -11374,7 +11379,7 @@ export declare function UiTags({ tags, tagOptions, addLabel, nameLabel, cancelLa
11374
11379
  *
11375
11380
  * @internal
11376
11381
  */
11377
- export declare function UiTextInput({ type, value, onChange, label, placeholder, iconBefore, iconAfter, onIconAfter, accessibilityConfig, disabled, autoFocus, onKeyDown, onFocus, onBlur, onClick, dataTestId, inputRef, wrapperRef, name, autoComplete, autoCapitalize, autoCorrect }: IUiTextInputProps): JSX.Element;
11382
+ export declare function UiTextInput({ type, value, onChange, label, placeholder, iconBefore, iconAfter, onIconAfter, accessibilityConfig, isError, disabled, autoFocus, onKeyDown, onFocus, onBlur, onClick, dataTestId, inputRef, wrapperRef, name, autoComplete, autoCapitalize, autoCorrect }: IUiTextInputProps): JSX.Element;
11378
11383
 
11379
11384
  /**
11380
11385
  * Interpolation values accepted by `react-intl`'s `formatMessage`, **narrowed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-kit",
3
- "version": "11.52.0-alpha.5",
3
+ "version": "11.52.0-alpha.6",
4
4
  "description": "GoodData SDK - UI Building Components",
5
5
  "license": "MIT",
6
6
  "author": "GoodData Corporation",
@@ -76,11 +76,11 @@
76
76
  "tslib": "2.8.1",
77
77
  "unified": "^11.0.5",
78
78
  "uuid": "11.1.1",
79
- "@gooddata/sdk-backend-spi": "11.52.0-alpha.5",
80
- "@gooddata/sdk-ui": "11.52.0-alpha.5",
81
- "@gooddata/sdk-model": "11.52.0-alpha.5",
82
- "@gooddata/sdk-ui-theme-provider": "11.52.0-alpha.5",
83
- "@gooddata/util": "11.52.0-alpha.5"
79
+ "@gooddata/sdk-backend-spi": "11.52.0-alpha.6",
80
+ "@gooddata/sdk-model": "11.52.0-alpha.6",
81
+ "@gooddata/sdk-ui-theme-provider": "11.52.0-alpha.6",
82
+ "@gooddata/sdk-ui": "11.52.0-alpha.6",
83
+ "@gooddata/util": "11.52.0-alpha.6"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@microsoft/api-documenter": "^7.17.0",
@@ -129,11 +129,11 @@
129
129
  "typescript": "5.9.3",
130
130
  "vitest": "4.1.8",
131
131
  "vitest-dom": "0.1.1",
132
- "@gooddata/oxlint-config": "11.52.0-alpha.5",
133
- "@gooddata/eslint-config": "11.52.0-alpha.5",
134
- "@gooddata/reference-workspace": "11.52.0-alpha.5",
135
- "@gooddata/stylelint-config": "11.52.0-alpha.5",
136
- "@gooddata/sdk-backend-mockingbird": "11.52.0-alpha.5"
132
+ "@gooddata/eslint-config": "11.52.0-alpha.6",
133
+ "@gooddata/oxlint-config": "11.52.0-alpha.6",
134
+ "@gooddata/reference-workspace": "11.52.0-alpha.6",
135
+ "@gooddata/sdk-backend-mockingbird": "11.52.0-alpha.6",
136
+ "@gooddata/stylelint-config": "11.52.0-alpha.6"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "react": "^18.0.0 || ^19.0.0",
@@ -143,7 +143,7 @@
143
143
  }
144
144
  }
145
145
 
146
- &__item-wrapper .gd-ui-kit-tooltip__anchor {
146
+ &__item-wrapper .gd-ui-kit-tooltip__anchor:not(.gd-ui-kit-tooltip__anchor--inline) {
147
147
  width: 100%;
148
148
  }
149
149
 
@@ -29,6 +29,13 @@
29
29
  background-color: var(--gd-palette-complementary-2);
30
30
  cursor: not-allowed;
31
31
  }
32
+
33
+ // Repeated with :focus-within to outweigh the focus rule above, so the border stays error-colored
34
+ // while the field is being corrected.
35
+ &--error,
36
+ &--error:focus-within {
37
+ border-color: var(--gd-palette-error-base);
38
+ }
32
39
  }
33
40
 
34
41
  &__icon-before {
@@ -1958,7 +1958,7 @@
1958
1958
  outline: auto 5px Highlight; /* For Firefox */
1959
1959
  outline: auto 5px -webkit-focus-ring-color; /* For Chrome */
1960
1960
  }
1961
- .gd-ui-kit-menu__item-wrapper .gd-ui-kit-tooltip__anchor {
1961
+ .gd-ui-kit-menu__item-wrapper .gd-ui-kit-tooltip__anchor:not(.gd-ui-kit-tooltip__anchor--inline) {
1962
1962
  width: 100%;
1963
1963
  }
1964
1964
  .gd-ui-kit-menu__item {
@@ -3608,6 +3608,9 @@
3608
3608
  background-color: var(--gd-palette-complementary-2);
3609
3609
  cursor: not-allowed;
3610
3610
  }
3611
+ .gd-ui-kit-text-input__field--error, .gd-ui-kit-text-input__field--error:focus-within {
3612
+ border-color: var(--gd-palette-error-base);
3613
+ }
3611
3614
  .gd-ui-kit-text-input__icon-before {
3612
3615
  display: inline-flex;
3613
3616
  align-items: center;