@ndla/ui 25.3.0 → 26.1.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/es/LanguageSelector/LanguageSelector.js +12 -7
- package/es/MyNdla/Resource/Folder.js +10 -8
- package/es/Resource/BlockResource.js +9 -7
- package/es/Resource/ListResource.js +10 -8
- package/es/TreeStructure/FolderItem.js +110 -94
- package/es/TreeStructure/FolderItems.js +26 -30
- package/es/TreeStructure/FolderNameInput.js +35 -27
- package/es/TreeStructure/NavigationLink.js +81 -0
- package/es/TreeStructure/TreeStructure.js +169 -45
- 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/LanguageSelector/LanguageSelector.js +13 -7
- package/lib/MyNdla/Resource/Folder.d.ts +2 -1
- package/lib/MyNdla/Resource/Folder.js +10 -8
- package/lib/Resource/BlockResource.d.ts +2 -1
- package/lib/Resource/BlockResource.js +9 -7
- package/lib/Resource/ListResource.d.ts +2 -1
- package/lib/Resource/ListResource.js +10 -8
- package/lib/TreeStructure/FolderItem.d.ts +2 -3
- package/lib/TreeStructure/FolderItem.js +107 -92
- package/lib/TreeStructure/FolderItems.d.ts +1 -3
- package/lib/TreeStructure/FolderItems.js +26 -29
- package/lib/TreeStructure/FolderNameInput.d.ts +2 -1
- package/lib/TreeStructure/FolderNameInput.js +33 -26
- package/lib/TreeStructure/NavigationLink.d.ts +15 -0
- package/lib/TreeStructure/NavigationLink.js +100 -0
- package/lib/TreeStructure/TreeStructure.d.ts +1 -2
- package/lib/TreeStructure/TreeStructure.js +163 -45
- package/lib/TreeStructure/types.d.ts +4 -1
- 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 +11 -11
- package/src/.DS_Store +0 -0
- package/src/LanguageSelector/LanguageSelector.tsx +4 -1
- package/src/MyNdla/Resource/Folder.tsx +3 -2
- package/src/Resource/BlockResource.tsx +3 -1
- package/src/Resource/ListResource.tsx +3 -1
- package/src/TreeStructure/FolderItem.tsx +126 -104
- package/src/TreeStructure/FolderItems.tsx +51 -43
- package/src/TreeStructure/FolderNameInput.tsx +43 -28
- package/src/TreeStructure/NavigationLink.tsx +100 -0
- package/src/TreeStructure/TreeStructure.tsx +187 -61
- package/src/TreeStructure/types.ts +5 -1
- 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/TreeStructure/TreeStructureWrapper.js +0 -13
- package/lib/TreeStructure/TreeStructureWrapper.d.ts +0 -12
- package/lib/TreeStructure/TreeStructureWrapper.js +0 -24
- package/src/TreeStructure/TreeStructureWrapper.tsx +0 -31
|
@@ -6,40 +6,38 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { KeyboardEvent,
|
|
9
|
+
import React, { KeyboardEvent, useEffect, useRef } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import styled from '@emotion/styled';
|
|
12
|
-
import {
|
|
12
|
+
import { ArrowDropDownRounded } from '@ndla/icons/common';
|
|
13
13
|
import { Done } from '@ndla/icons/editor';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { colors, spacing, misc, animations } from '@ndla/core';
|
|
14
|
+
import { ButtonV2 as Button } from '@ndla/button';
|
|
15
|
+
import { colors, spacing, animations, spacingUnit, misc, fonts } from '@ndla/core';
|
|
17
16
|
import SafeLink from '@ndla/safelink';
|
|
18
17
|
import { CommonFolderItemsProps, FolderType } from './types';
|
|
19
18
|
import { arrowNavigation } from './arrowNavigation';
|
|
20
19
|
|
|
21
|
-
const OpenButton = styled.
|
|
22
|
-
background: transparent;
|
|
23
|
-
border: 0;
|
|
24
|
-
transform: rotate(${({ isOpen }) => (isOpen ? '0' : '-90')}deg);
|
|
25
|
-
padding: ${spacing.xsmall};
|
|
20
|
+
const OpenButton = styled.span<{ isOpen: boolean }>`
|
|
26
21
|
display: flex;
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
align-self: stretch;
|
|
25
|
+
color: ${colors.brand.tertiary};
|
|
26
|
+
${misc.transition.default};
|
|
29
27
|
cursor: pointer;
|
|
30
28
|
&:hover {
|
|
31
29
|
color: ${colors.brand.primary};
|
|
32
30
|
}
|
|
33
31
|
svg {
|
|
34
|
-
width:
|
|
35
|
-
height:
|
|
36
|
-
transform: ${({ isOpen }) => (isOpen ? '
|
|
32
|
+
width: 24px;
|
|
33
|
+
height: 24px;
|
|
34
|
+
transform: rotate(${({ isOpen }) => (isOpen ? '0' : '-90')}deg);
|
|
37
35
|
}
|
|
38
36
|
`;
|
|
39
37
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
align
|
|
38
|
+
const StyledName = styled.span`
|
|
39
|
+
grid-column-start: 2;
|
|
40
|
+
text-align: left;
|
|
43
41
|
`;
|
|
44
42
|
|
|
45
43
|
const WrapperForFolderChild = styled.div`
|
|
@@ -47,44 +45,39 @@ const WrapperForFolderChild = styled.div`
|
|
|
47
45
|
flex-direction: row;
|
|
48
46
|
align-items: center;
|
|
49
47
|
gap: ${spacing.xsmall};
|
|
48
|
+
margin-left: auto;
|
|
50
49
|
`;
|
|
51
50
|
|
|
52
|
-
const shouldForwardProp = (name: string) => !['selected', 'noArrow', 'fullWidth'].includes(name);
|
|
51
|
+
const shouldForwardProp = (name: string) => !['selected', 'noArrow', 'fullWidth', 'level'].includes(name);
|
|
53
52
|
|
|
54
53
|
interface FolderNameProps {
|
|
55
54
|
selected?: boolean;
|
|
56
55
|
noArrow?: boolean;
|
|
57
56
|
fullWidth?: boolean;
|
|
57
|
+
level: number;
|
|
58
|
+
isCreatingFolder?: boolean;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const FolderName = styled(
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
padding: ${spacing.xsmall};
|
|
63
|
-
margin: 0;
|
|
64
|
-
outline-offset: -2px;
|
|
65
|
-
outline-color: ${colors.brand.primary};
|
|
66
|
-
margin-left: ${({ noArrow }) => (noArrow ? `29px` : `0px`)};
|
|
67
|
-
flex-grow: ${({ fullWidth }) => fullWidth && 1};
|
|
61
|
+
const FolderName = styled(Button, { shouldForwardProp })<FolderNameProps>`
|
|
68
62
|
display: grid;
|
|
69
|
-
grid-template-columns:
|
|
70
|
-
|
|
63
|
+
grid-template-columns: ${spacing.medium} 1fr auto;
|
|
64
|
+
|
|
65
|
+
padding-left: ${({ level }) => 0.75 * spacingUnit * level}px;
|
|
71
66
|
gap: ${spacing.xxsmall};
|
|
72
|
-
border:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
border: none;
|
|
68
|
+
outline: none;
|
|
69
|
+
background: ${({ selected, isCreatingFolder }) => selected && !isCreatingFolder && colors.brand.lighter};
|
|
70
|
+
color: ${({ isCreatingFolder, selected }) =>
|
|
71
|
+
isCreatingFolder && selected ? colors.brand.primary : colors.text.primary};
|
|
77
72
|
transition: ${animations.durations.superFast};
|
|
78
|
-
text-align: left;
|
|
79
73
|
line-height: 1;
|
|
80
74
|
word-break: break-word;
|
|
81
75
|
&:hover,
|
|
82
76
|
&:focus {
|
|
77
|
+
box-shadow: none;
|
|
78
|
+
outline: none;
|
|
83
79
|
background: ${({ selected }) => (selected ? colors.brand.light : colors.brand.lightest)};
|
|
84
|
-
color: ${colors.
|
|
85
|
-
+ ${WrapperForFolderChild} {
|
|
86
|
-
opacity: 1;
|
|
87
|
-
}
|
|
80
|
+
color: ${colors.text.primary};
|
|
88
81
|
}
|
|
89
82
|
`;
|
|
90
83
|
|
|
@@ -92,25 +85,42 @@ const StyledDone = styled(Done)`
|
|
|
92
85
|
color: ${colors.support.green};
|
|
93
86
|
`;
|
|
94
87
|
|
|
95
|
-
const FolderNameLink =
|
|
88
|
+
const FolderNameLink = styled(SafeLink, { shouldForwardProp })<FolderNameProps>`
|
|
89
|
+
display: grid;
|
|
90
|
+
align-items: center;
|
|
91
|
+
grid-template-columns: ${spacing.medium} 1fr auto;
|
|
92
|
+
padding: ${spacing.small} ${spacing.xxsmall};
|
|
93
|
+
margin-left: ${({ level }) => 0.75 * spacingUnit * level}px;
|
|
94
|
+
gap: ${spacing.xxsmall};
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
|
|
97
|
+
border: none;
|
|
98
|
+
box-shadow: none;
|
|
99
|
+
color: ${({ selected }) => (selected ? colors.brand.primary : colors.text.primary)};
|
|
100
|
+
font-weight: ${({ selected }) => selected && fonts.weight.semibold};
|
|
101
|
+
font-size: ${fonts.sizes('16px')};
|
|
102
|
+
transition: ${animations.durations.superFast};
|
|
103
|
+
line-height: 1;
|
|
104
|
+
word-break: break-word;
|
|
105
|
+
&:hover,
|
|
106
|
+
&:focus {
|
|
107
|
+
color: ${colors.brand.primary};
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
96
110
|
|
|
97
111
|
interface Props extends CommonFolderItemsProps {
|
|
98
|
-
hideArrow?: boolean;
|
|
99
112
|
isOpen: boolean;
|
|
100
113
|
folder: FolderType;
|
|
101
|
-
|
|
114
|
+
isCreatingFolder?: boolean;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
const FolderItem = ({
|
|
105
118
|
focusedFolderId,
|
|
106
|
-
menuItems,
|
|
107
|
-
hideArrow,
|
|
108
119
|
folder,
|
|
109
120
|
isOpen,
|
|
110
121
|
level,
|
|
111
122
|
loading,
|
|
112
123
|
selectedFolder,
|
|
113
|
-
noPaddingWhenArrowIsHidden,
|
|
114
124
|
onCloseFolder,
|
|
115
125
|
onOpenFolder,
|
|
116
126
|
onSelectFolder,
|
|
@@ -120,9 +130,11 @@ const FolderItem = ({
|
|
|
120
130
|
targetResource,
|
|
121
131
|
visibleFolders,
|
|
122
132
|
framed,
|
|
133
|
+
maxLevel,
|
|
134
|
+
isCreatingFolder,
|
|
123
135
|
}: Props) => {
|
|
124
136
|
const { t } = useTranslation();
|
|
125
|
-
const { id,
|
|
137
|
+
const { id, name } = folder;
|
|
126
138
|
const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
|
|
127
139
|
const selected = selectedFolder && selectedFolder.id === id;
|
|
128
140
|
const focused = focusedFolderId === id;
|
|
@@ -145,79 +157,89 @@ const FolderItem = ({
|
|
|
145
157
|
};
|
|
146
158
|
|
|
147
159
|
useEffect(() => {
|
|
148
|
-
if (focusedFolderId === id) {
|
|
160
|
+
if (focusedFolderId === id && !isCreatingFolder) {
|
|
149
161
|
ref.current?.focus();
|
|
150
162
|
}
|
|
151
|
-
}, [focusedFolderId, ref, id]);
|
|
152
|
-
|
|
153
|
-
const actions = menuItems?.map((item) => {
|
|
154
|
-
const { onClick } = item;
|
|
155
|
-
return {
|
|
156
|
-
...item,
|
|
157
|
-
onClick: (e?: MouseEvent<HTMLDivElement>) => onClick(e, folder),
|
|
158
|
-
};
|
|
159
|
-
});
|
|
163
|
+
}, [focusedFolderId, ref, id, isCreatingFolder]);
|
|
160
164
|
|
|
161
165
|
const linkPath = `/minndla${level > 0 ? '/folders' : ''}/${id}`;
|
|
162
166
|
|
|
163
167
|
const containsResource =
|
|
164
168
|
targetResource && folder.resources.some((resource) => resource.resourceId === targetResource.resourceId);
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
const emptyFolder = folder.subfolders.length === 0;
|
|
171
|
+
|
|
172
|
+
const isMaxDepth = level > maxLevel;
|
|
173
|
+
|
|
174
|
+
const hideArrow = isMaxDepth || emptyFolder;
|
|
175
|
+
|
|
176
|
+
return onSelectFolder ? (
|
|
177
|
+
<FolderName
|
|
178
|
+
variant="ghost"
|
|
179
|
+
shape="sharp"
|
|
180
|
+
fontWeight="normal"
|
|
181
|
+
colorTheme="light"
|
|
182
|
+
ref={ref}
|
|
183
|
+
level={level}
|
|
184
|
+
fullWidth={framed}
|
|
185
|
+
onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)}
|
|
186
|
+
noArrow={hideArrow}
|
|
187
|
+
tabIndex={selected || focused ? 0 : -1}
|
|
188
|
+
selected={selected}
|
|
189
|
+
disabled={loading}
|
|
190
|
+
onFocus={() => setFocusedId(id)}
|
|
191
|
+
onClick={handleClickFolder}
|
|
192
|
+
isCreatingFolder={isCreatingFolder}>
|
|
168
193
|
{!hideArrow && (
|
|
169
194
|
<OpenButton
|
|
170
195
|
tabIndex={-1}
|
|
171
196
|
isOpen={isOpen}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
onClick={() => {
|
|
198
|
+
ref.current?.focus();
|
|
199
|
+
if (isOpen) {
|
|
200
|
+
onCloseFolder(id);
|
|
201
|
+
} else {
|
|
202
|
+
onOpenFolder(id);
|
|
203
|
+
}
|
|
204
|
+
}}>
|
|
205
|
+
<ArrowDropDownRounded />
|
|
175
206
|
</OpenButton>
|
|
176
207
|
)}
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
noArrow={hideArrow}
|
|
211
|
-
to={loading ? '' : linkPath}
|
|
212
|
-
tabIndex={selected || focused || level === 0 ? 0 : -1}
|
|
213
|
-
selected={selected}
|
|
214
|
-
onFocus={() => setFocusedId(id)}
|
|
215
|
-
onClick={handleClickFolder}>
|
|
216
|
-
{icon || <FolderOutlined />}
|
|
217
|
-
{name}
|
|
218
|
-
</FolderNameLink>
|
|
208
|
+
<StyledName>{name}</StyledName>
|
|
209
|
+
<WrapperForFolderChild>
|
|
210
|
+
{containsResource && <StyledDone title={t('myNdla.alreadyInFolder')} />}
|
|
211
|
+
</WrapperForFolderChild>
|
|
212
|
+
</FolderName>
|
|
213
|
+
) : (
|
|
214
|
+
<FolderNameLink
|
|
215
|
+
ref={ref}
|
|
216
|
+
level={level}
|
|
217
|
+
onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
|
|
218
|
+
arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)
|
|
219
|
+
}
|
|
220
|
+
noArrow={!isMaxDepth}
|
|
221
|
+
to={loading ? '' : linkPath}
|
|
222
|
+
tabIndex={selected || focused ? 0 : -1}
|
|
223
|
+
selected={selected}
|
|
224
|
+
onFocus={() => setFocusedId(id)}
|
|
225
|
+
onClick={handleClickFolder}>
|
|
226
|
+
{(!hideArrow || level === 0) && (
|
|
227
|
+
<OpenButton
|
|
228
|
+
tabIndex={-1}
|
|
229
|
+
isOpen={isOpen}
|
|
230
|
+
onClick={() => {
|
|
231
|
+
ref.current?.focus();
|
|
232
|
+
if (isOpen) {
|
|
233
|
+
onCloseFolder(id);
|
|
234
|
+
} else {
|
|
235
|
+
onOpenFolder(id);
|
|
236
|
+
}
|
|
237
|
+
}}>
|
|
238
|
+
<ArrowDropDownRounded />
|
|
239
|
+
</OpenButton>
|
|
219
240
|
)}
|
|
220
|
-
|
|
241
|
+
<StyledName>{name}</StyledName>
|
|
242
|
+
</FolderNameLink>
|
|
221
243
|
);
|
|
222
244
|
};
|
|
223
245
|
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
|
-
import { animations
|
|
11
|
+
import { animations } from '@ndla/core';
|
|
12
12
|
import FolderItem from './FolderItem';
|
|
13
13
|
import FolderNameInput from './FolderNameInput';
|
|
14
|
-
import { CommonFolderItemsProps, FolderType } from './types';
|
|
14
|
+
import { CommonFolderItemsProps, FolderType, TreeStructureType } from './types';
|
|
15
|
+
import NavigationLink from './NavigationLink';
|
|
15
16
|
|
|
16
|
-
const StyledUL = styled.ul
|
|
17
|
+
const StyledUL = styled.ul`
|
|
17
18
|
${animations.fadeInLeft(animations.durations.fast)};
|
|
18
19
|
animation-fill-mode: forwards;
|
|
19
20
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -22,18 +23,22 @@ const StyledUL = styled.ul<{ firstLevel?: boolean }>`
|
|
|
22
23
|
list-style: none;
|
|
23
24
|
margin: 0;
|
|
24
25
|
padding: 0;
|
|
25
|
-
margin-left: ${({ firstLevel }) => (firstLevel ? `-${spacing.xsmall}` : spacing.small)};
|
|
26
26
|
`;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
interface StyledLiProps {
|
|
29
|
+
type?: TreeStructureType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const StyledLI = styled.li<StyledLiProps>`
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: ${({ type }) => type === 'navigation' && 'flex-start'};
|
|
29
36
|
margin: 0;
|
|
30
37
|
padding: 0;
|
|
31
38
|
`;
|
|
32
39
|
|
|
33
40
|
export interface FolderItemsProps extends CommonFolderItemsProps {
|
|
34
41
|
folders: FolderType[];
|
|
35
|
-
editable?: boolean;
|
|
36
|
-
maximumLevelsOfFoldersAllowed: number;
|
|
37
42
|
newFolderParentId: string | undefined;
|
|
38
43
|
onCancelNewFolder: () => void;
|
|
39
44
|
onSaveNewFolder: (name: string, parentId: string) => void;
|
|
@@ -41,56 +46,59 @@ export interface FolderItemsProps extends CommonFolderItemsProps {
|
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
const FolderItems = ({
|
|
44
|
-
editable,
|
|
45
49
|
folders,
|
|
46
50
|
level,
|
|
47
51
|
loading,
|
|
48
|
-
maximumLevelsOfFoldersAllowed,
|
|
49
52
|
newFolderParentId,
|
|
50
53
|
onCancelNewFolder,
|
|
51
54
|
onSaveNewFolder,
|
|
52
55
|
openFolders,
|
|
56
|
+
type,
|
|
53
57
|
...rest
|
|
54
58
|
}: FolderItemsProps) => (
|
|
55
|
-
<StyledUL role=
|
|
59
|
+
<StyledUL role={level === 0 ? 'tree' : 'group'}>
|
|
56
60
|
{folders.map((folder) => {
|
|
57
61
|
const { subfolders, id } = folder;
|
|
58
62
|
const isOpen = openFolders?.includes(id);
|
|
59
63
|
|
|
60
64
|
return (
|
|
61
|
-
<StyledLI key={id} role="treeitem">
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
65
|
+
<StyledLI key={id} role="treeitem" type={type}>
|
|
66
|
+
{folder.isNavigation ? (
|
|
67
|
+
<NavigationLink folder={folder} isOpen={isOpen} level={level} type={type} loading={loading} {...rest} />
|
|
68
|
+
) : (
|
|
69
|
+
<>
|
|
70
|
+
<FolderItem
|
|
71
|
+
folder={folder}
|
|
72
|
+
isOpen={isOpen}
|
|
73
|
+
level={level}
|
|
74
|
+
loading={loading}
|
|
75
|
+
type={type}
|
|
76
|
+
isCreatingFolder={newFolderParentId === folder.id}
|
|
77
|
+
{...rest}
|
|
78
|
+
/>
|
|
79
|
+
{newFolderParentId === id && (
|
|
80
|
+
<FolderNameInput
|
|
81
|
+
loading={loading}
|
|
82
|
+
level={level}
|
|
83
|
+
onCancelNewFolder={onCancelNewFolder}
|
|
84
|
+
onSaveNewFolder={onSaveNewFolder}
|
|
85
|
+
parentId={newFolderParentId}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
{subfolders && isOpen && (
|
|
89
|
+
<FolderItems
|
|
90
|
+
folders={subfolders}
|
|
91
|
+
level={level + 1}
|
|
92
|
+
loading={loading}
|
|
93
|
+
type={type}
|
|
94
|
+
newFolderParentId={newFolderParentId}
|
|
95
|
+
onCancelNewFolder={onCancelNewFolder}
|
|
96
|
+
onSaveNewFolder={onSaveNewFolder}
|
|
97
|
+
openFolders={openFolders}
|
|
98
|
+
{...rest}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
</>
|
|
94
102
|
)}
|
|
95
103
|
</StyledLI>
|
|
96
104
|
);
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import React, { useEffect, useState, useRef, ChangeEvent, KeyboardEvent } from 'react';
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
|
-
import {
|
|
12
|
-
import { ArrowDropDown as ArrowDropDownRaw } from '@ndla/icons/common';
|
|
13
|
-
import { spacing, colors, misc, animations } from '@ndla/core';
|
|
11
|
+
import { spacing, colors, animations, spacingUnit } from '@ndla/core';
|
|
14
12
|
import { useTranslation } from 'react-i18next';
|
|
15
13
|
import { isMobile } from 'react-device-detect';
|
|
16
14
|
import { Spinner } from '@ndla/icons';
|
|
@@ -18,18 +16,22 @@ import { IconButton } from '@ndla/button';
|
|
|
18
16
|
import { Cross } from '@ndla/icons/action';
|
|
19
17
|
import { Done } from '@ndla/icons/editor';
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`;
|
|
19
|
+
// Source: https://kovart.github.io/dashed-border-generator/
|
|
20
|
+
const borderStyle = `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='${encodeURIComponent(
|
|
21
|
+
colors.brand.tertiary,
|
|
22
|
+
)}' stroke-width='2' stroke-dasharray='8%2c8' stroke-dashoffset='4' stroke-linecap='square'/%3e%3c/svg%3e")`;
|
|
25
23
|
|
|
26
24
|
const NewFolderWrapper = styled.div`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
background: linear-gradient(
|
|
26
|
+
to bottom,
|
|
27
|
+
${colors.white} 0%,
|
|
28
|
+
${colors.white} 15%,
|
|
29
|
+
${colors.brand.lighter} 15%,
|
|
30
|
+
${colors.brand.lighter} 85%,
|
|
31
|
+
${colors.white} 85%,
|
|
32
|
+
${colors.white} 100%
|
|
33
|
+
);
|
|
34
|
+
background-size: auto 100%;
|
|
33
35
|
`;
|
|
34
36
|
|
|
35
37
|
const Row = styled.div`
|
|
@@ -38,23 +40,31 @@ const Row = styled.div`
|
|
|
38
40
|
padding-right: ${spacing.xsmall};
|
|
39
41
|
`;
|
|
40
42
|
|
|
41
|
-
const InputWrapper = styled.div<{
|
|
43
|
+
const InputWrapper = styled.div<{ level: number }>`
|
|
42
44
|
display: flex;
|
|
43
45
|
margin: ${spacing.xxsmall} 0;
|
|
46
|
+
margin-left: ${({ level }) => 0.75 * spacingUnit * level + 2 * spacingUnit}px;
|
|
47
|
+
margin-right: ${spacing.normal};
|
|
44
48
|
align-items: center;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
border-radius: ${misc.borderRadius};
|
|
49
|
+
background-color: ${colors.white};
|
|
50
|
+
background-image: ${borderStyle};
|
|
48
51
|
color: ${colors.brand.primary};
|
|
52
|
+
|
|
53
|
+
${animations.fadeInLeft(animations.durations.fast)};
|
|
54
|
+
animation-fill-mode: forwards;
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
animation: none;
|
|
57
|
+
}
|
|
49
58
|
`;
|
|
50
59
|
|
|
51
60
|
const StyledInput = styled.input`
|
|
61
|
+
padding: ${spacing.small};
|
|
52
62
|
flex-grow: 1;
|
|
53
63
|
border: 0;
|
|
54
64
|
outline: none;
|
|
55
65
|
min-width: 0;
|
|
56
66
|
background: transparent;
|
|
57
|
-
color: ${colors.
|
|
67
|
+
color: ${colors.brand.primary};
|
|
58
68
|
scroll-margin-top: 100px;
|
|
59
69
|
`;
|
|
60
70
|
|
|
@@ -63,11 +73,12 @@ interface FolderNameInputProps {
|
|
|
63
73
|
parentId: string;
|
|
64
74
|
onCancelNewFolder: () => void;
|
|
65
75
|
loading?: boolean;
|
|
76
|
+
level: number;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading }: FolderNameInputProps) => {
|
|
79
|
+
const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading, level }: FolderNameInputProps) => {
|
|
69
80
|
const { t } = useTranslation();
|
|
70
|
-
const [name, setName] = useState<string>(
|
|
81
|
+
const [name, setName] = useState<string>('');
|
|
71
82
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
72
83
|
|
|
73
84
|
useEffect(() => {
|
|
@@ -75,15 +86,14 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
|
|
|
75
86
|
if (isMobile) {
|
|
76
87
|
inputRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
77
88
|
}
|
|
78
|
-
|
|
89
|
+
return () => {
|
|
90
|
+
onCancelNewFolder();
|
|
91
|
+
};
|
|
92
|
+
}, [onCancelNewFolder]);
|
|
79
93
|
|
|
80
94
|
return (
|
|
81
95
|
<NewFolderWrapper>
|
|
82
|
-
<InputWrapper
|
|
83
|
-
<Row>
|
|
84
|
-
<ArrowRight />
|
|
85
|
-
<FolderOutlined />
|
|
86
|
-
</Row>
|
|
96
|
+
<InputWrapper level={level}>
|
|
87
97
|
<StyledInput
|
|
88
98
|
ref={inputRef}
|
|
89
99
|
autoFocus
|
|
@@ -96,6 +106,10 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
|
|
|
96
106
|
onCancelNewFolder();
|
|
97
107
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
98
108
|
e.preventDefault();
|
|
109
|
+
if (name === '') {
|
|
110
|
+
onCancelNewFolder();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
99
113
|
onSaveNewFolder(name, parentId);
|
|
100
114
|
}
|
|
101
115
|
}}
|
|
@@ -104,12 +118,13 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
|
|
|
104
118
|
<Row>
|
|
105
119
|
{!loading ? (
|
|
106
120
|
<>
|
|
107
|
-
<IconButton aria-label={t('close')} size="
|
|
121
|
+
<IconButton aria-label={t('close')} title={t('close')} size="small" ghostPill onClick={onCancelNewFolder}>
|
|
108
122
|
<Cross />
|
|
109
123
|
</IconButton>
|
|
110
124
|
<IconButton
|
|
111
125
|
aria-label={t('save')}
|
|
112
|
-
|
|
126
|
+
title={t('save')}
|
|
127
|
+
size="small"
|
|
113
128
|
ghostPill
|
|
114
129
|
onClick={() => onSaveNewFolder(name, parentId)}>
|
|
115
130
|
<Done />
|