@via-profit/ui-kit 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  ## Не выпущено
6
6
 
7
+ ## 0.12.1 — 2026-11-06
8
+
9
+ ### Исправлено
10
+
11
+ - **Popper**: автоматически закрывается при удалении элемента привязки из DOM или при передаче `anchorElement={null}`.
12
+
13
+ ## 0.12.0 — 2026-11-05
14
+
15
+ ### Добавлено
16
+
17
+ - **Tooltip**: свойство `interactive` — указатель можно перевести на подсказку, чтобы выделить текст или нажать ссылку внутри. Без него подсказка не перехватывает указатель.
18
+
19
+ ### Изменено
20
+
21
+ - **Tooltip**: по умолчанию подсказка не получает события указателя. Раньше указатель, попавший на подсказку, удерживал её открытой: в плотном списке подсказка нижней кнопки перекрывала кнопку выше, и навести на неё было нельзя. Если подсказку нужно выделять или нажимать в ней, передайте `interactive`;
22
+ - **Popper**: явный `pointerEvents` в `style` больше не затирается — скрытый поппер по-прежнему не перехватывает указатель.
23
+
7
24
  ## 0.11.0 — 2026-11-05
8
25
 
9
26
  ### Добавлено
@@ -16,7 +16,7 @@ export interface PopperProps extends React.HTMLAttributes<HTMLDivElement> {
16
16
  readonly isOpen: boolean;
17
17
  /**
18
18
  * The HTML element to which the popper will be attached and positioned relative to.
19
- * While it is `null`, the popper stays hidden even if `isOpen` is `true`.
19
+ * The popper closes if the element is removed from the DOM or becomes `null`.
20
20
  *
21
21
  * @required
22
22
  * @example
package/Popper/Popper.js CHANGED
@@ -33,11 +33,29 @@ const react_1 = __importDefault(require("react"));
33
33
  const react_dom_1 = __importDefault(require("react-dom"));
34
34
  const PopperContainer_1 = __importDefault(require("./PopperContainer"));
35
35
  const usePopper_1 = require("./usePopper");
36
+ const getServerAnchorConnected = () => false;
36
37
  __exportStar(require("./usePopper"), exports);
37
38
  exports.PORTAL_ID = 'ui-kit-portal';
38
39
  const Popper = (props, ref) => {
39
40
  const { isOpen, children, anchorElement, zIndex, overrides, anchorPos = 'auto', positionStrategy = 'fixed', viewportMargin = 30, autoFlip = false, alternativePlacements, offset = 0, onAnchorPosChanged } = props, nativeProps = __rest(props, ["isOpen", "children", "anchorElement", "zIndex", "overrides", "anchorPos", "positionStrategy", "viewportMargin", "autoFlip", "alternativePlacements", "offset", "onAnchorPosChanged"]);
40
41
  const [domLoaded, setDomLoaded] = react_1.default.useState(false);
42
+ const subscribeToAnchor = react_1.default.useCallback((onChange) => {
43
+ if (!isOpen || !anchorElement) {
44
+ return () => { };
45
+ }
46
+ const observer = new MutationObserver(onChange);
47
+ const options = { childList: true, subtree: true };
48
+ observer.observe(anchorElement.ownerDocument, options);
49
+ // Mutations inside a shadow root are not reported by the document observer.
50
+ let root = anchorElement.getRootNode();
51
+ while (root instanceof ShadowRoot) {
52
+ observer.observe(root, options);
53
+ root = root.host.getRootNode();
54
+ }
55
+ return () => observer.disconnect();
56
+ }, [isOpen, anchorElement]);
57
+ const getAnchorConnected = react_1.default.useCallback(() => isOpen && Boolean(anchorElement === null || anchorElement === void 0 ? void 0 : anchorElement.isConnected), [isOpen, anchorElement]);
58
+ const canOpen = react_1.default.useSyncExternalStore(subscribeToAnchor, getAnchorConnected, getServerAnchorConnected);
41
59
  const { actualPlacement, style, popperRef, isVisible } = (0, usePopper_1.usePopper)({
42
60
  anchorElement,
43
61
  anchorPos,
@@ -46,7 +64,7 @@ const Popper = (props, ref) => {
46
64
  offset,
47
65
  viewportMargin,
48
66
  alternativePlacements,
49
- isOpen,
67
+ isOpen: canOpen,
50
68
  });
51
69
  const overridesMap = react_1.default.useMemo(() => ({
52
70
  Container: (overrides === null || overrides === void 0 ? void 0 : overrides.Container) || PopperContainer_1.default,
@@ -81,19 +99,24 @@ const Popper = (props, ref) => {
81
99
  }
82
100
  }, [actualPlacement]);
83
101
  const childrenMemo = react_1.default.useMemo(() => children, [children]);
84
- const renderNode = react_1.default.useCallback(() => (react_1.default.createElement(overridesMap.Container, Object.assign({}, nativeProps, { style: Object.assign(Object.assign(Object.assign(Object.assign({}, style), nativeProps.style), { opacity: isVisible ? 1 : 0, pointerEvents: isVisible ? 'auto' : 'none' }), (positionStrategy === 'fixed' && {
85
- position: 'fixed',
86
- })), zIndex: zIndex, positionStrategy: positionStrategy, "data-popper-placement": actualPlacement, "data-popper-strategy": positionStrategy, ref: el => {
87
- popperRef.current = el;
88
- if (ref) {
89
- if (typeof ref === 'function') {
90
- ref(el);
91
- }
92
- if (typeof ref === 'object') {
93
- ref.current = el;
102
+ const renderNode = react_1.default.useCallback(() => {
103
+ var _a, _b;
104
+ return (react_1.default.createElement(overridesMap.Container, Object.assign({}, nativeProps, { style: Object.assign(Object.assign(Object.assign(Object.assign({}, style), nativeProps.style), { opacity: isVisible ? 1 : 0,
105
+ // A hidden popper never intercepts the pointer; a visible one lets the caller opt out
106
+ pointerEvents: !isVisible ? 'none' : ((_b = (_a = nativeProps.style) === null || _a === void 0 ? void 0 : _a.pointerEvents) !== null && _b !== void 0 ? _b : 'auto') }), (positionStrategy === 'fixed' && {
107
+ position: 'fixed',
108
+ })), zIndex: zIndex, positionStrategy: positionStrategy, "data-popper-placement": actualPlacement, "data-popper-strategy": positionStrategy, ref: el => {
109
+ popperRef.current = el;
110
+ if (ref) {
111
+ if (typeof ref === 'function') {
112
+ ref(el);
113
+ }
114
+ if (typeof ref === 'object') {
115
+ ref.current = el;
116
+ }
94
117
  }
95
- }
96
- } }), childrenMemo)), [
118
+ } }), childrenMemo));
119
+ }, [
97
120
  overridesMap,
98
121
  nativeProps,
99
122
  style,
@@ -106,7 +129,7 @@ const Popper = (props, ref) => {
106
129
  popperRef,
107
130
  ref,
108
131
  ]);
109
- if (!isOpen) {
132
+ if (!canOpen) {
110
133
  return null;
111
134
  }
112
135
  if (positionStrategy === 'fixed') {
@@ -380,7 +380,7 @@ const usePopper = (props) => {
380
380
  ]);
381
381
  // #region Calculate Position
382
382
  const calculatePosition = react_1.default.useCallback(() => {
383
- if (!anchorElement || !popperRef.current) {
383
+ if (!(anchorElement === null || anchorElement === void 0 ? void 0 : anchorElement.isConnected) || !popperRef.current) {
384
384
  return;
385
385
  }
386
386
  const anchorRect = anchorElement.getBoundingClientRect();
@@ -49,6 +49,12 @@ export type TooltipProps = {
49
49
  * Default: false
50
50
  */
51
51
  readonly describeChild?: boolean;
52
+ /**
53
+ * Lets the pointer move onto the tooltip, e.g. to select its text or click a link inside.
54
+ * When `false`, the tooltip does not receive pointer events and cannot block the elements under it
55
+ * Default: false
56
+ */
57
+ readonly interactive?: boolean;
52
58
  /**
53
59
  * Default: 8
54
60
  */
package/Tooltip/index.js CHANGED
@@ -45,7 +45,7 @@ const setRef = (ref, value) => {
45
45
  const isEmptyTitle = (title) => title === null || typeof title === 'undefined' || title === false || title === '';
46
46
  const Tooltip = props => {
47
47
  var _a;
48
- const { title, children, placement = 'top', arrow = false, enterDelay = 400, leaveDelay = 100, isOpen, onOpenChange, describeChild = false, offset = 8, zIndex, id, overrides, } = props;
48
+ const { title, children, placement = 'top', arrow = false, enterDelay = 400, leaveDelay = 100, isOpen, onOpenChange, describeChild = false, interactive = false, offset = 8, zIndex, id, overrides, } = props;
49
49
  const theme = (0, react_2.useTheme)();
50
50
  const generatedId = `tooltip-${react_1.default.useId().replace(/:/g, '')}`;
51
51
  const tooltipId = id || generatedId;
@@ -182,12 +182,15 @@ const Tooltip = props => {
182
182
  return (react_1.default.createElement(react_1.default.Fragment, null,
183
183
  anchor,
184
184
  react_1.default.createElement(Popper_1.default, { isOpen: open, anchorElement: anchorElement, anchorPos: placement, autoFlip: true, offset: offset, viewportMargin: 8, zIndex: zIndex !== null && zIndex !== void 0 ? zIndex : theme.zIndex.modal + 1, onAnchorPosChanged: setActualPlacement, id: tooltipId, role: "tooltip",
185
- // The pointer can move from the element onto the tooltip without hiding it
186
- onPointerEnter: clearTimer, onPointerLeave: event => {
187
- if (isHoverPointer(event)) {
188
- hide(leaveDelay);
185
+ // Without `interactive` the tooltip must not intercept the pointer: otherwise it
186
+ // blocks the elements under it and sticks under the cursor that moves across it
187
+ style: interactive ? undefined : { pointerEvents: 'none' }, onPointerEnter: interactive ? clearTimer : undefined, onPointerLeave: interactive
188
+ ? event => {
189
+ if (isHoverPointer(event)) {
190
+ hide(leaveDelay);
191
+ }
189
192
  }
190
- } },
193
+ : undefined },
191
194
  react_1.default.createElement(overridesMap.Container, null,
192
195
  title,
193
196
  arrow && react_1.default.createElement(overridesMap.Arrow, { placement: actualPlacement })))));
@@ -13,11 +13,29 @@ import React from 'react';
13
13
  import ReactDOM from 'react-dom';
14
14
  import Container from './PopperContainer';
15
15
  import { usePopper } from './usePopper';
16
+ const getServerAnchorConnected = () => false;
16
17
  export * from './usePopper';
17
18
  export const PORTAL_ID = 'ui-kit-portal';
18
19
  const Popper = (props, ref) => {
19
20
  const { isOpen, children, anchorElement, zIndex, overrides, anchorPos = 'auto', positionStrategy = 'fixed', viewportMargin = 30, autoFlip = false, alternativePlacements, offset = 0, onAnchorPosChanged } = props, nativeProps = __rest(props, ["isOpen", "children", "anchorElement", "zIndex", "overrides", "anchorPos", "positionStrategy", "viewportMargin", "autoFlip", "alternativePlacements", "offset", "onAnchorPosChanged"]);
20
21
  const [domLoaded, setDomLoaded] = React.useState(false);
22
+ const subscribeToAnchor = React.useCallback((onChange) => {
23
+ if (!isOpen || !anchorElement) {
24
+ return () => { };
25
+ }
26
+ const observer = new MutationObserver(onChange);
27
+ const options = { childList: true, subtree: true };
28
+ observer.observe(anchorElement.ownerDocument, options);
29
+ // Mutations inside a shadow root are not reported by the document observer.
30
+ let root = anchorElement.getRootNode();
31
+ while (root instanceof ShadowRoot) {
32
+ observer.observe(root, options);
33
+ root = root.host.getRootNode();
34
+ }
35
+ return () => observer.disconnect();
36
+ }, [isOpen, anchorElement]);
37
+ const getAnchorConnected = React.useCallback(() => isOpen && Boolean(anchorElement === null || anchorElement === void 0 ? void 0 : anchorElement.isConnected), [isOpen, anchorElement]);
38
+ const canOpen = React.useSyncExternalStore(subscribeToAnchor, getAnchorConnected, getServerAnchorConnected);
21
39
  const { actualPlacement, style, popperRef, isVisible } = usePopper({
22
40
  anchorElement,
23
41
  anchorPos,
@@ -26,7 +44,7 @@ const Popper = (props, ref) => {
26
44
  offset,
27
45
  viewportMargin,
28
46
  alternativePlacements,
29
- isOpen,
47
+ isOpen: canOpen,
30
48
  });
31
49
  const overridesMap = React.useMemo(() => ({
32
50
  Container: (overrides === null || overrides === void 0 ? void 0 : overrides.Container) || Container,
@@ -61,19 +79,24 @@ const Popper = (props, ref) => {
61
79
  }
62
80
  }, [actualPlacement]);
63
81
  const childrenMemo = React.useMemo(() => children, [children]);
64
- const renderNode = React.useCallback(() => (React.createElement(overridesMap.Container, Object.assign({}, nativeProps, { style: Object.assign(Object.assign(Object.assign(Object.assign({}, style), nativeProps.style), { opacity: isVisible ? 1 : 0, pointerEvents: isVisible ? 'auto' : 'none' }), (positionStrategy === 'fixed' && {
65
- position: 'fixed',
66
- })), zIndex: zIndex, positionStrategy: positionStrategy, "data-popper-placement": actualPlacement, "data-popper-strategy": positionStrategy, ref: el => {
67
- popperRef.current = el;
68
- if (ref) {
69
- if (typeof ref === 'function') {
70
- ref(el);
71
- }
72
- if (typeof ref === 'object') {
73
- ref.current = el;
82
+ const renderNode = React.useCallback(() => {
83
+ var _a, _b;
84
+ return (React.createElement(overridesMap.Container, Object.assign({}, nativeProps, { style: Object.assign(Object.assign(Object.assign(Object.assign({}, style), nativeProps.style), { opacity: isVisible ? 1 : 0,
85
+ // A hidden popper never intercepts the pointer; a visible one lets the caller opt out
86
+ pointerEvents: !isVisible ? 'none' : ((_b = (_a = nativeProps.style) === null || _a === void 0 ? void 0 : _a.pointerEvents) !== null && _b !== void 0 ? _b : 'auto') }), (positionStrategy === 'fixed' && {
87
+ position: 'fixed',
88
+ })), zIndex: zIndex, positionStrategy: positionStrategy, "data-popper-placement": actualPlacement, "data-popper-strategy": positionStrategy, ref: el => {
89
+ popperRef.current = el;
90
+ if (ref) {
91
+ if (typeof ref === 'function') {
92
+ ref(el);
93
+ }
94
+ if (typeof ref === 'object') {
95
+ ref.current = el;
96
+ }
74
97
  }
75
- }
76
- } }), childrenMemo)), [
98
+ } }), childrenMemo));
99
+ }, [
77
100
  overridesMap,
78
101
  nativeProps,
79
102
  style,
@@ -86,7 +109,7 @@ const Popper = (props, ref) => {
86
109
  popperRef,
87
110
  ref,
88
111
  ]);
89
- if (!isOpen) {
112
+ if (!canOpen) {
90
113
  return null;
91
114
  }
92
115
  if (positionStrategy === 'fixed') {
@@ -373,7 +373,7 @@ export const usePopper = (props) => {
373
373
  ]);
374
374
  // #region Calculate Position
375
375
  const calculatePosition = React.useCallback(() => {
376
- if (!anchorElement || !popperRef.current) {
376
+ if (!(anchorElement === null || anchorElement === void 0 ? void 0 : anchorElement.isConnected) || !popperRef.current) {
377
377
  return;
378
378
  }
379
379
  const anchorRect = anchorElement.getBoundingClientRect();
@@ -40,7 +40,7 @@ const setRef = (ref, value) => {
40
40
  const isEmptyTitle = (title) => title === null || typeof title === 'undefined' || title === false || title === '';
41
41
  const Tooltip = props => {
42
42
  var _a;
43
- const { title, children, placement = 'top', arrow = false, enterDelay = 400, leaveDelay = 100, isOpen, onOpenChange, describeChild = false, offset = 8, zIndex, id, overrides, } = props;
43
+ const { title, children, placement = 'top', arrow = false, enterDelay = 400, leaveDelay = 100, isOpen, onOpenChange, describeChild = false, interactive = false, offset = 8, zIndex, id, overrides, } = props;
44
44
  const theme = useTheme();
45
45
  const generatedId = `tooltip-${React.useId().replace(/:/g, '')}`;
46
46
  const tooltipId = id || generatedId;
@@ -177,12 +177,15 @@ const Tooltip = props => {
177
177
  return (React.createElement(React.Fragment, null,
178
178
  anchor,
179
179
  React.createElement(Popper, { isOpen: open, anchorElement: anchorElement, anchorPos: placement, autoFlip: true, offset: offset, viewportMargin: 8, zIndex: zIndex !== null && zIndex !== void 0 ? zIndex : theme.zIndex.modal + 1, onAnchorPosChanged: setActualPlacement, id: tooltipId, role: "tooltip",
180
- // The pointer can move from the element onto the tooltip without hiding it
181
- onPointerEnter: clearTimer, onPointerLeave: event => {
182
- if (isHoverPointer(event)) {
183
- hide(leaveDelay);
180
+ // Without `interactive` the tooltip must not intercept the pointer: otherwise it
181
+ // blocks the elements under it and sticks under the cursor that moves across it
182
+ style: interactive ? undefined : { pointerEvents: 'none' }, onPointerEnter: interactive ? clearTimer : undefined, onPointerLeave: interactive
183
+ ? event => {
184
+ if (isHoverPointer(event)) {
185
+ hide(leaveDelay);
186
+ }
184
187
  }
185
- } },
188
+ : undefined },
186
189
  React.createElement(overridesMap.Container, null,
187
190
  title,
188
191
  arrow && React.createElement(overridesMap.Arrow, { placement: actualPlacement })))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@via-profit/ui-kit",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "UI Kit",
5
5
  "repository": {
6
6
  "type": "git",