@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 +6 -0
- package/dist/cjs/helperComponents/TreeNode/TreeNode.js +6 -1
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.d.ts +2 -2
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.js +14 -7
- package/dist/esm/helperComponents/TreeNode/TreeNode.js +6 -1
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.d.ts +2 -2
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.js +14 -7
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/helperComponents/TreeNode/TreeNode.tsx +8 -1
- package/src/helperComponents/TreeNode/components/TreeNodeActions.tsx +22 -9
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
}
|