@equinor/eds-core-react 2.3.7 → 2.4.0-beta.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 (49) hide show
  1. package/build/index.css +389 -39
  2. package/build/index.min.css +1 -1
  3. package/dist/eds-core-react.cjs +29 -23
  4. package/dist/esm/components/Autocomplete/AutocompleteContext.js +2 -2
  5. package/dist/esm/components/Autocomplete/MultipleInput.js +2 -2
  6. package/dist/esm/components/Autocomplete/OptionList.js +6 -4
  7. package/dist/esm/components/Autocomplete/useAutocomplete.js +4 -0
  8. package/dist/esm/components/Datepicker/calendars/CalendarGrid.js +4 -8
  9. package/dist/esm/components/Datepicker/calendars/CalendarHeader.js +6 -6
  10. package/dist/esm/components/Datepicker/fields/DateSegment.js +4 -1
  11. package/dist/esm/components/InputWrapper/InputWrapper.js +1 -1
  12. package/dist/esm/components/Popover/Popover.js +4 -4
  13. package/dist/esm/components/Textarea/Textarea.js +1 -1
  14. package/dist/esm-next/components/next/Banner/Banner.js +88 -0
  15. package/dist/esm-next/components/next/Button/Button.js +1 -0
  16. package/dist/esm-next/components/next/Icon/Icon.js +27 -1
  17. package/dist/esm-next/components/next/Input/Input.js +1 -1
  18. package/dist/esm-next/components/next/Link/Link.js +24 -0
  19. package/dist/esm-next/components/next/Search/Search.js +118 -0
  20. package/dist/esm-next/components/next/TextArea/TextArea.js +131 -0
  21. package/dist/esm-next/components/next/Tooltip/Tooltip.js +84 -0
  22. package/dist/esm-next/index.next.js +5 -0
  23. package/dist/index.next.cjs +445 -6
  24. package/dist/types/components/Autocomplete/AutocompleteContext.d.ts +2 -2
  25. package/dist/types/components/Autocomplete/useAutocomplete.d.ts +2 -2
  26. package/dist/types/components/next/Banner/Banner.d.ts +23 -0
  27. package/dist/types/components/next/Banner/Banner.figma.d.ts +1 -0
  28. package/dist/types/components/next/Banner/Banner.types.d.ts +33 -0
  29. package/dist/types/components/next/Banner/index.d.ts +2 -0
  30. package/dist/types/components/next/Icon/Icon.d.ts +0 -1
  31. package/dist/types/components/next/Input/Input.types.d.ts +4 -1
  32. package/dist/types/components/next/Link/Link.d.ts +4 -0
  33. package/dist/types/components/next/Link/Link.figma.d.ts +1 -0
  34. package/dist/types/components/next/Link/Link.types.d.ts +11 -0
  35. package/dist/types/components/next/Link/index.d.ts +2 -0
  36. package/dist/types/components/next/Search/Search.d.ts +9 -0
  37. package/dist/types/components/next/Search/Search.figma.d.ts +1 -0
  38. package/dist/types/components/next/Search/Search.types.d.ts +16 -0
  39. package/dist/types/components/next/Search/index.d.ts +2 -0
  40. package/dist/types/components/next/TextArea/TextArea.d.ts +11 -0
  41. package/dist/types/components/next/TextArea/TextArea.figma.d.ts +1 -0
  42. package/dist/types/components/next/TextArea/TextArea.types.d.ts +21 -0
  43. package/dist/types/components/next/TextArea/index.d.ts +2 -0
  44. package/dist/types/components/next/Tooltip/Tooltip.d.ts +7 -0
  45. package/dist/types/components/next/Tooltip/Tooltip.figma.d.ts +1 -0
  46. package/dist/types/components/next/Tooltip/Tooltip.types.d.ts +17 -0
  47. package/dist/types/components/next/Tooltip/index.d.ts +2 -0
  48. package/dist/types/components/next/index.d.ts +10 -0
  49. package/package.json +41 -34
@@ -3,9 +3,9 @@ import styled from 'styled-components';
3
3
  import { useAutocompleteContext } from './AutocompleteContext.js';
4
4
  import { RightAdornments } from './RightAdornments.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { Chip } from '../Chip/Chip.js';
7
6
  import { useEds } from '../EdsProvider/eds.context.js';
8
7
  import { Input } from '../Input/Input.js';
8
+ import { Chip } from '../Chip/Chip.js';
9
9
 
10
10
  const UnstyledInput = styled.input.withConfig({
11
11
  displayName: "MultipleInput__UnstyledInput",
@@ -76,7 +76,7 @@ const MultipleInput = () => {
76
76
  if (el) chipRefs.current.set(getLabel(item), el);else chipRefs.current.delete(getLabel(item));
77
77
  },
78
78
  style: {
79
- outline: '1px solid var(--eds-color-accent-12)',
79
+ outline: '1px solid var(--eds_interactive_primary__resting, rgba(0, 112, 121, 1))',
80
80
  ...(density === 'compact' && {
81
81
  height: '16px',
82
82
  fontSize: '12px',
@@ -41,10 +41,12 @@ const OptionList = ({
41
41
 
42
42
  // MARK: popover toggle
43
43
  useIsomorphicLayoutEffect(() => {
44
- if (isOpen) {
45
- refs.floating.current?.showPopover();
46
- } else {
47
- refs.floating.current?.hidePopover();
44
+ if (refs.floating.current?.hasAttribute('popover')) {
45
+ if (isOpen) {
46
+ refs.floating.current.showPopover();
47
+ } else {
48
+ refs.floating.current.hidePopover();
49
+ }
48
50
  }
49
51
  }, [isOpen, refs.floating]);
50
52
  const showNoOptions = isOpen && !availableItems.length && noOptionsText.length > 0;
@@ -43,6 +43,7 @@ const useAutocomplete = ({
43
43
  variant,
44
44
  onClear,
45
45
  ref,
46
+ id,
46
47
  ...other
47
48
  }) => {
48
49
  const [lastScrollOffset, setLastScrollOffset] = useState(0);
@@ -204,6 +205,9 @@ const useAutocomplete = ({
204
205
 
205
206
  // MARK: downshift state
206
207
  let comboBoxProps = {
208
+ ...(id !== undefined && {
209
+ inputId: id
210
+ }),
207
211
  items: availableItems,
208
212
  //can not pass readonly type to downshift so we cast it to regular T[]
209
213
  initialSelectedItem: initialSelectedOptions[0],
@@ -1,5 +1,4 @@
1
- import { useLocale, useCalendarGrid } from 'react-aria';
2
- import { getWeeksInMonth } from '@internationalized/date';
1
+ import { useCalendarGrid } from 'react-aria';
3
2
  import { CalendarCell } from './CalendarCell.js';
4
3
  import { YearGrid } from './YearGrid.js';
5
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -17,9 +16,6 @@ function CalendarGrid({
17
16
  setYearPickerPage,
18
17
  ...props
19
18
  }) {
20
- const {
21
- locale
22
- } = useLocale();
23
19
  const {
24
20
  gridProps,
25
21
  headerProps,
@@ -29,9 +25,9 @@ function CalendarGrid({
29
25
  weekdayStyle: 'long'
30
26
  }, state);
31
27
 
32
- // Get the number of weeks in the month so that we can render the proper number of rows.
33
- const howManyWeeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
34
- const weeksInMonthArray = [...new Array(howManyWeeksInMonth).keys()];
28
+ // Always render 6 rows (the maximum weeks in any month) so the calendar
29
+ // height stays consistent when navigating between months.
30
+ const weeksInMonthArray = [...new Array(6).keys()];
35
31
  return showYearPicker ? /*#__PURE__*/jsx(YearGrid, {
36
32
  year: state.focusedDate.year,
37
33
  setFocusedYear: year => {
@@ -21,7 +21,7 @@ function TodayPicker({
21
21
  onClick: () => onClick(new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate())),
22
22
  variant: 'ghost',
23
23
  style: {
24
- marginLeft: 16
24
+ marginLeft: 4
25
25
  },
26
26
  children: "Today"
27
27
  });
@@ -30,6 +30,10 @@ const HeaderActions = styled.div.withConfig({
30
30
  displayName: "CalendarHeader__HeaderActions",
31
31
  componentId: "sc-kuy15-1"
32
32
  })(["display:flex;align-items:center;width:100%;"]);
33
+ const TitleButton = styled(Button).withConfig({
34
+ displayName: "CalendarHeader__TitleButton",
35
+ componentId: "sc-kuy15-2"
36
+ })(["min-width:13.1rem;white-space:nowrap;font-size:", ";text-transform:capitalize;& > span{display:flex;justify-content:space-between;align-items:center;width:100%;}"], tokens.typography.heading.h5.fontSize);
33
37
 
34
38
  /**
35
39
  * The default header for the calendar components if no custom header is provided
@@ -61,15 +65,11 @@ function CalendarHeader({
61
65
  style: {
62
66
  flex: '1 1 auto'
63
67
  }
64
- }), /*#__PURE__*/jsxs(Button, {
68
+ }), /*#__PURE__*/jsxs(TitleButton, {
65
69
  onClick: () => setShowYearPicker(!showYearPicker),
66
70
  "data-testid": 'heading',
67
71
  "aria-live": 'polite',
68
72
  variant: 'ghost',
69
- style: {
70
- fontSize: tokens.typography.heading.h5.fontSize,
71
- textTransform: 'capitalize'
72
- },
73
73
  children: [title, /*#__PURE__*/jsx(Icon, {
74
74
  data: showYearPicker ? chevron_up : chevron_down
75
75
  })]
@@ -30,7 +30,10 @@ function DateSegment({
30
30
  formatOptions,
31
31
  timezone
32
32
  } = useDatePickerContext();
33
- const formatter = useDateFormatter(formatOptions);
33
+ const formatter = useDateFormatter({
34
+ ...formatOptions,
35
+ timeZone: timezone
36
+ });
34
37
  const parts = state.value ? formatter.formatToParts(state.value.toDate(timezone)) : [];
35
38
  const part = parts.find(p => p.type === segment.type);
36
39
  const value = segment.isPlaceholder || segment.type === 'literal' ? segment.text : part?.value ?? segment.text;
@@ -3,9 +3,9 @@ import styled, { ThemeProvider } from 'styled-components';
3
3
  import { useToken } from '@equinor/eds-utils';
4
4
  import { inputToken } from './InputWrapper.tokens.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { useEds } from '../EdsProvider/eds.context.js';
6
7
  import { Label as Label$1 } from '../Label/Label.js';
7
8
  import { HelperText as TextfieldHelperText } from './HelperText/HelperText.js';
8
- import { useEds } from '../EdsProvider/eds.context.js';
9
9
 
10
10
  const Container = styled.div.withConfig({
11
11
  displayName: "InputWrapper__Container",
@@ -99,12 +99,12 @@ const Popover = /*#__PURE__*/forwardRef(function Popover({
99
99
  } = useInteractions([useDismiss(context)]);
100
100
  useEffect(() => {
101
101
  if (!elements.floating) return;
102
- if (open) {
103
- if (elements.floating.isConnected) {
102
+ if (elements.floating.hasAttribute('popover')) {
103
+ if (open) {
104
104
  elements.floating.showPopover();
105
+ } else {
106
+ elements.floating.hidePopover();
105
107
  }
106
- } else {
107
- elements.floating.hidePopover();
108
108
  }
109
109
  }, [open, elements.floating]);
110
110
  useEffect(() => {
@@ -4,8 +4,8 @@ import { input as input$1 } from '../Input/Input.tokens.js';
4
4
  import { useAutoResize, mergeRefs } from '@equinor/eds-utils';
5
5
  import { jsx } from 'react/jsx-runtime';
6
6
  import { useInputField } from '../InputWrapper/useInputField.js';
7
- import { InputWrapper } from '../InputWrapper/InputWrapper.js';
8
7
  import { useEds } from '../EdsProvider/eds.context.js';
8
+ import { InputWrapper } from '../InputWrapper/InputWrapper.js';
9
9
  import { Input } from '../Input/Input.js';
10
10
 
11
11
  const {
@@ -0,0 +1,88 @@
1
+ import { forwardRef } from 'react';
2
+ import { close } from '@equinor/eds-icons';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { Button } from '../Button/Button.js';
5
+ import { Icon } from '../Icon/Icon.js';
6
+ import { TypographyNext } from '../../Typography/Typography.new.js';
7
+
8
+ const BannerIcon = /*#__PURE__*/forwardRef(function BannerIcon({
9
+ className,
10
+ children,
11
+ ...rest
12
+ }, ref) {
13
+ return /*#__PURE__*/jsx("span", {
14
+ ref: ref,
15
+ className: ['eds-banner__icon', className].filter(Boolean).join(' '),
16
+ ...rest,
17
+ children: children
18
+ });
19
+ });
20
+ const BannerMessage = /*#__PURE__*/forwardRef(function BannerMessage({
21
+ className,
22
+ children,
23
+ ...rest
24
+ }, ref) {
25
+ return /*#__PURE__*/jsx(TypographyNext, {
26
+ ref: ref,
27
+ as: "p",
28
+ family: "ui",
29
+ size: "md",
30
+ baseline: "center",
31
+ lineHeight: "default",
32
+ tracking: "normal",
33
+ className: ['eds-banner__message', className].filter(Boolean).join(' '),
34
+ ...rest,
35
+ children: children
36
+ });
37
+ });
38
+ const BannerActions = /*#__PURE__*/forwardRef(function BannerActions({
39
+ placement = 'left',
40
+ className,
41
+ children,
42
+ ...rest
43
+ }, ref) {
44
+ return /*#__PURE__*/jsx("div", {
45
+ ref: ref,
46
+ className: ['eds-banner__actions', className].filter(Boolean).join(' '),
47
+ "data-placement": placement,
48
+ ...rest,
49
+ children: children
50
+ });
51
+ });
52
+ const BannerComponent = /*#__PURE__*/forwardRef(function Banner({
53
+ tone = 'info',
54
+ role = 'status',
55
+ onDismiss,
56
+ className,
57
+ children,
58
+ ...rest
59
+ }, ref) {
60
+ return /*#__PURE__*/jsxs("div", {
61
+ ref: ref,
62
+ className: ['eds-banner', className].filter(Boolean).join(' '),
63
+ "data-color-appearance": tone,
64
+ role: role,
65
+ ...rest,
66
+ children: [children, onDismiss && /*#__PURE__*/jsx(Button, {
67
+ variant: "ghost",
68
+ icon: true,
69
+ size: "small",
70
+ className: "eds-banner__dismiss",
71
+ "aria-label": "Dismiss",
72
+ onClick: onDismiss,
73
+ children: /*#__PURE__*/jsx(Icon, {
74
+ data: close
75
+ })
76
+ })]
77
+ });
78
+ });
79
+ BannerIcon.displayName = 'Banner.Icon';
80
+ BannerMessage.displayName = 'Banner.Message';
81
+ BannerActions.displayName = 'Banner.Actions';
82
+ BannerComponent.displayName = 'Banner';
83
+ const Banner = BannerComponent;
84
+ Banner.Icon = BannerIcon;
85
+ Banner.Message = BannerMessage;
86
+ Banner.Actions = BannerActions;
87
+
88
+ export { Banner };
@@ -38,6 +38,7 @@ const Button = /*#__PURE__*/forwardRef(function Button({
38
38
  ...rest,
39
39
  children: icon ? children : /*#__PURE__*/jsx(TypographyNext, {
40
40
  as: "span",
41
+ className: "eds-button__label",
41
42
  family: "ui",
42
43
  size: typographySize,
43
44
  lineHeight: "squished",
@@ -1,6 +1,32 @@
1
1
  import { forwardRef, useId } from 'react';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
 
4
+ /**
5
+ * Icon component for EDS 2.0
6
+ *
7
+ * Features:
8
+ * - Automatic sizing from parent's data-font-size via --eds-typography-icon-size
9
+ * - Dynamic fallback sizing (1.5em) when no tokens are set
10
+ * - Explicit size prop for standalone usage
11
+ * - WCAG 2.1 AA accessible with optional title for semantic icons
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { Icon } from '@equinor/eds-core-react/next'
16
+ * import { save } from '@equinor/eds-icons'
17
+ *
18
+ * // Auto-sized from parent's data-font-size
19
+ * <div data-font-size="md">
20
+ * <Icon data={warning} /> Error message
21
+ * </div>
22
+ *
23
+ * // Explicit size for standalone usage
24
+ * <Icon data={save} size="lg" />
25
+ *
26
+ * // Semantic icon with accessible name
27
+ * <Icon data={save} title="Save document" />
28
+ * ```
29
+ */
4
30
  const Icon = /*#__PURE__*/forwardRef(function Icon({
5
31
  data,
6
32
  title,
@@ -19,7 +45,7 @@ const Icon = /*#__PURE__*/forwardRef(function Icon({
19
45
  height = '24',
20
46
  width = '24'
21
47
  } = data;
22
- const classes = ['icon', className].filter(Boolean).join(' ');
48
+ const classes = ['eds-icon', className].filter(Boolean).join(' ');
23
49
 
24
50
  // Accessibility: decorative icons are hidden, semantic icons have role="img"
25
51
  const accessibilityProps = title ? {
@@ -56,7 +56,7 @@ const Input = /*#__PURE__*/forwardRef(function Input({
56
56
  })]
57
57
  }), /*#__PURE__*/jsx(Component, {
58
58
  ref: ref,
59
- type: type,
59
+ type: Component === 'textarea' ? undefined : type,
60
60
  disabled: disabled,
61
61
  readOnly: readOnly,
62
62
  className: ['eds-input', className].filter(Boolean).join(' '),
@@ -0,0 +1,24 @@
1
+ import { forwardRef } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ const Link = /*#__PURE__*/forwardRef(function Link({
5
+ variant = 'inline',
6
+ className,
7
+ children,
8
+ ...rest
9
+ }, ref) {
10
+ const classes = ['eds-link', className].filter(Boolean).join(' ');
11
+ return /*#__PURE__*/jsx("a", {
12
+ ref: ref,
13
+ className: classes,
14
+ "data-variant": variant,
15
+ "data-font-family": variant === 'standalone' ? 'ui' : undefined,
16
+ "data-font-size": variant === 'standalone' ? 'md' : undefined,
17
+ "data-line-height": variant === 'standalone' ? 'squished' : undefined,
18
+ ...rest,
19
+ children: children
20
+ });
21
+ });
22
+ Link.displayName = 'Link';
23
+
24
+ export { Link };
@@ -0,0 +1,118 @@
1
+ import { forwardRef, useState, useRef, useCallback } from 'react';
2
+ import { close, search } from '@equinor/eds-icons';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { useFieldIds } from '../Field/useFieldIds.js';
5
+ import { Field } from '../Field/Field.js';
6
+ import { Input } from '../Input/Input.js';
7
+ import { Button } from '../Button/Button.js';
8
+ import { Icon } from '../Icon/Icon.js';
9
+
10
+ const Search = /*#__PURE__*/forwardRef(function Search({
11
+ label,
12
+ description,
13
+ helperMessage,
14
+ id: providedId,
15
+ invalid,
16
+ disabled,
17
+ readOnly,
18
+ value,
19
+ defaultValue,
20
+ onChange,
21
+ onClear,
22
+ clearLabel = 'Clear search',
23
+ ...inputProps
24
+ }, forwardedRef) {
25
+ const {
26
+ inputId,
27
+ labelId,
28
+ descriptionId,
29
+ helperMessageId,
30
+ getDescribedBy
31
+ } = useFieldIds(providedId);
32
+ const isControlled = value !== undefined;
33
+ const [internalHasValue, setInternalHasValue] = useState(() => Boolean(defaultValue));
34
+ const hasValue = isControlled ? Boolean(value) : internalHasValue;
35
+ const inputRef = useRef(null);
36
+ const mergedRef = useCallback(node => {
37
+ inputRef.current = node;
38
+ if (typeof forwardedRef === 'function') {
39
+ forwardedRef(node);
40
+ } else if (forwardedRef) {
41
+ forwardedRef.current = node;
42
+ }
43
+ }, [forwardedRef]);
44
+ const handleChange = e => {
45
+ if (!isControlled) {
46
+ setInternalHasValue(Boolean(e.target.value));
47
+ }
48
+ onChange?.(e);
49
+ };
50
+ const handleClear = () => {
51
+ if (!isControlled && inputRef.current) {
52
+ // Direct DOM mutation: bypasses React's synthetic onChange, which is
53
+ // intentional — onClear is the designated callback for clear actions.
54
+ inputRef.current.value = '';
55
+ setInternalHasValue(false);
56
+ }
57
+ onClear?.();
58
+ inputRef.current?.focus();
59
+ };
60
+ const showClear = hasValue && !disabled && !readOnly;
61
+ // Accent only in interactive states — grey in error, readonly, disabled
62
+ const iconTone = disabled || readOnly || invalid ? 'neutral' : 'accent';
63
+ return /*#__PURE__*/jsx("search", {
64
+ className: "eds-search",
65
+ "aria-labelledby": label ? labelId : undefined,
66
+ children: /*#__PURE__*/jsxs(Field, {
67
+ disabled: disabled,
68
+ children: [label && /*#__PURE__*/jsx(Field.Label, {
69
+ id: labelId,
70
+ htmlFor: inputId,
71
+ children: label
72
+ }), description && /*#__PURE__*/jsx(Field.Description, {
73
+ id: descriptionId,
74
+ children: description
75
+ }), /*#__PURE__*/jsx(Input, {
76
+ ref: mergedRef,
77
+ id: inputId,
78
+ type: "search",
79
+ disabled: disabled,
80
+ readOnly: readOnly,
81
+ invalid: invalid,
82
+ value: value,
83
+ defaultValue: defaultValue,
84
+ onChange: handleChange,
85
+ "aria-describedby": getDescribedBy({
86
+ hasDescription: !!description,
87
+ hasHelperMessage: !!helperMessage
88
+ }),
89
+ hideErrorIcon: true,
90
+ startAdornment: /*#__PURE__*/jsx(Icon, {
91
+ data: search,
92
+ className: "search-icon",
93
+ "data-color-appearance": iconTone
94
+ }),
95
+ endAdornment: showClear ? /*#__PURE__*/jsx(Button, {
96
+ variant: "ghost",
97
+ icon: true,
98
+ round: true,
99
+ size: "small",
100
+ tone: invalid ? 'neutral' : 'accent',
101
+ onClick: handleClear,
102
+ "aria-label": clearLabel,
103
+ children: /*#__PURE__*/jsx(Icon, {
104
+ data: close
105
+ })
106
+ }) : undefined,
107
+ ...inputProps
108
+ }), helperMessage && /*#__PURE__*/jsx(Field.HelperMessage, {
109
+ id: helperMessageId,
110
+ role: invalid ? 'alert' : undefined,
111
+ children: helperMessage
112
+ })]
113
+ })
114
+ });
115
+ });
116
+ Search.displayName = 'Search';
117
+
118
+ export { Search };
@@ -0,0 +1,131 @@
1
+ import { forwardRef, useState, useEffect, useRef, useMemo } from 'react';
2
+ import { useAutoResize, mergeRefs } from '@equinor/eds-utils';
3
+ import { info_circle } from '@equinor/eds-icons';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+ import { useFieldIds } from '../Field/useFieldIds.js';
6
+ import { Field } from '../Field/Field.js';
7
+ import { Tooltip } from '../../Tooltip/Tooltip.js';
8
+ import { Button } from '../Button/Button.js';
9
+ import { Icon } from '../Icon/Icon.js';
10
+ import { Input } from '../Input/Input.js';
11
+
12
+ const TextArea = /*#__PURE__*/forwardRef(function TextArea({
13
+ label,
14
+ labelInfo,
15
+ indicator,
16
+ description,
17
+ helperMessage,
18
+ id: providedId,
19
+ invalid,
20
+ disabled,
21
+ maxRows,
22
+ showCharacterCount,
23
+ ...textareaProps
24
+ }, ref) {
25
+ const {
26
+ inputId,
27
+ descriptionId,
28
+ helperMessageId,
29
+ getDescribedBy
30
+ } = useFieldIds(providedId);
31
+ const [charCount, setCharCount] = useState(() => String(textareaProps.value ?? textareaProps.defaultValue ?? '').length);
32
+ useEffect(() => {
33
+ if (textareaProps.value !== undefined) {
34
+ setCharCount(String(textareaProps.value).length);
35
+ }
36
+ }, [textareaProps.value]);
37
+ const {
38
+ maxLength,
39
+ onChange: onChangeProp,
40
+ ...restTextareaProps
41
+ } = textareaProps;
42
+ const handleChange = e => {
43
+ setCharCount(e.target.value.length);
44
+ onChangeProp?.(e);
45
+ };
46
+ const internalRef = useRef(null);
47
+ const [maxPixelHeight, setMaxPixelHeight] = useState(undefined);
48
+
49
+ // Auto-grow is always on. When maxRows is set, compute a pixel cap after
50
+ // mount using the element's actual rendered line-height and padding
51
+ // (density-aware). Until the cap is computed (or if maxRows is not set),
52
+ // pass Infinity so the textarea grows without bound.
53
+ const autoResizeHeight = maxRows !== undefined && maxPixelHeight !== undefined ? maxPixelHeight : Infinity;
54
+ const autoResizeRef = useAutoResize(autoResizeHeight);
55
+ useEffect(() => {
56
+ if (!maxRows || !internalRef.current) return;
57
+ const el = internalRef.current;
58
+ const updateMaxHeight = () => {
59
+ const style = window.getComputedStyle(el);
60
+ const lineHeight = parseFloat(style.lineHeight);
61
+ const paddingBlockStart = parseFloat(style.paddingBlockStart);
62
+ const paddingBlockEnd = parseFloat(style.paddingBlockEnd);
63
+ setMaxPixelHeight(lineHeight * maxRows + paddingBlockStart + paddingBlockEnd);
64
+ };
65
+ const observer = new ResizeObserver(updateMaxHeight);
66
+ observer.observe(el);
67
+ updateMaxHeight();
68
+ return () => observer.disconnect();
69
+ }, [maxRows]);
70
+ const combinedRef = useMemo(() => mergeRefs(ref, autoResizeRef, internalRef), [ref, autoResizeRef, internalRef]);
71
+ const showHelperRow = helperMessage || showCharacterCount;
72
+ return /*#__PURE__*/jsxs(Field, {
73
+ className: "eds-text-area",
74
+ disabled: disabled,
75
+ children: [label && /*#__PURE__*/jsxs("div", {
76
+ className: "label-row",
77
+ children: [/*#__PURE__*/jsx(Field.Label, {
78
+ htmlFor: inputId,
79
+ indicator: indicator,
80
+ children: label
81
+ }), labelInfo && /*#__PURE__*/jsx(Tooltip, {
82
+ title: labelInfo,
83
+ placement: "top",
84
+ children: /*#__PURE__*/jsx(Button, {
85
+ variant: "ghost",
86
+ icon: true,
87
+ round: true,
88
+ size: "small",
89
+ tone: "neutral",
90
+ "aria-label": "More information",
91
+ children: /*#__PURE__*/jsx(Icon, {
92
+ data: info_circle,
93
+ size: "xs"
94
+ })
95
+ })
96
+ })]
97
+ }), description && /*#__PURE__*/jsx(Field.Description, {
98
+ id: descriptionId,
99
+ children: description
100
+ }), /*#__PURE__*/jsx(Input, {
101
+ ref: combinedRef,
102
+ as: "textarea",
103
+ id: inputId,
104
+ disabled: disabled,
105
+ invalid: invalid,
106
+ maxLength: maxLength,
107
+ onChange: handleChange,
108
+ "aria-describedby": getDescribedBy({
109
+ hasDescription: !!description,
110
+ hasHelperMessage: !!helperMessage
111
+ }),
112
+ ...restTextareaProps
113
+ }), showHelperRow && /*#__PURE__*/jsxs("div", {
114
+ className: "helper-row",
115
+ children: [helperMessage && /*#__PURE__*/jsx(Field.HelperMessage, {
116
+ id: helperMessageId,
117
+ role: invalid ? 'alert' : undefined,
118
+ children: helperMessage
119
+ }), showCharacterCount && /*#__PURE__*/jsx("span", {
120
+ className: "char-count",
121
+ "data-font-family": "ui",
122
+ "data-font-size": "xs",
123
+ "aria-live": maxLength !== undefined && charCount >= maxLength * 0.8 ? 'polite' : 'off',
124
+ children: maxLength !== undefined ? `${charCount} / ${maxLength}` : charCount
125
+ })]
126
+ })]
127
+ });
128
+ });
129
+ TextArea.displayName = 'TextArea';
130
+
131
+ export { TextArea };