@ndla/ui 26.1.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 +29 -13
- package/es/Resource/BlockResource.js +42 -62
- package/es/Resource/ListResource.js +41 -22
- 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 +6 -1
- package/es/locale/messages-nb.js +6 -1
- package/es/locale/messages-nn.js +6 -1
- package/es/locale/messages-se.js +6 -1
- package/es/locale/messages-sma.js +6 -1
- package/lib/Breadcrumb/Breadcrumb.js +3 -5
- package/lib/MyNdla/Resource/Folder.js +34 -13
- package/lib/Resource/BlockResource.js +47 -62
- package/lib/Resource/ListResource.js +46 -22
- 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 +5 -0
- package/lib/locale/messages-en.js +6 -1
- package/lib/locale/messages-nb.d.ts +5 -0
- package/lib/locale/messages-nb.js +6 -1
- package/lib/locale/messages-nn.d.ts +5 -0
- package/lib/locale/messages-nn.js +6 -1
- package/lib/locale/messages-se.d.ts +5 -0
- package/lib/locale/messages-se.js +6 -1
- package/lib/locale/messages-sma.d.ts +5 -0
- package/lib/locale/messages-sma.js +6 -1
- package/package.json +11 -11
- package/src/Breadcrumb/Breadcrumb.tsx +1 -2
- package/src/MyNdla/Resource/Folder.tsx +19 -4
- package/src/Resource/BlockResource.tsx +41 -33
- package/src/Resource/ListResource.tsx +35 -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 +6 -0
- package/src/locale/messages-nb.ts +5 -0
- package/src/locale/messages-nn.ts +5 -0
- package/src/locale/messages-se.ts +6 -0
- package/src/locale/messages-sma.ts +6 -0
- package/src/.DS_Store +0 -0
|
@@ -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 {
|
|
@@ -146,13 +152,7 @@ const ListResourceImage = ({ resourceImage, loading, type }: ListResourceImagePr
|
|
|
146
152
|
);
|
|
147
153
|
};
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
title: string;
|
|
151
|
-
topics: string[];
|
|
152
|
-
loading?: boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
|
|
155
|
+
const TopicAndTitleLoader = ({ loading, children }: LoaderProps) => {
|
|
156
156
|
if (loading) {
|
|
157
157
|
return (
|
|
158
158
|
<ContentLoader height={'40px'} width={'100%'} viewBox={null} preserveAspectRatio="none">
|
|
@@ -162,12 +162,7 @@ const TopicAndTitle = ({ title, topics, loading }: TopicAndTitleProps) => {
|
|
|
162
162
|
</ContentLoader>
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
|
-
return
|
|
166
|
-
<>
|
|
167
|
-
<ResourceTitle>{title}</ResourceTitle>
|
|
168
|
-
<TopicList topics={topics} />
|
|
169
|
-
</>
|
|
170
|
-
);
|
|
165
|
+
return <>{children}</>;
|
|
171
166
|
};
|
|
172
167
|
|
|
173
168
|
interface ResourceDescriptionProps {
|
|
@@ -200,14 +195,25 @@ const ListResource = ({
|
|
|
200
195
|
}: ListResourceProps) => {
|
|
201
196
|
const showDescription = description !== undefined;
|
|
202
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
|
+
};
|
|
203
204
|
|
|
204
205
|
return (
|
|
205
|
-
<ResourceWrapper
|
|
206
|
+
<ResourceWrapper onClick={handleClick} id={id}>
|
|
206
207
|
<StyledImageWrapper imageSize={imageType}>
|
|
207
208
|
<ListResourceImage resourceImage={resourceImage} loading={isLoading} type={imageType} />
|
|
208
209
|
</StyledImageWrapper>
|
|
209
210
|
<TopicAndTitleWrapper>
|
|
210
|
-
<
|
|
211
|
+
<TopicAndTitleLoader loading={isLoading}>
|
|
212
|
+
<ResourceTitleLink to={link} ref={linkRef}>
|
|
213
|
+
<ResourceTitle>{title}</ResourceTitle>
|
|
214
|
+
</ResourceTitleLink>
|
|
215
|
+
<TopicList topics={topics} />
|
|
216
|
+
</TopicAndTitleLoader>
|
|
211
217
|
</TopicAndTitleWrapper>
|
|
212
218
|
{showDescription && <ResourceDescription description={description} loading={isLoading} />}
|
|
213
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;
|