@ndla/ui 13.2.2 → 15.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.
Files changed (236) hide show
  1. package/es/Article/Article.js +22 -3
  2. package/es/Article/ArticleFavoritesButton.js +38 -0
  3. package/es/Article/index.js +2 -1
  4. package/es/Breadcrumb/ActionBreadcrumb.js +57 -0
  5. package/es/Breadcrumb/index.js +1 -0
  6. package/es/Footer/FooterAuth.js +15 -22
  7. package/es/InfoBlock/InfoBlock.js +55 -0
  8. package/es/InfoBlock/index.js +1 -0
  9. package/es/LearningPaths/LearningPathMenu.js +3 -4
  10. package/es/Masthead/MastheadAuthModal.js +2 -2
  11. package/es/MyNdla/Navigation/VerticalNavigation.js +51 -0
  12. package/es/MyNdla/Navigation/index.js +2 -0
  13. package/es/MyNdla/Resource/Folder.js +86 -0
  14. package/es/MyNdla/Resource/FolderInput.js +96 -0
  15. package/{lib/MyNdla/ResourceDash/ResourcesView.d.ts → es/MyNdla/Resource/index.js} +3 -3
  16. package/es/MyNdla/index.js +4 -4
  17. package/es/Resource/BlockResource.js +73 -0
  18. package/es/Resource/ListResource.js +66 -0
  19. package/es/Resource/index.js +10 -0
  20. package/es/Resource/resourceComponents.js +97 -0
  21. package/es/ResourceGroup/ResourceGroup.js +7 -5
  22. package/es/ResourceGroup/ResourceItem.js +28 -30
  23. package/es/ResourceGroup/ResourceList.js +18 -6
  24. package/es/Search/ActiveFilters.js +6 -7
  25. package/es/Search/ContentTypeResult.js +6 -8
  26. package/es/SearchTypeResult/ActiveFilters.js +6 -10
  27. package/es/SnackBar/SnackBar.js +117 -0
  28. package/es/SnackBar/index.js +9 -0
  29. package/es/TagSelector/SuggestionInput.js +240 -0
  30. package/es/TagSelector/Suggestions.js +93 -0
  31. package/es/TagSelector/TagSelector.js +137 -0
  32. package/es/TagSelector/index.js +9 -0
  33. package/es/TopicIntroductionList/TopicIntroduction.js +2 -4
  34. package/es/TopicIntroductionList/TopicShortcutItem.js +1 -3
  35. package/es/TreeStructure/FolderItem.js +130 -0
  36. package/es/TreeStructure/FolderItems.js +123 -0
  37. package/es/TreeStructure/FolderNameInput.js +112 -0
  38. package/es/TreeStructure/TreeStructure.js +254 -0
  39. package/es/TreeStructure/TreeStructure.types.js +0 -0
  40. package/es/TreeStructure/TreeStructureWrapper.js +13 -0
  41. package/es/TreeStructure/helperFunctions.js +92 -0
  42. package/es/TreeStructure/index.js +9 -0
  43. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +182 -0
  44. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -0
  45. package/es/User/AuthModal.js +15 -24
  46. package/es/User/UserInfo.js +70 -0
  47. package/es/User/apiTypes.js +0 -0
  48. package/es/User/index.js +2 -0
  49. package/es/User/parseUserObject.js +102 -0
  50. package/es/all.css +90 -0
  51. package/es/index.js +9 -3
  52. package/es/locale/messages-en.js +71 -4
  53. package/es/locale/messages-nb.js +70 -3
  54. package/es/locale/messages-nn.js +70 -3
  55. package/es/locale/messages-se.js +70 -3
  56. package/es/locale/messages-sma.js +70 -3
  57. package/lib/Article/Article.d.ts +3 -1
  58. package/lib/Article/Article.js +43 -23
  59. package/lib/Article/ArticleFavoritesButton.d.ts +15 -0
  60. package/lib/Article/ArticleFavoritesButton.js +56 -0
  61. package/lib/Article/index.d.ts +2 -1
  62. package/lib/Article/index.js +8 -0
  63. package/lib/Breadcrumb/ActionBreadcrumb.d.ts +16 -0
  64. package/lib/Breadcrumb/ActionBreadcrumb.js +72 -0
  65. package/lib/Breadcrumb/index.d.ts +1 -0
  66. package/lib/Breadcrumb/index.js +8 -0
  67. package/lib/Footer/FooterAuth.d.ts +1 -1
  68. package/lib/Footer/FooterAuth.js +17 -17
  69. package/lib/InfoBlock/InfoBlock.d.ts +8 -0
  70. package/lib/InfoBlock/InfoBlock.js +58 -0
  71. package/lib/InfoBlock/index.d.ts +1 -0
  72. package/lib/InfoBlock/index.js +13 -0
  73. package/lib/LearningPaths/LearningPathMenu.js +3 -4
  74. package/lib/Masthead/MastheadAuthModal.d.ts +3 -3
  75. package/lib/Masthead/MastheadAuthModal.js +3 -3
  76. package/lib/MyNdla/Navigation/VerticalNavigation.d.ts +10 -0
  77. package/lib/MyNdla/Navigation/VerticalNavigation.js +61 -0
  78. package/lib/MyNdla/Navigation/index.d.ts +2 -0
  79. package/lib/MyNdla/Navigation/index.js +15 -0
  80. package/lib/MyNdla/Resource/Folder.d.ts +20 -0
  81. package/lib/MyNdla/Resource/Folder.js +100 -0
  82. package/lib/MyNdla/Resource/FolderInput.d.ts +15 -0
  83. package/lib/MyNdla/Resource/FolderInput.js +116 -0
  84. package/lib/MyNdla/Resource/index.d.ts +10 -0
  85. package/lib/MyNdla/Resource/index.js +23 -0
  86. package/lib/MyNdla/index.d.ts +4 -4
  87. package/lib/MyNdla/index.js +13 -7
  88. package/lib/Resource/BlockResource.d.ts +20 -0
  89. package/lib/Resource/BlockResource.js +84 -0
  90. package/lib/Resource/ListResource.d.ts +20 -0
  91. package/lib/Resource/ListResource.js +78 -0
  92. package/lib/Resource/index.d.ts +11 -0
  93. package/lib/Resource/index.js +29 -0
  94. package/lib/Resource/resourceComponents.d.ts +24 -0
  95. package/lib/Resource/resourceComponents.js +106 -0
  96. package/lib/ResourceGroup/ResourceGroup.d.ts +2 -1
  97. package/lib/ResourceGroup/ResourceGroup.js +7 -5
  98. package/lib/ResourceGroup/ResourceItem.d.ts +5 -1
  99. package/lib/ResourceGroup/ResourceItem.js +29 -30
  100. package/lib/ResourceGroup/ResourceList.d.ts +3 -1
  101. package/lib/ResourceGroup/ResourceList.js +18 -6
  102. package/lib/Search/ActiveFilters.js +6 -7
  103. package/lib/Search/ContentTypeResult.js +6 -8
  104. package/lib/SearchTypeResult/ActiveFilters.js +6 -10
  105. package/lib/SnackBar/SnackBar.d.ts +23 -0
  106. package/lib/SnackBar/SnackBar.js +127 -0
  107. package/lib/SnackBar/index.d.ts +10 -0
  108. package/lib/SnackBar/index.js +15 -0
  109. package/lib/TagSelector/SuggestionInput.d.ts +19 -0
  110. package/lib/TagSelector/SuggestionInput.js +255 -0
  111. package/lib/TagSelector/Suggestions.d.ts +12 -0
  112. package/lib/TagSelector/Suggestions.js +96 -0
  113. package/lib/TagSelector/TagSelector.d.ts +16 -0
  114. package/lib/TagSelector/TagSelector.js +150 -0
  115. package/lib/TagSelector/index.d.ts +10 -0
  116. package/lib/TagSelector/index.js +19 -0
  117. package/lib/TopicIntroductionList/TopicIntroduction.js +2 -4
  118. package/lib/TopicIntroductionList/TopicShortcutItem.js +1 -3
  119. package/lib/TreeStructure/FolderItem.d.ts +27 -0
  120. package/lib/TreeStructure/FolderItem.js +140 -0
  121. package/lib/TreeStructure/FolderItems.d.ts +11 -0
  122. package/lib/TreeStructure/FolderItems.js +130 -0
  123. package/lib/TreeStructure/FolderNameInput.d.ts +15 -0
  124. package/lib/TreeStructure/FolderNameInput.js +125 -0
  125. package/lib/TreeStructure/TreeStructure.d.ts +12 -0
  126. package/lib/TreeStructure/TreeStructure.js +273 -0
  127. package/lib/TreeStructure/TreeStructure.types.d.ts +63 -0
  128. package/lib/TreeStructure/TreeStructure.types.js +1 -0
  129. package/lib/TreeStructure/TreeStructureWrapper.d.ts +12 -0
  130. package/lib/TreeStructure/TreeStructureWrapper.js +24 -0
  131. package/lib/TreeStructure/helperFunctions.d.ts +5 -0
  132. package/lib/TreeStructure/helperFunctions.js +103 -0
  133. package/lib/TreeStructure/index.d.ts +10 -0
  134. package/lib/TreeStructure/index.js +15 -0
  135. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.d.ts +11 -0
  136. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +186 -0
  137. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +26 -0
  138. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +1 -0
  139. package/lib/User/AuthModal.d.ts +3 -3
  140. package/lib/User/AuthModal.js +16 -23
  141. package/lib/User/UserInfo.d.ts +13 -0
  142. package/lib/User/UserInfo.js +84 -0
  143. package/lib/User/apiTypes.d.ts +61 -0
  144. package/lib/User/apiTypes.js +1 -0
  145. package/lib/User/index.d.ts +4 -0
  146. package/lib/User/index.js +8 -0
  147. package/lib/User/parseUserObject.d.ts +32 -0
  148. package/lib/User/parseUserObject.js +105 -0
  149. package/lib/all.css +90 -0
  150. package/lib/index.d.ts +14 -3
  151. package/lib/index.js +83 -10
  152. package/lib/locale/messages-en.d.ts +67 -0
  153. package/lib/locale/messages-en.js +71 -4
  154. package/lib/locale/messages-nb.d.ts +67 -0
  155. package/lib/locale/messages-nb.js +70 -3
  156. package/lib/locale/messages-nn.d.ts +67 -0
  157. package/lib/locale/messages-nn.js +70 -3
  158. package/lib/locale/messages-se.d.ts +67 -0
  159. package/lib/locale/messages-se.js +70 -3
  160. package/lib/locale/messages-sma.d.ts +67 -0
  161. package/lib/locale/messages-sma.js +70 -3
  162. package/lib/types.d.ts +1 -1
  163. package/package.json +11 -11
  164. package/src/Article/Article.tsx +31 -0
  165. package/src/Article/ArticleFavoritesButton.tsx +40 -0
  166. package/src/Article/index.ts +2 -0
  167. package/src/Breadcrumb/ActionBreadcrumb.tsx +68 -0
  168. package/src/Breadcrumb/index.ts +2 -0
  169. package/src/Footer/FooterAuth.tsx +7 -9
  170. package/src/InfoBlock/InfoBlock.tsx +61 -0
  171. package/src/InfoBlock/index.ts +1 -0
  172. package/src/LearningPaths/LearningPathMenu.tsx +1 -1
  173. package/src/Masthead/MastheadAuthModal.tsx +4 -5
  174. package/src/MyNdla/Navigation/VerticalNavigation.tsx +93 -0
  175. package/src/MyNdla/Navigation/index.ts +2 -0
  176. package/src/MyNdla/Resource/Folder.tsx +145 -0
  177. package/src/MyNdla/Resource/FolderInput.tsx +104 -0
  178. package/src/MyNdla/Resource/index.ts +11 -0
  179. package/src/MyNdla/index.ts +4 -5
  180. package/src/Resource/BlockResource.tsx +101 -0
  181. package/src/Resource/ListResource.tsx +111 -0
  182. package/src/Resource/index.ts +12 -0
  183. package/src/Resource/resourceComponents.tsx +143 -0
  184. package/src/ResourceGroup/ResourceGroup.tsx +3 -0
  185. package/src/ResourceGroup/ResourceItem.tsx +20 -3
  186. package/src/ResourceGroup/ResourceList.tsx +16 -3
  187. package/src/Search/ActiveFilters.jsx +0 -1
  188. package/src/Search/ContentTypeResult.tsx +8 -9
  189. package/src/SearchTypeResult/ActiveFilters.tsx +1 -3
  190. package/src/SnackBar/SnackBar.tsx +183 -0
  191. package/src/SnackBar/index.ts +13 -0
  192. package/src/TagSelector/SuggestionInput.tsx +230 -0
  193. package/src/TagSelector/Suggestions.tsx +125 -0
  194. package/src/TagSelector/TagSelector.tsx +111 -0
  195. package/src/TagSelector/index.ts +13 -0
  196. package/src/TopicIntroductionList/TopicIntroduction.tsx +2 -2
  197. package/src/TopicIntroductionList/TopicShortcutItem.tsx +1 -5
  198. package/src/TreeStructure/FolderItem.tsx +160 -0
  199. package/src/TreeStructure/FolderItems.tsx +109 -0
  200. package/src/TreeStructure/FolderNameInput.tsx +109 -0
  201. package/src/TreeStructure/TreeStructure.tsx +184 -0
  202. package/src/TreeStructure/TreeStructure.types.ts +69 -0
  203. package/src/TreeStructure/TreeStructureWrapper.tsx +34 -0
  204. package/src/TreeStructure/helperFunctions.ts +52 -0
  205. package/src/TreeStructure/index.ts +11 -0
  206. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +161 -0
  207. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +28 -0
  208. package/src/User/AuthModal.tsx +5 -26
  209. package/src/User/UserInfo.tsx +80 -0
  210. package/src/User/__tests__/parseUserObject-test.ts +315 -0
  211. package/src/User/apiTypes.ts +74 -0
  212. package/src/User/index.ts +4 -0
  213. package/src/User/parseUserObject.ts +83 -0
  214. package/src/all.scss +2 -0
  215. package/src/index.ts +15 -4
  216. package/src/locale/messages-en.ts +65 -3
  217. package/src/locale/messages-nb.ts +64 -2
  218. package/src/locale/messages-nn.ts +64 -2
  219. package/src/locale/messages-se.ts +64 -2
  220. package/src/locale/messages-sma.ts +64 -2
  221. package/src/types.ts +1 -1
  222. package/es/MyNdla/ResourceDash/Breadcrumbs.js +0 -22
  223. package/es/MyNdla/ResourceDash/ResourceElement.js +0 -27
  224. package/es/MyNdla/ResourceDash/ResourcesView.js +0 -43
  225. package/es/MyNdla/ResourceDash/index.js +0 -4
  226. package/lib/MyNdla/ResourceDash/Breadcrumbs.d.ts +0 -15
  227. package/lib/MyNdla/ResourceDash/Breadcrumbs.js +0 -35
  228. package/lib/MyNdla/ResourceDash/ResourceElement.d.ts +0 -18
  229. package/lib/MyNdla/ResourceDash/ResourceElement.js +0 -38
  230. package/lib/MyNdla/ResourceDash/ResourcesView.js +0 -57
  231. package/lib/MyNdla/ResourceDash/index.d.ts +0 -4
  232. package/lib/MyNdla/ResourceDash/index.js +0 -31
  233. package/src/MyNdla/ResourceDash/Breadcrumbs.tsx +0 -31
  234. package/src/MyNdla/ResourceDash/ResourceElement.tsx +0 -50
  235. package/src/MyNdla/ResourceDash/ResourcesView.tsx +0 -42
  236. package/src/MyNdla/ResourceDash/index.ts +0 -5
@@ -0,0 +1,109 @@
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 React from 'react';
10
+ import styled from '@emotion/styled';
11
+ import { animations, spacing } from '@ndla/core';
12
+ import FolderItem from './FolderItem';
13
+ import FolderNameInput from './FolderNameInput';
14
+ import { FolderItemsProps } from './TreeStructure.types';
15
+ import { MAX_LEVEL_FOR_FOLDERS } from './TreeStructure';
16
+
17
+ const StyledUL = styled.ul<{ firstLevel?: boolean }>`
18
+ ${animations.fadeInLeft(animations.durations.fast)};
19
+ animation-fill-mode: forwards;
20
+ @media (prefers-reduced-motion: reduce) {
21
+ animation: none;
22
+ }
23
+ list-style: none;
24
+ margin: 0;
25
+ padding: 0;
26
+ margin-left: ${({ firstLevel }) => (firstLevel ? `-${spacing.xsmall}` : spacing.normal)};
27
+ `;
28
+
29
+ const StyledLI = styled.li`
30
+ margin: 0;
31
+ padding: 0;
32
+ `;
33
+
34
+ const FolderItems = ({
35
+ loading,
36
+ data,
37
+ idPaths,
38
+ editable,
39
+ onToggleOpen,
40
+ onCreateNewFolder,
41
+ onCancelNewFolder,
42
+ onSaveNewFolder,
43
+ newFolder,
44
+ openFolders,
45
+ markedFolderId,
46
+ onMarkFolder,
47
+ openOnFolderClick,
48
+ focusedFolderId,
49
+ setFocusedFolderId,
50
+ firstLevel,
51
+ }: FolderItemsProps) => (
52
+ <StyledUL role="group" firstLevel={firstLevel}>
53
+ {data.map(({ name, data: dataChildren, id, url, icon }, _index) => {
54
+ const newIdPaths = [...idPaths, _index];
55
+ const isOpen = openFolders?.has(id);
56
+ return (
57
+ <StyledLI key={id} role="treeitem">
58
+ <div>
59
+ <FolderItem
60
+ icon={icon}
61
+ url={url}
62
+ openOnFolderClick={openOnFolderClick}
63
+ loading={loading}
64
+ isOpen={isOpen}
65
+ id={id}
66
+ name={name}
67
+ markedFolderId={markedFolderId}
68
+ focusedFolderId={focusedFolderId}
69
+ onToggleOpen={onToggleOpen}
70
+ onMarkFolder={onMarkFolder}
71
+ hideArrow={dataChildren?.length === 0 || newIdPaths.length >= MAX_LEVEL_FOR_FOLDERS}
72
+ noPaddingWhenArrowIsHidden={editable && firstLevel && dataChildren?.length === 0}
73
+ setFocusedFolderId={setFocusedFolderId}
74
+ />
75
+ </div>
76
+ {newFolder?.parentId === id && (
77
+ <FolderNameInput
78
+ loading={loading}
79
+ onCancelNewFolder={onCancelNewFolder}
80
+ onSaveNewFolder={onSaveNewFolder}
81
+ />
82
+ )}
83
+ {dataChildren && isOpen && (
84
+ <FolderItems
85
+ loading={loading}
86
+ newFolder={newFolder}
87
+ openFolders={openFolders}
88
+ idPaths={newIdPaths}
89
+ editable={editable}
90
+ data={dataChildren}
91
+ onToggleOpen={onToggleOpen}
92
+ onCreateNewFolder={onCreateNewFolder}
93
+ onSaveNewFolder={onSaveNewFolder}
94
+ onCancelNewFolder={onCancelNewFolder}
95
+ markedFolderId={markedFolderId}
96
+ onMarkFolder={onMarkFolder}
97
+ openOnFolderClick={openOnFolderClick}
98
+ focusedFolderId={focusedFolderId}
99
+ setFocusedFolderId={setFocusedFolderId}
100
+ firstLevel={false}
101
+ />
102
+ )}
103
+ </StyledLI>
104
+ );
105
+ })}
106
+ </StyledUL>
107
+ );
108
+
109
+ export default FolderItems;
@@ -0,0 +1,109 @@
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 React, { useEffect, useState, useRef } from 'react';
10
+ import styled from '@emotion/styled';
11
+ import { FolderOutlined } from '@ndla/icons/contentType';
12
+ import { ArrowDropDown as ArrowDropDownRaw } from '@ndla/icons/common';
13
+ import { Spinner } from '@ndla/editor';
14
+ import { spacing, colors, misc, animations } from '@ndla/core';
15
+ import { useTranslation } from 'react-i18next';
16
+ import { isMobile } from 'react-device-detect';
17
+
18
+ const ArrowRight = styled(ArrowDropDownRaw)`
19
+ color: ${colors.text.primary};
20
+ transform: rotate(-90deg);
21
+ `;
22
+
23
+ const NewFolderWrapper = styled.div`
24
+ padding-left: ${spacing.normal};
25
+ ${animations.fadeInLeft(animations.durations.fast)};
26
+ animation-fill-mode: forwards;
27
+ @media (prefers-reduced-motion: reduce) {
28
+ animation: none;
29
+ }
30
+ `;
31
+
32
+ const InputWrapper = styled.div<{ loading?: boolean }>`
33
+ margin: ${spacing.xxsmall} ${spacing.small} ${spacing.xxsmall} 0;
34
+ display: flex;
35
+ align-items: center;
36
+ border: 1px solid ${({ loading }) => (loading ? colors.brand.lighter : colors.brand.primary)};
37
+ border-style: dashed;
38
+ border-radius: ${misc.borderRadius};
39
+ padding-right: ${spacing.normal};
40
+ padding-left: ${spacing.xsmall};
41
+ color: ${colors.brand.primary};
42
+ `;
43
+
44
+ const StyledInput = styled.input`
45
+ flex-grow: 1;
46
+ border: 0;
47
+ outline: none;
48
+ padding: ${spacing.small};
49
+ padding-left: ${spacing.xsmall};
50
+ background: transparent;
51
+ color: ${colors.text.primary};
52
+ scroll-margin-top: 100px;
53
+ `;
54
+
55
+ interface FolderNameInputProps {
56
+ onSaveNewFolder: (value: string) => void;
57
+ onCancelNewFolder: () => void;
58
+ loading?: boolean;
59
+ }
60
+
61
+ const FolderNameInput = ({ onSaveNewFolder, onCancelNewFolder, loading }: FolderNameInputProps) => {
62
+ const { t } = useTranslation();
63
+ const [value, setValue] = useState<string>(t('treeStructure.newFolder.defaultName'));
64
+ const inputRef = useRef<HTMLInputElement>(null);
65
+
66
+ useEffect(() => {
67
+ if (inputRef.current) {
68
+ inputRef.current.select();
69
+ if (isMobile) {
70
+ inputRef.current.scrollIntoView({ behavior: 'smooth' });
71
+ }
72
+ }
73
+ }, []);
74
+
75
+ return (
76
+ <NewFolderWrapper>
77
+ <InputWrapper loading={loading}>
78
+ <ArrowRight />
79
+ <FolderOutlined />
80
+ <StyledInput
81
+ ref={inputRef}
82
+ autoFocus
83
+ placeholder={t('treeStructure.newFolder.placeholder')}
84
+ disabled={loading}
85
+ value={value}
86
+ onBlur={() => onCancelNewFolder()}
87
+ onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
88
+ if (e.key === 'Escape') {
89
+ onCancelNewFolder();
90
+ return;
91
+ }
92
+ if (e.key === 'Enter' || e.key === 'Tab') {
93
+ onSaveNewFolder(value);
94
+ e.preventDefault();
95
+ }
96
+ return;
97
+ }}
98
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
99
+ const target = e.target as HTMLInputElement;
100
+ setValue(target.value);
101
+ }}
102
+ />
103
+ {loading && <Spinner size="small" />}
104
+ </InputWrapper>
105
+ </NewFolderWrapper>
106
+ );
107
+ };
108
+
109
+ export default FolderNameInput;
@@ -0,0 +1,184 @@
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 React, { useEffect, useState, useRef, useMemo } from 'react';
10
+ import { uuid } from '@ndla/util';
11
+ import Button from '@ndla/button';
12
+ import Tooltip from '@ndla/tooltip';
13
+ import { useTranslation } from 'react-i18next';
14
+ import styled from '@emotion/styled';
15
+ import { spacing, fonts } from '@ndla/core';
16
+ import TreeStructureStyledWrapper from './TreeStructureWrapper';
17
+ import FolderItems from './FolderItems';
18
+ import { getIdPathsOfFolder, getPathOfFolder, getFolderName } from './helperFunctions';
19
+ import keyboardNavigation, { KEYBOARD_KEYS_OF_INTEREST } from './keyboardNavigation/keyboardNavigation';
20
+ import { NewFolderProps, TreeStructureProps } from './TreeStructure.types';
21
+
22
+ export const MAX_LEVEL_FOR_FOLDERS = 4;
23
+
24
+ const StyledLabel = styled.label`
25
+ font-weight: ${fonts.weight.semibold};
26
+ `;
27
+
28
+ const AddFolderWrapper = styled.div`
29
+ display: flex;
30
+ margin-top: ${spacing.xsmall};
31
+ `;
32
+
33
+ const TreeStructure = ({
34
+ data,
35
+ label,
36
+ editable,
37
+ loading,
38
+ onNewFolder,
39
+ openOnFolderClick,
40
+ framed,
41
+ folderIdMarkedByDefault,
42
+ defaultOpenFolders,
43
+ }: TreeStructureProps) => {
44
+ const { t } = useTranslation();
45
+ const [newFolder, setNewFolder] = useState<NewFolderProps | undefined>();
46
+ const [openFolders, setOpenFolders] = useState<Set<string>>(new Set(defaultOpenFolders || []));
47
+ const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
48
+ const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0].id);
49
+ const treestructureRef = useRef<HTMLDivElement>(null);
50
+ const wrapperRef = useRef<HTMLDivElement>(null);
51
+ const rootLevelId = useMemo(() => uuid(), []); // TODO: use useId hook when we update to React 18
52
+
53
+ useEffect(() => {
54
+ setOpenFolders((prev) => {
55
+ defaultOpenFolders?.forEach((id) => prev.add(id));
56
+ return new Set(prev);
57
+ });
58
+ }, [defaultOpenFolders]);
59
+
60
+ useEffect(() => {
61
+ if (!loading) {
62
+ setNewFolder(undefined);
63
+ }
64
+ }, [loading]);
65
+
66
+ const onToggleOpen = (id: string) => {
67
+ setOpenFolders((prev) => {
68
+ if (prev.has(id)) {
69
+ prev.delete(id);
70
+ // Did we just closed a folder with a marked folder inside it?
71
+ // If so, we need to mark the folder we just closed.
72
+ if (markedFolderId) {
73
+ const closingFolderPath = getPathOfFolder(data, id);
74
+ const markedFolderPath = getPathOfFolder(data, markedFolderId);
75
+ const markedFolderIsSubPath = closingFolderPath.every(
76
+ (folderId, _index) => markedFolderPath[_index] === folderId,
77
+ );
78
+ if (markedFolderIsSubPath) {
79
+ setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
80
+ }
81
+ }
82
+ } else {
83
+ prev.add(id);
84
+ }
85
+ return new Set(prev);
86
+ });
87
+ };
88
+
89
+ const onCreateNewFolder = (props: { idPaths: number[]; parentId?: string }) => {
90
+ setNewFolder(props);
91
+ };
92
+
93
+ const onSaveNewFolder = async (value: string) => {
94
+ if (newFolder) {
95
+ // We would like to create a new folder with the name of value.
96
+ // Its location in structure is based on newFolder object
97
+ const newFolderId = await onNewFolder({ ...newFolder, value });
98
+ if (newFolderId) {
99
+ setMarkedFolderId(newFolderId);
100
+ setFocusedFolderId(newFolderId);
101
+ // Open current folder in case it was closed..
102
+ setOpenFolders((prev) => {
103
+ if (newFolder.parentId) {
104
+ prev.add(newFolder.parentId);
105
+ }
106
+ return new Set(prev);
107
+ });
108
+ }
109
+ }
110
+ };
111
+
112
+ const onCancelNewFolder = () => {
113
+ setNewFolder(undefined);
114
+ };
115
+
116
+ const onMarkFolder = (id: string) => {
117
+ setMarkedFolderId(id);
118
+ setFocusedFolderId(id);
119
+ };
120
+
121
+ const disableAddFolderButton =
122
+ markedFolderId === undefined || getPathOfFolder(data, markedFolderId).length >= MAX_LEVEL_FOR_FOLDERS;
123
+
124
+ return (
125
+ <div
126
+ ref={treestructureRef}
127
+ onKeyDown={(e) => {
128
+ if (wrapperRef.current?.contains(document.activeElement) && KEYBOARD_KEYS_OF_INTEREST.includes(e.key)) {
129
+ keyboardNavigation({
130
+ e,
131
+ data,
132
+ setFocusedFolderId,
133
+ focusedFolderId,
134
+ onToggleOpen,
135
+ openFolders,
136
+ });
137
+ }
138
+ }}>
139
+ <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>
140
+ <TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
141
+ <FolderItems
142
+ idPaths={[]}
143
+ data={data}
144
+ editable={editable}
145
+ onToggleOpen={onToggleOpen}
146
+ newFolder={newFolder}
147
+ onCreateNewFolder={onCreateNewFolder}
148
+ onCancelNewFolder={onCancelNewFolder}
149
+ onSaveNewFolder={onSaveNewFolder}
150
+ openFolders={openFolders}
151
+ markedFolderId={markedFolderId}
152
+ onMarkFolder={onMarkFolder}
153
+ openOnFolderClick={openOnFolderClick}
154
+ loading={loading}
155
+ focusedFolderId={focusedFolderId}
156
+ setFocusedFolderId={setFocusedFolderId}
157
+ firstLevel
158
+ />
159
+ </TreeStructureStyledWrapper>
160
+ {editable && (
161
+ <AddFolderWrapper>
162
+ <Tooltip
163
+ tooltip={t('myNdla.newFolderUnder', {
164
+ folderName: getFolderName(data, markedFolderId),
165
+ })}>
166
+ <Button
167
+ size="small"
168
+ light
169
+ disabled={disableAddFolderButton}
170
+ onClick={() => {
171
+ const paths = getPathOfFolder(data, markedFolderId || '');
172
+ const idPaths = getIdPathsOfFolder(data, markedFolderId || '');
173
+ setNewFolder({ idPaths, parentId: paths[paths.length - 1] });
174
+ }}>
175
+ {t('myNdla.newFolder')}
176
+ </Button>
177
+ </Tooltip>
178
+ </AddFolderWrapper>
179
+ )}
180
+ </div>
181
+ );
182
+ };
183
+
184
+ export default TreeStructure;
@@ -0,0 +1,69 @@
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 React from 'react';
10
+
11
+ export interface FolderStructureProps {
12
+ id: string;
13
+ name: string;
14
+ isOpen?: boolean;
15
+ data?: FolderStructureProps[];
16
+ isFavorite?: boolean;
17
+ status?: string;
18
+ openAsDefault?: boolean;
19
+ url?: string;
20
+ icon?: React.ReactNode;
21
+ }
22
+
23
+ export interface NewFolderProps {
24
+ parentId?: string;
25
+ idPaths: number[];
26
+ }
27
+
28
+ interface CommonFolderProps {
29
+ data: FolderStructureProps[];
30
+ editable?: boolean;
31
+ loading?: boolean;
32
+ openOnFolderClick?: boolean;
33
+ }
34
+
35
+ export interface TreeStructureProps extends CommonFolderProps {
36
+ framed?: boolean;
37
+ label: string;
38
+ folderIdMarkedByDefault?: string;
39
+ onNewFolder: (props: { value: string; parentId?: string; idPaths: number[] }) => Promise<string>;
40
+ defaultOpenFolders?: string[];
41
+ }
42
+
43
+ export type onCreateNewFolderProp = ({
44
+ idPaths,
45
+ parentId,
46
+ }: {
47
+ idPaths: number[];
48
+ parentId: string | undefined;
49
+ }) => void;
50
+
51
+ export type SetOpenFolderProp = React.Dispatch<React.SetStateAction<Set<string>>>;
52
+ export type SetFocusedFolderId = React.Dispatch<React.SetStateAction<string | undefined>>;
53
+
54
+ export interface FolderItemsProps extends CommonFolderProps {
55
+ onToggleOpen: (id: string) => void;
56
+ onSaveNewFolder: (value: string) => void;
57
+ onCancelNewFolder: () => void;
58
+ onCreateNewFolder: onCreateNewFolderProp;
59
+ newFolder: NewFolderProps | undefined;
60
+ openFolders: Set<string>;
61
+ markedFolderId?: string;
62
+ onMarkFolder: (id: string) => void;
63
+ idPaths: number[];
64
+ focusedFolderId: string | undefined;
65
+ setFocusedFolderId: SetFocusedFolderId;
66
+ firstLevel: boolean;
67
+ keyNavigationFocusIsCreateFolderButton?: boolean;
68
+ icon?: React.ReactElement;
69
+ }
@@ -0,0 +1,34 @@
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 { css } from '@emotion/core';
11
+ import { colors, misc, spacing } from '@ndla/core';
12
+
13
+ const TreeStructureWrapper = styled.div<{ framed?: boolean }>`
14
+ padding: ${spacing.xsmall};
15
+ ${({ framed }) =>
16
+ framed
17
+ ? css`
18
+ border: 1px solid ${colors.brand.greyLighter};
19
+ border-radius: ${misc.borderRadius};
20
+ max-height: 400px;
21
+ overflow-y: scroll;
22
+ scroll-behavior: smooth;
23
+ padding: ${spacing.small};
24
+ `
25
+ : css`
26
+ margin-left: -${spacing.medium};
27
+ `}
28
+ transition: ${misc.transition.default};
29
+ &:focus-within {
30
+ border-color: ${colors.brand.primary};
31
+ }
32
+ `;
33
+
34
+ export default TreeStructureWrapper;
@@ -0,0 +1,52 @@
1
+ import { FolderStructureProps } from './TreeStructure.types';
2
+
3
+ const getPathOfFolder = (data: FolderStructureProps[], findId: string): string[] => {
4
+ const paths = (dataChildren: FolderStructureProps[], path: string[]): string[] => {
5
+ for (const { id, data: dataChildrenSub } of dataChildren) {
6
+ if (id === findId) {
7
+ return [...path, id];
8
+ } else if (dataChildrenSub?.length) {
9
+ return paths(dataChildrenSub, [...path, id]);
10
+ }
11
+ }
12
+ return [];
13
+ };
14
+ return paths(data, []);
15
+ };
16
+
17
+ const getIdPathsOfFolder = (data: FolderStructureProps[], findId: string): number[] => {
18
+ let currentPath: number[] = [];
19
+ const paths = (dataChildren: FolderStructureProps[], path: number[]) => {
20
+ dataChildren.forEach(({ id, data: dataChildrenSub }, _index) => {
21
+ if (id === findId) {
22
+ currentPath = [...path, _index];
23
+ } else if (dataChildrenSub?.length) {
24
+ paths(dataChildrenSub, [...path, _index]);
25
+ }
26
+ });
27
+ };
28
+ paths(data, []);
29
+ return currentPath;
30
+ };
31
+
32
+ const getFolderName = (data: FolderStructureProps[], findId: string | undefined): string | undefined => {
33
+ if (!findId) {
34
+ return undefined;
35
+ }
36
+ let folderName: string | undefined;
37
+ const paths = (dataChildren: FolderStructureProps[]) => {
38
+ dataChildren.some(({ id, name, data: dataChildrenSub }, _index) => {
39
+ if (id === findId) {
40
+ folderName = name;
41
+ return true;
42
+ } else if (dataChildrenSub?.length) {
43
+ return paths(dataChildrenSub);
44
+ }
45
+ return false;
46
+ });
47
+ };
48
+ paths(data);
49
+ return folderName;
50
+ };
51
+
52
+ export { getPathOfFolder, getIdPathsOfFolder, getFolderName };
@@ -0,0 +1,11 @@
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 TreeStructure from './TreeStructure';
10
+ export type { FolderStructureProps, TreeStructureProps } from './TreeStructure.types';
11
+ export { TreeStructure };