@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.
Files changed (109) hide show
  1. package/es/Article/ArticleByline.js +7 -4
  2. package/es/Article/ArticleNotions.js +10 -6
  3. package/es/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
  4. package/es/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
  5. package/es/CompetenceGoalTab/CompetenceItem.js +14 -12
  6. package/es/CompetenceGoalTab/SearchButton.js +7 -4
  7. package/es/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
  8. package/es/ContentLoader/index.js +8 -3
  9. package/es/Filter/FilterButtons.js +10 -9
  10. package/es/Footer/FooterPrivacy.js +3 -2
  11. package/es/Masthead/MastheadSearchModal.js +4 -3
  12. package/es/Resource/BlockResource.js +109 -18
  13. package/es/Resource/ListResource.js +126 -26
  14. package/es/Resource/resourceComponents.js +36 -25
  15. package/es/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
  16. package/es/SearchTypeResult/PopupFilter.js +12 -8
  17. package/es/SearchTypeResult/components/ItemContexts.js +8 -7
  18. package/es/SnackBar/DefaultSnackbar.js +56 -0
  19. package/es/SnackBar/SnackbarProvider.js +179 -0
  20. package/es/SnackBar/index.js +2 -2
  21. package/es/Topic/Topic.js +21 -20
  22. package/es/User/AuthModal.js +9 -8
  23. package/es/index.js +1 -1
  24. package/es/locale/messages-en.js +21 -6
  25. package/es/locale/messages-nb.js +21 -6
  26. package/es/locale/messages-nn.js +21 -6
  27. package/es/locale/messages-se.js +21 -6
  28. package/es/locale/messages-sma.js +21 -6
  29. package/lib/Article/ArticleByline.js +7 -4
  30. package/lib/Article/ArticleNotions.js +10 -6
  31. package/lib/CompetenceGoalTab/CompetenceGoalItem.d.ts +1 -1
  32. package/lib/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
  33. package/lib/CompetenceGoalTab/CompetenceGoalTab.d.ts +2 -1
  34. package/lib/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
  35. package/lib/CompetenceGoalTab/CompetenceItem.d.ts +2 -1
  36. package/lib/CompetenceGoalTab/CompetenceItem.js +14 -12
  37. package/lib/CompetenceGoalTab/SearchButton.d.ts +2 -1
  38. package/lib/CompetenceGoalTab/SearchButton.js +7 -4
  39. package/lib/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
  40. package/lib/ContentLoader/index.d.ts +4 -8
  41. package/lib/ContentLoader/index.js +8 -3
  42. package/lib/Filter/FilterButtons.js +10 -9
  43. package/lib/Footer/FooterPrivacy.js +3 -2
  44. package/lib/Masthead/MastheadSearchModal.js +4 -3
  45. package/lib/Resource/BlockResource.d.ts +3 -1
  46. package/lib/Resource/BlockResource.js +110 -18
  47. package/lib/Resource/ListResource.d.ts +3 -1
  48. package/lib/Resource/ListResource.js +127 -26
  49. package/lib/Resource/resourceComponents.d.ts +4 -2
  50. package/lib/Resource/resourceComponents.js +38 -19
  51. package/lib/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
  52. package/lib/SearchTypeResult/PopupFilter.js +12 -8
  53. package/lib/SearchTypeResult/components/ItemContexts.js +8 -7
  54. package/lib/SnackBar/DefaultSnackbar.d.ts +11 -0
  55. package/lib/SnackBar/DefaultSnackbar.js +70 -0
  56. package/lib/SnackBar/SnackbarProvider.d.ts +32 -0
  57. package/lib/SnackBar/SnackbarProvider.js +197 -0
  58. package/lib/SnackBar/index.d.ts +3 -3
  59. package/lib/SnackBar/index.js +23 -3
  60. package/lib/Topic/Topic.js +21 -20
  61. package/lib/User/AuthModal.js +9 -8
  62. package/lib/index.d.ts +2 -2
  63. package/lib/index.js +24 -3
  64. package/lib/locale/messages-en.d.ts +15 -0
  65. package/lib/locale/messages-en.js +21 -6
  66. package/lib/locale/messages-nb.d.ts +15 -0
  67. package/lib/locale/messages-nb.js +21 -6
  68. package/lib/locale/messages-nn.d.ts +15 -0
  69. package/lib/locale/messages-nn.js +21 -6
  70. package/lib/locale/messages-se.d.ts +15 -0
  71. package/lib/locale/messages-se.js +21 -6
  72. package/lib/locale/messages-sma.d.ts +15 -0
  73. package/lib/locale/messages-sma.js +21 -6
  74. package/lib/types.d.ts +1 -0
  75. package/package.json +8 -7
  76. package/src/Article/ArticleByline.tsx +4 -1
  77. package/src/Article/ArticleNotions.tsx +4 -1
  78. package/src/CompetenceGoalTab/CompetenceGoalItem.tsx +6 -2
  79. package/src/CompetenceGoalTab/CompetenceGoalTab.tsx +5 -4
  80. package/src/CompetenceGoalTab/CompetenceItem.tsx +9 -2
  81. package/src/CompetenceGoalTab/SearchButton.tsx +3 -2
  82. package/src/CompetenceGoals/CompetenceGoalsDialog.jsx +5 -2
  83. package/src/ContentLoader/index.tsx +9 -9
  84. package/src/Filter/FilterButtons.tsx +1 -0
  85. package/src/Footer/FooterPrivacy.tsx +4 -1
  86. package/src/Masthead/MastheadSearchModal.tsx +1 -0
  87. package/src/Resource/BlockResource.tsx +69 -6
  88. package/src/Resource/ListResource.tsx +88 -11
  89. package/src/Resource/resourceComponents.tsx +25 -9
  90. package/src/ResourcesWrapper/ResourcesTopicTitle.tsx +5 -1
  91. package/src/SearchTypeResult/PopupFilter.tsx +3 -1
  92. package/src/SearchTypeResult/components/ItemContexts.tsx +1 -0
  93. package/src/SnackBar/DefaultSnackbar.tsx +70 -0
  94. package/src/SnackBar/SnackbarProvider.tsx +147 -0
  95. package/src/SnackBar/index.ts +3 -5
  96. package/src/Topic/Topic.tsx +1 -0
  97. package/src/User/AuthModal.tsx +2 -1
  98. package/src/index.ts +2 -2
  99. package/src/locale/messages-en.ts +15 -0
  100. package/src/locale/messages-nb.ts +15 -0
  101. package/src/locale/messages-nn.ts +15 -0
  102. package/src/locale/messages-se.ts +15 -0
  103. package/src/locale/messages-sma.ts +15 -0
  104. package/src/types.ts +1 -0
  105. package/es/SnackBar/SnackBar.js +0 -117
  106. package/lib/SnackBar/SnackBar.d.ts +0 -23
  107. package/lib/SnackBar/SnackBar.js +0 -127
  108. package/src/.DS_Store +0 -0
  109. 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 activateButton={<StyledPrivacyButton type="button">{label}</StyledPrivacyButton>} size="fullscreen">
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>
@@ -117,6 +117,7 @@ const MastheadSearchModal = ({
117
117
  t,
118
118
  }: Props & WithTranslation) => (
119
119
  <Modal
120
+ label={t('searchPage.searchFieldPlaceholder')}
120
121
  backgroundColor="grey"
121
122
  animation="slide-down"
122
123
  animationDuration={200}
@@ -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
- const BlockResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: BlockResourceProps) => {
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
- <Image alt={resourceImage.alt} src={resourceImage.src} />
148
+ <BlockImage image={resourceImage} loading={isLoading} />
86
149
  </ImageWrapper>
87
150
  <BlockInfoWrapper>
88
151
  <div>
89
- <ResourceTitle>{title}</ResourceTitle>
152
+ <BlockTitle title={title} loading={isLoading} />
90
153
  </div>
91
- <TopicList topics={topics} />
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 ResourceDescription = styled.p`
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 TopicAndTitle = styled.div`
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
- const ListResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: ListResourceProps) => {
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={showDescription ? 'normal' : 'compact'}>
125
- <StyledImage alt={resourceImage.alt} src={resourceImage.src} />
202
+ <StyledImageWrapper imageSize={imageType}>
203
+ <ListResourceImage resourceImage={resourceImage} loading={isLoading} type={imageType} />
126
204
  </StyledImageWrapper>
127
- <TopicAndTitle>
128
- <ResourceTitle>{title}</ResourceTitle>
129
- <TopicList topics={topics} />
130
- </TopicAndTitle>
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 { fonts, colors, spacing } from '@ndla/core';
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}`}>{tag}</StyledTagListElement>
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 && (
@@ -98,6 +98,7 @@ const ItemContexts = ({ contexts, id, title }: ItemContextsType) => {
98
98
  &nbsp;
99
99
  {contexts.length > 1 && (
100
100
  <Modal
101
+ label={t('searchPage.contextModal.ariaLabel')}
101
102
  activateButton={
102
103
  <ModalButton link>
103
104
  {t('searchPage.contextModal.button', {
@@ -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;
@@ -6,8 +6,6 @@
6
6
  *
7
7
  */
8
8
 
9
- import SnackBar from './SnackBar';
10
-
11
- export type { SnackBarItem } from './SnackBar';
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';
@@ -268,6 +268,7 @@ const Topic = ({
268
268
  {topic.visualElement ? (
269
269
  <>
270
270
  <Modal
271
+ label={t('topicPage.imageModal')}
271
272
  activateButton={
272
273
  <VisualElementButton
273
274
  stripped
@@ -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 type { SnackBarItem } from './SnackBar';
244
- export { SnackBar } from './SnackBar';
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';