@ndla/ui 35.1.1 → 36.0.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 (47) hide show
  1. package/es/Embed/RelatedContentEmbed.js +2 -2
  2. package/es/NDLAFilm/FilmContentCard.js +21 -11
  3. package/es/NDLAFilm/FilmMovieList.js +2 -2
  4. package/es/NDLAFilm/FilmSlideshow.js +169 -335
  5. package/es/RelatedArticleList/RelatedArticleV2.js +27 -6
  6. package/es/Search/ToggleSearchButton.js +7 -2
  7. package/es/locale/messages-en.js +1 -0
  8. package/es/locale/messages-nb.js +2 -1
  9. package/es/locale/messages-nn.js +2 -1
  10. package/es/locale/messages-se.js +1 -0
  11. package/es/locale/messages-sma.js +2 -1
  12. package/lib/Embed/RelatedContentEmbed.js +2 -2
  13. package/lib/NDLAFilm/FilmContentCard.d.ts +4 -2
  14. package/lib/NDLAFilm/FilmContentCard.js +21 -12
  15. package/lib/NDLAFilm/FilmMovieList.d.ts +1 -1
  16. package/lib/NDLAFilm/FilmMovieList.js +2 -2
  17. package/lib/NDLAFilm/FilmSlideshow.d.ts +11 -5
  18. package/lib/NDLAFilm/FilmSlideshow.js +169 -333
  19. package/lib/RelatedArticleList/RelatedArticleV2.d.ts +4 -3
  20. package/lib/RelatedArticleList/RelatedArticleV2.js +35 -14
  21. package/lib/Search/ToggleSearchButton.js +7 -2
  22. package/lib/locale/messages-en.d.ts +1 -0
  23. package/lib/locale/messages-en.js +1 -0
  24. package/lib/locale/messages-nb.d.ts +1 -0
  25. package/lib/locale/messages-nb.js +2 -1
  26. package/lib/locale/messages-nn.d.ts +1 -0
  27. package/lib/locale/messages-nn.js +2 -1
  28. package/lib/locale/messages-se.d.ts +1 -0
  29. package/lib/locale/messages-se.js +1 -0
  30. package/lib/locale/messages-sma.d.ts +1 -0
  31. package/lib/locale/messages-sma.js +2 -1
  32. package/package.json +5 -5
  33. package/src/Embed/AudioEmbed.stories.tsx +226 -0
  34. package/src/Embed/BrightcoveEmbed.stories.tsx +209 -0
  35. package/src/Embed/ConceptEmbed.stories.tsx +190 -0
  36. package/src/Embed/ImageEmbed.stories.tsx +106 -0
  37. package/src/Embed/RelatedContentEmbed.tsx +1 -1
  38. package/src/NDLAFilm/FilmContentCard.tsx +11 -9
  39. package/src/NDLAFilm/FilmMovieList.tsx +2 -2
  40. package/src/NDLAFilm/FilmSlideshow.tsx +178 -387
  41. package/src/RelatedArticleList/RelatedArticleV2.tsx +24 -7
  42. package/src/Search/ToggleSearchButton.tsx +5 -1
  43. package/src/locale/messages-en.ts +1 -0
  44. package/src/locale/messages-nb.ts +2 -1
  45. package/src/locale/messages-nn.ts +2 -1
  46. package/src/locale/messages-se.ts +1 -0
  47. package/src/locale/messages-sma.ts +2 -1
@@ -1,436 +1,227 @@
1
1
  /**
2
- * Copyright (c) 2016-present, NDLA.
2
+ * Copyright (c) 2023-present, NDLA.
3
3
  *
4
4
  * This source code is licensed under the GPLv3 license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  */
8
8
 
9
- import React, { useCallback, useEffect, useRef, useState } from 'react';
10
- import { SwipeDirections, SwipeEventData, useSwipeable } from 'react-swipeable';
9
+ import React, { useCallback, useState } from 'react';
11
10
  import styled from '@emotion/styled';
12
- import { css } from '@emotion/react';
13
- import { breakpoints, mq, spacing, spacingUnit, fonts, colors } from '@ndla/core';
11
+ import { Carousel, CarouselAutosize } from '@ndla/carousel';
12
+ import { breakpoints, colors, misc, mq, spacing, spacingUnit } from '@ndla/core';
14
13
  import SafeLink from '@ndla/safelink';
15
- import { Spinner } from '@ndla/icons';
16
- import { OneColumn } from '../Layout';
17
- import NavigationArrow, { StyledNavigationArrow } from './NavigationArrow';
18
- import SlideshowIndicator from './SlideshowIndicator';
14
+ import { IconButtonV2 } from '@ndla/button';
15
+ import { ChevronLeft, ChevronRight } from '@ndla/icons/common';
16
+ import FilmContentCard from './FilmContentCard';
19
17
  import { MovieType } from './types';
20
18
 
19
+ export const slideshowBreakpoints: {
20
+ until?: keyof typeof breakpoints;
21
+ columnsPrSlide: number;
22
+ distanceBetweenItems: number;
23
+ arrowOffset: number;
24
+ margin?: number;
25
+ maxColumnWidth?: number;
26
+ }[] = [
27
+ {
28
+ until: 'mobileWide',
29
+ columnsPrSlide: 2,
30
+ distanceBetweenItems: spacingUnit / 2,
31
+ margin: spacingUnit,
32
+ arrowOffset: 13,
33
+ },
34
+ {
35
+ until: 'tabletWide',
36
+ columnsPrSlide: 3,
37
+ distanceBetweenItems: spacingUnit / 2,
38
+ margin: spacingUnit,
39
+ arrowOffset: 13,
40
+ },
41
+ {
42
+ until: 'desktop',
43
+ columnsPrSlide: 3,
44
+ distanceBetweenItems: spacingUnit,
45
+ margin: spacingUnit * 2,
46
+ arrowOffset: 0,
47
+ },
48
+ {
49
+ until: 'wide',
50
+ columnsPrSlide: 3,
51
+ distanceBetweenItems: spacingUnit,
52
+ margin: spacingUnit * 2,
53
+ arrowOffset: 0,
54
+ },
55
+ {
56
+ until: 'ultraWide',
57
+ columnsPrSlide: 3,
58
+ distanceBetweenItems: spacingUnit,
59
+ margin: spacingUnit * 3.5,
60
+ arrowOffset: 0,
61
+ },
62
+ {
63
+ columnsPrSlide: 3,
64
+ distanceBetweenItems: spacingUnit,
65
+ margin: spacingUnit * 3.5,
66
+ arrowOffset: 0,
67
+ },
68
+ ];
69
+
21
70
  interface Props {
22
- autoSlide?: boolean;
23
- randomStart?: boolean;
24
71
  slideshow: MovieType[];
25
- slideInterval?: number;
26
72
  }
27
73
 
28
- const SlideLinkWrapper = styled.div`
29
- margin: 0 auto;
30
- display: flex;
31
- justify-content: flex-start;
32
- align-items: flex-end;
74
+ const SlideInfoWrapper = styled.div`
33
75
  position: absolute;
34
- z-index: 2;
35
- height: 100vw;
36
- width: 100%;
37
- ${mq.range({ from: breakpoints.mobileWide })} {
38
- height: 100vw;
39
- }
40
- ${mq.range({ from: breakpoints.tablet })} {
41
- height: 75vw;
42
- }
43
- ${mq.range({ from: breakpoints.desktop })} {
44
- height: 55vw;
45
- }
46
- ${mq.range({ from: breakpoints.wide })} {
47
- height: 40vw;
48
- }
49
- ${mq.range({ from: breakpoints.ultraWide })} {
50
- height: 36vw;
76
+ color: ${colors.white};
77
+ max-width: 40%;
78
+ min-width: 40%;
79
+ top: 40%;
80
+ right: 5%;
81
+ ${mq.range({ until: breakpoints.desktop })} {
82
+ top: 30%;
83
+ max-width: 60%;
84
+ min-width: 60%;
85
+ }
86
+ ${mq.range({ until: breakpoints.tablet })} {
87
+ max-width: 90%;
88
+ min-width: 90%;
89
+ left: 5%;
51
90
  }
52
91
  `;
53
92
 
54
- const itemWrapperCSS = css`
55
- display: flex;
93
+ const StyledSafeLink = styled(SafeLink)`
94
+ position: relative;
95
+ display: block;
56
96
  box-shadow: none;
57
97
  `;
58
98
 
59
- interface SlideshowItemProps {
60
- fadeOver?: boolean;
61
- }
62
-
63
- const SlideshowItem = styled.div<SlideshowItemProps>`
64
- width: 100vw;
65
- height: 100vw;
66
- /* aspect ratios */
67
- ${mq.range({ from: breakpoints.mobileWide })} {
68
- height: 100vw;
69
- }
70
- ${mq.range({ from: breakpoints.tablet })} {
71
- height: 75vw;
72
- }
73
- ${mq.range({ from: breakpoints.desktop })} {
74
- height: 55vw;
75
- }
76
- ${mq.range({ from: breakpoints.wide })} {
77
- height: 40vw;
78
- }
79
- ${mq.range({ from: breakpoints.ultraWide })} {
80
- height: 36vw;
81
- }
82
- background-color: '#222';
83
- background-size: cover;
84
- background-position-x: center;
85
- background-position-y: center;
86
- border: 0;
87
- position: ${(props) => (props.fadeOver ? 'absolute' : 'relative')};
88
- animation: ${(props) => props.fadeOver && 'fadeIn 400ms ease'};
89
- z-index: ${(props) => props.fadeOver && 1};
90
- &:before {
91
- content: '';
92
- opacity: 0.4;
93
- background: #091a2a;
94
- top: 0;
95
- left: 0;
96
- bottom: 0;
97
- right: 0;
98
- position: absolute;
99
- z-index: 1;
99
+ const InfoWrapper = styled.div`
100
+ padding: ${spacing.normal};
101
+ border-radius: ${misc.borderRadius};
102
+ border: 0.5px solid ${colors.brand.primary};
103
+ background-color: rgba(11, 29, 45, 0.8);
104
+ h3 {
105
+ margin: 0px;
100
106
  }
101
107
  `;
102
108
 
103
- interface SlideshowLinkProps {
104
- out?: boolean;
105
- }
106
-
107
- const shouldForwardProp = (p: string) => p !== 'out';
108
-
109
- const SlideshowLink = styled(SafeLink, { shouldForwardProp })<SlideshowLinkProps>`
110
- display: flex;
111
- box-shadow: none;
112
- transition: all 400ms ease;
113
- opacity: ${(props) => props.out && 0};
114
- animation: ${(props) => !props.out && 'fadeInBottomFixed 600ms ease'};
115
- ${mq.range({ from: breakpoints.mobileWide })} {
116
- padding-bottom: ${spacing.medium};
117
- }
118
- ${mq.range({ from: breakpoints.tablet })} {
119
- padding-bottom: ${spacing.large};
120
- }
121
- ${mq.range({ from: breakpoints.desktop })} {
122
- padding-bottom: ${spacingUnit * 3}px;
123
- }
124
- &:hover {
125
- ${() => SlideshowName} {
126
- text-decoration: underline;
127
- text-decoration-color: white;
128
- }
129
- }
130
- `;
131
-
132
- const SlideshowWrapper = styled.section`
109
+ const StyledImg = styled.img`
110
+ max-height: 600px;
111
+ object-position: top;
133
112
  width: 100%;
134
- overflow: hidden;
135
- &:hover {
136
- ${StyledNavigationArrow} {
137
- opacity: 1;
138
- transform: translate(0, 0);
139
- }
113
+ aspect-ratio: 16/9;
114
+ ${mq.range({ until: breakpoints.tablet })} {
115
+ min-height: 440px;
116
+ max-height: 440px;
140
117
  }
118
+ object-fit: cover;
141
119
  `;
142
120
 
143
- const SlideshowInfo = styled.div`
144
- display: flex;
145
- flex-direction: column;
146
- gap: ${spacing.small};
147
- background-color: rgba(3, 23, 43, 0.7);
148
- border-radius: 4px;
149
- padding: ${spacing.medium} ${spacing.medium} ${spacing.medium} ${spacing.normal};
150
- margin: 0 -20px;
151
- width: 100vw;
152
- ${mq.range({ from: breakpoints.mobileWide })} {
153
- margin: 0;
154
- width: 100%;
155
- padding: ${spacing.medium} ${spacingUnit * 2}px ${spacing.medium} ${spacing.normal};
156
- }
157
- `;
158
-
159
- const SlideshowName = styled.p`
160
- ${fonts.sizes('22px', '30px')};
161
- color: ${colors.white};
162
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
163
- margin: 0;
164
- font-weight: ${fonts.weight.semibold};
165
- ${mq.range({ from: breakpoints.mobileWide })} {
166
- ${fonts.sizes('26px', '30px')};
167
- }
121
+ const CarouselContainer = styled.div`
122
+ margin-top: -50px;
168
123
  ${mq.range({ from: breakpoints.tablet })} {
169
- ${fonts.sizes('40px', '44px')};
124
+ margin-top: -70px;
170
125
  }
171
126
  ${mq.range({ from: breakpoints.desktop })} {
172
- ${fonts.sizes('48px', '54px')};
127
+ margin-top: -150px;
173
128
  }
174
129
  `;
175
130
 
176
- const SlideshowDescription = styled.p`
177
- color: ${colors.white};
178
- margin: 0;
179
- padding: 0;
180
- ${fonts.sizes('12px', '18px')};
181
- ${mq.range({ from: breakpoints.mobileWide })} {
182
- ${fonts.sizes('15px', '20px')};
183
- }
184
- ${mq.range({ from: breakpoints.tablet })} {
185
- ${fonts.sizes('18px', '24px')};
186
- }
187
- ${mq.range({ from: breakpoints.wide })} {
188
- ${fonts.sizes('20px', '32px')};
189
- }
190
- `;
131
+ interface StyledFilmContentCardProps {
132
+ current?: boolean;
133
+ }
191
134
 
192
- const EmptySlideshow = styled.div`
193
- background: rgba(255, 255, 255, 0.08);
194
- margin-bottom: $spacing--large * 4;
195
- display: flex;
196
- align-items: center;
197
- justify-content: center;
198
- height: 40vw;
135
+ const SlideshowButton = styled(IconButtonV2)`
136
+ margin-top: ${spacing.normal};
199
137
  `;
200
138
 
201
- const defaultTransitionSwipeEnd = 'transform 600ms cubic-bezier(0, 0.76, 0.09, 1)';
202
- const defaultTransitionText = 'opacity 600ms ease';
203
-
204
- const renderSlideItem = (slide: MovieType) => (
205
- <SlideshowItem
206
- key={slide.id}
207
- role="img"
208
- aria-label={(slide.metaImage && slide.metaImage.alt) || ''}
209
- style={{
210
- backgroundImage: `url(${(slide.metaImage && slide.metaImage.url) || ''})`,
211
- }}
212
- />
213
- );
214
-
215
- const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000 }: Props) => {
216
- const [swipeDistance, setSwipeDistance] = useState(0);
217
- const [slideIndex, setSlideIndex] = useState(0);
218
- const [slideIndexTarget, setSlideIndexTarget] = useState(0);
219
- const [animationComplete, setAnimationComplete] = useState(true);
220
- const [swipeDirection, setSwipeDirection] = useState<SwipeDirections | undefined>(undefined);
221
- const slideRef = useRef<HTMLDivElement>(null);
222
- const slideText = useRef<HTMLDivElement>(null);
223
- const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
224
-
225
- const gotoSlide = useCallback((indexTarget: number, useAnimation = false) => {
226
- setSwipeDistance(0);
227
- if (timer.current) {
228
- clearTimeout(timer.current);
229
- }
230
- setSlideIndexTarget(indexTarget);
231
- setAnimationComplete(!useAnimation);
232
- }, []);
233
-
234
- const onChangedSlide = () => {
235
- if (!animationComplete) {
236
- if (slideRef.current) {
237
- slideRef.current.style.transition = 'none';
238
- slideRef.current.style.transform = `translateX(${slideIndexTarget * 100}vw))`;
239
- }
139
+ const shouldForwardProp = (p: string) => p !== 'current';
240
140
 
241
- setAnimationComplete(true);
242
- setSlideIndex(slideIndexTarget);
243
- } else if (slideIndexTarget === -1) {
244
- if (slideRef.current) {
245
- // Go to last slide for continuous loop
246
- slideRef.current.style.transition = 'none';
247
- slideRef.current.style.transform = `translateX(${slideshow.length * 100}vw))`;
248
- }
249
-
250
- setSlideIndex(slideshow.length - 1);
251
- setSlideIndexTarget(slideshow.length - 1);
252
- setAnimationComplete(true);
253
- } else if (slideIndexTarget === slideshow.length) {
254
- if (slideRef.current) {
255
- // Go to first slide for continuous loop
256
- slideRef.current.style.transition = 'none';
257
- slideRef.current.style.transform = `translateX(100vw))`;
258
- }
259
- setSlideIndex(0);
260
- setSlideIndexTarget(0);
261
- setAnimationComplete(true);
262
- } else {
263
- setAnimationComplete(true);
264
- setSlideIndex(slideIndexTarget);
265
- }
266
- };
267
-
268
- const onSwipeEnd = () => {
269
- setSwipeDirection(undefined);
270
- let slide;
271
- if (swipeDistance > 40) {
272
- slide = -1;
273
- } else if (swipeDistance < -40) {
274
- slide = 1;
275
- } else {
276
- slide = 0;
277
- }
278
- if (slideRef.current && slideText.current) {
279
- slideRef.current.style.transition = defaultTransitionSwipeEnd;
280
- slideText.current.style.transition = defaultTransitionText;
281
- slideText.current.style.opacity = '1';
282
- }
283
- setSwipeDistance(0);
284
-
285
- initTimer();
286
-
287
- if (slide !== 0) {
288
- setSlideIndex(slideIndex + slide);
289
- setSlideIndexTarget(slideIndex + slide);
290
- } else {
291
- // Reset transfrom
292
- if (slideRef.current) {
293
- slideRef.current.style.transform = getSlidePosition(slideIndex + slide);
294
- }
295
- }
296
- };
297
-
298
- const onSwipe = (eventData: SwipeEventData) => {
299
- if (eventData.initial) {
300
- setSwipeDirection(eventData.dir);
301
- }
302
- const dir = eventData.initial ? eventData.dir : swipeDirection;
303
- if (dir === 'Up' || dir === 'Down') {
304
- return;
305
- }
306
- if (timer.current) {
307
- clearTimeout(timer.current);
308
- }
309
- setSwipeDistance(eventData.deltaX);
310
- if (slideRef && slideRef.current) {
311
- slideRef.current.style.transition = 'none';
312
- slideRef.current.style.transform = getSlidePosition(slideIndexTarget);
313
- }
314
- const opacityText = 1 - Math.min(100, Math.abs(swipeDistance)) / 100;
315
- if (slideText && slideText.current) {
316
- slideText.current.style.transition = 'none';
317
- slideText.current.style.opacity = opacityText.toString();
318
- }
319
- };
320
-
321
- const onTransitionEnd = () => {
322
- const slideshowLength = slideshow.length;
323
- if (slideIndex === -1) {
324
- if (slideRef.current) {
325
- slideRef.current.style.transition = 'none';
326
- slideRef.current.style.transform = getSlidePosition(slideshowLength - 1);
327
- }
328
- setSlideIndex(slideshowLength - 1);
329
- setSlideIndexTarget(slideshowLength - 1);
330
- } else if (slideIndex >= slideshowLength) {
331
- if (slideRef.current) {
332
- slideRef.current.style.transition = 'none';
333
- slideRef.current.style.transform = getSlidePosition(0);
334
- }
335
- setSlideIndex(0);
336
- setSlideIndexTarget(0);
337
- }
338
- };
339
-
340
- const getSlidePosition = (target: number) => {
341
- if (swipeDistance !== 0) {
342
- return `translateX(calc(${swipeDistance}px -
343
- ${(target + 1) * 100}vw))`;
344
- }
345
- return `translateX(-${(target + 1) * 100}vw)`;
346
- };
347
-
348
- const initTimer = useCallback(() => {
349
- if (autoSlide) {
350
- timer.current = setTimeout(() => {
351
- gotoSlide(slideIndex + 1);
352
- }, slideInterval);
353
- }
354
- }, [autoSlide, gotoSlide, slideInterval, slideIndex]);
141
+ const StyledFilmContentCard = styled(FilmContentCard, { shouldForwardProp })<StyledFilmContentCardProps>`
142
+ margin-bottom: 2%;
143
+ transform: ${(p) => (p.current ? 'translateY(0%)' : 'translateY(10%)')};
144
+ transition: all 200ms;
145
+ `;
355
146
 
356
- useEffect(() => {
357
- initTimer();
358
- }, [initTimer]);
147
+ const FilmSlideshow = ({ slideshow }: Props) => {
148
+ const [currentSlide, setCurrentSlide] = useState(slideshow[0]);
359
149
 
360
- const handlers = useSwipeable({
361
- onSwiped: onSwipeEnd,
362
- onSwiping: onSwipe,
363
- preventScrollOnSwipe: swipeDirection === 'Left' || swipeDirection === 'Right',
364
- });
150
+ return (
151
+ <CarouselAutosize breakpoints={slideshowBreakpoints} itemsLength={slideshow.length}>
152
+ {(autoSizedProps) => (
153
+ <section>
154
+ <StyledSafeLink to={currentSlide.path} tabIndex={-1} aria-hidden>
155
+ <StyledImg src={currentSlide.metaImage?.url ?? ''} alt={currentSlide.metaImage?.alt ?? ''} />
156
+ <SlideInfoWrapper>
157
+ <InfoWrapper>
158
+ <h3>{currentSlide.title}</h3>
159
+ <span id="currentMovieDescription">{currentSlide.metaDescription}</span>
160
+ </InfoWrapper>
161
+ </SlideInfoWrapper>
162
+ </StyledSafeLink>
163
+ <CarouselContainer>
164
+ <Carousel
165
+ leftButton={
166
+ <SlideshowButton aria-label={''}>
167
+ <ChevronLeft />
168
+ </SlideshowButton>
169
+ }
170
+ rightButton={
171
+ <SlideshowButton aria-label={''}>
172
+ <ChevronRight />
173
+ </SlideshowButton>
174
+ }
175
+ items={slideshow.map((movie) => (
176
+ <FilmCard
177
+ key={movie.id}
178
+ current={movie.id === currentSlide.id}
179
+ movie={movie}
180
+ columnWidth={autoSizedProps.columnWidth}
181
+ setCurrentSlide={() => setCurrentSlide(movie)}
182
+ />
183
+ ))}
184
+ {...autoSizedProps}
185
+ />
186
+ </CarouselContainer>
187
+ </section>
188
+ )}
189
+ </CarouselAutosize>
190
+ );
191
+ };
365
192
 
366
- if (slideshow.length === 0) {
367
- return (
368
- <div>
369
- <EmptySlideshow>
370
- <Spinner inverted />
371
- </EmptySlideshow>
372
- </div>
373
- );
374
- }
193
+ interface FilmCardProps {
194
+ setCurrentSlide: () => void;
195
+ movie: MovieType;
196
+ current: boolean;
197
+ columnWidth: number;
198
+ }
375
199
 
376
- const slideshowWidth = `${(slideshow.length + 2) * 100}vw`;
377
- let activeSlide = slideIndex;
378
- if (activeSlide < 0) {
379
- activeSlide = slideshow.length - 1;
380
- } else if (activeSlide >= slideshow.length) {
381
- activeSlide = 0;
382
- }
200
+ const FilmCard = ({ setCurrentSlide, movie, current, columnWidth }: FilmCardProps) => {
201
+ const [hoverCallback, setHoverCallback] = useState<ReturnType<typeof setTimeout> | undefined>(undefined);
383
202
 
384
- const backgroundImage = slideshow[activeSlide].metaImage;
203
+ const onHover = useCallback(() => {
204
+ const timeout = setTimeout(() => setCurrentSlide(), 500);
205
+ setHoverCallback(timeout);
206
+ }, [setCurrentSlide]);
385
207
 
386
208
  return (
387
- <SlideshowWrapper {...handlers}>
388
- <>
389
- <SlideLinkWrapper>
390
- <OneColumn>
391
- <SlideshowLink to={slideshow[activeSlide].path} out={!animationComplete}>
392
- <SlideshowInfo ref={slideText}>
393
- <SlideshowName>{slideshow[activeSlide].title}</SlideshowName>
394
- <SlideshowDescription>{slideshow[activeSlide].metaDescription}</SlideshowDescription>
395
- </SlideshowInfo>
396
- </SlideshowLink>
397
- </OneColumn>
398
- </SlideLinkWrapper>
399
- <NavigationArrow
400
- slideIndexTarget={slideIndexTarget > 0 ? slideIndexTarget - 1 : slideshow.length - 1}
401
- gotoSlide={gotoSlide}
402
- />
403
- <NavigationArrow
404
- slideIndexTarget={slideIndexTarget < slideshow.length - 1 ? slideIndexTarget + 1 : 0}
405
- gotoSlide={gotoSlide}
406
- rightArrow
407
- />
408
- {!animationComplete && (
409
- <SlideshowItem
410
- fadeOver
411
- role="img"
412
- onAnimationEnd={onChangedSlide}
413
- style={{
414
- backgroundImage: `url(${(backgroundImage && backgroundImage.url) || ''})`,
415
- }}
416
- />
417
- )}
418
- <div
419
- ref={slideRef}
420
- css={itemWrapperCSS}
421
- onTransitionEnd={onTransitionEnd}
422
- style={{
423
- width: slideshowWidth,
424
- transform: getSlidePosition(slideIndex),
425
- }}
426
- >
427
- {renderSlideItem(slideshow[slideshow.length - 1])}
428
- {slideshow.map(renderSlideItem)}
429
- {renderSlideItem(slideshow[0])}
430
- </div>
431
- <SlideshowIndicator slideshow={slideshow} activeSlide={activeSlide} gotoSlide={gotoSlide} />
432
- </>
433
- </SlideshowWrapper>
209
+ <StyledFilmContentCard
210
+ onMouseEnter={onHover}
211
+ onMouseLeave={() => {
212
+ if (hoverCallback) {
213
+ clearTimeout(hoverCallback);
214
+ setHoverCallback(undefined);
215
+ }
216
+ }}
217
+ onFocus={() => setCurrentSlide()}
218
+ current={current}
219
+ aria-describedby={'currentMovieDescription'}
220
+ key={movie.id}
221
+ movie={movie}
222
+ columnWidth={columnWidth}
223
+ resourceTypes={[]}
224
+ />
434
225
  );
435
226
  };
436
227
 
@@ -6,9 +6,10 @@
6
6
  *
7
7
  */
8
8
 
9
- import { Children, useMemo, useState } from 'react';
9
+ import { Children, HTMLProps, ReactNode, useMemo, useState } from 'react';
10
10
  import BEMHelper from 'react-bem-helper';
11
11
  import { useTranslation } from 'react-i18next';
12
+ import styled from '@emotion/styled';
12
13
  import { ButtonV2 } from '@ndla/button';
13
14
  import SafeLink from '@ndla/safelink';
14
15
  import SectionHeading from '../SectionHeading';
@@ -54,12 +55,25 @@ export const RelatedArticleV2 = ({
54
55
  );
55
56
  };
56
57
 
57
- interface Props {
58
+ const HeadingWrapper = styled.div`
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ `;
63
+
64
+ interface Props extends HTMLProps<HTMLElement> {
58
65
  children?: JSX.Element[];
59
66
  articleCount?: number;
60
67
  headingLevel?: HeadingLevel;
68
+ headingButtons?: ReactNode;
61
69
  }
62
- export const RelatedArticleListV2 = ({ children = [], articleCount, headingLevel = 'h3' }: Props) => {
70
+ export const RelatedArticleListV2 = ({
71
+ children = [],
72
+ articleCount,
73
+ headingLevel = 'h3',
74
+ headingButtons,
75
+ ...rest
76
+ }: Props) => {
63
77
  const [expanded, setExpanded] = useState(false);
64
78
  const { t } = useTranslation();
65
79
  const childCount = useMemo(() => articleCount ?? Children.count(children), [children, articleCount]);
@@ -69,10 +83,13 @@ export const RelatedArticleListV2 = ({ children = [], articleCount, headingLevel
69
83
  );
70
84
 
71
85
  return (
72
- <section {...classes('')}>
73
- <SectionHeading headingLevel={headingLevel} className={classes('component-title').className}>
74
- {t('related.title')}
75
- </SectionHeading>
86
+ <section {...classes('')} {...rest}>
87
+ <HeadingWrapper>
88
+ <SectionHeading headingLevel={headingLevel} className={classes('component-title').className}>
89
+ {t('related.title')}
90
+ </SectionHeading>
91
+ {headingButtons}
92
+ </HeadingWrapper>
76
93
  <div {...classes('articles')}>{childrenToShow}</div>
77
94
  {childCount > 2 ? (
78
95
  <ButtonV2 onClick={() => setExpanded((p) => !p)} variant="outline">
@@ -24,7 +24,11 @@ interface StyledButtonProps {
24
24
  ndlaFilm?: boolean;
25
25
  }
26
26
 
27
- const StyledButton = styled(ButtonV2)<StyledButtonProps>`
27
+ const props = ['hideOnNarrowScreen', 'hideOnWideScreen', 'ndlaFilm'];
28
+
29
+ const shouldForwardProp = (p: string) => !props.includes(p);
30
+
31
+ const StyledButton = styled(ButtonV2, { shouldForwardProp })<StyledButtonProps>`
28
32
  background: ${(p) => (p.ndlaFilm ? colors.ndlaFilm.filmColorBright : colors.brand.greyLighter)};
29
33
  border-radius: ${misc.borderRadius};
30
34
  border: 0;
@@ -1185,6 +1185,7 @@ const messages = {
1185
1185
  drawerButton: 'Show folders and resources',
1186
1186
  drawerTitle: 'Folders and resources',
1187
1187
  description: {
1188
+ all: 'In this folder you find articles and tasks from NDLA. The articles have been collected and placed in order by a teacher.\n\nYou can use the menu to navigate through the articles.\n\nIf you want to come back to the folder later, you can use the link the teacher gave you, or you can bookmark the page.',
1188
1189
  info1:
1189
1190
  'In this folder you find articles and tasks from NDLA. The articles have been collected and placed in order by a teacher.',
1190
1191
  info2: 'You can use the menu to navigate through the articles.',