@true-engineering/true-react-common-ui-kit 4.0.0-alpha30 → 4.0.0-alpha32

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 (37) hide show
  1. package/dist/components/DateInput/DateInput.d.ts +1 -2
  2. package/dist/components/DatePicker/DatePicker.d.ts +3 -2
  3. package/dist/components/DatePicker/components/DatePickerBase/DatePickerBase.d.ts +5 -0
  4. package/dist/components/DatePicker/components/DatePickerBase/index.d.ts +1 -0
  5. package/dist/components/DatePicker/components/index.d.ts +1 -0
  6. package/dist/components/DatePicker/constants.d.ts +7 -2
  7. package/dist/components/DatePicker/helpers.d.ts +0 -3
  8. package/dist/components/DatePicker/index.d.ts +1 -0
  9. package/dist/components/DatePicker/types.d.ts +1 -3
  10. package/dist/components/Input/InputBase.d.ts +1 -1
  11. package/dist/components/RadioButton/RadioButton.styles.d.ts +1 -1
  12. package/dist/components/Select/Select.d.ts +2 -2
  13. package/dist/components/Select/types.d.ts +4 -0
  14. package/dist/hooks/index.d.ts +7 -6
  15. package/dist/hooks/use-latest-ref.d.ts +2 -0
  16. package/dist/hooks/use-on-click-outside.d.ts +2 -2
  17. package/dist/true-react-common-ui-kit.js +384 -259
  18. package/dist/true-react-common-ui-kit.js.map +1 -1
  19. package/dist/true-react-common-ui-kit.umd.cjs +384 -259
  20. package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
  21. package/package.json +1 -1
  22. package/src/components/DateInput/DateInput.tsx +3 -4
  23. package/src/components/DatePicker/DatePicker.tsx +38 -15
  24. package/src/components/DatePicker/components/DatePickerBase/DatePickerBase.tsx +14 -0
  25. package/src/components/DatePicker/components/DatePickerBase/index.ts +1 -0
  26. package/src/components/DatePicker/components/index.ts +1 -0
  27. package/src/components/DatePicker/constants.ts +9 -3
  28. package/src/components/DatePicker/helpers.ts +1 -13
  29. package/src/components/DatePicker/index.ts +1 -0
  30. package/src/components/DatePicker/types.ts +1 -4
  31. package/src/components/Input/InputBase.tsx +1 -1
  32. package/src/components/Select/Select.tsx +5 -4
  33. package/src/components/Select/types.ts +3 -0
  34. package/src/hooks/index.ts +7 -6
  35. package/src/hooks/use-intersection-ref.ts +4 -4
  36. package/src/hooks/use-latest-ref.ts +7 -0
  37. package/src/hooks/use-on-click-outside.ts +22 -14
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-alpha30",
3
+ "version": "4.0.0-alpha32",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
- import { ChangeEvent, forwardRef, MouseEvent } from 'react';
1
+ import { ChangeEvent, forwardRef } from 'react';
2
2
  import clsx from 'clsx';
3
- import { addDataAttributes } from '../../helpers';
3
+ import { addDataAttributes } from '@true-engineering/true-react-platform-helpers';
4
4
  import { useTweakStyles } from '../../hooks';
5
5
  import { ICommonProps } from '../../types';
6
6
  import { IChangeInputEvent, IInputProps, Input } from '../Input';
@@ -18,7 +18,6 @@ export interface IDateInputProps
18
18
  className?: string;
19
19
  /** @default false */
20
20
  isRange?: boolean;
21
- onClick?: (event: MouseEvent<HTMLDivElement>) => void;
22
21
  // react-datepicker ожидает event первым аргументом
23
22
  onChange?: (event: IChangeInputEvent, value: string) => void;
24
23
  }
@@ -71,7 +70,7 @@ export const DateInput = forwardRef<HTMLInputElement, IDateInputProps>(
71
70
  };
72
71
 
73
72
  return (
74
- <div className={clsx(classes.root, className)} onClick={onClick} {...addDataAttributes(data)}>
73
+ <div className={clsx(classes.root, className)} {...addDataAttributes(data)} onClick={onClick}>
75
74
  <Input
76
75
  {...inputProps}
77
76
  ref={ref}
@@ -1,27 +1,37 @@
1
- import { FC, FocusEvent, forwardRef, SyntheticEvent, useEffect, useMemo, useState } from 'react';
2
- import ReactDatePicker from 'react-datepicker';
1
+ import {
2
+ FC,
3
+ FocusEvent,
4
+ SyntheticEvent,
5
+ forwardRef,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import type ReactDatePicker from 'react-datepicker';
3
12
  import 'react-datepicker/dist/react-datepicker.css';
4
13
  import clsx from 'clsx';
5
14
  import { isAfter, isBefore, isValid } from 'date-fns';
6
15
  import {
16
+ addDataAttributes,
7
17
  isEmpty,
8
18
  isNotEmpty,
19
+ isString,
9
20
  isStringNotEmpty,
10
21
  } from '@true-engineering/true-react-platform-helpers';
11
22
  import { offset } from '@floating-ui/react';
12
- import { addDataAttributes } from '../../helpers';
13
- import { useTweakStyles } from '../../hooks';
23
+ import { useMergedRefs, useOnClickOutside, useTweakStyles } from '../../hooks';
14
24
  import { ICommonProps } from '../../types';
15
25
  import { DateInput, EMPTY_DATE_INPUT_VALUE, IDateInputProps } from '../DateInput';
16
- import { DatePickerHeader, PopperContainer } from './components';
17
- import { DatePickerComponent, DEFAULT_DATE_FORMAT } from './constants';
26
+ import { DatePickerBase, DatePickerHeader, PopperContainer } from './components';
18
27
  import {
19
- areDatesEquals,
20
- getDateFormatter,
21
- getDateValueParser,
22
- preparateDatePickerLocale,
23
- } from './helpers';
24
- import { IDatePickerBaseProps, IDatePickerLocale, IRange } from './types';
28
+ DEFAULT_DATE_FORMAT,
29
+ IDatePickerLocale,
30
+ LocalesMap,
31
+ OUTSIDE_CLICK_IGNORE_CLASS,
32
+ } from './constants';
33
+ import { areDatesEquals, getDateFormatter, getDateValueParser } from './helpers';
34
+ import { IDatePickerBaseProps, IRange } from './types';
25
35
  import { IDatePickerStyles, useStyles } from './DatePicker.styles';
26
36
 
27
37
  export interface IDatePickerProps extends IDatePickerBaseProps, ICommonProps<IDatePickerStyles> {
@@ -111,6 +121,10 @@ export const DatePicker = forwardRef<ReactDatePicker, IDatePickerProps>(
111
121
  [dateFormat],
112
122
  );
113
123
 
124
+ const datePickerRef = useRef<DatePickerBase>();
125
+
126
+ const componentRef = useMergedRefs([ref, datePickerRef]);
127
+
114
128
  const [isOpen, setIsOpen] = useState(false);
115
129
 
116
130
  const [dateValue, setDateValue] = useState(formatDate(selectedDate));
@@ -242,13 +256,22 @@ export const DatePicker = forwardRef<ReactDatePicker, IDatePickerProps>(
242
256
  setDateRangeValues(startDate, endDate);
243
257
  }, [selectedDate, startDate, endDate]);
244
258
 
259
+ // Кастомный обработчик клика снаружи, чтобы можно было поставить фокус на Input при открытом календаре.
260
+ // Проблема в том, что класс OUTSIDE_CLICK_IGNORE_CLASS висит контейнере Input'а. А react-datepicker
261
+ // проверяет наличие класса непосредственно на элементе, который вызвал клик, но не на его родителях
262
+ useOnClickOutside(
263
+ () => datePickerRef.current?.calendar?.containerRef?.current,
264
+ (event) => datePickerRef.current?.handleClickOutside(event as MouseEvent),
265
+ OUTSIDE_CLICK_IGNORE_CLASS,
266
+ );
267
+
245
268
  return (
246
269
  <div className={classes.root} {...addDataAttributes(data)}>
247
- <DatePickerComponent
248
- ref={ref}
270
+ <DatePickerBase
271
+ ref={componentRef}
249
272
  minDate={minDate}
250
273
  maxDate={maxDate}
251
- locale={preparateDatePickerLocale(locale)}
274
+ locale={isString(locale) ? LocalesMap[locale] : locale}
252
275
  dateFormat={dateFormat}
253
276
  placeholderText={placeholder}
254
277
  calendarStartDay={calendarStartDay}
@@ -0,0 +1,14 @@
1
+ import ReactDatePicker, { DatePickerProps } from 'react-datepicker';
2
+ import { doNothing } from '@true-engineering/true-react-platform-helpers';
3
+
4
+ export class DatePickerBase extends ReactDatePicker {
5
+ public handleClickOutside;
6
+
7
+ constructor(props: DatePickerProps) {
8
+ super(props);
9
+ this.handleClickOutside = this.handleCalendarClickOutside;
10
+ // Затираем дефолтный обработчик клика снаружи, чтобы обрабатывать его самим
11
+ // (см. использование useOnClickOutside в DatePicker)
12
+ this.handleCalendarClickOutside = doNothing;
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export * from './DatePickerBase';
@@ -1,2 +1,3 @@
1
+ export * from './DatePickerBase';
1
2
  export * from './DatePickerHeader';
2
3
  export * from './PopperContainer';
@@ -1,6 +1,12 @@
1
- import ReactDatePicker from 'react-datepicker';
1
+ import { enUS as enLocale, ru as ruLocale, type Locale } from 'date-fns/locale';
2
2
 
3
3
  export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
4
4
 
5
- export const DatePickerComponent =
6
- (ReactDatePicker as unknown as { default: typeof ReactDatePicker }).default ?? ReactDatePicker;
5
+ export const OUTSIDE_CLICK_IGNORE_CLASS = 'react-datepicker-ignore-onclickoutside';
6
+
7
+ export const LocalesMap = {
8
+ ru: ruLocale,
9
+ en: enLocale,
10
+ } satisfies Record<string, Locale>;
11
+
12
+ export type IDatePickerLocale = keyof typeof LocalesMap | Locale;
@@ -1,12 +1,10 @@
1
- import { parse, format, isSameDay, type Locale } from 'date-fns';
2
- import { ru as ruLocale, enUS as enLocale } from 'date-fns/locale';
1
+ import { format, isSameDay, parse } from 'date-fns';
3
2
  import {
4
3
  isEmpty,
5
4
  isNotEmpty,
6
5
  isStringNotEmpty,
7
6
  } from '@true-engineering/true-react-platform-helpers';
8
7
  import { EMPTY_DATE_INPUT_VALUE } from '../DateInput';
9
- import { IDatePickerLocale } from './types';
10
8
 
11
9
  export const getDateFormatter =
12
10
  (dateFormat: string) =>
@@ -23,13 +21,3 @@ export const getDateValueParser =
23
21
  export const areDatesEquals = (date1?: Date | null, date2?: Date | null): boolean =>
24
22
  (isEmpty(date1) && isEmpty(date2)) ||
25
23
  (isNotEmpty(date1) && isNotEmpty(date2) && isSameDay(date1, date2));
26
-
27
- export const preparateDatePickerLocale = (locale: IDatePickerLocale): Locale => {
28
- if (locale === 'ru') {
29
- return ruLocale;
30
- }
31
- if (locale === 'en') {
32
- return enLocale;
33
- }
34
- return locale;
35
- };
@@ -1,4 +1,5 @@
1
1
  export * from './DatePicker';
2
2
  export * from './types';
3
+ export type { IDatePickerLocale } from './constants';
3
4
  export type { IDatePickerStyles } from './DatePicker.styles';
4
5
  export type { IDatePickerHeaderStyles } from './components';
@@ -1,11 +1,8 @@
1
- import { DatePickerProps } from 'react-datepicker';
2
- import { type Locale } from 'date-fns';
1
+ import { type DatePickerProps } from 'react-datepicker';
3
2
  import { type IDateInputProps } from '../DateInput';
4
3
 
5
4
  export type IRange = [Date | null, Date | null] | null;
6
5
 
7
- export type IDatePickerLocale = 'ru' | 'en' | Locale;
8
-
9
6
  export type IDatePickerBaseProps = Pick<
10
7
  DatePickerProps,
11
8
  | 'startDate'
@@ -28,7 +28,7 @@ import { IInputStyles, useStyles } from './Input.styles';
28
28
 
29
29
  export interface IInputBaseProps
30
30
  extends ICommonProps<IInputStyles>,
31
- Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size'>,
31
+ Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'className'>,
32
32
  Pick<
33
33
  IControlWrapperProps,
34
34
  | 'label'
@@ -17,6 +17,7 @@ import { Portal } from 'react-overlays';
17
17
  import clsx from 'clsx';
18
18
  import { debounce } from 'ts-debounce';
19
19
  import {
20
+ applyAction,
20
21
  createFilter,
21
22
  getArray,
22
23
  getTestId,
@@ -39,14 +40,14 @@ import {
39
40
  defaultIsOptionDisabled,
40
41
  getDefaultConvertToIdFunction,
41
42
  } from './helpers';
42
- import { IChangeSelectEvent, IMultipleSelectValue } from './types';
43
+ import { IChangeSelectEvent, IMultipleSelectValue, ISelectFooter } from './types';
43
44
  import { getInputStyles, ISelectStyles, useStyles } from './Select.styles';
44
45
 
45
46
  export interface ISelectProps<Value>
46
47
  extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type' | 'tweakStyles'>,
47
48
  ICommonProps<ISelectStyles> {
48
49
  header?: ReactNode;
49
- footer?: ReactNode;
50
+ footer?: ISelectFooter<Value>;
50
51
  defaultOptionLabel?: ReactNode;
51
52
  allOptionsLabel?: string;
52
53
  noMatchesLabel?: string;
@@ -194,7 +195,7 @@ export function Select<Value>(
194
195
 
195
196
  const filteredOptions = useMemo(() => {
196
197
  if (optionsMode !== 'search') {
197
- return options;
198
+ return options as Value[];
198
199
  }
199
200
 
200
201
  const filter =
@@ -554,7 +555,7 @@ export function Select<Value>(
554
555
  allOptionsLabel={shouldShowAllOption && allOptionsLabel}
555
556
  areAllOptionsSelected={areAllOptionsSelected}
556
557
  customListHeader={customHeader}
557
- customListFooter={footer}
558
+ customListFooter={applyAction(footer, { filteredOptions })}
558
559
  noMatchesLabel={noMatchesLabel}
559
560
  focusedIndex={focusedListCellIndex}
560
561
  activeValue={value}
@@ -1,6 +1,9 @@
1
1
  import { ChangeEvent, KeyboardEvent } from 'react';
2
+ import { IRenderNode } from '../../types';
2
3
  import { IChangeInputEvent } from '../Input';
3
4
 
4
5
  export type IMultipleSelectValue<Value> = Array<NonNullable<Value>>;
5
6
 
6
7
  export type IChangeSelectEvent = IChangeInputEvent | ChangeEvent<HTMLElement> | KeyboardEvent;
8
+
9
+ export type ISelectFooter<T> = IRenderNode<{ filteredOptions: T[] }>;
@@ -1,9 +1,10 @@
1
+ export * from './use-did-mount-effect';
2
+ export * from './use-dropdown';
3
+ export * from './use-intersection-ref';
1
4
  export * from './use-is-mounted';
5
+ export * from './use-latest-ref';
6
+ export * from './use-merge';
7
+ export * from './use-merged-refs';
8
+ export * from './use-mixed-styles';
2
9
  export * from './use-on-click-outside';
3
- export * from './use-dropdown';
4
10
  export * from './use-tweak-styles';
5
- export * from './use-did-mount-effect';
6
- export * from './use-mixed-styles';
7
- export * from './use-merged-refs';
8
- export * from './use-merge';
9
- export * from './use-intersection-ref';
@@ -1,5 +1,6 @@
1
- import { useRef, useMemo, RefCallback } from 'react';
1
+ import { RefCallback, useMemo } from 'react';
2
2
  import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
3
+ import { useLatestRef } from './use-latest-ref';
3
4
 
4
5
  export interface IInsertionRefOptions {
5
6
  /** @default false */
@@ -9,8 +10,7 @@ export interface IInsertionRefOptions {
9
10
  }
10
11
 
11
12
  export const useIntersectionRef = (options?: IInsertionRefOptions): RefCallback<Element> => {
12
- const optionsRef = useRef(options);
13
- optionsRef.current = options;
13
+ const optionsRef = useLatestRef(options);
14
14
 
15
15
  return useMemo(() => {
16
16
  const observer = new IntersectionObserver(([{ isIntersecting }]) => {
@@ -26,5 +26,5 @@ export const useIntersectionRef = (options?: IInsertionRefOptions): RefCallback<
26
26
  });
27
27
 
28
28
  return (node) => (isNotEmpty(node) ? observer.observe(node) : observer.disconnect());
29
- }, []);
29
+ }, [optionsRef]);
30
30
  };
@@ -0,0 +1,7 @@
1
+ import { MutableRefObject, useRef } from 'react';
2
+
3
+ export const useLatestRef = <T>(value: T): MutableRefObject<T> => {
4
+ const ref = useRef(value);
5
+ ref.current = value;
6
+ return ref;
7
+ };
@@ -1,4 +1,6 @@
1
1
  import { RefObject, useEffect } from 'react';
2
+ import { isEmpty, isFunction, isNotEmpty } from '@true-engineering/true-react-platform-helpers';
3
+ import { useLatestRef } from './use-latest-ref';
2
4
 
3
5
  export const checkElementParentsClassNames = (element: HTMLElement, className: string): boolean => {
4
6
  if (element.classList.contains(className)) {
@@ -33,37 +35,43 @@ export const isElementOneOfParents = (element: HTMLElement, elToSearch: HTMLElem
33
35
  };
34
36
 
35
37
  export function useOnClickOutsideWithRef<Elem extends HTMLElement, IgnoreElem extends HTMLElement>(
36
- ref: RefObject<Elem>,
38
+ refOrGetter: RefObject<Elem | null> | (() => Elem | null | undefined) | undefined,
37
39
  handler: (event: MouseEvent | TouchEvent) => void,
38
40
  ignoreRef?: RefObject<IgnoreElem>,
39
41
  ): void {
40
- useOnClickOutside(ref, handler, undefined, ignoreRef);
42
+ useOnClickOutside(refOrGetter, handler, undefined, ignoreRef);
41
43
  }
42
44
 
43
45
  export function useOnClickOutside<Elem extends HTMLElement, IgnoreElem extends HTMLElement>(
44
- ref: RefObject<Elem>,
46
+ refOrGetter: RefObject<Elem | null> | (() => Elem | null | undefined) | undefined,
45
47
  handler: (event: MouseEvent | TouchEvent) => void,
46
48
  ignoreClassName?: string,
47
49
  ignoreRef?: RefObject<IgnoreElem>,
48
50
  ): void {
51
+ const optionsRef = useLatestRef({ refOrGetter, ignoreRef, ignoreClassName, handler });
52
+
49
53
  useEffect(() => {
50
54
  const listener = (event: MouseEvent | TouchEvent) => {
51
- // Do nothing if clicking ref's element or descendent elements
52
- if (!ref.current || ref.current.contains(event.target as HTMLElement)) {
53
- return;
54
- }
55
+ const options = optionsRef.current;
56
+
57
+ const elem = isFunction(options.refOrGetter)
58
+ ? options.refOrGetter()
59
+ : options.refOrGetter?.current;
60
+ const ignoreElem = options.ignoreRef?.current;
61
+ const target = event.target as HTMLElement;
55
62
 
56
63
  if (
57
- (ignoreClassName !== undefined &&
58
- checkElementParentsClassNames(event.target as HTMLElement, ignoreClassName)) ||
59
- (ignoreRef !== undefined &&
60
- ignoreRef.current !== null &&
61
- isElementOneOfParents(event.target as HTMLElement, ignoreRef.current))
64
+ isEmpty(elem) ||
65
+ // Do nothing if clicking ref's element or descendent elements
66
+ elem.contains(target) ||
67
+ (isNotEmpty(options.ignoreClassName) &&
68
+ checkElementParentsClassNames(target, options.ignoreClassName)) ||
69
+ (isNotEmpty(ignoreElem) && isElementOneOfParents(target, ignoreElem))
62
70
  ) {
63
71
  return;
64
72
  }
65
73
 
66
- handler(event);
74
+ options.handler(event);
67
75
  };
68
76
 
69
77
  document.addEventListener('mousedown', listener);
@@ -73,5 +81,5 @@ export function useOnClickOutside<Elem extends HTMLElement, IgnoreElem extends H
73
81
  document.removeEventListener('mousedown', listener);
74
82
  document.removeEventListener('touchstart', listener);
75
83
  };
76
- }, [ref, handler, ignoreClassName]);
84
+ }, [optionsRef]);
77
85
  }