@snack-uikit/tree 0.9.28 → 0.9.30

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cjs/constants.d.ts +2 -0
  3. package/dist/cjs/constants.js +5 -3
  4. package/dist/cjs/helperComponents/TreeNode/TreeNode.js +33 -12
  5. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.d.ts +2 -2
  6. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
  7. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.js +32 -0
  8. package/dist/cjs/helperComponents/TreeNode/components/index.d.ts +1 -0
  9. package/dist/cjs/helperComponents/TreeNode/components/index.js +2 -1
  10. package/dist/cjs/helperComponents/TreeNode/styles.module.css +7 -0
  11. package/dist/cjs/types.d.ts +2 -0
  12. package/dist/esm/constants.d.ts +2 -0
  13. package/dist/esm/constants.js +2 -0
  14. package/dist/esm/helperComponents/TreeNode/TreeNode.js +18 -3
  15. package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.d.ts +2 -2
  16. package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
  17. package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.js +6 -0
  18. package/dist/esm/helperComponents/TreeNode/components/index.d.ts +1 -0
  19. package/dist/esm/helperComponents/TreeNode/components/index.js +1 -0
  20. package/dist/esm/helperComponents/TreeNode/styles.module.css +7 -0
  21. package/dist/esm/types.d.ts +2 -0
  22. package/package.json +4 -4
  23. package/src/constants.ts +3 -0
  24. package/src/helperComponents/TreeNode/TreeNode.tsx +26 -3
  25. package/src/helperComponents/TreeNode/components/TreeNodeActions.tsx +2 -2
  26. package/src/helperComponents/TreeNode/components/TreeNodeHref.tsx +29 -0
  27. package/src/helperComponents/TreeNode/components/index.ts +1 -0
  28. package/src/helperComponents/TreeNode/styles.module.scss +7 -0
  29. package/src/types.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
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
+ ## <small>0.9.30 (2025-11-14)</small>
7
+
8
+ ### Only dependencies have been changed
9
+ * [@snack-uikit/list@0.32.7](https://github.com/cloud-ru-tech/snack-uikit/blob/master/packages/list/CHANGELOG.md)
10
+ * [@snack-uikit/truncate-string@0.7.4](https://github.com/cloud-ru-tech/snack-uikit/blob/master/packages/truncate-string/CHANGELOG.md)
11
+
12
+
13
+
14
+
15
+
16
+ ## <small>0.9.29 (2025-11-13)</small>
17
+
18
+ * feat(PDS-2935): add href param to TreeNode ([c508075](https://github.com/cloud-ru-tech/snack-uikit/commit/c508075))
19
+
20
+
21
+
22
+
23
+
6
24
  ## <small>0.9.28 (2025-10-28)</small>
7
25
 
8
26
  ### Only dependencies have been changed
@@ -18,4 +18,6 @@ export declare const TEST_IDS: {
18
18
  droplistAction: string;
19
19
  expandable: string;
20
20
  expandableContent: string;
21
+ link: string;
21
22
  };
23
+ export declare const LINK_TEST_HREF = "/snack/?path=/story/welcome--welcome";
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.TEST_IDS = exports.TRANSITION_TIMING = exports.SELECTION_MODE = void 0;
6
+ exports.LINK_TEST_HREF = exports.TEST_IDS = exports.TRANSITION_TIMING = exports.SELECTION_MODE = void 0;
7
7
  exports.SELECTION_MODE = {
8
8
  Single: 'single',
9
9
  Multi: 'multi'
@@ -23,5 +23,7 @@ exports.TEST_IDS = {
23
23
  droplistTrigger: 'tree__node__droplist-trigger',
24
24
  droplistAction: 'tree__node__droplist-action',
25
25
  expandable: 'tree__node__expandable',
26
- expandableContent: 'tree__node__expandable-content'
27
- };
26
+ expandableContent: 'tree__node__expandable-content',
27
+ link: 'tree__node__link'
28
+ };
29
+ exports.LINK_TEST_HREF = '/snack/?path=/story/welcome--welcome';
@@ -54,9 +54,10 @@ exports.TreeNode = (0, react_1.forwardRef)((_a, ref) => {
54
54
  onKeyDown,
55
55
  isLoading,
56
56
  parentNode,
57
- tabIndexAvailable
57
+ tabIndexAvailable,
58
+ href
58
59
  } = _a,
59
- rest = __rest(_a, ["id", "title", "icon", "expandedIcon", "collapsedIcon", "disabled", "onClick", "nested", "className", "onChevronClick", "onKeyDown", "isLoading", "parentNode", "tabIndexAvailable"]);
60
+ rest = __rest(_a, ["id", "title", "icon", "expandedIcon", "collapsedIcon", "disabled", "onClick", "nested", "className", "onChevronClick", "onKeyDown", "isLoading", "parentNode", "tabIndexAvailable", "href"]);
60
61
  const {
61
62
  isMultiSelect,
62
63
  isSelectable,
@@ -78,6 +79,7 @@ exports.TreeNode = (0, react_1.forwardRef)((_a, ref) => {
78
79
  const [isDroplistOpen, setDroplistOpen] = (0, react_1.useState)(false);
79
80
  const [isDroplistTriggerFocused, setFocusDroplistTrigger] = (0, react_1.useState)(false);
80
81
  const contentRef = (0, react_1.useRef)(null);
82
+ const anchorRef = (0, react_1.useRef)(null);
81
83
  const isExpandable = Array.isArray(nested);
82
84
  const isExpanded = isExpandable ? expandedNodes === null || expandedNodes === void 0 ? void 0 : expandedNodes.includes(id) : undefined;
83
85
  const nestedNodesSelection = (0, react_1.useMemo)(() => {
@@ -104,9 +106,20 @@ exports.TreeNode = (0, react_1.forwardRef)((_a, ref) => {
104
106
  title,
105
107
  disabled,
106
108
  nested,
107
- onClick
109
+ onClick,
110
+ href
108
111
  }, e);
109
112
  };
113
+ const handleAnchorClick = e => {
114
+ e.stopPropagation();
115
+ if ((e === null || e === void 0 ? void 0 : e.metaKey) || (e === null || e === void 0 ? void 0 : e.ctrlKey) || (e === null || e === void 0 ? void 0 : e.button) === 1) {
116
+ return;
117
+ }
118
+ if (onClick) {
119
+ e.preventDefault();
120
+ handleClick(e);
121
+ }
122
+ };
110
123
  const handleSelect = () => {
111
124
  onSelect({
112
125
  id,
@@ -185,6 +198,9 @@ exports.TreeNode = (0, react_1.forwardRef)((_a, ref) => {
185
198
  {
186
199
  e.preventDefault();
187
200
  handleSelect();
201
+ if (href && anchorRef.current) {
202
+ anchorRef.current.click();
203
+ }
188
204
  return;
189
205
  }
190
206
  default:
@@ -257,17 +273,22 @@ exports.TreeNode = (0, react_1.forwardRef)((_a, ref) => {
257
273
  className: styles_module_scss_1.default.treeNodeIcon,
258
274
  "data-test-id": constants_1.TEST_IDS.icon,
259
275
  children: treeNodeIcon
260
- }), (0, jsx_runtime_1.jsxs)(typography_1.Typography.SansBodyM, {
276
+ }), (0, jsx_runtime_1.jsx)(typography_1.Typography.SansBodyM, {
261
277
  tag: 'div',
262
278
  className: styles_module_scss_1.default.treeNodeTitle,
263
- children: [typeof title === 'string' && (0, jsx_runtime_1.jsx)(truncate_string_1.TruncateString, {
264
- text: title,
265
- "data-test-id": constants_1.TEST_IDS.title
266
- }), typeof title !== 'string' && title({
267
- id,
268
- disabled,
269
- nested
270
- })]
279
+ children: (0, jsx_runtime_1.jsxs)(components_1.TreeNodeHref, {
280
+ href: href,
281
+ onClick: handleAnchorClick,
282
+ ref: anchorRef,
283
+ children: [typeof title === 'string' && (0, jsx_runtime_1.jsx)(truncate_string_1.TruncateString, {
284
+ text: title,
285
+ "data-test-id": constants_1.TEST_IDS.title
286
+ }), typeof title !== 'string' && title({
287
+ id,
288
+ disabled,
289
+ nested
290
+ })]
291
+ })
271
292
  }), getNodeActions && (0, jsx_runtime_1.jsx)(components_1.TreeNodeActions, {
272
293
  getNodeActions: getNodeActions,
273
294
  node: {
@@ -4,8 +4,8 @@ import { TreeNodeProps } from '../../../types';
4
4
  type TreeNodeActionsProps = {
5
5
  isDroplistOpen: boolean;
6
6
  setDroplistOpen: Dispatch<SetStateAction<boolean>>;
7
- getNodeActions(node: TreeNodeProps): ItemProps[];
8
- node: TreeNodeProps;
7
+ getNodeActions(node: Omit<TreeNodeProps, 'href'>): ItemProps[];
8
+ node: Omit<TreeNodeProps, 'href'>;
9
9
  isDroplistTriggerFocused: boolean;
10
10
  focusNode(): void;
11
11
  onBlurActions(): void;
@@ -0,0 +1,7 @@
1
+ import { MouseEventHandler, ReactNode } from 'react';
2
+ export type TreeNodeHrefProps = {
3
+ href?: string;
4
+ children: ReactNode;
5
+ onClick?: MouseEventHandler<HTMLAnchorElement>;
6
+ };
7
+ export declare const TreeNodeHref: import("react").ForwardRefExoticComponent<TreeNodeHrefProps & import("react").RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ var __importDefault = void 0 && (void 0).__importDefault || function (mod) {
4
+ return mod && mod.__esModule ? mod : {
5
+ "default": mod
6
+ };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", {
9
+ value: true
10
+ });
11
+ exports.TreeNodeHref = void 0;
12
+ const jsx_runtime_1 = require("react/jsx-runtime");
13
+ const react_1 = require("react");
14
+ const constants_1 = require("../../../constants");
15
+ const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
16
+ exports.TreeNodeHref = (0, react_1.forwardRef)((_ref, ref) => {
17
+ let {
18
+ href,
19
+ children,
20
+ onClick
21
+ } = _ref;
22
+ return href ? (0, jsx_runtime_1.jsx)("a", {
23
+ href: href,
24
+ className: styles_module_scss_1.default.treeNodeLink,
25
+ onClick: onClick,
26
+ "data-test-id": constants_1.TEST_IDS.link,
27
+ tabIndex: -2,
28
+ ref: ref,
29
+ children: children
30
+ }) : children;
31
+ });
32
+ exports.TreeNodeHref.displayName = 'TreeNodeHref';
@@ -1 +1,2 @@
1
1
  export * from './TreeNodeActions';
2
+ export * from './TreeNodeHref';
@@ -22,4 +22,5 @@ var __exportStar = void 0 && (void 0).__exportStar || function (m, exports) {
22
22
  Object.defineProperty(exports, "__esModule", {
23
23
  value: true
24
24
  });
25
- __exportStar(require("./TreeNodeActions"), exports);
25
+ __exportStar(require("./TreeNodeActions"), exports);
26
+ __exportStar(require("./TreeNodeHref"), exports);
@@ -124,4 +124,11 @@
124
124
  flex-direction:column;
125
125
  align-self:stretch;
126
126
  min-width:var(--size-button-xs, 24px);
127
+ }
128
+
129
+ .treeNodeLink{
130
+ display:block;
131
+ width:100%;
132
+ color:inherit;
133
+ text-decoration:none;
127
134
  }
@@ -14,6 +14,8 @@ export type BaseTreeNode = WithSupportProps<{
14
14
  /** Обработчик клика по элементу */
15
15
  onClick?: MouseEventHandler;
16
16
  className?: string;
17
+ /** Ссылка на элемент */
18
+ href?: string;
17
19
  }>;
18
20
  export type ChildTreeNode = BaseTreeNode & {
19
21
  /** Иконка дочернего элемента */
@@ -18,4 +18,6 @@ export declare const TEST_IDS: {
18
18
  droplistAction: string;
19
19
  expandable: string;
20
20
  expandableContent: string;
21
+ link: string;
21
22
  };
23
+ export declare const LINK_TEST_HREF = "/snack/?path=/story/welcome--welcome";
@@ -18,4 +18,6 @@ export const TEST_IDS = {
18
18
  droplistAction: 'tree__node__droplist-action',
19
19
  expandable: 'tree__node__expandable',
20
20
  expandableContent: 'tree__node__expandable-content',
21
+ link: 'tree__node__link',
21
22
  };
23
+ export const LINK_TEST_HREF = '/snack/?path=/story/welcome--welcome';
@@ -22,15 +22,16 @@ import { TEST_IDS } from '../../constants';
22
22
  import { useTreeContext } from '../../contexts/TreeContext';
23
23
  import { checkNestedNodesSelection } from '../../helpers';
24
24
  import { TreeLine } from '../TreeLine';
25
- import { TreeNodeActions } from './components';
25
+ import { TreeNodeActions, TreeNodeHref } from './components';
26
26
  import styles from './styles.module.css';
27
27
  import { stopPropagationClick } from './utils';
28
28
  export const TreeNode = forwardRef((_a, ref) => {
29
- var { id, title, icon = _jsx(FileSVG, { size: 24 }), expandedIcon = _jsx(FolderOpenSVG, { size: 24 }), collapsedIcon = _jsx(FolderSVG, { size: 24 }), disabled, onClick, nested, className, onChevronClick, onKeyDown, isLoading, parentNode, tabIndexAvailable } = _a, rest = __rest(_a, ["id", "title", "icon", "expandedIcon", "collapsedIcon", "disabled", "onClick", "nested", "className", "onChevronClick", "onKeyDown", "isLoading", "parentNode", "tabIndexAvailable"]);
29
+ var { id, title, icon = _jsx(FileSVG, { size: 24 }), expandedIcon = _jsx(FolderOpenSVG, { size: 24 }), collapsedIcon = _jsx(FolderSVG, { size: 24 }), disabled, onClick, nested, className, onChevronClick, onKeyDown, isLoading, parentNode, tabIndexAvailable, href } = _a, rest = __rest(_a, ["id", "title", "icon", "expandedIcon", "collapsedIcon", "disabled", "onClick", "nested", "className", "onChevronClick", "onKeyDown", "isLoading", "parentNode", "tabIndexAvailable", "href"]);
30
30
  const { isMultiSelect, isSelectable, onNodeClick, selected, expandedNodes, onSelect, nodeActions, parentActions, setFocusPosition, resetFocusPosition, focusedNodeId, setFocusIndex, focusableNodeIds, showToggle, showLines, showIcons, } = useTreeContext();
31
31
  const [isDroplistOpen, setDroplistOpen] = useState(false);
32
32
  const [isDroplistTriggerFocused, setFocusDroplistTrigger] = useState(false);
33
33
  const contentRef = useRef(null);
34
+ const anchorRef = useRef(null);
34
35
  const isExpandable = Array.isArray(nested);
35
36
  const isExpanded = isExpandable ? expandedNodes === null || expandedNodes === void 0 ? void 0 : expandedNodes.includes(id) : undefined;
36
37
  const nestedNodesSelection = useMemo(() => {
@@ -60,8 +61,19 @@ export const TreeNode = forwardRef((_a, ref) => {
60
61
  disabled,
61
62
  nested,
62
63
  onClick,
64
+ href,
63
65
  }, e);
64
66
  };
67
+ const handleAnchorClick = (e) => {
68
+ e.stopPropagation();
69
+ if ((e === null || e === void 0 ? void 0 : e.metaKey) || (e === null || e === void 0 ? void 0 : e.ctrlKey) || (e === null || e === void 0 ? void 0 : e.button) === 1) {
70
+ return;
71
+ }
72
+ if (onClick) {
73
+ e.preventDefault();
74
+ handleClick(e);
75
+ }
76
+ };
65
77
  const handleSelect = () => {
66
78
  onSelect({
67
79
  id,
@@ -132,6 +144,9 @@ export const TreeNode = forwardRef((_a, ref) => {
132
144
  case 'Enter': {
133
145
  e.preventDefault();
134
146
  handleSelect();
147
+ if (href && anchorRef.current) {
148
+ anchorRef.current.click();
149
+ }
135
150
  return;
136
151
  }
137
152
  default:
@@ -141,7 +156,7 @@ export const TreeNode = forwardRef((_a, ref) => {
141
156
  const getNodeActions = nested ? parentActions : nodeActions;
142
157
  return (_jsxs("div", Object.assign({ role: 'presentation', className: cn(styles.treeNode, className) }, extractSupportProps(rest), { "data-node-id": id, ref: ref, children: [parentNode && (_jsx(TreeLine, { halfWidth: Boolean(nested), horizontal: true, visible: showLines, "data-test-id": TEST_IDS.line })), !parentNode && !nested && _jsx(TreeLine, { visible: false }), isExpandable && (_jsxs("div", { className: styles.treeNodeExpandButtonWrapper, children: [_jsx(ButtonFunction, { size: 'xs', icon: _jsx(ChevronRightSVG, {}), loading: isLoading, onClick: onChevronClick, "data-expanded": isExpanded || undefined, className: styles.treeNodeExpandButton, tabIndex: -1, "data-test-id": TEST_IDS.chevron }), _jsx(TreeLine, { visible: isExpanded && showLines, height: '100%' })] })), _jsxs("div", { role: 'treeitem', "aria-expanded": isExpanded, "aria-selected": isSelectable
143
158
  ? isSelected || ((nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected) && !isExpanded && !isMultiSelect)
144
- : undefined, "aria-disabled": disabled, "data-multiselect": isMultiSelect || undefined, "data-droplist-active": isDroplistOpen || isDroplistTriggerFocused || undefined, onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: resetFocusPosition, tabIndex: tabIndexAvailable ? 0 : -1, className: styles.treeNodeContent, "data-test-id": TEST_IDS.item, ref: contentRef, children: [(isMultiSelect || showToggle) && (_jsxs("div", { className: styles.treeNodeCheckboxWrap, children: [isMultiSelect && (_jsx(Checkbox, { size: 's', disabled: disabled, checked: isSelected, indeterminate: !isSelected && (nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected), onChange: handleSelect, onClick: stopPropagationClick, "data-test-id": TEST_IDS.checkbox, tabIndex: -1 })), showToggle && (_jsx(Radio, { size: 's', checked: isSelected, disabled: disabled, "data-test-id": TEST_IDS.radio, tabIndex: -1 }))] })), treeNodeIcon && (_jsx("div", { className: styles.treeNodeIcon, "data-test-id": TEST_IDS.icon, children: treeNodeIcon })), _jsxs(Typography.SansBodyM, { tag: 'div', className: styles.treeNodeTitle, children: [typeof title === 'string' && _jsx(TruncateString, { text: title, "data-test-id": TEST_IDS.title }), typeof title !== 'string' && title({ id, disabled, nested })] }), getNodeActions && (_jsx(TreeNodeActions, { getNodeActions: getNodeActions, node: {
159
+ : undefined, "aria-disabled": disabled, "data-multiselect": isMultiSelect || undefined, "data-droplist-active": isDroplistOpen || isDroplistTriggerFocused || undefined, onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: resetFocusPosition, tabIndex: tabIndexAvailable ? 0 : -1, className: styles.treeNodeContent, "data-test-id": TEST_IDS.item, ref: contentRef, children: [(isMultiSelect || showToggle) && (_jsxs("div", { className: styles.treeNodeCheckboxWrap, children: [isMultiSelect && (_jsx(Checkbox, { size: 's', disabled: disabled, checked: isSelected, indeterminate: !isSelected && (nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected), onChange: handleSelect, onClick: stopPropagationClick, "data-test-id": TEST_IDS.checkbox, tabIndex: -1 })), showToggle && (_jsx(Radio, { size: 's', checked: isSelected, disabled: disabled, "data-test-id": TEST_IDS.radio, tabIndex: -1 }))] })), treeNodeIcon && (_jsx("div", { className: styles.treeNodeIcon, "data-test-id": TEST_IDS.icon, children: treeNodeIcon })), _jsx(Typography.SansBodyM, { tag: 'div', className: styles.treeNodeTitle, children: _jsxs(TreeNodeHref, { href: href, onClick: handleAnchorClick, ref: anchorRef, children: [typeof title === 'string' && _jsx(TruncateString, { text: title, "data-test-id": TEST_IDS.title }), typeof title !== 'string' && title({ id, disabled, nested })] }) }), getNodeActions && (_jsx(TreeNodeActions, { getNodeActions: getNodeActions, node: {
145
160
  id,
146
161
  title,
147
162
  disabled,
@@ -4,8 +4,8 @@ import { TreeNodeProps } from '../../../types';
4
4
  type TreeNodeActionsProps = {
5
5
  isDroplistOpen: boolean;
6
6
  setDroplistOpen: Dispatch<SetStateAction<boolean>>;
7
- getNodeActions(node: TreeNodeProps): ItemProps[];
8
- node: TreeNodeProps;
7
+ getNodeActions(node: Omit<TreeNodeProps, 'href'>): ItemProps[];
8
+ node: Omit<TreeNodeProps, 'href'>;
9
9
  isDroplistTriggerFocused: boolean;
10
10
  focusNode(): void;
11
11
  onBlurActions(): void;
@@ -0,0 +1,7 @@
1
+ import { MouseEventHandler, ReactNode } from 'react';
2
+ export type TreeNodeHrefProps = {
3
+ href?: string;
4
+ children: ReactNode;
5
+ onClick?: MouseEventHandler<HTMLAnchorElement>;
6
+ };
7
+ export declare const TreeNodeHref: import("react").ForwardRefExoticComponent<TreeNodeHrefProps & import("react").RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { TEST_IDS } from '../../../constants';
4
+ import styles from '../styles.module.css';
5
+ export const TreeNodeHref = forwardRef(({ href, children, onClick }, ref) => href ? (_jsx("a", { href: href, className: styles.treeNodeLink, onClick: onClick, "data-test-id": TEST_IDS.link, tabIndex: -2, ref: ref, children: children })) : (children));
6
+ TreeNodeHref.displayName = 'TreeNodeHref';
@@ -1 +1,2 @@
1
1
  export * from './TreeNodeActions';
2
+ export * from './TreeNodeHref';
@@ -1 +1,2 @@
1
1
  export * from './TreeNodeActions';
2
+ export * from './TreeNodeHref';
@@ -124,4 +124,11 @@
124
124
  flex-direction:column;
125
125
  align-self:stretch;
126
126
  min-width:var(--size-button-xs, 24px);
127
+ }
128
+
129
+ .treeNodeLink{
130
+ display:block;
131
+ width:100%;
132
+ color:inherit;
133
+ text-decoration:none;
127
134
  }
@@ -14,6 +14,8 @@ export type BaseTreeNode = WithSupportProps<{
14
14
  /** Обработчик клика по элементу */
15
15
  onClick?: MouseEventHandler;
16
16
  className?: string;
17
+ /** Ссылка на элемент */
18
+ href?: string;
17
19
  }>;
18
20
  export type ChildTreeNode = BaseTreeNode & {
19
21
  /** Иконка дочернего элемента */
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Tree",
7
- "version": "0.9.28",
7
+ "version": "0.9.30",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -38,14 +38,14 @@
38
38
  "dependencies": {
39
39
  "@snack-uikit/button": "0.19.16",
40
40
  "@snack-uikit/icons": "0.27.3",
41
- "@snack-uikit/list": "0.32.6",
41
+ "@snack-uikit/list": "0.32.7",
42
42
  "@snack-uikit/toggles": "0.13.22",
43
- "@snack-uikit/truncate-string": "0.7.3",
43
+ "@snack-uikit/truncate-string": "0.7.4",
44
44
  "@snack-uikit/typography": "0.8.11",
45
45
  "@snack-uikit/utils": "4.0.0",
46
46
  "classnames": "2.5.1",
47
47
  "react-transition-state": "2.1.1",
48
48
  "uncontrollable": "8.0.4"
49
49
  },
50
- "gitHead": "4b590b25862e58fae2addec0ca0ddbb56d6d39f5"
50
+ "gitHead": "fc6ef99551b60c7b9d2f8cacc87cfb0fd35030bf"
51
51
  }
package/src/constants.ts CHANGED
@@ -20,4 +20,7 @@ export const TEST_IDS = {
20
20
  droplistAction: 'tree__node__droplist-action',
21
21
  expandable: 'tree__node__expandable',
22
22
  expandableContent: 'tree__node__expandable-content',
23
+ link: 'tree__node__link',
23
24
  };
25
+
26
+ export const LINK_TEST_HREF = '/snack/?path=/story/welcome--welcome';
@@ -22,7 +22,7 @@ import { useTreeContext } from '../../contexts/TreeContext';
22
22
  import { checkNestedNodesSelection } from '../../helpers';
23
23
  import { ParentNode, TreeNodeProps } from '../../types';
24
24
  import { TreeLine } from '../TreeLine';
25
- import { TreeNodeActions } from './components';
25
+ import { TreeNodeActions, TreeNodeHref } from './components';
26
26
  import styles from './styles.module.scss';
27
27
  import { stopPropagationClick } from './utils';
28
28
 
@@ -53,6 +53,7 @@ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
53
53
  isLoading,
54
54
  parentNode,
55
55
  tabIndexAvailable,
56
+ href,
56
57
  ...rest
57
58
  },
58
59
  ref,
@@ -81,6 +82,8 @@ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
81
82
 
82
83
  const contentRef = useRef<HTMLDivElement | null>(null);
83
84
 
85
+ const anchorRef = useRef<HTMLAnchorElement | null>(null);
86
+
84
87
  const isExpandable = Array.isArray(nested);
85
88
  const isExpanded = isExpandable ? expandedNodes?.includes(id) : undefined;
86
89
 
@@ -119,11 +122,24 @@ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
119
122
  disabled,
120
123
  nested,
121
124
  onClick,
125
+ href,
122
126
  },
123
127
  e,
124
128
  );
125
129
  };
126
130
 
131
+ const handleAnchorClick = (e: React.MouseEvent<Element>) => {
132
+ e.stopPropagation();
133
+ if (e?.metaKey || e?.ctrlKey || e?.button === 1) {
134
+ return;
135
+ }
136
+
137
+ if (onClick) {
138
+ e.preventDefault();
139
+ handleClick(e);
140
+ }
141
+ };
142
+
127
143
  const handleSelect = () => {
128
144
  onSelect(
129
145
  {
@@ -209,6 +225,11 @@ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
209
225
  case 'Enter': {
210
226
  e.preventDefault();
211
227
  handleSelect();
228
+
229
+ if (href && anchorRef.current) {
230
+ anchorRef.current.click();
231
+ }
232
+
212
233
  return;
213
234
  }
214
235
  default:
@@ -296,8 +317,10 @@ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
296
317
  )}
297
318
 
298
319
  <Typography.SansBodyM tag='div' className={styles.treeNodeTitle}>
299
- {typeof title === 'string' && <TruncateString text={title} data-test-id={TEST_IDS.title} />}
300
- {typeof title !== 'string' && title({ id, disabled, nested } as TreeNodeProps)}
320
+ <TreeNodeHref href={href} onClick={handleAnchorClick} ref={anchorRef}>
321
+ {typeof title === 'string' && <TruncateString text={title} data-test-id={TEST_IDS.title} />}
322
+ {typeof title !== 'string' && title({ id, disabled, nested } as TreeNodeProps)}
323
+ </TreeNodeHref>
301
324
  </Typography.SansBodyM>
302
325
 
303
326
  {getNodeActions && (
@@ -12,8 +12,8 @@ import { stopPropagationClick, stopPropagationFocus } from '../utils';
12
12
  type TreeNodeActionsProps = {
13
13
  isDroplistOpen: boolean;
14
14
  setDroplistOpen: Dispatch<SetStateAction<boolean>>;
15
- getNodeActions(node: TreeNodeProps): ItemProps[];
16
- node: TreeNodeProps;
15
+ getNodeActions(node: Omit<TreeNodeProps, 'href'>): ItemProps[];
16
+ node: Omit<TreeNodeProps, 'href'>;
17
17
  isDroplistTriggerFocused: boolean;
18
18
  focusNode(): void;
19
19
  onBlurActions(): void;
@@ -0,0 +1,29 @@
1
+ import { forwardRef, MouseEventHandler, ReactNode } from 'react';
2
+
3
+ import { TEST_IDS } from '../../../constants';
4
+ import styles from '../styles.module.scss';
5
+
6
+ export type TreeNodeHrefProps = {
7
+ href?: string;
8
+ children: ReactNode;
9
+ onClick?: MouseEventHandler<HTMLAnchorElement>;
10
+ };
11
+
12
+ export const TreeNodeHref = forwardRef<HTMLAnchorElement, TreeNodeHrefProps>(({ href, children, onClick }, ref) =>
13
+ href ? (
14
+ <a
15
+ href={href}
16
+ className={styles.treeNodeLink}
17
+ onClick={onClick}
18
+ data-test-id={TEST_IDS.link}
19
+ tabIndex={-2}
20
+ ref={ref}
21
+ >
22
+ {children}
23
+ </a>
24
+ ) : (
25
+ children
26
+ ),
27
+ );
28
+
29
+ TreeNodeHref.displayName = 'TreeNodeHref';
@@ -1 +1,2 @@
1
1
  export * from './TreeNodeActions';
2
+ export * from './TreeNodeHref';
@@ -157,3 +157,10 @@
157
157
 
158
158
  min-width: tree.$size-button-xs;
159
159
  }
160
+
161
+ .treeNodeLink {
162
+ display: block;
163
+ width: 100%;
164
+ color: inherit;
165
+ text-decoration: none;
166
+ }
package/src/types.ts CHANGED
@@ -19,6 +19,8 @@ export type BaseTreeNode = WithSupportProps<{
19
19
  /** Обработчик клика по элементу */
20
20
  onClick?: MouseEventHandler;
21
21
  className?: string;
22
+ /** Ссылка на элемент */
23
+ href?: string;
22
24
  }>;
23
25
 
24
26
  export type ChildTreeNode = BaseTreeNode & {