@true-engineering/true-react-common-ui-kit 4.0.0-alpha72 → 4.0.0-alpha74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true-engineering/true-react-common-ui-kit",
3
- "version": "4.0.0-alpha72",
3
+ "version": "4.0.0-alpha74",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -70,6 +70,7 @@ export const ControlWrapper: FC<IControlWrapperProps> = ({
70
70
  const classes = useStyles({ tweakStyles });
71
71
 
72
72
  const [startControlsWidth, setStartControlsWidth] = useState<number>();
73
+ const [endControlsWidth, setEndControlsWidth] = useState<number>();
73
74
 
74
75
  const startIcons = getArray(startIcon).map(convertToControlWrapperIcon);
75
76
  const endIcons = getArray(endIcon).map(convertToControlWrapperIcon);
@@ -89,24 +90,41 @@ export const ControlWrapper: FC<IControlWrapperProps> = ({
89
90
  onTargetChange: (target) => setStartControlsWidth(target.clientWidth),
90
91
  });
91
92
 
93
+ const endControlsRef = useResizeRef({
94
+ onTargetChange: (target) => setEndControlsWidth(target.clientWidth),
95
+ });
96
+
92
97
  const renderIconControl = (
93
- { key, iconComponent, onClick, shouldResetSize = false }: IControlWrapperIcon,
98
+ {
99
+ key,
100
+ iconComponent,
101
+ onClick,
102
+ shouldResetSize = false,
103
+ isActiveIcon = isNotEmpty(onClick),
104
+ ...iconContainerProps
105
+ }: IControlWrapperIcon,
94
106
  iconType: 'start' | 'end' | 'clear',
95
107
  index?: number,
96
108
  ) => (
97
109
  <div
98
110
  key={key ?? index}
99
111
  className={clsx(classes.icon, classes[`${iconType}Icon`], {
100
- [classes.activeIcon]: !isDisabled && isNotEmpty(onClick),
112
+ [classes.activeIcon]: !isDisabled && isActiveIcon,
101
113
  [classes.customIcon]: shouldResetSize,
102
114
  })}
103
115
  {...addClickHandler(onClick, !isDisabled)}
104
116
  {...addDataTestId(testId, `${iconType}-icon`)}
117
+ {...iconContainerProps}
105
118
  >
106
119
  <div className={classes.iconInner}>{renderIcon(iconComponent)}</div>
107
120
  </div>
108
121
  );
109
122
 
123
+ const style = {
124
+ '--start-controls-width': hasStartIcons ? `${startControlsWidth}px` : undefined,
125
+ '--end-controls-width': hasEndControls ? `${endControlsWidth}px` : undefined,
126
+ } as CSSProperties;
127
+
110
128
  return (
111
129
  <div
112
130
  className={clsx(
@@ -123,11 +141,7 @@ export const ControlWrapper: FC<IControlWrapperProps> = ({
123
141
  [classes.withStartControls]: hasStartIcons,
124
142
  },
125
143
  )}
126
- style={
127
- hasStartIcons
128
- ? ({ '--start-controls-width': `${startControlsWidth}px` } as CSSProperties)
129
- : undefined
130
- }
144
+ style={style}
131
145
  {...addDataAttributes(data, testId)}
132
146
  >
133
147
  {isReactNodeNotEmpty(label) && (
@@ -151,7 +165,7 @@ export const ControlWrapper: FC<IControlWrapperProps> = ({
151
165
  {children}
152
166
 
153
167
  {hasEndControls && (
154
- <div className={classes.controls}>
168
+ <div className={classes.controls} ref={endControlsRef}>
155
169
  {hasClearButton &&
156
170
  renderIconControl({ iconComponent: 'close', onClick: onClear }, 'clear')}
157
171
 
@@ -1,4 +1,4 @@
1
- import { type Key } from 'react';
1
+ import { HTMLAttributes, type Key } from 'react';
2
2
  import { IClickHandlerEvent } from '../../types';
3
3
  import { IIcon } from '../Icon';
4
4
  import { GROUP_PLACEMENTS } from './constants';
@@ -11,9 +11,10 @@ export type IControlWrapperSize = keyof IControlWrapperSizes;
11
11
  export type IGroupPlacement = (typeof GROUP_PLACEMENTS)[number];
12
12
 
13
13
  // подумать над extend HTMLAttributes<HTMLDivElement>
14
- export interface IControlWrapperIcon {
14
+ export interface IControlWrapperIcon extends Pick<HTMLAttributes<HTMLDivElement>, 'tabIndex'> {
15
15
  key?: Key;
16
16
  iconComponent: IIcon;
17
17
  onClick?: (event: IClickHandlerEvent) => void;
18
18
  shouldResetSize?: boolean;
19
+ isActiveIcon?: boolean;
19
20
  }
@@ -1,23 +1,7 @@
1
1
  import { FC, useState } from 'react';
2
- import { ru } from 'date-fns/locale';
3
2
  import { type Meta } from '@storybook/react';
4
3
  import { DatePicker, IDatePickerProps } from './DatePicker';
5
4
 
6
- const months = [
7
- 'Январь',
8
- 'Февраль',
9
- 'Март',
10
- 'Апрель',
11
- 'Май',
12
- 'Июнь',
13
- 'Июль',
14
- 'Август',
15
- 'Сентябрь',
16
- 'Октябрь',
17
- 'Ноябрь',
18
- 'Декабрь',
19
- ];
20
-
21
5
  const Story: FC<IDatePickerProps> = (args) => {
22
6
  const [date, setDate] = useState<Date | null>(null);
23
7
 
@@ -36,9 +20,7 @@ const Story: FC<IDatePickerProps> = (args) => {
36
20
  <div style={{ display: 'flex', height: 48 }}>
37
21
  <DatePicker
38
22
  {...args}
39
- locale={ru}
40
23
  testId="datepicker"
41
- months={months}
42
24
  selectedDate={date}
43
25
  minDate={new Date()}
44
26
  onChangeDate={setDate}
@@ -57,6 +39,13 @@ const meta: Meta<typeof Story> = {
57
39
  isClearable: true,
58
40
  shouldRenderPopperInBody: false,
59
41
  popperPlacement: 'bottom-start',
42
+ locale: 'ru',
43
+ },
44
+ argTypes: {
45
+ locale: {
46
+ control: 'inline-radio',
47
+ options: ['ru', 'en'],
48
+ },
60
49
  },
61
50
  };
62
51
 
@@ -3,6 +3,7 @@ import {
3
3
  FocusEvent,
4
4
  SyntheticEvent,
5
5
  forwardRef,
6
+ useCallback,
6
7
  useEffect,
7
8
  useMemo,
8
9
  useRef,
@@ -11,9 +12,10 @@ import {
11
12
  import type ReactDatePicker from 'react-datepicker';
12
13
  import 'react-datepicker/dist/react-datepicker.css';
13
14
  import clsx from 'clsx';
14
- import { isAfter, isBefore, isValid } from 'date-fns';
15
+ import { isAfter, isBefore, isValid, Month } from 'date-fns';
15
16
  import {
16
17
  addDataAttributes,
18
+ getArray,
17
19
  isEmpty,
18
20
  isNotEmpty,
19
21
  isString,
@@ -140,13 +142,18 @@ export const DatePicker = forwardRef<ReactDatePicker, IDatePickerProps>(function
140
142
  ? isStringNotEmpty(startDateValue) || isStringNotEmpty(endDateValue)
141
143
  : isStringNotEmpty(dateValue);
142
144
 
145
+ const endIcon =
146
+ isClearable && hasDateInputValue
147
+ ? inputProps.endIcon
148
+ : getArray(inputProps.endIcon).concat('calendar');
149
+
143
150
  const dateInputProps: IDateInputProps = {
144
151
  ...inputProps,
145
152
  isRange,
146
153
  isDisabled,
147
154
  isClearable,
148
155
  isActive: isOpen,
149
- icon: isClearable && hasDateInputValue ? undefined : 'calendar',
156
+ endIcon,
150
157
  tweakStyles: tweakDateInputStyles,
151
158
  ...(isRange ? { startDate: startDateValue, endDate: endDateValue } : { date: dateValue }),
152
159
  };
@@ -267,13 +274,26 @@ export const DatePicker = forwardRef<ReactDatePicker, IDatePickerProps>(function
267
274
  OUTSIDE_CLICK_IGNORE_CLASS,
268
275
  );
269
276
 
277
+ const resolvedLocale = isString(locale) ? LocalesMap[locale] : locale;
278
+
279
+ const getLocalizedMonth = useCallback(
280
+ (month: Month) => {
281
+ if (isNotEmpty(months?.[month])) {
282
+ return months[month];
283
+ }
284
+ const localizedMonth = resolvedLocale.localize.month(month);
285
+ return localizedMonth.charAt(0).toUpperCase().concat(localizedMonth.slice(1));
286
+ },
287
+ [resolvedLocale, months],
288
+ );
289
+
270
290
  return (
271
291
  <div className={classes.root} {...addDataAttributes(data)}>
272
292
  <DatePickerBase
273
293
  ref={componentRef}
274
294
  minDate={minDate}
275
295
  maxDate={maxDate}
276
- locale={isString(locale) ? LocalesMap[locale] : locale}
296
+ locale={resolvedLocale}
277
297
  dateFormat={dateFormat}
278
298
  placeholderText={placeholder}
279
299
  calendarStartDay={calendarStartDay}
@@ -302,7 +322,10 @@ export const DatePicker = forwardRef<ReactDatePicker, IDatePickerProps>(function
302
322
  customInput={<CustomInput {...dateInputProps} />}
303
323
  renderDayContents={(day) => <div className={classes.dayInner}>{day}</div>}
304
324
  renderCustomHeader={
305
- renderCustomHeader ?? ((baseProps) => <DatePickerHeader {...baseProps} months={months} />)
325
+ renderCustomHeader ??
326
+ ((baseProps) => (
327
+ <DatePickerHeader {...baseProps} getMonthSelectString={getLocalizedMonth} />
328
+ ))
306
329
  }
307
330
  todayButton={todayButton}
308
331
  highlightDates={highlightDates}
@@ -1,21 +1,23 @@
1
- import { FC, useMemo } from 'react';
1
+ import { FC } from 'react';
2
2
  import { ReactDatePickerCustomHeaderProps as BaseProps } from 'react-datepicker';
3
- import { getMonth, getYear } from 'date-fns';
3
+ import { getMonth, getYear, Month } from 'date-fns';
4
+ import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
4
5
  import { useTweakStyles } from '../../../../hooks';
5
6
  import { ITweakStylesProps } from '../../../../types';
6
7
  import { Icon } from '../../../Icon';
7
8
  import { Select } from '../../../Select';
9
+ import { MONTH_SELECT_OPTIONS, YEARS_SELECT_OPTIONS } from './constants';
8
10
  import { IDatePickerHeaderStyles, selectStyles, useStyles } from './DatePickerHeader.styles';
9
11
 
10
12
  export interface IDatePickerHeaderProps
11
13
  extends BaseProps,
12
14
  ITweakStylesProps<IDatePickerHeaderStyles> {
13
- months?: string[];
15
+ getMonthSelectString: (value: Month) => string;
14
16
  }
15
17
 
16
18
  export const DatePickerHeader: FC<IDatePickerHeaderProps> = ({
17
19
  date,
18
- months = [],
20
+ getMonthSelectString,
19
21
  tweakStyles,
20
22
  prevMonthButtonDisabled,
21
23
  nextMonthButtonDisabled,
@@ -33,28 +35,24 @@ export const DatePickerHeader: FC<IDatePickerHeaderProps> = ({
33
35
  currentComponentName: 'DatePickerHeader',
34
36
  });
35
37
 
36
- const years = useMemo(
37
- () => Array.from(Array(41)).map((_, i) => getYear(new Date()) - 30 + i),
38
- [],
39
- );
40
-
41
38
  return (
42
39
  <div className={classes.header}>
43
40
  <Select
44
- value={months[getMonth(date)]}
45
- options={months}
41
+ value={getMonth(date) as Month}
42
+ options={MONTH_SELECT_OPTIONS}
43
+ convertValueToString={getMonthSelectString}
46
44
  dropdownIcon="chevron-down-small"
47
45
  isAutoSized
48
46
  tweakStyles={tweakSelectStyles}
49
- onChange={(value) => changeMonth(months.indexOf(value as string))}
47
+ onChange={(value) => isNotEmpty(value) && changeMonth(value)}
50
48
  />
51
49
  <Select
52
50
  value={getYear(date)}
53
- options={years}
51
+ options={YEARS_SELECT_OPTIONS}
54
52
  dropdownIcon="chevron-down-small"
55
53
  isAutoSized
56
54
  tweakStyles={tweakSelectStyles}
57
- onChange={(value) => changeYear(value as number)}
55
+ onChange={(value) => isNotEmpty(value) && changeYear(value)}
58
56
  />
59
57
 
60
58
  <div className={classes.buttons}>
@@ -0,0 +1,5 @@
1
+ import { getYear, Month } from 'date-fns';
2
+ import { indexMap } from '@true-engineering/true-react-platform-helpers';
3
+
4
+ export const MONTH_SELECT_OPTIONS = indexMap(12, (idx) => idx as Month);
5
+ export const YEARS_SELECT_OPTIONS = indexMap(41, (idx) => getYear(new Date()) - 30 + idx);
@@ -193,6 +193,7 @@ const meta: Meta<typeof Story> = {
193
193
  isClearable: false,
194
194
  isLoading: false,
195
195
  debounceTime: 400,
196
+ minSymbolsCountToOpenList: 0,
196
197
  endIcon: undefined,
197
198
  // custom options
198
199
  shouldUseReactNodes: false,
@@ -222,6 +223,10 @@ const meta: Meta<typeof Story> = {
222
223
  options: ['strings', 'objects'],
223
224
  },
224
225
  endIcon: { control: 'select', options: icons },
226
+ size: {
227
+ control: 'inline-radio',
228
+ options: [undefined, 'micro'],
229
+ },
225
230
  },
226
231
  };
227
232
 
@@ -28,6 +28,7 @@ import {
28
28
  import { hasExactParent } from '../../helpers';
29
29
  import { useDropdown, useIsMounted, useOnClickOutsideWithRef, useTweakStyles } from '../../hooks';
30
30
  import { IDropdownWithPopperOptions, IReplaceTweakStylesProps } from '../../types';
31
+ import { IControlWrapperIcon } from '../ControlWrapper';
31
32
  import { IIcon, renderIcon } from '../Icon';
32
33
  import { IChangeInputEvent, IInputProps, InputBase } from '../Input';
33
34
  import { ISearchInputProps, SearchInput } from '../SearchInput';
@@ -62,7 +63,7 @@ export interface ISelectProps<Value>
62
63
  minSymbolsCountToOpenList?: number;
63
64
  dropdownOptions?: IDropdownWithPopperOptions;
64
65
  /** @default 'chevron-down' */
65
- dropdownIcon?: IIcon;
66
+ dropdownIcon?: IIcon | null;
66
67
  options: Value[] | readonly Value[];
67
68
  value: Value | undefined;
68
69
  /** @default true */
@@ -611,28 +612,29 @@ export function Select<Value>(
611
612
  isLoading={areOptionsLoading}
612
613
  tweakStyles={tweakInputStyles}
613
614
  testId={testId}
614
- icon={[
615
+ endIcon={[
615
616
  isMultiSelect && shouldShowMultiSelectCounter
616
- ? {
617
+ ? ({
617
618
  key: 'counter',
618
- iconComponent: (
619
- <div key="counter" className={classes.counter}>
620
- (+{value.length - 1})
621
- </div>
622
- ),
619
+ iconComponent: <div className={classes.counter}>(+{value.length - 1})</div>,
623
620
  shouldResetSize: true,
624
- }
621
+ } satisfies IControlWrapperIcon)
625
622
  : undefined,
626
623
 
627
624
  ...getArray(endIcon),
628
625
 
629
- <div
630
- key="arrow"
631
- className={clsx(classes.arrow, isOpen && classes.activeArrow)}
632
- onClick={onArrowClick} // клик тут, потому что onClick в ControlWrapper добавляет tabIndex={0}
633
- >
634
- {renderIcon(dropdownIcon)}
635
- </div>,
626
+ isNotEmpty(dropdownIcon)
627
+ ? ({
628
+ key: 'arrow',
629
+ onClick: onArrowClick,
630
+ tabIndex: undefined,
631
+ iconComponent: (
632
+ <div className={clsx(classes.arrow, { [classes.activeArrow]: isOpen })}>
633
+ {renderIcon(dropdownIcon)}
634
+ </div>
635
+ ),
636
+ } satisfies IControlWrapperIcon)
637
+ : undefined,
636
638
  ].filter(isNotEmpty)}
637
639
  {...inputProps}
638
640
  />
@@ -175,12 +175,14 @@ export const WithPopup: FC<IWithPopupProps> = ({
175
175
  }),
176
176
  });
177
177
 
178
+ const triggerData: IDataAttributes = { popupOpen: isActive, ...data };
179
+
178
180
  const triggerElement = applyAction(trigger, {
179
181
  referenceProps: !isTriggerWrapped ? referenceProps : undefined,
180
182
  triggerProps: {
181
183
  isActive,
182
184
  isDisabled,
183
- ...(!isTriggerWrapped && { data, testId, ...referenceProps }),
185
+ ...(!isTriggerWrapped && { data: triggerData, testId, ...referenceProps }),
184
186
  },
185
187
  });
186
188
 
@@ -194,7 +196,7 @@ export const WithPopup: FC<IWithPopupProps> = ({
194
196
  [classes.active]: isActive,
195
197
  })}
196
198
  {...referenceProps}
197
- {...addDataAttributes(data, testId)}
199
+ {...addDataAttributes(triggerData, testId)}
198
200
  >
199
201
  {triggerElement}
200
202
  </div>