@ndla/ui 20.0.0 → 21.0.0

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 (79) hide show
  1. package/README.md +1 -1
  2. package/es/LearningPaths/LearningPathInformation.js +21 -3
  3. package/es/LearningPaths/LearningPathMenu.js +8 -5
  4. package/es/LearningPaths/LearningPathMenuAsideCopyright.js +17 -4
  5. package/es/LearningPaths/LearningPathMenuIntro.js +19 -8
  6. package/es/Masthead/Masthead.js +1 -0
  7. package/es/Messages/MessageBanner.js +3 -3
  8. package/es/Resource/ListResource.js +6 -6
  9. package/es/TopicMenu/TopicMenu.js +14 -1
  10. package/es/TreeStructure/FolderItem.js +72 -51
  11. package/es/TreeStructure/FolderItems.js +33 -61
  12. package/es/TreeStructure/FolderNameInput.js +14 -13
  13. package/es/TreeStructure/TreeStructure.js +80 -96
  14. package/es/TreeStructure/helperFunctions.js +4 -73
  15. package/es/TreeStructure/{TreeStructure.types.js → types.js} +0 -0
  16. package/es/all.css +1 -1
  17. package/es/locale/messages-en.js +6 -1
  18. package/es/locale/messages-nb.js +6 -1
  19. package/es/locale/messages-nn.js +6 -1
  20. package/es/locale/messages-se.js +6 -1
  21. package/es/locale/messages-sma.js +6 -1
  22. package/lib/LearningPaths/LearningPathInformation.js +19 -2
  23. package/lib/LearningPaths/LearningPathMenu.d.ts +2 -1
  24. package/lib/LearningPaths/LearningPathMenu.js +8 -5
  25. package/lib/LearningPaths/LearningPathMenuAsideCopyright.js +16 -3
  26. package/lib/LearningPaths/LearningPathMenuIntro.d.ts +3 -1
  27. package/lib/LearningPaths/LearningPathMenuIntro.js +19 -8
  28. package/lib/Masthead/Masthead.js +1 -0
  29. package/lib/Messages/MessageBanner.js +3 -3
  30. package/lib/Resource/ListResource.js +6 -6
  31. package/lib/TopicMenu/TopicMenu.js +14 -1
  32. package/lib/TreeStructure/FolderItem.d.ts +6 -20
  33. package/lib/TreeStructure/FolderItem.js +74 -51
  34. package/lib/TreeStructure/FolderItems.d.ts +11 -2
  35. package/lib/TreeStructure/FolderItems.js +33 -61
  36. package/lib/TreeStructure/FolderNameInput.js +14 -13
  37. package/lib/TreeStructure/TreeStructure.d.ts +12 -2
  38. package/lib/TreeStructure/TreeStructure.js +78 -94
  39. package/lib/TreeStructure/helperFunctions.d.ts +2 -4
  40. package/lib/TreeStructure/helperFunctions.js +5 -80
  41. package/lib/TreeStructure/index.d.ts +2 -1
  42. package/lib/TreeStructure/types.d.ts +32 -0
  43. package/lib/TreeStructure/{TreeStructure.types.js → types.js} +0 -0
  44. package/lib/all.css +1 -1
  45. package/lib/index.d.ts +1 -1
  46. package/lib/locale/messages-en.d.ts +6 -1
  47. package/lib/locale/messages-en.js +6 -1
  48. package/lib/locale/messages-nb.d.ts +6 -1
  49. package/lib/locale/messages-nb.js +6 -1
  50. package/lib/locale/messages-nn.d.ts +6 -1
  51. package/lib/locale/messages-nn.js +6 -1
  52. package/lib/locale/messages-se.d.ts +6 -1
  53. package/lib/locale/messages-se.js +6 -1
  54. package/lib/locale/messages-sma.d.ts +6 -1
  55. package/lib/locale/messages-sma.js +6 -1
  56. package/package.json +15 -14
  57. package/src/LearningPaths/LearningPathInformation.tsx +27 -12
  58. package/src/LearningPaths/LearningPathMenu.tsx +9 -1
  59. package/src/LearningPaths/LearningPathMenuAsideCopyright.tsx +22 -20
  60. package/src/LearningPaths/LearningPathMenuIntro.tsx +15 -2
  61. package/src/Masthead/Masthead.tsx +4 -1
  62. package/src/Messages/MessageBanner.tsx +1 -1
  63. package/src/Resource/ListResource.tsx +1 -0
  64. package/src/TopicMenu/TopicMenu.jsx +15 -2
  65. package/src/TreeStructure/FolderItem.tsx +59 -67
  66. package/src/TreeStructure/FolderItems.tsx +30 -50
  67. package/src/TreeStructure/FolderNameInput.tsx +6 -11
  68. package/src/TreeStructure/TreeStructure.tsx +73 -71
  69. package/src/TreeStructure/helperFunctions.ts +3 -37
  70. package/src/TreeStructure/index.ts +2 -1
  71. package/src/TreeStructure/types.ts +37 -0
  72. package/src/index.ts +1 -1
  73. package/src/locale/messages-en.ts +6 -1
  74. package/src/locale/messages-nb.ts +7 -1
  75. package/src/locale/messages-nn.ts +6 -1
  76. package/src/locale/messages-se.ts +7 -1
  77. package/src/locale/messages-sma.ts +7 -1
  78. package/lib/TreeStructure/TreeStructure.types.d.ts +0 -61
  79. package/src/TreeStructure/TreeStructure.types.ts +0 -71
@@ -6,13 +6,14 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { KeyboardEvent, useEffect, useRef } from 'react';
9
+ import React, { KeyboardEvent, MouseEvent, useEffect, useRef } from 'react';
10
10
  import styled from '@emotion/styled';
11
11
  import { ArrowDropDown } from '@ndla/icons/common';
12
+ import { MenuButton } from '@ndla/button';
12
13
  import { FolderOutlined } from '@ndla/icons/contentType';
13
14
  import { colors, spacing, misc, animations } from '@ndla/core';
14
15
  import SafeLink from '@ndla/safelink';
15
- import { SetFocusedFolderId, FolderChildFuncType } from './TreeStructure.types';
16
+ import { CommonFolderItemsProps, FolderType } from './types';
16
17
  import { arrowNavigation } from './arrowNavigation';
17
18
 
18
19
  const OpenButton = styled.button<{ isOpen: boolean }>`
@@ -39,10 +40,10 @@ const FolderItemWrapper = styled.div`
39
40
  align-items: center;
40
41
  `;
41
42
 
42
- const WrapperForFolderChild = styled.div<{ marked: boolean }>`
43
+ const WrapperForFolderChild = styled.div<{ selected?: boolean }>`
43
44
  position: absolute;
44
45
  right: ${spacing.xsmall};
45
- opacity: ${({ marked }) => (marked ? 1 : 0.25)};
46
+ opacity: ${({ selected }) => (selected ? 1 : 0.25)};
46
47
  &:hover,
47
48
  &:focus,
48
49
  &:focus-within {
@@ -50,16 +51,20 @@ const WrapperForFolderChild = styled.div<{ marked: boolean }>`
50
51
  }
51
52
  `;
52
53
 
53
- const FolderName = styled('button', { shouldForwardProp: (name) => !['marked', 'noArrow'].includes(name) })<{
54
- marked: boolean;
54
+ const shouldForwardProp = (name: string) => !['selected', 'noArrow'].includes(name);
55
+
56
+ interface FolderNameProps {
57
+ selected?: boolean;
55
58
  noArrow?: boolean;
56
- }>`
59
+ }
60
+
61
+ const FolderName = styled('button', { shouldForwardProp })<FolderNameProps>`
57
62
  line-height: 1;
58
- background: ${({ marked }) => (marked ? colors.brand.lighter : 'transparent')};
63
+ background: ${({ selected }) => (selected ? colors.brand.lighter : 'transparent')};
59
64
  color: ${colors.text.primary};
60
65
  &:hover,
61
66
  &:focus {
62
- background: ${({ marked }) => (marked ? colors.brand.light : colors.brand.lightest)};
67
+ background: ${({ selected }) => (selected ? colors.brand.light : colors.brand.lightest)};
63
68
  color: ${colors.brand.primary};
64
69
  + ${WrapperForFolderChild} {
65
70
  opacity: 1;
@@ -82,52 +87,40 @@ const FolderName = styled('button', { shouldForwardProp: (name) => !['marked', '
82
87
 
83
88
  const FolderNameLink = FolderName.withComponent(SafeLink);
84
89
 
85
- interface Props {
86
- name: string;
87
- id: string;
88
- level: number;
89
- onCloseFolder: (id: string) => void;
90
- onOpenFolder: (id: string) => void;
91
- onMarkFolder: (id: string) => void;
92
- onSelectFolder?: (id: string) => void;
93
- isOpen: boolean;
94
- markedFolderId?: string;
95
- focusedFolderId?: string;
96
- visibleFolders: string[];
97
- loading?: boolean;
98
- openOnFolderClick?: boolean;
90
+ interface Props extends CommonFolderItemsProps {
99
91
  hideArrow?: boolean;
100
- setFocusedFolderId: SetFocusedFolderId;
101
- icon?: React.ReactNode;
92
+ isOpen: boolean;
93
+ folder: FolderType;
102
94
  noPaddingWhenArrowIsHidden?: boolean;
103
- folderChild?: FolderChildFuncType;
104
95
  }
105
96
 
106
97
  const FolderItem = ({
98
+ focusedFolderId,
99
+ menuItems,
107
100
  hideArrow,
108
- loading,
101
+ folder,
102
+ isOpen,
109
103
  level,
110
- name,
111
- id,
112
- visibleFolders,
104
+ loading,
105
+ selectedFolder,
106
+ noPaddingWhenArrowIsHidden,
113
107
  onCloseFolder,
114
108
  onOpenFolder,
115
- onMarkFolder,
116
109
  onSelectFolder,
117
- isOpen,
118
- markedFolderId,
119
- focusedFolderId,
120
110
  openOnFolderClick,
121
- setFocusedFolderId,
122
- icon,
123
- noPaddingWhenArrowIsHidden,
124
- folderChild,
111
+ setFocusedId,
112
+ setSelectedFolder,
113
+ visibleFolders,
125
114
  }: Props) => {
115
+ const { id, icon, name } = folder;
126
116
  const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
127
- const marked = markedFolderId === id;
117
+ const selected = selectedFolder && selectedFolder.id === id;
118
+ const focused = focusedFolderId === id;
128
119
 
129
- const handleMarkFolder = () => {
130
- onMarkFolder(id);
120
+ const handleClickFolder = () => {
121
+ setSelectedFolder(folder);
122
+ setFocusedId(id);
123
+ onSelectFolder?.(id);
131
124
  if (openOnFolderClick) {
132
125
  if (isOpen) {
133
126
  onCloseFolder(id);
@@ -139,12 +132,20 @@ const FolderItem = ({
139
132
 
140
133
  useEffect(() => {
141
134
  if (focusedFolderId === id) {
142
- if (ref.current) {
143
- ref.current.focus();
144
- }
135
+ ref.current?.focus();
145
136
  }
146
137
  }, [focusedFolderId, ref, id]);
147
138
 
139
+ const actions = menuItems?.map((item) => {
140
+ const { onClick } = item;
141
+ return {
142
+ ...item,
143
+ onClick: (e?: MouseEvent<HTMLDivElement>) => onClick(e, folder),
144
+ };
145
+ });
146
+
147
+ const linkPath = `/minndla${level > 1 ? '/folders' : ''}/${id}`;
148
+
148
149
  return (
149
150
  <FolderItemWrapper>
150
151
  {!hideArrow && (
@@ -160,24 +161,19 @@ const FolderItem = ({
160
161
  <>
161
162
  <FolderName
162
163
  ref={ref}
163
- onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)}
164
+ onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)}
164
165
  noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
165
- tabIndex={marked ? 0 : -1}
166
- marked={marked}
166
+ tabIndex={selected || focused ? 0 : -1}
167
+ selected={selected}
167
168
  disabled={loading}
168
- onFocus={() => {
169
- setFocusedFolderId(id);
170
- }}
171
- onClick={() => {
172
- handleMarkFolder();
173
- onSelectFolder(id);
174
- }}>
169
+ onFocus={() => setFocusedId(id)}
170
+ onClick={handleClickFolder}>
175
171
  {icon || <FolderOutlined />}
176
172
  {name}
177
173
  </FolderName>
178
- {folderChild && (
179
- <WrapperForFolderChild marked={marked}>
180
- {folderChild(id, marked || id === focusedFolderId ? 0 : -1)}
174
+ {actions && (
175
+ <WrapperForFolderChild selected={selected}>
176
+ <MenuButton size="xsmall" menuItems={actions} tabIndex={selected || id === focusedFolderId ? 0 : -1} />
181
177
  </WrapperForFolderChild>
182
178
  )}
183
179
  </>
@@ -185,18 +181,14 @@ const FolderItem = ({
185
181
  <FolderNameLink
186
182
  ref={ref}
187
183
  onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
188
- arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)
184
+ arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)
189
185
  }
190
186
  noArrow={hideArrow}
191
- to={loading ? '' : `/minndla/${level > 1 ? 'folders/' : ''}${id}`}
192
- tabIndex={marked || level === 1 ? 0 : -1}
193
- marked={marked}
194
- onFocus={() => {
195
- setFocusedFolderId(id);
196
- }}
197
- onClick={() => {
198
- handleMarkFolder();
199
- }}>
187
+ to={loading ? '' : linkPath}
188
+ tabIndex={selected || focused || level === 1 ? 0 : -1}
189
+ selected={selected}
190
+ onFocus={() => setFocusedId(id)}
191
+ onClick={handleClickFolder}>
200
192
  {icon || <FolderOutlined />}
201
193
  {name}
202
194
  </FolderNameLink>
@@ -11,7 +11,7 @@ import styled from '@emotion/styled';
11
11
  import { animations, spacing } from '@ndla/core';
12
12
  import FolderItem from './FolderItem';
13
13
  import FolderNameInput from './FolderNameInput';
14
- import { FolderItemsProps } from './TreeStructure.types';
14
+ import { CommonFolderItemsProps, FolderType } from './types';
15
15
 
16
16
  const StyledUL = styled.ul<{ firstLevel?: boolean }>`
17
17
  ${animations.fadeInLeft(animations.durations.fast)};
@@ -30,85 +30,65 @@ const StyledLI = styled.li`
30
30
  padding: 0;
31
31
  `;
32
32
 
33
+ export interface FolderItemsProps extends CommonFolderItemsProps {
34
+ folders: FolderType[];
35
+ editable?: boolean;
36
+ maximumLevelsOfFoldersAllowed: number;
37
+ newFolderParentId: string | undefined;
38
+ onCancelNewFolder: () => void;
39
+ onSaveNewFolder: (name: string, parentId: string) => void;
40
+ openFolders: string[];
41
+ }
42
+
33
43
  const FolderItems = ({
34
- loading,
44
+ editable,
35
45
  folders,
36
46
  level,
37
- editable,
38
- onSelectFolder,
39
- onCloseFolder,
40
- onOpenFolder,
41
- onCreateNewFolder,
47
+ loading,
48
+ maximumLevelsOfFoldersAllowed,
49
+ newFolderParentId,
42
50
  onCancelNewFolder,
43
51
  onSaveNewFolder,
44
- newFolderParentId,
45
- visibleFolders,
46
52
  openFolders,
47
- markedFolderId,
48
- onMarkFolder,
49
- openOnFolderClick,
50
- focusedFolderId,
51
- setFocusedFolderId,
52
- folderChild,
53
- maximumLevelsOfFoldersAllowed,
53
+ ...rest
54
54
  }: FolderItemsProps) => (
55
55
  <StyledUL role="group" firstLevel={level === 1}>
56
- {folders.map(({ name, subfolders, id, icon }, _index) => {
56
+ {folders.map((folder) => {
57
+ const { subfolders, id } = folder;
57
58
  const isOpen = openFolders?.includes(id);
58
59
  return (
59
60
  <StyledLI key={id} role="treeitem">
60
61
  <div>
61
62
  <FolderItem
63
+ hideArrow={subfolders?.length === 0 || level > maximumLevelsOfFoldersAllowed}
64
+ folder={folder}
65
+ isOpen={isOpen}
62
66
  level={level}
63
- icon={icon}
64
- onSelectFolder={onSelectFolder}
65
- openOnFolderClick={openOnFolderClick}
66
67
  loading={loading}
67
- isOpen={isOpen}
68
- id={id}
69
- visibleFolders={visibleFolders}
70
- name={name}
71
- markedFolderId={markedFolderId}
72
- focusedFolderId={focusedFolderId}
73
- onMarkFolder={onMarkFolder}
74
- onCloseFolder={onCloseFolder}
75
- onOpenFolder={onOpenFolder}
76
- hideArrow={subfolders?.length === 0 || level > maximumLevelsOfFoldersAllowed}
77
68
  noPaddingWhenArrowIsHidden={editable && level === 1 && subfolders?.length === 0}
78
- setFocusedFolderId={setFocusedFolderId}
79
- folderChild={folderChild}
69
+ {...rest}
80
70
  />
81
71
  </div>
82
72
  {newFolderParentId === id && (
83
73
  <FolderNameInput
84
- parentId={newFolderParentId}
85
74
  loading={loading}
86
75
  onCancelNewFolder={onCancelNewFolder}
87
76
  onSaveNewFolder={onSaveNewFolder}
77
+ parentId={newFolderParentId}
88
78
  />
89
79
  )}
90
80
  {subfolders && isOpen && (
91
81
  <FolderItems
92
- onSelectFolder={onSelectFolder}
93
- loading={loading}
94
- newFolderParentId={newFolderParentId}
95
- visibleFolders={visibleFolders}
96
- openFolders={openFolders}
97
- level={level + 1}
98
82
  editable={editable}
99
83
  folders={subfolders}
100
- onCloseFolder={onCloseFolder}
101
- onOpenFolder={onOpenFolder}
102
- onCreateNewFolder={onCreateNewFolder}
103
- onSaveNewFolder={onSaveNewFolder}
104
- onCancelNewFolder={onCancelNewFolder}
105
- markedFolderId={markedFolderId}
106
- onMarkFolder={onMarkFolder}
107
- openOnFolderClick={openOnFolderClick}
108
- focusedFolderId={focusedFolderId}
109
- setFocusedFolderId={setFocusedFolderId}
110
- folderChild={folderChild}
84
+ level={level + 1}
85
+ loading={loading}
111
86
  maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
87
+ newFolderParentId={newFolderParentId}
88
+ onCancelNewFolder={onCancelNewFolder}
89
+ onSaveNewFolder={onSaveNewFolder}
90
+ openFolders={openFolders}
91
+ {...rest}
112
92
  />
113
93
  )}
114
94
  </StyledLI>
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { useEffect, useState, useRef } from 'react';
9
+ import React, { useEffect, useState, useRef, ChangeEvent, KeyboardEvent } from 'react';
10
10
  import styled from '@emotion/styled';
11
11
  import { FolderOutlined } from '@ndla/icons/contentType';
12
12
  import { ArrowDropDown as ArrowDropDownRaw } from '@ndla/icons/common';
@@ -65,11 +65,9 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
65
65
  const inputRef = useRef<HTMLInputElement>(null);
66
66
 
67
67
  useEffect(() => {
68
- if (inputRef.current) {
69
- inputRef.current.select();
70
- if (isMobile) {
71
- inputRef.current.scrollIntoView({ behavior: 'smooth' });
72
- }
68
+ inputRef.current?.select();
69
+ if (isMobile) {
70
+ inputRef.current?.scrollIntoView({ behavior: 'smooth' });
73
71
  }
74
72
  }, []);
75
73
 
@@ -85,7 +83,7 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
85
83
  disabled={loading}
86
84
  value={name}
87
85
  onBlur={() => onCancelNewFolder()}
88
- onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
86
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
89
87
  if (e.key === 'Escape') {
90
88
  onCancelNewFolder();
91
89
  } else if (e.key === 'Enter' || e.key === 'Tab') {
@@ -93,10 +91,7 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
93
91
  onSaveNewFolder(name, parentId);
94
92
  }
95
93
  }}
96
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
97
- const target = e.target;
98
- setName(target.value);
99
- }}
94
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
100
95
  />
101
96
  {loading && <Spinner size="small" />}
102
97
  </InputWrapper>
@@ -6,17 +6,18 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { useEffect, useState, useRef, useMemo } from 'react';
9
+ import React, { useEffect, useState, useMemo } from 'react';
10
10
  import { AddButton } from '@ndla/button';
11
11
  import Tooltip from '@ndla/tooltip';
12
12
  import { useTranslation } from 'react-i18next';
13
13
  import styled from '@emotion/styled';
14
14
  import { spacing, fonts } from '@ndla/core';
15
15
  import { uniq } from 'lodash';
16
+ import { IFolder } from '@ndla/types-learningpath-api';
16
17
  import TreeStructureStyledWrapper from './TreeStructureWrapper';
17
18
  import FolderItems from './FolderItems';
18
- import { getPathOfFolder, getFolderName, flattenFolders } from './helperFunctions';
19
- import { TreeStructureProps } from './TreeStructure.types';
19
+ import { flattenFolders } from './helperFunctions';
20
+ import { CommonTreeStructureProps, FolderType } from './types';
20
21
 
21
22
  export const MAX_LEVEL_FOR_FOLDERS = 4;
22
23
 
@@ -29,42 +30,60 @@ const AddFolderWrapper = styled.div`
29
30
  margin-top: ${spacing.xsmall};
30
31
  `;
31
32
 
33
+ export interface TreeStructureProps extends CommonTreeStructureProps {
34
+ defaultOpenFolders?: string[];
35
+ folders: FolderType[];
36
+ editable?: boolean;
37
+ framed?: boolean;
38
+ label?: string;
39
+ maximumLevelsOfFoldersAllowed?: number;
40
+ onNewFolder: (name: string, parentId: string) => Promise<IFolder>;
41
+ }
42
+
32
43
  const TreeStructure = ({
44
+ defaultOpenFolders,
45
+ editable,
46
+ menuItems,
33
47
  folders,
48
+ framed,
34
49
  label,
35
- editable,
36
50
  loading,
51
+ maximumLevelsOfFoldersAllowed = MAX_LEVEL_FOR_FOLDERS,
37
52
  onNewFolder,
38
53
  onSelectFolder,
39
54
  openOnFolderClick,
40
- framed,
41
- folderIdMarkedByDefault,
42
- defaultOpenFolders,
43
- folderChild,
44
- maximumLevelsOfFoldersAllowed = MAX_LEVEL_FOR_FOLDERS,
45
55
  }: TreeStructureProps) => {
46
56
  const { t } = useTranslation();
47
- const [newFolderParentId, setNewFolderParentId] = useState<string | undefined>();
57
+
58
+ const defaultSelectedFolderId = defaultOpenFolders && defaultOpenFolders[defaultOpenFolders.length - 1];
59
+
48
60
  const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
49
- const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
50
- const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || folders[0]?.id);
51
- const treestructureRef = useRef<HTMLDivElement>(null);
52
- const wrapperRef = useRef<HTMLDivElement>(null);
53
- const rootLevelId = 'treestructure-root';
54
-
55
- const visibleFolders = useMemo(
56
- () => flattenFolders(folders, openFolders).map((folder) => folder.id),
57
- [folders, openFolders],
58
- );
61
+
62
+ const [newFolderParentId, setNewFolderParentId] = useState<string | undefined>();
63
+ const [focusedId, setFocusedId] = useState<string | undefined>();
64
+ const [selectedFolder, setSelectedFolder] = useState<FolderType | undefined>();
65
+
66
+ const flattenedFolders = useMemo(() => flattenFolders(folders, openFolders), [folders, openFolders]);
67
+ const visibleFolderIds = flattenedFolders.map((folder) => folder.id);
59
68
 
60
69
  useEffect(() => {
61
70
  if (defaultOpenFolders) {
62
71
  setOpenFolders((prev) => {
63
- return uniq([...defaultOpenFolders, ...prev]);
72
+ return uniq(defaultOpenFolders.concat(prev));
64
73
  });
65
74
  }
66
75
  }, [defaultOpenFolders]);
67
76
 
77
+ useEffect(() => {
78
+ if (defaultSelectedFolderId !== undefined) {
79
+ const selected = flattenFolders(folders).find((folder) => folder.id === defaultSelectedFolderId);
80
+ if (selected) {
81
+ setSelectedFolder(selected);
82
+ }
83
+ }
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
+ }, [defaultSelectedFolderId]);
86
+
68
87
  useEffect(() => {
69
88
  if (!loading) {
70
89
  setNewFolderParentId(undefined);
@@ -72,39 +91,30 @@ const TreeStructure = ({
72
91
  }, [loading]);
73
92
 
74
93
  const onCloseFolder = (id: string) => {
75
- // Did we just closed a folder with a marked folder inside it?
76
- // If so, we need to mark the folder we just closed.
77
- if (markedFolderId) {
78
- const closingFolderPath = getPathOfFolder(folders, id);
79
- const markedFolderPath = getPathOfFolder(folders, markedFolderId);
80
- const markedFolderIsSubPath = closingFolderPath.every(
81
- (folderId, _index) => markedFolderPath[_index] === folderId,
82
- );
83
- if (markedFolderIsSubPath) {
94
+ const closedFolder = flattenedFolders.find((folder) => folder.id === id);
95
+
96
+ if (closedFolder) {
97
+ const subFolders = closedFolder.subfolders && flattenFolders(closedFolder.subfolders);
98
+ if (subFolders.some((folder) => folder.id === selectedFolder?.id)) {
84
99
  if (onSelectFolder) {
85
- setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
86
- onSelectFolder(closingFolderPath[closingFolderPath.length - 1]);
87
- } else {
88
- setFocusedFolderId(closingFolderPath[closingFolderPath.length - 1]);
100
+ setSelectedFolder(closedFolder);
101
+ onSelectFolder(closedFolder.id);
89
102
  }
103
+ setFocusedId(closedFolder.id);
90
104
  }
91
105
  }
92
- setOpenFolders(openFolders.filter((folder) => folder !== id));
106
+ setOpenFolders(openFolders.filter((folderId) => folderId !== id));
93
107
  };
94
108
 
95
109
  const onOpenFolder = (id: string) => {
96
110
  setOpenFolders(uniq(openFolders.concat(id)));
97
111
  };
98
112
 
99
- const onCreateNewFolder = (parentId: string) => {
100
- setNewFolderParentId(parentId);
101
- };
102
-
103
113
  const onSaveNewFolder = (name: string, parentId: string) => {
104
- onNewFolder(name, parentId).then((newFolderId) => {
105
- if (newFolderId) {
106
- setMarkedFolderId(newFolderId);
107
- setFocusedFolderId(newFolderId);
114
+ onNewFolder(name, parentId).then((newFolder) => {
115
+ if (newFolder) {
116
+ setSelectedFolder(newFolder);
117
+ setFocusedId(newFolder.id);
108
118
  setOpenFolders(uniq(openFolders.concat(parentId)));
109
119
  }
110
120
  });
@@ -114,39 +124,33 @@ const TreeStructure = ({
114
124
  setNewFolderParentId(undefined);
115
125
  };
116
126
 
117
- const onMarkFolder = (id: string) => {
118
- setMarkedFolderId(id);
119
- setFocusedFolderId(id);
120
- };
121
-
122
- const paths = getPathOfFolder(folders, markedFolderId || '');
123
- const canAddFolder = editable && paths.length < (maximumLevelsOfFoldersAllowed || 1);
127
+ const canAddFolder =
128
+ editable && selectedFolder && selectedFolder?.breadcrumbs.length < (maximumLevelsOfFoldersAllowed || 1);
124
129
 
125
130
  return (
126
- <div ref={treestructureRef}>
127
- {label && <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>}
128
- <TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
131
+ <div>
132
+ {label && <StyledLabel>{label}</StyledLabel>}
133
+ <TreeStructureStyledWrapper aria-label="Menu tree" role="tree" framed={framed}>
129
134
  <FolderItems
130
- onSelectFolder={onSelectFolder}
131
- level={1}
132
- folders={folders}
133
135
  editable={editable}
134
- onOpenFolder={onOpenFolder}
135
- onCloseFolder={onCloseFolder}
136
+ focusedFolderId={focusedId}
137
+ menuItems={menuItems}
138
+ folders={folders}
139
+ level={1}
140
+ loading={loading}
141
+ selectedFolder={selectedFolder}
142
+ maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
136
143
  newFolderParentId={newFolderParentId}
137
- onCreateNewFolder={onCreateNewFolder}
138
144
  onCancelNewFolder={onCancelNewFolder}
145
+ onCloseFolder={onCloseFolder}
146
+ onOpenFolder={onOpenFolder}
139
147
  onSaveNewFolder={onSaveNewFolder}
140
- visibleFolders={visibleFolders}
148
+ onSelectFolder={onSelectFolder}
141
149
  openFolders={openFolders}
142
- markedFolderId={markedFolderId}
143
- onMarkFolder={onMarkFolder}
144
150
  openOnFolderClick={openOnFolderClick}
145
- loading={loading}
146
- focusedFolderId={focusedFolderId}
147
- setFocusedFolderId={setFocusedFolderId}
148
- folderChild={folderChild}
149
- maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
151
+ setFocusedId={setFocusedId}
152
+ setSelectedFolder={setSelectedFolder}
153
+ visibleFolders={visibleFolderIds}
150
154
  />
151
155
  </TreeStructureStyledWrapper>
152
156
  {editable && (
@@ -155,16 +159,14 @@ const TreeStructure = ({
155
159
  tooltip={
156
160
  canAddFolder
157
161
  ? t('myNdla.newFolderUnder', {
158
- folderName: getFolderName(folders, markedFolderId),
162
+ folderName: selectedFolder?.name,
159
163
  })
160
164
  : t('treeStructure.maxFoldersAlreadyAdded')
161
165
  }>
162
166
  <AddButton
163
167
  disabled={!canAddFolder}
164
168
  aria-label={t('myNdla.newFolder')}
165
- onClick={() => {
166
- setNewFolderParentId(markedFolderId);
167
- }}>
169
+ onClick={() => setNewFolderParentId(selectedFolder?.id)}>
168
170
  {t('myNdla.newFolder')}
169
171
  </AddButton>
170
172
  </Tooltip>
@@ -1,44 +1,10 @@
1
- import { FolderStructureProps } from './TreeStructure.types';
1
+ import { FolderType } from './types';
2
2
 
3
- export const getPathOfFolder = (data: FolderStructureProps[], findId: string): string[] => {
4
- const paths = (folders: FolderStructureProps[], path: string[]): string[] => {
5
- for (const { id, subfolders } of folders) {
6
- if (id === findId) {
7
- return [...path, id];
8
- } else if (subfolders?.length) {
9
- return paths(subfolders, [...path, id]);
10
- }
11
- }
12
- return [];
13
- };
14
- return paths(data, []);
15
- };
16
-
17
- export const getFolderName = (data: FolderStructureProps[], findId: string | undefined): string | undefined => {
18
- if (!findId) {
19
- return undefined;
20
- }
21
- let folderName: string | undefined;
22
- const paths = (dataChildren: FolderStructureProps[]) => {
23
- dataChildren.some(({ id, name, subfolders }, _index) => {
24
- if (id === findId) {
25
- folderName = name;
26
- return true;
27
- } else if (subfolders?.length) {
28
- return paths(subfolders);
29
- }
30
- return false;
31
- });
32
- };
33
- paths(data);
34
- return folderName;
35
- };
36
-
37
- export const flattenFolders = (folders: FolderStructureProps[], openFolders?: string[]): FolderStructureProps[] => {
3
+ export const flattenFolders = (folders: FolderType[], openFolders?: string[]): FolderType[] => {
38
4
  return folders.reduce((acc, { subfolders, id, ...rest }) => {
39
5
  if (!subfolders || (openFolders && !openFolders.includes(id))) {
40
6
  return acc.concat({ subfolders, id, ...rest });
41
7
  }
42
8
  return acc.concat({ subfolders, id, ...rest }, flattenFolders(subfolders, openFolders));
43
- }, [] as FolderStructureProps[]);
9
+ }, [] as FolderType[]);
44
10
  };
@@ -7,5 +7,6 @@
7
7
  */
8
8
 
9
9
  import TreeStructure from './TreeStructure';
10
- export type { FolderStructureProps, TreeStructureProps } from './TreeStructure.types';
10
+ export type { FolderType, TreeStructureMenuProps } from './types';
11
+ export type { TreeStructureProps } from './TreeStructure';
11
12
  export { TreeStructure };