@ndla/ui 19.1.1 → 20.0.2
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/Masthead/Masthead.js +1 -0
- package/es/Messages/MessageBanner.js +3 -3
- package/es/MyNdla/Resource/FolderInput.js +29 -36
- package/es/NDLAFilm/FilmSlideshow.js +8 -8
- package/es/Resource/ListResource.js +6 -6
- package/es/Search/LoadingWrapper.js +2 -2
- package/es/Search/SearchResult.js +1 -1
- package/es/SearchTypeResult/SearchTypeResult.js +3 -3
- package/es/TopicMenu/TopicMenu.js +14 -1
- package/es/TreeStructure/FolderItem.js +54 -38
- package/es/TreeStructure/FolderItems.js +29 -35
- package/es/TreeStructure/FolderNameInput.js +12 -16
- package/es/TreeStructure/TreeStructure.js +64 -91
- package/es/TreeStructure/arrowNavigation.js +44 -0
- package/es/TreeStructure/helperFunctions.js +41 -35
- package/es/all.css +1 -1
- package/es/index.js +0 -1
- package/es/locale/messages-en.js +4 -1
- package/es/locale/messages-nb.js +4 -1
- package/es/locale/messages-nn.js +4 -1
- package/es/locale/messages-se.js +4 -1
- package/es/locale/messages-sma.js +4 -1
- package/lib/Masthead/Masthead.js +1 -0
- package/lib/Messages/MessageBanner.js +3 -3
- package/lib/MyNdla/Resource/FolderInput.d.ts +2 -2
- package/lib/MyNdla/Resource/FolderInput.js +26 -33
- package/lib/NDLAFilm/FilmSlideshow.js +10 -10
- package/lib/Resource/ListResource.js +6 -6
- package/lib/Search/LoadingWrapper.js +3 -3
- package/lib/Search/SearchResult.js +2 -2
- package/lib/SearchTypeResult/SearchTypeResult.js +4 -4
- package/lib/TopicMenu/TopicMenu.js +14 -1
- package/lib/TreeStructure/FolderItem.d.ts +6 -3
- package/lib/TreeStructure/FolderItem.js +55 -38
- package/lib/TreeStructure/FolderItems.d.ts +1 -1
- package/lib/TreeStructure/FolderItems.js +29 -35
- package/lib/TreeStructure/FolderNameInput.d.ts +3 -2
- package/lib/TreeStructure/FolderNameInput.js +13 -17
- package/lib/TreeStructure/TreeStructure.d.ts +1 -6
- package/lib/TreeStructure/TreeStructure.js +63 -92
- package/lib/TreeStructure/TreeStructure.types.d.ts +13 -20
- package/lib/TreeStructure/arrowNavigation.d.ts +9 -0
- package/lib/TreeStructure/arrowNavigation.js +54 -0
- package/lib/TreeStructure/helperFunctions.d.ts +3 -4
- package/lib/TreeStructure/helperFunctions.js +45 -35
- package/lib/all.css +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -9
- package/lib/locale/messages-en.d.ts +3 -0
- package/lib/locale/messages-en.js +4 -1
- package/lib/locale/messages-nb.d.ts +3 -0
- package/lib/locale/messages-nb.js +4 -1
- package/lib/locale/messages-nn.d.ts +3 -0
- package/lib/locale/messages-nn.js +4 -1
- package/lib/locale/messages-se.d.ts +3 -0
- package/lib/locale/messages-se.js +4 -1
- package/lib/locale/messages-sma.d.ts +3 -0
- package/lib/locale/messages-sma.js +4 -1
- package/package.json +14 -13
- package/src/Masthead/Masthead.tsx +4 -1
- package/src/Messages/MessageBanner.tsx +1 -1
- package/src/MyNdla/Resource/FolderInput.tsx +41 -44
- package/src/NDLAFilm/FilmSlideshow.tsx +1 -1
- package/src/Resource/ListResource.tsx +1 -0
- package/src/Search/LoadingWrapper.tsx +1 -1
- package/src/Search/SearchResult.jsx +1 -1
- package/src/SearchTypeResult/SearchTypeResult.tsx +1 -1
- package/src/TopicMenu/TopicMenu.jsx +15 -2
- package/src/TreeStructure/FolderItem.tsx +63 -40
- package/src/TreeStructure/FolderItems.tsx +26 -19
- package/src/TreeStructure/FolderNameInput.tsx +10 -12
- package/src/TreeStructure/TreeStructure.tsx +56 -71
- package/src/TreeStructure/TreeStructure.types.ts +13 -17
- package/src/TreeStructure/arrowNavigation.ts +53 -0
- package/src/TreeStructure/helperFunctions.ts +17 -25
- package/src/index.ts +0 -2
- package/src/locale/messages-en.ts +3 -0
- package/src/locale/messages-nb.ts +3 -0
- package/src/locale/messages-nn.ts +3 -0
- package/src/locale/messages-se.ts +3 -0
- package/src/locale/messages-sma.ts +3 -0
- package/es/Spinner/Spinner.js +0 -42
- package/es/Spinner/index.js +0 -2
- package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +0 -194
- package/es/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -0
- package/lib/Spinner/Spinner.d.ts +0 -16
- package/lib/Spinner/Spinner.js +0 -54
- package/lib/Spinner/index.d.ts +0 -2
- package/lib/Spinner/index.js +0 -13
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.d.ts +0 -11
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +0 -198
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +0 -26
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -1
- package/src/Spinner/Spinner.tsx +0 -46
- package/src/Spinner/index.ts +0 -3
- package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +0 -161
- package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +0 -28
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect, useRef } from 'react';
|
|
9
|
+
import React, { KeyboardEvent, useEffect, useRef } from 'react';
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
11
|
import { ArrowDropDown } from '@ndla/icons/common';
|
|
12
12
|
import { FolderOutlined } from '@ndla/icons/contentType';
|
|
13
13
|
import { colors, spacing, misc, animations } from '@ndla/core';
|
|
14
14
|
import SafeLink from '@ndla/safelink';
|
|
15
15
|
import { SetFocusedFolderId, FolderChildFuncType } from './TreeStructure.types';
|
|
16
|
+
import { arrowNavigation } from './arrowNavigation';
|
|
16
17
|
|
|
17
18
|
const OpenButton = styled.button<{ isOpen: boolean }>`
|
|
18
19
|
background: transparent;
|
|
@@ -49,7 +50,10 @@ const WrapperForFolderChild = styled.div<{ marked: boolean }>`
|
|
|
49
50
|
}
|
|
50
51
|
`;
|
|
51
52
|
|
|
52
|
-
const FolderName = styled
|
|
53
|
+
const FolderName = styled('button', { shouldForwardProp: (name) => !['marked', 'noArrow'].includes(name) })<{
|
|
54
|
+
marked: boolean;
|
|
55
|
+
noArrow?: boolean;
|
|
56
|
+
}>`
|
|
53
57
|
line-height: 1;
|
|
54
58
|
background: ${({ marked }) => (marked ? colors.brand.lighter : 'transparent')};
|
|
55
59
|
color: ${colors.text.primary};
|
|
@@ -81,16 +85,19 @@ const FolderNameLink = FolderName.withComponent(SafeLink);
|
|
|
81
85
|
interface Props {
|
|
82
86
|
name: string;
|
|
83
87
|
id: string;
|
|
84
|
-
|
|
88
|
+
level: number;
|
|
89
|
+
onCloseFolder: (id: string) => void;
|
|
90
|
+
onOpenFolder: (id: string) => void;
|
|
85
91
|
onMarkFolder: (id: string) => void;
|
|
92
|
+
onSelectFolder?: (id: string) => void;
|
|
86
93
|
isOpen: boolean;
|
|
87
94
|
markedFolderId?: string;
|
|
88
95
|
focusedFolderId?: string;
|
|
96
|
+
visibleFolders: string[];
|
|
89
97
|
loading?: boolean;
|
|
90
98
|
openOnFolderClick?: boolean;
|
|
91
99
|
hideArrow?: boolean;
|
|
92
100
|
setFocusedFolderId: SetFocusedFolderId;
|
|
93
|
-
url?: string;
|
|
94
101
|
icon?: React.ReactNode;
|
|
95
102
|
noPaddingWhenArrowIsHidden?: boolean;
|
|
96
103
|
folderChild?: FolderChildFuncType;
|
|
@@ -99,62 +106,61 @@ interface Props {
|
|
|
99
106
|
const FolderItem = ({
|
|
100
107
|
hideArrow,
|
|
101
108
|
loading,
|
|
109
|
+
level,
|
|
102
110
|
name,
|
|
103
111
|
id,
|
|
104
|
-
|
|
112
|
+
visibleFolders,
|
|
113
|
+
onCloseFolder,
|
|
114
|
+
onOpenFolder,
|
|
105
115
|
onMarkFolder,
|
|
116
|
+
onSelectFolder,
|
|
106
117
|
isOpen,
|
|
107
118
|
markedFolderId,
|
|
108
119
|
focusedFolderId,
|
|
109
120
|
openOnFolderClick,
|
|
110
121
|
setFocusedFolderId,
|
|
111
122
|
icon,
|
|
112
|
-
url,
|
|
113
123
|
noPaddingWhenArrowIsHidden,
|
|
114
124
|
folderChild,
|
|
115
125
|
}: Props) => {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
126
|
+
const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
|
|
127
|
+
const marked = markedFolderId === id;
|
|
128
|
+
|
|
129
|
+
const handleMarkFolder = () => {
|
|
130
|
+
onMarkFolder(id);
|
|
131
|
+
if (openOnFolderClick) {
|
|
132
|
+
if (isOpen) {
|
|
133
|
+
onCloseFolder(id);
|
|
134
|
+
} else {
|
|
135
|
+
onOpenFolder(id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
118
140
|
useEffect(() => {
|
|
119
141
|
if (focusedFolderId === id) {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
} else if (folderNameButtonRef.current) {
|
|
123
|
-
folderNameButtonRef.current.focus();
|
|
142
|
+
if (ref.current) {
|
|
143
|
+
ref.current.focus();
|
|
124
144
|
}
|
|
125
145
|
}
|
|
126
|
-
}, [focusedFolderId,
|
|
127
|
-
|
|
146
|
+
}, [focusedFolderId, ref, id]);
|
|
147
|
+
|
|
128
148
|
return (
|
|
129
149
|
<FolderItemWrapper>
|
|
130
150
|
{!hideArrow && (
|
|
131
|
-
<OpenButton
|
|
151
|
+
<OpenButton
|
|
152
|
+
tabIndex={-1}
|
|
153
|
+
isOpen={isOpen}
|
|
154
|
+
disabled={loading}
|
|
155
|
+
onClick={() => (isOpen ? onCloseFolder(id) : onOpenFolder(id))}>
|
|
132
156
|
<ArrowDropDown />
|
|
133
157
|
</OpenButton>
|
|
134
158
|
)}
|
|
135
|
-
{
|
|
136
|
-
<FolderNameLink
|
|
137
|
-
ref={folderNameLinkRef}
|
|
138
|
-
noArrow={hideArrow}
|
|
139
|
-
to={loading ? '' : url}
|
|
140
|
-
tabIndex={marked ? 0 : -1}
|
|
141
|
-
marked={marked}
|
|
142
|
-
onFocus={() => {
|
|
143
|
-
setFocusedFolderId(id);
|
|
144
|
-
}}
|
|
145
|
-
onClick={() => {
|
|
146
|
-
onMarkFolder(id);
|
|
147
|
-
if (openOnFolderClick) {
|
|
148
|
-
onToggleOpen(id);
|
|
149
|
-
}
|
|
150
|
-
}}>
|
|
151
|
-
{icon || <FolderOutlined />}
|
|
152
|
-
{name}
|
|
153
|
-
</FolderNameLink>
|
|
154
|
-
) : (
|
|
159
|
+
{onSelectFolder ? (
|
|
155
160
|
<>
|
|
156
161
|
<FolderName
|
|
157
|
-
ref={
|
|
162
|
+
ref={ref}
|
|
163
|
+
onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)}
|
|
158
164
|
noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
|
|
159
165
|
tabIndex={marked ? 0 : -1}
|
|
160
166
|
marked={marked}
|
|
@@ -163,10 +169,8 @@ const FolderItem = ({
|
|
|
163
169
|
setFocusedFolderId(id);
|
|
164
170
|
}}
|
|
165
171
|
onClick={() => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
onToggleOpen(id);
|
|
169
|
-
}
|
|
172
|
+
handleMarkFolder();
|
|
173
|
+
onSelectFolder(id);
|
|
170
174
|
}}>
|
|
171
175
|
{icon || <FolderOutlined />}
|
|
172
176
|
{name}
|
|
@@ -177,6 +181,25 @@ const FolderItem = ({
|
|
|
177
181
|
</WrapperForFolderChild>
|
|
178
182
|
)}
|
|
179
183
|
</>
|
|
184
|
+
) : (
|
|
185
|
+
<FolderNameLink
|
|
186
|
+
ref={ref}
|
|
187
|
+
onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
|
|
188
|
+
arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)
|
|
189
|
+
}
|
|
190
|
+
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
|
+
}}>
|
|
200
|
+
{icon || <FolderOutlined />}
|
|
201
|
+
{name}
|
|
202
|
+
</FolderNameLink>
|
|
180
203
|
)}
|
|
181
204
|
</FolderItemWrapper>
|
|
182
205
|
);
|
|
@@ -32,65 +32,73 @@ const StyledLI = styled.li`
|
|
|
32
32
|
|
|
33
33
|
const FolderItems = ({
|
|
34
34
|
loading,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
folders,
|
|
36
|
+
level,
|
|
37
37
|
editable,
|
|
38
|
-
|
|
38
|
+
onSelectFolder,
|
|
39
|
+
onCloseFolder,
|
|
40
|
+
onOpenFolder,
|
|
39
41
|
onCreateNewFolder,
|
|
40
42
|
onCancelNewFolder,
|
|
41
43
|
onSaveNewFolder,
|
|
42
|
-
|
|
44
|
+
newFolderParentId,
|
|
45
|
+
visibleFolders,
|
|
43
46
|
openFolders,
|
|
44
47
|
markedFolderId,
|
|
45
48
|
onMarkFolder,
|
|
46
49
|
openOnFolderClick,
|
|
47
50
|
focusedFolderId,
|
|
48
51
|
setFocusedFolderId,
|
|
49
|
-
firstLevel,
|
|
50
52
|
folderChild,
|
|
51
53
|
maximumLevelsOfFoldersAllowed,
|
|
52
54
|
}: FolderItemsProps) => (
|
|
53
|
-
<StyledUL role="group" firstLevel={
|
|
54
|
-
{
|
|
55
|
-
const newIdPaths = [...idPaths, _index];
|
|
55
|
+
<StyledUL role="group" firstLevel={level === 1}>
|
|
56
|
+
{folders.map(({ name, subfolders, id, icon }, _index) => {
|
|
56
57
|
const isOpen = openFolders?.includes(id);
|
|
57
58
|
return (
|
|
58
59
|
<StyledLI key={id} role="treeitem">
|
|
59
60
|
<div>
|
|
60
61
|
<FolderItem
|
|
62
|
+
level={level}
|
|
61
63
|
icon={icon}
|
|
62
|
-
|
|
64
|
+
onSelectFolder={onSelectFolder}
|
|
63
65
|
openOnFolderClick={openOnFolderClick}
|
|
64
66
|
loading={loading}
|
|
65
67
|
isOpen={isOpen}
|
|
66
68
|
id={id}
|
|
69
|
+
visibleFolders={visibleFolders}
|
|
67
70
|
name={name}
|
|
68
71
|
markedFolderId={markedFolderId}
|
|
69
72
|
focusedFolderId={focusedFolderId}
|
|
70
|
-
onToggleOpen={onToggleOpen}
|
|
71
73
|
onMarkFolder={onMarkFolder}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
onCloseFolder={onCloseFolder}
|
|
75
|
+
onOpenFolder={onOpenFolder}
|
|
76
|
+
hideArrow={subfolders?.length === 0 || level > maximumLevelsOfFoldersAllowed}
|
|
77
|
+
noPaddingWhenArrowIsHidden={editable && level === 1 && subfolders?.length === 0}
|
|
74
78
|
setFocusedFolderId={setFocusedFolderId}
|
|
75
79
|
folderChild={folderChild}
|
|
76
80
|
/>
|
|
77
81
|
</div>
|
|
78
|
-
{
|
|
82
|
+
{newFolderParentId === id && (
|
|
79
83
|
<FolderNameInput
|
|
84
|
+
parentId={newFolderParentId}
|
|
80
85
|
loading={loading}
|
|
81
86
|
onCancelNewFolder={onCancelNewFolder}
|
|
82
87
|
onSaveNewFolder={onSaveNewFolder}
|
|
83
88
|
/>
|
|
84
89
|
)}
|
|
85
|
-
{
|
|
90
|
+
{subfolders && isOpen && (
|
|
86
91
|
<FolderItems
|
|
92
|
+
onSelectFolder={onSelectFolder}
|
|
87
93
|
loading={loading}
|
|
88
|
-
|
|
94
|
+
newFolderParentId={newFolderParentId}
|
|
95
|
+
visibleFolders={visibleFolders}
|
|
89
96
|
openFolders={openFolders}
|
|
90
|
-
|
|
97
|
+
level={level + 1}
|
|
91
98
|
editable={editable}
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
folders={subfolders}
|
|
100
|
+
onCloseFolder={onCloseFolder}
|
|
101
|
+
onOpenFolder={onOpenFolder}
|
|
94
102
|
onCreateNewFolder={onCreateNewFolder}
|
|
95
103
|
onSaveNewFolder={onSaveNewFolder}
|
|
96
104
|
onCancelNewFolder={onCancelNewFolder}
|
|
@@ -99,7 +107,6 @@ const FolderItems = ({
|
|
|
99
107
|
openOnFolderClick={openOnFolderClick}
|
|
100
108
|
focusedFolderId={focusedFolderId}
|
|
101
109
|
setFocusedFolderId={setFocusedFolderId}
|
|
102
|
-
firstLevel={false}
|
|
103
110
|
folderChild={folderChild}
|
|
104
111
|
maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
|
|
105
112
|
/>
|
|
@@ -13,7 +13,7 @@ import { ArrowDropDown as ArrowDropDownRaw } from '@ndla/icons/common';
|
|
|
13
13
|
import { spacing, colors, misc, animations } from '@ndla/core';
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
15
|
import { isMobile } from 'react-device-detect';
|
|
16
|
-
import Spinner from '
|
|
16
|
+
import { Spinner } from '@ndla/icons';
|
|
17
17
|
|
|
18
18
|
const ArrowRight = styled(ArrowDropDownRaw)`
|
|
19
19
|
color: ${colors.text.primary};
|
|
@@ -53,14 +53,15 @@ const StyledInput = styled.input`
|
|
|
53
53
|
`;
|
|
54
54
|
|
|
55
55
|
interface FolderNameInputProps {
|
|
56
|
-
onSaveNewFolder: (
|
|
56
|
+
onSaveNewFolder: (name: string, parentId: string) => void;
|
|
57
|
+
parentId: string;
|
|
57
58
|
onCancelNewFolder: () => void;
|
|
58
59
|
loading?: boolean;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
const FolderNameInput = ({ onSaveNewFolder, onCancelNewFolder, loading }: FolderNameInputProps) => {
|
|
62
|
+
const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading }: FolderNameInputProps) => {
|
|
62
63
|
const { t } = useTranslation();
|
|
63
|
-
const [
|
|
64
|
+
const [name, setName] = useState<string>(t('treeStructure.newFolder.defaultName'));
|
|
64
65
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
65
66
|
|
|
66
67
|
useEffect(() => {
|
|
@@ -82,22 +83,19 @@ const FolderNameInput = ({ onSaveNewFolder, onCancelNewFolder, loading }: Folder
|
|
|
82
83
|
autoFocus
|
|
83
84
|
placeholder={t('treeStructure.newFolder.placeholder')}
|
|
84
85
|
disabled={loading}
|
|
85
|
-
value={
|
|
86
|
+
value={name}
|
|
86
87
|
onBlur={() => onCancelNewFolder()}
|
|
87
88
|
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
88
89
|
if (e.key === 'Escape') {
|
|
89
90
|
onCancelNewFolder();
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
93
|
-
onSaveNewFolder(value);
|
|
91
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
94
92
|
e.preventDefault();
|
|
93
|
+
onSaveNewFolder(name, parentId);
|
|
95
94
|
}
|
|
96
|
-
return;
|
|
97
95
|
}}
|
|
98
96
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
-
const target = e.target
|
|
100
|
-
|
|
97
|
+
const target = e.target;
|
|
98
|
+
setName(target.value);
|
|
101
99
|
}}
|
|
102
100
|
/>
|
|
103
101
|
{loading && <Spinner size="small" />}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
|
10
|
-
import { uuid } from '@ndla/util';
|
|
11
10
|
import { AddButton } from '@ndla/button';
|
|
12
11
|
import Tooltip from '@ndla/tooltip';
|
|
13
12
|
import { useTranslation } from 'react-i18next';
|
|
@@ -16,9 +15,8 @@ import { spacing, fonts } from '@ndla/core';
|
|
|
16
15
|
import { uniq } from 'lodash';
|
|
17
16
|
import TreeStructureStyledWrapper from './TreeStructureWrapper';
|
|
18
17
|
import FolderItems from './FolderItems';
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import { NewFolderProps, TreeStructureProps } from './TreeStructure.types';
|
|
18
|
+
import { getPathOfFolder, getFolderName, flattenFolders } from './helperFunctions';
|
|
19
|
+
import { TreeStructureProps } from './TreeStructure.types';
|
|
22
20
|
|
|
23
21
|
export const MAX_LEVEL_FOR_FOLDERS = 4;
|
|
24
22
|
|
|
@@ -32,26 +30,32 @@ const AddFolderWrapper = styled.div`
|
|
|
32
30
|
`;
|
|
33
31
|
|
|
34
32
|
const TreeStructure = ({
|
|
35
|
-
|
|
33
|
+
folders,
|
|
36
34
|
label,
|
|
37
35
|
editable,
|
|
38
36
|
loading,
|
|
39
37
|
onNewFolder,
|
|
38
|
+
onSelectFolder,
|
|
40
39
|
openOnFolderClick,
|
|
41
40
|
framed,
|
|
42
41
|
folderIdMarkedByDefault,
|
|
43
42
|
defaultOpenFolders,
|
|
44
43
|
folderChild,
|
|
45
|
-
maximumLevelsOfFoldersAllowed,
|
|
44
|
+
maximumLevelsOfFoldersAllowed = MAX_LEVEL_FOR_FOLDERS,
|
|
46
45
|
}: TreeStructureProps) => {
|
|
47
46
|
const { t } = useTranslation();
|
|
48
|
-
const [
|
|
47
|
+
const [newFolderParentId, setNewFolderParentId] = useState<string | undefined>();
|
|
49
48
|
const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
|
|
50
49
|
const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
|
|
51
|
-
const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault ||
|
|
50
|
+
const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || folders[0]?.id);
|
|
52
51
|
const treestructureRef = useRef<HTMLDivElement>(null);
|
|
53
52
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
54
|
-
const rootLevelId =
|
|
53
|
+
const rootLevelId = 'treestructure-root';
|
|
54
|
+
|
|
55
|
+
const visibleFolders = useMemo(
|
|
56
|
+
() => flattenFolders(folders, openFolders).map((folder) => folder.id),
|
|
57
|
+
[folders, openFolders],
|
|
58
|
+
);
|
|
55
59
|
|
|
56
60
|
useEffect(() => {
|
|
57
61
|
if (defaultOpenFolders) {
|
|
@@ -63,54 +67,51 @@ const TreeStructure = ({
|
|
|
63
67
|
|
|
64
68
|
useEffect(() => {
|
|
65
69
|
if (!loading) {
|
|
66
|
-
|
|
70
|
+
setNewFolderParentId(undefined);
|
|
67
71
|
}
|
|
68
72
|
}, [loading]);
|
|
69
73
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
74
|
+
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) {
|
|
84
|
+
if (onSelectFolder) {
|
|
81
85
|
setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
|
|
86
|
+
onSelectFolder(closingFolderPath[closingFolderPath.length - 1]);
|
|
87
|
+
} else {
|
|
88
|
+
setFocusedFolderId(closingFolderPath[closingFolderPath.length - 1]);
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
|
-
setOpenFolders(openFolders.filter((folder) => folder !== id));
|
|
85
|
-
} else {
|
|
86
|
-
setOpenFolders(uniq([...openFolders, id]));
|
|
87
91
|
}
|
|
92
|
+
setOpenFolders(openFolders.filter((folder) => folder !== id));
|
|
88
93
|
};
|
|
89
94
|
|
|
90
|
-
const
|
|
91
|
-
|
|
95
|
+
const onOpenFolder = (id: string) => {
|
|
96
|
+
setOpenFolders(uniq(openFolders.concat(id)));
|
|
92
97
|
};
|
|
93
98
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
99
|
+
const onCreateNewFolder = (parentId: string) => {
|
|
100
|
+
setNewFolderParentId(parentId);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onSaveNewFolder = (name: string, parentId: string) => {
|
|
104
|
+
onNewFolder(name, parentId).then((newFolderId) => {
|
|
105
|
+
if (newFolderId) {
|
|
106
|
+
setMarkedFolderId(newFolderId);
|
|
107
|
+
setFocusedFolderId(newFolderId);
|
|
108
|
+
setOpenFolders(uniq(openFolders.concat(parentId)));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
110
111
|
};
|
|
111
112
|
|
|
112
113
|
const onCancelNewFolder = () => {
|
|
113
|
-
|
|
114
|
+
setNewFolderParentId(undefined);
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
const onMarkFolder = (id: string) => {
|
|
@@ -118,35 +119,25 @@ const TreeStructure = ({
|
|
|
118
119
|
setFocusedFolderId(id);
|
|
119
120
|
};
|
|
120
121
|
|
|
121
|
-
const paths = getPathOfFolder(
|
|
122
|
+
const paths = getPathOfFolder(folders, markedFolderId || '');
|
|
122
123
|
const canAddFolder = editable && paths.length < (maximumLevelsOfFoldersAllowed || 1);
|
|
123
124
|
|
|
124
125
|
return (
|
|
125
|
-
<div
|
|
126
|
-
ref={treestructureRef}
|
|
127
|
-
onKeyDown={(e) => {
|
|
128
|
-
if (wrapperRef.current?.contains(document.activeElement) && KEYBOARD_KEYS_OF_INTEREST.includes(e.key)) {
|
|
129
|
-
keyboardNavigation({
|
|
130
|
-
e,
|
|
131
|
-
data,
|
|
132
|
-
setFocusedFolderId,
|
|
133
|
-
focusedFolderId,
|
|
134
|
-
onToggleOpen,
|
|
135
|
-
openFolders,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}}>
|
|
126
|
+
<div ref={treestructureRef}>
|
|
139
127
|
{label && <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>}
|
|
140
128
|
<TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
|
|
141
129
|
<FolderItems
|
|
142
|
-
|
|
143
|
-
|
|
130
|
+
onSelectFolder={onSelectFolder}
|
|
131
|
+
level={1}
|
|
132
|
+
folders={folders}
|
|
144
133
|
editable={editable}
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
onOpenFolder={onOpenFolder}
|
|
135
|
+
onCloseFolder={onCloseFolder}
|
|
136
|
+
newFolderParentId={newFolderParentId}
|
|
147
137
|
onCreateNewFolder={onCreateNewFolder}
|
|
148
138
|
onCancelNewFolder={onCancelNewFolder}
|
|
149
139
|
onSaveNewFolder={onSaveNewFolder}
|
|
140
|
+
visibleFolders={visibleFolders}
|
|
150
141
|
openFolders={openFolders}
|
|
151
142
|
markedFolderId={markedFolderId}
|
|
152
143
|
onMarkFolder={onMarkFolder}
|
|
@@ -154,7 +145,6 @@ const TreeStructure = ({
|
|
|
154
145
|
loading={loading}
|
|
155
146
|
focusedFolderId={focusedFolderId}
|
|
156
147
|
setFocusedFolderId={setFocusedFolderId}
|
|
157
|
-
firstLevel
|
|
158
148
|
folderChild={folderChild}
|
|
159
149
|
maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
|
|
160
150
|
/>
|
|
@@ -165,16 +155,15 @@ const TreeStructure = ({
|
|
|
165
155
|
tooltip={
|
|
166
156
|
canAddFolder
|
|
167
157
|
? t('myNdla.newFolderUnder', {
|
|
168
|
-
folderName: getFolderName(
|
|
158
|
+
folderName: getFolderName(folders, markedFolderId),
|
|
169
159
|
})
|
|
170
|
-
: t('
|
|
160
|
+
: t('treeStructure.maxFoldersAlreadyAdded')
|
|
171
161
|
}>
|
|
172
162
|
<AddButton
|
|
173
163
|
disabled={!canAddFolder}
|
|
174
164
|
aria-label={t('myNdla.newFolder')}
|
|
175
165
|
onClick={() => {
|
|
176
|
-
|
|
177
|
-
setNewFolder({ idPaths, parentId: paths[paths.length - 1] });
|
|
166
|
+
setNewFolderParentId(markedFolderId);
|
|
178
167
|
}}>
|
|
179
168
|
{t('myNdla.newFolder')}
|
|
180
169
|
</AddButton>
|
|
@@ -185,8 +174,4 @@ const TreeStructure = ({
|
|
|
185
174
|
);
|
|
186
175
|
};
|
|
187
176
|
|
|
188
|
-
TreeStructure.defaultProps = {
|
|
189
|
-
maximumLevelsOfFoldersAllowed: MAX_LEVEL_FOR_FOLDERS,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
177
|
export default TreeStructure;
|
|
@@ -11,35 +11,29 @@ import React, { ReactNode } from 'react';
|
|
|
11
11
|
export interface FolderStructureProps {
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
|
-
|
|
15
|
-
data?: FolderStructureProps[];
|
|
14
|
+
subfolders: FolderStructureProps[];
|
|
16
15
|
isFavorite?: boolean;
|
|
17
16
|
status?: string;
|
|
18
17
|
openAsDefault?: boolean;
|
|
19
|
-
url?: string;
|
|
20
18
|
icon?: ReactNode;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
export interface NewFolderProps {
|
|
24
|
-
parentId?: string;
|
|
25
|
-
idPaths: number[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
interface CommonFolderProps {
|
|
29
|
-
data: FolderStructureProps[];
|
|
30
22
|
editable?: boolean;
|
|
31
23
|
loading?: boolean;
|
|
32
24
|
openOnFolderClick?: boolean;
|
|
25
|
+
onSelectFolder?: (id: string) => void;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
export interface TreeStructureProps extends CommonFolderProps {
|
|
29
|
+
folders: FolderStructureProps[];
|
|
36
30
|
framed?: boolean;
|
|
37
31
|
label?: string;
|
|
38
32
|
folderIdMarkedByDefault?: string;
|
|
39
|
-
onNewFolder: (
|
|
33
|
+
onNewFolder: (name: string, parentId: string) => Promise<string>;
|
|
40
34
|
defaultOpenFolders?: string[];
|
|
41
35
|
folderChild?: FolderChildFuncType;
|
|
42
|
-
maximumLevelsOfFoldersAllowed
|
|
36
|
+
maximumLevelsOfFoldersAllowed?: number;
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
export type onCreateNewFolderProp = ({
|
|
@@ -56,18 +50,20 @@ export type SetFocusedFolderId = React.Dispatch<React.SetStateAction<string | un
|
|
|
56
50
|
export type FolderChildFuncType = (id: string, tabIndex: number) => ReactNode;
|
|
57
51
|
|
|
58
52
|
export interface FolderItemsProps extends CommonFolderProps {
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
folders: FolderStructureProps[];
|
|
54
|
+
onCloseFolder: (id: string) => void;
|
|
55
|
+
onOpenFolder: (id: string) => void;
|
|
56
|
+
onSaveNewFolder: (name: string, parentId: string) => void;
|
|
61
57
|
onCancelNewFolder: () => void;
|
|
62
|
-
onCreateNewFolder:
|
|
63
|
-
|
|
58
|
+
onCreateNewFolder: (parentId: string) => void;
|
|
59
|
+
newFolderParentId: string | undefined;
|
|
60
|
+
visibleFolders: string[];
|
|
64
61
|
openFolders: string[];
|
|
65
62
|
markedFolderId?: string;
|
|
66
63
|
onMarkFolder: (id: string) => void;
|
|
67
|
-
|
|
64
|
+
level: number;
|
|
68
65
|
focusedFolderId: string | undefined;
|
|
69
66
|
setFocusedFolderId: SetFocusedFolderId;
|
|
70
|
-
firstLevel: boolean;
|
|
71
67
|
keyNavigationFocusIsCreateFolderButton?: boolean;
|
|
72
68
|
icon?: ReactNode;
|
|
73
69
|
folderChild?: FolderChildFuncType;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2022-present, NDLA.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { KeyboardEvent } from 'react';
|
|
10
|
+
|
|
11
|
+
const navigateVertical = (
|
|
12
|
+
visibleFolders: string[],
|
|
13
|
+
folderId: string,
|
|
14
|
+
setFocusedFolderId: (id: string) => void,
|
|
15
|
+
direction: 1 | -1,
|
|
16
|
+
) => {
|
|
17
|
+
const currentIndex = visibleFolders.findIndex((id) => id === folderId);
|
|
18
|
+
const target = visibleFolders[currentIndex + direction];
|
|
19
|
+
if (target !== undefined) {
|
|
20
|
+
setFocusedFolderId(target);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
|
25
|
+
|
|
26
|
+
export const arrowNavigation = (
|
|
27
|
+
e: KeyboardEvent<HTMLElement>,
|
|
28
|
+
id: string,
|
|
29
|
+
visibleFolders: string[],
|
|
30
|
+
setFocusedFolderId: (id: string) => void,
|
|
31
|
+
onOpen: (id: string) => void,
|
|
32
|
+
onClose: (id: string) => void,
|
|
33
|
+
) => {
|
|
34
|
+
if (!arrowKeys.includes(e.key)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
|
|
41
|
+
switch (e.key) {
|
|
42
|
+
case 'ArrowUp':
|
|
43
|
+
return navigateVertical(visibleFolders, id, setFocusedFolderId, -1);
|
|
44
|
+
case 'ArrowDown':
|
|
45
|
+
return navigateVertical(visibleFolders, id, setFocusedFolderId, 1);
|
|
46
|
+
case 'ArrowLeft':
|
|
47
|
+
return onClose(id);
|
|
48
|
+
case 'ArrowRight':
|
|
49
|
+
return onOpen(id);
|
|
50
|
+
default:
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
};
|