@ndla/ui 25.3.0 → 26.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/es/LanguageSelector/LanguageSelector.js +12 -7
- 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 +2 -0
- package/es/locale/messages-nb.js +2 -0
- package/es/locale/messages-nn.js +2 -0
- package/es/locale/messages-se.js +2 -0
- package/es/locale/messages-sma.js +2 -0
- package/lib/LanguageSelector/LanguageSelector.js +13 -7
- 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 +2 -0
- package/lib/locale/messages-en.js +2 -0
- package/lib/locale/messages-nb.d.ts +2 -0
- package/lib/locale/messages-nb.js +2 -0
- package/lib/locale/messages-nn.d.ts +2 -0
- package/lib/locale/messages-nn.js +2 -0
- package/lib/locale/messages-se.d.ts +2 -0
- package/lib/locale/messages-se.js +2 -0
- package/lib/locale/messages-sma.d.ts +2 -0
- package/lib/locale/messages-sma.js +2 -0
- package/package.json +11 -11
- package/src/LanguageSelector/LanguageSelector.tsx +4 -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 +2 -0
- package/src/locale/messages-nb.ts +2 -0
- package/src/locale/messages-nn.ts +2 -0
- package/src/locale/messages-se.ts +2 -0
- package/src/locale/messages-sma.ts +2 -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
|
@@ -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 />
|
|
@@ -0,0 +1,100 @@
|
|
|
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 styled from '@emotion/styled';
|
|
10
|
+
import { colors, fonts, spacing } from '@ndla/core';
|
|
11
|
+
import React, { KeyboardEvent, useEffect, useRef } from 'react';
|
|
12
|
+
import SafeLink from '@ndla/safelink';
|
|
13
|
+
import { arrowNavigation } from './arrowNavigation';
|
|
14
|
+
import { CommonFolderItemsProps, FolderType } from './types';
|
|
15
|
+
|
|
16
|
+
interface StyledProps {
|
|
17
|
+
selected?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const StyledSafeLink = styled(SafeLink)<StyledProps>`
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: ${spacing.medium} 1fr;
|
|
23
|
+
align-items: center;
|
|
24
|
+
padding: ${spacing.xxsmall};
|
|
25
|
+
margin: ${spacing.xsmall} 0;
|
|
26
|
+
gap: ${spacing.xxsmall};
|
|
27
|
+
box-shadow: none;
|
|
28
|
+
|
|
29
|
+
color: ${({ selected }) => (selected ? colors.brand.primary : colors.text.primary)};
|
|
30
|
+
font-weight: ${({ selected }) => (selected ? fonts.weight.semibold : fonts.weight.normal)};
|
|
31
|
+
${fonts.sizes('16px')};
|
|
32
|
+
|
|
33
|
+
:hover,
|
|
34
|
+
:focus {
|
|
35
|
+
color: ${colors.brand.primary};
|
|
36
|
+
}
|
|
37
|
+
svg {
|
|
38
|
+
height: 26px;
|
|
39
|
+
width: 26px;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const IconWrapper = styled.span`
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
interface Props extends CommonFolderItemsProps {
|
|
50
|
+
isOpen: boolean;
|
|
51
|
+
folder: FolderType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const NavigationLink = ({
|
|
55
|
+
loading,
|
|
56
|
+
folder,
|
|
57
|
+
selectedFolder,
|
|
58
|
+
focusedFolderId,
|
|
59
|
+
setSelectedFolder,
|
|
60
|
+
setFocusedId,
|
|
61
|
+
visibleFolders,
|
|
62
|
+
onOpenFolder,
|
|
63
|
+
onCloseFolder,
|
|
64
|
+
}: Props) => {
|
|
65
|
+
const { id, icon, name } = folder;
|
|
66
|
+
const selected = selectedFolder && selectedFolder.id === id;
|
|
67
|
+
const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
|
|
68
|
+
const focused = focusedFolderId === id;
|
|
69
|
+
|
|
70
|
+
const handleClick = () => {
|
|
71
|
+
if (!selected) {
|
|
72
|
+
setSelectedFolder(folder);
|
|
73
|
+
setFocusedId(id);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (focusedFolderId === id) {
|
|
79
|
+
ref.current?.focus();
|
|
80
|
+
}
|
|
81
|
+
}, [focusedFolderId, ref, id]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<StyledSafeLink
|
|
85
|
+
ref={ref}
|
|
86
|
+
onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
|
|
87
|
+
arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)
|
|
88
|
+
}
|
|
89
|
+
tabIndex={selected || focused ? 0 : -1}
|
|
90
|
+
selected={selected}
|
|
91
|
+
onFocus={() => setFocusedId(id)}
|
|
92
|
+
onClick={handleClick}
|
|
93
|
+
to={loading ? '' : `/minndla/${id}`}>
|
|
94
|
+
<IconWrapper>{icon}</IconWrapper>
|
|
95
|
+
{name}
|
|
96
|
+
</StyledSafeLink>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default NavigationLink;
|
|
@@ -6,18 +6,20 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect, useState, useMemo } from 'react';
|
|
10
|
-
import {
|
|
9
|
+
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
|
10
|
+
import { ButtonV2 as Button, IconButtonDualStates } from '@ndla/button';
|
|
11
|
+
import { Plus } from '@ndla/icons/action';
|
|
12
|
+
import { ChevronDown, ChevronUp } from '@ndla/icons/common';
|
|
11
13
|
import Tooltip from '@ndla/tooltip';
|
|
12
14
|
import { useTranslation } from 'react-i18next';
|
|
13
15
|
import styled from '@emotion/styled';
|
|
14
|
-
import {
|
|
16
|
+
import { colors, fonts, misc, spacing } from '@ndla/core';
|
|
17
|
+
import { css } from '@emotion/core';
|
|
15
18
|
import { uniq } from 'lodash';
|
|
16
19
|
import { IFolder } from '@ndla/types-learningpath-api';
|
|
17
|
-
import TreeStructureStyledWrapper from './TreeStructureWrapper';
|
|
18
20
|
import FolderItems from './FolderItems';
|
|
19
21
|
import { flattenFolders } from './helperFunctions';
|
|
20
|
-
import { CommonTreeStructureProps, FolderType } from './types';
|
|
22
|
+
import { CommonTreeStructureProps, FolderType, TreeStructureType } from './types';
|
|
21
23
|
|
|
22
24
|
export const MAX_LEVEL_FOR_FOLDERS = 4;
|
|
23
25
|
|
|
@@ -25,15 +27,86 @@ const StyledLabel = styled.label`
|
|
|
25
27
|
font-weight: ${fonts.weight.semibold};
|
|
26
28
|
`;
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
interface StyledRowProps {
|
|
31
|
+
isOpen: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const StyledRow = styled.div<StyledRowProps>`
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
padding: ${spacing.xxsmall};
|
|
38
|
+
border-bottom: ${({ isOpen }) => isOpen && `1px solid ${colors.brand.tertiary}`};
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const StyledTreeStructure = styled.div`
|
|
42
|
+
flex: 1;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const TreeStructureWrapper = styled.div<{ type: TreeStructureType }>`
|
|
29
48
|
display: flex;
|
|
30
|
-
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
${({ type }) =>
|
|
51
|
+
(type === 'normal' || type === 'picker') &&
|
|
52
|
+
css`
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
border: 1px solid ${colors.brand.neutral7};
|
|
55
|
+
border-radius: ${misc.borderRadius};
|
|
56
|
+
scroll-behavior: smooth;
|
|
57
|
+
`}
|
|
58
|
+
transition: ${misc.transition.default};
|
|
59
|
+
&:focus-within {
|
|
60
|
+
border-color: ${colors.brand.tertiary};
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
interface ScrollableDivProps {
|
|
64
|
+
type: TreeStructureType;
|
|
65
|
+
}
|
|
66
|
+
const ScrollableDiv = styled.div<ScrollableDivProps>`
|
|
67
|
+
${({ type }) =>
|
|
68
|
+
(type === 'picker' || type === 'normal') &&
|
|
69
|
+
css`
|
|
70
|
+
overflow: overlay;
|
|
71
|
+
::-webkit-scrollbar {
|
|
72
|
+
width: ${spacing.small};
|
|
73
|
+
}
|
|
74
|
+
::-webkit-scrollbar-thumb {
|
|
75
|
+
border: 4px solid transparent;
|
|
76
|
+
border-radius: 14px;
|
|
77
|
+
background-clip: padding-box;
|
|
78
|
+
padding: 0 4px;
|
|
79
|
+
background-color: ${colors.brand.neutral7};
|
|
80
|
+
}
|
|
81
|
+
`}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const StyledSelectedFolder = styled(Button)`
|
|
85
|
+
flex: 1;
|
|
86
|
+
justify-content: flex-start;
|
|
87
|
+
:hover,
|
|
88
|
+
:focus {
|
|
89
|
+
background: none;
|
|
90
|
+
box-shadow: none;
|
|
91
|
+
border-color: transparent;
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const StyledAddFolderButton = styled(Button)`
|
|
96
|
+
&,
|
|
97
|
+
&:disabled {
|
|
98
|
+
border-color: transparent;
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const StyledPlus = styled(Plus)`
|
|
103
|
+
height: 24px;
|
|
104
|
+
width: 24px;
|
|
31
105
|
`;
|
|
32
106
|
|
|
33
107
|
export interface TreeStructureProps extends CommonTreeStructureProps {
|
|
34
108
|
defaultOpenFolders?: string[];
|
|
35
109
|
folders: FolderType[];
|
|
36
|
-
editable?: boolean;
|
|
37
110
|
label?: string;
|
|
38
111
|
maximumLevelsOfFoldersAllowed?: number;
|
|
39
112
|
onNewFolder?: (name: string, parentId: string) => Promise<IFolder>;
|
|
@@ -41,10 +114,7 @@ export interface TreeStructureProps extends CommonTreeStructureProps {
|
|
|
41
114
|
|
|
42
115
|
const TreeStructure = ({
|
|
43
116
|
defaultOpenFolders,
|
|
44
|
-
editable,
|
|
45
|
-
menuItems,
|
|
46
117
|
folders,
|
|
47
|
-
framed,
|
|
48
118
|
label,
|
|
49
119
|
loading,
|
|
50
120
|
maximumLevelsOfFoldersAllowed = MAX_LEVEL_FOR_FOLDERS,
|
|
@@ -52,9 +122,12 @@ const TreeStructure = ({
|
|
|
52
122
|
onSelectFolder,
|
|
53
123
|
openOnFolderClick,
|
|
54
124
|
targetResource,
|
|
125
|
+
type = 'normal',
|
|
55
126
|
}: TreeStructureProps) => {
|
|
56
127
|
const { t } = useTranslation();
|
|
57
128
|
|
|
129
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
130
|
+
|
|
58
131
|
const defaultSelectedFolderId = defaultOpenFolders && defaultOpenFolders[defaultOpenFolders.length - 1];
|
|
59
132
|
|
|
60
133
|
const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
|
|
@@ -62,10 +135,25 @@ const TreeStructure = ({
|
|
|
62
135
|
const [newFolderParentId, setNewFolderParentId] = useState<string | undefined>();
|
|
63
136
|
const [focusedId, setFocusedId] = useState<string | undefined>();
|
|
64
137
|
const [selectedFolder, setSelectedFolder] = useState<FolderType | undefined>();
|
|
138
|
+
const [showTree, setShowTree] = useState(type !== 'picker');
|
|
65
139
|
|
|
66
140
|
const flattenedFolders = useMemo(() => flattenFolders(folders, openFolders), [folders, openFolders]);
|
|
67
141
|
const visibleFolderIds = flattenedFolders.map((folder) => folder.id);
|
|
68
142
|
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
145
|
+
if (e.target instanceof Element && ref.current && !ref.current.contains(e.target)) {
|
|
146
|
+
setShowTree(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
if (type === 'picker') {
|
|
150
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
151
|
+
return () => {
|
|
152
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}, [ref, type]);
|
|
156
|
+
|
|
69
157
|
useEffect(() => {
|
|
70
158
|
if (defaultOpenFolders) {
|
|
71
159
|
if (!defaultOpenFolders.every((element) => openFolders.includes(element))) {
|
|
@@ -77,6 +165,10 @@ const TreeStructure = ({
|
|
|
77
165
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
166
|
}, [defaultOpenFolders]);
|
|
79
167
|
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
setNewFolderParentId(undefined);
|
|
170
|
+
}, [selectedFolder]);
|
|
171
|
+
|
|
80
172
|
useEffect(() => {
|
|
81
173
|
if (defaultSelectedFolderId !== undefined) {
|
|
82
174
|
const selected = flattenFolders(folders).find((folder) => folder.id === defaultSelectedFolderId);
|
|
@@ -114,9 +206,9 @@ const TreeStructure = ({
|
|
|
114
206
|
};
|
|
115
207
|
|
|
116
208
|
const onSaveNewFolder = (name: string, parentId: string) => {
|
|
117
|
-
setNewFolderParentId(undefined);
|
|
118
209
|
onNewFolder?.(name, parentId).then((newFolder) => {
|
|
119
210
|
if (newFolder) {
|
|
211
|
+
setNewFolderParentId?.(undefined);
|
|
120
212
|
setSelectedFolder(newFolder);
|
|
121
213
|
onSelectFolder?.(newFolder.id);
|
|
122
214
|
setFocusedId(newFolder.id);
|
|
@@ -126,60 +218,94 @@ const TreeStructure = ({
|
|
|
126
218
|
};
|
|
127
219
|
|
|
128
220
|
const onCancelNewFolder = () => {
|
|
129
|
-
setNewFolderParentId(undefined);
|
|
221
|
+
setNewFolderParentId?.(undefined);
|
|
130
222
|
};
|
|
131
223
|
|
|
132
|
-
const canAddFolder =
|
|
133
|
-
editable && selectedFolder && selectedFolder?.breadcrumbs.length < (maximumLevelsOfFoldersAllowed || 1);
|
|
224
|
+
const canAddFolder = selectedFolder && selectedFolder?.breadcrumbs.length < (maximumLevelsOfFoldersAllowed || 1);
|
|
134
225
|
|
|
135
226
|
return (
|
|
136
|
-
<
|
|
227
|
+
<StyledTreeStructure ref={ref}>
|
|
137
228
|
{label && <StyledLabel>{label}</StyledLabel>}
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{t('
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
229
|
+
<TreeStructureWrapper aria-label={label} type={type}>
|
|
230
|
+
{type === 'picker' && (
|
|
231
|
+
<StyledRow isOpen={showTree}>
|
|
232
|
+
<StyledSelectedFolder
|
|
233
|
+
variant="ghost"
|
|
234
|
+
colorTheme="light"
|
|
235
|
+
fontWeight="normal"
|
|
236
|
+
shape="sharp"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
setShowTree(!showTree);
|
|
239
|
+
}}>
|
|
240
|
+
{selectedFolder?.name}
|
|
241
|
+
</StyledSelectedFolder>
|
|
242
|
+
{onNewFolder && showTree && (
|
|
243
|
+
<Tooltip
|
|
244
|
+
tooltip={
|
|
245
|
+
canAddFolder
|
|
246
|
+
? t('myNdla.newFolderUnder', {
|
|
247
|
+
folderName: selectedFolder?.name,
|
|
248
|
+
})
|
|
249
|
+
: t('treeStructure.maxFoldersAlreadyAdded')
|
|
250
|
+
}>
|
|
251
|
+
<StyledAddFolderButton
|
|
252
|
+
variant="outline"
|
|
253
|
+
shape="pill"
|
|
254
|
+
disabled={!canAddFolder}
|
|
255
|
+
aria-label={
|
|
256
|
+
canAddFolder
|
|
257
|
+
? t('myNdla.newFolderUnder', {
|
|
258
|
+
folderName: selectedFolder?.name,
|
|
259
|
+
})
|
|
260
|
+
: t('treeStructure.maxFoldersAlreadyAdded')
|
|
261
|
+
}
|
|
262
|
+
onClick={() => setNewFolderParentId(selectedFolder?.id)}>
|
|
263
|
+
<StyledPlus /> {t('myNdla.newFolder')}
|
|
264
|
+
</StyledAddFolderButton>
|
|
265
|
+
</Tooltip>
|
|
266
|
+
)}
|
|
267
|
+
<IconButtonDualStates
|
|
268
|
+
ariaLabelActive={t('treeStructure.hideFolders')}
|
|
269
|
+
ariaLabelInActive={t('treeStructure.showFolders')}
|
|
270
|
+
active={showTree}
|
|
271
|
+
variant="ghost"
|
|
272
|
+
colorTheme="greyLighter"
|
|
273
|
+
inactiveIcon={<ChevronDown />}
|
|
274
|
+
activeIcon={<ChevronUp />}
|
|
275
|
+
size="small"
|
|
276
|
+
onClick={() => {
|
|
277
|
+
setShowTree(!showTree);
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
</StyledRow>
|
|
281
|
+
)}
|
|
282
|
+
{showTree && (
|
|
283
|
+
<ScrollableDiv type={type}>
|
|
284
|
+
<FolderItems
|
|
285
|
+
focusedFolderId={focusedId}
|
|
286
|
+
folders={folders}
|
|
287
|
+
level={0}
|
|
288
|
+
loading={loading}
|
|
289
|
+
selectedFolder={selectedFolder}
|
|
290
|
+
maxLevel={maximumLevelsOfFoldersAllowed}
|
|
291
|
+
newFolderParentId={newFolderParentId}
|
|
292
|
+
onCancelNewFolder={onCancelNewFolder}
|
|
293
|
+
onCloseFolder={onCloseFolder}
|
|
294
|
+
onOpenFolder={onOpenFolder}
|
|
295
|
+
onSaveNewFolder={onSaveNewFolder}
|
|
296
|
+
onSelectFolder={onSelectFolder}
|
|
297
|
+
openFolders={openFolders}
|
|
298
|
+
openOnFolderClick={openOnFolderClick}
|
|
299
|
+
setFocusedId={setFocusedId}
|
|
300
|
+
setSelectedFolder={setSelectedFolder}
|
|
301
|
+
targetResource={targetResource}
|
|
302
|
+
visibleFolders={visibleFolderIds}
|
|
303
|
+
type={type}
|
|
304
|
+
/>
|
|
305
|
+
</ScrollableDiv>
|
|
306
|
+
)}
|
|
307
|
+
</TreeStructureWrapper>
|
|
308
|
+
</StyledTreeStructure>
|
|
183
309
|
);
|
|
184
310
|
};
|
|
185
311
|
|
|
@@ -12,8 +12,11 @@ import { MenuItemProps } from '@ndla/button';
|
|
|
12
12
|
|
|
13
13
|
export interface FolderType extends IFolder {
|
|
14
14
|
icon?: ReactNode;
|
|
15
|
+
isNavigation?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export type TreeStructureType = 'normal' | 'navigation' | 'picker';
|
|
19
|
+
|
|
17
20
|
export interface TreeStructureMenuProps extends Omit<MenuItemProps, 'onClick'> {
|
|
18
21
|
onClick: (e: MouseEvent<HTMLDivElement> | undefined, folder: FolderType) => void;
|
|
19
22
|
}
|
|
@@ -22,14 +25,15 @@ export interface CommonTreeStructureProps {
|
|
|
22
25
|
loading?: boolean;
|
|
23
26
|
onSelectFolder?: (id: string) => void;
|
|
24
27
|
openOnFolderClick?: boolean;
|
|
25
|
-
menuItems?: TreeStructureMenuProps[];
|
|
26
28
|
targetResource?: IResource;
|
|
27
29
|
framed?: boolean;
|
|
30
|
+
type: TreeStructureType;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface CommonFolderItemsProps extends CommonTreeStructureProps {
|
|
31
34
|
focusedFolderId?: string;
|
|
32
35
|
level: number;
|
|
36
|
+
maxLevel: number;
|
|
33
37
|
selectedFolder?: FolderType;
|
|
34
38
|
onCloseFolder: (id: string) => void;
|
|
35
39
|
onOpenFolder: (id: string) => void;
|
|
@@ -19,6 +19,8 @@ const messages = {
|
|
|
19
19
|
edit: 'Rievdat máhpa nama',
|
|
20
20
|
delete: 'Sihko',
|
|
21
21
|
},
|
|
22
|
+
hideFolders: 'Skjul alle mapper',
|
|
23
|
+
showFolders: 'Vis alle mapper',
|
|
22
24
|
createFolder: 'Ráhkat máhpa',
|
|
23
25
|
maxFoldersAlreadyAdded: 'Vuollemáhpaid badjerádji lea olahuvvon',
|
|
24
26
|
newFolder: {
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import _styled from "@emotion/styled-base";
|
|
2
|
-
import { css } from '@emotion/core';
|
|
3
|
-
import { colors, misc, spacing } from '@ndla/core';
|
|
4
|
-
|
|
5
|
-
var TreeStructureWrapper = _styled("div", {
|
|
6
|
-
target: "ez53f4u0",
|
|
7
|
-
label: "TreeStructureWrapper"
|
|
8
|
-
})("padding:", spacing.xsmall, ";", function (_ref) {
|
|
9
|
-
var framed = _ref.framed;
|
|
10
|
-
return framed && /*#__PURE__*/css("border:1px solid ", colors.brand.neutral7, ";border-radius:", misc.borderRadius, ";max-height:400px;overflow-y:scroll;scroll-behavior:smooth;padding:", spacing.small, ";;label:TreeStructureWrapper;" + (process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlRyZWVTdHJ1Y3R1cmVXcmFwcGVyLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFnQk8iLCJmaWxlIjoiVHJlZVN0cnVjdHVyZVdyYXBwZXIudHN4Iiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIDIwMjItcHJlc2VudCwgTkRMQS5cbiAqXG4gKiBUaGlzIHNvdXJjZSBjb2RlIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBHUEx2MyBsaWNlbnNlIGZvdW5kIGluIHRoZVxuICogTElDRU5TRSBmaWxlIGluIHRoZSByb290IGRpcmVjdG9yeSBvZiB0aGlzIHNvdXJjZSB0cmVlLlxuICpcbiAqL1xuXG5pbXBvcnQgc3R5bGVkIGZyb20gJ0BlbW90aW9uL3N0eWxlZCc7XG5pbXBvcnQgeyBjc3MgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcbmltcG9ydCB7IGNvbG9ycywgbWlzYywgc3BhY2luZyB9IGZyb20gJ0BuZGxhL2NvcmUnO1xuXG5jb25zdCBUcmVlU3RydWN0dXJlV3JhcHBlciA9IHN0eWxlZC5kaXY8eyBmcmFtZWQ/OiBib29sZWFuIH0+YFxuICBwYWRkaW5nOiAke3NwYWNpbmcueHNtYWxsfTtcbiAgJHsoeyBmcmFtZWQgfSkgPT5cbiAgICBmcmFtZWQgJiZcbiAgICBjc3NgXG4gICAgICBib3JkZXI6IDFweCBzb2xpZCAke2NvbG9ycy5icmFuZC5uZXV0cmFsN307XG4gICAgICBib3JkZXItcmFkaXVzOiAke21pc2MuYm9yZGVyUmFkaXVzfTtcbiAgICAgIG1heC1oZWlnaHQ6IDQwMHB4O1xuICAgICAgb3ZlcmZsb3cteTogc2Nyb2xsO1xuICAgICAgc2Nyb2xsLWJlaGF2aW9yOiBzbW9vdGg7XG4gICAgICBwYWRkaW5nOiAke3NwYWNpbmcuc21hbGx9O1xuICAgIGB9XG4gIHRyYW5zaXRpb246ICR7bWlzYy50cmFuc2l0aW9uLmRlZmF1bHR9O1xuICAmOmZvY3VzLXdpdGhpbiB7XG4gICAgYm9yZGVyLWNvbG9yOiAke2NvbG9ycy5icmFuZC5wcmltYXJ5fTtcbiAgfVxuYDtcblxuZXhwb3J0IGRlZmF1bHQgVHJlZVN0cnVjdHVyZVdyYXBwZXI7XG4iXX0= */"));
|
|
11
|
-
}, " transition:", misc.transition["default"], ";&:focus-within{border-color:", colors.brand.primary, ";}" + (process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlRyZWVTdHJ1Y3R1cmVXcmFwcGVyLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFZNkQiLCJmaWxlIjoiVHJlZVN0cnVjdHVyZVdyYXBwZXIudHN4Iiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIDIwMjItcHJlc2VudCwgTkRMQS5cbiAqXG4gKiBUaGlzIHNvdXJjZSBjb2RlIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBHUEx2MyBsaWNlbnNlIGZvdW5kIGluIHRoZVxuICogTElDRU5TRSBmaWxlIGluIHRoZSByb290IGRpcmVjdG9yeSBvZiB0aGlzIHNvdXJjZSB0cmVlLlxuICpcbiAqL1xuXG5pbXBvcnQgc3R5bGVkIGZyb20gJ0BlbW90aW9uL3N0eWxlZCc7XG5pbXBvcnQgeyBjc3MgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcbmltcG9ydCB7IGNvbG9ycywgbWlzYywgc3BhY2luZyB9IGZyb20gJ0BuZGxhL2NvcmUnO1xuXG5jb25zdCBUcmVlU3RydWN0dXJlV3JhcHBlciA9IHN0eWxlZC5kaXY8eyBmcmFtZWQ/OiBib29sZWFuIH0+YFxuICBwYWRkaW5nOiAke3NwYWNpbmcueHNtYWxsfTtcbiAgJHsoeyBmcmFtZWQgfSkgPT5cbiAgICBmcmFtZWQgJiZcbiAgICBjc3NgXG4gICAgICBib3JkZXI6IDFweCBzb2xpZCAke2NvbG9ycy5icmFuZC5uZXV0cmFsN307XG4gICAgICBib3JkZXItcmFkaXVzOiAke21pc2MuYm9yZGVyUmFkaXVzfTtcbiAgICAgIG1heC1oZWlnaHQ6IDQwMHB4O1xuICAgICAgb3ZlcmZsb3cteTogc2Nyb2xsO1xuICAgICAgc2Nyb2xsLWJlaGF2aW9yOiBzbW9vdGg7XG4gICAgICBwYWRkaW5nOiAke3NwYWNpbmcuc21hbGx9O1xuICAgIGB9XG4gIHRyYW5zaXRpb246ICR7bWlzYy50cmFuc2l0aW9uLmRlZmF1bHR9O1xuICAmOmZvY3VzLXdpdGhpbiB7XG4gICAgYm9yZGVyLWNvbG9yOiAke2NvbG9ycy5icmFuZC5wcmltYXJ5fTtcbiAgfVxuYDtcblxuZXhwb3J0IGRlZmF1bHQgVHJlZVN0cnVjdHVyZVdyYXBwZXI7XG4iXX0= */"));
|
|
12
|
-
|
|
13
|
-
export default TreeStructureWrapper;
|