@via-profit/ui-kit 0.2.4 → 0.2.6

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.
@@ -2,7 +2,8 @@ import React from 'react';
2
2
  import { TextFieldProps } from '../TextField';
3
3
  import { PositionStrategy } from '../Popper';
4
4
  import { AnchorPos, GetOptionSelected, MenuItemProps, OnRequestClose, Value } from '../Menu';
5
- export interface AutocompleteProps<T, Multiple extends boolean | undefined = undefined> extends Omit<TextFieldProps, 'value' | 'onChange' | 'children' | 'overrides'> {
5
+ export type AutocompleteTextFieldProps = Omit<TextFieldProps, 'value' | 'onChange' | 'children' | 'overrides'>;
6
+ export interface AutocompleteProps<T, Multiple extends boolean | undefined = undefined> extends AutocompleteTextFieldProps {
6
7
  readonly items: readonly T[];
7
8
  readonly value: Value<T, Multiple>;
8
9
  /**
@@ -217,9 +217,11 @@ export type CalendarBadge = {
217
217
  };
218
218
  export type CalendarRef<IsRangeValue extends boolean | undefined = undefined> = {
219
219
  readonly setView: (view: CalendarView) => void;
220
+ readonly setViews: (views: readonly CalendarView[]) => void;
220
221
  readonly getActiveView: () => CalendarView;
221
222
  readonly setValue: (value: CalendarValue<IsRangeValue>) => void;
222
223
  readonly setCalendarDate: (date: Date) => void;
224
+ readonly getCalendarDate: () => Date;
223
225
  readonly reset: () => void;
224
226
  };
225
227
  declare const CalendarComponent: React.ForwardRefExoticComponent<CalendarProps<boolean | undefined> & React.RefAttributes<CalendarRef<boolean | undefined>>>;
@@ -99,20 +99,38 @@ const computeView = (inputParams) => {
99
99
  const computeCalendarDate = (params) => {
100
100
  const { inputValue, defaultValue } = params;
101
101
  const today = new Date();
102
- if (inputValue) {
103
- if (isRangeValue(inputValue) && inputValue[1]) {
104
- return inputValue[1];
102
+ // Проверяем inputValue
103
+ if (inputValue !== null && inputValue !== undefined) {
104
+ if (Array.isArray(inputValue) && inputValue.length > 0) {
105
+ // Для range режима - берем первый элемент, если он есть
106
+ if (inputValue[0] instanceof Date) {
107
+ return inputValue[0];
108
+ }
109
+ // Если первый элемент null, пробуем второй
110
+ if (inputValue[1] instanceof Date) {
111
+ return inputValue[1];
112
+ }
105
113
  }
106
- if (isRangeValue(inputValue) && inputValue[0]) {
107
- return inputValue[0];
114
+ else if (inputValue instanceof Date) {
115
+ // Для non-range режима
116
+ return inputValue;
108
117
  }
109
118
  }
110
- if (isRangeValue(defaultValue) && defaultValue && defaultValue[1]) {
111
- return defaultValue[1];
112
- }
113
- if (isRangeValue(defaultValue) && defaultValue && defaultValue[0]) {
114
- return defaultValue[0];
119
+ // Проверяем defaultValue
120
+ if (defaultValue !== null && defaultValue !== undefined) {
121
+ if (Array.isArray(defaultValue) && defaultValue.length > 0) {
122
+ if (defaultValue[0] instanceof Date) {
123
+ return defaultValue[0];
124
+ }
125
+ if (defaultValue[1] instanceof Date) {
126
+ return defaultValue[1];
127
+ }
128
+ }
129
+ else if (defaultValue instanceof Date) {
130
+ return defaultValue;
131
+ }
115
132
  }
133
+ // Возвращаем сегодняшнюю дату, если ничего не подошло
116
134
  return today;
117
135
  };
118
136
  const CalendarComponent = react_1.default.forwardRef((props, ref) => {
@@ -199,15 +217,19 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
199
217
  /**
200
218
  * List of possibility views
201
219
  */
202
- const [views] = react_1.default.useState(() => computeViews(inputViews, range));
220
+ const [views, setComputedViews] = react_1.default.useState(() => computeViews(inputViews, range));
221
+ const setViews = react_1.default.useCallback((variants) => setComputedViews(variants), []);
203
222
  const selectView = react_1.default.useCallback((view) => {
204
223
  var _a;
224
+ if (!views.includes(view)) {
225
+ setViews([...views, view]);
226
+ }
205
227
  setView(view);
206
228
  const index = views.findIndex(v => v === view);
207
229
  if (index > -1) {
208
230
  (_a = swiperRef.current) === null || _a === void 0 ? void 0 : _a.goToIndex(index);
209
231
  }
210
- }, [views]);
232
+ }, [setViews, views]);
211
233
  const resetVariablesRef = react_1.default.useRef({
212
234
  calendarDate,
213
235
  value,
@@ -248,11 +270,13 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
248
270
  if (JSON.stringify(inputValue) !== JSON.stringify(inputValueRef.current)) {
249
271
  inputValueRef.current = inputValue;
250
272
  setValue(inputValue);
251
- if (isRangeValue(inputValue) && inputValue && inputValue[0] instanceof Date) {
252
- setCalendarDate(inputValue[0]);
253
- }
254
- else {
255
- setCalendarDate(inputValue);
273
+ if (inputValue !== null && inputValue !== undefined) {
274
+ if (Array.isArray(inputValue) && inputValue[0] instanceof Date) {
275
+ setCalendarDate(inputValue[0]);
276
+ }
277
+ else if (inputValue instanceof Date) {
278
+ setCalendarDate(inputValue);
279
+ }
256
280
  }
257
281
  }
258
282
  }, [inputValue]);
@@ -503,6 +527,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
503
527
  ]);
504
528
  const initialIndex = react_1.default.useMemo(() => views.findIndex(v => v === view), [views, view]);
505
529
  const getActiveView = react_1.default.useCallback(() => view, [view]);
530
+ const getCalendarDate = react_1.default.useCallback(() => calendarDate, [calendarDate]);
506
531
  /**
507
532
  * API
508
533
  */
@@ -510,9 +535,11 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
510
535
  setView: selectView,
511
536
  reset: handleReset,
512
537
  setValue,
538
+ getCalendarDate,
513
539
  setCalendarDate,
514
540
  getActiveView,
515
- }), [selectView, handleReset, getActiveView]);
541
+ setViews,
542
+ }), [selectView, handleReset, getActiveView, setViews, getCalendarDate]);
516
543
  return (react_1.default.createElement(overridesMap.Paper, null,
517
544
  react_1.default.createElement(overridesMap.Header, null,
518
545
  typeof heading !== 'undefined' && react_1.default.createElement(overridesMap.Heading, null, heading),
@@ -1,4 +1,4 @@
1
- export type CalendarValue<Range> = Range extends undefined ? Date | null : [Date, Date] | [Date | null] | null;
1
+ export type CalendarValue<Range> = Range extends undefined ? Date | null : [Date, Date] | [Date, Date | null] | null;
2
2
  export type WeekDayName = 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday';
3
3
  export type Day = {
4
4
  readonly date: Date;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@via-profit/ui-kit",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "UI Kit",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,18 +0,0 @@
1
- import React from 'react';
2
- import { TextFieldProps } from '../TextField';
3
- import { AnchorPos } from '../Menu';
4
- export interface AutocompleteTextFieldProps extends TextFieldProps {
5
- /**
6
- * Text field loading state\
7
- * If `true` then text field has been contained the loading indicator, otherwise - nop
8
- */
9
- readonly isLoading?: boolean;
10
- readonly anchorPos?: AnchorPos;
11
- readonly isOpen?: boolean;
12
- /**
13
- * Native <input> element reference
14
- */
15
- readonly inputRef?: React.MutableRefObject<HTMLInputElement | null> | React.RefCallback<HTMLInputElement>;
16
- }
17
- declare const _default: React.ForwardRefExoticComponent<AutocompleteTextFieldProps & React.RefAttributes<HTMLDivElement>>;
18
- export default _default;
@@ -1,71 +0,0 @@
1
- "use strict";
2
- var __rest = (this && this.__rest) || function (s, e) {
3
- var t = {};
4
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
- t[p] = s[p];
6
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
- t[p[i]] = s[p[i]];
10
- }
11
- return t;
12
- };
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- const react_1 = __importDefault(require("react"));
18
- const styled_1 = __importDefault(require("@emotion/styled"));
19
- const react_2 = require("@emotion/react");
20
- const TextField_1 = __importDefault(require("../TextField"));
21
- const TextFieldInputWrapper_1 = __importDefault(require("../TextField/TextFieldInputWrapper"));
22
- const StyledTextFieldInputWrapper = (0, styled_1.default)(TextFieldInputWrapper_1.default) `
23
- ${({ theme, $isOpen, $anchorPos }) => $isOpen &&
24
- $anchorPos &&
25
- ['bottom-fill', 'top-fill'].includes($anchorPos) &&
26
- (0, react_2.css) `
27
- border-width: 1px;
28
- border-style: solid;
29
- border-color: ${theme.isDark
30
- ? theme.color.textPrimary.darken(100).toString()
31
- : theme.color.textPrimary.lighten(150).toString()};
32
- `};
33
-
34
- ${({ focused, $anchorPos, $isOpen }) => $isOpen &&
35
- $anchorPos &&
36
- ['bottom-fill', 'top-fill'].includes($anchorPos) &&
37
- focused &&
38
- (0, react_2.css) `
39
- outline: none;
40
- `};
41
-
42
- ${({ $anchorPos, $isOpen }) => $isOpen &&
43
- $anchorPos === 'bottom-fill' &&
44
- (0, react_2.css) `
45
- border-bottom-left-radius: 0;
46
- border-bottom-right-radius: 0;
47
- border-bottom: 0;
48
- `};
49
-
50
- ${({ $anchorPos, $isOpen }) => $isOpen &&
51
- $anchorPos === 'top-fill' &&
52
- (0, react_2.css) `
53
- border-top-left-radius: 0;
54
- border-top-right-radius: 0;
55
- border-top: 0;
56
- `};
57
- `;
58
- const WrapperContext = react_1.default.createContext({ anchorPos: undefined, isOpen: false });
59
- const InputWrapper = react_1.default.forwardRef(function StyledInputWrapper(props, ref) {
60
- const ctx = react_1.default.useContext(WrapperContext);
61
- const { anchorPos, isOpen } = ctx;
62
- return (react_1.default.createElement(StyledTextFieldInputWrapper, Object.assign({ "$anchorPos": anchorPos, "$isOpen": isOpen, ref: ref }, props)));
63
- });
64
- const AutocompleteTextField = (props, ref) => {
65
- const { anchorPos, isOpen } = props, restProps = __rest(props, ["anchorPos", "isOpen"]);
66
- return (react_1.default.createElement(WrapperContext.Provider, { value: { anchorPos, isOpen } },
67
- react_1.default.createElement(TextField_1.default, Object.assign({ overrides: {
68
- InputWrapper,
69
- } }, restProps, { ref: ref }))));
70
- };
71
- exports.default = react_1.default.forwardRef(AutocompleteTextField);
@@ -1,5 +0,0 @@
1
- export declare const splitTextIntoParts: (text: string, regex: RegExp, patterns: string[]) => {
2
- text: string;
3
- highlighted: boolean;
4
- }[];
5
- export default splitTextIntoParts;
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.splitTextIntoParts = void 0;
4
- const MAX_MATCHES = 1000;
5
- const splitTextIntoParts = (text, regex, patterns) => {
6
- if (patterns.length === 0) {
7
- return [{ text, highlighted: false }];
8
- }
9
- const parts = [];
10
- let lastIndex = 0;
11
- let match;
12
- let matchCount = 0;
13
- const cleanRegex = new RegExp(regex.source, regex.flags);
14
- while ((match = cleanRegex.exec(text)) !== null) {
15
- matchCount++;
16
- if (matchCount > MAX_MATCHES) {
17
- console.warn(`splitTextIntoParts: Too many matches (${matchCount}), stopping at ${MAX_MATCHES} matches`);
18
- break;
19
- }
20
- const matchStart = match.index;
21
- const matchEnd = matchStart + match[0].length;
22
- if (matchStart > lastIndex) {
23
- parts.push({
24
- text: text.substring(lastIndex, matchStart),
25
- highlighted: false,
26
- });
27
- }
28
- parts.push({
29
- text: match[0],
30
- highlighted: true,
31
- });
32
- lastIndex = matchEnd;
33
- if (match[0].length === 0) {
34
- break;
35
- }
36
- }
37
- if (lastIndex < text.length) {
38
- parts.push({
39
- text: text.substring(lastIndex),
40
- highlighted: false,
41
- });
42
- }
43
- return parts;
44
- };
45
- exports.splitTextIntoParts = splitTextIntoParts;
46
- exports.default = exports.splitTextIntoParts;
@@ -1,110 +0,0 @@
1
- import React from 'react';
2
- import { PositionStrategy } from './PopperContainer';
3
- /**
4
- * Base directions for popper positioning.
5
- * These are the primary axes around which the popper can be placed.
6
- *
7
- * @default 'bottom' (when used as anchorPos)
8
- * @example
9
- * ```tsx
10
- * type Direction = 'top' | 'bottom' | 'left' | 'right';
11
- * ```
12
- */
13
- export type Direction = 'top' | 'left' | 'right' | 'bottom';
14
- /**
15
- * Auto placement modifiers that enable intelligent positioning.
16
- * The popper will automatically choose the best placement that fits in the viewport.
17
- * - `auto`: Dynamically selects the optimal direction
18
- * - `auto-top`, `auto-bottom`, `auto-left`, `auto-right`: Auto placement with priority given to the specified direction
19
- *
20
- * @example
21
- * ```tsx
22
- * // Will try to place on top first, but will flip if needed
23
- * <Popper anchorPos="auto-top">...</Popper>
24
- *
25
- * // Will try all directions equally
26
- * <Popper anchorPos="auto">...</Popper>
27
- * ```
28
- */
29
- export type AutoModifier = 'auto' | 'auto-top' | 'auto-bottom' | 'auto-left' | 'auto-right';
30
- /**
31
- * Precise placement modifiers for fine-tuned positioning.
32
- * These combine a base direction with specific alignment options:
33
- * - `start`/`end`: Aligns to the beginning or end of the anchor (useful for RTL layouts)
34
- * - `left`/`right`: Horizontal alignment for vertical placements
35
- * - `top`/`bottom`: Vertical alignment for horizontal placements
36
- *
37
- * @example
38
- * ```tsx
39
- * // Corner placements
40
- * <Popper anchorPos="top-left">Aligns to top-left corner</Popper>
41
- *
42
- * // Logical placements (RTL-friendly)
43
- * <Popper anchorPos="bottom-start">Aligns to bottom edge, start side</Popper>
44
- *
45
- * // Side alignments
46
- * <Popper anchorPos="left-top">Aligns to left side, top edge</Popper>
47
- * ```
48
- */
49
- export type Modifier = 'top-start' | 'top-end' | 'top-left' | 'top-right' | 'bottom-end' | 'bottom-start' | 'bottom-right' | 'bottom-left' | 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom' | 'bottom-fill' | 'top-fill';
50
- /**
51
- * Available placement options for the popper.
52
- * Combines basic directions, precise modifiers, and auto-placement options
53
- * to provide full control over popper positioning.
54
- *
55
- * Categories:
56
- * - **Basic directions**: `top`, `bottom`, `left`, `right` - Centered placement
57
- * - **Corner placements**: `top-left`, `top-right`, `bottom-left`, `bottom-right` - Aligned to corners
58
- * - **Logical placements**: `top-start`, `top-end`, `bottom-start`, `bottom-end` - RTL-aware alignment
59
- * - **Side alignments**: `left-top`, `left-bottom`, `right-top`, `right-bottom` - Edge-aligned
60
- * - **Auto placements**: `auto`, `auto-top`, `auto-bottom`, `auto-left`, `auto-right` - Intelligent positioning
61
- *
62
- * @default 'bottom'
63
- * @example
64
- * ```tsx
65
- * // Basic centered placement
66
- * <Popper anchorPos="bottom">Centered below anchor</Popper>
67
- *
68
- * // Corner placement
69
- * <Popper anchorPos="top-right">Attached to top-right corner</Popper>
70
- *
71
- * // Auto placement with priority
72
- * <Popper anchorPos="auto-left">Try left first, then find best fit</Popper>
73
- *
74
- * // RTL-aware placement
75
- * <Popper anchorPos="top-start">Respects text direction</Popper>
76
- * ```
77
- */
78
- export type AnchorPos = Direction | Modifier | AutoModifier;
79
- export interface UsePopperProps {
80
- readonly anchorElement: HTMLElement | null;
81
- readonly anchorPos?: AnchorPos;
82
- readonly positionStrategy?: PositionStrategy;
83
- readonly autoFlip?: boolean;
84
- readonly offset?: number;
85
- readonly viewportMargin?: number;
86
- readonly isOpen?: boolean;
87
- }
88
- export interface UsePopperResult {
89
- readonly actualPlacement: AnchorPos;
90
- readonly style: React.CSSProperties | null;
91
- readonly popperRef: React.MutableRefObject<HTMLDivElement | null>;
92
- readonly isVisible: boolean;
93
- readonly calculatePosition: () => void;
94
- readonly scrollableAncestor: HTMLElement | Window | null;
95
- readonly setActualPlacement: React.Dispatch<React.SetStateAction<AnchorPos>>;
96
- readonly setStyle: React.Dispatch<React.SetStateAction<React.CSSProperties | null>>;
97
- readonly setIsVisible: React.Dispatch<React.SetStateAction<boolean>>;
98
- }
99
- /**
100
- * Finds the closest scrollable ancestor of an element.
101
- * A scrollable element is one that has overflow set to auto, scroll, or overlay
102
- * and has content that overflows, allowing scrolling.
103
- *
104
- * @param element - The element to start searching from
105
- * @param includeHidden - Whether to consider elements with overflow: hidden
106
- * @returns The closest scrollable ancestor or window if none found
107
- */
108
- export declare const findScrollableAncestor: (element: HTMLElement | null, includeHidden?: boolean) => HTMLElement | Window | null;
109
- export declare const usePopper: ({ anchorElement, anchorPos, positionStrategy, autoFlip, offset, viewportMargin, isOpen, }: UsePopperProps) => UsePopperResult;
110
- export default usePopper;
@@ -1,505 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.usePopper = exports.findScrollableAncestor = void 0;
7
- const react_1 = __importDefault(require("react"));
8
- /**
9
- * Finds the closest scrollable ancestor of an element.
10
- * A scrollable element is one that has overflow set to auto, scroll, or overlay
11
- * and has content that overflows, allowing scrolling.
12
- *
13
- * @param element - The element to start searching from
14
- * @param includeHidden - Whether to consider elements with overflow: hidden
15
- * @returns The closest scrollable ancestor or window if none found
16
- */
17
- const findScrollableAncestor = (element, includeHidden = false) => {
18
- if (!element) {
19
- return null;
20
- }
21
- // Helper to check if an element is scrollable
22
- const isScrollable = (el) => {
23
- const style = window.getComputedStyle(el);
24
- const overflow = style.overflow + style.overflowY + style.overflowX;
25
- // Check if element has overflow that enables scrolling
26
- const hasOverflow = overflow.includes('auto') ||
27
- overflow.includes('scroll') ||
28
- (includeHidden && overflow.includes('hidden'));
29
- if (!hasOverflow) {
30
- return false;
31
- }
32
- // Check if element actually has scrollable content
33
- return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
34
- };
35
- // Start from parent (skip the element itself)
36
- let parent = element.parentElement;
37
- while (parent) {
38
- if (isScrollable(parent)) {
39
- return parent;
40
- }
41
- parent = parent.parentElement;
42
- }
43
- // If no scrollable ancestor found, return window (which is scrollable)
44
- return window;
45
- };
46
- exports.findScrollableAncestor = findScrollableAncestor;
47
- const usePopper = ({ anchorElement, anchorPos = 'auto', positionStrategy = 'fixed', autoFlip = true, offset = 0, viewportMargin = 10, isOpen = false, }) => {
48
- const [actualPlacement, setActualPlacement] = react_1.default.useState(anchorPos);
49
- const [style, setStyle] = react_1.default.useState(null);
50
- const [isVisible, setIsVisible] = react_1.default.useState(false);
51
- const popperRef = react_1.default.useRef(null);
52
- // Finding the nearest scrollable ancestor
53
- const scrollableAncestor = react_1.default.useMemo(() => {
54
- if (!anchorElement || typeof window === 'undefined') {
55
- return null;
56
- }
57
- return (0, exports.findScrollableAncestor)(anchorElement);
58
- }, [anchorElement]);
59
- // Synchronizing actualPlacement with anchorPos
60
- react_1.default.useEffect(() => {
61
- if (actualPlacement !== anchorPos) {
62
- setActualPlacement(anchorPos);
63
- }
64
- }, [actualPlacement, anchorPos]);
65
- const getPositionStyle = react_1.default.useCallback((props) => {
66
- const { place, anchorRect, popperRect, forViewportCheck = false } = props;
67
- const [direction, modifier = ''] = place.split('-');
68
- let left = anchorRect.left;
69
- let top = anchorRect.top;
70
- let width = undefined;
71
- // Calculating the position relative to the viewport
72
- switch (direction) {
73
- case 'top':
74
- top = anchorRect.top - popperRect.height - offset;
75
- if (modifier === 'fill') {
76
- left = anchorRect.left;
77
- width = anchorRect.width;
78
- }
79
- else if (modifier === 'left' || modifier === 'start') {
80
- left = anchorRect.left;
81
- }
82
- else if (modifier === 'right' || modifier === 'end') {
83
- left = anchorRect.right - popperRect.width;
84
- }
85
- else {
86
- left = anchorRect.left + (anchorRect.width - popperRect.width) / 2;
87
- }
88
- break;
89
- case 'bottom':
90
- top = anchorRect.bottom + offset;
91
- if (modifier === 'fill') {
92
- left = anchorRect.left;
93
- width = anchorRect.width;
94
- }
95
- else if (modifier === 'left' || modifier === 'start') {
96
- left = anchorRect.left;
97
- }
98
- else if (modifier === 'right' || modifier === 'end') {
99
- left = anchorRect.right - popperRect.width;
100
- }
101
- else {
102
- left = anchorRect.left + (anchorRect.width - popperRect.width) / 2;
103
- }
104
- break;
105
- case 'left':
106
- left = anchorRect.left - popperRect.width - offset;
107
- if (modifier === 'top' || modifier === 'start') {
108
- top = anchorRect.top;
109
- }
110
- else if (modifier === 'bottom' || modifier === 'end') {
111
- top = anchorRect.bottom - popperRect.height;
112
- }
113
- else {
114
- top = anchorRect.top + (anchorRect.height - popperRect.height) / 2;
115
- }
116
- break;
117
- case 'right':
118
- left = anchorRect.right + offset;
119
- if (modifier === 'top' || modifier === 'start') {
120
- top = anchorRect.top;
121
- }
122
- else if (modifier === 'bottom' || modifier === 'end') {
123
- top = anchorRect.bottom - popperRect.height;
124
- }
125
- else {
126
- top = anchorRect.top + (anchorRect.height - popperRect.height) / 2;
127
- }
128
- break;
129
- default:
130
- left = anchorRect.left + (anchorRect.width - popperRect.width) / 2;
131
- top = anchorRect.bottom + offset;
132
- }
133
- // Returning the viewport coordinates for verification or the final coordinates
134
- if (forViewportCheck) {
135
- return { left, top, width };
136
- }
137
- // For the final positioning, we take into account the strategy
138
- if (positionStrategy === 'absolute') {
139
- return {
140
- left: left + window.scrollX,
141
- top: top + window.scrollY,
142
- width,
143
- };
144
- }
145
- return { left, top, width };
146
- }, [offset, positionStrategy]);
147
- // const clampPopperToContainer = React.useCallback(
148
- // (style: { left: number; top: number; width?: number }, _popperRect: DOMRect) => style,
149
- // [],
150
- // );
151
- const clampPopperToContainer = react_1.default.useCallback((style, popperRect) => {
152
- // Если нет scrollableAncestor, используем document.documentElement как fallback
153
- const container = scrollableAncestor instanceof HTMLElement ? scrollableAncestor : document.documentElement;
154
- const containerRect = container.getBoundingClientRect();
155
- // Базовые границы контейнера с учетом отступа
156
- let minLeft = containerRect.left + viewportMargin;
157
- let maxLeft = containerRect.right - popperRect.width - viewportMargin;
158
- let minTop = containerRect.top + viewportMargin;
159
- let maxTop = containerRect.bottom - popperRect.height - viewportMargin;
160
- // Для fixed позиционирования добавляем ограничения viewport
161
- if (positionStrategy === 'fixed') {
162
- // Границы viewport с учетом отступа
163
- const viewportMinLeft = viewportMargin;
164
- const viewportMaxLeft = window.innerWidth - popperRect.width - viewportMargin;
165
- const viewportMinTop = viewportMargin;
166
- const viewportMaxTop = window.innerHeight - popperRect.height - viewportMargin;
167
- // Берем пересечение границ контейнера и viewport
168
- minLeft = Math.max(minLeft, viewportMinLeft);
169
- maxLeft = Math.min(maxLeft, viewportMaxLeft);
170
- minTop = Math.max(minTop, viewportMinTop);
171
- maxTop = Math.min(maxTop, viewportMaxTop);
172
- }
173
- // Убеждаемся, что границы валидны (min не больше max)
174
- const validMinLeft = Math.min(minLeft, maxLeft);
175
- const validMaxLeft = Math.max(minLeft, maxLeft);
176
- const validMinTop = Math.min(minTop, maxTop);
177
- const validMaxTop = Math.max(minTop, maxTop);
178
- // Ограничиваем позицию
179
- const clampedLeft = Math.max(validMinLeft, Math.min(style.left, validMaxLeft));
180
- const clampedTop = Math.max(validMinTop, Math.min(style.top, validMaxTop));
181
- return {
182
- left: clampedLeft,
183
- top: clampedTop,
184
- width: style.width,
185
- };
186
- }, [positionStrategy, scrollableAncestor, viewportMargin]);
187
- const checkIfViewportFits = react_1.default.useCallback((style, popperRect) => {
188
- const left = style.left;
189
- const top = style.top;
190
- const right = left + popperRect.width;
191
- const bottom = top + popperRect.height;
192
- if (positionStrategy === 'absolute' && scrollableAncestor instanceof HTMLElement) {
193
- const containerRect = scrollableAncestor.getBoundingClientRect();
194
- const relativeLeft = left - containerRect.left;
195
- const relativeTop = top - containerRect.top;
196
- const relativeRight = relativeLeft + popperRect.width;
197
- const relativeBottom = relativeTop + popperRect.height;
198
- const isWithinHorizontal = relativeLeft >= viewportMargin && relativeRight <= containerRect.width - viewportMargin;
199
- const isWithinVertical = relativeTop >= viewportMargin && relativeBottom <= containerRect.height - viewportMargin;
200
- return isWithinHorizontal && isWithinVertical;
201
- }
202
- const viewportWidth = window.innerWidth;
203
- const viewportHeight = window.innerHeight;
204
- const isWithinHorizontal = left >= viewportMargin && right <= viewportWidth - viewportMargin;
205
- const isWithinVertical = top >= viewportMargin && bottom <= viewportHeight - viewportMargin;
206
- const hasValidPosition = left >= 0 && top >= 0;
207
- return hasValidPosition && isWithinHorizontal && isWithinVertical;
208
- }, [viewportMargin, scrollableAncestor, positionStrategy]);
209
- const placementsOrder = react_1.default.useMemo(() => {
210
- const basePlacements = {
211
- top: ['top', 'top-left', 'top-right', 'top-start', 'top-end'],
212
- bottom: ['bottom', 'bottom-left', 'bottom-right', 'bottom-start', 'bottom-end'],
213
- left: ['left', 'left-top', 'left-bottom'],
214
- right: ['right', 'right-top', 'right-bottom'],
215
- };
216
- const allPlacements = [
217
- ...basePlacements.top,
218
- ...basePlacements.bottom,
219
- ...basePlacements.left,
220
- ...basePlacements.right,
221
- ];
222
- const order = {};
223
- allPlacements.forEach(placement => {
224
- const [direction] = placement.split('-');
225
- // Priority: same direction > opposite > the rest
226
- const opposite = direction === 'top'
227
- ? 'bottom'
228
- : direction === 'bottom'
229
- ? 'top'
230
- : direction === 'left'
231
- ? 'right'
232
- : 'left';
233
- order[placement] = [
234
- placement,
235
- ...basePlacements[direction].filter(p => p !== placement),
236
- ...(basePlacements[opposite] || []),
237
- ...(direction !== 'left' && direction !== 'right' ? basePlacements.left : []),
238
- ...(direction !== 'left' && direction !== 'right' ? basePlacements.right : []),
239
- ];
240
- });
241
- // console.log(order);
242
- return order;
243
- }, []);
244
- const getPreferredPlacements = react_1.default.useCallback((preferredPlacement, anchorRect, popperRect) => {
245
- // Check the auto placements
246
- if (preferredPlacement.startsWith('auto')) {
247
- const [, preferredDirection = 'bottom'] = preferredPlacement.split('-');
248
- const allPlacements = [
249
- 'bottom',
250
- 'bottom-start',
251
- 'bottom-end',
252
- 'bottom-left',
253
- 'bottom-right',
254
- 'top',
255
- 'top-left',
256
- 'top-right',
257
- 'top-start',
258
- 'top-end',
259
- 'left',
260
- 'left-top',
261
- 'left-bottom',
262
- 'right',
263
- 'right-top',
264
- 'right-bottom',
265
- ];
266
- // Sorting placements
267
- const sortedPlacements = [...allPlacements].sort((a, b) => {
268
- const aDir = a.split('-')[0];
269
- const bDir = b.split('-')[0];
270
- if (aDir === preferredDirection && bDir !== preferredDirection)
271
- return -1;
272
- if (aDir !== preferredDirection && bDir === preferredDirection)
273
- return 1;
274
- return 0;
275
- });
276
- for (const place of sortedPlacements) {
277
- const viewportStyle = getPositionStyle({
278
- place,
279
- anchorRect,
280
- popperRect,
281
- forViewportCheck: true,
282
- });
283
- const fits = checkIfViewportFits(viewportStyle, popperRect);
284
- if (fits) {
285
- const finalStyle = getPositionStyle({
286
- place,
287
- anchorRect,
288
- popperRect,
289
- forViewportCheck: false,
290
- });
291
- return {
292
- found: true,
293
- placement: place,
294
- style: clampPopperToContainer(finalStyle, popperRect),
295
- };
296
- }
297
- }
298
- }
299
- if (!autoFlip) {
300
- const finalStyle = getPositionStyle({
301
- place: preferredPlacement,
302
- anchorRect,
303
- popperRect,
304
- forViewportCheck: false,
305
- });
306
- return {
307
- found: false,
308
- placement: preferredPlacement,
309
- style: clampPopperToContainer(finalStyle, popperRect),
310
- };
311
- }
312
- /**
313
- * bottom-fill, top-fill
314
- */
315
- if (preferredPlacement.endsWith('fill')) {
316
- const [preferredDirection = 'bottom'] = preferredPlacement.split('-');
317
- const allPlacements = ['top-fill', 'bottom-fill'];
318
- const sortedPlacements = [...allPlacements].sort((a, b) => {
319
- const aDir = a.split('-')[0];
320
- const bDir = b.split('-')[0];
321
- if (aDir === preferredDirection && bDir !== preferredDirection)
322
- return -1;
323
- if (aDir !== preferredDirection && bDir === preferredDirection)
324
- return 1;
325
- return 0;
326
- });
327
- for (const place of sortedPlacements) {
328
- const viewportStyle = getPositionStyle({
329
- place,
330
- anchorRect,
331
- popperRect,
332
- forViewportCheck: true,
333
- });
334
- const fits = checkIfViewportFits(viewportStyle, popperRect);
335
- if (fits) {
336
- const finalStyle = getPositionStyle({
337
- place,
338
- anchorRect,
339
- popperRect,
340
- forViewportCheck: false,
341
- });
342
- return {
343
- found: true,
344
- placement: place,
345
- style: clampPopperToContainer(finalStyle, popperRect),
346
- };
347
- }
348
- }
349
- }
350
- /**
351
- * Other
352
- */
353
- const order = placementsOrder[preferredPlacement] || placementsOrder.bottom;
354
- for (const place of order) {
355
- const viewportStyle = getPositionStyle({
356
- place,
357
- anchorRect,
358
- popperRect,
359
- forViewportCheck: true,
360
- });
361
- const fits = checkIfViewportFits(viewportStyle, popperRect);
362
- if (fits) {
363
- const finalStyle = getPositionStyle({
364
- place,
365
- anchorRect,
366
- popperRect,
367
- forViewportCheck: false,
368
- });
369
- return {
370
- found: true,
371
- placement: place,
372
- style: clampPopperToContainer(finalStyle, popperRect),
373
- };
374
- }
375
- }
376
- const finalStyle = getPositionStyle({
377
- place: preferredPlacement,
378
- anchorRect,
379
- popperRect,
380
- forViewportCheck: false,
381
- });
382
- return {
383
- found: false,
384
- placement: preferredPlacement,
385
- style: clampPopperToContainer(finalStyle, popperRect),
386
- };
387
- }, [autoFlip, getPositionStyle, checkIfViewportFits, clampPopperToContainer, placementsOrder]);
388
- const calculatePosition = react_1.default.useCallback(() => {
389
- if (!anchorElement || !popperRef.current) {
390
- return;
391
- }
392
- const anchorRect = anchorElement.getBoundingClientRect();
393
- const popperRect = popperRef.current.getBoundingClientRect();
394
- const { style, placement, found } = getPreferredPlacements(anchorPos, anchorRect, popperRect);
395
- if (found) {
396
- setActualPlacement(placement);
397
- }
398
- setStyle(style);
399
- }, [anchorElement, anchorPos, getPreferredPlacements]);
400
- // const getTransformOrigin = React.useCallback((placement: AnchorPos): string => {
401
- // if (placement.startsWith('top')) return 'bottom center';
402
- // if (placement.startsWith('bottom')) return 'top center';
403
- // if (placement.startsWith('left')) return 'right center';
404
- // if (placement.startsWith('right')) return 'left center';
405
- //
406
- // return 'top center';
407
- // }, []);
408
- /**
409
- * Open / Close popper logic
410
- */
411
- react_1.default.useEffect(() => {
412
- if (!isOpen) {
413
- setIsVisible(false);
414
- setStyle(null);
415
- }
416
- else if (anchorElement && isOpen && popperRef.current) {
417
- calculatePosition();
418
- setIsVisible(true);
419
- }
420
- }, [actualPlacement, anchorElement, getPreferredPlacements, isOpen, calculatePosition]);
421
- /**
422
- * The anchor element changes
423
- */
424
- react_1.default.useEffect(() => {
425
- if (!isOpen || !anchorElement) {
426
- return;
427
- }
428
- const observer = new MutationObserver(() => {
429
- window.requestAnimationFrame(() => {
430
- // console.log('The anchor element was change');
431
- calculatePosition();
432
- });
433
- });
434
- observer.observe(anchorElement, {
435
- attributes: true,
436
- childList: true,
437
- subtree: true,
438
- });
439
- // eslint-disable-next-line consistent-return
440
- return () => {
441
- observer.disconnect();
442
- };
443
- }, [isOpen, anchorElement, calculatePosition]);
444
- /**
445
- * On scroll - recalculate position
446
- */
447
- react_1.default.useEffect(() => {
448
- if (!isOpen || !scrollableAncestor) {
449
- return;
450
- }
451
- const handleScroll = () => {
452
- window.requestAnimationFrame(() => {
453
- calculatePosition();
454
- });
455
- };
456
- // parent scroll listener
457
- if (scrollableAncestor instanceof HTMLElement) {
458
- scrollableAncestor.addEventListener('scroll', handleScroll, { passive: true });
459
- }
460
- else if (scrollableAncestor === window) {
461
- window.addEventListener('scroll', handleScroll, { passive: true });
462
- }
463
- // eslint-disable-next-line consistent-return
464
- return () => {
465
- if (scrollableAncestor instanceof HTMLElement) {
466
- scrollableAncestor.removeEventListener('scroll', handleScroll);
467
- }
468
- else if (scrollableAncestor === window) {
469
- window.removeEventListener('scroll', handleScroll);
470
- }
471
- };
472
- }, [isOpen, scrollableAncestor, calculatePosition]);
473
- /**
474
- * On window resize - recalculate position
475
- */
476
- react_1.default.useEffect(() => {
477
- if (!isOpen) {
478
- return;
479
- }
480
- const handleResize = () => {
481
- window.requestAnimationFrame(() => {
482
- calculatePosition();
483
- });
484
- };
485
- // parent scroll listener
486
- window.addEventListener('resize', handleResize);
487
- // eslint-disable-next-line consistent-return
488
- return () => {
489
- window.removeEventListener('resize', handleResize);
490
- };
491
- }, [calculatePosition, isOpen]);
492
- return {
493
- actualPlacement,
494
- style,
495
- popperRef,
496
- isVisible,
497
- scrollableAncestor,
498
- calculatePosition,
499
- setActualPlacement,
500
- setStyle,
501
- setIsVisible,
502
- };
503
- };
504
- exports.usePopper = usePopper;
505
- exports.default = exports.usePopper;