@true-engineering/true-react-common-ui-kit 1.2.0 → 1.3.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.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -38,7 +38,8 @@
38
38
  "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
39
39
  "format": "prettier --write \"**/*.{js,jsx,css,json,ts,tsx}\"",
40
40
  "format:check": "prettier --check \"**/*.{js,jsx,css,json,ts,tsx}\"",
41
- "full-check": "yarn format:check && yarn lint"
41
+ "full-check": "yarn format:check && yarn lint",
42
+ "types-check": "tsc --noEmit"
42
43
  },
43
44
  "dependencies": {
44
45
  "clsx": "1.2.1",
@@ -10,7 +10,7 @@ export default {
10
10
  };
11
11
 
12
12
  const Template: ComponentStory<typeof DateInput> = (args) => {
13
- const [value, setValue] = useState<string | undefined>(args.date);
13
+ const [value, setValue] = useState(args.date);
14
14
 
15
15
  useEffect(() => {
16
16
  setValue(args.date);
@@ -93,7 +93,7 @@ export const FilterWithPeriod: FC<IFilterWithPeriodProps> = ({
93
93
  value?.periodType === 'CUSTOM',
94
94
  );
95
95
 
96
- const [period, setPeriod] = useState<IPeriod | undefined>(value);
96
+ const [period, setPeriod] = useState(value);
97
97
 
98
98
  const periodGetters = useMemo(() => {
99
99
  const result: Record<string, IPeriodGetter> = { ...PERIODS_GETTERS };
@@ -21,6 +21,7 @@ import TableRow from './TableRow';
21
21
 
22
22
  import { FlexibleTableStyles, styles } from './FlexibleTable.styles';
23
23
 
24
+ // TODO: Заменить Record<string, any> на Record<string, unknown>
24
25
  export interface IFlexibleTableProps<Values extends Record<string, any>>
25
26
  extends ICommonProps {
26
27
  tweakStyles?: FlexibleTableStyles;
@@ -34,6 +35,7 @@ export interface IFlexibleTableProps<Values extends Record<string, any>>
34
35
  infinityScrollConfig?: IInfinityScrollConfig;
35
36
  uniqueField?: keyof Values;
36
37
  onHeadClick?: (column: keyof Values) => void;
38
+ // TODO: Заменить string на Generic Values[uniqueField]
37
39
  onRowClick?: (id: string) => void;
38
40
  onRowHover?: (id?: string) => void;
39
41
  rowAttributes?: Array<keyof Values>;
@@ -6,6 +6,7 @@ import { ICommonProps, IDataAttributes } from '../../types';
6
6
  import TableValue from './TableValue';
7
7
  import { addDataAttributes } from '../../helpers';
8
8
 
9
+ // TODO: Заменить Record<string, any> на Record<string, unknown>
9
10
  interface ITableRowProps<Values extends Record<string, any>>
10
11
  extends ICommonProps {
11
12
  item: Values;
@@ -20,6 +21,7 @@ interface ITableRowProps<Values extends Record<string, any>>
20
21
  isOpen: boolean,
21
22
  close: () => void,
22
23
  ) => React.ReactNode;
24
+ // TODO: Заменить string на Generic Values[uniqueField]
23
25
  onRowHover?: (id?: string) => void;
24
26
  onRowClick?: (id: string) => void;
25
27
  // чтобы не перерендеривать стили для каждой строчки / ячейки
@@ -131,7 +133,7 @@ function TableRow<Values extends Record<string, any>>({
131
133
  >
132
134
  {items.map((key, idx) => (
133
135
  <TableValue
134
- columnName={key as string}
136
+ columnName={key}
135
137
  isSticky={isFirstColumnSticky && idx === 0}
136
138
  isSecond={isFirstColumnSticky && idx === 1}
137
139
  key={key as string}
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
3
3
  import { format } from 'date-fns';
4
4
 
5
5
  import type { ICommonProps } from '../../types';
6
- import type { FlexibleTableConfigType, IValueComponent } from './types';
6
+ import type { FlexibleTableConfigType } from './types';
7
7
 
8
8
  interface ITableValueProps<Values extends Record<string, any>>
9
9
  extends ICommonProps {
@@ -42,10 +42,7 @@ function TableValue<Values extends Record<string, any>>({
42
42
  let content = null;
43
43
 
44
44
  if (itemConfig?.component) {
45
- const ValueComponent = itemConfig?.component as IValueComponent<
46
- Values,
47
- typeof columnName
48
- >;
45
+ const ValueComponent = itemConfig?.component;
49
46
  content = (
50
47
  <ValueComponent
51
48
  value={value}
@@ -17,7 +17,7 @@ export type ITitleComponent<Value> = FC<{
17
17
  }>;
18
18
 
19
19
  export type IValueComponent<Values, Value> = FC<{
20
- value?: Value;
20
+ value: Value;
21
21
  row: Values;
22
22
  isFocusedRow?: boolean;
23
23
  isNestedComponentExpanded: boolean;
@@ -28,7 +28,7 @@ export type IValueComponent<Values, Value> = FC<{
28
28
  export type FlexibleTableConfigType<Values> = {
29
29
  [Key in keyof Values]?: {
30
30
  title?: ReactNode;
31
- titleComponent?: ITitleComponent<any>;
31
+ titleComponent?: ITitleComponent<unknown>;
32
32
  component?: IValueComponent<Values, Values[Key]>;
33
33
  dateFormat?: string;
34
34
  minWidth?: string | number;
@@ -9,7 +9,7 @@ export default {
9
9
  };
10
10
 
11
11
  const Template: ComponentStory<typeof IncrementInput> = (args) => {
12
- const [value, setValue] = useState<number | undefined>(undefined);
12
+ const [value, setValue] = useState<number>();
13
13
  return (
14
14
  <IncrementInput {...args} value={value} onChange={(v) => setValue(v)} />
15
15
  );
@@ -9,7 +9,7 @@ export default {
9
9
  };
10
10
 
11
11
  const Template: ComponentStory<typeof NumberInput> = (args) => {
12
- const [value, setValue] = useState<number | undefined>(undefined);
12
+ const [value, setValue] = useState<number>();
13
13
  return <NumberInput {...args} value={value} onChange={(v) => setValue(v)} />;
14
14
  };
15
15
 
@@ -31,8 +31,11 @@ const convertObjectToString = (v: ObjectValue): string => v.name;
31
31
 
32
32
  const convertObjectToId = (v: ObjectValue): string => `${v.name}${v.age}`;
33
33
 
34
- const convertObjectToReactNode = (v: ObjectValue): ReactNode => (
35
- <span>
34
+ const convertObjectToReactNode = (
35
+ v: ObjectValue,
36
+ isDisabled: boolean,
37
+ ): ReactNode => (
38
+ <span style={{ color: isDisabled ? 'red' : undefined }}>
36
39
  <i>{v.name}</i>, {v.age}
37
40
  </span>
38
41
  );
@@ -40,6 +43,7 @@ const convertObjectToReactNode = (v: ObjectValue): ReactNode => (
40
43
  const convertStringToReactNode = (v: string): ReactNode => <i>{v}</i>;
41
44
 
42
45
  const isOptionDisabled = (option: string) => option.startsWith('Опция');
46
+ const isObjectOptionDisabled = (option: ObjectValue) => option.age > 30;
43
47
 
44
48
  const stringOptions = [
45
49
  'Опция 1',
@@ -91,12 +95,12 @@ function SelectWithCustomProps<T>({
91
95
  scrollParent,
92
96
  ...rest
93
97
  }: ISelectWithCustomProps<T>) {
94
- const [stringValue, setStringValue] = useState<string | undefined>();
98
+ const [stringValue, setStringValue] = useState<string>();
95
99
  const stringHandler = (newValue?: string) => {
96
100
  setStringValue(newValue);
97
101
  };
98
102
 
99
- const [objectValue, setObjectValue] = useState<ObjectValue | undefined>();
103
+ const [objectValue, setObjectValue] = useState<ObjectValue>();
100
104
  const objectHandler = (newValue?: ObjectValue) => {
101
105
  setObjectValue(newValue);
102
106
  };
@@ -125,6 +129,10 @@ function SelectWithCustomProps<T>({
125
129
  Array<string | ObjectValue>
126
130
  >([]);
127
131
 
132
+ const handleOpen = () => {
133
+ console.log('isOpen');
134
+ };
135
+
128
136
  useEffect(() => {
129
137
  const api = async () => {
130
138
  setDynamicOptions(await getOptions());
@@ -133,10 +141,6 @@ function SelectWithCustomProps<T>({
133
141
  api();
134
142
  }, [selectValuesType]);
135
143
 
136
- const handleOpen = () => {
137
- console.log('isOpen');
138
- };
139
-
140
144
  const props =
141
145
  selectValuesType === 'strings'
142
146
  ? {
@@ -146,6 +150,9 @@ function SelectWithCustomProps<T>({
146
150
  convertValueToReactNode: shouldRenderAsReactNodes
147
151
  ? convertStringToReactNode
148
152
  : undefined,
153
+ isOptionDisabled: shouldUseCustomIsDisabledFunction
154
+ ? isOptionDisabled
155
+ : undefined,
149
156
  }
150
157
  : {
151
158
  onChange: objectHandler,
@@ -156,6 +163,9 @@ function SelectWithCustomProps<T>({
156
163
  convertValueToReactNode: shouldRenderAsReactNodes
157
164
  ? convertObjectToReactNode
158
165
  : undefined,
166
+ isOptionDisabled: shouldUseCustomIsDisabledFunction
167
+ ? isObjectOptionDisabled
168
+ : undefined,
159
169
  };
160
170
 
161
171
  return (
@@ -165,9 +175,6 @@ function SelectWithCustomProps<T>({
165
175
  optionsMode={optionsMode}
166
176
  onType={async () => setDynamicOptions(await getOptions())}
167
177
  onOpen={handleOpen}
168
- isOptionDisabled={
169
- shouldUseCustomIsDisabledFunction ? isOptionDisabled : undefined
170
- }
171
178
  dropdownOptions={{
172
179
  shouldUsePopper,
173
180
  shouldRenderInBody,
@@ -229,6 +236,7 @@ Default.args = {
229
236
  shouldRenderInBody: false,
230
237
  shouldHideOnScroll: false,
231
238
  shouldUseCustomIsDisabledFunction: false,
239
+ shouldScrollToList: true,
232
240
  canBeFlipped: false,
233
241
  scrollParent: 'document',
234
242
  };
@@ -56,7 +56,7 @@ export interface ISelectProps<Value>
56
56
  // Для избежания проблем юзайте useCallback на эти функции
57
57
  // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
58
58
  convertValueToString?(value: Value): string | undefined;
59
- convertValueToReactNode?(value: Value): ReactNode;
59
+ convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
60
60
  convertValueToId?(value: Value): string | undefined;
61
61
  }
62
62
 
@@ -366,6 +366,47 @@ export function Select<Value>({
366
366
  }
367
367
  }, [isOpen, onOpen]);
368
368
 
369
+ const listEl = (
370
+ <div
371
+ className={clsx(classes.listWrapper, {
372
+ [classes.withoutPopper]: !shouldUsePopper,
373
+ [classes.listWrapperInBody]: shouldRenderInBody,
374
+ })}
375
+ ref={list}
376
+ // чтобы предотвратить onBlur на инпуте
377
+ onMouseDown={(event) => event.preventDefault()}
378
+ style={popperData?.styles.popper as Styles}
379
+ {...popperData?.attributes.popper}
380
+ >
381
+ {isOpen && (
382
+ <SelectList
383
+ options={filteredOptions}
384
+ defaultOptionLabel={
385
+ hasDefaultOption && shouldShowDefaultOption
386
+ ? defaultOptionLabel
387
+ : undefined
388
+ }
389
+ noMatchesLabel={noMatchesLabel}
390
+ focusedIndex={focusedListCellIndex}
391
+ activeValue={value}
392
+ isLoading={inputProps.isLoading}
393
+ loadingLabel={loadingLabel}
394
+ tweakStyles={tweakStyles?.tweakSelectList as Styles}
395
+ testId={testId !== undefined ? `${testId}-list` : undefined}
396
+ // скролл не работает с включеным поппером
397
+ shouldScrollToList={
398
+ shouldScrollToList && !shouldUsePopper && !shouldHideOnScroll
399
+ }
400
+ isOptionDisabled={isOptionDisabled}
401
+ convertValueToString={convertValueToString}
402
+ convertValueToReactNode={convertValueToReactNode}
403
+ convertValueToId={convertValueToId}
404
+ onOptionClick={handleOptionClick}
405
+ />
406
+ )}
407
+ </div>
408
+ );
409
+
369
410
  return (
370
411
  <div className={classes.root} onKeyDown={handleKeyDown}>
371
412
  <div
@@ -397,54 +438,15 @@ export function Select<Value>({
397
438
  <Icon type={dropdownIcon} />
398
439
  </div>
399
440
  </div>
400
- <Portal
401
- container={shouldRenderInBody ? document.body : inputWrapper.current}
402
- >
403
- <>
404
- {(shouldUsePopper || isOpen) && (
405
- <div
406
- className={clsx(classes.listWrapper, {
407
- [classes.withoutPopper]: !shouldUsePopper,
408
- [classes.listWrapperInBody]: shouldRenderInBody,
409
- })}
410
- ref={list}
411
- // чтобы предотвратить onBlur на инпуте
412
- onMouseDown={(event) => event.preventDefault()}
413
- style={popperData?.styles.popper as Styles}
414
- {...popperData?.attributes.popper}
415
- >
416
- {isOpen && (
417
- <SelectList
418
- options={filteredOptions}
419
- defaultOptionLabel={
420
- hasDefaultOption && shouldShowDefaultOption
421
- ? defaultOptionLabel
422
- : undefined
423
- }
424
- noMatchesLabel={noMatchesLabel}
425
- focusedIndex={focusedListCellIndex}
426
- activeValue={value}
427
- isLoading={inputProps.isLoading}
428
- loadingLabel={loadingLabel}
429
- tweakStyles={tweakStyles?.tweakSelectList as Styles}
430
- testId={testId !== undefined ? `${testId}-list` : undefined}
431
- // скролл не работает с включеным поппером
432
- shouldScrollToList={
433
- shouldScrollToList &&
434
- !shouldUsePopper &&
435
- !shouldHideOnScroll
436
- }
437
- isOptionDisabled={isOptionDisabled}
438
- convertValueToString={convertValueToString}
439
- convertValueToReactNode={convertValueToReactNode}
440
- convertValueToId={convertValueToId}
441
- onOptionClick={handleOptionClick}
442
- />
443
- )}
444
- </div>
445
- )}
446
- </>
447
- </Portal>
441
+ {shouldUsePopper ? (
442
+ <Portal
443
+ container={shouldRenderInBody ? document.body : inputWrapper.current}
444
+ >
445
+ <>{listEl}</>
446
+ </Portal>
447
+ ) : (
448
+ <>{isOpen && listEl}</>
449
+ )}
448
450
  </div>
449
451
  );
450
452
  }
@@ -3,7 +3,7 @@ import clsx from 'clsx';
3
3
  import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
4
4
  import { useTheme } from '../../../hooks';
5
5
  import { ICommonProps } from '../../../types';
6
- import { isNotEmpty } from '../../../helpers';
6
+ import { addDataAttributes, isNotEmpty } from '../../../helpers';
7
7
  import { SelectListStyles, styles } from './SelectList.styles';
8
8
 
9
9
  export interface ISelectListProps<Value> extends ICommonProps {
@@ -20,7 +20,7 @@ export interface ISelectListProps<Value> extends ICommonProps {
20
20
  onOptionClick(index: number): void;
21
21
  isOptionDisabled(value: Value): boolean;
22
22
  convertValueToString(value: Value): string | undefined;
23
- convertValueToReactNode?(value: Value): ReactNode;
23
+ convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
24
24
  convertValueToId?(value: Value): string | undefined;
25
25
  }
26
26
 
@@ -48,26 +48,20 @@ export function SelectList<Value>({
48
48
  ? convertValueToId(activeValue)
49
49
  : undefined;
50
50
 
51
- const convertedToStringOptions = useMemo(
52
- () => options.map(convertValueToString),
53
- [options, convertValueToString],
54
- );
55
-
56
51
  const isActiveOption = (item: Value): boolean =>
57
52
  convertValueToId(item) === activeValueId;
58
53
 
59
- const convertedToReactNodesOptions = useMemo(
60
- () =>
61
- convertValueToReactNode !== undefined
62
- ? options.map(convertValueToReactNode)
63
- : [],
64
- [options, convertValueToReactNode],
54
+ const convertFunction = convertValueToReactNode ?? convertValueToString;
55
+
56
+ const optionsDisableMap = useMemo(
57
+ () => options.map((o) => isOptionDisabled(o)),
58
+ [options, isOptionDisabled],
65
59
  );
66
60
 
67
- const listOptions =
68
- convertValueToReactNode !== undefined
69
- ? convertedToReactNodesOptions
70
- : convertedToStringOptions;
61
+ const listOptions = useMemo(
62
+ () => options.map((opt, i) => convertFunction(opt, optionsDisableMap[i])),
63
+ [options, convertFunction, optionsDisableMap],
64
+ );
71
65
 
72
66
  return (
73
67
  <ScrollIntoViewIfNeeded
@@ -95,23 +89,29 @@ export function SelectList<Value>({
95
89
  {defaultOptionLabel}
96
90
  </ScrollIntoViewIfNeeded>
97
91
  )}
98
- {listOptions.map((opt, index) => {
99
- const optionValue = options[index];
92
+ {listOptions.map((opt, i) => {
93
+ const optionValue = options[i];
94
+ const isFocused = i === focusedIndex;
100
95
  const isActive = isActiveOption(optionValue);
101
96
  // проверяем, что опция задизейблена
102
- const isDisabled = isOptionDisabled(optionValue);
97
+ const isDisabled = optionsDisableMap[i];
103
98
 
104
99
  return (
105
100
  <ScrollIntoViewIfNeeded
106
- active={index === focusedIndex}
101
+ active={isFocused}
107
102
  options={{ block: 'nearest' }}
108
- key={index}
103
+ key={i}
109
104
  className={clsx(classes.cell, {
110
- [classes.focused]: index === focusedIndex,
105
+ [classes.focused]: isFocused,
111
106
  [classes.active]: isActive,
112
107
  [classes.disabled]: isDisabled,
113
108
  })}
114
- onClick={!isDisabled ? () => onOptionClick(index) : undefined}
109
+ {...addDataAttributes({
110
+ disabled: isDisabled,
111
+ active: isActive,
112
+ focused: isFocused,
113
+ })}
114
+ onClick={!isDisabled ? () => onOptionClick(i) : undefined}
115
115
  >
116
116
  {opt}
117
117
  </ScrollIntoViewIfNeeded>