@ndla/ui 21.0.0 → 22.0.2

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 (94) hide show
  1. package/es/Breadcrumb/ActionBreadcrumb.js +4 -4
  2. package/es/ContentLoader/index.js +8 -3
  3. package/es/MyNdla/Resource/Folder.js +7 -6
  4. package/es/Resource/BlockResource.js +106 -18
  5. package/es/Resource/ListResource.js +125 -26
  6. package/es/ResourceGroup/ResourceGroup.js +3 -3
  7. package/es/ResourceGroup/ResourceItem.js +12 -12
  8. package/es/ResourceGroup/ResourceList.js +2 -2
  9. package/es/Search/ContentTypeResult.js +1 -2
  10. package/es/SearchTypeResult/SearchItem.js +8 -8
  11. package/es/SnackBar/DefaultSnackbar.js +56 -0
  12. package/es/SnackBar/SnackbarProvider.js +179 -0
  13. package/es/SnackBar/index.js +2 -2
  14. package/es/TopicMenu/TopicMenuButton.js +4 -2
  15. package/es/TreeStructure/FolderItem.js +64 -44
  16. package/es/TreeStructure/FolderItems.js +4 -4
  17. package/es/TreeStructure/TreeStructure.js +16 -9
  18. package/es/index.js +1 -1
  19. package/es/locale/messages-en.js +17 -2
  20. package/es/locale/messages-nb.js +17 -2
  21. package/es/locale/messages-nn.js +17 -2
  22. package/es/locale/messages-se.js +17 -2
  23. package/es/locale/messages-sma.js +17 -2
  24. package/lib/Breadcrumb/ActionBreadcrumb.js +4 -4
  25. package/lib/ContentLoader/index.d.ts +4 -8
  26. package/lib/ContentLoader/index.js +8 -3
  27. package/lib/MyNdla/Resource/Folder.js +7 -6
  28. package/lib/Resource/BlockResource.d.ts +2 -1
  29. package/lib/Resource/BlockResource.js +107 -18
  30. package/lib/Resource/ListResource.d.ts +2 -1
  31. package/lib/Resource/ListResource.js +126 -26
  32. package/lib/ResourceGroup/ResourceGroup.d.ts +1 -1
  33. package/lib/ResourceGroup/ResourceGroup.js +3 -3
  34. package/lib/ResourceGroup/ResourceItem.d.ts +1 -1
  35. package/lib/ResourceGroup/ResourceItem.js +12 -12
  36. package/lib/ResourceGroup/ResourceList.d.ts +1 -1
  37. package/lib/ResourceGroup/ResourceList.js +2 -2
  38. package/lib/Search/ContentTypeResult.js +1 -2
  39. package/lib/SearchTypeResult/SearchItem.js +8 -8
  40. package/lib/SnackBar/DefaultSnackbar.d.ts +11 -0
  41. package/lib/SnackBar/DefaultSnackbar.js +70 -0
  42. package/lib/SnackBar/SnackbarProvider.d.ts +32 -0
  43. package/lib/SnackBar/SnackbarProvider.js +197 -0
  44. package/lib/SnackBar/index.d.ts +3 -3
  45. package/lib/SnackBar/index.js +23 -3
  46. package/lib/TopicMenu/TopicMenuButton.js +3 -1
  47. package/lib/TreeStructure/FolderItem.d.ts +1 -1
  48. package/lib/TreeStructure/FolderItem.js +65 -44
  49. package/lib/TreeStructure/FolderItems.js +4 -4
  50. package/lib/TreeStructure/TreeStructure.d.ts +2 -2
  51. package/lib/TreeStructure/TreeStructure.js +16 -9
  52. package/lib/TreeStructure/types.d.ts +2 -1
  53. package/lib/index.d.ts +2 -2
  54. package/lib/index.js +24 -3
  55. package/lib/locale/messages-en.d.ts +15 -0
  56. package/lib/locale/messages-en.js +17 -2
  57. package/lib/locale/messages-nb.d.ts +15 -0
  58. package/lib/locale/messages-nb.js +17 -2
  59. package/lib/locale/messages-nn.d.ts +15 -0
  60. package/lib/locale/messages-nn.js +17 -2
  61. package/lib/locale/messages-se.d.ts +15 -0
  62. package/lib/locale/messages-se.js +17 -2
  63. package/lib/locale/messages-sma.d.ts +15 -0
  64. package/lib/locale/messages-sma.js +17 -2
  65. package/package.json +14 -14
  66. package/src/.DS_Store +0 -0
  67. package/src/Breadcrumb/ActionBreadcrumb.tsx +1 -1
  68. package/src/ContentLoader/index.tsx +9 -9
  69. package/src/MyNdla/Resource/Folder.tsx +1 -1
  70. package/src/Resource/BlockResource.tsx +66 -5
  71. package/src/Resource/ListResource.tsx +86 -11
  72. package/src/ResourceGroup/ResourceGroup.tsx +1 -1
  73. package/src/ResourceGroup/ResourceItem.tsx +2 -2
  74. package/src/ResourceGroup/ResourceList.tsx +1 -1
  75. package/src/Search/ContentTypeResult.tsx +0 -1
  76. package/src/SearchTypeResult/SearchItem.tsx +0 -1
  77. package/src/SnackBar/DefaultSnackbar.tsx +70 -0
  78. package/src/SnackBar/SnackbarProvider.tsx +147 -0
  79. package/src/SnackBar/index.ts +3 -5
  80. package/src/TopicMenu/TopicMenuButton.jsx +5 -1
  81. package/src/TreeStructure/FolderItem.tsx +56 -37
  82. package/src/TreeStructure/FolderItems.tsx +3 -2
  83. package/src/TreeStructure/TreeStructure.tsx +11 -6
  84. package/src/TreeStructure/types.ts +2 -1
  85. package/src/index.ts +2 -2
  86. package/src/locale/messages-en.ts +15 -0
  87. package/src/locale/messages-nb.ts +15 -0
  88. package/src/locale/messages-nn.ts +15 -0
  89. package/src/locale/messages-se.ts +15 -0
  90. package/src/locale/messages-sma.ts +15 -0
  91. package/es/SnackBar/SnackBar.js +0 -117
  92. package/lib/SnackBar/SnackBar.d.ts +0 -23
  93. package/lib/SnackBar/SnackBar.js +0 -127
  94. package/src/SnackBar/SnackBar.tsx +0 -183
@@ -13,6 +13,7 @@ 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;
@@ -22,6 +23,7 @@ interface BlockResourceProps {
22
23
  tags?: string[];
23
24
  description?: string;
24
25
  menuItems?: MenuItemProps[];
26
+ isLoading?: boolean;
25
27
  }
26
28
 
27
29
  const BlockElementWrapper = styled(SafeLink)`
@@ -78,21 +80,80 @@ const ImageWrapper = styled.div`
78
80
  }
79
81
  `;
80
82
 
81
- const BlockResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: BlockResourceProps) => {
83
+ interface BlockImageProps {
84
+ image: ResourceImageProps;
85
+ loading?: boolean;
86
+ }
87
+
88
+ const BlockImage = ({ image, loading }: BlockImageProps) => {
89
+ if (loading) {
90
+ return (
91
+ <ContentLoader height={'100%'} width={'100%'} viewBox={null} preserveAspectRatio="none">
92
+ <rect x="0" y="0" rx="3" ry="3" width="100%" height="100%" />
93
+ </ContentLoader>
94
+ );
95
+ }
96
+ return <Image alt={image.alt} src={image.src} />;
97
+ };
98
+
99
+ interface BlockTitleProps {
100
+ title: string;
101
+ loading?: boolean;
102
+ }
103
+
104
+ const BlockTitle = ({ title, loading }: BlockTitleProps) => {
105
+ if (loading) {
106
+ return (
107
+ <ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
108
+ <rect x="0" y="0" rx="3" ry="3" width="100%" height="18px" />
109
+ </ContentLoader>
110
+ );
111
+ }
112
+ return <ResourceTitle>{title}</ResourceTitle>;
113
+ };
114
+
115
+ interface BlockTopicListProps {
116
+ topics: string[];
117
+ loading?: boolean;
118
+ }
119
+
120
+ const BlockTopicList = ({ topics, loading }: BlockTopicListProps) => {
121
+ if (loading) {
122
+ return (
123
+ <ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
124
+ <rect x="0" y="0" rx="3" ry="3" width="20%" height="18px" />
125
+ <rect x="25%" y="0" rx="3" ry="3" width="20%" height="18px" />
126
+ </ContentLoader>
127
+ );
128
+ }
129
+
130
+ return <TopicList topics={topics} />;
131
+ };
132
+
133
+ const BlockResource = ({
134
+ link,
135
+ title,
136
+ tags,
137
+ resourceImage,
138
+ topics,
139
+ description,
140
+ menuItems,
141
+ isLoading,
142
+ }: BlockResourceProps) => {
82
143
  return (
83
144
  <BlockElementWrapper to={link}>
84
145
  <ImageWrapper>
85
- <Image alt={resourceImage.alt} src={resourceImage.src} />
146
+ <BlockImage image={resourceImage} loading={isLoading} />
86
147
  </ImageWrapper>
87
148
  <BlockInfoWrapper>
88
149
  <div>
89
- <ResourceTitle>{title}</ResourceTitle>
150
+ <BlockTitle title={title} loading={isLoading} />
90
151
  </div>
91
- <TopicList topics={topics} />
152
+ <BlockTopicList topics={topics} loading={isLoading} />
92
153
  <BlockDescription>{description}</BlockDescription>
93
154
  <RightRow>
94
155
  {tags && <CompressedTagList tags={tags} />}
95
- {menuItems && menuItems.length > 0 && <MenuButton size="small" menuItems={menuItems} />}
156
+ {menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
96
157
  </RightRow>
97
158
  </BlockInfoWrapper>
98
159
  </BlockElementWrapper>
@@ -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
  `;
@@ -114,24 +115,98 @@ export interface ListResourceProps {
114
115
  tags?: string[];
115
116
  description?: string;
116
117
  menuItems?: MenuItemProps[];
118
+ isLoading?: boolean;
117
119
  }
118
120
 
119
- const ListResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: ListResourceProps) => {
121
+ interface ListResourceImageProps {
122
+ resourceImage: ResourceImageProps;
123
+ loading?: boolean;
124
+ type: 'normal' | 'compact';
125
+ }
126
+
127
+ const ListResourceImage = ({ resourceImage, loading, type }: ListResourceImageProps) => {
128
+ if (!loading) {
129
+ return <StyledImage alt={resourceImage.alt} src={resourceImage.src} />;
130
+ }
131
+ return (
132
+ <ContentLoader height={'100%'} width={'100%'} viewBox={null} preserveAspectRatio="none">
133
+ <rect
134
+ x="0"
135
+ y="0"
136
+ rx="3"
137
+ ry="3"
138
+ width={type === 'compact' ? '56' : '136'}
139
+ height={type === 'compact' ? '40' : '96'}
140
+ />
141
+ </ContentLoader>
142
+ );
143
+ };
144
+
145
+ interface TopicAndTitleProps {
146
+ title: string;
147
+ topics: string[];
148
+ loading?: boolean;
149
+ }
150
+
151
+ const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
152
+ if (loading) {
153
+ return (
154
+ <ContentLoader height={'40px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
155
+ <rect x="0" y="0" rx="3" ry="3" width={'100%'} height={'16'} />
156
+ <rect x="0" y="18" rx="3" ry="3" width={'70'} height={'16'} />
157
+ <rect x="80" y="18" rx="3" ry="3" width={'70'} height={'16'} />
158
+ </ContentLoader>
159
+ );
160
+ }
161
+ return (
162
+ <>
163
+ <ResourceTitle>{title}</ResourceTitle>
164
+ <TopicList topics={topics} />
165
+ </>
166
+ );
167
+ };
168
+
169
+ interface ResourceDescriptionProps {
170
+ description?: string;
171
+ loading?: boolean;
172
+ }
173
+
174
+ const ResourceDescription = ({ description, loading }: ResourceDescriptionProps) => {
175
+ if (loading) {
176
+ return (
177
+ <ContentLoader height={'20px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
178
+ <rect x="0" y="0" width="100%" height="20" />
179
+ </ContentLoader>
180
+ );
181
+ }
182
+ return <StyledResourceDescription>{description}</StyledResourceDescription>;
183
+ };
184
+
185
+ const ListResource = ({
186
+ link,
187
+ title,
188
+ tags,
189
+ resourceImage,
190
+ topics,
191
+ description,
192
+ menuItems,
193
+ isLoading = false,
194
+ }: ListResourceProps) => {
120
195
  const showDescription = description !== undefined;
196
+ const imageType = showDescription ? 'normal' : 'compact';
121
197
 
122
198
  return (
123
199
  <ResourceWrapper to={link}>
124
- <StyledImageWrapper imageSize={showDescription ? 'normal' : 'compact'}>
125
- <StyledImage alt={resourceImage.alt} src={resourceImage.src} />
200
+ <StyledImageWrapper imageSize={imageType}>
201
+ <ListResourceImage resourceImage={resourceImage} loading={isLoading} type={imageType} />
126
202
  </StyledImageWrapper>
127
- <TopicAndTitle>
128
- <ResourceTitle>{title}</ResourceTitle>
129
- <TopicList topics={topics} />
130
- </TopicAndTitle>
131
- {showDescription && <ResourceDescription>{description}</ResourceDescription>}
203
+ <TopicAndTitleWrapper>
204
+ <TopicAndTitle topics={topics} title={title} loading={isLoading} />
205
+ </TopicAndTitleWrapper>
206
+ {showDescription && <ResourceDescription description={description} loading={isLoading} />}
132
207
  <TagsandActionMenu>
133
208
  {tags && <CompressedTagList tags={tags} />}
134
- {menuItems && menuItems.length > 0 && <MenuButton size="small" menuItems={menuItems} />}
209
+ {menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
135
210
  </TagsandActionMenu>
136
211
  </ResourceWrapper>
137
212
  );
@@ -36,7 +36,7 @@ const StyledHeading = styled.h1`
36
36
  type Props = {
37
37
  invertedStyle?: boolean;
38
38
  toggleAdditionalResources: () => void;
39
- onToggleAddToFavorites: (id: string | number, add: string) => void;
39
+ onToggleAddToFavorites: (id: string) => void;
40
40
  showAddToFavoriteButton: boolean;
41
41
  };
42
42
 
@@ -229,7 +229,7 @@ type Props = {
229
229
  showAdditionalResources?: boolean;
230
230
  access?: 'teacher';
231
231
  isFavorite?: boolean;
232
- onToggleAddToFavorites: (id: string, add: boolean) => void;
232
+ onToggleAddToFavorites: (id: string) => void;
233
233
  showAddToFavoriteButton: boolean;
234
234
  };
235
235
 
@@ -305,7 +305,7 @@ const ResourceItem = ({
305
305
  <ArticleFavoritesButton
306
306
  isFavorite={isFavorite}
307
307
  articleId={id}
308
- onToggleAddToFavorites={() => onToggleAddToFavorites(id, true)}
308
+ onToggleAddToFavorites={() => onToggleAddToFavorites(id)}
309
309
  />
310
310
  )}
311
311
  </TypeWrapper>
@@ -48,7 +48,7 @@ export type ResourceListProps = {
48
48
  contentType?: string;
49
49
  title?: string;
50
50
  showAdditionalResources?: boolean;
51
- onToggleAddToFavorites: (id: string, add: boolean) => void;
51
+ onToggleAddToFavorites: (id: string) => void;
52
52
  showAddToFavoriteButton: boolean;
53
53
  };
54
54
 
@@ -129,7 +129,6 @@ const ContentTypeResult = ({
129
129
  return (
130
130
  <StyledListItem key={path} delayAnimation={delayAnimation}>
131
131
  <SafeLink
132
- tabIndex={-1}
133
132
  css={shouldHighlight && highlightStyle}
134
133
  data-highlighted={shouldHighlight || false}
135
134
  {...linkProps}
@@ -154,7 +154,6 @@ const ItemText = styled.div<ItemTypeProps>`
154
154
 
155
155
  const ContextWrapper = styled.div`
156
156
  background: white;
157
- z-index: 1;
158
157
  padding: 0 ${spacing.normal} ${spacing.small};
159
158
  transition: all ${animations.durations.fast} ease-in-out;
160
159
  ${ItemWrapper}:hover & {
@@ -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';
@@ -9,7 +9,7 @@
9
9
  import React from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import { css } from '@emotion/core';
12
- import { spacing, fonts, colors } from '@ndla/core';
12
+ import { spacing, fonts, colors, mq, breakpoints } from '@ndla/core';
13
13
  import { Menu } from '@ndla/icons/common';
14
14
  import Button from '@ndla/button';
15
15
 
@@ -38,6 +38,10 @@ const style = css`
38
38
  background: ${colors.white};
39
39
  color: ${colors.brand.primary};
40
40
  }
41
+ ${mq.range({ until: breakpoints.tablet })} {
42
+ padding-left: ${spacing.xsmall};
43
+ padding-right: ${spacing.xsmall};
44
+ }
41
45
  `;
42
46
 
43
47
  const TopicMenuButton = ({ ndlaFilm, children, ...rest }) => (