@true-engineering/true-react-common-ui-kit 1.10.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true-engineering/true-react-common-ui-kit",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  useMemo,
3
- FC,
4
3
  ReactElement,
5
4
  ReactNode,
6
5
  ButtonHTMLAttributes,
7
6
  MouseEvent,
7
+ forwardRef,
8
8
  } from 'react';
9
9
  import clsx from 'clsx';
10
10
  import merge from 'lodash-es/merge';
@@ -102,94 +102,106 @@ export interface IButtonProps extends ICommonProps {
102
102
  onMouseDown?(event: MouseEvent<HTMLButtonElement>): void | Promise<void>;
103
103
  }
104
104
 
105
- export const Button: FC<IButtonProps> = ({
106
- type = 'button',
107
- children,
108
- size = 'l',
109
- view = 'primary',
110
- isFullWidth = false,
111
- isInline = false,
112
- isDisabled = false,
113
- isActive = false,
114
- isLoading = false,
115
- shouldSkipTabNavigation = false,
116
- data,
117
- testId,
118
- tweakStyles,
119
- icon,
120
- iconPosition = 'left',
121
- preloaderType = 'dots',
122
- onClick,
123
- onMouseDown,
124
- }) => {
125
- const { classes, componentStyles } = useTheme('Button', styles, tweakStyles);
105
+ export const Button = forwardRef<HTMLButtonElement, IButtonProps>(
106
+ (
107
+ {
108
+ type = 'button',
109
+ children,
110
+ size = 'l',
111
+ view = 'primary',
112
+ isFullWidth = false,
113
+ isInline = false,
114
+ isDisabled = false,
115
+ isActive = false,
116
+ isLoading = false,
117
+ shouldSkipTabNavigation = false,
118
+ data,
119
+ testId,
120
+ tweakStyles,
121
+ icon,
122
+ iconPosition = 'left',
123
+ preloaderType = 'dots',
124
+ onClick,
125
+ onMouseDown,
126
+ },
127
+ ref,
128
+ ) => {
129
+ const { classes, componentStyles } = useTheme(
130
+ 'Button',
131
+ styles,
132
+ tweakStyles,
133
+ );
126
134
 
127
- const tweakPreloaderStyles = useMemo(
128
- () =>
129
- merge(
130
- {},
131
- size === 's' || size === 'm' ? dotsPreloaderStyles : undefined,
132
- componentStyles.tweakPreloader,
133
- tweakStyles?.tweakPreloader,
134
- ) as ThemedPreloaderStyles,
135
- [tweakStyles?.tweakPreloader, size],
136
- );
135
+ const tweakPreloaderStyles = useMemo(
136
+ () =>
137
+ merge(
138
+ {},
139
+ size === 's' || size === 'm' ? dotsPreloaderStyles : undefined,
140
+ componentStyles.tweakPreloader,
141
+ tweakStyles?.tweakPreloader,
142
+ ) as ThemedPreloaderStyles,
143
+ [tweakStyles?.tweakPreloader, size],
144
+ );
137
145
 
138
- const hasIcon = isNotEmpty(icon);
139
- const hasChildren = isNotEmpty(children);
140
- const hasNoAction = isDisabled || isLoading;
146
+ const hasIcon = isNotEmpty(icon);
147
+ const hasChildren = isNotEmpty(children);
148
+ const hasNoAction = isDisabled || isLoading;
141
149
 
142
- return (
143
- <button
144
- type={type}
145
- className={clsx(classes.root, classes[size], classes[view], {
146
- [classes.disabled]: isDisabled,
147
- [classes.fullWidth]: isFullWidth,
148
- [classes.inline]: isInline,
149
- [classes.active]: isActive,
150
- [classes.loading]: isLoading,
151
- [classes.onlyIcon]: hasIcon && !hasChildren,
152
- })}
153
- tabIndex={shouldSkipTabNavigation ? -1 : undefined}
154
- disabled={hasNoAction}
155
- onClick={!hasNoAction ? onClick : undefined}
156
- onMouseDown={!hasNoAction ? onMouseDown : undefined}
157
- {...addDataTestId(testId)}
158
- {...addDataAttributes(data)}
159
- >
160
- <span
161
- className={clsx(classes.content, {
162
- [classes.iconFromRight]:
163
- hasChildren && hasIcon && iconPosition === 'right',
164
- [classes.iconFromLeft]:
165
- hasChildren && hasIcon && iconPosition === 'left',
150
+ return (
151
+ <button
152
+ ref={ref}
153
+ type={type}
154
+ className={clsx(classes.root, classes[size], classes[view], {
155
+ [classes.disabled]: isDisabled,
156
+ [classes.fullWidth]: isFullWidth,
157
+ [classes.inline]: isInline,
158
+ [classes.active]: isActive,
159
+ [classes.loading]: isLoading,
160
+ [classes.onlyIcon]: hasIcon && !hasChildren,
166
161
  })}
162
+ tabIndex={shouldSkipTabNavigation ? -1 : undefined}
163
+ disabled={hasNoAction}
164
+ onClick={!hasNoAction ? onClick : undefined}
165
+ onMouseDown={!hasNoAction ? onMouseDown : undefined}
166
+ {...addDataTestId(testId)}
167
+ {...addDataAttributes(data)}
167
168
  >
168
- {hasIcon && (
169
- <span className={classes.icon}>
170
- {typeof icon === 'string' ? (
171
- <Icon type={icon as IIconType} />
172
- ) : (
173
- icon
174
- )}
175
- </span>
176
- )}
177
- {hasChildren && (
178
- <span className={clsx(classes.children, hasIcon && classes.withIcon)}>
179
- {children}
169
+ <span
170
+ className={clsx(classes.content, {
171
+ [classes.iconFromRight]:
172
+ hasChildren && hasIcon && iconPosition === 'right',
173
+ [classes.iconFromLeft]:
174
+ hasChildren && hasIcon && iconPosition === 'left',
175
+ })}
176
+ >
177
+ {hasIcon && (
178
+ <span className={classes.icon}>
179
+ {typeof icon === 'string' ? (
180
+ <Icon type={icon as IIconType} />
181
+ ) : (
182
+ icon
183
+ )}
184
+ </span>
185
+ )}
186
+ {hasChildren && (
187
+ <span
188
+ className={clsx(classes.children, hasIcon && classes.withIcon)}
189
+ >
190
+ {children}
191
+ </span>
192
+ )}
193
+ </span>
194
+
195
+ {isLoading && (
196
+ <span className={classes.loader}>
197
+ <ThemedPreloader
198
+ type={preloaderType}
199
+ useCurrentColor
200
+ tweakStyles={tweakPreloaderStyles}
201
+ />
180
202
  </span>
181
203
  )}
182
- </span>
183
-
184
- {isLoading && (
185
- <span className={classes.loader}>
186
- <ThemedPreloader
187
- type={preloaderType}
188
- useCurrentColor
189
- tweakStyles={tweakPreloaderStyles}
190
- />
191
- </span>
192
- )}
193
- </button>
194
- );
195
- };
204
+ </button>
205
+ );
206
+ },
207
+ );
@@ -240,6 +240,7 @@ MultiSelect.args = {
240
240
  errorPosition: 'bottom',
241
241
  hasFloatingLabel: true,
242
242
  hasRequiredLabel: true,
243
+ iconType: 'document',
243
244
  defaultOptionLabel: '',
244
245
  allOptionsLabel: 'Все опции',
245
246
  isDisabled: false,
@@ -52,22 +52,20 @@ export const styles = {
52
52
  },
53
53
  },
54
54
 
55
- tweakInput: {
56
- input: {
57
- paddingRight: 32,
58
- },
59
-
60
- withUnits: {
55
+ counter: {
56
+ '&:not(:last-child)': {
61
57
  paddingRight: 8,
62
58
  },
59
+ },
63
60
 
64
- unitsWrapper: {
65
- position: 'unset',
66
- padding: [0, 32, 0, 0],
67
- },
61
+ icon: {
62
+ width: 16,
63
+ height: 16,
64
+ },
68
65
 
69
- fakeValue: {
70
- display: 'none',
66
+ tweakInput: {
67
+ input: {
68
+ paddingRight: 32,
71
69
  },
72
70
 
73
71
  disabled: {
@@ -44,6 +44,7 @@ import { SelectStyles, styles } from './Select.styles';
44
44
  import { ISearchInputProps, SearchInput } from '../SearchInput';
45
45
  import { IMultipleSelectValue } from './types';
46
46
  import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from './constants';
47
+ import { renderIcon } from '../../helpers/snippets';
47
48
 
48
49
  export interface ISelectProps<Value>
49
50
  extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type'> {
@@ -114,7 +115,7 @@ export function Select<Value>(
114
115
  dropdownIcon = 'chevron-down',
115
116
  shouldScrollToList = true,
116
117
  searchInput,
117
- units,
118
+ iconType,
118
119
  onChange,
119
120
  onFocus,
120
121
  onBlur,
@@ -167,6 +168,19 @@ export function Select<Value>(
167
168
  return filter(options, searchValue);
168
169
  }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
169
170
 
171
+ const availableOptions = useMemo(
172
+ () => options.filter((o) => !isOptionDisabled(o)),
173
+ [options, isOptionDisabled],
174
+ );
175
+
176
+ const areAllOptionsSelected =
177
+ isMultiSelect && value?.length === availableOptions.length;
178
+ const shouldShowMultiSelectCounter =
179
+ isMultiSelect &&
180
+ isNotEmpty(value) &&
181
+ value.length > 1 &&
182
+ !areAllOptionsSelected;
183
+
170
184
  const optionsIndexesForNavigation = useMemo(() => {
171
185
  const result: number[] = [];
172
186
  if (shouldShowDefaultOption && hasDefaultOption) {
@@ -190,9 +204,7 @@ export function Select<Value>(
190
204
  : undefined;
191
205
  // Для мультиселекта пытаемся показать "Все опции" если выбраны все опции
192
206
  const showedStringValue =
193
- isMultiSelect &&
194
- value?.length === filteredOptions.length &&
195
- isNotEmpty(allOptionsLabel)
207
+ areAllOptionsSelected && isNotEmpty(allOptionsLabel)
196
208
  ? allOptionsLabel
197
209
  : stringValue;
198
210
 
@@ -292,7 +304,7 @@ export function Select<Value>(
292
304
  return;
293
305
  }
294
306
  if (index === ALL_OPTION_INDEX && isSelected) {
295
- handleOnChange(options as IMultipleSelectValue<Value>);
307
+ handleOnChange(availableOptions as IMultipleSelectValue<Value>);
296
308
  return;
297
309
  }
298
310
  const option = filteredOptions[index];
@@ -374,7 +386,7 @@ export function Select<Value>(
374
386
  if (isMultiSelect) {
375
387
  let isThisValueAlreadySelected: boolean;
376
388
  if (indexToSelect === ALL_OPTION_INDEX) {
377
- isThisValueAlreadySelected = value?.length === options.length;
389
+ isThisValueAlreadySelected = areAllOptionsSelected;
378
390
  } else {
379
391
  // подумать над концептом реального фокуса на опциях, а не вот эти вот focusedCell
380
392
  const valueIdToSelect = convertToId(filteredOptions[indexToSelect]);
@@ -456,6 +468,7 @@ export function Select<Value>(
456
468
  {},
457
469
  componentStyles.tweakInput,
458
470
  { ...(hasReadonlyInput && { input: { cursor: 'pointer' } }) },
471
+ { ...(isMultiSelect && { inputIcon: { width: 'auto' } }) },
459
472
  tweakStyles?.tweakInput,
460
473
  ) as Styles,
461
474
  [tweakStyles?.tweakInput, hasReadonlyInput],
@@ -519,6 +532,7 @@ export function Select<Value>(
519
532
  : undefined
520
533
  }
521
534
  allOptionsLabel={shouldShowAllOption ? allOptionsLabel : undefined}
535
+ areAllOptionsSelected={areAllOptionsSelected}
522
536
  customListHeader={
523
537
  hasSearchInputInList ? (
524
538
  <SearchInput
@@ -554,6 +568,18 @@ export function Select<Value>(
554
568
  </div>
555
569
  );
556
570
 
571
+ const multiSelectCounterWithIcon =
572
+ shouldShowMultiSelectCounter || isNotEmpty(iconType) ? (
573
+ <>
574
+ {shouldShowMultiSelectCounter && (
575
+ <div className={classes.counter}>(+{value.length - 1})</div>
576
+ )}
577
+ {isNotEmpty(iconType) && (
578
+ <div className={classes.icon}>{renderIcon(iconType)}</div>
579
+ )}
580
+ </>
581
+ ) : undefined;
582
+
557
583
  return (
558
584
  <div className={classes.root} onKeyDown={handleKeyDown}>
559
585
  <div
@@ -577,14 +603,7 @@ export function Select<Value>(
577
603
  isLoading={areOptionsLoading}
578
604
  tweakStyles={tweakInputStyles}
579
605
  testId={testId}
580
- units={
581
- isMultiSelect &&
582
- isNotEmpty(value) &&
583
- value.length > 1 &&
584
- value.length !== options.length
585
- ? `(+${value.length - 1})`
586
- : units
587
- }
606
+ iconType={isMultiSelect ? multiSelectCounterWithIcon : iconType}
588
607
  {...inputProps}
589
608
  />
590
609
  <div
@@ -20,6 +20,7 @@ export interface ISelectListProps<Value> extends ICommonProps {
20
20
  defaultOptionLabel?: string;
21
21
  testId?: string;
22
22
  allOptionsLabel?: string;
23
+ areAllOptionsSelected?: boolean;
23
24
  shouldScrollToList?: boolean;
24
25
  customListHeader?: ReactNode;
25
26
  onOptionSelect(index: number, event: MouseEvent<HTMLElement>): void;
@@ -41,6 +42,7 @@ export function SelectList<Value>({
41
42
  tweakStyles,
42
43
  testId,
43
44
  shouldScrollToList = true,
45
+ areAllOptionsSelected,
44
46
  customListHeader,
45
47
  isOptionDisabled,
46
48
  allOptionsLabel,
@@ -118,10 +120,9 @@ export function SelectList<Value>({
118
120
  classes={classes}
119
121
  index={ALL_OPTION_INDEX}
120
122
  isSemiChecked={
121
- selectedOptionsCount > 0 &&
122
- selectedOptionsCount < options.length
123
+ selectedOptionsCount > 0 && !areAllOptionsSelected
123
124
  }
124
- isActive={selectedOptionsCount === options.length}
125
+ isActive={areAllOptionsSelected}
125
126
  isFocused={focusedIndex === ALL_OPTION_INDEX}
126
127
  onOptionSelect={onOptionSelect}
127
128
  onToggleCheckbox={onToggleCheckbox}