@cloud-ru/ds-tree 1.0.25-preview-ac2c37a8.0 → 1.0.25

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
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 1.0.25 (2026-10-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **FF-8862:** innerRef components recognition ([fca7d68](https://github.com/cloud-ru-tech/snack-v2/commit/fca7d68600da3d349dca0d1d1d702fef286ad4dd))
11
+
6
12
  ## 1.0.24 (2026-10-01)
7
13
 
8
14
  **Note:** Version bump only for package @ds/tree
@@ -109,7 +109,12 @@ exports.TreeNode = (0, react_1.forwardRef)(({ id, title, icon: iconProp, expande
109
109
  contentRef.current?.focus();
110
110
  }
111
111
  };
112
- const handleBlurActions = () => {
112
+ const handleBlurActions = (event) => {
113
+ const nextTarget = event.relatedTarget;
114
+ // Фокус уходит на саму строку (возврат после закрытия дроплиста) — строку не блюрим.
115
+ if (nextTarget instanceof Node && contentRef.current?.contains(nextTarget)) {
116
+ return;
117
+ }
113
118
  if (isDroplistTriggerFocused && !isDroplistOpen) {
114
119
  setFocusDroplistTrigger(false);
115
120
  contentRef.current?.blur();
@@ -1,5 +1,5 @@
1
1
  import { ItemProps } from '@cloud-ru/ds-list';
2
- import { Dispatch, SetStateAction } from 'react';
2
+ import { Dispatch, FocusEvent, SetStateAction } from 'react';
3
3
  import { Size, TreeNodeProps } from '../../../types';
4
4
  type TreeNodeActionsProps = {
5
5
  isDroplistOpen: boolean;
@@ -8,7 +8,7 @@ type TreeNodeActionsProps = {
8
8
  node: Omit<TreeNodeProps, 'href'>;
9
9
  isDroplistTriggerFocused: boolean;
10
10
  focusNode(): void;
11
- onBlurActions(): void;
11
+ onBlurActions(event: FocusEvent<HTMLElement>): void;
12
12
  size: Size;
13
13
  };
14
14
  export declare function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }: TreeNodeActionsProps): import("react/jsx-runtime").JSX.Element | null;
@@ -15,11 +15,15 @@ const utils_1 = require("../utils");
15
15
  function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }) {
16
16
  const droplistActions = getNodeActions(node);
17
17
  const localRef = (0, react_1.useRef)(null);
18
+ const returnFocusTimerRef = (0, react_1.useRef)();
18
19
  (0, react_1.useEffect)(() => {
19
20
  if (localRef.current && isDroplistTriggerFocused) {
20
21
  localRef.current.focus();
21
22
  }
22
23
  }, [isDroplistTriggerFocused, localRef]);
24
+ (0, react_1.useEffect)(() => () => {
25
+ clearTimeout(returnFocusTimerRef.current);
26
+ }, []);
23
27
  const handleKeyDown = e => {
24
28
  switch (e.key) {
25
29
  case 'Tab': {
@@ -50,6 +54,8 @@ function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode,
50
54
  return;
51
55
  }
52
56
  case 'ArrowUp': {
57
+ // onOpenChange уже поставил возврат фокуса на строку — здесь фокус остаётся на kebab.
58
+ clearTimeout(returnFocusTimerRef.current);
53
59
  setDroplistOpen(false);
54
60
  localRef.current?.focus();
55
61
  e.stopPropagation();
@@ -64,13 +70,14 @@ function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode,
64
70
  }
65
71
  return ((0, jsx_runtime_1.jsx)("div", { role: 'presentation', className: styles_module_scss_1.default.treeNodeActions, "data-focused": isDroplistTriggerFocused || undefined, onClick: utils_1.stopPropagationClick, onKeyDown: handleKeyDown, onFocus: utils_1.stopPropagationFocus, children: (0, jsx_runtime_1.jsx)(list_1.Droplist, { open: isDroplistOpen, onOpenChange: open => {
66
72
  setDroplistOpen(open);
67
- // Закрытие дроплиста (по клику в пункт, ESC, outside-click) должно
68
- // вернуть фокус на строку и снять `isDroplistTriggerFocused` —
69
- // иначе kebab остаётся в visible-hover-состоянии. Tab и ArrowLeft
70
- // уже делают это сами; для пути через `closeDroplistOnItemClick`
71
- // ресет навешиваем здесь.
72
- if (!open) {
73
- focusNode();
73
+ if (open) {
74
+ return;
74
75
  }
76
+ // Droplist возвращает фокус на триггер после onOpenChange: синхронно
77
+ // при выборе пункта и через setTimeout(0) при Escape. Возврат на строку
78
+ // ставим следующим тиком, чтобы он шёл после обоих. Tab и ArrowLeft
79
+ // фокусируют строку сами и до onOpenChange не доходят.
80
+ clearTimeout(returnFocusTimerRef.current);
81
+ returnFocusTimerRef.current = setTimeout(focusNode);
75
82
  }, items: droplistActions, closeDroplistOnItemClick: true, placement: 'bottom-end', size: size, children: (0, jsx_runtime_1.jsx)(button_1.Button, { view: 'elevated', appearance: 'neutral', size: size, icon: (0, jsx_runtime_1.jsx)(system_1.KebabSVG, {}), onBlur: onBlurActions, tabIndex: -1, "data-test-id": constants_1.TEST_IDS.droplistTrigger, innerRef: localRef }) }) }));
76
83
  }
@@ -103,7 +103,12 @@ export const TreeNode = forwardRef(({ id, title, icon: iconProp, expandedIcon: e
103
103
  contentRef.current?.focus();
104
104
  }
105
105
  };
106
- const handleBlurActions = () => {
106
+ const handleBlurActions = (event) => {
107
+ const nextTarget = event.relatedTarget;
108
+ // Фокус уходит на саму строку (возврат после закрытия дроплиста) — строку не блюрим.
109
+ if (nextTarget instanceof Node && contentRef.current?.contains(nextTarget)) {
110
+ return;
111
+ }
107
112
  if (isDroplistTriggerFocused && !isDroplistOpen) {
108
113
  setFocusDroplistTrigger(false);
109
114
  contentRef.current?.blur();
@@ -1,5 +1,5 @@
1
1
  import { ItemProps } from '@cloud-ru/ds-list';
2
- import { Dispatch, SetStateAction } from 'react';
2
+ import { Dispatch, FocusEvent, SetStateAction } from 'react';
3
3
  import { Size, TreeNodeProps } from '../../../types.js';
4
4
  type TreeNodeActionsProps = {
5
5
  isDroplistOpen: boolean;
@@ -8,7 +8,7 @@ type TreeNodeActionsProps = {
8
8
  node: Omit<TreeNodeProps, 'href'>;
9
9
  isDroplistTriggerFocused: boolean;
10
10
  focusNode(): void;
11
- onBlurActions(): void;
11
+ onBlurActions(event: FocusEvent<HTMLElement>): void;
12
12
  size: Size;
13
13
  };
14
14
  export declare function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }: TreeNodeActionsProps): import("react/jsx-runtime").JSX.Element | null;
@@ -9,11 +9,15 @@ import { stopPropagationClick, stopPropagationFocus } from '../utils.js';
9
9
  export function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }) {
10
10
  const droplistActions = getNodeActions(node);
11
11
  const localRef = useRef(null);
12
+ const returnFocusTimerRef = useRef();
12
13
  useEffect(() => {
13
14
  if (localRef.current && isDroplistTriggerFocused) {
14
15
  localRef.current.focus();
15
16
  }
16
17
  }, [isDroplistTriggerFocused, localRef]);
18
+ useEffect(() => () => {
19
+ clearTimeout(returnFocusTimerRef.current);
20
+ }, []);
17
21
  const handleKeyDown = e => {
18
22
  switch (e.key) {
19
23
  case 'Tab': {
@@ -44,6 +48,8 @@ export function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focu
44
48
  return;
45
49
  }
46
50
  case 'ArrowUp': {
51
+ // onOpenChange уже поставил возврат фокуса на строку — здесь фокус остаётся на kebab.
52
+ clearTimeout(returnFocusTimerRef.current);
47
53
  setDroplistOpen(false);
48
54
  localRef.current?.focus();
49
55
  e.stopPropagation();
@@ -58,13 +64,14 @@ export function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focu
58
64
  }
59
65
  return (_jsx("div", { role: 'presentation', className: styles.treeNodeActions, "data-focused": isDroplistTriggerFocused || undefined, onClick: stopPropagationClick, onKeyDown: handleKeyDown, onFocus: stopPropagationFocus, children: _jsx(Droplist, { open: isDroplistOpen, onOpenChange: open => {
60
66
  setDroplistOpen(open);
61
- // Закрытие дроплиста (по клику в пункт, ESC, outside-click) должно
62
- // вернуть фокус на строку и снять `isDroplistTriggerFocused` —
63
- // иначе kebab остаётся в visible-hover-состоянии. Tab и ArrowLeft
64
- // уже делают это сами; для пути через `closeDroplistOnItemClick`
65
- // ресет навешиваем здесь.
66
- if (!open) {
67
- focusNode();
67
+ if (open) {
68
+ return;
68
69
  }
70
+ // Droplist возвращает фокус на триггер после onOpenChange: синхронно
71
+ // при выборе пункта и через setTimeout(0) при Escape. Возврат на строку
72
+ // ставим следующим тиком, чтобы он шёл после обоих. Tab и ArrowLeft
73
+ // фокусируют строку сами и до onOpenChange не доходят.
74
+ clearTimeout(returnFocusTimerRef.current);
75
+ returnFocusTimerRef.current = setTimeout(focusNode);
69
76
  }, items: droplistActions, closeDroplistOnItemClick: true, placement: 'bottom-end', size: size, children: _jsx(Button, { view: 'elevated', appearance: 'neutral', size: size, icon: _jsx(KebabSVG, {}), onBlur: onBlurActions, tabIndex: -1, "data-test-id": TEST_IDS.droplistTrigger, innerRef: localRef }) }) }));
70
77
  }