@ndla/ui 34.0.1 → 34.1.1

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 (91) hide show
  1. package/es/Figure/Figure.js +3 -2
  2. package/es/FileList/File.js +6 -6
  3. package/es/LanguageSelector/LanguageSelector.js +68 -99
  4. package/es/NDLAFilm/AboutNdlaFilm.js +13 -12
  5. package/es/NDLAFilm/AllMoviesAlphabetically.js +79 -144
  6. package/es/NDLAFilm/FilmContentCard.js +41 -26
  7. package/es/NDLAFilm/FilmContentCardTags.js +5 -3
  8. package/es/NDLAFilm/FilmMovieList.js +13 -8
  9. package/es/NDLAFilm/FilmMovieSearch.js +6 -5
  10. package/es/NDLAFilm/FilmSlideshow.js +44 -20
  11. package/es/NDLAFilm/filmStyles.js +2 -2
  12. package/es/ResourceGroup/ResourceItem.js +72 -48
  13. package/es/Search/ActiveFilterContent.js +6 -5
  14. package/es/Search/ContentTypeResult.js +6 -3
  15. package/es/SearchTypeResult/ActiveFilterContent.js +9 -10
  16. package/es/Topic/Topic.js +171 -213
  17. package/es/all.css +1 -1
  18. package/es/locale/messages-en.js +3 -1
  19. package/es/locale/messages-nb.js +3 -1
  20. package/es/locale/messages-nn.js +7 -5
  21. package/es/locale/messages-se.js +2 -0
  22. package/es/locale/messages-sma.js +3 -1
  23. package/lib/Figure/Figure.d.ts +2 -1
  24. package/lib/Figure/Figure.js +3 -2
  25. package/lib/FileList/File.js +6 -6
  26. package/lib/LanguageSelector/LanguageSelector.d.ts +6 -15
  27. package/lib/LanguageSelector/LanguageSelector.js +65 -99
  28. package/lib/NDLAFilm/AboutNdlaFilm.js +11 -14
  29. package/lib/NDLAFilm/AllMoviesAlphabetically.d.ts +1 -2
  30. package/lib/NDLAFilm/AllMoviesAlphabetically.js +77 -142
  31. package/lib/NDLAFilm/FilmContentCard.d.ts +7 -0
  32. package/lib/NDLAFilm/FilmContentCard.js +41 -26
  33. package/lib/NDLAFilm/FilmContentCardTags.d.ts +2 -1
  34. package/lib/NDLAFilm/FilmContentCardTags.js +5 -3
  35. package/lib/NDLAFilm/FilmMovieList.js +12 -7
  36. package/lib/NDLAFilm/FilmMovieSearch.js +5 -4
  37. package/lib/NDLAFilm/FilmSlideshow.js +44 -20
  38. package/lib/NDLAFilm/filmStyles.js +2 -2
  39. package/lib/Resource/resourceComponents.d.ts +1 -1
  40. package/lib/ResourceGroup/ResourceItem.d.ts +2 -2
  41. package/lib/ResourceGroup/ResourceItem.js +72 -48
  42. package/lib/Search/ActiveFilterContent.d.ts +1 -1
  43. package/lib/Search/ActiveFilterContent.js +9 -5
  44. package/lib/Search/ContentTypeResult.js +6 -3
  45. package/lib/SearchTypeResult/ActiveFilterContent.d.ts +1 -1
  46. package/lib/SearchTypeResult/ActiveFilterContent.js +12 -10
  47. package/lib/Topic/Topic.js +170 -215
  48. package/lib/all.css +1 -1
  49. package/lib/locale/messages-en.d.ts +2 -0
  50. package/lib/locale/messages-en.js +3 -1
  51. package/lib/locale/messages-nb.d.ts +2 -0
  52. package/lib/locale/messages-nb.js +3 -1
  53. package/lib/locale/messages-nn.d.ts +4 -2
  54. package/lib/locale/messages-nn.js +7 -5
  55. package/lib/locale/messages-se.d.ts +2 -0
  56. package/lib/locale/messages-se.js +2 -0
  57. package/lib/locale/messages-sma.d.ts +2 -0
  58. package/lib/locale/messages-sma.js +3 -1
  59. package/package.json +15 -14
  60. package/src/Figure/Figure.tsx +6 -2
  61. package/src/FileList/File.tsx +4 -4
  62. package/src/LanguageSelector/LanguageSelector.stories.tsx +48 -0
  63. package/src/LanguageSelector/LanguageSelector.tsx +71 -149
  64. package/src/NDLAFilm/AboutNdlaFilm.tsx +11 -14
  65. package/src/NDLAFilm/AllMoviesAlphabetically.tsx +44 -160
  66. package/src/NDLAFilm/FilmContentCard.tsx +40 -21
  67. package/src/NDLAFilm/FilmContentCardTags.tsx +3 -2
  68. package/src/NDLAFilm/FilmMovieList.tsx +14 -7
  69. package/src/NDLAFilm/FilmMovieSearch.tsx +2 -2
  70. package/src/NDLAFilm/FilmSlideshow.tsx +49 -40
  71. package/src/NDLAFilm/filmStyles.ts +1 -1
  72. package/src/ResourceGroup/ResourceItem.tsx +79 -94
  73. package/src/Search/ActiveFilterContent.tsx +4 -3
  74. package/src/Search/ContentTypeResult.tsx +3 -1
  75. package/src/SearchTypeResult/ActiveFilterContent.tsx +7 -8
  76. package/src/Topic/Topic.tsx +166 -193
  77. package/src/locale/messages-en.ts +3 -1
  78. package/src/locale/messages-nb.ts +3 -1
  79. package/src/locale/messages-nn.ts +5 -4
  80. package/src/locale/messages-se.ts +2 -0
  81. package/src/locale/messages-sma.ts +3 -1
  82. package/src/main.scss +0 -1
  83. package/es/LanguageSelector/LanguageSelectorContent.js +0 -61
  84. package/es/Subject/SubjectCarousel.js +0 -133
  85. package/lib/LanguageSelector/LanguageSelectorContent.d.ts +0 -15
  86. package/lib/LanguageSelector/LanguageSelectorContent.js +0 -68
  87. package/lib/Subject/SubjectCarousel.d.ts +0 -18
  88. package/lib/Subject/SubjectCarousel.js +0 -138
  89. package/src/LanguageSelector/LanguageSelectorContent.tsx +0 -80
  90. package/src/NDLAFilm/component.film-movielist.scss +0 -105
  91. package/src/Subject/SubjectCarousel.tsx +0 -162
@@ -1,10 +1,9 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import styled from '@emotion/styled';
4
- import Modal, { ModalHeader, ModalBody, ModalCloseButton } from '@ndla/modal';
5
- import { colors, spacing, fonts, mq, breakpoints } from '@ndla/core';
4
+ import { ModalHeader, ModalBody, ModalCloseButton, ModalV2 } from '@ndla/modal';
5
+ import { colors, spacing, mq, breakpoints } from '@ndla/core';
6
6
  import { ButtonV2 as Button } from '@ndla/button';
7
- import { OneColumn } from '..';
8
7
  import VisualElement from './VisualElement';
9
8
 
10
9
  const StyledAside = styled.aside`
@@ -15,9 +14,7 @@ const StyledAside = styled.aside`
15
14
  > div {
16
15
  padding: ${spacing.normal};
17
16
  width: 50%;
18
- h1 {
19
- @include font-size(22px, 26px);
20
- font-weight: ${fonts.weight.bold};
17
+ h2 {
21
18
  text-transform: uppercase;
22
19
  letter-spacing: 0.05em;
23
20
  color: #fff;
@@ -25,7 +22,6 @@ const StyledAside = styled.aside`
25
22
  }
26
23
  }
27
24
  button {
28
- text-align: left;
29
25
  color: #fff;
30
26
  &:hover,
31
27
  &:focus {
@@ -58,25 +54,26 @@ interface Props {
58
54
 
59
55
  const AboutNdlaFilm = ({ aboutNDLAVideo, moreAboutNdlaFilm }: Props) => {
60
56
  const { t } = useTranslation();
57
+ const titleId = 'about-ndla-film-title';
61
58
  return (
62
59
  <div className="o-wrapper">
63
- <StyledAside>
60
+ <StyledAside aria-labelledby={titleId}>
64
61
  <div>
65
62
  <VisualElement visualElement={aboutNDLAVideo.visualElement} />
66
63
  </div>
67
64
  <div>
68
- <h1>{aboutNDLAVideo.title}</h1>
65
+ <h2 id={titleId}>{aboutNDLAVideo.title}</h2>
69
66
  <p>{aboutNDLAVideo.description}</p>
70
- <Modal size="fullscreen" activateButton={<Button variant="link">{t('ndlaFilm.about.more')}</Button>}>
67
+ <ModalV2 size="full" activateButton={<Button variant="link">{t('ndlaFilm.about.more')}</Button>}>
71
68
  {(onClose) => (
72
- <OneColumn cssModifier="medium">
69
+ <>
73
70
  <ModalHeader>
74
- <ModalCloseButton onClick={onClose} title="Lukk" />
71
+ <ModalCloseButton onClick={onClose} />
75
72
  </ModalHeader>
76
73
  <ModalBody>{moreAboutNdlaFilm}</ModalBody>
77
- </OneColumn>
74
+ </>
78
75
  )}
79
- </Modal>
76
+ </ModalV2>
80
77
  </div>
81
78
  </StyledAside>
82
79
  </div>
@@ -6,22 +6,20 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { Fragment, RefObject, useEffect, useRef, useState } from 'react';
10
- import { isIE, browserVersion } from 'react-device-detect';
9
+ import React, { RefObject, useMemo, useRef } from 'react';
11
10
  import styled from '@emotion/styled';
12
- import { css } from '@emotion/react';
13
- import throttle from 'lodash/throttle';
14
- import { breakpoints, mq, spacing, spacingUnit, colors, fonts, animations } from '@ndla/core';
11
+ import { breakpoints, mq, spacing, spacingUnit, colors } from '@ndla/core';
15
12
  import SafeLink from '@ndla/safelink';
13
+ import groupBy from 'lodash/groupBy';
14
+ import sortBy from 'lodash/sortBy';
15
+ import { useTranslation } from 'react-i18next';
16
16
  import { makeSrcQueryString } from '../Image';
17
17
  import { MovieType } from './types';
18
- import { isLetter } from './isLetter';
18
+
19
19
  const IMAGE_WIDTH = 143;
20
20
 
21
- const StyledNewLetter = styled.h2`
22
- color: #fff;
23
- margin: ${spacing.large} 0 ${spacingUnit * 0.75}px;
24
- ${fonts.sizes(26, 1.1)};
21
+ const Letter = styled.h2`
22
+ color: ${colors.white};
25
23
  ${mq.range({ from: breakpoints.tablet })} {
26
24
  text-indent: ${spacingUnit * 0.75}px;
27
25
  }
@@ -39,29 +37,12 @@ const StyledWrapper = styled.section`
39
37
  max-width: 100%;
40
38
  margin: ${spacing.large} auto;
41
39
  padding: 0 ${spacing.normal};
42
- display: flex;
43
- flex-direction: column;
44
40
  `;
45
41
 
46
- type MovieItemProps = {
47
- inView: boolean | null;
48
- };
49
-
50
- const MovieItem = styled.div<MovieItemProps>`
51
- margin: 0 0 ${spacingUnit * 0.75}px;
52
- display: inline-flex;
53
- &:last-child {
54
- margin-bottom: ${spacing.large};
55
- }
56
- opacity: 0;
57
- transform: translateY(${spacing.xsmall});
58
- transition: all ${animations.durations.slow} ease;
59
- ${(props: MovieItemProps) =>
60
- props.inView &&
61
- css`
62
- opacity: 1;
63
- transform: translateY(0);
64
- `};
42
+ const MovieItem = styled.div`
43
+ display: flex;
44
+ gap: ${spacing.small};
45
+ color: ${colors.white};
65
46
  `;
66
47
 
67
48
  const MovieTextWrapper = styled.div`
@@ -71,57 +52,23 @@ const MovieTextWrapper = styled.div`
71
52
  flex: 1;
72
53
  `;
73
54
 
74
- type movieImageType = {
75
- backgroundImage?: string | null;
76
- };
77
-
78
- const MovieImage = styled.div<movieImageType>`
55
+ const MovieImage = styled.img`
79
56
  width: 104px;
80
57
  height: 80px;
81
- background-color: ${colors.ndlaFilm.filmColorLight};
82
- ${(props: movieImageType) =>
83
- props.backgroundImage !== null &&
84
- css`
85
- background-image: url(${props.backgroundImage});
86
- `}
87
- background-size: cover;
88
- background-position: center center;
89
- margin: 0 ${spacingUnit * 0.75}px 0 0;
90
58
  ${mq.range({ from: breakpoints.tablet })} {
91
- margin-left: ${spacingUnit * 0.75}px;
92
59
  width: ${IMAGE_WIDTH}px;
93
60
  height: 90px;
94
61
  }
95
- position: relative;
96
- &:after {
97
- content: '';
98
- position: absolute;
99
- z-index: 1;
100
- background: ${colors.ndlaFilm.filmColor};
101
- opacity: 0;
102
- left: 0;
103
- right: 0;
104
- bottom: 0;
105
- top: 0;
106
- }
107
62
  `;
108
63
 
109
64
  const MovieTitle = styled.h3`
110
- color: #fff;
111
- margin: 0 0 2px;
112
- ${fonts.sizes(18, 1.3)};
113
- ${mq.range({ from: breakpoints.tablet })} {
114
- ${fonts.sizes(20, 1.3)};
115
- }
65
+ color: ${colors.white};
66
+ margin: 0;
116
67
  `;
117
68
 
118
69
  const MovieDescription = styled.p`
119
70
  color: ${colors.brand.greyLighter};
120
71
  margin: 0;
121
- ${fonts.sizes(14, 1.5)};
122
- ${mq.range({ from: breakpoints.tablet })} {
123
- ${fonts.sizes(16, 1.5)};
124
- }
125
72
  overflow: hidden;
126
73
  text-overflow: ellipsis;
127
74
  display: -webkit-box;
@@ -129,18 +76,10 @@ const MovieDescription = styled.p`
129
76
  -webkit-line-clamp: 2;
130
77
  `;
131
78
 
132
- type isIEProps = {
133
- isIE11: boolean;
134
- };
135
-
136
- const StyledSafeLink = styled(SafeLink)<isIEProps>`
79
+ const StyledSafeLink = styled(SafeLink)`
137
80
  box-shadow: none;
138
81
  display: flex;
139
- ${(props) =>
140
- props.isIE11 &&
141
- css`
142
- flex: 1;
143
- `}
82
+ gap: ${spacing.small};
144
83
  &:hover,
145
84
  &:focus {
146
85
  ${MovieTitle} {
@@ -158,97 +97,42 @@ const StyledSafeLink = styled(SafeLink)<isIEProps>`
158
97
 
159
98
  interface Props {
160
99
  movies: MovieType[];
161
- locale: string;
162
100
  }
163
101
 
164
- type visibleImagesProps = {
165
- [key: string]: boolean | null;
166
- };
102
+ const MovieGroup = styled.section`
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: ${spacing.normal};
106
+ `;
167
107
 
168
- const hasForEachPolyfill = () => {
169
- // Polyfill for ie11
170
- if ('NodeList' in window && !NodeList.prototype.forEach) {
171
- NodeList.prototype.forEach = function (callback, thisArg) {
172
- thisArg = thisArg || window;
173
- for (let i = 0; i < this.length; i++) {
174
- callback.call(thisArg, this[i], i, this);
175
- }
176
- };
177
- }
108
+ const groupMovies = (movies: MovieType[]) => {
109
+ const sortedMovies = sortBy(movies, (m) => m.title);
110
+ const grouped = groupBy(sortedMovies, (movie) => {
111
+ const firstChar = movie.title[0]?.toUpperCase();
112
+ const isLetter = firstChar?.match(/[A-Z\WÆØÅ]+/);
113
+ return isLetter ? firstChar : '#';
114
+ });
115
+ return Object.entries(grouped).map(([letter, movies]) => ({ letter, movies }));
178
116
  };
179
117
 
180
- const AllMoviesAlphabetically = ({ movies, locale }: Props) => {
181
- const isIE11 = isIE && parseInt(browserVersion) < 12;
118
+ const AllMoviesAlphabetically = ({ movies }: Props) => {
119
+ const groupedMovies = useMemo(() => groupMovies(movies), [movies]);
120
+ const { t } = useTranslation();
182
121
  // Split into Letters.
183
- let previousLetter = '';
184
122
  const wrapperRef: RefObject<HTMLElement> = useRef(null);
185
- const [visibleImages, setVisibleImages] = useState<visibleImagesProps>({});
186
-
187
- const scrollEvent = () => {
188
- hasForEachPolyfill();
189
- const updates: visibleImagesProps = {};
190
- const allChildren: NodeListOf<HTMLElement> | null =
191
- wrapperRef.current && wrapperRef.current.querySelectorAll('[role=img]');
192
- const windowInnerHeight = window.innerHeight;
193
- if (allChildren) {
194
- let started: boolean = false;
195
- let ended: boolean = false;
196
- allChildren.forEach((el: HTMLElement, index: number) => {
197
- if (!ended) {
198
- const rect: ClientRect = el.getBoundingClientRect();
199
- if (!started) {
200
- if (rect.top > -20) {
201
- updates[index] = true;
202
- started = true;
203
- }
204
- } else {
205
- updates[index] = true;
206
- if (rect.top > windowInnerHeight + 20) {
207
- ended = true;
208
- }
209
- }
210
- }
211
- });
212
- }
213
- setVisibleImages((visibleImages) => ({ ...visibleImages, ...updates }));
214
- };
215
-
216
- useEffect(() => {
217
- const throttledScrollEvent = throttle(() => {
218
- scrollEvent();
219
- }, 100);
220
- window.addEventListener('scroll', throttledScrollEvent);
221
- scrollEvent();
222
- return () => {
223
- window.removeEventListener('scroll', throttledScrollEvent);
224
- };
225
- }, []);
226
-
227
- useEffect(() => {
228
- scrollEvent();
229
- }, [movies]);
230
123
 
231
124
  return (
232
125
  <StyledWrapper ref={wrapperRef}>
233
- {movies.map((movie: MovieType, index: number) => {
234
- const currentLetter = movie.title.substr(0, 1);
235
- const isNewLetter = currentLetter.localeCompare(previousLetter, locale) === 1 && isLetter(movie.title);
236
- previousLetter = currentLetter;
237
- const inView: boolean | null = visibleImages ? visibleImages[index] : null;
238
- return (
239
- <Fragment key={movie.id}>
240
- {isNewLetter && <StyledNewLetter>{movie.title.substr(0, 1)}</StyledNewLetter>}
241
- <MovieItem inView={inView}>
242
- <StyledSafeLink isIE11={isIE11} to={movie.path}>
126
+ {groupedMovies.map(({ letter, movies }) => (
127
+ <MovieGroup key={letter}>
128
+ <Letter aria-label={t('filmfrontpage.allMovieGroupTitleLabel', { letter })}>{letter}</Letter>
129
+ {movies.map((movie) => (
130
+ <MovieItem key={movie.id}>
131
+ <StyledSafeLink to={movie.path}>
243
132
  <MovieImage
244
- role="img"
245
- backgroundImage={
246
- inView && movie.metaImage && movie.metaImage.url
247
- ? `${movie.metaImage.url}?${makeSrcQueryString(IMAGE_WIDTH * 2)}`
248
- : null
133
+ src={
134
+ movie?.metaImage?.url ? `${movie.metaImage?.url}?${makeSrcQueryString(IMAGE_WIDTH * 2)}` : undefined
249
135
  }
250
- aria-label={movie.metaImage && movie.metaImage.alt}
251
- title={movie.title}
252
136
  />
253
137
  <MovieTextWrapper>
254
138
  <MovieTitle>{movie.title}</MovieTitle>
@@ -256,9 +140,9 @@ const AllMoviesAlphabetically = ({ movies, locale }: Props) => {
256
140
  </MovieTextWrapper>
257
141
  </StyledSafeLink>
258
142
  </MovieItem>
259
- </Fragment>
260
- );
261
- })}
143
+ ))}
144
+ </MovieGroup>
145
+ ))}
262
146
  </StyledWrapper>
263
147
  );
264
148
  };
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Copyright (c) 2019-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
+
1
9
  import React from 'react';
2
10
  import { spacing, colors, fonts, breakpoints } from '@ndla/core';
3
11
  import SafeLink from '@ndla/safelink';
@@ -24,41 +32,45 @@ const FilmContentCard = ({
24
32
  hideTags = false,
25
33
  }: Props) => {
26
34
  let backgroundImage = `${(metaImage && metaImage.url) || ''}`;
35
+ const contentTypeId = `content-type-${id}`;
27
36
  if (resizeThumbnailImages && metaImage) {
28
37
  backgroundImage += '?width=480';
29
38
  }
30
39
 
31
40
  return (
32
- <StyledSlideWrapper key={id} columnWidth={columnWidth} style={{ marginRight: `${distanceBetweenItems}px` }}>
33
- <SafeLink to={path}>
34
- <StyledImage
35
- role="img"
36
- columnWidth={columnWidth}
37
- aria-label={(metaImage && metaImage.alt) || ''}
38
- style={{
39
- backgroundImage: `url(${backgroundImage}?${makeSrcQueryString(600)})`,
40
- }}>
41
- {movieResourceTypes && !hideTags && (
42
- <FilmContentCardTags movieResourceTypes={movieResourceTypes} resourceTypes={resourceTypes} />
43
- )}
44
- </StyledImage>
45
- <StyledMovieTitle>{title}</StyledMovieTitle>
46
- </SafeLink>
47
- </StyledSlideWrapper>
41
+ <StyledSafeLink
42
+ onMouseDown={(e) => e.preventDefault()}
43
+ to={path}
44
+ aria-describedby={contentTypeId}
45
+ columnWidth={columnWidth}
46
+ style={{ marginRight: `${distanceBetweenItems}px` }}>
47
+ <StyledImage
48
+ role="img"
49
+ columnWidth={columnWidth}
50
+ style={{
51
+ backgroundImage: `url(${backgroundImage}?${makeSrcQueryString(600)})`,
52
+ }}>
53
+ {movieResourceTypes && !hideTags && (
54
+ <FilmContentCardTags
55
+ id={contentTypeId}
56
+ movieResourceTypes={movieResourceTypes}
57
+ resourceTypes={resourceTypes}
58
+ />
59
+ )}
60
+ </StyledImage>
61
+ <StyledMovieTitle>{title}</StyledMovieTitle>
62
+ </StyledSafeLink>
48
63
  );
49
64
  };
50
65
 
51
- const StyledMovieTitle = styled.h2`
66
+ const StyledMovieTitle = styled.span`
52
67
  ${fonts.sizes('14px', '20px')};
53
68
  font-weight: ${fonts.weight.semibold};
54
69
  color: #fff;
55
- margin: ${spacing.xsmall} 0 ${spacing.normal};
56
- min-height: ${spacing.large};
57
70
  @media (min-width: ${breakpoints.mobileWide}) {
58
71
  ${fonts.sizes('16px', '22px')};
59
72
  }
60
73
  @media (min-width: ${breakpoints.tablet}) {
61
- margin: ${spacing.small} 0;
62
74
  ${fonts.sizes('18px', '24px')};
63
75
  }
64
76
  `;
@@ -93,11 +105,18 @@ interface StyledSlideWrapperProps {
93
105
  columnWidth: number;
94
106
  }
95
107
 
96
- const StyledSlideWrapper = styled.div<StyledSlideWrapperProps>`
108
+ const shouldForwardProp = (p: string) => p !== 'columnWidth';
109
+
110
+ const StyledSafeLink = styled(SafeLink, { shouldForwardProp })<StyledSlideWrapperProps>`
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: ${spacing.small};
97
114
  width: ${(props) => props.columnWidth}px;
98
115
  color: #fff;
99
116
  box-shadow: none;
100
117
  &:hover,
118
+ &:focus-within,
119
+ &:active,
101
120
  &:focus {
102
121
  ${StyledMovieTitle} {
103
122
  text-decoration: underline;
@@ -23,10 +23,11 @@ const StyledMovieTags = styled.span`
23
23
  `;
24
24
 
25
25
  interface Props {
26
+ id: string;
26
27
  movieResourceTypes: MovieResourceType[];
27
28
  resourceTypes: MovieResourceType[];
28
29
  }
29
- const FilmContentCardTags = ({ movieResourceTypes, resourceTypes }: Props) => {
30
+ const FilmContentCardTags = ({ movieResourceTypes, resourceTypes, id }: Props) => {
30
31
  const resources: Record<string, boolean> = {};
31
32
  movieResourceTypes.forEach((movieResourceType) => {
32
33
  const resource = resourceTypes.find((resourceType) => resourceType.id === movieResourceType.id);
@@ -35,7 +36,7 @@ const FilmContentCardTags = ({ movieResourceTypes, resourceTypes }: Props) => {
35
36
  }
36
37
  });
37
38
  return (
38
- <StyledWrapperDiv>
39
+ <StyledWrapperDiv id={id}>
39
40
  {Object.keys(resources).map((resourceName) => (
40
41
  <StyledMovieTags key={resourceName}>{resourceName}</StyledMovieTags>
41
42
  ))}
@@ -8,10 +8,12 @@
8
8
  import React from 'react';
9
9
  import { Carousel, CalculatedCarouselProps } from '@ndla/carousel';
10
10
  import styled from '@emotion/styled';
11
+ import { IconButtonV2 } from '@ndla/button';
12
+ import { ChevronLeft, ChevronRight } from '@ndla/icons/common';
11
13
  import { breakpoints, mq, spacing } from '@ndla/core';
12
14
  import FilmContentCard from './FilmContentCard';
13
15
  import { MovieResourceType, MovieType } from './types';
14
- import { StyledHeadingH1 } from './filmStyles';
16
+ import { StyledHeadingH2 } from './filmStyles';
15
17
 
16
18
  interface Props {
17
19
  movies: MovieType[];
@@ -40,13 +42,18 @@ const FilmMovieList = ({
40
42
  resizeThumbnailImages,
41
43
  }: Props) => (
42
44
  <StyledSection>
43
- <StyledHeadingH1 marginLeft={autoSizedProps.margin}>{name}</StyledHeadingH1>
45
+ <StyledHeadingH2 marginLeft={autoSizedProps.margin}>{name}</StyledHeadingH2>
44
46
  <Carousel
45
- disableScroll={false}
46
- slideBackwardsLabel={slideBackwardsLabel}
47
- slideForwardsLabel={slideForwardsLabel}
48
- buttonClass="c-film-movielist__carousel-buttons"
49
- wrapperClass="c-film-movielist__carousel-wrapper-buttons"
47
+ leftButton={
48
+ <IconButtonV2 aria-label={slideBackwardsLabel}>
49
+ <ChevronLeft />
50
+ </IconButtonV2>
51
+ }
52
+ rightButton={
53
+ <IconButtonV2 aria-label={slideForwardsLabel}>
54
+ <ChevronRight />
55
+ </IconButtonV2>
56
+ }
50
57
  items={movies.map((movie) => (
51
58
  <FilmContentCard
52
59
  key={movie.id}
@@ -78,9 +78,9 @@ const FilmMovieSearch = ({
78
78
  <OneColumn>
79
79
  <TopicNavigation>
80
80
  <StyledHeadingH2 id={skipToContentId} className="u-12/12 u-4/12@tablet">
81
- {t('ndlaFilm.subjectsInMovies')}:
81
+ {`${t('ndlaFilm.subjectsInMovies')}:`}
82
82
  </StyledHeadingH2>
83
- <nav className="u-12/12 u-8/12@tablet">
83
+ <nav className="u-12/12 u-8/12@tablet" aria-labelledby={skipToContentId}>
84
84
  <ul>
85
85
  {topics.map((topic) => (
86
86
  <li key={topic.id}>
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import React, { useCallback, useEffect, useRef, useState } from 'react';
10
- import { SwipeEventData, useSwipeable } from 'react-swipeable';
10
+ import { SwipeDirections, SwipeEventData, useSwipeable } from 'react-swipeable';
11
11
  import styled from '@emotion/styled';
12
12
  import { css } from '@emotion/react';
13
13
  import { breakpoints, mq, spacing, spacingUnit, fonts, colors } from '@ndla/core';
@@ -104,7 +104,9 @@ interface SlideshowLinkProps {
104
104
  out?: boolean;
105
105
  }
106
106
 
107
- const SlideshowLink = styled(SafeLink)<SlideshowLinkProps>`
107
+ const shouldForwardProp = (p: string) => p !== 'out';
108
+
109
+ const SlideshowLink = styled(SafeLink, { shouldForwardProp })<SlideshowLinkProps>`
108
110
  display: flex;
109
111
  box-shadow: none;
110
112
  transition: all 400ms ease;
@@ -120,8 +122,9 @@ const SlideshowLink = styled(SafeLink)<SlideshowLinkProps>`
120
122
  padding-bottom: ${spacingUnit * 3}px;
121
123
  }
122
124
  &:hover {
123
- h1 {
125
+ ${() => SlideshowName} {
124
126
  text-decoration: underline;
127
+ text-decoration-color: white;
125
128
  }
126
129
  }
127
130
  `;
@@ -138,8 +141,9 @@ const SlideshowWrapper = styled.section`
138
141
  `;
139
142
 
140
143
  const SlideshowInfo = styled.div`
141
- border: 0;
142
- background: none;
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: ${spacing.small};
143
147
  background-color: rgba(3, 23, 43, 0.7);
144
148
  border-radius: 4px;
145
149
  padding: ${spacing.medium} ${spacing.medium} ${spacing.medium} ${spacing.normal};
@@ -150,40 +154,38 @@ const SlideshowInfo = styled.div`
150
154
  width: 100%;
151
155
  padding: ${spacing.medium} ${spacingUnit * 2}px ${spacing.medium} ${spacing.normal};
152
156
  }
153
- h1 {
154
- ${fonts.sizes('22px', '30px')};
155
- color: ${colors.white};
156
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
157
- margin: 0;
158
- font-weight: ${fonts.weight.semibold};
159
- ${mq.range({ from: breakpoints.mobileWide })} {
160
- margin: 0 0 ${spacing.small};
161
- ${fonts.sizes('26px', '30px')};
162
- }
163
- ${mq.range({ from: breakpoints.tablet })} {
164
- ${fonts.sizes('40px', '44px')};
165
- }
166
- ${mq.range({ from: breakpoints.desktop })} {
167
- ${fonts.sizes('48px', '54px')};
168
- }
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')};
169
167
  }
168
+ ${mq.range({ from: breakpoints.tablet })} {
169
+ ${fonts.sizes('40px', '44px')};
170
+ }
171
+ ${mq.range({ from: breakpoints.desktop })} {
172
+ ${fonts.sizes('48px', '54px')};
173
+ }
174
+ `;
170
175
 
171
- p {
172
- color: ${colors.white};
173
- display: inline-block;
174
- margin: 0;
175
- padding: 0;
176
- border-radius: 4px;
177
- ${fonts.sizes('12px', '18px')};
178
- ${mq.range({ from: breakpoints.mobileWide })} {
179
- ${fonts.sizes('15px', '20px')};
180
- }
181
- ${mq.range({ from: breakpoints.tablet })} {
182
- ${fonts.sizes('18px', '24px')};
183
- }
184
- ${mq.range({ from: breakpoints.wide })} {
185
- ${fonts.sizes('20px', '32px')};
186
- }
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')};
187
189
  }
188
190
  `;
189
191
 
@@ -215,6 +217,7 @@ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000
215
217
  const [slideIndex, setSlideIndex] = useState(0);
216
218
  const [slideIndexTarget, setSlideIndexTarget] = useState(0);
217
219
  const [animationComplete, setAnimationComplete] = useState(true);
220
+ const [swipeDirection, setSwipeDirection] = useState<SwipeDirections | undefined>(undefined);
218
221
  const slideRef = useRef<HTMLDivElement>(null);
219
222
  const slideText = useRef<HTMLDivElement>(null);
220
223
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -263,6 +266,7 @@ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000
263
266
  };
264
267
 
265
268
  const onSwipeEnd = () => {
269
+ setSwipeDirection(undefined);
266
270
  let slide;
267
271
  if (swipeDistance > 40) {
268
272
  slide = -1;
@@ -292,7 +296,11 @@ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000
292
296
  };
293
297
 
294
298
  const onSwipe = (eventData: SwipeEventData) => {
295
- if (eventData.dir === 'Up' || eventData.dir === 'Down') {
299
+ if (eventData.initial) {
300
+ setSwipeDirection(eventData.dir);
301
+ }
302
+ const dir = eventData.initial ? eventData.dir : swipeDirection;
303
+ if (dir === 'Up' || dir === 'Down') {
296
304
  return;
297
305
  }
298
306
  if (timer.current) {
@@ -352,6 +360,7 @@ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000
352
360
  const handlers = useSwipeable({
353
361
  onSwiped: onSwipeEnd,
354
362
  onSwiping: onSwipe,
363
+ preventScrollOnSwipe: swipeDirection === 'Left' || swipeDirection === 'Right',
355
364
  });
356
365
 
357
366
  if (slideshow.length === 0) {
@@ -381,8 +390,8 @@ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000
381
390
  <OneColumn>
382
391
  <SlideshowLink to={slideshow[activeSlide].path} out={!animationComplete}>
383
392
  <SlideshowInfo ref={slideText}>
384
- <h1>{slideshow[activeSlide].title}</h1>
385
- <p>{slideshow[activeSlide].metaDescription}</p>
393
+ <SlideshowName>{slideshow[activeSlide].title}</SlideshowName>
394
+ <SlideshowDescription>{slideshow[activeSlide].metaDescription}</SlideshowDescription>
386
395
  </SlideshowInfo>
387
396
  </SlideshowLink>
388
397
  </OneColumn>