@ndla/ui 14.0.0 → 15.1.1

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 (193) 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/InfoBlock/InfoBlock.js +55 -0
  7. package/es/InfoBlock/index.js +1 -0
  8. package/es/MyNdla/Navigation/VerticalNavigation.js +51 -0
  9. package/es/MyNdla/Navigation/index.js +2 -0
  10. package/es/MyNdla/Resource/Folder.js +86 -0
  11. package/es/MyNdla/Resource/FolderInput.js +96 -0
  12. package/{lib/MyNdla/ResourceDash/ResourcesView.d.ts → es/MyNdla/Resource/index.js} +3 -3
  13. package/es/MyNdla/index.js +4 -4
  14. package/es/Resource/BlockResource.js +73 -0
  15. package/es/Resource/ListResource.js +66 -0
  16. package/es/Resource/index.js +10 -0
  17. package/es/Resource/resourceComponents.js +97 -0
  18. package/es/ResourceGroup/ResourceGroup.js +7 -5
  19. package/es/ResourceGroup/ResourceItem.js +25 -24
  20. package/es/ResourceGroup/ResourceList.js +18 -6
  21. package/es/SnackBar/SnackBar.js +117 -0
  22. package/es/SnackBar/index.js +9 -0
  23. package/es/TagSelector/SuggestionInput.js +240 -0
  24. package/es/TagSelector/Suggestions.js +93 -0
  25. package/es/TagSelector/TagSelector.js +137 -0
  26. package/es/TagSelector/index.js +9 -0
  27. package/es/TreeStructure/FolderItem.js +130 -0
  28. package/es/TreeStructure/FolderItems.js +123 -0
  29. package/es/TreeStructure/FolderNameInput.js +112 -0
  30. package/es/TreeStructure/TreeStructure.js +254 -0
  31. package/es/TreeStructure/TreeStructure.types.js +0 -0
  32. package/es/TreeStructure/TreeStructureWrapper.js +13 -0
  33. package/es/TreeStructure/helperFunctions.js +92 -0
  34. package/es/TreeStructure/index.js +9 -0
  35. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +182 -0
  36. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -0
  37. package/es/all.css +72 -0
  38. package/es/index.js +8 -3
  39. package/es/locale/messages-en.js +62 -4
  40. package/es/locale/messages-nb.js +61 -3
  41. package/es/locale/messages-nn.js +61 -3
  42. package/es/locale/messages-se.js +61 -3
  43. package/es/locale/messages-sma.js +61 -3
  44. package/lib/Article/Article.d.ts +3 -1
  45. package/lib/Article/Article.js +43 -23
  46. package/lib/Article/ArticleFavoritesButton.d.ts +15 -0
  47. package/lib/Article/ArticleFavoritesButton.js +56 -0
  48. package/lib/Article/index.d.ts +2 -1
  49. package/lib/Article/index.js +8 -0
  50. package/lib/Breadcrumb/ActionBreadcrumb.d.ts +16 -0
  51. package/lib/Breadcrumb/ActionBreadcrumb.js +72 -0
  52. package/lib/Breadcrumb/index.d.ts +1 -0
  53. package/lib/Breadcrumb/index.js +8 -0
  54. package/lib/InfoBlock/InfoBlock.d.ts +8 -0
  55. package/lib/InfoBlock/InfoBlock.js +58 -0
  56. package/lib/InfoBlock/index.d.ts +1 -0
  57. package/lib/InfoBlock/index.js +13 -0
  58. package/lib/MyNdla/Navigation/VerticalNavigation.d.ts +10 -0
  59. package/lib/MyNdla/Navigation/VerticalNavigation.js +61 -0
  60. package/lib/MyNdla/Navigation/index.d.ts +2 -0
  61. package/lib/MyNdla/Navigation/index.js +15 -0
  62. package/lib/MyNdla/Resource/Folder.d.ts +20 -0
  63. package/lib/MyNdla/Resource/Folder.js +100 -0
  64. package/lib/MyNdla/Resource/FolderInput.d.ts +15 -0
  65. package/lib/MyNdla/Resource/FolderInput.js +116 -0
  66. package/lib/MyNdla/Resource/index.d.ts +10 -0
  67. package/lib/MyNdla/Resource/index.js +23 -0
  68. package/lib/MyNdla/index.d.ts +4 -4
  69. package/lib/MyNdla/index.js +13 -7
  70. package/lib/Resource/BlockResource.d.ts +20 -0
  71. package/lib/Resource/BlockResource.js +84 -0
  72. package/lib/Resource/ListResource.d.ts +20 -0
  73. package/lib/Resource/ListResource.js +78 -0
  74. package/lib/Resource/index.d.ts +11 -0
  75. package/lib/Resource/index.js +29 -0
  76. package/lib/Resource/resourceComponents.d.ts +24 -0
  77. package/lib/Resource/resourceComponents.js +106 -0
  78. package/lib/ResourceGroup/ResourceGroup.d.ts +2 -1
  79. package/lib/ResourceGroup/ResourceGroup.js +7 -5
  80. package/lib/ResourceGroup/ResourceItem.d.ts +5 -1
  81. package/lib/ResourceGroup/ResourceItem.js +26 -24
  82. package/lib/ResourceGroup/ResourceList.d.ts +3 -1
  83. package/lib/ResourceGroup/ResourceList.js +18 -6
  84. package/lib/SnackBar/SnackBar.d.ts +23 -0
  85. package/lib/SnackBar/SnackBar.js +127 -0
  86. package/lib/SnackBar/index.d.ts +10 -0
  87. package/lib/SnackBar/index.js +15 -0
  88. package/lib/TagSelector/SuggestionInput.d.ts +19 -0
  89. package/lib/TagSelector/SuggestionInput.js +255 -0
  90. package/lib/TagSelector/Suggestions.d.ts +12 -0
  91. package/lib/TagSelector/Suggestions.js +96 -0
  92. package/lib/TagSelector/TagSelector.d.ts +16 -0
  93. package/lib/TagSelector/TagSelector.js +150 -0
  94. package/lib/TagSelector/index.d.ts +10 -0
  95. package/lib/TagSelector/index.js +19 -0
  96. package/lib/TreeStructure/FolderItem.d.ts +27 -0
  97. package/lib/TreeStructure/FolderItem.js +140 -0
  98. package/lib/TreeStructure/FolderItems.d.ts +11 -0
  99. package/lib/TreeStructure/FolderItems.js +130 -0
  100. package/lib/TreeStructure/FolderNameInput.d.ts +15 -0
  101. package/lib/TreeStructure/FolderNameInput.js +125 -0
  102. package/lib/TreeStructure/TreeStructure.d.ts +12 -0
  103. package/lib/TreeStructure/TreeStructure.js +273 -0
  104. package/lib/TreeStructure/TreeStructure.types.d.ts +63 -0
  105. package/lib/TreeStructure/TreeStructure.types.js +1 -0
  106. package/lib/TreeStructure/TreeStructureWrapper.d.ts +12 -0
  107. package/lib/TreeStructure/TreeStructureWrapper.js +24 -0
  108. package/lib/TreeStructure/helperFunctions.d.ts +5 -0
  109. package/lib/TreeStructure/helperFunctions.js +103 -0
  110. package/lib/TreeStructure/index.d.ts +10 -0
  111. package/lib/TreeStructure/index.js +15 -0
  112. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.d.ts +11 -0
  113. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +186 -0
  114. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +26 -0
  115. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +1 -0
  116. package/lib/User/apiTypes.d.ts +1 -1
  117. package/lib/User/index.d.ts +2 -2
  118. package/lib/all.css +72 -0
  119. package/lib/index.d.ts +13 -4
  120. package/lib/index.js +75 -9
  121. package/lib/locale/messages-en.d.ts +58 -0
  122. package/lib/locale/messages-en.js +62 -4
  123. package/lib/locale/messages-nb.d.ts +58 -0
  124. package/lib/locale/messages-nb.js +61 -3
  125. package/lib/locale/messages-nn.d.ts +58 -0
  126. package/lib/locale/messages-nn.js +61 -3
  127. package/lib/locale/messages-se.d.ts +58 -0
  128. package/lib/locale/messages-se.js +61 -3
  129. package/lib/locale/messages-sma.d.ts +58 -0
  130. package/lib/locale/messages-sma.js +61 -3
  131. package/lib/types.d.ts +1 -1
  132. package/package.json +12 -11
  133. package/src/Article/Article.tsx +31 -0
  134. package/src/Article/ArticleFavoritesButton.tsx +40 -0
  135. package/src/Article/index.ts +2 -0
  136. package/src/Breadcrumb/ActionBreadcrumb.tsx +68 -0
  137. package/src/Breadcrumb/index.ts +2 -0
  138. package/src/InfoBlock/InfoBlock.tsx +61 -0
  139. package/src/InfoBlock/index.ts +1 -0
  140. package/src/MyNdla/Navigation/VerticalNavigation.tsx +93 -0
  141. package/src/MyNdla/Navigation/index.ts +2 -0
  142. package/src/MyNdla/Resource/Folder.tsx +145 -0
  143. package/src/MyNdla/Resource/FolderInput.tsx +104 -0
  144. package/src/MyNdla/Resource/index.ts +11 -0
  145. package/src/MyNdla/index.ts +4 -5
  146. package/src/Resource/BlockResource.tsx +101 -0
  147. package/src/Resource/ListResource.tsx +111 -0
  148. package/src/Resource/index.ts +12 -0
  149. package/src/Resource/resourceComponents.tsx +143 -0
  150. package/src/ResourceGroup/ResourceGroup.tsx +3 -0
  151. package/src/ResourceGroup/ResourceItem.tsx +17 -0
  152. package/src/ResourceGroup/ResourceList.tsx +16 -3
  153. package/src/SnackBar/SnackBar.tsx +183 -0
  154. package/src/SnackBar/index.ts +13 -0
  155. package/src/TagSelector/SuggestionInput.tsx +230 -0
  156. package/src/TagSelector/Suggestions.tsx +125 -0
  157. package/src/TagSelector/TagSelector.tsx +111 -0
  158. package/src/TagSelector/index.ts +13 -0
  159. package/src/TreeStructure/FolderItem.tsx +160 -0
  160. package/src/TreeStructure/FolderItems.tsx +109 -0
  161. package/src/TreeStructure/FolderNameInput.tsx +109 -0
  162. package/src/TreeStructure/TreeStructure.tsx +184 -0
  163. package/src/TreeStructure/TreeStructure.types.ts +69 -0
  164. package/src/TreeStructure/TreeStructureWrapper.tsx +34 -0
  165. package/src/TreeStructure/helperFunctions.ts +52 -0
  166. package/src/TreeStructure/index.ts +11 -0
  167. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +161 -0
  168. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +28 -0
  169. package/src/User/apiTypes.ts +1 -1
  170. package/src/User/index.ts +2 -2
  171. package/src/all.scss +1 -0
  172. package/src/index.ts +14 -5
  173. package/src/locale/messages-en.ts +56 -3
  174. package/src/locale/messages-nb.ts +55 -2
  175. package/src/locale/messages-nn.ts +55 -2
  176. package/src/locale/messages-se.ts +55 -2
  177. package/src/locale/messages-sma.ts +55 -2
  178. package/src/types.ts +1 -1
  179. package/es/MyNdla/ResourceDash/Breadcrumbs.js +0 -22
  180. package/es/MyNdla/ResourceDash/ResourceElement.js +0 -27
  181. package/es/MyNdla/ResourceDash/ResourcesView.js +0 -43
  182. package/es/MyNdla/ResourceDash/index.js +0 -4
  183. package/lib/MyNdla/ResourceDash/Breadcrumbs.d.ts +0 -15
  184. package/lib/MyNdla/ResourceDash/Breadcrumbs.js +0 -35
  185. package/lib/MyNdla/ResourceDash/ResourceElement.d.ts +0 -18
  186. package/lib/MyNdla/ResourceDash/ResourceElement.js +0 -38
  187. package/lib/MyNdla/ResourceDash/ResourcesView.js +0 -57
  188. package/lib/MyNdla/ResourceDash/index.d.ts +0 -4
  189. package/lib/MyNdla/ResourceDash/index.js +0 -31
  190. package/src/MyNdla/ResourceDash/Breadcrumbs.tsx +0 -31
  191. package/src/MyNdla/ResourceDash/ResourceElement.tsx +0 -50
  192. package/src/MyNdla/ResourceDash/ResourcesView.tsx +0 -42
  193. package/src/MyNdla/ResourceDash/index.ts +0 -5
@@ -0,0 +1,111 @@
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, { useState, useRef, useEffect, ChangeEvent } from 'react';
10
+ import styled from '@emotion/styled';
11
+ import { spacingUnit, fonts } from '@ndla/core';
12
+ import { uuid } from '@ndla/util';
13
+ import SuggestionInput from './SuggestionInput';
14
+
15
+ const DEFAULT_DROPDOWN_MAXHEIGHT = '240px';
16
+
17
+ const StyledLabel = styled.label`
18
+ font-weight: ${fonts.weight.semibold};
19
+ `;
20
+
21
+ export interface TagType {
22
+ name: string;
23
+ id: string;
24
+ }
25
+
26
+ interface Props {
27
+ label: string;
28
+ tags: TagType[];
29
+ tagsSelected: string[];
30
+ onToggleTag: (id: string) => void;
31
+ onCreateTag: (tagName: string) => void;
32
+ inline?: boolean;
33
+ prefix?: string;
34
+ }
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'));
40
+
41
+ const getSuggestions = (tags: TagType[], inputValue: string): TagType[] => {
42
+ if (inputValue === '') {
43
+ return [];
44
+ }
45
+ const inputLowercase = inputValue.toLowerCase();
46
+ return tags
47
+ .filter(({ name }) => name.toLowerCase().startsWith(inputLowercase))
48
+ .sort((a, b) => a.name.localeCompare(b.name, 'nb'));
49
+ };
50
+
51
+ const TagSelector = ({ label, tags, tagsSelected, onCreateTag, onToggleTag, inline, prefix }: Props) => {
52
+ const [inputValue, setInputValue] = useState('');
53
+ const [expanded, setExpanded] = useState(false);
54
+ const [dropdownMaxHeight, setDropdownMaxHeight] = useState(DEFAULT_DROPDOWN_MAXHEIGHT);
55
+ const containerRef = useRef<HTMLDivElement>(null);
56
+ const inputIdRef = useRef<string>(uuid());
57
+
58
+ useEffect(() => {
59
+ setExpanded(false);
60
+ }, [tagsSelected]);
61
+
62
+ useEffect(() => {
63
+ const setMaxDropdownMaxHeight = () => {
64
+ if (!inline && containerRef.current && typeof window !== 'undefined') {
65
+ // Calculate distance from bottom of container to bottom of viewport
66
+ const containerBottom = containerRef.current.getBoundingClientRect().bottom;
67
+ const viewportBottom = document.documentElement.scrollHeight;
68
+ const maxDropdownHeight = viewportBottom - containerBottom;
69
+ setDropdownMaxHeight(`${maxDropdownHeight - spacingUnit}px`);
70
+ }
71
+ };
72
+ if (!inline && typeof window !== 'undefined') {
73
+ if (expanded) {
74
+ setMaxDropdownMaxHeight();
75
+ window.addEventListener('resize', setMaxDropdownMaxHeight);
76
+ } else {
77
+ window.removeEventListener('resize', setMaxDropdownMaxHeight);
78
+ }
79
+ }
80
+ return () => {
81
+ typeof window !== 'undefined' && window.removeEventListener('resize', setMaxDropdownMaxHeight);
82
+ };
83
+ }, [expanded, inline]);
84
+
85
+ return (
86
+ <div ref={containerRef}>
87
+ <StyledLabel htmlFor={inputIdRef.current}>{label}</StyledLabel>
88
+ <SuggestionInput
89
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
90
+ const target = e.target as HTMLInputElement;
91
+ setInputValue(target.value);
92
+ setExpanded(false);
93
+ }}
94
+ suggestions={expanded ? tags : getSuggestions(tags, inputValue)}
95
+ value={inputValue}
96
+ onCreateTag={onCreateTag}
97
+ onToggleTag={onToggleTag}
98
+ setInputValue={setInputValue}
99
+ addedTags={sortedTags(tags, tagsSelected)}
100
+ expanded={expanded}
101
+ setExpanded={setExpanded}
102
+ dropdownMaxHeight={dropdownMaxHeight}
103
+ inline={inline}
104
+ scrollAnchorElement={containerRef}
105
+ prefix={prefix}
106
+ />
107
+ </div>
108
+ );
109
+ };
110
+
111
+ export default TagSelector;
@@ -0,0 +1,13 @@
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 TagSelector, { TagType } from './TagSelector';
10
+
11
+ export type { TagType };
12
+
13
+ export { TagSelector };
@@ -0,0 +1,160 @@
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, useRef } from 'react';
10
+ import styled from '@emotion/styled';
11
+ import { ArrowDropDown } from '@ndla/icons/common';
12
+ import { FolderOutlined } from '@ndla/icons/contentType';
13
+ import { colors, spacing, misc, animations } from '@ndla/core';
14
+ import { SetFocusedFolderId } from './TreeStructure.types';
15
+
16
+ const OpenButton = styled.button<{ isOpen: boolean }>`
17
+ background: transparent;
18
+ border: 0;
19
+ transform: rotate(${({ isOpen }) => (isOpen ? '0' : '-90')}deg);
20
+ padding: ${spacing.xsmall};
21
+ display: flex;
22
+ margin: 0;
23
+ color: ${colors.brand.secondary};
24
+ cursor: pointer;
25
+ &:hover {
26
+ color: ${colors.brand.primary};
27
+ }
28
+ svg {
29
+ width: 16px;
30
+ height: 16px;
31
+ transform: ${({ isOpen }) => (isOpen ? 'translateX(3px)' : 'translateY(3px)')};
32
+ }
33
+ `;
34
+
35
+ const FolderItemWrapper = styled.div`
36
+ display: flex;
37
+ align-items: center;
38
+ `;
39
+
40
+ const FolderName = styled.button<{ marked: boolean; noArrow?: boolean }>`
41
+ line-height: 1;
42
+ background: ${({ marked }) => (marked ? colors.brand.lighter : 'transparent')};
43
+ color: ${colors.text.primary};
44
+ &:hover,
45
+ &:focus {
46
+ background: ${({ marked }) => (marked ? colors.brand.light : colors.brand.lightest)};
47
+ color: ${colors.brand.primary};
48
+ }
49
+ transition: ${animations.durations.superFast};
50
+ border: 0;
51
+ border-radius: ${misc.borderRadius};
52
+ display: flex;
53
+ gap: ${spacing.xxsmall};
54
+ align-items: center;
55
+ cursor: pointer;
56
+ padding: ${spacing.xsmall};
57
+ margin: 0;
58
+ margin-left: ${({ noArrow }) => (noArrow ? `29px` : `0px`)};
59
+ flex-grow: 1;
60
+ box-shadow: none;
61
+ `;
62
+
63
+ const FolderNameLink = FolderName.withComponent('a');
64
+
65
+ interface Props {
66
+ name: string;
67
+ id: string;
68
+ onToggleOpen: (id: string) => void;
69
+ onMarkFolder: (id: string) => void;
70
+ isOpen: boolean;
71
+ markedFolderId?: string;
72
+ focusedFolderId?: string;
73
+ loading?: boolean;
74
+ openOnFolderClick?: boolean;
75
+ hideArrow?: boolean;
76
+ setFocusedFolderId: SetFocusedFolderId;
77
+ url?: string;
78
+ icon?: React.ReactNode;
79
+ noPaddingWhenArrowIsHidden?: boolean;
80
+ }
81
+
82
+ const FolderItem = ({
83
+ hideArrow,
84
+ loading,
85
+ name,
86
+ id,
87
+ onToggleOpen,
88
+ onMarkFolder,
89
+ isOpen,
90
+ markedFolderId,
91
+ focusedFolderId,
92
+ openOnFolderClick,
93
+ setFocusedFolderId,
94
+ icon,
95
+ url,
96
+ noPaddingWhenArrowIsHidden,
97
+ }: Props) => {
98
+ const folderNameLinkRef = useRef<HTMLAnchorElement | null>(null);
99
+ const folderNameButtonRef = useRef<HTMLButtonElement | null>(null);
100
+ useEffect(() => {
101
+ if (focusedFolderId === id) {
102
+ if (url && folderNameLinkRef.current) {
103
+ folderNameLinkRef.current.focus();
104
+ } else if (folderNameButtonRef.current) {
105
+ folderNameButtonRef.current.focus();
106
+ }
107
+ }
108
+ }, [focusedFolderId, folderNameLinkRef, folderNameButtonRef, url, id]);
109
+ const marked = markedFolderId === id;
110
+ return (
111
+ <FolderItemWrapper>
112
+ {!hideArrow && (
113
+ <OpenButton tabIndex={-1} isOpen={isOpen} disabled={loading} onClick={() => onToggleOpen(id)}>
114
+ <ArrowDropDown />
115
+ </OpenButton>
116
+ )}
117
+ {url ? (
118
+ <FolderNameLink
119
+ ref={folderNameLinkRef}
120
+ noArrow={hideArrow}
121
+ tabIndex={marked ? 0 : -1}
122
+ marked={marked}
123
+ href={loading ? undefined : url}
124
+ onFocus={() => {
125
+ setFocusedFolderId(id);
126
+ }}
127
+ onClick={() => {
128
+ onMarkFolder(id);
129
+ if (openOnFolderClick) {
130
+ onToggleOpen(id);
131
+ }
132
+ }}>
133
+ {icon || <FolderOutlined />}
134
+ {name}
135
+ </FolderNameLink>
136
+ ) : (
137
+ <FolderName
138
+ ref={folderNameButtonRef}
139
+ noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
140
+ tabIndex={marked ? 0 : -1}
141
+ marked={marked}
142
+ disabled={loading}
143
+ onFocus={() => {
144
+ setFocusedFolderId(id);
145
+ }}
146
+ onClick={() => {
147
+ onMarkFolder(id);
148
+ if (openOnFolderClick) {
149
+ onToggleOpen(id);
150
+ }
151
+ }}>
152
+ {icon || <FolderOutlined />}
153
+ {name}
154
+ </FolderName>
155
+ )}
156
+ </FolderItemWrapper>
157
+ );
158
+ };
159
+
160
+ export default FolderItem;
@@ -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;