@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.
- package/README.md +1 -1
- package/es/LearningPaths/LearningPathInformation.js +21 -3
- package/es/LearningPaths/LearningPathMenu.js +8 -5
- package/es/LearningPaths/LearningPathMenuAsideCopyright.js +17 -4
- package/es/LearningPaths/LearningPathMenuIntro.js +19 -8
- package/es/Masthead/Masthead.js +1 -0
- package/es/Messages/MessageBanner.js +3 -3
- package/es/Resource/ListResource.js +6 -6
- package/es/TopicMenu/TopicMenu.js +14 -1
- package/es/TreeStructure/FolderItem.js +72 -51
- package/es/TreeStructure/FolderItems.js +33 -61
- package/es/TreeStructure/FolderNameInput.js +14 -13
- package/es/TreeStructure/TreeStructure.js +80 -96
- package/es/TreeStructure/helperFunctions.js +4 -73
- package/es/TreeStructure/{TreeStructure.types.js → types.js} +0 -0
- package/es/all.css +1 -1
- package/es/locale/messages-en.js +6 -1
- package/es/locale/messages-nb.js +6 -1
- package/es/locale/messages-nn.js +6 -1
- package/es/locale/messages-se.js +6 -1
- package/es/locale/messages-sma.js +6 -1
- package/lib/LearningPaths/LearningPathInformation.js +19 -2
- package/lib/LearningPaths/LearningPathMenu.d.ts +2 -1
- package/lib/LearningPaths/LearningPathMenu.js +8 -5
- package/lib/LearningPaths/LearningPathMenuAsideCopyright.js +16 -3
- package/lib/LearningPaths/LearningPathMenuIntro.d.ts +3 -1
- package/lib/LearningPaths/LearningPathMenuIntro.js +19 -8
- package/lib/Masthead/Masthead.js +1 -0
- package/lib/Messages/MessageBanner.js +3 -3
- package/lib/Resource/ListResource.js +6 -6
- package/lib/TopicMenu/TopicMenu.js +14 -1
- package/lib/TreeStructure/FolderItem.d.ts +6 -20
- package/lib/TreeStructure/FolderItem.js +74 -51
- package/lib/TreeStructure/FolderItems.d.ts +11 -2
- package/lib/TreeStructure/FolderItems.js +33 -61
- package/lib/TreeStructure/FolderNameInput.js +14 -13
- package/lib/TreeStructure/TreeStructure.d.ts +12 -2
- package/lib/TreeStructure/TreeStructure.js +78 -94
- package/lib/TreeStructure/helperFunctions.d.ts +2 -4
- package/lib/TreeStructure/helperFunctions.js +5 -80
- package/lib/TreeStructure/index.d.ts +2 -1
- package/lib/TreeStructure/types.d.ts +32 -0
- package/lib/TreeStructure/{TreeStructure.types.js → types.js} +0 -0
- package/lib/all.css +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/locale/messages-en.d.ts +6 -1
- package/lib/locale/messages-en.js +6 -1
- package/lib/locale/messages-nb.d.ts +6 -1
- package/lib/locale/messages-nb.js +6 -1
- package/lib/locale/messages-nn.d.ts +6 -1
- package/lib/locale/messages-nn.js +6 -1
- package/lib/locale/messages-se.d.ts +6 -1
- package/lib/locale/messages-se.js +6 -1
- package/lib/locale/messages-sma.d.ts +6 -1
- package/lib/locale/messages-sma.js +6 -1
- package/package.json +15 -14
- package/src/LearningPaths/LearningPathInformation.tsx +27 -12
- package/src/LearningPaths/LearningPathMenu.tsx +9 -1
- package/src/LearningPaths/LearningPathMenuAsideCopyright.tsx +22 -20
- package/src/LearningPaths/LearningPathMenuIntro.tsx +15 -2
- package/src/Masthead/Masthead.tsx +4 -1
- package/src/Messages/MessageBanner.tsx +1 -1
- package/src/Resource/ListResource.tsx +1 -0
- package/src/TopicMenu/TopicMenu.jsx +15 -2
- package/src/TreeStructure/FolderItem.tsx +59 -67
- package/src/TreeStructure/FolderItems.tsx +30 -50
- package/src/TreeStructure/FolderNameInput.tsx +6 -11
- package/src/TreeStructure/TreeStructure.tsx +73 -71
- package/src/TreeStructure/helperFunctions.ts +3 -37
- package/src/TreeStructure/index.ts +2 -1
- package/src/TreeStructure/types.ts +37 -0
- package/src/index.ts +1 -1
- package/src/locale/messages-en.ts +6 -1
- package/src/locale/messages-nb.ts +7 -1
- package/src/locale/messages-nn.ts +6 -1
- package/src/locale/messages-se.ts +7 -1
- package/src/locale/messages-sma.ts +7 -1
- package/lib/TreeStructure/TreeStructure.types.d.ts +0 -61
- 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 {
|
|
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<{
|
|
43
|
+
const WrapperForFolderChild = styled.div<{ selected?: boolean }>`
|
|
43
44
|
position: absolute;
|
|
44
45
|
right: ${spacing.xsmall};
|
|
45
|
-
opacity: ${({
|
|
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
|
|
54
|
-
|
|
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: ${({
|
|
63
|
+
background: ${({ selected }) => (selected ? colors.brand.lighter : 'transparent')};
|
|
59
64
|
color: ${colors.text.primary};
|
|
60
65
|
&:hover,
|
|
61
66
|
&:focus {
|
|
62
|
-
background: ${({
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
101
|
+
folder,
|
|
102
|
+
isOpen,
|
|
109
103
|
level,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
117
|
+
const selected = selectedFolder && selectedFolder.id === id;
|
|
118
|
+
const focused = focusedFolderId === id;
|
|
128
119
|
|
|
129
|
-
const
|
|
130
|
-
|
|
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
|
-
|
|
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,
|
|
164
|
+
onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)}
|
|
164
165
|
noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
|
|
165
|
-
tabIndex={
|
|
166
|
-
|
|
166
|
+
tabIndex={selected || focused ? 0 : -1}
|
|
167
|
+
selected={selected}
|
|
167
168
|
disabled={loading}
|
|
168
|
-
onFocus={() =>
|
|
169
|
-
|
|
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
|
-
{
|
|
179
|
-
<WrapperForFolderChild
|
|
180
|
-
{
|
|
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,
|
|
184
|
+
arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)
|
|
189
185
|
}
|
|
190
186
|
noArrow={hideArrow}
|
|
191
|
-
to={loading ? '' :
|
|
192
|
-
tabIndex={
|
|
193
|
-
|
|
194
|
-
onFocus={() =>
|
|
195
|
-
|
|
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 {
|
|
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
|
-
|
|
44
|
+
editable,
|
|
35
45
|
folders,
|
|
36
46
|
level,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
onOpenFolder,
|
|
41
|
-
onCreateNewFolder,
|
|
47
|
+
loading,
|
|
48
|
+
maximumLevelsOfFoldersAllowed,
|
|
49
|
+
newFolderParentId,
|
|
42
50
|
onCancelNewFolder,
|
|
43
51
|
onSaveNewFolder,
|
|
44
|
-
newFolderParentId,
|
|
45
|
-
visibleFolders,
|
|
46
52
|
openFolders,
|
|
47
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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 {
|
|
19
|
-
import {
|
|
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
|
-
|
|
57
|
+
|
|
58
|
+
const defaultSelectedFolderId = defaultOpenFolders && defaultOpenFolders[defaultOpenFolders.length - 1];
|
|
59
|
+
|
|
48
60
|
const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
|
|
49
|
-
|
|
50
|
-
const [
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
-
|
|
86
|
-
onSelectFolder(
|
|
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((
|
|
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((
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
118
|
-
|
|
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
|
|
127
|
-
{label && <StyledLabel
|
|
128
|
-
<TreeStructureStyledWrapper
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
148
|
+
onSelectFolder={onSelectFolder}
|
|
141
149
|
openFolders={openFolders}
|
|
142
|
-
markedFolderId={markedFolderId}
|
|
143
|
-
onMarkFolder={onMarkFolder}
|
|
144
150
|
openOnFolderClick={openOnFolderClick}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
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 {
|
|
1
|
+
import { FolderType } from './types';
|
|
2
2
|
|
|
3
|
-
export const
|
|
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
|
|
9
|
+
}, [] as FolderType[]);
|
|
44
10
|
};
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import TreeStructure from './TreeStructure';
|
|
10
|
-
export type {
|
|
10
|
+
export type { FolderType, TreeStructureMenuProps } from './types';
|
|
11
|
+
export type { TreeStructureProps } from './TreeStructure';
|
|
11
12
|
export { TreeStructure };
|