@ndla/ui 17.0.0 → 19.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 (86) hide show
  1. package/es/Article/ArticleFavoritesButton.js +4 -3
  2. package/es/Masthead/MastheadAuthModal.js +8 -3
  3. package/es/MyNdla/Resource/Folder.js +11 -10
  4. package/es/MyNdla/index.js +1 -2
  5. package/es/Resource/BlockResource.js +14 -8
  6. package/es/Resource/ListResource.js +15 -9
  7. package/es/Resource/resourceComponents.js +12 -11
  8. package/es/TagSelector/SuggestionInput.js +111 -56
  9. package/es/TagSelector/Suggestions.js +19 -15
  10. package/es/TagSelector/TagSelector.js +8 -7
  11. package/es/TreeStructure/FolderItem.js +5 -5
  12. package/es/TreeStructure/FolderItems.js +8 -7
  13. package/es/TreeStructure/TreeStructure.js +65 -80
  14. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +23 -11
  15. package/es/index.js +1 -1
  16. package/es/locale/messages-en.js +10 -1
  17. package/es/locale/messages-nb.js +11 -2
  18. package/es/locale/messages-nn.js +12 -3
  19. package/es/locale/messages-se.js +11 -2
  20. package/es/locale/messages-sma.js +11 -2
  21. package/lib/Article/ArticleFavoritesButton.js +4 -3
  22. package/lib/Masthead/MastheadAuthModal.js +14 -7
  23. package/lib/MyNdla/Resource/Folder.d.ts +4 -3
  24. package/lib/MyNdla/Resource/Folder.js +11 -10
  25. package/lib/MyNdla/index.d.ts +1 -2
  26. package/lib/MyNdla/index.js +0 -8
  27. package/lib/Resource/BlockResource.d.ts +4 -3
  28. package/lib/Resource/BlockResource.js +14 -7
  29. package/lib/Resource/ListResource.d.ts +4 -3
  30. package/lib/Resource/ListResource.js +15 -8
  31. package/lib/Resource/resourceComponents.d.ts +4 -1
  32. package/lib/Resource/resourceComponents.js +14 -13
  33. package/lib/TagSelector/SuggestionInput.js +111 -57
  34. package/lib/TagSelector/Suggestions.js +26 -23
  35. package/lib/TagSelector/TagSelector.js +8 -7
  36. package/lib/TreeStructure/FolderItem.js +5 -5
  37. package/lib/TreeStructure/FolderItems.d.ts +1 -1
  38. package/lib/TreeStructure/FolderItems.js +8 -8
  39. package/lib/TreeStructure/TreeStructure.d.ts +6 -1
  40. package/lib/TreeStructure/TreeStructure.js +66 -80
  41. package/lib/TreeStructure/TreeStructure.types.d.ts +5 -3
  42. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +23 -11
  43. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +1 -1
  44. package/lib/index.d.ts +1 -1
  45. package/lib/index.js +0 -7
  46. package/lib/locale/messages-en.d.ts +9 -0
  47. package/lib/locale/messages-en.js +10 -1
  48. package/lib/locale/messages-nb.d.ts +9 -0
  49. package/lib/locale/messages-nb.js +11 -2
  50. package/lib/locale/messages-nn.d.ts +9 -0
  51. package/lib/locale/messages-nn.js +12 -3
  52. package/lib/locale/messages-se.d.ts +9 -0
  53. package/lib/locale/messages-se.js +11 -2
  54. package/lib/locale/messages-sma.d.ts +9 -0
  55. package/lib/locale/messages-sma.js +11 -2
  56. package/package.json +5 -5
  57. package/src/Article/ArticleFavoritesButton.tsx +4 -3
  58. package/src/Masthead/MastheadAuthModal.tsx +9 -0
  59. package/src/MyNdla/Resource/Folder.tsx +7 -7
  60. package/src/MyNdla/index.ts +1 -2
  61. package/src/Resource/BlockResource.tsx +7 -6
  62. package/src/Resource/ListResource.tsx +8 -7
  63. package/src/Resource/resourceComponents.tsx +8 -1
  64. package/src/TagSelector/SuggestionInput.tsx +90 -24
  65. package/src/TagSelector/Suggestions.tsx +14 -0
  66. package/src/TagSelector/TagSelector.tsx +6 -4
  67. package/src/TreeStructure/FolderItem.tsx +5 -2
  68. package/src/TreeStructure/FolderItems.tsx +4 -3
  69. package/src/TreeStructure/TreeStructure.tsx +54 -42
  70. package/src/TreeStructure/TreeStructure.types.ts +5 -3
  71. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +7 -7
  72. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +1 -1
  73. package/src/index.ts +1 -1
  74. package/src/locale/messages-en.ts +11 -1
  75. package/src/locale/messages-nb.ts +11 -2
  76. package/src/locale/messages-nn.ts +12 -3
  77. package/src/locale/messages-se.ts +11 -2
  78. package/src/locale/messages-sma.ts +11 -2
  79. package/es/MyNdla/Navigation/VerticalNavigation.js +0 -51
  80. package/es/MyNdla/Navigation/index.js +0 -2
  81. package/lib/MyNdla/Navigation/VerticalNavigation.d.ts +0 -10
  82. package/lib/MyNdla/Navigation/VerticalNavigation.js +0 -61
  83. package/lib/MyNdla/Navigation/index.d.ts +0 -2
  84. package/lib/MyNdla/Navigation/index.js +0 -15
  85. package/src/MyNdla/Navigation/VerticalNavigation.tsx +0 -93
  86. package/src/MyNdla/Navigation/index.ts +0 -2
@@ -19,6 +19,39 @@ import { uuid } from '@ndla/util';
19
19
  import Suggestions from './Suggestions';
20
20
  import type { TagType } from './TagSelector';
21
21
 
22
+ const SuggestionTextWrapper = styled.div`
23
+ ${fonts.sizes(18)};
24
+ position: absolute;
25
+ display: flex;
26
+ flex-grow: 1;
27
+ left: 0;
28
+ right: 0;
29
+ overflow: hidden;
30
+ max-height: ${spacing.large};
31
+ padding: 8.333px;
32
+ padding-right: ${spacing.large};
33
+ span {
34
+ color: ${colors.brand.grey};
35
+ white-space: nowrap;
36
+ overflow: hidden !important;
37
+ text-overflow: ellipsis;
38
+ &:first-of-type {
39
+ color: transparent;
40
+ }
41
+ }
42
+ `;
43
+
44
+ const SuggestionText = ({ value, suggestionValue }: { value: string; suggestionValue: string }) => (
45
+ <SuggestionTextWrapper>
46
+ {!!value && (
47
+ <>
48
+ <span>{value}</span>
49
+ <span>{suggestionValue.substring(value.length)}</span>
50
+ </>
51
+ )}
52
+ </SuggestionTextWrapper>
53
+ );
54
+
22
55
  const Cross = styled(CrossRaw)`
23
56
  margin-left: ${spacing.xxsmall};
24
57
  `;
@@ -31,15 +64,18 @@ const StyledInput = styled.input`
31
64
  flex-grow: 1;
32
65
  border: 0;
33
66
  outline: none;
34
- background: transparent;
35
67
  ${fonts.sizes(18)};
68
+ z-index: 1;
69
+ position: relative;
70
+ background: transparent;
36
71
  `;
72
+
37
73
  const StyledInputWrapper = styled.div`
38
74
  display: flex;
39
75
  flex-wrap: wrap;
40
76
  gap: ${spacing.xsmall};
41
77
  padding: ${spacing.small};
42
- border: 1px solid ${colors.brand.greyLighter};
78
+ border: 1px solid ${colors.brand.neutral7};
43
79
  transition: border-color ${animations.durations.normal} ease;
44
80
  border-radius: ${misc.borderRadius};
45
81
  &:focus-within {
@@ -50,6 +86,12 @@ const StyledInputWrapper = styled.div`
50
86
  const CombinedInputAndDropdownWrapper = styled.div`
51
87
  display: flex;
52
88
  flex-grow: 1;
89
+ position: relative;
90
+ `;
91
+
92
+ const StyledTagButton = styled(Button)<{ enableTagButtonAnimation: boolean }>`
93
+ ${({ enableTagButtonAnimation }) =>
94
+ enableTagButtonAnimation ? animations.fadeInScaled(animations.durations.slow) : ''}
53
95
  `;
54
96
 
55
97
  interface SuggestionInputProps {
@@ -90,6 +132,7 @@ const SuggestionInput = ({
90
132
  const inputRef = useRef<HTMLInputElement>(null);
91
133
  const containerRef = useRef<HTMLDivElement>(null);
92
134
  const suggestionIdRef = useRef<string>(uuid());
135
+ const initalTags = useRef<string[]>(addedTags.map(({ id }) => id));
93
136
 
94
137
  useEffect(() => {
95
138
  setCurrentHighlightedIndex(0);
@@ -122,7 +165,8 @@ const SuggestionInput = ({
122
165
  <SuggestionInputContainer ref={containerRef}>
123
166
  <StyledInputWrapper>
124
167
  {addedTags.map(({ id, name }) => (
125
- <Button
168
+ <StyledTagButton
169
+ enableTagButtonAnimation={!initalTags.current.includes(id)}
126
170
  aria-label={t('tagSelector.removeTag', { name })}
127
171
  onClick={() => onToggleTag(id)}
128
172
  light
@@ -132,9 +176,12 @@ const SuggestionInput = ({
132
176
  {prefix}
133
177
  {name}
134
178
  <Cross />
135
- </Button>
179
+ </StyledTagButton>
136
180
  ))}
137
181
  <CombinedInputAndDropdownWrapper>
182
+ {suggestions[currentHighlightedIndex] && (
183
+ <SuggestionText value={value} suggestionValue={suggestions[currentHighlightedIndex].name} />
184
+ )}
138
185
  <StyledInput
139
186
  placeholder={t('tagSelector.placeholder')}
140
187
  value={value}
@@ -157,38 +204,57 @@ const SuggestionInput = ({
157
204
  }}
158
205
  ref={inputRef}
159
206
  onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
207
+ if (!['Enter', ' ', 'Tab', 'ArrowDown', 'ArrowUp', 'Backspace'].includes(e.key)) {
208
+ return;
209
+ }
210
+ const trimmedValue = value.replace(/\s/g, '');
160
211
  if (e.key === 'Escape') {
161
212
  setExpanded(false);
162
213
  e.preventDefault();
163
- } else if (e.key === 'Enter' || e.key === 'Tab') {
164
- if (value !== '' || expanded) {
165
- if (suggestions.length > 0) {
166
- if (!hasBeenAdded(suggestions[currentHighlightedIndex].id)) {
167
- onToggleTag(suggestions[currentHighlightedIndex].id);
168
- }
169
- setInputValue('');
170
- if (e.key === 'Enter') {
171
- e.preventDefault();
172
- }
173
- } else {
174
- onCreateTag(value);
175
- setInputValue('');
176
- e.preventDefault();
177
- }
178
- } else if (e.key === 'Enter') {
179
- e.preventDefault();
180
- }
181
- } else if (e.key === 'ArrowUp') {
214
+ return;
215
+ }
216
+ if (e.key === 'Backspace' && trimmedValue === '' && addedTags.length) {
217
+ // Remove the added last tag
218
+ onToggleTag(addedTags[addedTags.length - 1].id);
219
+ return;
220
+ }
221
+ if (e.key === 'ArrowUp') {
182
222
  setCurrentHighlightedIndex(
183
223
  currentHighlightedIndex - 1 < 0 ? suggestions.length - 1 : currentHighlightedIndex - 1,
184
224
  );
185
225
  e.preventDefault();
186
- } else if (e.key === 'ArrowDown') {
226
+ return;
227
+ }
228
+ if (e.key === 'ArrowDown') {
187
229
  setCurrentHighlightedIndex(
188
230
  currentHighlightedIndex + 1 >= suggestions.length ? 0 : currentHighlightedIndex + 1,
189
231
  );
190
232
  e.preventDefault();
233
+ return;
234
+ }
235
+ if (trimmedValue === '' && !expanded) {
236
+ if (e.key === 'Enter' || e.key === ' ') {
237
+ e.preventDefault();
238
+ }
239
+ return;
240
+ }
241
+ if (e.key === 'Enter' || e.key === 'Tab' || e.key === ' ') {
242
+ if (suggestions.length > 0) {
243
+ if (!hasBeenAdded(suggestions[currentHighlightedIndex].id)) {
244
+ onToggleTag(suggestions[currentHighlightedIndex].id);
245
+ } else if (trimmedValue.length < suggestions[currentHighlightedIndex].name.length) {
246
+ onCreateTag(trimmedValue);
247
+ e.preventDefault();
248
+ }
249
+ setInputValue('');
250
+ e.preventDefault();
251
+ return;
252
+ }
253
+ onCreateTag(trimmedValue);
254
+ setInputValue('');
255
+ e.preventDefault();
191
256
  }
257
+ return;
192
258
  }}
193
259
  />
194
260
  <Tooltip tooltip={expanded ? t('tagSelector.hideAllTags') : t('tagSelector.showAllTags')}>
@@ -8,6 +8,7 @@
8
8
 
9
9
  import React from 'react';
10
10
  import styled from '@emotion/styled';
11
+ import { css } from '@emotion/core';
11
12
  import { Check } from '@ndla/icons/editor';
12
13
  import { spacing, colors, misc, animations, fonts, shadows } from '@ndla/core';
13
14
  import Button from '@ndla/button';
@@ -60,6 +61,7 @@ interface SuggestionButtonProps {
60
61
  const SuggestionButton = styled(Button)<SuggestionButtonProps>`
61
62
  display: flex;
62
63
  justify-content: space-between;
64
+ align-items: center;
63
65
  ${fonts.sizes(18)};
64
66
  transition: ${misc.transition.default};
65
67
  font-weight: 400;
@@ -72,6 +74,18 @@ const SuggestionButton = styled(Button)<SuggestionButtonProps>`
72
74
  }
73
75
  }
74
76
  }
77
+ ${({ isHighlighted }) =>
78
+ isHighlighted
79
+ ? css`
80
+ background: ${colors.brand.lighter};
81
+ &:disabled {
82
+ background: ${colors.brand.greyLighter};
83
+ svg {
84
+ fill: ${colors.brand.grey};
85
+ }
86
+ }
87
+ `
88
+ : ''}
75
89
  `;
76
90
 
77
91
  interface Props {
@@ -33,10 +33,12 @@ interface Props {
33
33
  prefix?: string;
34
34
  }
35
35
 
36
- const sortedTags = (tags: TagType[], selectedTags: string[]): TagType[] =>
37
- tags
38
- .filter(({ id }) => selectedTags.some((idSelected) => idSelected === id))
39
- .sort((a, b) => a.name.localeCompare(b.name, 'nb'));
36
+ const sortedTags = (tags: TagType[], selectedTags: string[]): TagType[] => {
37
+ const returnTags = selectedTags
38
+ .map((selectedId) => tags.find(({ id }) => selectedId === id))
39
+ .filter((notUndefined) => notUndefined) as unknown as TagType[];
40
+ return returnTags;
41
+ };
40
42
 
41
43
  const getSuggestions = (tags: TagType[], inputValue: string): TagType[] => {
42
44
  if (inputValue === '') {
@@ -42,7 +42,8 @@ const WrapperForFolderChild = styled.div<{ marked: boolean }>`
42
42
  right: ${spacing.xsmall};
43
43
  opacity: ${({ marked }) => (marked ? 1 : 0.25)};
44
44
  &:hover,
45
- &:focus {
45
+ &:focus,
46
+ &:focus-within {
46
47
  opacity: 1;
47
48
  }
48
49
  `;
@@ -170,7 +171,9 @@ const FolderItem = ({
170
171
  {name}
171
172
  </FolderName>
172
173
  {folderChild && (
173
- <WrapperForFolderChild marked={marked}>{folderChild(id, marked ? 0 : -1)}</WrapperForFolderChild>
174
+ <WrapperForFolderChild marked={marked}>
175
+ {folderChild(id, marked || id === focusedFolderId ? 0 : -1)}
176
+ </WrapperForFolderChild>
174
177
  )}
175
178
  </>
176
179
  )}
@@ -12,7 +12,6 @@ import { animations, spacing } from '@ndla/core';
12
12
  import FolderItem from './FolderItem';
13
13
  import FolderNameInput from './FolderNameInput';
14
14
  import { FolderItemsProps } from './TreeStructure.types';
15
- import { MAX_LEVEL_FOR_FOLDERS } from './TreeStructure';
16
15
 
17
16
  const StyledUL = styled.ul<{ firstLevel?: boolean }>`
18
17
  ${animations.fadeInLeft(animations.durations.fast)};
@@ -49,11 +48,12 @@ const FolderItems = ({
49
48
  setFocusedFolderId,
50
49
  firstLevel,
51
50
  folderChild,
51
+ maximumLevelsOfFoldersAllowed,
52
52
  }: FolderItemsProps) => (
53
53
  <StyledUL role="group" firstLevel={firstLevel}>
54
54
  {data.map(({ name, data: dataChildren, id, url, icon }, _index) => {
55
55
  const newIdPaths = [...idPaths, _index];
56
- const isOpen = openFolders?.has(id);
56
+ const isOpen = openFolders?.includes(id);
57
57
  return (
58
58
  <StyledLI key={id} role="treeitem">
59
59
  <div>
@@ -69,7 +69,7 @@ const FolderItems = ({
69
69
  focusedFolderId={focusedFolderId}
70
70
  onToggleOpen={onToggleOpen}
71
71
  onMarkFolder={onMarkFolder}
72
- hideArrow={dataChildren?.length === 0 || newIdPaths.length >= MAX_LEVEL_FOR_FOLDERS}
72
+ hideArrow={dataChildren?.length === 0 || newIdPaths.length >= maximumLevelsOfFoldersAllowed}
73
73
  noPaddingWhenArrowIsHidden={editable && firstLevel && dataChildren?.length === 0}
74
74
  setFocusedFolderId={setFocusedFolderId}
75
75
  folderChild={folderChild}
@@ -101,6 +101,7 @@ const FolderItems = ({
101
101
  setFocusedFolderId={setFocusedFolderId}
102
102
  firstLevel={false}
103
103
  folderChild={folderChild}
104
+ maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
104
105
  />
105
106
  )}
106
107
  </StyledLI>
@@ -13,6 +13,7 @@ import Tooltip from '@ndla/tooltip';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import styled from '@emotion/styled';
15
15
  import { spacing, fonts } from '@ndla/core';
16
+ import { uniq } from 'lodash';
16
17
  import TreeStructureStyledWrapper from './TreeStructureWrapper';
17
18
  import FolderItems from './FolderItems';
18
19
  import { getIdPathsOfFolder, getPathOfFolder, getFolderName } from './helperFunctions';
@@ -41,21 +42,23 @@ const TreeStructure = ({
41
42
  folderIdMarkedByDefault,
42
43
  defaultOpenFolders,
43
44
  folderChild,
45
+ maximumLevelsOfFoldersAllowed,
44
46
  }: TreeStructureProps) => {
45
47
  const { t } = useTranslation();
46
48
  const [newFolder, setNewFolder] = useState<NewFolderProps | undefined>();
47
- const [openFolders, setOpenFolders] = useState<Set<string>>(new Set(defaultOpenFolders || []));
49
+ const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
48
50
  const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
49
- const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0].id);
51
+ const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0]?.id);
50
52
  const treestructureRef = useRef<HTMLDivElement>(null);
51
53
  const wrapperRef = useRef<HTMLDivElement>(null);
52
54
  const rootLevelId = useMemo(() => uuid(), []); // TODO: use useId hook when we update to React 18
53
55
 
54
56
  useEffect(() => {
55
- setOpenFolders((prev) => {
56
- defaultOpenFolders?.forEach((id) => prev.add(id));
57
- return new Set(prev);
58
- });
57
+ if (defaultOpenFolders) {
58
+ setOpenFolders((prev) => {
59
+ return uniq([...defaultOpenFolders, ...prev]);
60
+ });
61
+ }
59
62
  }, [defaultOpenFolders]);
60
63
 
61
64
  useEffect(() => {
@@ -65,48 +68,44 @@ const TreeStructure = ({
65
68
  }, [loading]);
66
69
 
67
70
  const onToggleOpen = (id: string) => {
68
- setOpenFolders((prev) => {
69
- if (prev.has(id)) {
70
- prev.delete(id);
71
- // Did we just closed a folder with a marked folder inside it?
72
- // If so, we need to mark the folder we just closed.
73
- if (markedFolderId) {
74
- const closingFolderPath = getPathOfFolder(data, id);
75
- const markedFolderPath = getPathOfFolder(data, markedFolderId);
76
- const markedFolderIsSubPath = closingFolderPath.every(
77
- (folderId, _index) => markedFolderPath[_index] === folderId,
78
- );
79
- if (markedFolderIsSubPath) {
80
- setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
81
- }
71
+ if (openFolders.includes(id)) {
72
+ // Did we just closed a folder with a marked folder inside it?
73
+ // If so, we need to mark the folder we just closed.
74
+ if (markedFolderId) {
75
+ const closingFolderPath = getPathOfFolder(data, id);
76
+ const markedFolderPath = getPathOfFolder(data, markedFolderId);
77
+ const markedFolderIsSubPath = closingFolderPath.every(
78
+ (folderId, _index) => markedFolderPath[_index] === folderId,
79
+ );
80
+ if (markedFolderIsSubPath) {
81
+ setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
82
82
  }
83
- } else {
84
- prev.add(id);
85
83
  }
86
- return new Set(prev);
87
- });
84
+ setOpenFolders(openFolders.filter((folder) => folder !== id));
85
+ } else {
86
+ setOpenFolders(uniq([...openFolders, id]));
87
+ }
88
88
  };
89
89
 
90
90
  const onCreateNewFolder = (props: { idPaths: number[]; parentId?: string }) => {
91
91
  setNewFolder(props);
92
92
  };
93
93
 
94
- const onSaveNewFolder = async (value: string) => {
94
+ const onSaveNewFolder = (value: string) => {
95
95
  if (newFolder) {
96
96
  // We would like to create a new folder with the name of value.
97
97
  // Its location in structure is based on newFolder object
98
- const newFolderId = await onNewFolder({ ...newFolder, value });
99
- if (newFolderId) {
100
- setMarkedFolderId(newFolderId);
101
- setFocusedFolderId(newFolderId);
102
- // Open current folder in case it was closed..
103
- setOpenFolders((prev) => {
98
+ onNewFolder({ ...newFolder, value }).then((newFolderId) => {
99
+ if (newFolderId) {
100
+ setMarkedFolderId(newFolderId);
101
+ setFocusedFolderId(newFolderId);
102
+ // Open current folder in case it was closed..
103
+
104
104
  if (newFolder.parentId) {
105
- prev.add(newFolder.parentId);
105
+ setOpenFolders(uniq([...openFolders, newFolder.parentId]));
106
106
  }
107
- return new Set(prev);
108
- });
109
- }
107
+ }
108
+ });
110
109
  }
111
110
  };
112
111
 
@@ -119,6 +118,9 @@ const TreeStructure = ({
119
118
  setFocusedFolderId(id);
120
119
  };
121
120
 
121
+ const paths = getPathOfFolder(data, markedFolderId || '');
122
+ const canAddFolder = editable && paths.length < (maximumLevelsOfFoldersAllowed || 1);
123
+
122
124
  return (
123
125
  <div
124
126
  ref={treestructureRef}
@@ -134,7 +136,7 @@ const TreeStructure = ({
134
136
  });
135
137
  }
136
138
  }}>
137
- <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>
139
+ {label && <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>}
138
140
  <TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
139
141
  <FolderItems
140
142
  idPaths={[]}
@@ -154,22 +156,28 @@ const TreeStructure = ({
154
156
  setFocusedFolderId={setFocusedFolderId}
155
157
  firstLevel
156
158
  folderChild={folderChild}
159
+ maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
157
160
  />
158
161
  </TreeStructureStyledWrapper>
159
162
  {editable && (
160
163
  <AddFolderWrapper>
161
164
  <Tooltip
162
- tooltip={t('myNdla.newFolderUnder', {
163
- folderName: getFolderName(data, markedFolderId),
164
- })}>
165
+ tooltip={
166
+ canAddFolder
167
+ ? t('myNdla.newFolderUnder', {
168
+ folderName: getFolderName(data, markedFolderId),
169
+ })
170
+ : t('myNdla.maxFoldersAlreadyAdded')
171
+ }>
165
172
  <AddButton
173
+ disabled={!canAddFolder}
166
174
  aria-label={t('myNdla.newFolder')}
167
175
  onClick={() => {
168
- const paths = getPathOfFolder(data, markedFolderId || '');
169
176
  const idPaths = getIdPathsOfFolder(data, markedFolderId || '');
170
177
  setNewFolder({ idPaths, parentId: paths[paths.length - 1] });
171
- }}
172
- />
178
+ }}>
179
+ {t('myNdla.newFolder')}
180
+ </AddButton>
173
181
  </Tooltip>
174
182
  </AddFolderWrapper>
175
183
  )}
@@ -177,4 +185,8 @@ const TreeStructure = ({
177
185
  );
178
186
  };
179
187
 
188
+ TreeStructure.defaultProps = {
189
+ maximumLevelsOfFoldersAllowed: MAX_LEVEL_FOR_FOLDERS,
190
+ };
191
+
180
192
  export default TreeStructure;
@@ -34,11 +34,12 @@ interface CommonFolderProps {
34
34
 
35
35
  export interface TreeStructureProps extends CommonFolderProps {
36
36
  framed?: boolean;
37
- label: string;
37
+ label?: string;
38
38
  folderIdMarkedByDefault?: string;
39
39
  onNewFolder: (props: { value: string; parentId?: string; idPaths: number[] }) => Promise<string>;
40
40
  defaultOpenFolders?: string[];
41
41
  folderChild?: FolderChildFuncType;
42
+ maximumLevelsOfFoldersAllowed: number;
42
43
  }
43
44
 
44
45
  export type onCreateNewFolderProp = ({
@@ -49,7 +50,7 @@ export type onCreateNewFolderProp = ({
49
50
  parentId: string | undefined;
50
51
  }) => void;
51
52
 
52
- export type SetOpenFolderProp = React.Dispatch<React.SetStateAction<Set<string>>>;
53
+ export type SetOpenFolderProp = React.Dispatch<React.SetStateAction<string[]>>;
53
54
  export type SetFocusedFolderId = React.Dispatch<React.SetStateAction<string | undefined>>;
54
55
 
55
56
  export type FolderChildFuncType = (id: string, tabIndex: number) => ReactNode;
@@ -60,7 +61,7 @@ export interface FolderItemsProps extends CommonFolderProps {
60
61
  onCancelNewFolder: () => void;
61
62
  onCreateNewFolder: onCreateNewFolderProp;
62
63
  newFolder: NewFolderProps | undefined;
63
- openFolders: Set<string>;
64
+ openFolders: string[];
64
65
  markedFolderId?: string;
65
66
  onMarkFolder: (id: string) => void;
66
67
  idPaths: number[];
@@ -70,4 +71,5 @@ export interface FolderItemsProps extends CommonFolderProps {
70
71
  keyNavigationFocusIsCreateFolderButton?: boolean;
71
72
  icon?: ReactNode;
72
73
  folderChild?: FolderChildFuncType;
74
+ maximumLevelsOfFoldersAllowed: number;
73
75
  }
@@ -66,7 +66,7 @@ const keyboardNavigation = ({
66
66
  if (dataId === id) {
67
67
  elementWithKeyFocus.paths = paths;
68
68
  elementWithKeyFocus.index = _index;
69
- elementWithKeyFocus.isOpen = openFolders.has(dataId) && childData && childData.length > 0;
69
+ elementWithKeyFocus.isOpen = openFolders.includes(dataId) && childData && childData.length > 0;
70
70
  elementWithKeyFocus.data = childData;
71
71
  elementWithKeyFocus.parent = parent;
72
72
  elementWithKeyFocus.parentId = parentId;
@@ -78,7 +78,7 @@ const keyboardNavigation = ({
78
78
  if (!updatePathToElementWithKeyFocus(data, [], data)) {
79
79
  // Couldn't find its location in the tree.
80
80
  // This should not happen, reset its value to root.
81
- setFocusedFolderId(e.key === 'ArrowDown' ? data[0].id : undefined);
81
+ setFocusedFolderId(e.key === 'ArrowDown' ? data[0]?.id : undefined);
82
82
  return;
83
83
  }
84
84
  e.preventDefault();
@@ -113,7 +113,7 @@ const keyboardNavigation = ({
113
113
  }
114
114
 
115
115
  if (!id && e.key === 'ArrowDown') {
116
- setFocusedFolderId(data[0].id);
116
+ setFocusedFolderId(data[0]?.id);
117
117
  return;
118
118
  }
119
119
  if (!id) {
@@ -124,7 +124,7 @@ const keyboardNavigation = ({
124
124
  if (elementWithKeyFocus.index > 0) {
125
125
  // Move upwards to the parent folder
126
126
  setFocusedFolderId(
127
- elementWithKeyFocus.parent ? elementWithKeyFocus.parent[elementWithKeyFocus.index - 1].id : undefined,
127
+ elementWithKeyFocus.parent ? elementWithKeyFocus.parent[elementWithKeyFocus.index - 1]?.id : undefined,
128
128
  );
129
129
  } else if (elementWithKeyFocus.paths.length > 0) {
130
130
  elementWithKeyFocus.paths.pop();
@@ -133,14 +133,14 @@ const keyboardNavigation = ({
133
133
  findParent = findParent[index].data as FolderStructureProps[];
134
134
  });
135
135
  const parentsCurrentIndex = findParent.findIndex(({ id }) => id === elementWithKeyFocus.parentId);
136
- setFocusedFolderId(findParent[parentsCurrentIndex].id);
136
+ setFocusedFolderId(findParent[parentsCurrentIndex]?.id);
137
137
  }
138
138
  return;
139
139
  }
140
140
 
141
141
  if (elementWithKeyFocus.isOpen) {
142
142
  if (elementWithKeyFocus.data?.length) {
143
- setFocusedFolderId(elementWithKeyFocus.data[0].id);
143
+ setFocusedFolderId(elementWithKeyFocus.data[0]?.id);
144
144
  } else {
145
145
  // move to next child of parent if any... need new traverse :-/
146
146
  traverseUpwards(data, setFocusedFolderId, elementWithKeyFocus.paths, elementWithKeyFocus.index);
@@ -150,7 +150,7 @@ const keyboardNavigation = ({
150
150
 
151
151
  if (elementWithKeyFocus.parent && elementWithKeyFocus.index < elementWithKeyFocus.parent?.length - 1) {
152
152
  // Move downwards to the next child
153
- setFocusedFolderId(elementWithKeyFocus.parent[elementWithKeyFocus.index + 1].id);
153
+ setFocusedFolderId(elementWithKeyFocus.parent[elementWithKeyFocus.index + 1]?.id);
154
154
  return;
155
155
  }
156
156
 
@@ -12,7 +12,7 @@ export interface KeyboardNavigationProps {
12
12
  e: React.KeyboardEvent<HTMLElement>;
13
13
  data: FolderStructureProps[];
14
14
  setFocusedFolderId: SetFocusedFolderId;
15
- openFolders: Set<string>;
15
+ openFolders: string[];
16
16
  onToggleOpen: (id: string) => void;
17
17
  focusedFolderId: string | undefined;
18
18
  }
package/src/index.ts CHANGED
@@ -236,7 +236,7 @@ export { Notion, ConceptNotion } from './Notion';
236
236
  export type { NotionVisualElementType, ConceptNotionType } from './Notion';
237
237
 
238
238
  export { BannerCard } from './BannerCard';
239
- export { VerticalNavigation, Folder, FolderInput } from './MyNdla';
239
+ export { Folder, FolderInput } from './MyNdla';
240
240
  export { ListResource, BlockResource } from './Resource';
241
241
  export type { ListResourceProps } from './Resource';
242
242
  export type { TagType } from './TagSelector';
@@ -15,14 +15,20 @@ const titleTemplate = ' - NDLA';
15
15
 
16
16
  const messages = {
17
17
  treeStructure: {
18
+ folderChildOptions: {
19
+ edit: 'Edit foldername',
20
+ delete: 'Delete',
21
+ },
18
22
  createFolder: 'Create folder',
23
+ maxFoldersAlreadyAdded: 'Maximum subfolders reached',
19
24
  newFolder: {
20
25
  placeholder: 'Add foldername',
21
26
  defaultName: 'New folder',
22
27
  },
23
28
  },
24
29
  tagSelector: {
25
- placeholder: 'Add to tag',
30
+ label: 'Add tag',
31
+ placeholder: 'Enter tag name',
26
32
  removeTag: 'Remove tag {{name}}',
27
33
  hideAllTags: 'Hide all tags',
28
34
  showAllTags: 'Show all tags',
@@ -978,6 +984,7 @@ const messages = {
978
984
  close: 'Close fact box',
979
985
  },
980
986
  myNdla: {
987
+ myNDLA: 'My NDLA',
981
988
  resources: '{{count}} Resource',
982
989
  resources_plural: '{{count}} Resources',
983
990
  folders: '{{count}} Folder',
@@ -989,6 +996,8 @@ const messages = {
989
996
  newFolderUnder: 'Create new folder under {{folderName}}',
990
997
  myAccount: 'My account',
991
998
  favourites: 'Favourites',
999
+ addToFavourites: 'Add to my favourites',
1000
+ alreadyFavourited: 'Already in my favourites',
992
1001
  help: 'Help',
993
1002
  more: 'More options',
994
1003
  listView: 'List view',
@@ -1007,6 +1016,7 @@ const messages = {
1007
1016
  terms: 'terms of use',
1008
1017
  feide: 'We have retrieved this information from Feide',
1009
1018
  newFavourite: 'Recently favourited',
1019
+
1010
1020
  storageInfo: {
1011
1021
  title: 'How to save your favourite resources from NDLA',
1012
1022
  text: 'When you wish to save a resource, you can do so by clicking the heart on the top right corner of the page. You will then get an option to store the resource in a folder',
@@ -15,15 +15,21 @@ const titleTemplate = ' - NDLA';
15
15
 
16
16
  const messages = {
17
17
  treeStructure: {
18
+ folderChildOptions: {
19
+ edit: 'Endre mappenavn',
20
+ delete: 'Slett',
21
+ },
18
22
  createFolder: 'Lag mappe',
23
+ maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
19
24
  newFolder: {
20
25
  placeholder: 'Skriv navn på mappe',
21
26
  defaultName: 'Ny mappe',
22
27
  },
23
28
  },
24
29
  tagSelector: {
25
- placeholder: 'Tilknytt tag',
26
- removeTag: 'Ta vekk tilknytning til {{name}}',
30
+ label: 'Legg til tag',
31
+ placeholder: 'Skriv tag navn',
32
+ removeTag: 'Ta vekk {{name}}',
27
33
  hideAllTags: 'Skjul alle tagger',
28
34
  showAllTags: 'Vis alle tagger',
29
35
  },
@@ -976,6 +982,7 @@ const messages = {
976
982
  close: 'Lukk faktaboks',
977
983
  },
978
984
  myNdla: {
985
+ myNDLA: 'Min NDLA',
979
986
  resources: '{{count}} ressurs',
980
987
  resources_plural: '{{count}} ressurser',
981
988
  folders: '{{count}} mappe',
@@ -987,6 +994,8 @@ const messages = {
987
994
  newFolderUnder: 'Lag ny mappe under {{folderName}}',
988
995
  myAccount: 'Min konto',
989
996
  favourites: 'Favoritter',
997
+ addToFavourites: 'Legg til i mine favoritter',
998
+ alreadyFavourited: 'Allerede lagt til i mine favoritter',
990
999
  help: 'Hjelp',
991
1000
  more: 'Flere valg',
992
1001
  listView: 'Listevisning',