@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.
Files changed (50) hide show
  1. package/es/LanguageSelector/LanguageSelector.js +12 -7
  2. package/es/TreeStructure/FolderItem.js +110 -94
  3. package/es/TreeStructure/FolderItems.js +26 -30
  4. package/es/TreeStructure/FolderNameInput.js +35 -27
  5. package/es/TreeStructure/NavigationLink.js +81 -0
  6. package/es/TreeStructure/TreeStructure.js +169 -45
  7. package/es/locale/messages-en.js +2 -0
  8. package/es/locale/messages-nb.js +2 -0
  9. package/es/locale/messages-nn.js +2 -0
  10. package/es/locale/messages-se.js +2 -0
  11. package/es/locale/messages-sma.js +2 -0
  12. package/lib/LanguageSelector/LanguageSelector.js +13 -7
  13. package/lib/TreeStructure/FolderItem.d.ts +2 -3
  14. package/lib/TreeStructure/FolderItem.js +107 -92
  15. package/lib/TreeStructure/FolderItems.d.ts +1 -3
  16. package/lib/TreeStructure/FolderItems.js +26 -29
  17. package/lib/TreeStructure/FolderNameInput.d.ts +2 -1
  18. package/lib/TreeStructure/FolderNameInput.js +33 -26
  19. package/lib/TreeStructure/NavigationLink.d.ts +15 -0
  20. package/lib/TreeStructure/NavigationLink.js +100 -0
  21. package/lib/TreeStructure/TreeStructure.d.ts +1 -2
  22. package/lib/TreeStructure/TreeStructure.js +163 -45
  23. package/lib/TreeStructure/types.d.ts +4 -1
  24. package/lib/locale/messages-en.d.ts +2 -0
  25. package/lib/locale/messages-en.js +2 -0
  26. package/lib/locale/messages-nb.d.ts +2 -0
  27. package/lib/locale/messages-nb.js +2 -0
  28. package/lib/locale/messages-nn.d.ts +2 -0
  29. package/lib/locale/messages-nn.js +2 -0
  30. package/lib/locale/messages-se.d.ts +2 -0
  31. package/lib/locale/messages-se.js +2 -0
  32. package/lib/locale/messages-sma.d.ts +2 -0
  33. package/lib/locale/messages-sma.js +2 -0
  34. package/package.json +11 -11
  35. package/src/LanguageSelector/LanguageSelector.tsx +4 -1
  36. package/src/TreeStructure/FolderItem.tsx +126 -104
  37. package/src/TreeStructure/FolderItems.tsx +51 -43
  38. package/src/TreeStructure/FolderNameInput.tsx +43 -28
  39. package/src/TreeStructure/NavigationLink.tsx +100 -0
  40. package/src/TreeStructure/TreeStructure.tsx +187 -61
  41. package/src/TreeStructure/types.ts +5 -1
  42. package/src/locale/messages-en.ts +2 -0
  43. package/src/locale/messages-nb.ts +2 -0
  44. package/src/locale/messages-nn.ts +2 -0
  45. package/src/locale/messages-se.ts +2 -0
  46. package/src/locale/messages-sma.ts +2 -0
  47. package/es/TreeStructure/TreeStructureWrapper.js +0 -13
  48. package/lib/TreeStructure/TreeStructureWrapper.d.ts +0 -12
  49. package/lib/TreeStructure/TreeStructureWrapper.js +0 -24
  50. 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 { FolderOutlined } from '@ndla/icons/contentType';
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
- const ArrowRight = styled(ArrowDropDownRaw)`
22
- color: ${colors.text.primary};
23
- transform: rotate(-90deg);
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
- padding-left: ${spacing.normal};
28
- ${animations.fadeInLeft(animations.durations.fast)};
29
- animation-fill-mode: forwards;
30
- @media (prefers-reduced-motion: reduce) {
31
- animation: none;
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<{ loading?: boolean }>`
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
- border: 1px solid ${({ loading }) => (loading ? colors.brand.lighter : colors.brand.primary)};
46
- border-style: dashed;
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.text.primary};
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>(t('treeStructure.newFolder.defaultName'));
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 loading={loading}>
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="xsmall" ghostPill onClick={onCancelNewFolder}>
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
- size="xsmall"
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 { AddButton } from '@ndla/button';
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 { spacing, fonts } from '@ndla/core';
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
- const AddFolderWrapper = styled.div`
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
- margin-top: ${spacing.xsmall};
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
- <div>
227
+ <StyledTreeStructure ref={ref}>
137
228
  {label && <StyledLabel>{label}</StyledLabel>}
138
- <TreeStructureStyledWrapper aria-label="Menu tree" role="tree" framed={framed}>
139
- <FolderItems
140
- editable={editable}
141
- focusedFolderId={focusedId}
142
- menuItems={menuItems}
143
- folders={folders}
144
- level={0}
145
- loading={loading}
146
- selectedFolder={selectedFolder}
147
- maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
148
- newFolderParentId={newFolderParentId}
149
- onCancelNewFolder={onCancelNewFolder}
150
- onCloseFolder={onCloseFolder}
151
- onOpenFolder={onOpenFolder}
152
- onSaveNewFolder={onSaveNewFolder}
153
- onSelectFolder={onSelectFolder}
154
- openFolders={openFolders}
155
- openOnFolderClick={openOnFolderClick}
156
- setFocusedId={setFocusedId}
157
- setSelectedFolder={setSelectedFolder}
158
- targetResource={targetResource}
159
- visibleFolders={visibleFolderIds}
160
- framed={framed}
161
- />
162
- </TreeStructureStyledWrapper>
163
- {editable && (
164
- <AddFolderWrapper>
165
- <Tooltip
166
- tooltip={
167
- canAddFolder
168
- ? t('myNdla.newFolderUnder', {
169
- folderName: selectedFolder?.name,
170
- })
171
- : t('treeStructure.maxFoldersAlreadyAdded')
172
- }>
173
- <AddButton
174
- disabled={!canAddFolder}
175
- aria-label={t('myNdla.newFolder')}
176
- onClick={() => setNewFolderParentId(selectedFolder?.id)}>
177
- {t('myNdla.newFolder')}
178
- </AddButton>
179
- </Tooltip>
180
- </AddFolderWrapper>
181
- )}
182
- </div>
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: 'Edit foldername',
20
20
  delete: 'Delete',
21
21
  },
22
+ hideFolders: 'Hide all folders',
23
+ showFolders: 'Show all folders',
22
24
  createFolder: 'Create folder',
23
25
  maxFoldersAlreadyAdded: 'Maximum subfolders reached',
24
26
  newFolder: {
@@ -19,6 +19,8 @@ const messages = {
19
19
  edit: 'Endre mappenavn',
20
20
  delete: 'Slett',
21
21
  },
22
+ hideFolders: 'Skjul alle mapper',
23
+ showFolders: 'Vis alle mapper',
22
24
  createFolder: 'Lag mappe',
23
25
  maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
24
26
  newFolder: {
@@ -19,6 +19,8 @@ const messages = {
19
19
  edit: 'Endre mappenamn',
20
20
  delete: 'Slett',
21
21
  },
22
+ hideFolders: 'Skjul alle mapper',
23
+ showFolders: 'Vis alle mapper',
22
24
  createFolder: 'Lag mappe',
23
25
  maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
24
26
  newFolder: {
@@ -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: {
@@ -19,6 +19,8 @@ const messages = {
19
19
  edit: 'Endre mappenavn',
20
20
  delete: 'Slett',
21
21
  },
22
+ hideFolders: 'Skjul alle mapper',
23
+ showFolders: 'Vis alle mapper',
22
24
  createFolder: 'Lag mappe',
23
25
  maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
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;