@via-profit/ui-kit 0.2.3 → 0.2.5

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>>>;
@@ -199,15 +199,19 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
199
199
  /**
200
200
  * List of possibility views
201
201
  */
202
- const [views] = react_1.default.useState(() => computeViews(inputViews, range));
202
+ const [views, setComputedViews] = react_1.default.useState(() => computeViews(inputViews, range));
203
+ const setViews = react_1.default.useCallback((variants) => setComputedViews(variants), []);
203
204
  const selectView = react_1.default.useCallback((view) => {
204
205
  var _a;
206
+ if (!views.includes(view)) {
207
+ setViews([...views, view]);
208
+ }
205
209
  setView(view);
206
210
  const index = views.findIndex(v => v === view);
207
211
  if (index > -1) {
208
212
  (_a = swiperRef.current) === null || _a === void 0 ? void 0 : _a.goToIndex(index);
209
213
  }
210
- }, [views]);
214
+ }, [setViews, views]);
211
215
  const resetVariablesRef = react_1.default.useRef({
212
216
  calendarDate,
213
217
  value,
@@ -503,6 +507,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
503
507
  ]);
504
508
  const initialIndex = react_1.default.useMemo(() => views.findIndex(v => v === view), [views, view]);
505
509
  const getActiveView = react_1.default.useCallback(() => view, [view]);
510
+ const getCalendarDate = react_1.default.useCallback(() => calendarDate, [calendarDate]);
506
511
  /**
507
512
  * API
508
513
  */
@@ -510,9 +515,11 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
510
515
  setView: selectView,
511
516
  reset: handleReset,
512
517
  setValue,
518
+ getCalendarDate,
513
519
  setCalendarDate,
514
520
  getActiveView,
515
- }), [selectView, handleReset, getActiveView]);
521
+ setViews,
522
+ }), [selectView, handleReset, getActiveView, setViews, getCalendarDate]);
516
523
  return (react_1.default.createElement(overridesMap.Paper, null,
517
524
  react_1.default.createElement(overridesMap.Header, null,
518
525
  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;
@@ -173,63 +173,30 @@ const usePopper = (props) => {
173
173
  return { left, top, width };
174
174
  }, [offset, positionStrategy]);
175
175
  const clampPopperToContainer = react_1.default.useCallback((style, popperRect) => {
176
- // Если нет scrollableAncestor, используем document.documentElement как fallback
177
- const container = scrollableAncestor instanceof HTMLElement ? scrollableAncestor : document.documentElement;
178
- const containerRect = container.getBoundingClientRect();
179
- // Базовые границы контейнера с учетом отступа
180
- let minLeft = containerRect.left + viewportMargin;
181
- let maxLeft = containerRect.right - popperRect.width - viewportMargin;
182
- let minTop = containerRect.top + viewportMargin;
183
- let maxTop = containerRect.bottom - popperRect.height - viewportMargin;
184
- // Для fixed позиционирования добавляем ограничения viewport
185
- if (positionStrategy === 'fixed') {
186
- // Границы viewport с учетом отступа
187
- const viewportMinLeft = viewportMargin;
188
- const viewportMaxLeft = window.innerWidth - popperRect.width - viewportMargin;
189
- const viewportMinTop = viewportMargin;
190
- const viewportMaxTop = window.innerHeight - popperRect.height - viewportMargin;
191
- // Берем пересечение границ контейнера и viewport
192
- minLeft = Math.max(minLeft, viewportMinLeft);
193
- maxLeft = Math.min(maxLeft, viewportMaxLeft);
194
- minTop = Math.max(minTop, viewportMinTop);
195
- maxTop = Math.min(maxTop, viewportMaxTop);
176
+ if (positionStrategy === 'absolute') {
177
+ return style;
196
178
  }
197
- // Убеждаемся, что границы валидны (min не больше max)
198
- const validMinLeft = Math.min(minLeft, maxLeft);
199
- const validMaxLeft = Math.max(minLeft, maxLeft);
200
- const validMinTop = Math.min(minTop, maxTop);
201
- const validMaxTop = Math.max(minTop, maxTop);
202
- // Ограничиваем позицию
203
- const clampedLeft = Math.max(validMinLeft, Math.min(style.left, validMaxLeft));
204
- const clampedTop = Math.max(validMinTop, Math.min(style.top, validMaxTop));
179
+ const viewportMinLeft = viewportMargin;
180
+ const viewportMaxLeft = window.innerWidth - popperRect.width - viewportMargin;
181
+ const viewportMinTop = viewportMargin;
182
+ const viewportMaxTop = window.innerHeight - popperRect.height - viewportMargin;
205
183
  return {
206
- left: clampedLeft,
207
- top: clampedTop,
184
+ left: Math.max(viewportMinLeft, Math.min(style.left, viewportMaxLeft)),
185
+ top: Math.max(viewportMinTop, Math.min(style.top, viewportMaxTop)),
208
186
  width: style.width,
209
187
  };
210
- }, [positionStrategy, scrollableAncestor, viewportMargin]);
188
+ }, [positionStrategy, viewportMargin]);
211
189
  const checkIfViewportFits = react_1.default.useCallback((style, popperRect) => {
212
190
  const left = style.left;
213
191
  const top = style.top;
214
192
  const right = left + popperRect.width;
215
193
  const bottom = top + popperRect.height;
216
- if (positionStrategy === 'absolute' && scrollableAncestor instanceof HTMLElement) {
217
- const containerRect = scrollableAncestor.getBoundingClientRect();
218
- const relativeLeft = left - containerRect.left;
219
- const relativeTop = top - containerRect.top;
220
- const relativeRight = relativeLeft + popperRect.width;
221
- const relativeBottom = relativeTop + popperRect.height;
222
- const isWithinHorizontal = relativeLeft >= viewportMargin && relativeRight <= containerRect.width - viewportMargin;
223
- const isWithinVertical = relativeTop >= viewportMargin && relativeBottom <= containerRect.height - viewportMargin;
224
- return isWithinHorizontal && isWithinVertical;
225
- }
226
194
  const viewportWidth = window.innerWidth;
227
195
  const viewportHeight = window.innerHeight;
228
196
  const isWithinHorizontal = left >= viewportMargin && right <= viewportWidth - viewportMargin;
229
197
  const isWithinVertical = top >= viewportMargin && bottom <= viewportHeight - viewportMargin;
230
- const hasValidPosition = left >= 0 && top >= 0;
231
- return hasValidPosition && isWithinHorizontal && isWithinVertical;
232
- }, [viewportMargin, scrollableAncestor, positionStrategy]);
198
+ return isWithinHorizontal && isWithinVertical;
199
+ }, [viewportMargin]);
233
200
  const placementsOrder = react_1.default.useMemo(() => {
234
201
  const basePlacements = {
235
202
  top: ['top', 'top-left', 'top-right', 'top-start', 'top-end'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@via-profit/ui-kit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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;