@ndla/ui 4.3.2 → 5.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.
- package/README.md +11 -11
- package/es/Article/Article.js +3 -7
- package/es/Article/ArticleByline.js +4 -4
- package/es/Article/ArticleNotions.js +37 -92
- package/es/ContentTypeBadge/ContentTypeBadge.js +10 -1
- package/es/FactBox/FactBox.js +36 -11
- package/es/Filter/FilterButtons.js +18 -17
- package/es/Filter/FilterCarousel.js +198 -0
- package/es/Masthead/Masthead.js +24 -2
- package/es/Notion/ConceptNotion.js +57 -0
- package/es/Notion/FigureNotion.js +82 -0
- package/es/Notion/Notion.js +122 -48
- package/es/Notion/NotionImage.js +47 -0
- package/es/Notion/NotionVisualElement.js +38 -0
- package/es/Notion/index.js +2 -1
- package/es/SearchTypeResult/ActiveFilterContent.js +17 -2
- package/es/SearchTypeResult/ActiveFilters.js +64 -50
- package/es/SearchTypeResult/PopupFilter.js +28 -125
- package/es/SearchTypeResult/ResultNavigation.js +41 -14
- package/es/SearchTypeResult/SearchFieldHeader.js +20 -41
- package/es/SearchTypeResult/SearchFilterContent.js +61 -0
- package/es/SearchTypeResult/SearchHeader.js +51 -28
- package/es/SearchTypeResult/SearchItem.js +64 -190
- package/es/SearchTypeResult/SearchItemList.js +132 -0
- package/es/SearchTypeResult/SearchItems.js +17 -13
- package/es/SearchTypeResult/SearchNotionItem.js +13 -13
- package/es/SearchTypeResult/SearchNotionsResult.js +16 -21
- package/es/SearchTypeResult/SearchTypeHeader.js +43 -26
- package/es/SearchTypeResult/SearchTypeResult.js +7 -5
- package/es/SearchTypeResult/SearchViewType.js +93 -0
- package/es/SearchTypeResult/components/ItemContexts.js +125 -0
- package/es/SearchTypeResult/components/ItemResourceHeader.js +87 -0
- package/es/SearchTypeResult/components/ItemTopicHeader.js +56 -0
- package/es/SearchTypeResult/components/SubjectFilters.js +177 -0
- package/es/SearchTypeResult/index.js +2 -1
- package/es/all.css +1 -1
- package/es/index.js +3 -2
- package/es/locale/messages-en.js +20 -5
- package/es/locale/messages-nb.js +21 -6
- package/es/locale/messages-nn.js +21 -6
- package/es/model/ContentType.js +2 -1
- package/es/shapes.js +1 -1
- package/lib/Article/Article.d.ts +4 -5
- package/lib/Article/Article.js +3 -7
- package/lib/Article/ArticleByline.js +4 -4
- package/lib/Article/ArticleNotions.d.ts +3 -8
- package/lib/Article/ArticleNotions.js +41 -90
- package/lib/ContentTypeBadge/ContentTypeBadge.d.ts +1 -0
- package/lib/ContentTypeBadge/ContentTypeBadge.js +14 -2
- package/lib/FactBox/FactBox.js +41 -8
- package/lib/Filter/FilterButtons.d.ts +3 -10
- package/lib/Filter/FilterButtons.js +19 -17
- package/lib/Filter/FilterCarousel.d.ts +13 -0
- package/lib/Filter/FilterCarousel.js +207 -0
- package/lib/Masthead/Masthead.js +30 -2
- package/lib/Notion/ConceptNotion.d.ts +25 -0
- package/lib/Notion/ConceptNotion.js +79 -0
- package/lib/Notion/FigureNotion.d.ts +23 -0
- package/lib/Notion/FigureNotion.js +97 -0
- package/lib/Notion/Notion.d.ts +24 -11
- package/lib/Notion/Notion.js +120 -48
- package/lib/Notion/NotionImage.d.ts +15 -0
- package/lib/Notion/NotionImage.js +63 -0
- package/lib/Notion/NotionVisualElement.d.ts +22 -0
- package/lib/Notion/NotionVisualElement.js +51 -0
- package/lib/Notion/index.d.ts +1 -0
- package/lib/Notion/index.js +8 -0
- package/lib/SearchTypeResult/ActiveFilterContent.js +16 -9
- package/lib/SearchTypeResult/ActiveFilters.d.ts +2 -1
- package/lib/SearchTypeResult/ActiveFilters.js +65 -50
- package/lib/SearchTypeResult/PopupFilter.d.ts +13 -19
- package/lib/SearchTypeResult/PopupFilter.js +27 -123
- package/lib/SearchTypeResult/ResultNavigation.d.ts +2 -2
- package/lib/SearchTypeResult/ResultNavigation.js +38 -14
- package/lib/SearchTypeResult/SearchFieldHeader.d.ts +3 -8
- package/lib/SearchTypeResult/SearchFieldHeader.js +18 -39
- package/lib/SearchTypeResult/SearchFilterContent.d.ts +16 -0
- package/lib/SearchTypeResult/SearchFilterContent.js +67 -0
- package/lib/SearchTypeResult/SearchHeader.d.ts +3 -7
- package/lib/SearchTypeResult/SearchHeader.js +59 -30
- package/lib/SearchTypeResult/SearchItem.d.ts +8 -12
- package/lib/SearchTypeResult/SearchItem.js +64 -187
- package/lib/SearchTypeResult/SearchItemList.d.ts +10 -0
- package/lib/SearchTypeResult/SearchItemList.js +139 -0
- package/lib/SearchTypeResult/SearchItems.d.ts +4 -3
- package/lib/SearchTypeResult/SearchItems.js +18 -13
- package/lib/SearchTypeResult/SearchNotionItem.js +13 -13
- package/lib/SearchTypeResult/SearchNotionsResult.d.ts +2 -4
- package/lib/SearchTypeResult/SearchNotionsResult.js +23 -23
- package/lib/SearchTypeResult/SearchTypeHeader.d.ts +2 -2
- package/lib/SearchTypeResult/SearchTypeHeader.js +40 -25
- package/lib/SearchTypeResult/SearchTypeResult.d.ts +7 -6
- package/lib/SearchTypeResult/SearchTypeResult.js +7 -5
- package/lib/SearchTypeResult/SearchViewType.d.ts +13 -0
- package/lib/SearchTypeResult/SearchViewType.js +99 -0
- package/lib/SearchTypeResult/components/ItemContexts.d.ts +19 -0
- package/lib/SearchTypeResult/components/ItemContexts.js +134 -0
- package/lib/SearchTypeResult/components/ItemResourceHeader.d.ts +16 -0
- package/lib/SearchTypeResult/components/ItemResourceHeader.js +98 -0
- package/lib/SearchTypeResult/components/ItemTopicHeader.d.ts +17 -0
- package/lib/SearchTypeResult/components/ItemTopicHeader.js +67 -0
- package/lib/SearchTypeResult/components/SubjectFilters.d.ts +32 -0
- package/lib/SearchTypeResult/components/SubjectFilters.js +192 -0
- package/lib/SearchTypeResult/index.d.ts +2 -1
- package/lib/SearchTypeResult/index.js +8 -0
- package/lib/all.css +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +24 -1
- package/lib/locale/messages-en.d.ts +16 -1
- package/lib/locale/messages-en.js +20 -5
- package/lib/locale/messages-nb.d.ts +15 -0
- package/lib/locale/messages-nb.js +21 -6
- package/lib/locale/messages-nn.d.ts +16 -1
- package/lib/locale/messages-nn.js +21 -6
- package/lib/model/ContentType.d.ts +1 -0
- package/lib/model/ContentType.js +4 -2
- package/lib/shapes.js +1 -1
- package/lib/types.d.ts +1 -0
- package/package.json +14 -14
- package/src/Article/Article.tsx +8 -15
- package/src/Article/ArticleByline.tsx +1 -1
- package/src/Article/ArticleNotions.tsx +13 -33
- package/src/ContentTypeBadge/ContentTypeBadge.tsx +8 -0
- package/src/ContentTypeBadge/component.content-type-badge.scss +5 -0
- package/src/FactBox/FactBox.tsx +22 -15
- package/src/Figure/component.figure.scss +1 -1
- package/src/Filter/FilterButtons.tsx +14 -15
- package/src/Filter/FilterCarousel.tsx +166 -0
- package/src/Masthead/Masthead.tsx +42 -18
- package/src/Notion/ConceptNotion.tsx +80 -0
- package/src/Notion/FigureNotion.tsx +86 -0
- package/src/Notion/Notion.tsx +205 -75
- package/src/Notion/NotionImage.tsx +51 -0
- package/src/Notion/NotionVisualElement.tsx +50 -0
- package/src/Notion/index.ts +1 -0
- package/src/SearchTypeResult/ActiveFilterContent.tsx +7 -2
- package/src/SearchTypeResult/ActiveFilters.tsx +72 -38
- package/src/SearchTypeResult/PopupFilter.tsx +73 -146
- package/src/SearchTypeResult/ResultNavigation.tsx +54 -16
- package/src/SearchTypeResult/SearchFieldHeader.tsx +15 -40
- package/src/SearchTypeResult/SearchFilterContent.tsx +63 -0
- package/src/SearchTypeResult/SearchHeader.tsx +31 -31
- package/src/SearchTypeResult/SearchItem.tsx +145 -233
- package/src/SearchTypeResult/SearchItemList.tsx +167 -0
- package/src/SearchTypeResult/SearchItems.tsx +26 -19
- package/src/SearchTypeResult/SearchNotionItem.tsx +5 -12
- package/src/SearchTypeResult/SearchNotionsResult.tsx +29 -22
- package/src/SearchTypeResult/SearchTypeHeader.tsx +51 -33
- package/src/SearchTypeResult/SearchTypeResult.tsx +13 -12
- package/src/SearchTypeResult/SearchViewType.tsx +109 -0
- package/src/SearchTypeResult/components/ItemContexts.tsx +138 -0
- package/src/SearchTypeResult/components/ItemResourceHeader.tsx +133 -0
- package/src/SearchTypeResult/components/ItemTopicHeader.tsx +95 -0
- package/src/SearchTypeResult/components/SubjectFilters.tsx +152 -0
- package/src/SearchTypeResult/index.ts +9 -1
- package/src/index.ts +3 -0
- package/src/locale/messages-en.ts +18 -3
- package/src/locale/messages-nb.ts +19 -4
- package/src/locale/messages-nn.ts +19 -4
- package/src/model/ContentType.ts +1 -0
- package/src/shapes.js +1 -0
- package/src/types.ts +1 -0
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React
|
|
9
|
+
import React from 'react';
|
|
10
10
|
import styled from '@emotion/styled';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
12
|
import Modal, { ModalHeader, ModalCloseButton, ModalBody } from '@ndla/modal';
|
|
13
|
-
import { mq, breakpoints } from '@ndla/core';
|
|
14
|
-
import { Explanation } from '@ndla/icons/common';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { NotionProps } from '../Notion/Notion';
|
|
13
|
+
import { mq, breakpoints, fonts, colors } from '@ndla/core';
|
|
14
|
+
import { Explanation, NotionFlip } from '@ndla/icons/common';
|
|
15
|
+
import { ConceptNotion } from '../Notion';
|
|
16
|
+
import { ConceptNotionType } from '../Notion/ConceptNotion';
|
|
18
17
|
|
|
19
18
|
const ArticleNotionsContainer = styled.div`
|
|
20
19
|
margin-bottom: 26px;
|
|
@@ -23,7 +22,6 @@ const ArticleNotionsContainer = styled.div`
|
|
|
23
22
|
margin-bottom: 0;
|
|
24
23
|
}
|
|
25
24
|
`;
|
|
26
|
-
/* Stored for later
|
|
27
25
|
|
|
28
26
|
const NotionsTrigger = styled.div`
|
|
29
27
|
padding: 0 16px;
|
|
@@ -32,7 +30,7 @@ const NotionsTrigger = styled.div`
|
|
|
32
30
|
background-color: ${colors.brand.greyLighter};
|
|
33
31
|
border-radius: 4px;
|
|
34
32
|
cursor: pointer;
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
${mq.range({ from: breakpoints.tablet })} {
|
|
37
35
|
position: fixed;
|
|
38
36
|
border: none;
|
|
@@ -80,7 +78,7 @@ const NotionsTrigger = styled.div`
|
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
`;
|
|
83
|
-
|
|
81
|
+
|
|
84
82
|
const ModalHeadingContainer = styled.div`
|
|
85
83
|
display: flex;
|
|
86
84
|
align-items: center;
|
|
@@ -158,36 +156,24 @@ export type NotionRelatedContent = {
|
|
|
158
156
|
};
|
|
159
157
|
|
|
160
158
|
type ArticleNotionsProps = {
|
|
161
|
-
|
|
162
|
-
notions: NotionProps[];
|
|
163
|
-
onReferenceClick?: MouseEventHandler<HTMLButtonElement>;
|
|
159
|
+
notions: ConceptNotionType[];
|
|
164
160
|
relatedContent?: NotionRelatedContent[];
|
|
165
|
-
renderMarkdown?: (text: string) => string;
|
|
166
161
|
buttonOffsetRight: number;
|
|
167
162
|
};
|
|
168
163
|
|
|
169
|
-
export const ArticleNotions = ({
|
|
170
|
-
locale,
|
|
171
|
-
notions,
|
|
172
|
-
onReferenceClick,
|
|
173
|
-
relatedContent = [],
|
|
174
|
-
renderMarkdown,
|
|
175
|
-
buttonOffsetRight,
|
|
176
|
-
}: ArticleNotionsProps) => {
|
|
164
|
+
export const ArticleNotions = ({ notions, relatedContent = [], buttonOffsetRight }: ArticleNotionsProps) => {
|
|
177
165
|
const { t } = useTranslation();
|
|
178
|
-
|
|
166
|
+
const leftOffset = `${buttonOffsetRight - 32}px`;
|
|
179
167
|
return (
|
|
180
168
|
<ArticleNotionsContainer>
|
|
181
169
|
<Modal
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
activateButton={
|
|
170
|
+
activateButton={
|
|
185
171
|
<NotionsTrigger role="button" aria-label={t('article.notionsPrompt')} style={{ left: leftOffset }}>
|
|
186
172
|
<NotionFlip />
|
|
187
173
|
<Explanation />
|
|
188
174
|
<span>{t('article.notionsPrompt')}</span>
|
|
189
175
|
</NotionsTrigger>
|
|
190
|
-
}
|
|
176
|
+
}
|
|
191
177
|
size="large"
|
|
192
178
|
backgroundColor="white">
|
|
193
179
|
{(onClose: () => void) => (
|
|
@@ -202,13 +188,7 @@ export const ArticleNotions = ({
|
|
|
202
188
|
</ModalHeadingContainer>
|
|
203
189
|
<NotionsContainer>
|
|
204
190
|
{notions.map((notion) => (
|
|
205
|
-
<
|
|
206
|
-
key={notion.id}
|
|
207
|
-
locale={locale}
|
|
208
|
-
onReferenceClick={onReferenceClick}
|
|
209
|
-
renderMarkdown={renderMarkdown}
|
|
210
|
-
{...notion}
|
|
211
|
-
/>
|
|
191
|
+
<ConceptNotion key={notion.id} concept={notion} />
|
|
212
192
|
))}
|
|
213
193
|
</NotionsContainer>
|
|
214
194
|
{relatedContent.length > 0 && (
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ExternalLearningResource,
|
|
11
11
|
SharedResource,
|
|
12
12
|
LearningPath,
|
|
13
|
+
MultidisciplinaryTopic,
|
|
13
14
|
} from '@ndla/icons/contentType';
|
|
14
15
|
|
|
15
16
|
import { MenuBook } from '@ndla/icons/action';
|
|
@@ -67,6 +68,9 @@ export const ContentTypeBadge = ({ type, background, title, size = 'small', bord
|
|
|
67
68
|
case contentTypes.TOPIC:
|
|
68
69
|
icon = <Subject title={title} />;
|
|
69
70
|
break;
|
|
71
|
+
case contentTypes.MULTIDISCIPLINARY_TOPIC:
|
|
72
|
+
icon = <MultidisciplinaryTopic />;
|
|
73
|
+
break;
|
|
70
74
|
default:
|
|
71
75
|
break;
|
|
72
76
|
}
|
|
@@ -106,3 +110,7 @@ export const SourceMaterialBadge = (props: Omit<Props, 'type'>) => (
|
|
|
106
110
|
export const LearningPathBadge = (props: Omit<Props, 'type'>) => (
|
|
107
111
|
<ContentTypeBadge {...props} type={contentTypes.LEARNING_PATH} />
|
|
108
112
|
);
|
|
113
|
+
|
|
114
|
+
export const MultidisciplinaryTopicBadge = (props: Omit<Props, 'type'>) => (
|
|
115
|
+
<ContentTypeBadge {...props} type={contentTypes.MULTIDISCIPLINARY_TOPIC} />
|
|
116
|
+
);
|
package/src/FactBox/FactBox.tsx
CHANGED
|
@@ -6,15 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { ReactNode, MouseEvent } from 'react';
|
|
9
|
+
import React, { ReactNode, MouseEvent, useState } from 'react';
|
|
10
10
|
import BEMHelper from 'react-bem-helper';
|
|
11
11
|
import Button from '@ndla/button';
|
|
12
|
-
|
|
13
|
-
const toggleFactBox = (event: MouseEvent<HTMLButtonElement>) => {
|
|
14
|
-
const button = event.currentTarget;
|
|
15
|
-
const aside = button?.previousSibling?.parentElement;
|
|
16
|
-
aside?.classList?.toggle('expanded');
|
|
17
|
-
};
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
18
13
|
|
|
19
14
|
const classes = new BEMHelper({
|
|
20
15
|
name: 'factbox',
|
|
@@ -26,13 +21,25 @@ interface Props {
|
|
|
26
21
|
children?: ReactNode;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
const FactBox = ({ children, dangerouslySetInnerHTML }: Props) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
24
|
+
const FactBox = ({ children, dangerouslySetInnerHTML }: Props) => {
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
|
|
28
|
+
const toggleFactBox = (event: MouseEvent<HTMLButtonElement>) => {
|
|
29
|
+
const button = event.currentTarget;
|
|
30
|
+
const aside = button?.previousSibling?.parentElement;
|
|
31
|
+
aside?.classList?.toggle('expanded');
|
|
32
|
+
setOpen((prev) => !prev);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<aside {...classes()}>
|
|
37
|
+
<div {...classes('content')} dangerouslySetInnerHTML={dangerouslySetInnerHTML}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
<Button {...classes('button')} onClick={toggleFactBox} title={t(open ? 'factbox.close' : 'factbox.open')} />
|
|
41
|
+
</aside>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
37
44
|
|
|
38
45
|
export default FactBox;
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
8
|
import React, { useEffect, useState } from 'react';
|
|
10
9
|
import styled from '@emotion/styled';
|
|
11
10
|
import Button from '@ndla/button';
|
|
@@ -15,6 +14,7 @@ import { Cross as CrossIcon, Plus as PlusIcon } from '@ndla/icons/action';
|
|
|
15
14
|
import Modal, { ModalHeader, ModalBody, ModalCloseButton } from '@ndla/modal';
|
|
16
15
|
// @ts-ignore
|
|
17
16
|
import ToggleItem from './ToggleItem';
|
|
17
|
+
import FilterCarousel from './FilterCarousel';
|
|
18
18
|
|
|
19
19
|
const StyledHeading = styled.h3`
|
|
20
20
|
${fonts.sizes('16px', '32px')};
|
|
@@ -25,12 +25,14 @@ const StyledHeading = styled.h3`
|
|
|
25
25
|
|
|
26
26
|
const StyledButtonsWrapper = styled.div`
|
|
27
27
|
display: flex;
|
|
28
|
-
flex-wrap:
|
|
28
|
+
flex-wrap: nowrap;
|
|
29
|
+
position: relative;
|
|
29
30
|
`;
|
|
30
31
|
|
|
31
32
|
const StyledButtonElementWrapper = styled.div`
|
|
32
33
|
margin: 0 ${spacing.xsmall} ${spacing.xsmall} 0;
|
|
33
34
|
break-inside: avoid;
|
|
35
|
+
flex: 1 0 auto;
|
|
34
36
|
`;
|
|
35
37
|
|
|
36
38
|
const StyledList = styled.ul`
|
|
@@ -61,6 +63,7 @@ const StyledButtonContentSelected = styled.span`
|
|
|
61
63
|
|
|
62
64
|
const StyledRemoveWrapper = styled.div`
|
|
63
65
|
margin-top: ${spacing.xsmall};
|
|
66
|
+
margin-left: 10px;
|
|
64
67
|
`;
|
|
65
68
|
|
|
66
69
|
const ButtonRemoveText = styled.span`
|
|
@@ -73,8 +76,8 @@ export type ItemProps = {
|
|
|
73
76
|
value: string;
|
|
74
77
|
selected?: boolean;
|
|
75
78
|
};
|
|
76
|
-
type
|
|
77
|
-
heading
|
|
79
|
+
export type FilterButtonsProps = {
|
|
80
|
+
heading?: string;
|
|
78
81
|
items: ItemProps[];
|
|
79
82
|
onFilterToggle: (value: string) => void;
|
|
80
83
|
onRemoveAllFilters: () => void;
|
|
@@ -83,7 +86,7 @@ type Props = {
|
|
|
83
86
|
};
|
|
84
87
|
};
|
|
85
88
|
|
|
86
|
-
export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilters, labels }:
|
|
89
|
+
export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilters, labels }: FilterButtonsProps) => {
|
|
87
90
|
const { t } = useTranslation();
|
|
88
91
|
const [isNarrowScreen, setIsNarrowScreen] = useState(false);
|
|
89
92
|
|
|
@@ -155,9 +158,7 @@ export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilte
|
|
|
155
158
|
checked={item.selected}
|
|
156
159
|
label={item.label}
|
|
157
160
|
component="div"
|
|
158
|
-
onChange={() =>
|
|
159
|
-
onFilterToggle(item.value);
|
|
160
|
-
}}
|
|
161
|
+
onChange={() => onFilterToggle(item.value)}
|
|
161
162
|
/>
|
|
162
163
|
</StyledListItem>
|
|
163
164
|
))}
|
|
@@ -170,18 +171,16 @@ export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilte
|
|
|
170
171
|
)}
|
|
171
172
|
{!isNarrowScreen && (
|
|
172
173
|
<>
|
|
173
|
-
<StyledHeading>{heading}</StyledHeading>
|
|
174
|
-
<
|
|
174
|
+
{heading && <StyledHeading>{heading}</StyledHeading>}
|
|
175
|
+
<FilterCarousel>
|
|
175
176
|
{items.map((item: ItemProps) => (
|
|
176
177
|
<StyledButtonElementWrapper key={item.value}>
|
|
177
178
|
<Button
|
|
178
179
|
type="button"
|
|
179
180
|
size="normal"
|
|
180
|
-
|
|
181
|
+
greyLighter={!item.selected}
|
|
181
182
|
borderShape="rounded"
|
|
182
|
-
onClick={() =>
|
|
183
|
-
onFilterToggle(item.value);
|
|
184
|
-
}}>
|
|
183
|
+
onClick={() => onFilterToggle(item.value)}>
|
|
185
184
|
<StyledButtonContent>{item.label}</StyledButtonContent>
|
|
186
185
|
{item.selected && (
|
|
187
186
|
<StyledButtonContentSelected>
|
|
@@ -191,7 +190,7 @@ export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilte
|
|
|
191
190
|
</Button>
|
|
192
191
|
</StyledButtonElementWrapper>
|
|
193
192
|
))}
|
|
194
|
-
</
|
|
193
|
+
</FilterCarousel>
|
|
195
194
|
<StyledRemoveWrapper>
|
|
196
195
|
{hasSelectedFilters && (
|
|
197
196
|
<Button onClick={onRemoveAllFilters} link>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2021-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, { ReactChild, useLayoutEffect, useRef, useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import styled from '@emotion/styled';
|
|
12
|
+
import { css } from '@emotion/core';
|
|
13
|
+
import { ChevronLeft, ChevronRight } from '@ndla/icons/common';
|
|
14
|
+
import { useSwipeable } from 'react-swipeable';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
children: ReactChild[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Carousel = styled.div`
|
|
21
|
+
overflow-x: hidden;
|
|
22
|
+
position: relative;
|
|
23
|
+
margin: 0 13px;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const Inner = styled.div`
|
|
27
|
+
flex-wrap: nowrap;
|
|
28
|
+
transition: transform 0.3s;
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
padding: 8px 0;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const NavButton = styled('button')<{
|
|
34
|
+
hide?: boolean;
|
|
35
|
+
alignRight?: boolean;
|
|
36
|
+
}>`
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
width: 48px;
|
|
39
|
+
height: 48px;
|
|
40
|
+
background-color: #fff;
|
|
41
|
+
border: none;
|
|
42
|
+
box-shadow: none;
|
|
43
|
+
border-radius: 100px;
|
|
44
|
+
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
45
|
+
transform: translate(0px, 0px, 1);
|
|
46
|
+
position: absolute;
|
|
47
|
+
bottom: 0;
|
|
48
|
+
top: 4px;
|
|
49
|
+
z-index: 3;
|
|
50
|
+
left: 3px;
|
|
51
|
+
|
|
52
|
+
svg {
|
|
53
|
+
color: #20588f;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
box-shadow: 0 0 4px rgba(0, 0, 0, 0.24);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
${(props) =>
|
|
62
|
+
props.hide &&
|
|
63
|
+
`
|
|
64
|
+
opacity: 0;
|
|
65
|
+
transform: translate(-40px, 0px);
|
|
66
|
+
`}
|
|
67
|
+
|
|
68
|
+
${(props) =>
|
|
69
|
+
props.alignRight &&
|
|
70
|
+
`
|
|
71
|
+
right: 2px;
|
|
72
|
+
left: unset;
|
|
73
|
+
`}
|
|
74
|
+
|
|
75
|
+
${(props) =>
|
|
76
|
+
props.alignRight &&
|
|
77
|
+
props.hide &&
|
|
78
|
+
`
|
|
79
|
+
transform: translate(40px, 0px);
|
|
80
|
+
`}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const handlerWrapperCSS = css`
|
|
84
|
+
position: relative;
|
|
85
|
+
max-width: 100%;
|
|
86
|
+
overflow-x: hidden;
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const FilterCarousel = ({ children }: Props) => {
|
|
90
|
+
const { t } = useTranslation();
|
|
91
|
+
const [hideNext, setHideNext] = useState(false);
|
|
92
|
+
const [translateX, setTranslateX] = useState(0);
|
|
93
|
+
const carouselRef: { current: HTMLDivElement | null } = useRef(null);
|
|
94
|
+
const innerRef: { current: HTMLDivElement | null } = useRef(null);
|
|
95
|
+
|
|
96
|
+
// Check if we need to show the nav buttons
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
if (carouselRef.current && innerRef.current) {
|
|
99
|
+
const carouselWidth = carouselRef.current.offsetWidth || 0;
|
|
100
|
+
const innerWidth = innerRef.current.scrollWidth;
|
|
101
|
+
if (innerWidth <= carouselWidth) {
|
|
102
|
+
setHideNext(true);
|
|
103
|
+
} else {
|
|
104
|
+
setHideNext(false);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
setHideNext(true);
|
|
108
|
+
}
|
|
109
|
+
}, [children]);
|
|
110
|
+
|
|
111
|
+
const updateIndex = (direction: string) => {
|
|
112
|
+
const carousel = carouselRef.current;
|
|
113
|
+
const inner = innerRef.current;
|
|
114
|
+
if (carousel && inner) {
|
|
115
|
+
const carouselWidth = carousel.offsetWidth;
|
|
116
|
+
const innerWidth = inner.scrollWidth;
|
|
117
|
+
if (direction === 'NEXT') {
|
|
118
|
+
// If we cannot move more than one carouselWidth then just adjust to show all of the concealed items
|
|
119
|
+
if (carouselWidth + translateX + carouselWidth > innerWidth) {
|
|
120
|
+
const newOffset = innerWidth - carouselWidth;
|
|
121
|
+
setTranslateX(translateX + newOffset);
|
|
122
|
+
setHideNext(true);
|
|
123
|
+
} else {
|
|
124
|
+
if (hideNext) {
|
|
125
|
+
setHideNext(false);
|
|
126
|
+
}
|
|
127
|
+
setTranslateX(translateX + carouselWidth);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (translateX - carouselWidth > 0) {
|
|
131
|
+
setTranslateX(translateX - carouselWidth);
|
|
132
|
+
} else {
|
|
133
|
+
setTranslateX(0);
|
|
134
|
+
}
|
|
135
|
+
if (hideNext) {
|
|
136
|
+
setHideNext(false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handlers = useSwipeable({
|
|
143
|
+
onSwipedLeft: () => updateIndex('NEXT'),
|
|
144
|
+
onSwipedRight: () => updateIndex('PREV'),
|
|
145
|
+
preventDefaultTouchmoveEvent: false,
|
|
146
|
+
trackMouse: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div {...handlers} css={handlerWrapperCSS}>
|
|
151
|
+
<Carousel ref={carouselRef}>
|
|
152
|
+
<Inner ref={innerRef} style={{ transform: `translateX(-${translateX}px)` }}>
|
|
153
|
+
{children}
|
|
154
|
+
</Inner>
|
|
155
|
+
</Carousel>
|
|
156
|
+
<NavButton title={t('carousel.back')} onClick={() => updateIndex('PREV')} hide={translateX < 1}>
|
|
157
|
+
<ChevronLeft style={{ width: '18px', height: '18px' }} aria-hidden title="" />
|
|
158
|
+
</NavButton>
|
|
159
|
+
<NavButton title={t('carousel.forward')} onClick={() => updateIndex('NEXT')} alignRight hide={hideNext}>
|
|
160
|
+
<ChevronRight style={{ width: '18px', height: '18px' }} aria-hidden title="" />
|
|
161
|
+
</NavButton>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export default FilterCarousel;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { ReactNode } from 'react';
|
|
9
|
+
import React, { ReactNode, useEffect, useRef } from 'react';
|
|
10
10
|
import BEMHelper from 'react-bem-helper';
|
|
11
11
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
12
12
|
import { DisplayOnPageYOffset } from '../Animation';
|
|
@@ -56,23 +56,47 @@ export const Masthead = ({
|
|
|
56
56
|
ndlaFilm,
|
|
57
57
|
skipToMainContentId,
|
|
58
58
|
t,
|
|
59
|
-
}: Props & WithTranslation) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
}: Props & WithTranslation) => {
|
|
60
|
+
const mastheadRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const focusHandler = (evt: FocusEvent) => {
|
|
62
|
+
const mastheadHeight = (mastheadRef.current && mastheadRef.current.offsetHeight) || 0;
|
|
63
|
+
const { target } = evt;
|
|
64
|
+
const rect = (target as HTMLElement).getBoundingClientRect();
|
|
65
|
+
// Focused target is hidden behind Masthead
|
|
66
|
+
if (rect.y < mastheadHeight) {
|
|
67
|
+
window.scrollTo(window.scrollX, window.scrollY - (mastheadHeight + 10));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (fixed) {
|
|
73
|
+
document.addEventListener('focusin', focusHandler);
|
|
74
|
+
return () => {
|
|
75
|
+
document.removeEventListener('focusin', focusHandler);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}, [fixed]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{skipToMainContentId && (
|
|
83
|
+
<a tabIndex={0} href={`#${skipToMainContentId}`} {...classes('skip-to-main-content')}>
|
|
84
|
+
{t('masthead.skipToContent')}
|
|
85
|
+
</a>
|
|
72
86
|
)}
|
|
73
|
-
<div
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
<div {...classes('placeholder', { infoContent: !!infoContent })} />
|
|
88
|
+
<div
|
|
89
|
+
{...classes('', { fixed: !!fixed, infoContent: !!infoContent, showLoaderWhenNeeded, ndlaFilm: !!ndlaFilm })}
|
|
90
|
+
ref={mastheadRef}>
|
|
91
|
+
{infoContent && (
|
|
92
|
+
<DisplayOnPageYOffset yOffsetMin={0} yOffsetMax={90}>
|
|
93
|
+
<MastheadInfo>{infoContent}</MastheadInfo>
|
|
94
|
+
</DisplayOnPageYOffset>
|
|
95
|
+
)}
|
|
96
|
+
<div className={`u-1/1 ${classes('content').className}`}>{children}</div>
|
|
97
|
+
</div>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
77
101
|
|
|
78
102
|
export default withTranslation()(Masthead);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2022-present, NDLA.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the GPLv3 license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree. *
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect } from 'react';
|
|
9
|
+
//@ts-ignore
|
|
10
|
+
import { initArticleScripts } from '@ndla/article-scripts';
|
|
11
|
+
import { Notion } from '.';
|
|
12
|
+
import { NotionImage } from './NotionImage';
|
|
13
|
+
import NotionVisualElement, { NotionVisualElementType } from './NotionVisualElement';
|
|
14
|
+
import FigureNotion from './FigureNotion';
|
|
15
|
+
import { Copyright } from '../types';
|
|
16
|
+
|
|
17
|
+
export interface ConceptNotionType {
|
|
18
|
+
id: number;
|
|
19
|
+
copyright?: Partial<Copyright>;
|
|
20
|
+
title: string;
|
|
21
|
+
text: string;
|
|
22
|
+
subjectNames?: string[];
|
|
23
|
+
visualElement?: NotionVisualElementType;
|
|
24
|
+
image?: {
|
|
25
|
+
src: string;
|
|
26
|
+
alt: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface Props {
|
|
30
|
+
concept: ConceptNotionType;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ConceptNotion = ({ concept }: Props) => {
|
|
34
|
+
const notionId = `notion-${concept.id}`;
|
|
35
|
+
const figureId = `notion-figure-${concept.id}`;
|
|
36
|
+
const visualElementId = `visual-element-${concept.id}`;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
initArticleScripts();
|
|
40
|
+
}, []);
|
|
41
|
+
return (
|
|
42
|
+
<FigureNotion
|
|
43
|
+
id={figureId}
|
|
44
|
+
figureId={visualElementId}
|
|
45
|
+
copyright={concept.copyright}
|
|
46
|
+
title={concept.title}
|
|
47
|
+
licenseString={concept.copyright?.license?.license ?? ''}
|
|
48
|
+
type="concept">
|
|
49
|
+
<Notion
|
|
50
|
+
id={notionId}
|
|
51
|
+
title={concept.title}
|
|
52
|
+
text={concept.text}
|
|
53
|
+
labels={concept.subjectNames ?? []}
|
|
54
|
+
imageElement={
|
|
55
|
+
concept.visualElement?.resource === 'image' && concept.visualElement.image ? (
|
|
56
|
+
<NotionImage
|
|
57
|
+
id={visualElementId}
|
|
58
|
+
src={concept.visualElement.image.src}
|
|
59
|
+
alt={concept.visualElement.image.alt ?? ''}
|
|
60
|
+
imageCopyright={concept.visualElement.copyright}
|
|
61
|
+
/>
|
|
62
|
+
) : undefined
|
|
63
|
+
}
|
|
64
|
+
visualElement={
|
|
65
|
+
concept.visualElement && concept.visualElement.resource !== 'image' && concept.visualElement.url
|
|
66
|
+
? {
|
|
67
|
+
type: concept.visualElement.resource === 'brightcove' ? 'video' : 'other',
|
|
68
|
+
metaImage: concept.image && {
|
|
69
|
+
url: concept.image.src,
|
|
70
|
+
alt: concept.image.alt,
|
|
71
|
+
},
|
|
72
|
+
element: <NotionVisualElement visualElement={concept.visualElement} />,
|
|
73
|
+
}
|
|
74
|
+
: undefined
|
|
75
|
+
}></Notion>
|
|
76
|
+
</FigureNotion>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default ConceptNotion;
|