@true-engineering/true-react-common-ui-kit 1.0.1 → 1.1.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.
package/dist/types.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Styles } from 'react-jss';
2
2
  import { Modifier, Placement } from 'react-overlays/usePopper';
3
3
  import { ICommonIcon, IComplexIcon, IPreloaderSvgType, ISvgIcon } from './components';
4
4
  export interface IDataAttributes {
5
- [key: string]: string | null | undefined;
5
+ [key: string]: unknown;
6
6
  }
7
7
  export interface ICommonProps {
8
8
  data?: IDataAttributes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true-engineering/true-react-common-ui-kit",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -2,7 +2,12 @@ import { FC, Fragment, ReactElement, ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTheme } from '../../hooks';
4
4
  import { ICommonProps } from '../../types';
5
- import { isNotEmpty, addDataTestId, addDataAttributes } from '../../helpers';
5
+ import {
6
+ isNotEmpty,
7
+ getTestId,
8
+ addDataTestId,
9
+ addDataAttributes,
10
+ } from '../../helpers';
6
11
  import { Icon, IIconType } from '../Icon';
7
12
 
8
13
  import { ListStyles, styles } from './List.styles';
@@ -53,7 +58,9 @@ export const List: FC<IListProps> = ({
53
58
  [classes.disabledItem]: item.disabled,
54
59
  [classes.withIconGap]: item.withIconGap,
55
60
  })}
56
- {...addDataTestId(testId, `item-${idx}`)}
61
+ {...addDataTestId(item.testId ?? getTestId(testId, `item-${idx}`))}
62
+ {...(item.disabled &&
63
+ addDataAttributes({ disabled: item.disabled }))}
57
64
  onClick={item.disabled ? undefined : () => handleItemClick(item)}
58
65
  >
59
66
  {isNotEmpty(item.icon) && (
@@ -26,7 +26,7 @@ export type IModalPosition = 'center' | 'left' | 'right' | 'static';
26
26
 
27
27
  export interface IModalProps extends ICommonProps {
28
28
  tweakStyles?: ModalStyles;
29
- title?: string;
29
+ title?: ReactNode;
30
30
  size?: 'l' | 'm' | 's';
31
31
  isFooterSticky?: boolean;
32
32
  buttons?: ReactNode[];
@@ -63,6 +63,8 @@ export const styles = {
63
63
  right: 0,
64
64
  marginTop: 6,
65
65
  },
66
+
67
+ tweakList: {},
66
68
  };
67
69
 
68
70
  export type MoreMenuStyles = ComponentStyles<typeof styles>;
@@ -1,8 +1,12 @@
1
+ import { FC, MouseEvent, useRef, useState } from 'react';
1
2
  import clsx from 'clsx';
2
- import React, { FC, useRef, useState } from 'react';
3
- import { useOnClickOutsideWithRef, useTheme } from '../../hooks';
3
+ import {
4
+ useTheme,
5
+ useTweakStyles,
6
+ useOnClickOutsideWithRef,
7
+ } from '../../hooks';
4
8
  import { ICommonProps } from '../../types';
5
- import { addDataAttributes } from '../../helpers';
9
+ import { addDataAttributes, addDataTestId, getTestId } from '../../helpers';
6
10
  import { IListItem, List } from '../List';
7
11
  import { Icon } from '../Icon';
8
12
 
@@ -12,66 +16,72 @@ export interface IMoreMenuProps extends ICommonProps {
12
16
  tweakStyles?: MoreMenuStyles;
13
17
  items: IListItem[];
14
18
  isDisabled?: boolean;
15
- onMenuOpen?: () => void;
16
- onMenuClose?: () => void;
19
+ /**
20
+ * @default true
21
+ */
17
22
  hasDefaultStateBackground?: boolean;
18
23
  testId?: string;
24
+ onMenuOpen?(): void;
25
+ onMenuClose?(): void;
19
26
  }
20
27
 
21
28
  export const MoreMenu: FC<IMoreMenuProps> = ({
22
29
  items,
23
30
  isDisabled,
31
+ hasDefaultStateBackground = true,
24
32
  data,
33
+ testId,
25
34
  tweakStyles,
26
35
  onMenuOpen,
27
36
  onMenuClose,
28
- hasDefaultStateBackground = true,
29
- testId,
30
37
  }) => {
31
- const { classes } = useTheme('MoreMenu', styles, tweakStyles);
38
+ const { classes, componentStyles } = useTheme(
39
+ 'MoreMenu',
40
+ styles,
41
+ tweakStyles,
42
+ );
43
+ const tweakListStyles = useTweakStyles(
44
+ componentStyles,
45
+ tweakStyles,
46
+ 'tweakList',
47
+ );
32
48
 
33
49
  const [isMenuShown, setIsMenuShown] = useState(false);
34
50
  const list = useRef<HTMLDivElement>(null);
35
51
  const button = useRef<HTMLButtonElement>(null);
36
52
 
37
- const toggleMenu = (event: React.MouseEvent) => {
53
+ const isButtonDisabled = isDisabled || items.length === 0;
54
+
55
+ const toggleMenu = (event: MouseEvent) => {
38
56
  const isShown = !isMenuShown;
39
57
  event.stopPropagation();
40
58
  setIsMenuShown(isShown);
41
59
  if (isShown) {
42
- if (onMenuOpen !== undefined) {
43
- onMenuOpen();
44
- }
60
+ onMenuOpen?.();
45
61
  } else {
46
- if (onMenuClose !== undefined) {
47
- onMenuClose();
48
- }
62
+ onMenuClose?.();
49
63
  }
50
64
  };
51
65
 
52
66
  const handleCloseMenu = () => {
53
67
  setIsMenuShown(false);
54
- if (onMenuClose !== undefined) {
55
- onMenuClose();
56
- }
68
+ onMenuClose?.();
57
69
  };
58
70
 
59
71
  useOnClickOutsideWithRef(list, handleCloseMenu, button);
60
72
 
61
- const isButtonDisabled = isDisabled || items.length === 0;
62
-
63
73
  return (
64
74
  <div className={classes.root}>
65
75
  <button
76
+ ref={button}
66
77
  className={clsx(classes.button, {
67
78
  [classes.hasCircle]: hasDefaultStateBackground,
68
79
  [classes.disabled]: isButtonDisabled,
69
80
  [classes.active]: isMenuShown,
70
81
  })}
71
- onClick={!isButtonDisabled ? toggleMenu : undefined}
72
- ref={button}
73
- data-testid={testId}
82
+ {...addDataTestId(testId)}
74
83
  {...addDataAttributes(data)}
84
+ onClick={!isButtonDisabled ? toggleMenu : undefined}
75
85
  >
76
86
  <div className={classes.icon}>
77
87
  <Icon type="menu" />
@@ -81,8 +91,9 @@ export const MoreMenu: FC<IMoreMenuProps> = ({
81
91
  <div className={classes.menu} ref={list}>
82
92
  <List
83
93
  items={items}
94
+ testId={getTestId(testId, 'list')}
95
+ tweakStyles={tweakListStyles}
84
96
  onClick={handleCloseMenu}
85
- testId={testId !== undefined ? `${testId}-list` : undefined}
86
97
  />
87
98
  </div>
88
99
  )}
@@ -22,14 +22,15 @@ import {
22
22
  useDropdown,
23
23
  } from '../../hooks';
24
24
  import { IDropdownWithPopperOptions } from '../../types';
25
-
26
- import { SelectStyles, styles } from './Select.styles';
25
+ import { isNotEmpty } from '../../helpers';
27
26
  import {
28
27
  defaultConvertFunction,
29
28
  defaultCompareFunction,
30
29
  getActiveValueIndex,
31
30
  } from './helpers';
32
31
 
32
+ import { SelectStyles, styles } from './Select.styles';
33
+
33
34
  export interface ISelectProps<Value>
34
35
  extends Omit<IInputProps, 'value' | 'onChange' | 'type'> {
35
36
  tweakStyles?: SelectStyles;
@@ -46,19 +47,17 @@ export interface ISelectProps<Value>
46
47
 
47
48
  optionsFilter?: (options: Value[], query: string) => Value[];
48
49
  options: Value[];
49
- value?: Value;
50
- onChange: (value?: Value) => void; // подумать как возвращать индекс
51
- compareValuesOnChange?: (
52
- v1: Value | undefined,
53
- v2: Value | undefined,
54
- ) => boolean;
50
+ value: Value | undefined;
51
+ shouldScrollToList?: boolean;
52
+ onChange(value: Value | undefined): void; // подумать как возвращать индекс
53
+ compareValuesOnChange?(v1: Value | undefined, v2: Value | undefined): boolean;
55
54
  // возможно делать какую-то индексацию опций
56
55
 
57
56
  // Для избежания проблем юзайте useCallback на эти функции
58
57
  // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
59
- convertValueToString?: (value?: Value) => string | undefined;
60
- convertValueToReactNode?: (value?: Value) => ReactNode | undefined;
61
- convertValueToId?: (value?: Value) => string | undefined;
58
+ convertValueToString?(value: Value): string | undefined;
59
+ convertValueToReactNode?(value: Value): ReactNode;
60
+ convertValueToId?(value: Value): string | undefined;
62
61
  }
63
62
 
64
63
  export function Select<Value>({
@@ -85,6 +84,7 @@ export function Select<Value>({
85
84
  dropdownOptions,
86
85
  minSymbolsCountToOpenList = 0,
87
86
  dropdownIcon = 'chevron-down',
87
+ shouldScrollToList = true,
88
88
  ...inputProps
89
89
  }: ISelectProps<Value>): JSX.Element {
90
90
  const { classes, componentStyles } = useTheme('Select', styles, tweakStyles);
@@ -104,6 +104,10 @@ export function Select<Value>({
104
104
  const list = useRef<HTMLDivElement>(null);
105
105
  const input = useRef<HTMLInputElement>(null); // TODO ref снаружи?
106
106
 
107
+ const stringValue = isNotEmpty(value)
108
+ ? convertValueToString(value)
109
+ : undefined;
110
+
107
111
  const filteredOptions = useMemo(() => {
108
112
  if (optionsMode !== 'search') {
109
113
  return options;
@@ -369,7 +373,7 @@ export function Select<Value>({
369
373
  ref={inputWrapper}
370
374
  >
371
375
  <Input
372
- value={searchValue !== '' ? searchValue : convertValueToString(value)}
376
+ value={searchValue !== '' ? searchValue : stringValue}
373
377
  onChange={handleInputChange}
374
378
  isActive={isListOpen}
375
379
  isReadonly={optionsMode === 'normal'}
@@ -428,7 +432,11 @@ export function Select<Value>({
428
432
  tweakStyles={tweakStyles?.tweakSelectList as Styles}
429
433
  testId={testId !== undefined ? `${testId}-list` : undefined}
430
434
  // скролл не работает с включеным поппером
431
- shouldScrollToList={!shouldUsePopper && !shouldHideOnScroll}
435
+ shouldScrollToList={
436
+ shouldScrollToList &&
437
+ !shouldUsePopper &&
438
+ !shouldHideOnScroll
439
+ }
432
440
  />
433
441
  )}
434
442
  </div>
@@ -1,9 +1,9 @@
1
1
  import React, { ReactNode, useMemo } from 'react';
2
- import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
3
2
  import clsx from 'clsx';
4
-
3
+ import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
5
4
  import { useTheme } from '../../../hooks';
6
5
  import { ICommonProps } from '../../../types';
6
+ import { isNotEmpty } from '../../../helpers';
7
7
 
8
8
  import { SelectListStyles, styles } from './SelectList.styles';
9
9
 
@@ -19,9 +19,9 @@ export interface ISelectListProps<Value> extends ICommonProps {
19
19
  onOptionClick: (index: number) => void;
20
20
  testId?: string;
21
21
  shouldScrollToList?: boolean;
22
- convertValueToString: (value?: Value) => string | undefined;
23
- convertValueToReactNode?: (value?: Value) => ReactNode | undefined;
24
- convertValueToId?: (value?: Value) => string | undefined;
22
+ convertValueToString: (value: Value) => string | undefined;
23
+ convertValueToReactNode?: (value: Value) => ReactNode;
24
+ convertValueToId?: (value: Value) => string | undefined;
25
25
  }
26
26
 
27
27
  export function isOptionDisabled<Value>(option: Value): boolean {
@@ -51,7 +51,9 @@ export function SelectList<Value>({
51
51
  shouldScrollToList = true,
52
52
  }: ISelectListProps<Value>): JSX.Element {
53
53
  const { classes } = useTheme('SelectList', styles, tweakStyles);
54
- const activeValueId = convertValueToId(activeValue);
54
+ const activeValueId = isNotEmpty(activeValue)
55
+ ? convertValueToId(activeValue)
56
+ : undefined;
55
57
 
56
58
  const convertedToStringOptions = useMemo(
57
59
  () => options.map(convertValueToString),
@@ -1,13 +1,16 @@
1
+ import { isNotEmpty } from '../../helpers';
2
+
1
3
  export const defaultConvertFunction = (v: unknown) =>
2
4
  v === undefined ? undefined : String(v);
3
5
 
4
- export const defaultCompareFunction = <Value>(
5
- v1: Value | undefined,
6
- v2: Value | undefined,
7
- ) => v1 === v2;
6
+ export const defaultCompareFunction = <Value>(v1: Value, v2: Value) =>
7
+ v1 === v2;
8
8
 
9
9
  export const getActiveValueIndex = <Value>(
10
10
  options: Value[],
11
11
  value: Value | undefined,
12
- convertFunc: (v?: Value) => string | undefined,
13
- ): number => options.findIndex((o) => convertFunc(o) === convertFunc(value));
12
+ convertFunc: (v: Value) => string | undefined,
13
+ ): number =>
14
+ isNotEmpty(value)
15
+ ? options.findIndex((o) => convertFunc(o) === convertFunc(value))
16
+ : -1;
@@ -1,4 +1,4 @@
1
- import React, {useState, useEffect, forwardRef} from 'react';
1
+ import React, { useState, useEffect, forwardRef } from 'react';
2
2
  import { Input, IInputProps } from '../Input';
3
3
  import {
4
4
  CharactersMap,
@@ -38,140 +38,143 @@ export const SMART_INPUT_REGEX_MAP = {
38
38
  benefitCert: /^[a-zA-Z0-9/]*$/i,
39
39
  };
40
40
 
41
- export const SmartInput = forwardRef<HTMLInputElement, ISmartInputProps>(({
42
- onChange,
43
- isUpperCase,
44
- smartType = 'default',
45
- regExp,
46
- value = '',
47
- maxLength,
48
- ...rest
49
- }, ref) => {
50
- const [currentValue, setCurrentValue] = useState<string>(
51
- getUpperCaseIfNeeded(value),
52
- );
53
- const [caretPosition, setCaretPosition] = useState<number | null>(null);
54
- const [input, setInput] = useState<HTMLInputElement | null>(null);
55
- const regex = regExp || SMART_INPUT_REGEX_MAP[smartType];
56
-
57
- useEffect(() => {
58
- if (
59
- input &&
60
- input.type !== 'email' &&
61
- input.selectionStart !== caretPosition
62
- ) {
63
- input.selectionStart = caretPosition;
64
- input.selectionEnd = caretPosition;
41
+ export const SmartInput = forwardRef<HTMLInputElement, ISmartInputProps>(
42
+ (
43
+ {
44
+ onChange,
45
+ isUpperCase,
46
+ smartType = 'default',
47
+ regExp,
48
+ value = '',
49
+ maxLength,
50
+ ...rest
51
+ },
52
+ ref,
53
+ ) => {
54
+ const [currentValue, setCurrentValue] = useState<string>(
55
+ getUpperCaseIfNeeded(value),
56
+ );
57
+ const [caretPosition, setCaretPosition] = useState<number | null>(null);
58
+ const [input, setInput] = useState<HTMLInputElement | null>(null);
59
+ const regex = regExp || SMART_INPUT_REGEX_MAP[smartType];
60
+
61
+ useEffect(() => {
62
+ if (
63
+ input &&
64
+ input.type !== 'email' &&
65
+ input.selectionStart !== caretPosition
66
+ ) {
67
+ input.selectionStart = caretPosition;
68
+ input.selectionEnd = caretPosition;
69
+ }
70
+ }, [caretPosition]);
71
+
72
+ useEffect(() => {
73
+ setCurrentValue(getUpperCaseIfNeeded(value));
74
+ }, [value]);
75
+
76
+ function getUpperCaseIfNeeded(str: string) {
77
+ return isUpperCase ? str.toUpperCase() : str;
65
78
  }
66
- }, [caretPosition]);
67
79
 
68
- useEffect(() => {
69
- setCurrentValue(getUpperCaseIfNeeded(value));
70
- }, [value]);
80
+ const handleChange = (
81
+ str: string,
82
+ event?: React.FormEvent<HTMLInputElement>,
83
+ ) => {
84
+ const mappedValue = str
85
+ .split('')
86
+ .map((symbol) =>
87
+ regex.test(symbol)
88
+ ? symbol
89
+ : transformCaseSensitive(
90
+ smartType,
91
+ smartType !== 'email'
92
+ ? CharactersMap
93
+ : { ...CharactersMap, '"': '@' },
94
+ symbol,
95
+ ),
96
+ )
97
+ .filter((symbol) => regex.test(symbol))
98
+ .join('');
99
+ const domElement = event?.currentTarget;
100
+
101
+ if (domElement) {
102
+ if (!input) {
103
+ setInput(domElement);
104
+ }
105
+
106
+ setCurrentValue(getUpperCaseIfNeeded(mappedValue));
107
+ onChange(getUpperCaseIfNeeded(mappedValue));
108
+
109
+ if (mappedValue !== currentValue) {
110
+ setCaretPosition(domElement.selectionStart);
111
+ } else {
112
+ setCaretPosition(
113
+ domElement.selectionStart ? domElement.selectionStart - 1 : null,
114
+ );
115
+ }
116
+ }
117
+ };
71
118
 
72
- function getUpperCaseIfNeeded(str: string) {
73
- return isUpperCase ? str.toUpperCase() : str;
74
- }
119
+ const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
120
+ const str = event.clipboardData.getData('text/plain').split('').join('');
121
+ const domElement = event.currentTarget;
75
122
 
76
- const handleChange = (
77
- str: string,
78
- event?: React.FormEvent<HTMLInputElement>,
79
- ) => {
80
- const mappedValue = str
81
- .split('')
82
- .map((symbol) =>
83
- regex.test(symbol)
84
- ? symbol
85
- : transformCaseSensitive(
86
- smartType,
87
- smartType !== 'email'
88
- ? CharactersMap
89
- : { ...CharactersMap, '"': '@' },
90
- symbol,
91
- ),
92
- )
93
- .filter((symbol) => regex.test(symbol))
94
- .join('');
95
- const domElement = event?.currentTarget;
96
-
97
- if (domElement) {
98
123
  if (!input) {
99
124
  setInput(domElement);
100
125
  }
101
126
 
102
- setCurrentValue(getUpperCaseIfNeeded(mappedValue));
103
- onChange(getUpperCaseIfNeeded(mappedValue));
104
-
105
- if (mappedValue !== currentValue) {
106
- setCaretPosition(domElement.selectionStart);
107
- } else {
108
- setCaretPosition(
109
- domElement.selectionStart ? domElement.selectionStart - 1 : null,
110
- );
127
+ event.preventDefault();
128
+ const selectionStart = domElement.selectionStart ?? 0;
129
+ const selectionEnd = domElement.selectionEnd ?? 0;
130
+
131
+ let mappedValue = str
132
+ .split('')
133
+ .map((symbol) =>
134
+ regex.test(symbol)
135
+ ? symbol
136
+ : transformCaseSensitive(smartType, TransliterationMap, symbol),
137
+ )
138
+ .filter((letter) => regex.test(letter))
139
+ .join('');
140
+
141
+ const newValueLength =
142
+ mappedValue.length +
143
+ currentValue.length -
144
+ (selectionEnd - selectionStart);
145
+
146
+ if (
147
+ maxLength !== undefined &&
148
+ maxLength >= 0 &&
149
+ newValueLength > maxLength
150
+ ) {
151
+ const validMappedValueLength =
152
+ mappedValue.length - (newValueLength - maxLength);
153
+
154
+ mappedValue = mappedValue.substring(0, validMappedValueLength);
111
155
  }
112
- }
113
- };
114
-
115
- const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
116
- const str = event.clipboardData.getData('text/plain').split('').join('');
117
- const domElement = event.currentTarget;
118
-
119
- if (!input) {
120
- setInput(domElement);
121
- }
122
156
 
123
- event.preventDefault();
124
- const selectionStart = domElement.selectionStart ?? 0;
125
- const selectionEnd = domElement.selectionEnd ?? 0;
126
-
127
- let mappedValue = str
128
- .split('')
129
- .map((symbol) =>
130
- regex.test(symbol)
131
- ? symbol
132
- : transformCaseSensitive(smartType, TransliterationMap, symbol),
133
- )
134
- .filter((letter) => regex.test(letter))
135
- .join('');
136
-
137
- const newValueLength =
138
- mappedValue.length +
139
- currentValue.length -
140
- (selectionEnd - selectionStart);
141
-
142
- if (
143
- maxLength !== undefined &&
144
- maxLength >= 0 &&
145
- newValueLength > maxLength
146
- ) {
147
- const validMappedValueLength =
148
- mappedValue.length - (newValueLength - maxLength);
149
-
150
- mappedValue = mappedValue.substring(0, validMappedValueLength);
151
- }
152
-
153
- const newValue = getUpperCaseIfNeeded(
154
- `${currentValue?.substring(
155
- 0,
156
- selectionStart,
157
- )}${mappedValue}${currentValue?.substring(
158
- selectionEnd,
159
- )}`,
157
+ const newValue = getUpperCaseIfNeeded(
158
+ `${currentValue?.substring(
159
+ 0,
160
+ selectionStart,
161
+ )}${mappedValue}${currentValue?.substring(selectionEnd)}`,
162
+ );
163
+
164
+ setCaretPosition(selectionStart + mappedValue.length);
165
+ setCurrentValue(newValue);
166
+ onChange(newValue);
167
+ };
168
+
169
+ return (
170
+ <Input
171
+ {...rest}
172
+ ref={ref}
173
+ maxLength={maxLength}
174
+ onChange={handleChange}
175
+ onPaste={handlePaste}
176
+ value={currentValue}
177
+ />
160
178
  );
161
-
162
- setCaretPosition(selectionStart + mappedValue.length);
163
- setCurrentValue(newValue);
164
- onChange(newValue);
165
- };
166
-
167
- return (
168
- <Input
169
- {...rest}
170
- ref={ref}
171
- maxLength={maxLength}
172
- onChange={handleChange}
173
- onPaste={handlePaste}
174
- value={currentValue}
175
- />
176
- );
177
- });
179
+ },
180
+ );
package/src/types.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from './components';
9
9
 
10
10
  export interface IDataAttributes {
11
- [key: string]: string | null | undefined;
11
+ [key: string]: unknown;
12
12
  }
13
13
 
14
14
  export interface ICommonProps {