@ndla/ui 3.3.4 → 3.3.8

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 (50) hide show
  1. package/es/AudioPlayer/Controls.js +30 -30
  2. package/es/Breadcrumb/Breadcrumb.js +2 -1
  3. package/es/ContentTypeBadge/ContentTypeBadge.js +30 -10
  4. package/es/NDLAFilm/FilmSlideshow.js +213 -247
  5. package/es/NDLAFilm/NavigationArrow.js +13 -60
  6. package/es/NDLAFilm/SlideshowIndicator.js +16 -63
  7. package/es/NDLAFilm/interfaces.js +0 -0
  8. package/es/NDLAFilm/shapes.js +0 -10
  9. package/es/Programme/Programme.js +11 -36
  10. package/es/Programme/ProgrammeSubjects.js +15 -42
  11. package/es/SearchTypeResult/SearchTypeHeader.js +7 -6
  12. package/es/Spinner/Spinner.js +3 -3
  13. package/lib/AudioPlayer/Controls.js +30 -30
  14. package/lib/Breadcrumb/Breadcrumb.js +2 -1
  15. package/lib/ContentTypeBadge/ContentTypeBadge.d.ts +3 -1
  16. package/lib/ContentTypeBadge/ContentTypeBadge.js +30 -10
  17. package/lib/NDLAFilm/FilmSlideshow.d.ts +16 -0
  18. package/lib/NDLAFilm/FilmSlideshow.js +214 -248
  19. package/lib/NDLAFilm/NavigationArrow.d.ts +15 -0
  20. package/lib/NDLAFilm/NavigationArrow.js +20 -65
  21. package/lib/NDLAFilm/SlideshowIndicator.d.ts +15 -0
  22. package/lib/NDLAFilm/SlideshowIndicator.js +16 -69
  23. package/lib/NDLAFilm/interfaces.d.ts +10 -0
  24. package/lib/NDLAFilm/interfaces.js +1 -0
  25. package/lib/NDLAFilm/shapes.d.ts +5 -0
  26. package/lib/NDLAFilm/shapes.js +1 -14
  27. package/lib/Programme/Programme.d.ts +1 -1
  28. package/lib/Programme/Programme.js +11 -36
  29. package/lib/Programme/ProgrammeSubjects.d.ts +12 -17
  30. package/lib/Programme/ProgrammeSubjects.js +23 -48
  31. package/lib/SearchTypeResult/SearchTypeHeader.js +7 -6
  32. package/lib/Spinner/Spinner.d.ts +3 -3
  33. package/lib/Spinner/Spinner.js +2 -2
  34. package/package.json +10 -10
  35. package/src/AudioPlayer/Controls.tsx +2 -2
  36. package/src/Breadcrumb/Breadcrumb.tsx +1 -1
  37. package/src/ContentTypeBadge/ContentTypeBadge.tsx +11 -9
  38. package/src/NDLAFilm/FilmSlideshow.tsx +264 -0
  39. package/src/NDLAFilm/NavigationArrow.tsx +42 -0
  40. package/src/NDLAFilm/SlideshowIndicator.tsx +40 -0
  41. package/src/NDLAFilm/interfaces.ts +10 -0
  42. package/src/NDLAFilm/shapes.ts +6 -0
  43. package/src/Programme/Programme.tsx +3 -15
  44. package/src/Programme/ProgrammeSubjects.tsx +19 -43
  45. package/src/SearchTypeResult/SearchTypeHeader.tsx +1 -1
  46. package/src/Spinner/Spinner.tsx +9 -5
  47. package/src/NDLAFilm/FilmSlideshow.jsx +0 -277
  48. package/src/NDLAFilm/NavigationArrow.jsx +0 -46
  49. package/src/NDLAFilm/SlideshowIndicator.jsx +0 -43
  50. package/src/NDLAFilm/shapes.js +0 -17
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Copyright (c) 2016-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, useEffect, useRef, useState } from 'react';
10
+ import { SwipeEventData, useSwipeable } from 'react-swipeable';
11
+ import BEMHelper from 'react-bem-helper';
12
+ import SafeLink from '@ndla/safelink';
13
+ import { OneColumn } from '../Layout';
14
+ import Spinner from '../Spinner';
15
+ import { NDLAMovie } from './interfaces';
16
+ import NavigationArrow from './NavigationArrow';
17
+ import SlideshowIndicator from './SlideshowIndicator';
18
+
19
+ interface Props {
20
+ autoSlide: boolean;
21
+ randomStart: boolean;
22
+ slideshow: NDLAMovie[];
23
+ slideInterval: number;
24
+ }
25
+
26
+ const classes = new BEMHelper({
27
+ name: 'film-slideshow',
28
+ prefix: 'c-',
29
+ });
30
+
31
+ const defaultTransitionSwipeEnd = 'transform 600ms cubic-bezier(0, 0.76, 0.09, 1)';
32
+ const defaultTransitionText = 'opacity 600ms ease';
33
+
34
+ const renderSlideItem = (slide: NDLAMovie) => (
35
+ <div
36
+ {...classes('item')}
37
+ key={slide.id}
38
+ role="img"
39
+ aria-label={(slide.metaImage && slide.metaImage.alt) || ''}
40
+ style={{
41
+ backgroundImage: `url(${(slide.metaImage && slide.metaImage.url) || ''})`,
42
+ }}
43
+ />
44
+ );
45
+
46
+ const FilmSlideshow = ({ autoSlide = false, slideshow = [], slideInterval = 5000 }: Props) => {
47
+ const [swipeDistance, setSwipeDistance] = useState(0);
48
+ const [slideIndex, setSlideIndex] = useState(0);
49
+ const [slideIndexTarget, setSlideIndexTarget] = useState(0);
50
+ const [animationComplete, setAnimationComplete] = useState(true);
51
+ const slideRef = useRef<HTMLDivElement>(null);
52
+ const slideText = useRef<HTMLDivElement>(null);
53
+ let timer = useRef<ReturnType<typeof setTimeout> | null>(null);
54
+
55
+ const gotoSlide = useCallback((indexTarget: number, useAnimation = false) => {
56
+ setSwipeDistance(0);
57
+ if (timer.current) {
58
+ clearTimeout(timer.current);
59
+ }
60
+ setSlideIndexTarget(indexTarget);
61
+ setAnimationComplete(!useAnimation);
62
+ }, []);
63
+
64
+ const onChangedSlide = () => {
65
+ if (!animationComplete) {
66
+ if (slideRef.current) {
67
+ slideRef.current.style.transition = 'none';
68
+ slideRef.current.style.transform = `translateX(${slideIndexTarget * 100}vw))`;
69
+ }
70
+
71
+ setAnimationComplete(true);
72
+ setSlideIndex(slideIndexTarget);
73
+ } else if (slideIndexTarget === -1) {
74
+ if (slideRef.current) {
75
+ // Go to last slide for continuous loop
76
+ slideRef.current.style.transition = 'none';
77
+ slideRef.current.style.transform = `translateX(${slideshow.length * 100}vw))`;
78
+ }
79
+
80
+ setSlideIndex(slideshow.length - 1);
81
+ setSlideIndexTarget(slideshow.length - 1);
82
+ setAnimationComplete(true);
83
+ } else if (slideIndexTarget === slideshow.length) {
84
+ if (slideRef.current) {
85
+ // Go to first slide for continuous loop
86
+ slideRef.current.style.transition = 'none';
87
+ slideRef.current.style.transform = `translateX(100vw))`;
88
+ }
89
+ setSlideIndex(0);
90
+ setSlideIndexTarget(0);
91
+ setAnimationComplete(true);
92
+ } else {
93
+ setAnimationComplete(true);
94
+ setSlideIndex(slideIndexTarget);
95
+ }
96
+ };
97
+
98
+ const onSwipeEnd = () => {
99
+ let slide;
100
+ if (swipeDistance > 40) {
101
+ slide = -1;
102
+ } else if (swipeDistance < -40) {
103
+ slide = 1;
104
+ } else {
105
+ slide = 0;
106
+ }
107
+ if (slideRef.current && slideText.current) {
108
+ slideRef.current.style.transition = defaultTransitionSwipeEnd;
109
+ slideText.current.style.transition = defaultTransitionText;
110
+ slideText.current.style.opacity = '1';
111
+ }
112
+ setSwipeDistance(0);
113
+
114
+ initTimer();
115
+
116
+ if (slide !== 0) {
117
+ setSlideIndex(slideIndex + slide);
118
+ setSlideIndexTarget(slideIndex + slide);
119
+ } else {
120
+ // Reset transfrom
121
+ if (slideRef.current) {
122
+ slideRef.current.style.transform = getSlidePosition(slideIndex + slide);
123
+ }
124
+ }
125
+ };
126
+
127
+ const onSwipe = (eventData: SwipeEventData) => {
128
+ if (eventData.dir === 'Up' || eventData.dir === 'Down') {
129
+ return;
130
+ }
131
+ if (timer.current) {
132
+ clearTimeout(timer.current);
133
+ }
134
+ setSwipeDistance(eventData.deltaX);
135
+ if (slideRef && slideRef.current) {
136
+ slideRef.current.style.transition = 'none';
137
+ slideRef.current.style.transform = getSlidePosition(slideIndexTarget);
138
+ }
139
+ const opacityText = 1 - Math.min(100, Math.abs(swipeDistance)) / 100;
140
+ if (slideText && slideText.current) {
141
+ slideText.current.style.transition = 'none';
142
+ slideText.current.style.opacity = opacityText.toString();
143
+ }
144
+ };
145
+
146
+ const onTransitionEnd = () => {
147
+ const slideshowLength = slideshow.length;
148
+ if (slideIndex === -1) {
149
+ if (slideRef.current) {
150
+ slideRef.current.style.transition = 'none';
151
+ slideRef.current.style.transform = getSlidePosition(slideshowLength - 1);
152
+ }
153
+ setSlideIndex(slideshowLength - 1);
154
+ setSlideIndexTarget(slideshowLength - 1);
155
+ } else if (slideIndex >= slideshowLength) {
156
+ if (slideRef.current) {
157
+ slideRef.current.style.transition = 'none';
158
+ slideRef.current.style.transform = getSlidePosition(0);
159
+ }
160
+ setSlideIndex(0);
161
+ setSlideIndexTarget(0);
162
+ }
163
+ };
164
+
165
+ const getSlidePosition = (target: number) => {
166
+ if (swipeDistance !== 0) {
167
+ return `translateX(calc(${swipeDistance}px -
168
+ ${(target + 1) * 100}vw))`;
169
+ }
170
+ return `translateX(-${(target + 1) * 100}vw)`;
171
+ };
172
+
173
+ const initTimer = useCallback(() => {
174
+ if (autoSlide) {
175
+ timer.current = setTimeout(() => {
176
+ gotoSlide(slideIndex + 1);
177
+ }, slideInterval);
178
+ }
179
+ }, [autoSlide, gotoSlide, slideInterval, slideIndex]);
180
+
181
+ useEffect(() => {
182
+ initTimer();
183
+ }, [initTimer]);
184
+
185
+ const handlers = useSwipeable({
186
+ onSwiped: onSwipeEnd,
187
+ onSwiping: onSwipe,
188
+ });
189
+
190
+ if (slideshow.length === 0) {
191
+ return (
192
+ <div>
193
+ <div {...classes('slideshow')}>
194
+ <Spinner inverted />
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ const slideshowWidth = `${(slideshow.length + 2) * 100}vw`;
201
+ let activeSlide = slideIndex;
202
+ if (activeSlide < 0) {
203
+ activeSlide = slideshow.length - 1;
204
+ } else if (activeSlide >= slideshow.length) {
205
+ activeSlide = 0;
206
+ }
207
+
208
+ return (
209
+ <section {...classes('')} {...handlers}>
210
+ <>
211
+ <div {...classes('slide-link-wrapper')}>
212
+ <OneColumn>
213
+ <SafeLink
214
+ to={slideshow[activeSlide].path}
215
+ {...classes('item-wrapper', 'text', {
216
+ out: !animationComplete,
217
+ })}>
218
+ <div {...classes('slide-info')} ref={slideText}>
219
+ <h1>{slideshow[activeSlide].title}</h1>
220
+ <p>{slideshow[activeSlide].metaDescription}</p>
221
+ </div>
222
+ </SafeLink>
223
+ </OneColumn>
224
+ </div>
225
+ <NavigationArrow
226
+ slideIndexTarget={slideIndexTarget > 0 ? slideIndexTarget - 1 : slideshow.length - 1}
227
+ gotoSlide={gotoSlide}
228
+ />
229
+ <NavigationArrow
230
+ slideIndexTarget={slideIndexTarget < slideshow.length - 1 ? slideIndexTarget + 1 : 0}
231
+ gotoSlide={gotoSlide}
232
+ rightArrow
233
+ />
234
+ {!animationComplete && (
235
+ <div
236
+ {...classes('item', 'fade-over')}
237
+ role="img"
238
+ onAnimationEnd={onChangedSlide}
239
+ style={{
240
+ backgroundImage: `url(${
241
+ (slideshow[activeSlide].metaImage && slideshow[activeSlide].metaImage.url) || ''
242
+ })`,
243
+ }}
244
+ />
245
+ )}
246
+ <div
247
+ ref={slideRef}
248
+ {...classes('item-wrapper')}
249
+ onTransitionEnd={onTransitionEnd}
250
+ style={{
251
+ width: slideshowWidth,
252
+ transform: getSlidePosition(slideIndex),
253
+ }}>
254
+ {renderSlideItem(slideshow[slideshow.length - 1])}
255
+ {slideshow.map(renderSlideItem)}
256
+ {renderSlideItem(slideshow[0])}
257
+ </div>
258
+ <SlideshowIndicator slideshow={slideshow} activeSlide={activeSlide} gotoSlide={gotoSlide} />
259
+ </>
260
+ </section>
261
+ );
262
+ };
263
+
264
+ export default FilmSlideshow;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright (c) 2016-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 BEMHelper from 'react-bem-helper';
11
+ import { ChevronRight, ChevronLeft } from '@ndla/icons/common';
12
+
13
+ interface Props {
14
+ slideIndexTarget: number;
15
+ slideshowLength?: number;
16
+ gotoSlide: (indexTarget: number, useAnimation: boolean) => void;
17
+ rightArrow?: boolean;
18
+ }
19
+
20
+ const classes = new BEMHelper({
21
+ name: 'film-slideshow',
22
+ prefix: 'c-',
23
+ });
24
+
25
+ const NavigationArrow = ({ slideIndexTarget, gotoSlide, rightArrow }: Props) => {
26
+ const Chevron = rightArrow ? ChevronRight : ChevronLeft;
27
+
28
+ return (
29
+ <div {...classes('navigation-arrows', rightArrow ? 'right' : '')}>
30
+ <button
31
+ type="button"
32
+ tabIndex={-1}
33
+ onClick={() => {
34
+ gotoSlide(slideIndexTarget, true);
35
+ }}>
36
+ <Chevron />
37
+ </button>
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export default NavigationArrow;
@@ -0,0 +1,40 @@
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
+
9
+ import React from 'react';
10
+ import BEMHelper from 'react-bem-helper';
11
+ import { NDLAMovie } from './interfaces';
12
+
13
+ interface Props {
14
+ slideshow: NDLAMovie[];
15
+ activeSlide: number;
16
+ gotoSlide: (indexTarget: number, useAnimation: boolean) => void;
17
+ }
18
+
19
+ const classes = new BEMHelper({
20
+ name: 'film-slideshow',
21
+ prefix: 'c-',
22
+ });
23
+
24
+ const SlideshowIndicator = ({ slideshow, activeSlide, gotoSlide }: Props) => {
25
+ return (
26
+ <div {...classes('indicator-wrapper')}>
27
+ {slideshow.map((slide, index) => (
28
+ <button
29
+ key={`indicator_${index}`}
30
+ type="button"
31
+ {...classes('indicator-dot', index === activeSlide ? 'active' : '')}
32
+ onClick={() => gotoSlide(index, true)}>
33
+ <span />
34
+ </button>
35
+ ))}
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default SlideshowIndicator;
@@ -0,0 +1,10 @@
1
+ export interface NDLAMovie {
2
+ id: string;
3
+ metaDescription: string;
4
+ title: string;
5
+ metaImage: {
6
+ url: string;
7
+ alt: string;
8
+ };
9
+ path: string;
10
+ }
@@ -0,0 +1,6 @@
1
+ import PropTypes from 'prop-types';
2
+
3
+ export const topicShape = PropTypes.shape({
4
+ id: PropTypes.string,
5
+ name: PropTypes.string,
6
+ });
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React from 'react';
2
2
  import styled from '@emotion/styled';
3
3
  import { breakpoints, mq, spacing } from '@ndla/core';
4
4
  import LayoutItem, { OneColumn } from '../Layout';
@@ -65,19 +65,7 @@ type Props = GradesProps & {
65
65
  image?: string;
66
66
  };
67
67
 
68
- export const Programme = ({ heading, image, grades }: Props) => {
69
- const [showGradeIndex, setShowGradeIndex] = useState(0);
70
- const isWindowContext = typeof window !== 'undefined';
71
-
72
- useEffect(() => {
73
- if (isWindowContext) {
74
- const rememberGradeIndex = window.localStorage.getItem('programmeShowGradeIndex') || '0';
75
- if (grades.length > Number(rememberGradeIndex)) {
76
- setShowGradeIndex(Number(rememberGradeIndex));
77
- }
78
- }
79
- }, [isWindowContext, grades]);
80
-
68
+ export const Programme = ({ heading, image, grades, selectedGrade, onChangeGrade }: Props) => {
81
69
  return (
82
70
  <StyledWrapper>
83
71
  <StyledBackground image={image} />
@@ -87,7 +75,7 @@ export const Programme = ({ heading, image, grades }: Props) => {
87
75
  <StyledContentWrapper>
88
76
  <NavigationHeading>{heading}</NavigationHeading>
89
77
  <SubjectsWrapper>
90
- <ProgrammeSubjects grades={grades} preSelectedGradeIndex={showGradeIndex} />
78
+ <ProgrammeSubjects grades={grades} selectedGrade={selectedGrade} onChangeGrade={onChangeGrade} />
91
79
  </SubjectsWrapper>
92
80
  </StyledContentWrapper>
93
81
  </LayoutItem>
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { useState } from 'react';
9
+ import React from 'react';
10
10
  import styled from '@emotion/styled';
11
11
  // @ts-ignore
12
12
  import Button from '@ndla/button';
@@ -27,66 +27,42 @@ const GradesMenu = styled.div`
27
27
  `;
28
28
 
29
29
  export type GradesProps = {
30
- grades: [
31
- {
30
+ selectedGrade?: string;
31
+ onChangeGrade: (newGrade: string) => void;
32
+ grades: {
33
+ name: string;
34
+ categories: {
32
35
  name: string;
33
- categories: [
34
- {
35
- name: string;
36
- subjects: [
37
- {
38
- label: string;
39
- url: string;
40
- },
41
- ];
42
- },
43
- ];
44
- },
45
- ];
36
+ subjects: {
37
+ label: string;
38
+ url: string;
39
+ }[];
40
+ }[];
41
+ }[];
46
42
  };
47
43
 
48
44
  type Props = GradesProps & {
49
- preSelectedGradeIndex?: number;
50
45
  onNavigate?: () => void;
51
46
  };
52
47
 
53
- const ProgrammeSubjects = ({ grades, onNavigate, preSelectedGradeIndex = 0 }: Props) => {
54
- const [showGradeIndex, setShowGradeIndex] = useState(preSelectedGradeIndex);
55
- const isWindowContext = typeof window !== 'undefined';
56
-
57
- const toggleGradeIndex = (index: number) => {
58
- setShowGradeIndex(index);
59
- if (isWindowContext) {
60
- window.localStorage.setItem('programmeShowGradeIndex', `${index}`);
61
- }
62
- };
63
-
64
- const selectedGrade = grades[showGradeIndex];
48
+ const ProgrammeSubjects = ({ grades, onNavigate, onChangeGrade, selectedGrade = 'vg1' }: Props) => {
49
+ const grade = grades.find((grade) => grade.name.toLowerCase() === selectedGrade) ?? grades[0];
65
50
  return (
66
51
  <>
67
52
  <GradesMenu>
68
- {grades.map((item, index) => (
53
+ {grades.map((item) => (
69
54
  <Button
70
55
  key={item.name}
71
- onClick={() => toggleGradeIndex(index)}
72
- lighter={showGradeIndex !== index}
56
+ onClick={() => onChangeGrade(item.name.toLowerCase())}
57
+ lighter={item !== grade}
73
58
  size="normal"
74
59
  borderShape="rounded">
75
60
  {item.name}
76
61
  </Button>
77
62
  ))}
78
63
  </GradesMenu>
79
- {selectedGrade.categories.map((category) => (
80
- <NavigationBox
81
- key={category.name}
82
- heading={category.name}
83
- items={category.subjects}
84
- onClick={() => {
85
- if (onNavigate) {
86
- onNavigate();
87
- }
88
- }}
89
- />
64
+ {grade.categories.map((category) => (
65
+ <NavigationBox key={category.name} heading={category.name} items={category.subjects} onClick={onNavigate} />
90
66
  ))}
91
67
  </>
92
68
  );
@@ -85,7 +85,7 @@ type Props = {
85
85
  const SearchTypeHeader = ({ filters, onFilterClick, totalCount, type, t }: Props & WithTranslation) => (
86
86
  <HeaderWrapper>
87
87
  <TypeWrapper>
88
- {type && <ContentTypeBadge type={type} background size="large" />}
88
+ {type && <ContentTypeBadge type={type} title={t(`contentTypes.${type}`)} background size="large" />}
89
89
  <SubjectName>
90
90
  {type && <b>{t(`contentTypes.${type}`)}</b>}{' '}
91
91
  {totalCount && <Count>{t(`searchPage.resultType.hits`, { count: totalCount })}</Count>}
@@ -8,23 +8,27 @@
8
8
 
9
9
  import React from 'react';
10
10
  import styled from '@emotion/styled';
11
- import { spacing, colors, SpacingNames } from '@ndla/core';
11
+ import { colors, spacing, SpacingNames } from '@ndla/core';
12
12
 
13
13
  interface Props {
14
+ size?: SpacingNames;
15
+ margin?: string;
16
+ inverted?: boolean;
17
+ }
18
+
19
+ interface StyledProps extends Props {
14
20
  size: SpacingNames;
15
- margin: string;
16
- inverted: boolean;
17
21
  }
18
22
 
19
- const SpinnerDiv = styled('div')<Props>`
23
+ const SpinnerDiv = styled('div')<StyledProps>`
20
24
  border: calc(${(props) => spacing[props.size]} / 6.5) solid rgba(0, 0, 0, 0.1);
21
25
  border-bottom-color: ${(props) => (props.inverted ? '#fff' : colors.brand.primary)};
22
26
  border-radius: 50%;
23
27
  animation: spinnerAnimation 0.7s linear infinite;
24
28
  height: ${(props) => spacing[props.size]};
25
29
  width: ${(props) => spacing[props.size]};
26
- display: block;
27
30
  margin: ${(props) => props.margin};
31
+ display: block;
28
32
  @keyframes spinnerAnimation {
29
33
  0% {
30
34
  transform: rotate(0deg);