@ndla/ui 22.0.1 → 22.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.
- package/es/Article/ArticleByline.js +7 -4
- package/es/Article/ArticleNotions.js +10 -6
- package/es/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
- package/es/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
- package/es/CompetenceGoalTab/CompetenceItem.js +14 -12
- package/es/CompetenceGoalTab/SearchButton.js +7 -4
- package/es/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
- package/es/ContentLoader/index.js +8 -3
- package/es/Filter/FilterButtons.js +10 -9
- package/es/Footer/FooterPrivacy.js +3 -2
- package/es/Masthead/MastheadSearchModal.js +4 -3
- package/es/Resource/BlockResource.js +109 -18
- package/es/Resource/ListResource.js +126 -26
- package/es/Resource/resourceComponents.js +36 -25
- package/es/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
- package/es/SearchTypeResult/PopupFilter.js +12 -8
- package/es/SearchTypeResult/components/ItemContexts.js +8 -7
- package/es/SnackBar/DefaultSnackbar.js +56 -0
- package/es/SnackBar/SnackbarProvider.js +179 -0
- package/es/SnackBar/index.js +2 -2
- package/es/Topic/Topic.js +21 -20
- package/es/User/AuthModal.js +9 -8
- package/es/index.js +1 -1
- package/es/locale/messages-en.js +21 -6
- package/es/locale/messages-nb.js +21 -6
- package/es/locale/messages-nn.js +21 -6
- package/es/locale/messages-se.js +21 -6
- package/es/locale/messages-sma.js +21 -6
- package/lib/Article/ArticleByline.js +7 -4
- package/lib/Article/ArticleNotions.js +10 -6
- package/lib/CompetenceGoalTab/CompetenceGoalItem.d.ts +1 -1
- package/lib/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
- package/lib/CompetenceGoalTab/CompetenceGoalTab.d.ts +2 -1
- package/lib/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
- package/lib/CompetenceGoalTab/CompetenceItem.d.ts +2 -1
- package/lib/CompetenceGoalTab/CompetenceItem.js +14 -12
- package/lib/CompetenceGoalTab/SearchButton.d.ts +2 -1
- package/lib/CompetenceGoalTab/SearchButton.js +7 -4
- package/lib/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
- package/lib/ContentLoader/index.d.ts +4 -8
- package/lib/ContentLoader/index.js +8 -3
- package/lib/Filter/FilterButtons.js +10 -9
- package/lib/Footer/FooterPrivacy.js +3 -2
- package/lib/Masthead/MastheadSearchModal.js +4 -3
- package/lib/Resource/BlockResource.d.ts +3 -1
- package/lib/Resource/BlockResource.js +110 -18
- package/lib/Resource/ListResource.d.ts +3 -1
- package/lib/Resource/ListResource.js +127 -26
- package/lib/Resource/resourceComponents.d.ts +4 -2
- package/lib/Resource/resourceComponents.js +38 -19
- package/lib/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
- package/lib/SearchTypeResult/PopupFilter.js +12 -8
- package/lib/SearchTypeResult/components/ItemContexts.js +8 -7
- package/lib/SnackBar/DefaultSnackbar.d.ts +11 -0
- package/lib/SnackBar/DefaultSnackbar.js +70 -0
- package/lib/SnackBar/SnackbarProvider.d.ts +32 -0
- package/lib/SnackBar/SnackbarProvider.js +197 -0
- package/lib/SnackBar/index.d.ts +3 -3
- package/lib/SnackBar/index.js +23 -3
- package/lib/Topic/Topic.js +21 -20
- package/lib/User/AuthModal.js +9 -8
- package/lib/index.d.ts +2 -2
- package/lib/index.js +24 -3
- package/lib/locale/messages-en.d.ts +15 -0
- package/lib/locale/messages-en.js +21 -6
- package/lib/locale/messages-nb.d.ts +15 -0
- package/lib/locale/messages-nb.js +21 -6
- package/lib/locale/messages-nn.d.ts +15 -0
- package/lib/locale/messages-nn.js +21 -6
- package/lib/locale/messages-se.d.ts +15 -0
- package/lib/locale/messages-se.js +21 -6
- package/lib/locale/messages-sma.d.ts +15 -0
- package/lib/locale/messages-sma.js +21 -6
- package/lib/types.d.ts +1 -0
- package/package.json +8 -7
- package/src/Article/ArticleByline.tsx +4 -1
- package/src/Article/ArticleNotions.tsx +4 -1
- package/src/CompetenceGoalTab/CompetenceGoalItem.tsx +6 -2
- package/src/CompetenceGoalTab/CompetenceGoalTab.tsx +5 -4
- package/src/CompetenceGoalTab/CompetenceItem.tsx +9 -2
- package/src/CompetenceGoalTab/SearchButton.tsx +3 -2
- package/src/CompetenceGoals/CompetenceGoalsDialog.jsx +5 -2
- package/src/ContentLoader/index.tsx +9 -9
- package/src/Filter/FilterButtons.tsx +1 -0
- package/src/Footer/FooterPrivacy.tsx +4 -1
- package/src/Masthead/MastheadSearchModal.tsx +1 -0
- package/src/Resource/BlockResource.tsx +69 -6
- package/src/Resource/ListResource.tsx +88 -11
- package/src/Resource/resourceComponents.tsx +25 -9
- package/src/ResourcesWrapper/ResourcesTopicTitle.tsx +5 -1
- package/src/SearchTypeResult/PopupFilter.tsx +3 -1
- package/src/SearchTypeResult/components/ItemContexts.tsx +1 -0
- package/src/SnackBar/DefaultSnackbar.tsx +70 -0
- package/src/SnackBar/SnackbarProvider.tsx +147 -0
- package/src/SnackBar/index.ts +3 -5
- package/src/Topic/Topic.tsx +1 -0
- package/src/User/AuthModal.tsx +2 -1
- package/src/index.ts +2 -2
- package/src/locale/messages-en.ts +15 -0
- package/src/locale/messages-nb.ts +15 -0
- package/src/locale/messages-nn.ts +15 -0
- package/src/locale/messages-se.ts +15 -0
- package/src/locale/messages-sma.ts +15 -0
- package/src/types.ts +1 -0
- package/es/SnackBar/SnackBar.js +0 -117
- package/lib/SnackBar/SnackBar.d.ts +0 -23
- package/lib/SnackBar/SnackBar.js +0 -127
- package/src/.DS_Store +0 -0
- package/src/SnackBar/SnackBar.tsx +0 -183
|
@@ -69,7 +69,10 @@ const StyledFooterText = styled.div`
|
|
|
69
69
|
|
|
70
70
|
const FooterPrivacy = ({ lang, label }: FooterPrivacyProps) => (
|
|
71
71
|
<StyledFooterText>
|
|
72
|
-
<Modal
|
|
72
|
+
<Modal
|
|
73
|
+
label={label}
|
|
74
|
+
activateButton={<StyledPrivacyButton type="button">{label}</StyledPrivacyButton>}
|
|
75
|
+
size="fullscreen">
|
|
73
76
|
{(onClose: () => void) => (
|
|
74
77
|
<OneColumn cssModifier="medium">
|
|
75
78
|
<ModalHeader>
|
|
@@ -13,15 +13,18 @@ import { colors, fonts, spacing } from '@ndla/core';
|
|
|
13
13
|
import { MenuButton, MenuItemProps } from '@ndla/button';
|
|
14
14
|
import Image from '../Image';
|
|
15
15
|
import { CompressedTagList, ResourceImageProps, ResourceTitle, Row, TopicList } from './resourceComponents';
|
|
16
|
+
import ContentLoader from '../ContentLoader';
|
|
16
17
|
|
|
17
18
|
interface BlockResourceProps {
|
|
18
19
|
link: string;
|
|
20
|
+
tagLinkPrefix?: string;
|
|
19
21
|
title: string;
|
|
20
22
|
resourceImage: ResourceImageProps;
|
|
21
23
|
topics: string[];
|
|
22
24
|
tags?: string[];
|
|
23
25
|
description?: string;
|
|
24
26
|
menuItems?: MenuItemProps[];
|
|
27
|
+
isLoading?: boolean;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const BlockElementWrapper = styled(SafeLink)`
|
|
@@ -45,7 +48,7 @@ const BlockDescription = styled.p`
|
|
|
45
48
|
overflow: hidden;
|
|
46
49
|
text-overflow: ellipsis;
|
|
47
50
|
transition: height 0.2s ease-out;
|
|
48
|
-
${() => BlockElementWrapper}:hover & {
|
|
51
|
+
${() => BlockElementWrapper}:hover &, ${() => BlockElementWrapper}:focus & {
|
|
49
52
|
// Unfortunate css needed for multi-line text overflow ellipsis.
|
|
50
53
|
height: 3.1em;
|
|
51
54
|
-webkit-line-clamp: 2;
|
|
@@ -78,20 +81,80 @@ const ImageWrapper = styled.div`
|
|
|
78
81
|
}
|
|
79
82
|
`;
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
interface BlockImageProps {
|
|
85
|
+
image: ResourceImageProps;
|
|
86
|
+
loading?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const BlockImage = ({ image, loading }: BlockImageProps) => {
|
|
90
|
+
if (loading) {
|
|
91
|
+
return (
|
|
92
|
+
<ContentLoader height={'100%'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
93
|
+
<rect x="0" y="0" rx="3" ry="3" width="100%" height="100%" />
|
|
94
|
+
</ContentLoader>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return <Image alt={image.alt} src={image.src} />;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
interface BlockTitleProps {
|
|
101
|
+
title: string;
|
|
102
|
+
loading?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const BlockTitle = ({ title, loading }: BlockTitleProps) => {
|
|
106
|
+
if (loading) {
|
|
107
|
+
return (
|
|
108
|
+
<ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
109
|
+
<rect x="0" y="0" rx="3" ry="3" width="100%" height="18px" />
|
|
110
|
+
</ContentLoader>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return <ResourceTitle>{title}</ResourceTitle>;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface BlockTopicListProps {
|
|
117
|
+
topics: string[];
|
|
118
|
+
loading?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const BlockTopicList = ({ topics, loading }: BlockTopicListProps) => {
|
|
122
|
+
if (loading) {
|
|
123
|
+
return (
|
|
124
|
+
<ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
125
|
+
<rect x="0" y="0" rx="3" ry="3" width="20%" height="18px" />
|
|
126
|
+
<rect x="25%" y="0" rx="3" ry="3" width="20%" height="18px" />
|
|
127
|
+
</ContentLoader>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return <TopicList topics={topics} />;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const BlockResource = ({
|
|
135
|
+
link,
|
|
136
|
+
tagLinkPrefix,
|
|
137
|
+
title,
|
|
138
|
+
tags,
|
|
139
|
+
resourceImage,
|
|
140
|
+
topics,
|
|
141
|
+
description,
|
|
142
|
+
menuItems,
|
|
143
|
+
isLoading,
|
|
144
|
+
}: BlockResourceProps) => {
|
|
82
145
|
return (
|
|
83
146
|
<BlockElementWrapper to={link}>
|
|
84
147
|
<ImageWrapper>
|
|
85
|
-
<
|
|
148
|
+
<BlockImage image={resourceImage} loading={isLoading} />
|
|
86
149
|
</ImageWrapper>
|
|
87
150
|
<BlockInfoWrapper>
|
|
88
151
|
<div>
|
|
89
|
-
<
|
|
152
|
+
<BlockTitle title={title} loading={isLoading} />
|
|
90
153
|
</div>
|
|
91
|
-
<
|
|
154
|
+
<BlockTopicList topics={topics} loading={isLoading} />
|
|
92
155
|
<BlockDescription>{description}</BlockDescription>
|
|
93
156
|
<RightRow>
|
|
94
|
-
{tags && <CompressedTagList tags={tags} />}
|
|
157
|
+
{tags && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
|
|
95
158
|
{menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
|
|
96
159
|
</RightRow>
|
|
97
160
|
</BlockInfoWrapper>
|
|
@@ -13,8 +13,9 @@ import { fonts, spacing, colors, breakpoints, mq } from '@ndla/core';
|
|
|
13
13
|
import { MenuButton, MenuItemProps } from '@ndla/button';
|
|
14
14
|
import Image from '../Image';
|
|
15
15
|
import { CompressedTagList, ResourceImageProps, ResourceTitle, TopicList } from './resourceComponents';
|
|
16
|
+
import ContentLoader from '../ContentLoader';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const StyledResourceDescription = styled.p`
|
|
18
19
|
grid-area: description;
|
|
19
20
|
line-clamp: 2;
|
|
20
21
|
line-height: 1em;
|
|
@@ -97,7 +98,7 @@ const StyledImage = styled(Image)`
|
|
|
97
98
|
object-fit: cover;
|
|
98
99
|
`;
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
+
const TopicAndTitleWrapper = styled.div`
|
|
101
102
|
grid-area: topicAndTitle;
|
|
102
103
|
margin-top: ${spacing.xxsmall};
|
|
103
104
|
`;
|
|
@@ -108,29 +109,105 @@ interface StyledImageProps {
|
|
|
108
109
|
|
|
109
110
|
export interface ListResourceProps {
|
|
110
111
|
link: string;
|
|
112
|
+
tagLinkPrefix?: string;
|
|
111
113
|
title: string;
|
|
112
114
|
resourceImage: ResourceImageProps;
|
|
113
115
|
topics: string[];
|
|
114
116
|
tags?: string[];
|
|
115
117
|
description?: string;
|
|
116
118
|
menuItems?: MenuItemProps[];
|
|
119
|
+
isLoading?: boolean;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
interface ListResourceImageProps {
|
|
123
|
+
resourceImage: ResourceImageProps;
|
|
124
|
+
loading?: boolean;
|
|
125
|
+
type: 'normal' | 'compact';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ListResourceImage = ({ resourceImage, loading, type }: ListResourceImageProps) => {
|
|
129
|
+
if (!loading) {
|
|
130
|
+
return <StyledImage alt={resourceImage.alt} src={resourceImage.src} />;
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<ContentLoader height={'100%'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
134
|
+
<rect
|
|
135
|
+
x="0"
|
|
136
|
+
y="0"
|
|
137
|
+
rx="3"
|
|
138
|
+
ry="3"
|
|
139
|
+
width={type === 'compact' ? '56' : '136'}
|
|
140
|
+
height={type === 'compact' ? '40' : '96'}
|
|
141
|
+
/>
|
|
142
|
+
</ContentLoader>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
interface TopicAndTitleProps {
|
|
147
|
+
title: string;
|
|
148
|
+
topics: string[];
|
|
149
|
+
loading?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
|
|
153
|
+
if (loading) {
|
|
154
|
+
return (
|
|
155
|
+
<ContentLoader height={'40px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
156
|
+
<rect x="0" y="0" rx="3" ry="3" width={'100%'} height={'16'} />
|
|
157
|
+
<rect x="0" y="18" rx="3" ry="3" width={'70'} height={'16'} />
|
|
158
|
+
<rect x="80" y="18" rx="3" ry="3" width={'70'} height={'16'} />
|
|
159
|
+
</ContentLoader>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
<ResourceTitle>{title}</ResourceTitle>
|
|
165
|
+
<TopicList topics={topics} />
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
interface ResourceDescriptionProps {
|
|
171
|
+
description?: string;
|
|
172
|
+
loading?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ResourceDescription = ({ description, loading }: ResourceDescriptionProps) => {
|
|
176
|
+
if (loading) {
|
|
177
|
+
return (
|
|
178
|
+
<ContentLoader height={'20px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
179
|
+
<rect x="0" y="0" width="100%" height="20" />
|
|
180
|
+
</ContentLoader>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return <StyledResourceDescription>{description}</StyledResourceDescription>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const ListResource = ({
|
|
187
|
+
link,
|
|
188
|
+
tagLinkPrefix,
|
|
189
|
+
title,
|
|
190
|
+
tags,
|
|
191
|
+
resourceImage,
|
|
192
|
+
topics,
|
|
193
|
+
description,
|
|
194
|
+
menuItems,
|
|
195
|
+
isLoading = false,
|
|
196
|
+
}: ListResourceProps) => {
|
|
120
197
|
const showDescription = description !== undefined;
|
|
198
|
+
const imageType = showDescription ? 'normal' : 'compact';
|
|
121
199
|
|
|
122
200
|
return (
|
|
123
201
|
<ResourceWrapper to={link}>
|
|
124
|
-
<StyledImageWrapper imageSize={
|
|
125
|
-
<
|
|
202
|
+
<StyledImageWrapper imageSize={imageType}>
|
|
203
|
+
<ListResourceImage resourceImage={resourceImage} loading={isLoading} type={imageType} />
|
|
126
204
|
</StyledImageWrapper>
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{showDescription && <ResourceDescription>{description}</ResourceDescription>}
|
|
205
|
+
<TopicAndTitleWrapper>
|
|
206
|
+
<TopicAndTitle topics={topics} title={title} loading={isLoading} />
|
|
207
|
+
</TopicAndTitleWrapper>
|
|
208
|
+
{showDescription && <ResourceDescription description={description} loading={isLoading} />}
|
|
132
209
|
<TagsandActionMenu>
|
|
133
|
-
{tags && <CompressedTagList tags={tags} />}
|
|
210
|
+
{tags && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
|
|
134
211
|
{menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
|
|
135
212
|
</TagsandActionMenu>
|
|
136
213
|
</ResourceWrapper>
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React from 'react';
|
|
10
9
|
import styled from '@emotion/styled';
|
|
11
|
-
import {
|
|
10
|
+
import { colors, fonts, spacing } from '@ndla/core';
|
|
11
|
+
import React, { MouseEvent } from 'react';
|
|
12
12
|
|
|
13
13
|
import { MenuButton } from '@ndla/button';
|
|
14
|
+
import SafeLink from '@ndla/safelink';
|
|
15
|
+
import { useNavigate } from 'react-router-dom';
|
|
14
16
|
|
|
15
17
|
export interface ResourceImageProps {
|
|
16
18
|
alt: string;
|
|
@@ -42,9 +44,13 @@ const StyledTagList = styled.ul`
|
|
|
42
44
|
`;
|
|
43
45
|
|
|
44
46
|
const StyledTagListElement = styled.li`
|
|
45
|
-
color: ${colors.brand.grey};
|
|
46
47
|
margin: 0;
|
|
47
48
|
${fonts.sizes(14)};
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const StyledSafeLink = styled(SafeLink)`
|
|
52
|
+
box-shadow: none;
|
|
53
|
+
color: ${colors.brand.grey};
|
|
48
54
|
::before {
|
|
49
55
|
content: '#';
|
|
50
56
|
}
|
|
@@ -90,14 +96,20 @@ const TagCounterWrapper = styled.p`
|
|
|
90
96
|
|
|
91
97
|
interface TagListProps {
|
|
92
98
|
tags?: string[];
|
|
99
|
+
tagLinkPrefix?: string;
|
|
93
100
|
}
|
|
94
|
-
|
|
95
|
-
export const TagList = ({ tags }: TagListProps) => {
|
|
101
|
+
export const TagList = ({ tags, tagLinkPrefix }: TagListProps) => {
|
|
96
102
|
if (!tags) return null;
|
|
97
103
|
return (
|
|
98
104
|
<StyledTagList>
|
|
99
105
|
{tags.map((tag, i) => (
|
|
100
|
-
<StyledTagListElement key={`tag-${i}`}>
|
|
106
|
+
<StyledTagListElement key={`tag-${i}`}>
|
|
107
|
+
<StyledSafeLink
|
|
108
|
+
onClick={(e: MouseEvent<HTMLAnchorElement | HTMLElement>) => e.stopPropagation()}
|
|
109
|
+
to={`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`}>
|
|
110
|
+
{tag}
|
|
111
|
+
</StyledSafeLink>
|
|
112
|
+
</StyledTagListElement>
|
|
101
113
|
))}
|
|
102
114
|
</StyledTagList>
|
|
103
115
|
);
|
|
@@ -105,20 +117,24 @@ export const TagList = ({ tags }: TagListProps) => {
|
|
|
105
117
|
|
|
106
118
|
interface CompressedTagListProps {
|
|
107
119
|
tags: string[];
|
|
120
|
+
tagLinkPrefix?: string;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
export const CompressedTagList = ({ tags }: CompressedTagListProps) => {
|
|
123
|
+
export const CompressedTagList = ({ tags, tagLinkPrefix }: CompressedTagListProps) => {
|
|
124
|
+
const navigate = useNavigate();
|
|
111
125
|
const visibleTags = tags.slice(0, 3);
|
|
112
126
|
const remainingTags = tags.slice(3, tags.length).map((tag) => {
|
|
113
127
|
return {
|
|
114
128
|
text: '#' + tag,
|
|
115
|
-
onClick: () => {
|
|
129
|
+
onClick: () => {
|
|
130
|
+
navigate(`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`);
|
|
131
|
+
},
|
|
116
132
|
};
|
|
117
133
|
});
|
|
118
134
|
|
|
119
135
|
return (
|
|
120
136
|
<>
|
|
121
|
-
<TagList tags={visibleTags} />
|
|
137
|
+
<TagList tagLinkPrefix={tagLinkPrefix} tags={visibleTags} />
|
|
122
138
|
{remainingTags.length > 0 && (
|
|
123
139
|
<MenuButton hideMenuIcon={true} menuItems={remainingTags}>
|
|
124
140
|
<TagCounterWrapper>{`+${remainingTags.length}`}</TagCounterWrapper>
|
|
@@ -66,6 +66,9 @@ const ResourcesTopicTitle = ({
|
|
|
66
66
|
} else {
|
|
67
67
|
heading = <h1 {...classes('topic-title', 'single')}>{messages.label}</h1>;
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
const tooltipId = 'popupDialogTooltip';
|
|
71
|
+
|
|
69
72
|
return (
|
|
70
73
|
<header {...classes('topic-title-wrapper', { invertedStyle })}>
|
|
71
74
|
<div>
|
|
@@ -82,6 +85,7 @@ const ResourcesTopicTitle = ({
|
|
|
82
85
|
css={invertedStyle ? invertedSwitchCSS : switchCSS}
|
|
83
86
|
/>
|
|
84
87
|
<Modal
|
|
88
|
+
labelledBy={tooltipId}
|
|
85
89
|
narrow
|
|
86
90
|
wrapperFunctionForButton={(activateButton: ReactNode) => (
|
|
87
91
|
<TooltipWrapper>
|
|
@@ -89,7 +93,7 @@ const ResourcesTopicTitle = ({
|
|
|
89
93
|
</TooltipWrapper>
|
|
90
94
|
)}
|
|
91
95
|
activateButton={
|
|
92
|
-
<TooltipButton aria-label={t('resource.dialogTooltip')}>
|
|
96
|
+
<TooltipButton id={tooltipId} aria-label={t('resource.dialogTooltip')}>
|
|
93
97
|
<HelpIcon invertedStyle={invertedStyle} />
|
|
94
98
|
</TooltipButton>
|
|
95
99
|
}>
|
|
@@ -92,9 +92,11 @@ const PopupFilter = ({
|
|
|
92
92
|
}: PopupFilterProps) => {
|
|
93
93
|
const { t } = useTranslation();
|
|
94
94
|
const [selectedMenu, setSelectedMenu] = useState(MENU_ALL_SUBJECTS);
|
|
95
|
+
const headingId = 'popupFilterHeading';
|
|
95
96
|
|
|
96
97
|
return (
|
|
97
98
|
<Modal
|
|
99
|
+
labelledBy={headingId}
|
|
98
100
|
controllable
|
|
99
101
|
backgroundColor="white"
|
|
100
102
|
animation="subtle"
|
|
@@ -107,7 +109,7 @@ const PopupFilter = ({
|
|
|
107
109
|
<ModalWrapper>
|
|
108
110
|
<ModalContent>
|
|
109
111
|
<ModalHeaderWrapper>
|
|
110
|
-
<ModalHeading>{t('searchPage.searchFilterMessages.filterLabel')}</ModalHeading>
|
|
112
|
+
<ModalHeading id={headingId}>{t('searchPage.searchFilterMessages.filterLabel')}</ModalHeading>
|
|
111
113
|
<ModalCloseButton onClick={() => onClose()} title={t('searchPage.close')} />
|
|
112
114
|
</ModalHeaderWrapper>
|
|
113
115
|
{subjectCategories && programmes && (
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { useTranslation } from 'react-i18next';
|
|
11
|
+
import styled from '@emotion/styled';
|
|
12
|
+
import { IconButton } from '@ndla/button';
|
|
13
|
+
import { breakpoints, colors, fonts, misc, mq, shadows, spacing } from '@ndla/core';
|
|
14
|
+
import { Cross } from '@ndla/icons/action';
|
|
15
|
+
import { Snack, useSnack } from './SnackbarProvider';
|
|
16
|
+
|
|
17
|
+
const DefaultSnackContainer = styled.div`
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: ${spacing.small};
|
|
20
|
+
padding: ${spacing.small};
|
|
21
|
+
background-color: ${colors.text.primary};
|
|
22
|
+
color: ${colors.white};
|
|
23
|
+
border-radius: ${misc.borderRadius};
|
|
24
|
+
margin: 0 ${spacing.large};
|
|
25
|
+
pointer-events: all;
|
|
26
|
+
box-shadow: ${shadows.levitate1};
|
|
27
|
+
font-family: ${fonts.sans};
|
|
28
|
+
font-size: 18px;
|
|
29
|
+
align-items: center;
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const StyledCloseButton = styled(IconButton)`
|
|
33
|
+
svg {
|
|
34
|
+
color: ${colors.brand.greyMedium};
|
|
35
|
+
}
|
|
36
|
+
&:hover,
|
|
37
|
+
&:focus {
|
|
38
|
+
background: ${colors.brand.greyDark};
|
|
39
|
+
svg {
|
|
40
|
+
color: ${colors.brand.greyLightest};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const ButtonWrapper = styled.div`
|
|
46
|
+
${mq.range({ from: breakpoints.tablet })} {
|
|
47
|
+
gap: ${spacing.xxsmall};
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const DefaultSnack = (snack: Snack) => {
|
|
52
|
+
const { closable = true, icon } = snack;
|
|
53
|
+
const { t } = useTranslation();
|
|
54
|
+
const { closeSnack } = useSnack();
|
|
55
|
+
return (
|
|
56
|
+
<DefaultSnackContainer>
|
|
57
|
+
{icon}
|
|
58
|
+
{snack.content}
|
|
59
|
+
<ButtonWrapper>
|
|
60
|
+
{closable && (
|
|
61
|
+
<StyledCloseButton size="xsmall" outline onClick={() => closeSnack(snack)} aria-label={t('snackbar.close')}>
|
|
62
|
+
<Cross />
|
|
63
|
+
</StyledCloseButton>
|
|
64
|
+
)}
|
|
65
|
+
</ButtonWrapper>
|
|
66
|
+
</DefaultSnackContainer>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default DefaultSnack;
|
|
@@ -0,0 +1,147 @@
|
|
|
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, { useCallback, useMemo, createContext, ReactNode, useState, useContext, useEffect } from 'react';
|
|
10
|
+
import { keyframes } from '@emotion/core';
|
|
11
|
+
import styled from '@emotion/styled';
|
|
12
|
+
import { spacing } from '@ndla/core';
|
|
13
|
+
import DefaultSnack from './DefaultSnackbar';
|
|
14
|
+
|
|
15
|
+
export interface Snack {
|
|
16
|
+
content?: ReactNode;
|
|
17
|
+
duration?: number;
|
|
18
|
+
render?: (id: string, onClose?: () => void) => ReactNode;
|
|
19
|
+
id: string;
|
|
20
|
+
icon?: ReactNode;
|
|
21
|
+
closable?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SnackContext {
|
|
25
|
+
addSnack: (snack: Snack) => void;
|
|
26
|
+
removeSnack: (id: string) => void;
|
|
27
|
+
clearSnacks: () => void;
|
|
28
|
+
closeSnack: (snack: Snack) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SnackbarContext = createContext<SnackContext | undefined>(undefined);
|
|
32
|
+
|
|
33
|
+
export const useSnack = () => {
|
|
34
|
+
const context = useContext(SnackbarContext);
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error('useSnack can only be used within a SnackbarProvider!');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return context;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const SnackbarProvider = ({ children }: Props) => {
|
|
47
|
+
const [snacks, setSnacks] = useState<Snack[]>([]);
|
|
48
|
+
|
|
49
|
+
const addSnack = useCallback((snack: Snack) => {
|
|
50
|
+
setSnacks((prev) => prev.filter((s) => s.id !== snack.id).concat(snack));
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const removeSnack = useCallback((snackId: string) => {
|
|
54
|
+
setSnacks((prev) => prev.filter(({ id }) => snackId !== id));
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const closeSnack = useCallback((snack: Snack) => {
|
|
58
|
+
setSnacks((prev) => prev.map((p) => (p.id === snack.id ? { ...p, duration: 0 } : p)));
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const clearSnacks = useCallback(() => setSnacks([]), []);
|
|
62
|
+
|
|
63
|
+
const value = useMemo(
|
|
64
|
+
() => ({ addSnack, removeSnack, clearSnacks, closeSnack }),
|
|
65
|
+
[addSnack, removeSnack, clearSnacks, closeSnack],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<SnackbarContext.Provider value={value}>
|
|
70
|
+
{children}
|
|
71
|
+
<SnackbarContainer snacks={snacks} />
|
|
72
|
+
</SnackbarContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
interface BaseSnackProps extends Snack {
|
|
77
|
+
children: ReactNode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface BaseSnackContainerProps {
|
|
81
|
+
expired: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const snackbarInAnimation = keyframes({
|
|
85
|
+
'0%': { transform: `translateY(${spacing.medium})`, opacity: 0 },
|
|
86
|
+
'100%': { opacity: 1 },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const snackbarOutAnimation = keyframes({
|
|
90
|
+
'0%': { opacity: 1 },
|
|
91
|
+
'100%': { transform: `translateY(${spacing.medium})`, opacity: 0 },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const BaseSnackContainer = styled.li<BaseSnackContainerProps>`
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
align-items: center;
|
|
98
|
+
animation: ${(p) => (p.expired ? snackbarOutAnimation : snackbarInAnimation)} 200ms ease-in-out;
|
|
99
|
+
animation-fill-mode: forwards;
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
export const BaseSnack = ({ duration = 5000, id, children }: BaseSnackProps) => {
|
|
103
|
+
const { removeSnack } = useSnack();
|
|
104
|
+
const [expired, setExpired] = useState(false);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const timeout = setTimeout(() => setExpired(true), duration);
|
|
108
|
+
return () => clearTimeout(timeout);
|
|
109
|
+
}, [duration]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<BaseSnackContainer expired={expired} onAnimationEnd={() => expired && removeSnack(id)}>
|
|
113
|
+
{children}
|
|
114
|
+
</BaseSnackContainer>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
interface SnackbarContainerProps {
|
|
119
|
+
snacks: Snack[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const StyledSnackList = styled.ul`
|
|
123
|
+
position: fixed;
|
|
124
|
+
z-index: 99999;
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
margin: 0 auto;
|
|
129
|
+
padding: 0;
|
|
130
|
+
bottom: env(safe-area-inset-bottom, 40px);
|
|
131
|
+
right: env(safe-area-inset-right, 0px);
|
|
132
|
+
left: env(safe-area-inset-left, 0px);
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const SnackbarContainer = ({ snacks }: SnackbarContainerProps) => {
|
|
136
|
+
return (
|
|
137
|
+
<StyledSnackList aria-live="polite" role="region">
|
|
138
|
+
{snacks.map((snack) => (
|
|
139
|
+
<BaseSnack key={snack.id} {...snack}>
|
|
140
|
+
{snack.render?.(snack.id) ?? <DefaultSnack {...snack} />}
|
|
141
|
+
</BaseSnack>
|
|
142
|
+
))}
|
|
143
|
+
</StyledSnackList>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default SnackbarProvider;
|
package/src/SnackBar/index.ts
CHANGED
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
export { SnackBar };
|
|
9
|
+
export type { Snack, SnackContext } from './SnackbarProvider';
|
|
10
|
+
export { SnackbarProvider, useSnack, BaseSnack } from './SnackbarProvider';
|
|
11
|
+
export { default as DefaultSnackbar } from './DefaultSnackbar';
|
package/src/Topic/Topic.tsx
CHANGED
package/src/User/AuthModal.tsx
CHANGED
|
@@ -83,7 +83,8 @@ const AuthModal = ({
|
|
|
83
83
|
position={position}
|
|
84
84
|
isOpen={isOpen}
|
|
85
85
|
onClose={onClose}
|
|
86
|
-
controllable={!activateButton}
|
|
86
|
+
controllable={!activateButton}
|
|
87
|
+
label={isAuthenticated ? t('user.modal.isAuth') : t('user.modal.isNotAuth')}>
|
|
87
88
|
{(onClose: () => void) => (
|
|
88
89
|
<StyledModalBody>
|
|
89
90
|
<StyledModalHeader>
|
package/src/index.ts
CHANGED
|
@@ -240,8 +240,8 @@ export type { ListResourceProps } from './Resource';
|
|
|
240
240
|
export type { TagType } from './TagSelector';
|
|
241
241
|
export { TagSelector } from './TagSelector';
|
|
242
242
|
|
|
243
|
-
export
|
|
244
|
-
export {
|
|
243
|
+
export { SnackbarProvider, useSnack, BaseSnack, DefaultSnackbar } from './SnackBar';
|
|
244
|
+
export type { Snack, SnackContext } from './SnackBar';
|
|
245
245
|
export { InfoBlock } from './InfoBlock';
|
|
246
246
|
export { TreeStructure } from './TreeStructure';
|
|
247
247
|
export type { FolderType, TreeStructureProps, TreeStructureMenuProps } from './TreeStructure';
|