@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.
- package/es/Article/Article.js +22 -3
- package/es/Article/ArticleFavoritesButton.js +38 -0
- package/es/Article/index.js +2 -1
- package/es/Breadcrumb/ActionBreadcrumb.js +57 -0
- package/es/Breadcrumb/index.js +1 -0
- package/es/InfoBlock/InfoBlock.js +55 -0
- package/es/InfoBlock/index.js +1 -0
- package/es/MyNdla/Navigation/VerticalNavigation.js +51 -0
- package/es/MyNdla/Navigation/index.js +2 -0
- package/es/MyNdla/Resource/Folder.js +86 -0
- package/es/MyNdla/Resource/FolderInput.js +96 -0
- package/{lib/MyNdla/ResourceDash/ResourcesView.d.ts → es/MyNdla/Resource/index.js} +3 -3
- package/es/MyNdla/index.js +4 -4
- package/es/Resource/BlockResource.js +73 -0
- package/es/Resource/ListResource.js +66 -0
- package/es/Resource/index.js +10 -0
- package/es/Resource/resourceComponents.js +97 -0
- package/es/ResourceGroup/ResourceGroup.js +7 -5
- package/es/ResourceGroup/ResourceItem.js +25 -24
- package/es/ResourceGroup/ResourceList.js +18 -6
- package/es/SnackBar/SnackBar.js +117 -0
- package/es/SnackBar/index.js +9 -0
- package/es/TagSelector/SuggestionInput.js +240 -0
- package/es/TagSelector/Suggestions.js +93 -0
- package/es/TagSelector/TagSelector.js +137 -0
- package/es/TagSelector/index.js +9 -0
- package/es/TreeStructure/FolderItem.js +130 -0
- package/es/TreeStructure/FolderItems.js +123 -0
- package/es/TreeStructure/FolderNameInput.js +112 -0
- package/es/TreeStructure/TreeStructure.js +254 -0
- package/es/TreeStructure/TreeStructure.types.js +0 -0
- package/es/TreeStructure/TreeStructureWrapper.js +13 -0
- package/es/TreeStructure/helperFunctions.js +92 -0
- package/es/TreeStructure/index.js +9 -0
- package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +182 -0
- package/es/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -0
- package/es/all.css +72 -0
- package/es/index.js +8 -3
- package/es/locale/messages-en.js +62 -4
- package/es/locale/messages-nb.js +61 -3
- package/es/locale/messages-nn.js +61 -3
- package/es/locale/messages-se.js +61 -3
- package/es/locale/messages-sma.js +61 -3
- package/lib/Article/Article.d.ts +3 -1
- package/lib/Article/Article.js +43 -23
- package/lib/Article/ArticleFavoritesButton.d.ts +15 -0
- package/lib/Article/ArticleFavoritesButton.js +56 -0
- package/lib/Article/index.d.ts +2 -1
- package/lib/Article/index.js +8 -0
- package/lib/Breadcrumb/ActionBreadcrumb.d.ts +16 -0
- package/lib/Breadcrumb/ActionBreadcrumb.js +72 -0
- package/lib/Breadcrumb/index.d.ts +1 -0
- package/lib/Breadcrumb/index.js +8 -0
- package/lib/InfoBlock/InfoBlock.d.ts +8 -0
- package/lib/InfoBlock/InfoBlock.js +58 -0
- package/lib/InfoBlock/index.d.ts +1 -0
- package/lib/InfoBlock/index.js +13 -0
- package/lib/MyNdla/Navigation/VerticalNavigation.d.ts +10 -0
- package/lib/MyNdla/Navigation/VerticalNavigation.js +61 -0
- package/lib/MyNdla/Navigation/index.d.ts +2 -0
- package/lib/MyNdla/Navigation/index.js +15 -0
- package/lib/MyNdla/Resource/Folder.d.ts +20 -0
- package/lib/MyNdla/Resource/Folder.js +100 -0
- package/lib/MyNdla/Resource/FolderInput.d.ts +15 -0
- package/lib/MyNdla/Resource/FolderInput.js +116 -0
- package/lib/MyNdla/Resource/index.d.ts +10 -0
- package/lib/MyNdla/Resource/index.js +23 -0
- package/lib/MyNdla/index.d.ts +4 -4
- package/lib/MyNdla/index.js +13 -7
- package/lib/Resource/BlockResource.d.ts +20 -0
- package/lib/Resource/BlockResource.js +84 -0
- package/lib/Resource/ListResource.d.ts +20 -0
- package/lib/Resource/ListResource.js +78 -0
- package/lib/Resource/index.d.ts +11 -0
- package/lib/Resource/index.js +29 -0
- package/lib/Resource/resourceComponents.d.ts +24 -0
- package/lib/Resource/resourceComponents.js +106 -0
- package/lib/ResourceGroup/ResourceGroup.d.ts +2 -1
- package/lib/ResourceGroup/ResourceGroup.js +7 -5
- package/lib/ResourceGroup/ResourceItem.d.ts +5 -1
- package/lib/ResourceGroup/ResourceItem.js +26 -24
- package/lib/ResourceGroup/ResourceList.d.ts +3 -1
- package/lib/ResourceGroup/ResourceList.js +18 -6
- package/lib/SnackBar/SnackBar.d.ts +23 -0
- package/lib/SnackBar/SnackBar.js +127 -0
- package/lib/SnackBar/index.d.ts +10 -0
- package/lib/SnackBar/index.js +15 -0
- package/lib/TagSelector/SuggestionInput.d.ts +19 -0
- package/lib/TagSelector/SuggestionInput.js +255 -0
- package/lib/TagSelector/Suggestions.d.ts +12 -0
- package/lib/TagSelector/Suggestions.js +96 -0
- package/lib/TagSelector/TagSelector.d.ts +16 -0
- package/lib/TagSelector/TagSelector.js +150 -0
- package/lib/TagSelector/index.d.ts +10 -0
- package/lib/TagSelector/index.js +19 -0
- package/lib/TreeStructure/FolderItem.d.ts +27 -0
- package/lib/TreeStructure/FolderItem.js +140 -0
- package/lib/TreeStructure/FolderItems.d.ts +11 -0
- package/lib/TreeStructure/FolderItems.js +130 -0
- package/lib/TreeStructure/FolderNameInput.d.ts +15 -0
- package/lib/TreeStructure/FolderNameInput.js +125 -0
- package/lib/TreeStructure/TreeStructure.d.ts +12 -0
- package/lib/TreeStructure/TreeStructure.js +273 -0
- package/lib/TreeStructure/TreeStructure.types.d.ts +63 -0
- package/lib/TreeStructure/TreeStructure.types.js +1 -0
- package/lib/TreeStructure/TreeStructureWrapper.d.ts +12 -0
- package/lib/TreeStructure/TreeStructureWrapper.js +24 -0
- package/lib/TreeStructure/helperFunctions.d.ts +5 -0
- package/lib/TreeStructure/helperFunctions.js +103 -0
- package/lib/TreeStructure/index.d.ts +10 -0
- package/lib/TreeStructure/index.js +15 -0
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.d.ts +11 -0
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +186 -0
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +26 -0
- package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +1 -0
- package/lib/User/apiTypes.d.ts +1 -1
- package/lib/User/index.d.ts +2 -2
- package/lib/all.css +72 -0
- package/lib/index.d.ts +13 -4
- package/lib/index.js +75 -9
- package/lib/locale/messages-en.d.ts +58 -0
- package/lib/locale/messages-en.js +62 -4
- package/lib/locale/messages-nb.d.ts +58 -0
- package/lib/locale/messages-nb.js +61 -3
- package/lib/locale/messages-nn.d.ts +58 -0
- package/lib/locale/messages-nn.js +61 -3
- package/lib/locale/messages-se.d.ts +58 -0
- package/lib/locale/messages-se.js +61 -3
- package/lib/locale/messages-sma.d.ts +58 -0
- package/lib/locale/messages-sma.js +61 -3
- package/lib/types.d.ts +1 -1
- package/package.json +12 -11
- package/src/Article/Article.tsx +31 -0
- package/src/Article/ArticleFavoritesButton.tsx +40 -0
- package/src/Article/index.ts +2 -0
- package/src/Breadcrumb/ActionBreadcrumb.tsx +68 -0
- package/src/Breadcrumb/index.ts +2 -0
- package/src/InfoBlock/InfoBlock.tsx +61 -0
- package/src/InfoBlock/index.ts +1 -0
- package/src/MyNdla/Navigation/VerticalNavigation.tsx +93 -0
- package/src/MyNdla/Navigation/index.ts +2 -0
- package/src/MyNdla/Resource/Folder.tsx +145 -0
- package/src/MyNdla/Resource/FolderInput.tsx +104 -0
- package/src/MyNdla/Resource/index.ts +11 -0
- package/src/MyNdla/index.ts +4 -5
- package/src/Resource/BlockResource.tsx +101 -0
- package/src/Resource/ListResource.tsx +111 -0
- package/src/Resource/index.ts +12 -0
- package/src/Resource/resourceComponents.tsx +143 -0
- package/src/ResourceGroup/ResourceGroup.tsx +3 -0
- package/src/ResourceGroup/ResourceItem.tsx +17 -0
- package/src/ResourceGroup/ResourceList.tsx +16 -3
- package/src/SnackBar/SnackBar.tsx +183 -0
- package/src/SnackBar/index.ts +13 -0
- package/src/TagSelector/SuggestionInput.tsx +230 -0
- package/src/TagSelector/Suggestions.tsx +125 -0
- package/src/TagSelector/TagSelector.tsx +111 -0
- package/src/TagSelector/index.ts +13 -0
- package/src/TreeStructure/FolderItem.tsx +160 -0
- package/src/TreeStructure/FolderItems.tsx +109 -0
- package/src/TreeStructure/FolderNameInput.tsx +109 -0
- package/src/TreeStructure/TreeStructure.tsx +184 -0
- package/src/TreeStructure/TreeStructure.types.ts +69 -0
- package/src/TreeStructure/TreeStructureWrapper.tsx +34 -0
- package/src/TreeStructure/helperFunctions.ts +52 -0
- package/src/TreeStructure/index.ts +11 -0
- package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +161 -0
- package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +28 -0
- package/src/User/apiTypes.ts +1 -1
- package/src/User/index.ts +2 -2
- package/src/all.scss +1 -0
- package/src/index.ts +14 -5
- package/src/locale/messages-en.ts +56 -3
- package/src/locale/messages-nb.ts +55 -2
- package/src/locale/messages-nn.ts +55 -2
- package/src/locale/messages-se.ts +55 -2
- package/src/locale/messages-sma.ts +55 -2
- package/src/types.ts +1 -1
- package/es/MyNdla/ResourceDash/Breadcrumbs.js +0 -22
- package/es/MyNdla/ResourceDash/ResourceElement.js +0 -27
- package/es/MyNdla/ResourceDash/ResourcesView.js +0 -43
- package/es/MyNdla/ResourceDash/index.js +0 -4
- package/lib/MyNdla/ResourceDash/Breadcrumbs.d.ts +0 -15
- package/lib/MyNdla/ResourceDash/Breadcrumbs.js +0 -35
- package/lib/MyNdla/ResourceDash/ResourceElement.d.ts +0 -18
- package/lib/MyNdla/ResourceDash/ResourceElement.js +0 -38
- package/lib/MyNdla/ResourceDash/ResourcesView.js +0 -57
- package/lib/MyNdla/ResourceDash/index.d.ts +0 -4
- package/lib/MyNdla/ResourceDash/index.js +0 -31
- package/src/MyNdla/ResourceDash/Breadcrumbs.tsx +0 -31
- package/src/MyNdla/ResourceDash/ResourceElement.tsx +0 -50
- package/src/MyNdla/ResourceDash/ResourcesView.tsx +0 -42
- package/src/MyNdla/ResourceDash/index.ts +0 -5
|
@@ -15,6 +15,7 @@ import SafeLink from '@ndla/safelink';
|
|
|
15
15
|
import { Additional, Core, HumanMaleBoard } from '@ndla/icons/common';
|
|
16
16
|
import { breakpoints, colors, fonts, mq, spacing } from '@ndla/core';
|
|
17
17
|
import Tooltip from '@ndla/tooltip';
|
|
18
|
+
import { ArticleFavoritesButton } from '../Article';
|
|
18
19
|
import { Resource } from '../types';
|
|
19
20
|
import ContentTypeBadge from '../ContentTypeBadge';
|
|
20
21
|
import * as contentTypes from '../model/ContentType';
|
|
@@ -208,6 +209,7 @@ const IconWrapper = styled.div`
|
|
|
208
209
|
const TypeWrapper = styled.div`
|
|
209
210
|
display: flex;
|
|
210
211
|
align-items: center;
|
|
212
|
+
gap: ${spacing.xsmall};
|
|
211
213
|
`;
|
|
212
214
|
|
|
213
215
|
const ContentTypeName = styled.span`
|
|
@@ -219,15 +221,20 @@ const ContentTypeName = styled.span`
|
|
|
219
221
|
`;
|
|
220
222
|
|
|
221
223
|
type Props = {
|
|
224
|
+
id: string;
|
|
222
225
|
showContentTypeDescription?: boolean;
|
|
223
226
|
contentTypeName?: string;
|
|
224
227
|
contentTypeDescription?: string;
|
|
225
228
|
extraBottomMargin?: boolean;
|
|
226
229
|
showAdditionalResources?: boolean;
|
|
227
230
|
access?: 'teacher';
|
|
231
|
+
isFavorite?: boolean;
|
|
232
|
+
onToggleAddToFavorites: (id: string, add: boolean) => void;
|
|
233
|
+
hideAddToFavoriteButton?: boolean;
|
|
228
234
|
};
|
|
229
235
|
|
|
230
236
|
const ResourceItem = ({
|
|
237
|
+
id,
|
|
231
238
|
contentTypeName,
|
|
232
239
|
contentTypeDescription,
|
|
233
240
|
name,
|
|
@@ -238,6 +245,9 @@ const ResourceItem = ({
|
|
|
238
245
|
extraBottomMargin,
|
|
239
246
|
showAdditionalResources,
|
|
240
247
|
access,
|
|
248
|
+
onToggleAddToFavorites,
|
|
249
|
+
isFavorite,
|
|
250
|
+
hideAddToFavoriteButton,
|
|
241
251
|
}: Props & Resource) => {
|
|
242
252
|
const { t } = useTranslation();
|
|
243
253
|
const hidden = additional ? !showAdditionalResources : false;
|
|
@@ -291,6 +301,13 @@ const ResourceItem = ({
|
|
|
291
301
|
)}
|
|
292
302
|
</>
|
|
293
303
|
)}
|
|
304
|
+
{!hideAddToFavoriteButton && (
|
|
305
|
+
<ArticleFavoritesButton
|
|
306
|
+
isFavorite={isFavorite}
|
|
307
|
+
articleId={id}
|
|
308
|
+
onToggleAddToFavorites={() => onToggleAddToFavorites(id, true)}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
294
311
|
</TypeWrapper>
|
|
295
312
|
</ListElement>
|
|
296
313
|
);
|
|
@@ -48,9 +48,19 @@ export type ResourceListProps = {
|
|
|
48
48
|
contentType?: string;
|
|
49
49
|
title?: string;
|
|
50
50
|
showAdditionalResources?: boolean;
|
|
51
|
+
onToggleAddToFavorites: (id: string, add: boolean) => void;
|
|
52
|
+
hideAddToFavoriteButton?: boolean;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
const ResourceList = ({
|
|
55
|
+
const ResourceList = ({
|
|
56
|
+
resources,
|
|
57
|
+
onClick,
|
|
58
|
+
onToggleAddToFavorites,
|
|
59
|
+
contentType,
|
|
60
|
+
title,
|
|
61
|
+
showAdditionalResources,
|
|
62
|
+
hideAddToFavoriteButton,
|
|
63
|
+
}: ResourceListProps) => {
|
|
54
64
|
const { t } = useTranslation();
|
|
55
65
|
const renderAdditionalResourceTrigger =
|
|
56
66
|
!showAdditionalResources &&
|
|
@@ -60,11 +70,14 @@ const ResourceList = ({ resources, onClick, contentType, title, showAdditionalRe
|
|
|
60
70
|
return (
|
|
61
71
|
<div>
|
|
62
72
|
<StyledResourceList showAdditionalResources={showAdditionalResources}>
|
|
63
|
-
{resources.map((resource) => (
|
|
73
|
+
{resources.map(({ id, ...resource }) => (
|
|
64
74
|
<ResourceItem
|
|
65
|
-
|
|
75
|
+
id={id}
|
|
76
|
+
key={id}
|
|
66
77
|
contentType={contentType}
|
|
67
78
|
showAdditionalResources={showAdditionalResources}
|
|
79
|
+
hideAddToFavoriteButton={hideAddToFavoriteButton}
|
|
80
|
+
onToggleAddToFavorites={onToggleAddToFavorites}
|
|
68
81
|
{...resource}
|
|
69
82
|
contentTypeDescription={
|
|
70
83
|
resource.additional ? t('resource.tooltipAdditionalTopic') : t('resource.tooltipCoreTopic')
|
|
@@ -0,0 +1,183 @@
|
|
|
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, { ReactElement, useRef, useEffect, useState } from 'react';
|
|
10
|
+
import styled from '@emotion/styled';
|
|
11
|
+
import Button, { IconButton } from '@ndla/button';
|
|
12
|
+
import { spacing, spacingUnit, shadows, misc, fonts, colors, mq, breakpoints } from '@ndla/core';
|
|
13
|
+
import { Cross } from '@ndla/icons/action';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
|
|
16
|
+
const StyledActionButton = styled(Button)`
|
|
17
|
+
color: ${colors.white};
|
|
18
|
+
padding: ${spacing.xsmall} ${spacing.small};
|
|
19
|
+
box-shadow: none;
|
|
20
|
+
&:focus,
|
|
21
|
+
&:hover {
|
|
22
|
+
color: ${colors.brand.greyLightest};
|
|
23
|
+
background: ${colors.brand.greyDark};
|
|
24
|
+
&:after {
|
|
25
|
+
opacity: 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
&:after {
|
|
29
|
+
content: '';
|
|
30
|
+
display: flex;
|
|
31
|
+
height: 1px;
|
|
32
|
+
width: 100%;
|
|
33
|
+
background: ${colors.white};
|
|
34
|
+
transform: translateY(-2px);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const StyledIconButton = styled(IconButton)`
|
|
39
|
+
svg {
|
|
40
|
+
color: ${colors.brand.greyMedium};
|
|
41
|
+
}
|
|
42
|
+
&:hover,
|
|
43
|
+
&:focus {
|
|
44
|
+
background: ${colors.brand.greyDark};
|
|
45
|
+
svg {
|
|
46
|
+
color: ${colors.brand.greyLightest};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const WrapperForButtons = styled.div`
|
|
52
|
+
display: flex;
|
|
53
|
+
${mq.range({ from: breakpoints.tablet })} {
|
|
54
|
+
gap: ${spacing.xxsmall};
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
interface StyledProps {
|
|
59
|
+
expired?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Wrapper = styled.div`
|
|
63
|
+
position: fixed;
|
|
64
|
+
z-index: 99999;
|
|
65
|
+
bottom: ${spacing.small};
|
|
66
|
+
left: ${spacing.small};
|
|
67
|
+
right: ${spacing.small};
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const StyledNotification = styled.div<StyledProps>`
|
|
73
|
+
max-width: 960px;
|
|
74
|
+
${fonts.sizes(18, 1.25)};
|
|
75
|
+
background: ${colors.text.primary};
|
|
76
|
+
color: ${colors.white};
|
|
77
|
+
box-shadow: ${shadows.levitate1};
|
|
78
|
+
padding: ${spacing.small};
|
|
79
|
+
padding-right: ${spacing.xsmall};
|
|
80
|
+
gap: ${spacing.medium};
|
|
81
|
+
${mq.range({ from: breakpoints.tablet })} {
|
|
82
|
+
gap: ${spacing.large};
|
|
83
|
+
padding: ${spacing.small} ${spacing.normal} ${spacing.small} ${spacing.medium};
|
|
84
|
+
}
|
|
85
|
+
${mq.range({ from: breakpoints.desktop })} {
|
|
86
|
+
gap: ${spacingUnit * 3};
|
|
87
|
+
}
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
> div:first-of-type {
|
|
91
|
+
flex-grow: 1;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
}
|
|
96
|
+
&:empty {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
border-radius: ${misc.borderRadius};
|
|
100
|
+
@keyframes snackbar-animations-in {
|
|
101
|
+
0% {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
transform: translateY(${spacing.medium});
|
|
104
|
+
}
|
|
105
|
+
100% {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
@keyframes snackbar-animations-out {
|
|
110
|
+
0% {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
100% {
|
|
114
|
+
opacity: 0;
|
|
115
|
+
transform: translateY(${spacing.medium});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
animation: ${(props) => (props.expired ? 'snackbar-animations-out' : 'snackbar-animations-in')} 200ms ease-in-out;
|
|
119
|
+
animation-fill-mode: forwards;
|
|
120
|
+
${fonts.sizes('18px')};
|
|
121
|
+
font-family: ${fonts.sans};
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
export interface SnackBarItem {
|
|
125
|
+
children?: ReactElement;
|
|
126
|
+
snackbarItemId?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface SnackBarProps extends SnackBarItem {
|
|
130
|
+
id: string;
|
|
131
|
+
onKill?: (id: string | undefined) => void;
|
|
132
|
+
actionButtons?: {
|
|
133
|
+
text: string;
|
|
134
|
+
onClick: () => void;
|
|
135
|
+
ariaLabel: string;
|
|
136
|
+
}[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const SnackBar = ({ onKill, children, snackbarItemId, id, actionButtons }: SnackBarProps) => {
|
|
140
|
+
const { t } = useTranslation();
|
|
141
|
+
const [expired, setExpired] = useState(false);
|
|
142
|
+
const timeoutId = useRef<null | ReturnType<typeof setTimeout>>();
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (timeoutId.current) {
|
|
145
|
+
timeoutId && clearTimeout(timeoutId.current);
|
|
146
|
+
}
|
|
147
|
+
timeoutId.current = setTimeout(() => {
|
|
148
|
+
setExpired(true);
|
|
149
|
+
}, 8000);
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
timeoutId.current && clearTimeout(timeoutId.current);
|
|
153
|
+
};
|
|
154
|
+
}, [snackbarItemId, timeoutId]);
|
|
155
|
+
return (
|
|
156
|
+
<Wrapper>
|
|
157
|
+
<StyledNotification
|
|
158
|
+
id={id}
|
|
159
|
+
aria-live="polite"
|
|
160
|
+
expired={expired || !children}
|
|
161
|
+
onAnimationEnd={() => expired && onKill && onKill(snackbarItemId)}>
|
|
162
|
+
{children && (
|
|
163
|
+
<>
|
|
164
|
+
<div>{children}</div>
|
|
165
|
+
<WrapperForButtons>
|
|
166
|
+
{actionButtons &&
|
|
167
|
+
actionButtons.map(({ onClick, text, ariaLabel }) => (
|
|
168
|
+
<StyledActionButton key={text} link aria-label={ariaLabel} onClick={onClick}>
|
|
169
|
+
{text}
|
|
170
|
+
</StyledActionButton>
|
|
171
|
+
))}
|
|
172
|
+
<StyledIconButton aria-label={t('snackbar.close')} size="xsmall" outline onClick={() => setExpired(true)}>
|
|
173
|
+
<Cross />
|
|
174
|
+
</StyledIconButton>
|
|
175
|
+
</WrapperForButtons>
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
</StyledNotification>
|
|
179
|
+
</Wrapper>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default SnackBar;
|
|
@@ -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 SnackBar from './SnackBar';
|
|
10
|
+
|
|
11
|
+
export type { SnackBarItem } from './SnackBar';
|
|
12
|
+
|
|
13
|
+
export { SnackBar };
|
|
@@ -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;
|