@ndla/ui 55.0.12-alpha.0 → 55.0.13-alpha.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 (192) hide show
  1. package/dist/all-aout.js +0 -0
  2. package/dist/all.css +1 -0
  3. package/dist/panda.buildinfo.json +170 -0
  4. package/dist/styles.css +686 -0
  5. package/es/Article/Article.js +3 -4
  6. package/es/Article/ArticleByline.js +9 -9
  7. package/es/Article/ArticleFootNotes.js +4 -4
  8. package/es/AudioPlayer/AudioPlayer.js +142 -163
  9. package/es/AudioPlayer/Controls.js +187 -203
  10. package/es/AudioPlayer/SpeechControl.js +13 -11
  11. package/es/BlogPost/BlogPost.js +85 -23
  12. package/es/CampaignBlock/CampaignBlock.js +3 -4
  13. package/es/CodeBlock/CodeBlock.js +88 -96
  14. package/es/ContactBlock/ContactBlock.js +54 -40
  15. package/es/ContentLoader/index.js +7 -7
  16. package/es/CopyParagraphButton/CopyParagraphButton.js +4 -4
  17. package/es/Embed/AudioEmbed.js +5 -9
  18. package/es/Embed/BrightcoveEmbed.js +12 -15
  19. package/es/Embed/CodeEmbed.js +58 -10
  20. package/es/Embed/ConceptEmbed.js +15 -20
  21. package/es/Embed/ContentLinkEmbed.js +1 -1
  22. package/es/Embed/EmbedErrorPlaceholder.js +32 -17
  23. package/es/Embed/ExternalEmbed.js +7 -10
  24. package/es/Embed/FootnoteEmbed.js +3 -3
  25. package/es/Embed/H5pEmbed.js +1 -2
  26. package/es/Embed/IframeEmbed.js +8 -9
  27. package/es/Embed/ImageEmbed.js +167 -122
  28. package/es/Embed/RelatedContentEmbed.js +8 -10
  29. package/es/Embed/UuDisclaimerEmbed.js +2 -2
  30. package/es/Embed/conceptComponents.js +9 -9
  31. package/es/ErrorMessage/ErrorMessage.js +1 -1
  32. package/es/FactBox/FactBox.js +2 -2
  33. package/es/FileList/File.js +1 -1
  34. package/es/FileList/Format.js +3 -3
  35. package/es/FrontpageArticle/FrontpageArticle.js +1 -1
  36. package/es/Gloss/Gloss.js +9 -11
  37. package/es/Gloss/GlossExample.js +3 -4
  38. package/es/Grid/Grid.js +1 -1
  39. package/es/Image/Image.js +7 -8
  40. package/es/Image/ImageLink.js +1 -1
  41. package/es/KeyFigure/KeyFigure.js +2 -2
  42. package/es/LanguageSelector/LanguageSelector.js +2 -2
  43. package/es/LetterFilter/LetterFilter.js +1 -1
  44. package/es/LicenseByline/EmbedByline.js +5 -6
  45. package/es/LicenseByline/LicenseDescription.js +1 -1
  46. package/es/LicenseByline/LicenseLink.js +1 -2
  47. package/es/Messages/MessageBox.js +1 -1
  48. package/es/Notion/Notion.js +2 -2
  49. package/es/Notion/NotionImage.js +12 -57
  50. package/es/RelatedArticleList/RelatedArticleList.js +3 -3
  51. package/es/ResourceBox/ResourceBox.js +12 -17
  52. package/es/Search/ActiveFilters.js +1 -1
  53. package/es/Search/ContentTypeResult.js +9 -6
  54. package/es/Search/ContentTypeResultStyles.js +1 -1
  55. package/es/Search/IsPathToHighlight.js +1 -1
  56. package/es/Search/SearchField.js +6 -8
  57. package/es/Search/SearchResult.js +14 -19
  58. package/es/Search/SearchResultSleeve.js +14 -16
  59. package/es/SnackBar/SnackbarProvider.js +8 -11
  60. package/es/TagSelector/TagSelector.js +1 -1
  61. package/es/TagSelector/ariaMessages.js +6 -6
  62. package/es/TreeStructure/AddFolderButton.js +4 -6
  63. package/es/TreeStructure/ComboboxButton.js +4 -7
  64. package/es/TreeStructure/FolderItem.js +12 -15
  65. package/es/TreeStructure/FolderItems.js +3 -3
  66. package/es/TreeStructure/TreeStructure.js +9 -12
  67. package/es/TreeStructure/helperFunctions.js +1 -1
  68. package/es/ZendeskButton/ZendeskButton.js +55 -0
  69. package/es/i18n/formatNestedMessages.js +1 -1
  70. package/es/index.js +2 -1
  71. package/es/locale/messages-en.js +9 -8
  72. package/es/locale/messages-nb.js +9 -8
  73. package/es/locale/messages-nn.js +9 -8
  74. package/es/locale/messages-se.js +9 -8
  75. package/es/locale/messages-sma.js +9 -8
  76. package/es/styles.css +686 -0
  77. package/es/utils/relativeUrl.js +3 -3
  78. package/lib/Article/Article.js +3 -4
  79. package/lib/Article/ArticleByline.js +9 -9
  80. package/lib/Article/ArticleFootNotes.js +4 -4
  81. package/lib/AudioPlayer/AudioPlayer.d.ts +1 -2
  82. package/lib/AudioPlayer/AudioPlayer.js +142 -162
  83. package/lib/AudioPlayer/Controls.js +190 -205
  84. package/lib/AudioPlayer/SpeechControl.js +13 -11
  85. package/lib/BlogPost/BlogPost.d.ts +2 -2
  86. package/lib/BlogPost/BlogPost.js +85 -24
  87. package/lib/CampaignBlock/CampaignBlock.js +3 -4
  88. package/lib/CodeBlock/CodeBlock.d.ts +5 -8
  89. package/lib/CodeBlock/CodeBlock.js +88 -96
  90. package/lib/ContactBlock/ContactBlock.js +55 -43
  91. package/lib/ContentLoader/index.js +7 -7
  92. package/lib/CopyParagraphButton/CopyParagraphButton.js +4 -4
  93. package/lib/Embed/AudioEmbed.js +5 -9
  94. package/lib/Embed/BrightcoveEmbed.js +12 -15
  95. package/lib/Embed/CodeEmbed.js +56 -8
  96. package/lib/Embed/ConceptEmbed.js +15 -20
  97. package/lib/Embed/ContentLinkEmbed.js +1 -1
  98. package/lib/Embed/EmbedErrorPlaceholder.d.ts +4 -3
  99. package/lib/Embed/EmbedErrorPlaceholder.js +32 -18
  100. package/lib/Embed/ExternalEmbed.js +7 -10
  101. package/lib/Embed/FootnoteEmbed.js +3 -3
  102. package/lib/Embed/H5pEmbed.js +1 -2
  103. package/lib/Embed/IframeEmbed.js +8 -9
  104. package/lib/Embed/ImageEmbed.d.ts +1 -2
  105. package/lib/Embed/ImageEmbed.js +167 -123
  106. package/lib/Embed/RelatedContentEmbed.js +8 -10
  107. package/lib/Embed/UuDisclaimerEmbed.js +2 -2
  108. package/lib/Embed/conceptComponents.js +9 -9
  109. package/lib/ErrorMessage/ErrorMessage.js +1 -1
  110. package/lib/FactBox/FactBox.js +2 -2
  111. package/lib/FileList/File.js +1 -1
  112. package/lib/FileList/Format.js +3 -3
  113. package/lib/FrontpageArticle/FrontpageArticle.js +1 -1
  114. package/lib/Gloss/Gloss.js +9 -11
  115. package/lib/Gloss/GlossExample.js +3 -4
  116. package/lib/Grid/Grid.js +1 -1
  117. package/lib/Image/Image.js +7 -8
  118. package/lib/Image/ImageLink.js +1 -1
  119. package/lib/KeyFigure/KeyFigure.js +2 -2
  120. package/lib/LanguageSelector/LanguageSelector.js +2 -2
  121. package/lib/LetterFilter/LetterFilter.js +1 -1
  122. package/lib/LicenseByline/EmbedByline.js +5 -6
  123. package/lib/LicenseByline/LicenseDescription.js +1 -1
  124. package/lib/LicenseByline/LicenseLink.js +1 -2
  125. package/lib/Messages/MessageBox.js +1 -1
  126. package/lib/Notion/Notion.js +2 -2
  127. package/lib/Notion/NotionImage.d.ts +1 -11
  128. package/lib/Notion/NotionImage.js +12 -59
  129. package/lib/RelatedArticleList/RelatedArticleList.js +3 -3
  130. package/lib/ResourceBox/ResourceBox.js +13 -18
  131. package/lib/Search/ActiveFilters.js +1 -1
  132. package/lib/Search/ContentTypeResult.js +9 -6
  133. package/lib/Search/ContentTypeResultStyles.js +1 -1
  134. package/lib/Search/IsPathToHighlight.js +1 -1
  135. package/lib/Search/SearchField.js +6 -8
  136. package/lib/Search/SearchResult.js +14 -19
  137. package/lib/Search/SearchResultSleeve.js +14 -16
  138. package/lib/SnackBar/SnackbarProvider.js +8 -11
  139. package/lib/TagSelector/TagSelector.js +1 -1
  140. package/lib/TagSelector/ariaMessages.js +6 -6
  141. package/lib/TreeStructure/AddFolderButton.js +4 -6
  142. package/lib/TreeStructure/ComboboxButton.js +4 -7
  143. package/lib/TreeStructure/FolderItem.js +12 -15
  144. package/lib/TreeStructure/FolderItems.js +3 -3
  145. package/lib/TreeStructure/TreeStructure.js +9 -12
  146. package/lib/TreeStructure/helperFunctions.js +1 -1
  147. package/lib/ZendeskButton/ZendeskButton.d.ts +19 -0
  148. package/lib/ZendeskButton/ZendeskButton.js +61 -0
  149. package/lib/i18n/formatNestedMessages.js +1 -1
  150. package/lib/index.d.ts +2 -0
  151. package/lib/index.js +7 -0
  152. package/lib/locale/messages-en.d.ts +1 -0
  153. package/lib/locale/messages-en.js +9 -8
  154. package/lib/locale/messages-nb.d.ts +1 -0
  155. package/lib/locale/messages-nb.js +9 -8
  156. package/lib/locale/messages-nn.d.ts +1 -0
  157. package/lib/locale/messages-nn.js +9 -8
  158. package/lib/locale/messages-se.d.ts +1 -0
  159. package/lib/locale/messages-se.js +9 -8
  160. package/lib/locale/messages-sma.d.ts +1 -0
  161. package/lib/locale/messages-sma.js +9 -8
  162. package/lib/styles.css +686 -0
  163. package/lib/types.d.ts +1 -0
  164. package/lib/utils/relativeUrl.js +3 -3
  165. package/package.json +17 -12
  166. package/src/AudioPlayer/AudioPlayer.tsx +139 -176
  167. package/src/AudioPlayer/Controls.tsx +210 -250
  168. package/src/AudioPlayer/SpeechControl.tsx +9 -7
  169. package/src/BlogPost/BlogPost.tsx +82 -58
  170. package/src/CodeBlock/CodeBlock.stories.tsx +0 -43
  171. package/src/CodeBlock/CodeBlock.tsx +91 -202
  172. package/src/ContactBlock/ContactBlock.tsx +10 -2
  173. package/src/Embed/CodeEmbed.stories.tsx +95 -0
  174. package/src/Embed/CodeEmbed.tsx +62 -7
  175. package/src/Embed/ConceptEmbed.tsx +1 -9
  176. package/src/Embed/EmbedErrorPlaceholder.tsx +31 -28
  177. package/src/Embed/ImageEmbed.stories.tsx +53 -11
  178. package/src/Embed/ImageEmbed.tsx +162 -166
  179. package/src/Notion/NotionImage.tsx +4 -54
  180. package/src/ResourceBox/ResourceBox.tsx +3 -15
  181. package/src/Search/ContentTypeResult.tsx +9 -3
  182. package/src/Search/SearchResultSleeve.tsx +5 -2
  183. package/src/ZendeskButton/ZendeskButton.tsx +58 -0
  184. package/src/index.ts +4 -0
  185. package/src/locale/messages-en.ts +1 -0
  186. package/src/locale/messages-nb.ts +1 -0
  187. package/src/locale/messages-nn.ts +1 -0
  188. package/src/locale/messages-se.ts +1 -0
  189. package/src/locale/messages-sma.ts +1 -0
  190. package/src/types.ts +2 -0
  191. package/src/Image/__tests__/Image-test.tsx +0 -66
  192. package/src/Image/__tests__/__snapshots__/Image-test.tsx.snap +0 -194
@@ -6,26 +6,21 @@
6
6
  *
7
7
  */
8
8
 
9
- /** @jsxImportSource @emotion/react */
10
9
  import parse from "html-react-parser";
11
- import { MouseEventHandler, ReactNode, useMemo, useState } from "react";
10
+ import { ReactNode, useMemo, useState } from "react";
12
11
  import { useTranslation } from "react-i18next";
13
- import styled from "@emotion/styled";
14
- import { colors, misc, spacing } from "@ndla/core";
15
12
  import { Plus } from "@ndla/icons/action";
16
13
  import { ChevronDown, ChevronUp } from "@ndla/icons/common";
17
- import { COPYRIGHTED } from "@ndla/licenses";
14
+ import { Figure, FigureSize, FigureVariantProps, Image } from "@ndla/primitives";
15
+ import { styled } from "@ndla/styled-system/jsx";
18
16
  import { ImageEmbedData, ImageMetaData } from "@ndla/types-embed";
19
17
  import EmbedErrorPlaceholder from "./EmbedErrorPlaceholder";
20
18
  import { RenderContext } from "./types";
21
- import { Figure, FigureType } from "../Figure";
22
- import Image from "../Image";
23
19
  import { EmbedByline } from "../LicenseByline";
24
20
 
25
21
  interface Props {
26
22
  embed: ImageMetaData;
27
23
  previewAlt?: boolean;
28
- inGrid?: boolean;
29
24
  lang?: string;
30
25
  renderContext?: RenderContext;
31
26
  children?: ReactNode;
@@ -49,21 +44,15 @@ export const getLicenseCredits = (copyright?: {
49
44
  };
50
45
 
51
46
  export const errorSvgSrc = `data:image/svg+xml;charset=UTF-8,%3Csvg fill='%238A8888' height='400' viewBox='0 0 24 12' width='100%25' xmlns='http://www.w3.org/2000/svg' style='background-color: %23EFF0F2'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath transform='scale(0.3) translate(28, 8.5)' d='M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z'/%3E%3C/svg%3E`;
52
- const isSmall = (size?: string): size is "xsmall" | "small" => size === "xsmall" || size === "small";
53
47
 
54
- const isAlign = (align?: string): align is "left" | "right" => align === "left" || align === "right";
55
-
56
- const getFigureType = (size?: string, align?: string): FigureType => {
57
- if (size && isSmall(size) && align && isAlign(align)) {
58
- return `${size}-${align}`;
59
- }
60
- if (size && isSmall(size) && !align) {
61
- return size as FigureType;
62
- }
63
- if (align && isAlign(align)) {
64
- return align;
65
- }
66
- return "full";
48
+ const getFigureProps = (size?: string, float?: string): FigureVariantProps => {
49
+ const actualFloat = float === "left" ? "left" : float === "right" ? "right" : undefined;
50
+ const replacedSize: FigureSize = (size?.replace("-hide-byline", "") ?? "full") as FigureSize;
51
+ return {
52
+ float: actualFloat,
53
+ // Figure expects you to set a size when floating. If you don't, the figure will simply take up the available width. Fallback to medium in those cases.
54
+ size: replacedSize === "full" && float ? "medium" : replacedSize,
55
+ };
67
56
  };
68
57
 
69
58
  const getSizes = (size?: string, align?: string) => {
@@ -111,36 +100,127 @@ export const getCrop = (data: ImageEmbedData) => {
111
100
 
112
101
  const expandedSizes = "(min-width: 1024px) 1024px, 100vw";
113
102
 
114
- const StyledFigure = styled(Figure)`
115
- &:hover {
116
- [data-byline-button] {
117
- background: ${colors.white};
118
- }
119
- button[data-expanded] {
120
- transform: scale(1.2);
121
- }
122
- }
123
- button[data-expanded="true"] {
124
- svg {
125
- transform: rotate(-45deg);
126
- }
127
- }
128
- &[data-float="right"] {
129
- float: right;
130
- }
131
- &[data-float="left"] {
132
- float: left;
133
- }
134
- `;
103
+ const ImageWrapper = styled("div", {
104
+ base: {
105
+ overflow: "hidden",
106
+ position: "relative",
107
+ width: "100%",
108
+ "& img": {
109
+ width: "100%",
110
+ },
111
+ },
112
+ variants: {
113
+ svg: {
114
+ true: {
115
+ display: "flex",
116
+ justifyContent: "center",
117
+ },
118
+ false: {},
119
+ },
120
+ border: {
121
+ true: {
122
+ border: "1px solid",
123
+ // TODO: Not sure if we want this color.
124
+ borderColor: "surface.brand.1.strong",
125
+ borderBottom: "0",
126
+ borderRadius: "xsmall",
127
+ borderBottomLeftRadius: "0",
128
+ borderBottomRightRadius: "0",
129
+ },
130
+ false: {},
131
+ },
132
+ expandable: {
133
+ true: {
134
+ cursor: "pointer",
135
+ },
136
+ false: {},
137
+ },
138
+ },
139
+ });
135
140
 
136
- const ImageEmbed = ({ embed, previewAlt, inGrid, lang, renderContext = "article", children }: Props) => {
141
+ const StyledFigure = styled(Figure, {
142
+ base: {
143
+ _hover: {
144
+ "& [data-byline-button]": {
145
+ background: "background.default",
146
+ },
147
+ "& button[data-expanded]": {
148
+ transform: "scale(1.2)",
149
+ },
150
+ },
151
+ "& button[data-expanded='true']": {
152
+ "& svg": {
153
+ transform: "rotate(-45deg)",
154
+ },
155
+ },
156
+ },
157
+ });
158
+
159
+ // TODO: Ask about BylineButton styling. Not included in design
160
+ const BylineButton = styled(
161
+ "button",
162
+ {
163
+ base: {
164
+ cursor: "pointer",
165
+ position: "absolute",
166
+ zIndex: "base",
167
+ bottom: "0",
168
+ right: "0",
169
+ paddingBlock: "xsmall",
170
+ paddingInline: "xsmall",
171
+ transitionProperty: "transform, background-color, color",
172
+ transitionDuration: "normal",
173
+ transitionTimingFunction: "ease-out",
174
+ background: "background.default/20",
175
+ border: "0",
176
+ "& svg": {
177
+ transitionProperty: "transform",
178
+ transitionDuration: "normal",
179
+ transitionTimingFunction: "ease-out",
180
+ fill: "primary",
181
+ },
182
+ },
183
+ },
184
+ { defaultProps: { type: "button" } },
185
+ );
186
+
187
+ const ExpandButton = styled(
188
+ "button",
189
+ {
190
+ base: {
191
+ display: "flex",
192
+ alignItems: "center",
193
+ justifyContent: "center",
194
+ cursor: "pointer",
195
+ position: "absolute",
196
+ padding: "0",
197
+ top: "xsmall",
198
+ right: "xsmall",
199
+ width: "medium",
200
+ height: "medium",
201
+ border: "2px solid",
202
+ borderColor: "background.default",
203
+ transitionProperty: "transform, background-color, color",
204
+ transitionDuration: "normal",
205
+ transitionTimingFunction: "ease-out",
206
+ color: "background.default",
207
+ backgroundColor: "surface.action",
208
+ borderRadius: "large",
209
+ "& svg": {
210
+ transitionProperty: "transform",
211
+ transitionDuration: "normal",
212
+ transitionTimingFunction: "ease-out",
213
+ },
214
+ },
215
+ },
216
+ { defaultProps: { type: "button" } },
217
+ );
218
+
219
+ const ImageEmbed = ({ embed, previewAlt, lang, renderContext = "article", children }: Props) => {
137
220
  const [isBylineHidden, setIsBylineHidden] = useState(hideByline(embed.embedData));
138
221
  const [imageSizes, setImageSizes] = useState<string | undefined>(undefined);
139
- // Full-size figures automatically get a margin of {spacing.normal} on its y-axis if a float is not set (or if float is an empty string).
140
- // This adds some margin to normal figures within an article, but should not happen for figures in a grid.
141
- const [floatAttr, setFloatAttr] = useState<{ "data-float"?: string }>(() =>
142
- inGrid && !embed.embedData.align ? {} : { "data-float": embed.embedData.align },
143
- );
222
+ const figureProps = getFigureProps(embed.embedData.size, embed.embedData.align);
223
+ const { t } = useTranslation();
144
224
 
145
225
  const parsedDescription = useMemo(() => {
146
226
  if (embed.embedData.caption || renderContext === "article") {
@@ -152,60 +232,56 @@ const ImageEmbed = ({ embed, previewAlt, inGrid, lang, renderContext = "article"
152
232
  }, [embed, renderContext]);
153
233
 
154
234
  if (embed.status === "error") {
155
- const { align, size } = embed.embedData;
156
- const figureType = getFigureType(size, align);
157
- return <EmbedErrorPlaceholder type={"image"} figureType={figureType} />;
235
+ return <EmbedErrorPlaceholder type={"image"} figureType={figureProps?.size} float={figureProps?.float} />;
158
236
  }
159
237
 
160
238
  const { data, embedData } = embed;
161
239
 
162
240
  const altText = embedData.alt || "";
163
241
 
164
- const figureType = getFigureType(embedData.size, embedData.align);
165
242
  const sizes = getSizes(embedData.size, embedData.align);
166
243
 
167
244
  const focalPoint = getFocalPoint(embedData);
168
245
  const crop = getCrop(embedData);
169
246
 
170
- const isCopyrighted = data.copyright.license.license.toLowerCase() === COPYRIGHTED;
171
-
172
247
  const toggleImageSize = () => {
173
- if (!imageSizes) {
174
- setImageSizes(expandedSizes);
175
- setTimeout(() => {
176
- setFloatAttr({});
177
- }, 400); //Removing the float parameter too quickly causes the image to be resized from left regardless
178
- } else {
179
- setImageSizes(undefined);
180
- setFloatAttr({ "data-float": embedData.align });
181
- }
248
+ setImageSizes((sizes) => (!sizes ? expandedSizes : undefined));
182
249
  };
183
250
 
251
+ // TODO: Check how this works with `children`. Will only be important for ED
184
252
  return (
185
- <StyledFigure type={imageSizes ? undefined : figureType} {...floatAttr}>
253
+ <StyledFigure float={figureProps?.float} size={imageSizes ? "full" : figureProps?.size ?? "medium"}>
186
254
  {children}
187
- <Image
188
- focalPoint={focalPoint}
189
- contentType={data.image.contentType}
190
- crop={crop}
191
- sizes={imageSizes ?? sizes}
192
- alt={altText}
193
- src={data.image.imageUrl}
194
- border={embedData.border}
195
- onExpand={isAlign(embedData.align) ? toggleImageSize : undefined}
196
- expanded={!!imageSizes}
197
- expandButton={
255
+ <ImageWrapper border={embedData.border === "true"} expandable={!!figureProps?.float}>
256
+ <Image
257
+ focalPoint={focalPoint}
258
+ contentType={data.image.contentType}
259
+ crop={crop}
260
+ sizes={imageSizes ?? sizes}
261
+ alt={altText}
262
+ src={data.image.imageUrl}
263
+ lang={lang}
264
+ onClick={figureProps?.float ? toggleImageSize : undefined}
265
+ />
266
+ {!!embedData.align && (
198
267
  <ExpandButton
199
- embedData={embedData}
200
- expanded={!!imageSizes}
201
- align={embedData.align}
202
- bylineHidden={isBylineHidden}
203
- onExpand={toggleImageSize}
204
- onHideByline={() => setIsBylineHidden((p) => !p)}
205
- />
206
- }
207
- lang={lang}
208
- />
268
+ aria-label={t(`license.images.itemImage.zoom${imageSizes ? "Out" : ""}ImageButtonLabel`)}
269
+ onClick={toggleImageSize}
270
+ data-expanded={!!imageSizes}
271
+ >
272
+ <Plus />
273
+ </ExpandButton>
274
+ )}
275
+ {(embedData.size?.endsWith("-hide-byline") || embedData.hideByline === "true") && (
276
+ <BylineButton
277
+ data-byline-button=""
278
+ aria-label={t(`license.images.itemImage.${isBylineHidden ? "expandByline" : "minimizeByline"}`)}
279
+ onClick={() => setIsBylineHidden((p) => !p)}
280
+ >
281
+ {isBylineHidden ? <ChevronDown /> : <ChevronUp />}
282
+ </BylineButton>
283
+ )}
284
+ </ImageWrapper>
209
285
  {isBylineHidden ? null : (
210
286
  <EmbedByline
211
287
  type="image"
@@ -222,84 +298,4 @@ const hideByline = (embed: ImageEmbedData): boolean => {
222
298
  return (!!embed.size && embed.size.endsWith("-hide-byline")) || embed.hideByline === "true";
223
299
  };
224
300
 
225
- interface ExpandButtonProps {
226
- embedData: ImageEmbedData;
227
- align?: string;
228
- expanded: boolean;
229
- bylineHidden: boolean;
230
- onExpand: MouseEventHandler<HTMLButtonElement>;
231
- onHideByline: MouseEventHandler<HTMLButtonElement>;
232
- }
233
-
234
- const BylineButton = styled.button`
235
- cursor: pointer;
236
- position: absolute;
237
- z-index: 1;
238
- bottom: 0;
239
- right: 0;
240
- padding: ${spacing.small};
241
- transition: all 0.3s ease-out;
242
- background: ${colors.background.default}20;
243
- border: 0;
244
-
245
- svg {
246
- transition: transform 0.4s ease-out;
247
- width: ${spacing.normal};
248
- height: ${spacing.normal};
249
- fill: ${colors.brand.primary};
250
- }
251
- `;
252
-
253
- const StyledButton = styled.button`
254
- display: flex;
255
- align-items: center;
256
- justify-content: center;
257
- cursor: pointer;
258
- position: absolute;
259
- padding: 0;
260
- top: ${spacing.small};
261
- right: ${spacing.small};
262
- width: ${spacing.normal};
263
- height: ${spacing.normal};
264
- border: 2px solid ${colors.white};
265
- transition: all 0.3s ease-out;
266
- color: ${colors.white};
267
- background-color: ${colors.brand.primary};
268
- border-radius: ${misc.borderRadiusLarge};
269
- svg {
270
- transition: transform 0.4s ease-out;
271
- height: ${spacing.nsmall};
272
- width: ${spacing.nsmall};
273
- }
274
- `;
275
-
276
- const ExpandButton = ({ align, embedData, expanded, bylineHidden, onExpand, onHideByline }: ExpandButtonProps) => {
277
- const { t } = useTranslation();
278
- if (isAlign(align)) {
279
- return (
280
- <StyledButton
281
- type="button"
282
- aria-label={t(`license.images.itemImage.zoom${expanded ? "Out" : ""}ImageButtonLabel`)}
283
- onClick={onExpand}
284
- data-expanded={expanded}
285
- >
286
- <Plus />
287
- </StyledButton>
288
- );
289
- }
290
- if (hideByline(embedData)) {
291
- return (
292
- <BylineButton
293
- type="button"
294
- data-byline-button=""
295
- aria-label={t(`license.images.itemImage.${bylineHidden ? "expandByline" : "minimizeByline"}`)}
296
- onClick={onHideByline}
297
- >
298
- {bylineHidden ? <ChevronDown /> : <ChevronUp />}
299
- </BylineButton>
300
- );
301
- }
302
- return null;
303
- };
304
-
305
301
  export default ImageEmbed;
@@ -7,13 +7,10 @@
7
7
  */
8
8
 
9
9
  /** @jsxImportSource @emotion/react */
10
- import { useTranslation } from "react-i18next";
11
10
  import styled from "@emotion/styled";
12
- import { animations, breakpoints, colors, misc, mq, spacing } from "@ndla/core";
13
- import { ExpandTwoArrows, CursorClick } from "@ndla/icons/action";
14
- import { Play } from "@ndla/icons/common";
11
+ import { animations, breakpoints, colors, mq, spacing } from "@ndla/core";
12
+ import { Image } from "@ndla/primitives";
15
13
  import { Figure } from "../Figure";
16
- import Image from "../Image";
17
14
 
18
15
  const StyledImageWrapper = styled.div`
19
16
  overflow: hidden;
@@ -41,7 +38,6 @@ const StyledImage = styled(Image)`
41
38
  `;
42
39
 
43
40
  interface Props {
44
- type: "image" | "video" | "h5p" | "iframe" | "external" | "audio" | undefined;
45
41
  src: string;
46
42
  alt: string;
47
43
  }
@@ -57,58 +53,12 @@ const StyledFigure = styled(Figure)`
57
53
  }
58
54
  `;
59
55
 
60
- export const NotionImage = ({ src, alt, type }: Props) => {
56
+ export const NotionImage = ({ src, alt }: Props) => {
61
57
  return (
62
58
  <StyledFigure type={"full-column"}>
63
59
  <StyledImageWrapper>
64
- <StyledImage alt={alt} src={src} expandButton={<OpenButton type={type} />} />
60
+ <StyledImage alt={alt} src={src} />
65
61
  </StyledImageWrapper>
66
62
  </StyledFigure>
67
63
  );
68
64
  };
69
-
70
- interface OpenButtonProps {
71
- type?: "image" | "video" | "h5p" | "iframe" | "external" | "audio";
72
- }
73
-
74
- export const FigureActionIndicator = styled.div`
75
- all: unset;
76
- cursor: pointer;
77
- position: absolute;
78
- padding: 0;
79
- bottom: 8px;
80
- right: 8px;
81
- width: 40px;
82
- height: 40px;
83
- display: flex;
84
- justify-content: center;
85
- align-items: center;
86
- transition: all 0.3s ease-out;
87
- // The 65 is added to alter the opacity.
88
- background-color: ${colors.background.default}65;
89
- border-radius: ${misc.borderRadiusLarge};
90
- border: 0;
91
- svg {
92
- transition: transform 0.4s ease-out;
93
- width: 18px;
94
- height: 18px;
95
- fill: ${colors.brand.primary};
96
- color: ${colors.brand.primary};
97
- }
98
- ${mq.range({ until: breakpoints.tablet })} {
99
- display: none;
100
- }
101
- `;
102
-
103
- export const OpenButton = ({ type }: OpenButtonProps) => {
104
- const { t } = useTranslation();
105
- return (
106
- <FigureActionIndicator data-open-button="" aria-label={t("license.images.itemImage.zoomImageButtonLabel")}>
107
- {type === "image" && <ExpandTwoArrows />}
108
- {type === "h5p" && <CursorClick style={{ width: "24px", height: "24px" }} />}
109
- {type === "iframe" && <CursorClick style={{ width: "24px", height: "24px" }} />}
110
- {type === "external" && <CursorClick style={{ width: "24px", height: "24px" }} />}
111
- {type === "video" && <Play style={{ width: "24px", height: "24px" }} />}
112
- </FigureActionIndicator>
113
- );
114
- };
@@ -9,8 +9,8 @@
9
9
  import styled from "@emotion/styled";
10
10
  import { breakpoints, fonts, mq, colors, spacing } from "@ndla/core";
11
11
  import { Launch } from "@ndla/icons/common";
12
+ import { Image } from "@ndla/primitives";
12
13
  import { SafeLinkButton } from "@ndla/safelink";
13
- import Image from "../Image";
14
14
 
15
15
  const ResourceBoxContainer = styled.div`
16
16
  display: flex;
@@ -52,18 +52,6 @@ const ContentWrapper = styled.div`
52
52
  }
53
53
  `;
54
54
 
55
- const StyledButton = styled(SafeLinkButton)`
56
- display: flex;
57
- gap: ${spacing.xxsmall};
58
- align-items: center;
59
- border: 1px solid ${colors.brand.tertiary};
60
- :hover {
61
- background-color: ${colors.brand.primary};
62
- border: 1px solid ${colors.brand.primary};
63
- color: ${colors.white};
64
- }
65
- `;
66
-
67
55
  const StyledImage = styled(Image)`
68
56
  && {
69
57
  object-fit: cover;
@@ -98,10 +86,10 @@ export const ResourceBox = ({ image, title, caption, url, buttonText }: Props) =
98
86
  <ContentWrapper>
99
87
  <Title>{title}</Title>
100
88
  <Caption>{caption}</Caption>
101
- <StyledButton to={url} target="_blank" variant="outline" shape="pill">
89
+ <SafeLinkButton to={url} target="_blank" variant="secondary">
102
90
  {buttonText}
103
91
  <Launch aria-hidden />
104
- </StyledButton>
92
+ </SafeLinkButton>
105
93
  </ContentWrapper>
106
94
  </ResourceBoxContainer>
107
95
  );
@@ -9,6 +9,7 @@
9
9
  /** @jsxImportSource @emotion/react */
10
10
  import { ReactElement, useEffect, useRef, useState } from "react";
11
11
  import { useTranslation } from "react-i18next";
12
+ import styled from "@emotion/styled";
12
13
  import { ButtonV2 } from "@ndla/button";
13
14
  import { Additional, ChevronDown, ChevronUp } from "@ndla/icons/common";
14
15
  import { SafeLink } from "@ndla/safelink";
@@ -64,6 +65,12 @@ type Props = {
64
65
  unGrouped?: boolean;
65
66
  };
66
67
 
68
+ const StyledSafeLink = styled(SafeLink)`
69
+ &[data-highlighted="true"] {
70
+ ${highlightStyle}
71
+ }
72
+ `;
73
+
67
74
  const ContentTypeResult = ({
68
75
  contentTypeResult,
69
76
  onNavigate,
@@ -139,8 +146,7 @@ const ContentTypeResult = ({
139
146
 
140
147
  return (
141
148
  <StyledListItem key={path} delayAnimation={delayAnimation}>
142
- <SafeLink
143
- css={shouldHighlight && highlightStyle}
149
+ <StyledSafeLink
144
150
  data-highlighted={shouldHighlight || false}
145
151
  {...linkProps}
146
152
  onClick={() => {
@@ -154,7 +160,7 @@ const ContentTypeResult = ({
154
160
  )}
155
161
  {linkContent}
156
162
  {renderAdditionalIcon(t("resource.additionalTooltip"), additional)}
157
- </SafeLink>
163
+ </StyledSafeLink>
158
164
  </StyledListItem>
159
165
  );
160
166
  })}
@@ -79,6 +79,9 @@ const StyledSearchLink = styled(SafeLink)`
79
79
  color: ${colors.text.light};
80
80
  padding-left: ${spacing.xsmall};
81
81
  }
82
+ &[data-highlighted="true"] {
83
+ ${highlightStyle};
84
+ }
82
85
  `;
83
86
 
84
87
  type WrapperProps = {
@@ -304,7 +307,7 @@ const SearchResultSleeve = ({
304
307
  <div>
305
308
  <SearchLinkContainer>
306
309
  <StyledSearchLink
307
- css={keyboardPathNavigation === GO_TO_SEARCHPAGE && highlightStyle}
310
+ data-highlighted={keyboardPathNavigation === GO_TO_SEARCHPAGE}
308
311
  to={allResultUrl}
309
312
  tabIndex={-1}
310
313
  >
@@ -314,7 +317,7 @@ const SearchResultSleeve = ({
314
317
  </StyledSearchLink>
315
318
  {suggestion && suggestionUrl && (
316
319
  <StyledSearchLink
317
- css={keyboardPathNavigation === GO_TO_SUGGESTION && highlightStyle}
320
+ data-highlighted={keyboardPathNavigation === GO_TO_SUGGESTION}
318
321
  to={suggestionUrl}
319
322
  tabIndex={-1}
320
323
  >
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2024-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 { forwardRef, useState } from "react";
10
+ import { Button, ButtonProps } from "@ndla/primitives";
11
+
12
+ // TODO: Let's consider abandoning `disabled` on the button here. It should instead just open/close the widget based on its current state.
13
+
14
+ export interface ZendeskButtonProps extends ButtonProps {
15
+ widgetKey: string;
16
+ locale: string;
17
+ }
18
+
19
+ declare global {
20
+ interface Window {
21
+ zE: (modifier: string, action: string, callback?: (() => void) | string) => void;
22
+ }
23
+ }
24
+
25
+ export const ZendeskButton = forwardRef<HTMLButtonElement, ZendeskButtonProps>(
26
+ ({ locale, variant = "secondary", widgetKey, children, ...rest }, ref) => {
27
+ const [loading, setLoading] = useState(false);
28
+ const handleClick = () => {
29
+ if (window && !window.zE) {
30
+ setLoading(true);
31
+ // Asynchronously load zendesk scripts for better performance
32
+ const script = document.createElement("script");
33
+ script.id = "ze-snippet";
34
+ script.type = "text/javascript";
35
+ script.async = true;
36
+ script.onload = () => {
37
+ if (window.zE) {
38
+ window.zE("webWidget", "setLocale", locale);
39
+ window.zE("webWidget:on", "close", () => {
40
+ setLoading(false);
41
+ });
42
+ window.zE("webWidget", "open");
43
+ }
44
+ };
45
+ script.src = `https://static.zdassets.com/ekr/snippet.js?key=${widgetKey}`;
46
+ document.body.appendChild(script);
47
+ } else if (window?.zE) {
48
+ window.zE("webWidget", "open");
49
+ }
50
+ };
51
+
52
+ return (
53
+ <Button onClick={handleClick} variant={variant} id="zendeskButton" disabled={loading} {...rest} ref={ref}>
54
+ {children}
55
+ </Button>
56
+ );
57
+ },
58
+ );
package/src/index.ts CHANGED
@@ -172,3 +172,7 @@ export { LinkBlock, LinkBlockSection } from "./LinkBlock";
172
172
  export type { Article as ArticleType } from "./types";
173
173
 
174
174
  export { CodeBlock, codeLanguageOptions } from "./CodeBlock";
175
+
176
+ export { ZendeskButton } from "./ZendeskButton/ZendeskButton";
177
+
178
+ export type { ZendeskButtonProps } from "./ZendeskButton/ZendeskButton";
@@ -822,6 +822,7 @@ const messages = {
822
822
  url: "Error loading the audio.",
823
823
  caption: "Sorry, an error occurred while loading the audio.",
824
824
  },
825
+ valueText: "{{start}} of {{end}}",
825
826
  controls: {
826
827
  forward15sec: "Forward 15 seconds",
827
828
  rewind15sec: "Rewind 15 seconds",
@@ -822,6 +822,7 @@ const messages = {
822
822
  url: "Feil ved lasting av lydfil.",
823
823
  caption: "Beklager, en feil oppstod ved lasting av lydfil.",
824
824
  },
825
+ valueText: "{{start}} av {{end}}",
825
826
  controls: {
826
827
  forward15sec: "Spol 15 sekunder fram",
827
828
  rewind15sec: "Spol 15 sekunder tilbake",
@@ -822,6 +822,7 @@ const messages = {
822
822
  url: "Feil ved lasting av lydfil.",
823
823
  caption: "Orsak, ein feil oppstod ved lasting av lydfil.",
824
824
  },
825
+ valueText: "{{start}} av {{end}}",
825
826
  controls: {
826
827
  forward15sec: "Spol 15 sekund fram",
827
828
  rewind15sec: "Spol 15 sekund tilbake",