@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,230 @@
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, ReactNode, RefObject, ChangeEvent, KeyboardEvent } from 'react';
10
+ import { isMobile } from 'react-device-detect';
11
+ import { useTranslation } from 'react-i18next';
12
+ import styled from '@emotion/styled';
13
+ import Button, { IconButtonDualStates } from '@ndla/button';
14
+ import { ChevronDown, ChevronUp } from '@ndla/icons/common';
15
+ import { Cross as CrossRaw } from '@ndla/icons/action';
16
+ import { spacing, colors, misc, animations, fonts } from '@ndla/core';
17
+ import Tooltip from '@ndla/tooltip';
18
+ import { uuid } from '@ndla/util';
19
+ import Suggestions from './Suggestions';
20
+ import type { TagType } from './TagSelector';
21
+
22
+ const Cross = styled(CrossRaw)`
23
+ margin-left: ${spacing.xxsmall};
24
+ `;
25
+
26
+ const SuggestionInputContainer = styled.div`
27
+ margin-bottom: ${spacing.large};
28
+ `;
29
+
30
+ const StyledInput = styled.input`
31
+ flex-grow: 1;
32
+ border: 0;
33
+ outline: none;
34
+ background: transparent;
35
+ ${fonts.sizes(18)};
36
+ `;
37
+ const StyledInputWrapper = styled.div`
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ gap: ${spacing.xsmall};
41
+ padding: ${spacing.small};
42
+ border: 1px solid ${colors.brand.greyLighter};
43
+ transition: border-color ${animations.durations.normal} ease;
44
+ border-radius: ${misc.borderRadius};
45
+ &:focus-within {
46
+ border-color: ${colors.brand.primary};
47
+ }
48
+ `;
49
+
50
+ const CombinedInputAndDropdownWrapper = styled.div`
51
+ display: flex;
52
+ flex-grow: 1;
53
+ `;
54
+
55
+ interface SuggestionInputProps {
56
+ suggestions: TagType[];
57
+ value: string;
58
+ onChange: (e: ChangeEvent<HTMLInputElement>) => void;
59
+ setExpanded: (expanded: boolean) => void;
60
+ expanded: boolean;
61
+ onToggleTag: (id: string) => void;
62
+ setInputValue: (value: string) => void;
63
+ onCreateTag: (tagName: string) => void;
64
+ addedTags: TagType[];
65
+ dropdownMaxHeight: string;
66
+ prefix?: string | ReactNode;
67
+ inline?: boolean;
68
+ scrollAnchorElement: RefObject<HTMLDivElement>;
69
+ }
70
+
71
+ const SuggestionInput = ({
72
+ suggestions,
73
+ value,
74
+ setInputValue,
75
+ onCreateTag,
76
+ onChange,
77
+ onToggleTag,
78
+ addedTags,
79
+ setExpanded,
80
+ expanded,
81
+ dropdownMaxHeight,
82
+ prefix,
83
+ inline,
84
+ scrollAnchorElement,
85
+ }: SuggestionInputProps) => {
86
+ const { t } = useTranslation();
87
+ const [currentHighlightedIndex, setCurrentHighlightedIndex] = useState(0);
88
+ const [hasFocus, setHasFocus] = useState(false);
89
+ const initalRender = useRef(true);
90
+ const inputRef = useRef<HTMLInputElement>(null);
91
+ const containerRef = useRef<HTMLDivElement>(null);
92
+ const suggestionIdRef = useRef<string>(uuid());
93
+
94
+ useEffect(() => {
95
+ setCurrentHighlightedIndex(0);
96
+ }, [suggestions]);
97
+
98
+ useEffect(() => {
99
+ if (!initalRender.current) {
100
+ inputRef.current?.focus();
101
+ } else {
102
+ initalRender.current = false;
103
+ }
104
+ }, [addedTags]);
105
+
106
+ useEffect(() => {
107
+ const selectedSuggestionElement = document
108
+ .getElementById(suggestionIdRef.current)
109
+ ?.querySelector('[aria-selected="true"]');
110
+ if (selectedSuggestionElement) {
111
+ // Do we need to scroll this into view?
112
+ selectedSuggestionElement.scrollIntoView({
113
+ behavior: 'smooth',
114
+ block: 'nearest',
115
+ });
116
+ }
117
+ }, [currentHighlightedIndex]);
118
+
119
+ const hasBeenAdded = (id: string) => addedTags.some(({ id: idAdded }) => idAdded === id);
120
+
121
+ return (
122
+ <SuggestionInputContainer ref={containerRef}>
123
+ <StyledInputWrapper>
124
+ {addedTags.map(({ id, name }) => (
125
+ <Button
126
+ aria-label={t('tagSelector.removeTag', { name })}
127
+ onClick={() => onToggleTag(id)}
128
+ light
129
+ borderShape="rounded"
130
+ key={id}
131
+ size="small">
132
+ {prefix}
133
+ {name}
134
+ <Cross />
135
+ </Button>
136
+ ))}
137
+ <CombinedInputAndDropdownWrapper>
138
+ <StyledInput
139
+ placeholder={t('tagSelector.placeholder')}
140
+ value={value}
141
+ autoComplete="off"
142
+ onBlur={(e) => {
143
+ const relatedTarget = e.relatedTarget as HTMLElement;
144
+ if (!relatedTarget?.dataset?.suggestionbutton) {
145
+ setExpanded(false);
146
+ setHasFocus(false);
147
+ }
148
+ }}
149
+ onChange={onChange}
150
+ onFocus={() => {
151
+ if (isMobile && scrollAnchorElement?.current) {
152
+ scrollAnchorElement.current.scrollIntoView({
153
+ behavior: 'smooth',
154
+ });
155
+ }
156
+ setHasFocus(true);
157
+ }}
158
+ ref={inputRef}
159
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
160
+ if (e.key === 'Escape') {
161
+ setExpanded(false);
162
+ 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') {
182
+ setCurrentHighlightedIndex(
183
+ currentHighlightedIndex - 1 < 0 ? suggestions.length - 1 : currentHighlightedIndex - 1,
184
+ );
185
+ e.preventDefault();
186
+ } else if (e.key === 'ArrowDown') {
187
+ setCurrentHighlightedIndex(
188
+ currentHighlightedIndex + 1 >= suggestions.length ? 0 : currentHighlightedIndex + 1,
189
+ );
190
+ e.preventDefault();
191
+ }
192
+ }}
193
+ />
194
+ <Tooltip tooltip={expanded ? t('tagSelector.hideAllTags') : t('tagSelector.showAllTags')}>
195
+ <IconButtonDualStates
196
+ data-suggestionbutton
197
+ ariaLabelActive={t('tagSelector.showAllTags')}
198
+ ariaLabelInActive={t('tagSelector.hideAllTags')}
199
+ active={expanded}
200
+ greyLighter
201
+ inactiveIcon={<ChevronDown />}
202
+ activeIcon={<ChevronUp />}
203
+ size="small"
204
+ aria-controls={suggestionIdRef.current}
205
+ onClick={() => {
206
+ setInputValue('');
207
+ setExpanded(!expanded);
208
+ inputRef.current?.focus();
209
+ }}
210
+ />
211
+ </Tooltip>
212
+ </CombinedInputAndDropdownWrapper>
213
+ </StyledInputWrapper>
214
+ <div id={suggestionIdRef.current} aria-live="polite">
215
+ {(hasFocus || expanded) && suggestions.length > 0 ? (
216
+ <Suggestions
217
+ inline={inline}
218
+ dropdownMaxHeight={dropdownMaxHeight}
219
+ suggestions={suggestions}
220
+ currentHighlightedIndex={currentHighlightedIndex}
221
+ onToggleTag={onToggleTag}
222
+ hasBeenAdded={hasBeenAdded}
223
+ />
224
+ ) : null}
225
+ </div>
226
+ </SuggestionInputContainer>
227
+ );
228
+ };
229
+
230
+ export default SuggestionInput;
@@ -0,0 +1,125 @@
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 { Check } from '@ndla/icons/editor';
12
+ import { spacing, colors, misc, animations, fonts, shadows } from '@ndla/core';
13
+ import Button from '@ndla/button';
14
+ import type { TagType } from './TagSelector';
15
+
16
+ const ABSOLUTE_DROPDOWN_MAXHEIGHT = '360px';
17
+
18
+ const CheckedIcon = styled(Check)`
19
+ width: ${spacing.normal};
20
+ height: ${spacing.normal};
21
+ fill: ${colors.brand.light};
22
+ `;
23
+
24
+ interface SuggestionsWrapperProps {
25
+ dropdownMaxHeight: string;
26
+ inline?: boolean;
27
+ }
28
+
29
+ const SuggestionsWrapper = styled.div`
30
+ position: relative;
31
+ `;
32
+
33
+ const Suggestions = styled.div<SuggestionsWrapperProps>`
34
+ position: ${({ inline }) => (inline ? 'static' : 'absolute')};
35
+ z-index: 99999;
36
+ right: 0;
37
+ left: 0;
38
+ box-shadow: ${shadows.levitate1};
39
+ margin: 0 ${spacing.small};
40
+ padding: ${spacing.small} 0;
41
+ overflow-y: scroll;
42
+ scroll-behavior: smooth;
43
+ max-height: min(${({ dropdownMaxHeight }) => dropdownMaxHeight}, ${ABSOLUTE_DROPDOWN_MAXHEIGHT});
44
+ border-radius: ${misc.borderRadius};
45
+ background: ${colors.white};
46
+ ${animations.fadeIn(animations.durations.fast)}
47
+ `;
48
+
49
+ const SuggestionList = styled.div`
50
+ opacity: 0;
51
+ ${animations.fadeInBottom()}
52
+ animation-delay: ${animations.durations.fast};
53
+ animation-fill-mode: forwards;
54
+ `;
55
+
56
+ interface SuggestionButtonProps {
57
+ isHighlighted: boolean;
58
+ }
59
+
60
+ const SuggestionButton = styled(Button)<SuggestionButtonProps>`
61
+ display: flex;
62
+ justify-content: space-between;
63
+ ${fonts.sizes(18)};
64
+ transition: ${misc.transition.default};
65
+ font-weight: 400;
66
+
67
+ &:disabled {
68
+ color: ${colors.brand.greyMedium};
69
+ &:hover {
70
+ svg {
71
+ fill: ${colors.brand.greyLight};
72
+ }
73
+ }
74
+ }
75
+ `;
76
+
77
+ interface Props {
78
+ inline?: boolean;
79
+ dropdownMaxHeight: string;
80
+ suggestions: TagType[];
81
+ currentHighlightedIndex: number;
82
+ onToggleTag: (id: string) => void;
83
+ hasBeenAdded: (id: string) => boolean;
84
+ }
85
+
86
+ const TagSuggestions = ({
87
+ inline,
88
+ dropdownMaxHeight,
89
+ suggestions,
90
+ currentHighlightedIndex,
91
+ onToggleTag,
92
+ hasBeenAdded,
93
+ }: Props) => (
94
+ <SuggestionsWrapper>
95
+ <Suggestions inline={inline} dropdownMaxHeight={dropdownMaxHeight}>
96
+ <SuggestionList role="listbox">
97
+ {suggestions.map(({ id, name }, index: number) => {
98
+ const alreadyAdded = hasBeenAdded(id);
99
+ const selected = index === currentHighlightedIndex;
100
+ return (
101
+ <SuggestionButton
102
+ borderShape="sharpened"
103
+ ghostPill
104
+ width="full"
105
+ textAlign="left"
106
+ data-suggestionbutton
107
+ role="option"
108
+ aria-selected={selected}
109
+ disabled={alreadyAdded}
110
+ isHighlighted={selected}
111
+ onMouseDown={() => {
112
+ onToggleTag(id);
113
+ }}
114
+ key={id}>
115
+ <span>{name}</span>
116
+ {alreadyAdded && <CheckedIcon />}
117
+ </SuggestionButton>
118
+ );
119
+ })}
120
+ </SuggestionList>
121
+ </Suggestions>
122
+ </SuggestionsWrapper>
123
+ );
124
+
125
+ export default TagSuggestions;
@@ -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 };
@@ -64,12 +64,12 @@ export const TopicIntroduction = ({
64
64
  {contentTypeDescription}
65
65
  </span>
66
66
  {additional && (
67
- <Tooltip tooltip={t('resource.tooltipAdditionalTopic')} align="left">
67
+ <Tooltip tooltip={t('resource.tooltipAdditionalTopic')}>
68
68
  <Additional className="c-icon--20 u-margin-left-tiny" />
69
69
  </Tooltip>
70
70
  )}
71
71
  {!additional && showAdditionalCores && (
72
- <Tooltip tooltip={t('resource.tooltipCoreTopic')} align="left">
72
+ <Tooltip tooltip={t('resource.tooltipCoreTopic')}>
73
73
  <Core className="c-icon--20 u-margin-left-tiny" />
74
74
  </Tooltip>
75
75
  )}
@@ -17,11 +17,7 @@ interface Props {
17
17
  const ShortcutItem = ({ shortcut: { id, tooltip, contentType, url, count } }: Props) => {
18
18
  const { t } = useTranslation();
19
19
  return (
20
- <Tooltip
21
- id={`shortcut-tooltip-${id}`}
22
- tooltip={t('resource.shortcutsTooltip', { count })}
23
- delay={100}
24
- align="bottom">
20
+ <Tooltip id={`shortcut-tooltip-${id}`} tooltip={t('resource.shortcutsTooltip', { count })}>
25
21
  <SafeLink {...classes('item-link')} aria-label={tooltip} to={url}>
26
22
  <ContentTypeBadge type={contentType} size="x-small" background />
27
23
  <span {...classes('count')}>{count}</span>
@@ -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;