@ndla/ui 26.0.0 → 27.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/es/Breadcrumb/Breadcrumb.js +3 -4
- package/es/MyNdla/Resource/Folder.js +32 -14
- package/es/Resource/BlockResource.js +43 -61
- package/es/Resource/ListResource.js +44 -23
- package/es/Resource/resourceComponents.js +64 -38
- package/es/TreeStructure/ComboboxButton.js +162 -0
- package/es/TreeStructure/FolderItem.js +98 -78
- package/es/TreeStructure/FolderItems.js +25 -14
- package/es/TreeStructure/FolderNameInput.js +40 -33
- package/es/TreeStructure/NavigationLink.js +18 -10
- package/es/TreeStructure/TreeStructure.js +92 -165
- package/es/TreeStructure/arrowNavigation.js +3 -3
- package/es/TreeStructure/helperFunctions.js +3 -0
- package/es/locale/messages-en.js +8 -2
- package/es/locale/messages-nb.js +8 -2
- package/es/locale/messages-nn.js +8 -2
- package/es/locale/messages-se.js +8 -2
- package/es/locale/messages-sma.js +8 -2
- package/lib/Breadcrumb/Breadcrumb.js +3 -5
- package/lib/MyNdla/Resource/Folder.d.ts +2 -1
- package/lib/MyNdla/Resource/Folder.js +37 -14
- package/lib/Resource/BlockResource.d.ts +2 -1
- package/lib/Resource/BlockResource.js +48 -61
- package/lib/Resource/ListResource.d.ts +2 -1
- package/lib/Resource/ListResource.js +49 -23
- package/lib/Resource/resourceComponents.d.ts +6 -1
- package/lib/Resource/resourceComponents.js +64 -37
- package/lib/TreeStructure/ComboboxButton.d.ts +28 -0
- package/lib/TreeStructure/ComboboxButton.js +176 -0
- package/lib/TreeStructure/FolderItem.d.ts +1 -1
- package/lib/TreeStructure/FolderItem.js +99 -77
- package/lib/TreeStructure/FolderItems.d.ts +4 -2
- package/lib/TreeStructure/FolderItems.js +26 -14
- package/lib/TreeStructure/FolderNameInput.d.ts +3 -1
- package/lib/TreeStructure/FolderNameInput.js +41 -32
- package/lib/TreeStructure/NavigationLink.d.ts +1 -1
- package/lib/TreeStructure/NavigationLink.js +18 -10
- package/lib/TreeStructure/TreeStructure.d.ts +2 -2
- package/lib/TreeStructure/TreeStructure.js +92 -165
- package/lib/TreeStructure/arrowNavigation.d.ts +2 -1
- package/lib/TreeStructure/arrowNavigation.js +3 -3
- package/lib/TreeStructure/helperFunctions.d.ts +2 -1
- package/lib/TreeStructure/helperFunctions.js +8 -2
- package/lib/TreeStructure/types.d.ts +6 -7
- package/lib/locale/messages-en.d.ts +6 -0
- package/lib/locale/messages-en.js +8 -2
- package/lib/locale/messages-nb.d.ts +6 -0
- package/lib/locale/messages-nb.js +8 -2
- package/lib/locale/messages-nn.d.ts +6 -0
- package/lib/locale/messages-nn.js +8 -2
- package/lib/locale/messages-se.d.ts +6 -0
- package/lib/locale/messages-se.js +8 -2
- package/lib/locale/messages-sma.d.ts +6 -0
- package/lib/locale/messages-sma.js +8 -2
- package/package.json +11 -11
- package/src/Breadcrumb/Breadcrumb.tsx +1 -2
- package/src/MyNdla/Resource/Folder.tsx +21 -5
- package/src/Resource/BlockResource.tsx +43 -33
- package/src/Resource/ListResource.tsx +37 -29
- package/src/Resource/resourceComponents.tsx +60 -26
- package/src/TreeStructure/ComboboxButton.tsx +189 -0
- package/src/TreeStructure/FolderItem.tsx +89 -70
- package/src/TreeStructure/FolderItems.tsx +36 -16
- package/src/TreeStructure/FolderNameInput.tsx +43 -18
- package/src/TreeStructure/NavigationLink.tsx +17 -10
- package/src/TreeStructure/TreeStructure.tsx +63 -139
- package/src/TreeStructure/arrowNavigation.ts +7 -6
- package/src/TreeStructure/helperFunctions.ts +5 -1
- package/src/TreeStructure/types.ts +6 -7
- package/src/locale/messages-en.ts +7 -0
- package/src/locale/messages-nb.ts +6 -0
- package/src/locale/messages-nn.ts +6 -0
- package/src/locale/messages-se.ts +7 -0
- package/src/locale/messages-sma.ts +7 -0
|
@@ -7,15 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import styled from '@emotion/styled';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import SafeLink from '@ndla/safelink';
|
|
10
|
+
import React, { useRef } from 'react';
|
|
12
11
|
import { colors, fonts, spacing } from '@ndla/core';
|
|
13
12
|
import { MenuButton, MenuItemProps } from '@ndla/button';
|
|
14
13
|
import Image from '../Image';
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CompressedTagList,
|
|
16
|
+
ResourceImageProps,
|
|
17
|
+
ResourceTitle,
|
|
18
|
+
Row,
|
|
19
|
+
TopicList,
|
|
20
|
+
ResourceTitleLink,
|
|
21
|
+
LoaderProps,
|
|
22
|
+
} from './resourceComponents';
|
|
16
23
|
import ContentLoader from '../ContentLoader';
|
|
17
24
|
|
|
18
25
|
interface BlockResourceProps {
|
|
26
|
+
id: string;
|
|
19
27
|
link: string;
|
|
20
28
|
tagLinkPrefix?: string;
|
|
21
29
|
title: string;
|
|
@@ -27,7 +35,7 @@ interface BlockResourceProps {
|
|
|
27
35
|
isLoading?: boolean;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
const BlockElementWrapper = styled
|
|
38
|
+
const BlockElementWrapper = styled.div`
|
|
31
39
|
display: flex;
|
|
32
40
|
text-decoration: none;
|
|
33
41
|
box-shadow: none;
|
|
@@ -37,6 +45,16 @@ const BlockElementWrapper = styled(SafeLink)`
|
|
|
37
45
|
border: 1px solid ${colors.brand.light};
|
|
38
46
|
border-radius: 2px;
|
|
39
47
|
color: ${colors.brand.greyDark};
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
box-shadow: 1px 1px 6px 2px rgba(9, 55, 101, 0.08);
|
|
52
|
+
transition-duration: 0.2s;
|
|
53
|
+
${() => ResourceTitleLink} {
|
|
54
|
+
color: ${colors.brand.primary};
|
|
55
|
+
text-decoration: underline;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
40
58
|
`;
|
|
41
59
|
|
|
42
60
|
const BlockDescription = styled.p`
|
|
@@ -48,7 +66,8 @@ const BlockDescription = styled.p`
|
|
|
48
66
|
overflow: hidden;
|
|
49
67
|
text-overflow: ellipsis;
|
|
50
68
|
transition: height 0.2s ease-out;
|
|
51
|
-
${() => BlockElementWrapper}:hover &, ${() => BlockElementWrapper}:focus & {
|
|
69
|
+
${() => BlockElementWrapper}:hover &, ${() => BlockElementWrapper}:focus & , ${() =>
|
|
70
|
+
BlockElementWrapper}:focus-within & {
|
|
52
71
|
// Unfortunate css needed for multi-line text overflow ellipsis.
|
|
53
72
|
height: 3.1em;
|
|
54
73
|
-webkit-line-clamp: 2;
|
|
@@ -59,6 +78,7 @@ const BlockDescription = styled.p`
|
|
|
59
78
|
|
|
60
79
|
const RightRow = styled(Row)`
|
|
61
80
|
justify-content: flex-end;
|
|
81
|
+
margin-bottom: -${spacing.xxsmall};
|
|
62
82
|
`;
|
|
63
83
|
|
|
64
84
|
const BlockInfoWrapper = styled.div`
|
|
@@ -97,28 +117,7 @@ const BlockImage = ({ image, loading }: BlockImageProps) => {
|
|
|
97
117
|
return <Image alt={image.alt} src={image.src} fallbackWidth={300} />;
|
|
98
118
|
};
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
title: string;
|
|
102
|
-
loading?: boolean;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const BlockTitle = ({ title, loading }: BlockTitleProps) => {
|
|
106
|
-
if (loading) {
|
|
107
|
-
return (
|
|
108
|
-
<ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
109
|
-
<rect x="0" y="0" rx="3" ry="3" width="100%" height="18px" />
|
|
110
|
-
</ContentLoader>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
return <ResourceTitle>{title}</ResourceTitle>;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
interface BlockTopicListProps {
|
|
117
|
-
topics: string[];
|
|
118
|
-
loading?: boolean;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const BlockTopicList = ({ topics, loading }: BlockTopicListProps) => {
|
|
120
|
+
const TopicAndTitleLoader = ({ children, loading }: LoaderProps) => {
|
|
122
121
|
if (loading) {
|
|
123
122
|
return (
|
|
124
123
|
<ContentLoader height={'18px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
@@ -128,10 +127,11 @@ const BlockTopicList = ({ topics, loading }: BlockTopicListProps) => {
|
|
|
128
127
|
);
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
return
|
|
130
|
+
return <>{children}</>;
|
|
132
131
|
};
|
|
133
132
|
|
|
134
133
|
const BlockResource = ({
|
|
134
|
+
id,
|
|
135
135
|
link,
|
|
136
136
|
tagLinkPrefix,
|
|
137
137
|
title,
|
|
@@ -142,16 +142,26 @@ const BlockResource = ({
|
|
|
142
142
|
menuItems,
|
|
143
143
|
isLoading,
|
|
144
144
|
}: BlockResourceProps) => {
|
|
145
|
+
const linkRef = useRef<HTMLAnchorElement>(null);
|
|
146
|
+
|
|
147
|
+
const handleClick = () => {
|
|
148
|
+
if (linkRef.current) {
|
|
149
|
+
linkRef.current.click();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
145
153
|
return (
|
|
146
|
-
<BlockElementWrapper
|
|
154
|
+
<BlockElementWrapper onClick={handleClick} id={id}>
|
|
147
155
|
<ImageWrapper>
|
|
148
156
|
<BlockImage image={resourceImage} loading={isLoading} />
|
|
149
157
|
</ImageWrapper>
|
|
150
158
|
<BlockInfoWrapper>
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
<TopicAndTitleLoader loading={isLoading}>
|
|
160
|
+
<ResourceTitleLink title={title} to={link} ref={linkRef}>
|
|
161
|
+
<ResourceTitle>{title}</ResourceTitle>
|
|
162
|
+
</ResourceTitleLink>
|
|
163
|
+
</TopicAndTitleLoader>
|
|
164
|
+
<TopicList topics={topics} />
|
|
155
165
|
<BlockDescription>{description}</BlockDescription>
|
|
156
166
|
<RightRow>
|
|
157
167
|
{tags && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import styled from '@emotion/styled';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import SafeLink from '@ndla/safelink';
|
|
10
|
+
import React, { useRef } from 'react';
|
|
12
11
|
import { fonts, spacing, colors, breakpoints, mq } from '@ndla/core';
|
|
13
12
|
import { MenuButton, MenuItemProps } from '@ndla/button';
|
|
14
13
|
import Image from '../Image';
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CompressedTagList,
|
|
16
|
+
ResourceImageProps,
|
|
17
|
+
ResourceTitle,
|
|
18
|
+
ResourceTitleLink,
|
|
19
|
+
TopicList,
|
|
20
|
+
LoaderProps,
|
|
21
|
+
} from './resourceComponents';
|
|
16
22
|
import ContentLoader from '../ContentLoader';
|
|
17
23
|
|
|
18
24
|
const StyledResourceDescription = styled.p`
|
|
@@ -31,7 +37,7 @@ const StyledResourceDescription = styled.p`
|
|
|
31
37
|
-webkit-box-orient: vertical;
|
|
32
38
|
`;
|
|
33
39
|
|
|
34
|
-
const ResourceWrapper = styled
|
|
40
|
+
const ResourceWrapper = styled.div`
|
|
35
41
|
flex: 1;
|
|
36
42
|
display: grid;
|
|
37
43
|
grid-template-columns: auto 1fr auto;
|
|
@@ -47,38 +53,38 @@ const ResourceWrapper = styled(SafeLink)`
|
|
|
47
53
|
'tags tags';
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
box-shadow: none;
|
|
56
|
+
cursor: pointer;
|
|
52
57
|
padding: ${spacing.small};
|
|
53
58
|
border: 1px solid ${colors.brand.neutral7};
|
|
54
59
|
border-radius: 2px;
|
|
55
|
-
color: ${colors.brand.greyDark};
|
|
56
60
|
gap: 0 ${spacing.small};
|
|
57
61
|
|
|
58
62
|
&:hover {
|
|
59
63
|
box-shadow: 1px 1px 6px 2px rgba(9, 55, 101, 0.08);
|
|
60
64
|
transition-duration: 0.2s;
|
|
61
|
-
${
|
|
65
|
+
${() => ResourceTitleLink} {
|
|
62
66
|
color: ${colors.brand.primary};
|
|
63
67
|
text-decoration: underline;
|
|
64
68
|
}
|
|
65
|
-
a {
|
|
66
|
-
display: flex;
|
|
67
|
-
align-items: center;
|
|
68
|
-
}
|
|
69
69
|
}
|
|
70
70
|
`;
|
|
71
71
|
|
|
72
72
|
const TagsandActionMenu = styled.div`
|
|
73
|
+
box-sizing: content-box;
|
|
74
|
+
padding: 2px;
|
|
73
75
|
grid-area: tags;
|
|
74
76
|
display: flex;
|
|
75
77
|
align-items: center;
|
|
76
78
|
width: 100%;
|
|
77
79
|
overflow: hidden;
|
|
78
|
-
gap: ${spacing.small};
|
|
79
80
|
align-self: flex-start;
|
|
80
81
|
justify-self: flex-end;
|
|
81
82
|
justify-content: flex-end;
|
|
83
|
+
|
|
84
|
+
${mq.range({ from: breakpoints.mobileWide })} {
|
|
85
|
+
margin-top: -${spacing.xsmall};
|
|
86
|
+
margin-right: -${spacing.xxsmall};
|
|
87
|
+
}
|
|
82
88
|
`;
|
|
83
89
|
|
|
84
90
|
const StyledImageWrapper = styled.div<StyledImageProps>`
|
|
@@ -100,7 +106,7 @@ const StyledImage = styled(Image)`
|
|
|
100
106
|
|
|
101
107
|
const TopicAndTitleWrapper = styled.div`
|
|
102
108
|
grid-area: topicAndTitle;
|
|
103
|
-
margin-top:
|
|
109
|
+
margin-top: 2px;
|
|
104
110
|
`;
|
|
105
111
|
|
|
106
112
|
interface StyledImageProps {
|
|
@@ -108,6 +114,7 @@ interface StyledImageProps {
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
export interface ListResourceProps {
|
|
117
|
+
id: string;
|
|
111
118
|
link: string;
|
|
112
119
|
tagLinkPrefix?: string;
|
|
113
120
|
title: string;
|
|
@@ -145,13 +152,7 @@ const ListResourceImage = ({ resourceImage, loading, type }: ListResourceImagePr
|
|
|
145
152
|
);
|
|
146
153
|
};
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
title: string;
|
|
150
|
-
topics: string[];
|
|
151
|
-
loading?: boolean;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
|
|
155
|
+
const TopicAndTitleLoader = ({ loading, children }: LoaderProps) => {
|
|
155
156
|
if (loading) {
|
|
156
157
|
return (
|
|
157
158
|
<ContentLoader height={'40px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
@@ -161,12 +162,7 @@ const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
|
|
|
161
162
|
</ContentLoader>
|
|
162
163
|
);
|
|
163
164
|
}
|
|
164
|
-
return
|
|
165
|
-
<>
|
|
166
|
-
<ResourceTitle>{title}</ResourceTitle>
|
|
167
|
-
<TopicList topics={topics} />
|
|
168
|
-
</>
|
|
169
|
-
);
|
|
165
|
+
return <>{children}</>;
|
|
170
166
|
};
|
|
171
167
|
|
|
172
168
|
interface ResourceDescriptionProps {
|
|
@@ -186,6 +182,7 @@ const ResourceDescription = ({ description, loading }: ResourceDescriptionProps)
|
|
|
186
182
|
};
|
|
187
183
|
|
|
188
184
|
const ListResource = ({
|
|
185
|
+
id,
|
|
189
186
|
link,
|
|
190
187
|
tagLinkPrefix,
|
|
191
188
|
title,
|
|
@@ -198,14 +195,25 @@ const ListResource = ({
|
|
|
198
195
|
}: ListResourceProps) => {
|
|
199
196
|
const showDescription = description !== undefined;
|
|
200
197
|
const imageType = showDescription ? 'normal' : 'compact';
|
|
198
|
+
const linkRef = useRef<HTMLAnchorElement>(null);
|
|
199
|
+
const handleClick = () => {
|
|
200
|
+
if (linkRef.current) {
|
|
201
|
+
linkRef.current.click();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
201
204
|
|
|
202
205
|
return (
|
|
203
|
-
<ResourceWrapper
|
|
206
|
+
<ResourceWrapper onClick={handleClick} id={id}>
|
|
204
207
|
<StyledImageWrapper imageSize={imageType}>
|
|
205
208
|
<ListResourceImage resourceImage={resourceImage} loading={isLoading} type={imageType} />
|
|
206
209
|
</StyledImageWrapper>
|
|
207
210
|
<TopicAndTitleWrapper>
|
|
208
|
-
<
|
|
211
|
+
<TopicAndTitleLoader loading={isLoading}>
|
|
212
|
+
<ResourceTitleLink to={link} ref={linkRef}>
|
|
213
|
+
<ResourceTitle>{title}</ResourceTitle>
|
|
214
|
+
</ResourceTitleLink>
|
|
215
|
+
<TopicList topics={topics} />
|
|
216
|
+
</TopicAndTitleLoader>
|
|
209
217
|
</TopicAndTitleWrapper>
|
|
210
218
|
{showDescription && <ResourceDescription description={description} loading={isLoading} />}
|
|
211
219
|
<TagsandActionMenu>
|
|
@@ -7,19 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import styled from '@emotion/styled';
|
|
10
|
-
import { colors, fonts, spacing } from '@ndla/core';
|
|
11
|
-
import React, { MouseEvent } from 'react';
|
|
12
|
-
|
|
10
|
+
import { colors, fonts, misc, spacing } from '@ndla/core';
|
|
11
|
+
import React, { MouseEvent, ReactNode } from 'react';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
13
|
import { MenuButton } from '@ndla/button';
|
|
14
14
|
import SafeLink from '@ndla/safelink';
|
|
15
15
|
import { useNavigate } from 'react-router-dom';
|
|
16
|
+
import { HashTag } from '@ndla/icons/common';
|
|
16
17
|
|
|
17
18
|
export interface ResourceImageProps {
|
|
18
19
|
alt: string;
|
|
19
20
|
src: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export const
|
|
23
|
+
export const ResourceTitleLink = styled(SafeLink)`
|
|
24
|
+
box-shadow: none;
|
|
25
|
+
color: ${colors.brand.primary};
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const ResourceTitle = styled.h2`
|
|
23
29
|
min-width: 50px;
|
|
24
30
|
margin: 0;
|
|
25
31
|
flex: 1;
|
|
@@ -32,13 +38,14 @@ export const ResourceTitle = styled.h3`
|
|
|
32
38
|
line-clamp: 1;
|
|
33
39
|
-webkit-box-orient: vertical;
|
|
34
40
|
grid-area: resourceTitle;
|
|
41
|
+
${fonts.sizes('18px', '18px')};
|
|
35
42
|
`;
|
|
36
43
|
|
|
37
44
|
const StyledTagList = styled.ul`
|
|
38
45
|
list-style: none;
|
|
39
46
|
display: flex;
|
|
40
47
|
margin: 0;
|
|
41
|
-
padding:
|
|
48
|
+
padding: 2px;
|
|
42
49
|
gap: ${spacing.xsmall};
|
|
43
50
|
overflow: hidden;
|
|
44
51
|
`;
|
|
@@ -49,11 +56,10 @@ const StyledTagListElement = styled.li`
|
|
|
49
56
|
`;
|
|
50
57
|
|
|
51
58
|
const StyledSafeLink = styled(SafeLink)`
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
52
61
|
box-shadow: none;
|
|
53
62
|
color: ${colors.brand.grey};
|
|
54
|
-
::before {
|
|
55
|
-
content: '#';
|
|
56
|
-
}
|
|
57
63
|
&:hover {
|
|
58
64
|
color: ${colors.brand.primary};
|
|
59
65
|
}
|
|
@@ -68,16 +74,18 @@ const StyledTopicList = styled.ul`
|
|
|
68
74
|
grid-area: topicList;
|
|
69
75
|
`;
|
|
70
76
|
|
|
77
|
+
const StyledTopicDivider = styled.span`
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 0 ${spacing.xxsmall};
|
|
80
|
+
`;
|
|
81
|
+
|
|
71
82
|
const StyledTopicListElement = styled.li`
|
|
72
83
|
${fonts.sizes(12)};
|
|
73
84
|
margin: 0;
|
|
74
85
|
line-height: 1.5;
|
|
75
86
|
padding: 0;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const StyledTopicDivider = styled.span`
|
|
79
|
-
margin: 0;
|
|
80
|
-
padding: 0 ${spacing.xxsmall};
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
81
89
|
`;
|
|
82
90
|
|
|
83
91
|
export const Row = styled.div`
|
|
@@ -86,27 +94,36 @@ export const Row = styled.div`
|
|
|
86
94
|
gap: ${spacing.xsmall};
|
|
87
95
|
`;
|
|
88
96
|
|
|
89
|
-
const TagCounterWrapper = styled.
|
|
90
|
-
color: ${colors.brand.
|
|
97
|
+
const TagCounterWrapper = styled.span`
|
|
98
|
+
color: ${colors.brand.secondary};
|
|
91
99
|
box-shadow: none;
|
|
92
100
|
margin: 0;
|
|
93
101
|
font-weight: ${fonts.weight.semibold};
|
|
94
|
-
${fonts.sizes(
|
|
102
|
+
${fonts.sizes('14px', '14px')};
|
|
103
|
+
padding: 5px;
|
|
95
104
|
`;
|
|
96
105
|
|
|
97
106
|
interface TagListProps {
|
|
98
107
|
tags?: string[];
|
|
99
108
|
tagLinkPrefix?: string;
|
|
100
109
|
}
|
|
110
|
+
|
|
111
|
+
export interface LoaderProps {
|
|
112
|
+
loading?: boolean;
|
|
113
|
+
children?: ReactNode;
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
export const TagList = ({ tags, tagLinkPrefix }: TagListProps) => {
|
|
117
|
+
const { t } = useTranslation();
|
|
102
118
|
if (!tags) return null;
|
|
103
119
|
return (
|
|
104
|
-
<StyledTagList>
|
|
120
|
+
<StyledTagList aria-label={t('myNdla.tagList')}>
|
|
105
121
|
{tags.map((tag, i) => (
|
|
106
122
|
<StyledTagListElement key={`tag-${i}`}>
|
|
107
123
|
<StyledSafeLink
|
|
108
124
|
onClick={(e: MouseEvent<HTMLAnchorElement | HTMLElement>) => e.stopPropagation()}
|
|
109
125
|
to={`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`}>
|
|
126
|
+
<HashTag />
|
|
110
127
|
{tag}
|
|
111
128
|
</StyledSafeLink>
|
|
112
129
|
</StyledTagListElement>
|
|
@@ -120,25 +137,39 @@ interface CompressedTagListProps {
|
|
|
120
137
|
tagLinkPrefix?: string;
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
const TagMenuButton = styled(MenuButton)`
|
|
141
|
+
&:hover,
|
|
142
|
+
&:active,
|
|
143
|
+
&:focus {
|
|
144
|
+
transition: ${misc.transition.default};
|
|
145
|
+
border-radius: 100%;
|
|
146
|
+
background-color: ${colors.brand.light};
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
|
|
123
150
|
export const CompressedTagList = ({ tags, tagLinkPrefix }: CompressedTagListProps) => {
|
|
124
151
|
const navigate = useNavigate();
|
|
152
|
+
const { t } = useTranslation();
|
|
125
153
|
const visibleTags = tags.slice(0, 3);
|
|
126
154
|
const remainingTags = tags.slice(3, tags.length).map((tag) => {
|
|
127
155
|
return {
|
|
128
|
-
|
|
156
|
+
icon: <HashTag />,
|
|
157
|
+
text: tag,
|
|
129
158
|
onClick: () => {
|
|
130
159
|
navigate(`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`);
|
|
131
160
|
},
|
|
132
161
|
};
|
|
133
162
|
});
|
|
134
|
-
|
|
135
163
|
return (
|
|
136
164
|
<>
|
|
137
165
|
<TagList tagLinkPrefix={tagLinkPrefix} tags={visibleTags} />
|
|
138
166
|
{remainingTags.length > 0 && (
|
|
139
|
-
<
|
|
167
|
+
<TagMenuButton
|
|
168
|
+
hideMenuIcon={true}
|
|
169
|
+
menuItems={remainingTags}
|
|
170
|
+
aria-label={t('myNdla.moreTags', { count: remainingTags.length })}>
|
|
140
171
|
<TagCounterWrapper>{`+${remainingTags.length}`}</TagCounterWrapper>
|
|
141
|
-
</
|
|
172
|
+
</TagMenuButton>
|
|
142
173
|
)}
|
|
143
174
|
</>
|
|
144
175
|
);
|
|
@@ -149,14 +180,17 @@ interface TopicListProps {
|
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
export const TopicList = ({ topics }: TopicListProps) => {
|
|
183
|
+
const { t } = useTranslation();
|
|
152
184
|
if (!topics) return null;
|
|
153
185
|
return (
|
|
154
|
-
<StyledTopicList>
|
|
186
|
+
<StyledTopicList aria-label={t('navigation.topics')}>
|
|
155
187
|
{topics.map((topic, i) => (
|
|
156
|
-
|
|
157
|
-
{topic}
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
<>
|
|
189
|
+
<StyledTopicListElement key={topic}>
|
|
190
|
+
{topic}
|
|
191
|
+
{i !== topics.length - 1 && <StyledTopicDivider aria-hidden="true">•</StyledTopicDivider>}
|
|
192
|
+
</StyledTopicListElement>
|
|
193
|
+
</>
|
|
160
194
|
))}
|
|
161
195
|
</StyledTopicList>
|
|
162
196
|
);
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
|
|
9
|
+
import React, { KeyboardEvent } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import styled from '@emotion/styled';
|
|
12
|
+
import { useForwardedRef } from '@ndla/util';
|
|
13
|
+
import Tooltip from '@ndla/tooltip';
|
|
14
|
+
import { colors, spacing } from '@ndla/core';
|
|
15
|
+
import { IFolder } from '@ndla/types-learningpath-api';
|
|
16
|
+
import { Plus } from '@ndla/icons/action';
|
|
17
|
+
import { ChevronUp, ChevronDown } from '@ndla/icons/common';
|
|
18
|
+
import { forwardRef } from 'react';
|
|
19
|
+
import { ButtonV2 as Button, IconButtonV2 as IconButton } from '@ndla/button';
|
|
20
|
+
import { treestructureId } from './helperFunctions';
|
|
21
|
+
import { FolderType, TreeStructureType } from './types';
|
|
22
|
+
import { arrowNavigation } from './arrowNavigation';
|
|
23
|
+
|
|
24
|
+
interface StyledRowProps {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const StyledRow = styled.div<StyledRowProps>`
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
padding: ${spacing.xxsmall};
|
|
32
|
+
border-bottom: ${({ isOpen }) => isOpen && `1px solid ${colors.brand.tertiary}`};
|
|
33
|
+
`;
|
|
34
|
+
const StyledSelectedFolder = styled(Button)`
|
|
35
|
+
flex: 1;
|
|
36
|
+
justify-content: flex-start;
|
|
37
|
+
color: ${colors.black};
|
|
38
|
+
:hover,
|
|
39
|
+
:focus {
|
|
40
|
+
background: none;
|
|
41
|
+
box-shadow: none;
|
|
42
|
+
border-color: transparent;
|
|
43
|
+
}
|
|
44
|
+
:focus-visible {
|
|
45
|
+
outline: none;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const StyledAddFolderButton = styled(Button)`
|
|
50
|
+
&,
|
|
51
|
+
&:disabled {
|
|
52
|
+
border-color: transparent;
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const StyledPlus = styled(Plus)`
|
|
57
|
+
height: 24px;
|
|
58
|
+
width: 24px;
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
showTree: boolean;
|
|
63
|
+
type: TreeStructureType;
|
|
64
|
+
label?: string;
|
|
65
|
+
focusedFolder?: FolderType;
|
|
66
|
+
selectedFolder?: FolderType;
|
|
67
|
+
setSelectedFolder: (folder?: FolderType) => void;
|
|
68
|
+
onToggleTree: (open: boolean) => void;
|
|
69
|
+
flattenedFolders: FolderType[];
|
|
70
|
+
onOpenFolder: (id: string) => void;
|
|
71
|
+
onCloseFolder: (id: string) => void;
|
|
72
|
+
setFocusedFolder: (folder?: FolderType) => void;
|
|
73
|
+
onNewFolder?: (name: string, parentId: string) => Promise<IFolder>;
|
|
74
|
+
maxLevel: number;
|
|
75
|
+
setNewFolderParentId: (id?: string) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
showTree,
|
|
82
|
+
type,
|
|
83
|
+
label,
|
|
84
|
+
focusedFolder,
|
|
85
|
+
selectedFolder,
|
|
86
|
+
setSelectedFolder,
|
|
87
|
+
onToggleTree,
|
|
88
|
+
flattenedFolders,
|
|
89
|
+
setFocusedFolder,
|
|
90
|
+
onOpenFolder,
|
|
91
|
+
onCloseFolder,
|
|
92
|
+
onNewFolder,
|
|
93
|
+
maxLevel,
|
|
94
|
+
setNewFolderParentId,
|
|
95
|
+
},
|
|
96
|
+
ref,
|
|
97
|
+
) => {
|
|
98
|
+
const { t } = useTranslation();
|
|
99
|
+
const innerRef = useForwardedRef(ref);
|
|
100
|
+
|
|
101
|
+
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
102
|
+
if (e.key === 'Enter') {
|
|
103
|
+
if (showTree) {
|
|
104
|
+
setSelectedFolder(focusedFolder);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (e.key === 'Escape') {
|
|
109
|
+
onToggleTree(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (['ArrowUp', 'ArrowDown'].includes(e.key) && !showTree) {
|
|
113
|
+
onToggleTree(true);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (focusedFolder) {
|
|
117
|
+
arrowNavigation(e, focusedFolder.id, flattenedFolders, setFocusedFolder, onOpenFolder, onCloseFolder);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const canAddFolder = selectedFolder && selectedFolder?.breadcrumbs.length < (maxLevel || 1);
|
|
122
|
+
return (
|
|
123
|
+
<StyledRow isOpen={showTree}>
|
|
124
|
+
<StyledSelectedFolder
|
|
125
|
+
ref={innerRef}
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
id={treestructureId(type, 'combobox')}
|
|
128
|
+
role="combobox"
|
|
129
|
+
aria-controls={treestructureId(type, 'popup')}
|
|
130
|
+
aria-haspopup="tree"
|
|
131
|
+
aria-expanded={showTree}
|
|
132
|
+
aria-labelledby={label ? treestructureId(type, 'label') : undefined}
|
|
133
|
+
aria-activedescendant={focusedFolder ? treestructureId(type, focusedFolder.id) : undefined}
|
|
134
|
+
variant="ghost"
|
|
135
|
+
colorTheme="light"
|
|
136
|
+
fontWeight="normal"
|
|
137
|
+
shape="sharp"
|
|
138
|
+
onKeyDown={onKeyDown}
|
|
139
|
+
onClick={() => {
|
|
140
|
+
onToggleTree(!showTree);
|
|
141
|
+
}}>
|
|
142
|
+
{selectedFolder?.name}
|
|
143
|
+
</StyledSelectedFolder>
|
|
144
|
+
{onNewFolder && showTree && (
|
|
145
|
+
<Tooltip
|
|
146
|
+
tooltip={
|
|
147
|
+
canAddFolder
|
|
148
|
+
? t('myNdla.newFolderUnder', {
|
|
149
|
+
folderName: selectedFolder?.name,
|
|
150
|
+
})
|
|
151
|
+
: t('treeStructure.maxFoldersAlreadyAdded')
|
|
152
|
+
}>
|
|
153
|
+
<StyledAddFolderButton
|
|
154
|
+
variant="outline"
|
|
155
|
+
shape="pill"
|
|
156
|
+
disabled={!canAddFolder}
|
|
157
|
+
aria-label={
|
|
158
|
+
canAddFolder
|
|
159
|
+
? t('myNdla.newFolderUnder', {
|
|
160
|
+
folderName: selectedFolder?.name,
|
|
161
|
+
})
|
|
162
|
+
: t('treeStructure.maxFoldersAlreadyAdded')
|
|
163
|
+
}
|
|
164
|
+
onClick={() => setNewFolderParentId(focusedFolder?.id)}>
|
|
165
|
+
<StyledPlus /> {t('myNdla.newFolder')}
|
|
166
|
+
</StyledAddFolderButton>
|
|
167
|
+
</Tooltip>
|
|
168
|
+
)}
|
|
169
|
+
<IconButton
|
|
170
|
+
aria-hidden
|
|
171
|
+
aria-label=""
|
|
172
|
+
tabIndex={-1}
|
|
173
|
+
variant="ghost"
|
|
174
|
+
colorTheme="greyLighter"
|
|
175
|
+
size="small"
|
|
176
|
+
onClick={() => {
|
|
177
|
+
if (!showTree) {
|
|
178
|
+
innerRef.current?.focus();
|
|
179
|
+
}
|
|
180
|
+
onToggleTree(!showTree);
|
|
181
|
+
}}>
|
|
182
|
+
{showTree ? <ChevronUp /> : <ChevronDown />}
|
|
183
|
+
</IconButton>
|
|
184
|
+
</StyledRow>
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export default ComboboxButton;
|