@ndla/ui 42.1.1 → 43.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.
Files changed (71) hide show
  1. package/es/AudioPlayer/AudioPlayer.js +27 -33
  2. package/es/AudioPlayer/Controls.js +41 -39
  3. package/es/Breadcrumb/index.js +0 -1
  4. package/es/CampaignBlock/CampaignBlock.js +32 -18
  5. package/es/LanguageSelector/LanguageSelector.js +31 -36
  6. package/es/LicenseByline/EmbedByline.js +5 -5
  7. package/es/MyNdla/Resource/Folder.js +27 -72
  8. package/es/MyNdla/index.js +1 -3
  9. package/es/Resource/BlockResource.js +15 -27
  10. package/es/Resource/ListResource.js +14 -19
  11. package/es/Resource/resourceComponents.js +82 -41
  12. package/es/TreeStructure/ComboboxButton.js +17 -20
  13. package/es/TreeStructure/FolderItem.js +42 -69
  14. package/es/TreeStructure/FolderItems.js +25 -19
  15. package/es/TreeStructure/TreeStructure.js +19 -26
  16. package/es/index.js +2 -2
  17. package/lib/AudioPlayer/AudioPlayer.js +27 -33
  18. package/lib/AudioPlayer/Controls.js +40 -38
  19. package/lib/Breadcrumb/index.d.ts +0 -1
  20. package/lib/Breadcrumb/index.js +0 -7
  21. package/lib/CampaignBlock/CampaignBlock.js +32 -18
  22. package/lib/LanguageSelector/LanguageSelector.js +31 -36
  23. package/lib/LicenseByline/EmbedByline.js +5 -5
  24. package/lib/MyNdla/Resource/Folder.d.ts +3 -4
  25. package/lib/MyNdla/Resource/Folder.js +27 -72
  26. package/lib/MyNdla/index.d.ts +1 -3
  27. package/lib/MyNdla/index.js +0 -14
  28. package/lib/Resource/BlockResource.d.ts +3 -3
  29. package/lib/Resource/BlockResource.js +14 -26
  30. package/lib/Resource/ListResource.d.ts +3 -3
  31. package/lib/Resource/ListResource.js +13 -18
  32. package/lib/Resource/resourceComponents.d.ts +5 -10
  33. package/lib/Resource/resourceComponents.js +85 -42
  34. package/lib/TreeStructure/ComboboxButton.js +17 -20
  35. package/lib/TreeStructure/FolderItem.js +40 -67
  36. package/lib/TreeStructure/FolderItems.js +31 -26
  37. package/lib/TreeStructure/TreeStructure.js +19 -26
  38. package/lib/index.d.ts +2 -2
  39. package/lib/index.js +0 -12
  40. package/package.json +16 -17
  41. package/src/AudioPlayer/AudioPlayer.tsx +24 -34
  42. package/src/AudioPlayer/Controls.tsx +22 -26
  43. package/src/Breadcrumb/index.ts +0 -2
  44. package/src/CampaignBlock/CampaignBlock.tsx +10 -10
  45. package/src/LanguageSelector/LanguageSelector.tsx +26 -32
  46. package/src/LicenseByline/EmbedByline.tsx +1 -1
  47. package/src/MyNdla/Resource/Folder.stories.tsx +27 -5
  48. package/src/MyNdla/Resource/Folder.tsx +32 -54
  49. package/src/MyNdla/index.ts +1 -3
  50. package/src/Resource/BlockResource.stories.tsx +1 -1
  51. package/src/Resource/BlockResource.tsx +25 -24
  52. package/src/Resource/ListResource.tsx +21 -18
  53. package/src/Resource/Resource.stories.tsx +32 -2
  54. package/src/Resource/resourceComponents.tsx +55 -26
  55. package/src/TreeStructure/ComboboxButton.tsx +5 -7
  56. package/src/TreeStructure/FolderItem.tsx +50 -35
  57. package/src/TreeStructure/FolderItems.tsx +6 -8
  58. package/src/TreeStructure/TreeStructure.tsx +16 -25
  59. package/src/index.ts +2 -2
  60. package/es/Breadcrumb/ActionBreadcrumb.js +0 -74
  61. package/es/MyNdla/Resource/FolderMenu.js +0 -74
  62. package/es/MyNdla/SettingsMenu.js +0 -98
  63. package/lib/Breadcrumb/ActionBreadcrumb.d.ts +0 -15
  64. package/lib/Breadcrumb/ActionBreadcrumb.js +0 -82
  65. package/lib/MyNdla/Resource/FolderMenu.d.ts +0 -16
  66. package/lib/MyNdla/Resource/FolderMenu.js +0 -81
  67. package/lib/MyNdla/SettingsMenu.d.ts +0 -15
  68. package/lib/MyNdla/SettingsMenu.js +0 -102
  69. package/src/Breadcrumb/ActionBreadcrumb.tsx +0 -87
  70. package/src/MyNdla/Resource/FolderMenu.tsx +0 -102
  71. package/src/MyNdla/SettingsMenu.tsx +0 -96
@@ -28,7 +28,7 @@ export default {
28
28
  headingLevel: {
29
29
  control: false,
30
30
  },
31
- menuItems: {
31
+ menu: {
32
32
  control: false,
33
33
  },
34
34
  },
@@ -7,23 +7,21 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import React from 'react';
10
+ import React, { ReactNode } from 'react';
11
11
  import { colors, fonts, spacing } from '@ndla/core';
12
- import { MenuItemProps } from '@ndla/button';
13
12
  import ContentTypeBadge from '../ContentTypeBadge';
14
13
  import Image from '../Image';
15
14
  import {
16
15
  CompressedTagList,
17
16
  ResourceImageProps,
18
- ResourceTitle,
19
17
  ResourceTypeList,
20
18
  ResourceTitleLink,
21
19
  LoaderProps,
22
- StyledContentIconWrapper,
20
+ ContentIconWrapper,
21
+ resourceHeadingStyle,
23
22
  } from './resourceComponents';
24
23
  import ContentLoader from '../ContentLoader';
25
24
  import { contentTypeMapping, resourceEmbedTypeMapping } from '../model/ContentType';
26
- import { SettingsMenu } from '../MyNdla';
27
25
 
28
26
  const BlockElementWrapper = styled.div`
29
27
  display: flex;
@@ -41,11 +39,23 @@ const BlockElementWrapper = styled.div`
41
39
  &:hover {
42
40
  box-shadow: 1px 1px 6px 2px rgba(9, 55, 101, 0.08);
43
41
  transition-duration: 0.2s;
44
- ${() => ResourceTitleLink} {
42
+ [data-link] {
45
43
  color: ${colors.brand.primary};
46
44
  text-decoration: underline;
47
45
  }
48
46
  }
47
+
48
+ &:hover,
49
+ &:focus,
50
+ &:focus-within {
51
+ [data-description] {
52
+ /* Unfortunate css needed for multi-line text overflow ellipsis. */
53
+ height: 3.1em;
54
+ -webkit-line-clamp: 2;
55
+ line-clamp: 2;
56
+ -webkit-box-orient: vertical;
57
+ }
58
+ }
49
59
  `;
50
60
 
51
61
  const BlockDescription = styled.p`
@@ -57,14 +67,6 @@ const BlockDescription = styled.p`
57
67
  overflow: hidden;
58
68
  text-overflow: ellipsis;
59
69
  transition: height 0.2s ease-out;
60
- ${() => BlockElementWrapper}:hover &, ${() => BlockElementWrapper}:focus & , ${() =>
61
- BlockElementWrapper}:focus-within & {
62
- // Unfortunate css needed for multi-line text overflow ellipsis.
63
- height: 3.1em;
64
- -webkit-line-clamp: 2;
65
- line-clamp: 2;
66
- -webkit-box-orient: vertical;
67
- }
68
70
  `;
69
71
 
70
72
  const TagsAndActionMenu = styled.div`
@@ -114,9 +116,9 @@ const BlockImage = ({ image, loading, contentType }: BlockImageProps) => {
114
116
  }
115
117
  if (image.src === '') {
116
118
  return (
117
- <StyledContentIconWrapper contentType={contentType}>
119
+ <ContentIconWrapper contentType={contentType}>
118
120
  <ContentTypeBadge type={contentType} size="large" />
119
- </StyledContentIconWrapper>
121
+ </ContentIconWrapper>
120
122
  );
121
123
  } else {
122
124
  return <Image alt={image.alt} src={image.src} fallbackWidth={300} />;
@@ -145,7 +147,7 @@ interface Props {
145
147
  tags?: string[];
146
148
  description?: string;
147
149
  headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
148
- menuItems?: MenuItemProps[];
150
+ menu?: ReactNode;
149
151
  isLoading?: boolean;
150
152
  targetBlank?: boolean;
151
153
  resourceTypes?: { id: string; name: string }[];
@@ -159,14 +161,13 @@ const BlockResource = ({
159
161
  tags,
160
162
  resourceImage,
161
163
  description,
162
- menuItems,
164
+ menu,
163
165
  isLoading,
164
- headingLevel = 'h2',
166
+ headingLevel: Heading = 'h2',
165
167
  targetBlank,
166
168
  resourceTypes,
167
169
  }: Props) => {
168
170
  const firstResourceType = resourceTypes?.[0]?.id ?? '';
169
- const Title = ResourceTitle.withComponent(headingLevel);
170
171
 
171
172
  return (
172
173
  <BlockElementWrapper id={id}>
@@ -184,16 +185,16 @@ const BlockResource = ({
184
185
  <BlockInfoWrapper>
185
186
  <ContentWrapper>
186
187
  <ResourceTypeAndTitleLoader loading={isLoading}>
187
- <ResourceTitleLink title={title} target={targetBlank ? '_blank' : undefined} to={link}>
188
- <Title>{title}</Title>
188
+ <ResourceTitleLink data-link="" title={title} target={targetBlank ? '_blank' : undefined} to={link}>
189
+ <Heading css={resourceHeadingStyle}>{title}</Heading>
189
190
  </ResourceTitleLink>
190
191
  </ResourceTypeAndTitleLoader>
191
192
  <ResourceTypeList resourceTypes={resourceTypes} />
192
- <BlockDescription>{description}</BlockDescription>
193
+ <BlockDescription data-description="">{description}</BlockDescription>
193
194
  </ContentWrapper>
194
195
  <TagsAndActionMenu>
195
196
  {tags && tags.length > 0 && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
196
- {menuItems && menuItems.length > 0 && <SettingsMenu menuItems={menuItems} />}
197
+ {menu}
197
198
  </TagsAndActionMenu>
198
199
  </BlockInfoWrapper>
199
200
  </BlockElementWrapper>
@@ -7,23 +7,21 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import React from 'react';
10
+ import React, { ReactNode } from 'react';
11
11
  import { fonts, spacing, colors, breakpoints, mq } from '@ndla/core';
12
- import { MenuItemProps } from '@ndla/button';
13
12
  import Image from '../Image';
14
13
  import {
15
14
  CompressedTagList,
16
15
  ResourceImageProps,
17
- ResourceTitle,
16
+ resourceHeadingStyle,
18
17
  ResourceTitleLink as StyledLink,
19
18
  ResourceTypeList,
20
- StyledContentIconWrapper,
21
19
  LoaderProps,
20
+ ContentIconWrapper,
22
21
  } from './resourceComponents';
23
22
  import ContentLoader from '../ContentLoader';
24
23
  import ContentTypeBadge from '../ContentTypeBadge';
25
24
  import { contentTypeMapping, resourceEmbedTypeMapping } from '../model/ContentType';
26
- import { SettingsMenu } from '../MyNdla';
27
25
 
28
26
  const ListResourceWrapper = styled.div`
29
27
  flex: 1;
@@ -48,7 +46,7 @@ const ListResourceWrapper = styled.div`
48
46
  &:hover {
49
47
  box-shadow: 1px 1px 6px 2px rgba(9, 55, 101, 0.08);
50
48
  transition-duration: 0.2s;
51
- ${() => StyledLink} {
49
+ [data-link] {
52
50
  color: ${colors.brand.primary};
53
51
  text-decoration: underline;
54
52
  }
@@ -61,11 +59,7 @@ interface StyledImageProps {
61
59
 
62
60
  const ImageWrapper = styled.div<StyledImageProps>`
63
61
  grid-area: image;
64
- width: ${(p) => (p.imageSize === 'normal' ? '136px' : '56px')};
65
- ${mq.range({ until: breakpoints.mobileWide })} {
66
- width: 56px;
67
- margin-bottom: 0;
68
- }
62
+ width: 56px;
69
63
  overflow: hidden;
70
64
  border-radius: 2px;
71
65
  display: flex;
@@ -73,6 +67,13 @@ const ImageWrapper = styled.div<StyledImageProps>`
73
67
  align-items: center;
74
68
  justify-content: center;
75
69
  aspect-ratio: 4/3;
70
+ &[data-image-size='normal'] {
71
+ width: 136px;
72
+ }
73
+ ${mq.range({ until: breakpoints.mobileWide })} {
74
+ width: 56px;
75
+ margin-bottom: 0;
76
+ }
76
77
  `;
77
78
 
78
79
  const StyledImage = styled(Image)`
@@ -138,9 +139,9 @@ const ListResourceImage = ({ resourceImage, loading, type, contentType, backgrou
138
139
  if (!loading) {
139
140
  if (resourceImage.src === '') {
140
141
  return (
141
- <StyledContentIconWrapper contentType={contentType}>
142
+ <ContentIconWrapper contentType={contentType}>
142
143
  <ContentTypeBadge type={contentType} background={background} size="x-small" />
143
- </StyledContentIconWrapper>
144
+ </ContentIconWrapper>
144
145
  );
145
146
  } else {
146
147
  return (
@@ -201,7 +202,7 @@ export interface ListResourceProps {
201
202
  resourceTypes: { id: string; name: string }[];
202
203
  tags?: string[];
203
204
  description?: string;
204
- menuItems?: MenuItemProps[];
205
+ menu?: ReactNode;
205
206
  isLoading?: boolean;
206
207
  targetBlank?: boolean;
207
208
  }
@@ -215,7 +216,7 @@ const ListResource = ({
215
216
  resourceImage,
216
217
  resourceTypes,
217
218
  description,
218
- menuItems,
219
+ menu,
219
220
  isLoading = false,
220
221
  targetBlank,
221
222
  }: ListResourceProps) => {
@@ -237,8 +238,10 @@ const ListResource = ({
237
238
  </ImageWrapper>
238
239
  <TopicAndTitleWrapper>
239
240
  <TypeAndTitleLoader loading={isLoading}>
240
- <StyledLink to={link} target={targetBlank ? '_blank' : undefined}>
241
- <ResourceTitle title={title}>{title}</ResourceTitle>
241
+ <StyledLink to={link} data-link="" target={targetBlank ? '_blank' : undefined}>
242
+ <h1 css={resourceHeadingStyle} title={title}>
243
+ {title}
244
+ </h1>
242
245
  </StyledLink>
243
246
  <ResourceTypeList resourceTypes={resourceTypes} />
244
247
  </TypeAndTitleLoader>
@@ -246,7 +249,7 @@ const ListResource = ({
246
249
  {showDescription && <Description description={description} loading={isLoading} />}
247
250
  <TagsandActionMenu>
248
251
  {tags && tags.length > 0 && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
249
- {menuItems && menuItems.length > 0 && <SettingsMenu menuItems={menuItems} />}
252
+ {menu}
250
253
  </TagsandActionMenu>
251
254
  </ListResourceWrapper>
252
255
  );
@@ -8,8 +8,37 @@
8
8
 
9
9
  import React from 'react';
10
10
  import { Meta, StoryFn } from '@storybook/react';
11
- import { defaultParameters } from '../../../../stories/defaults';
11
+ import { DropdownMenu, DropdownContent, DropdownItem, DropdownTrigger } from '@ndla/dropdown-menu';
12
+ import { ButtonV2, IconButtonV2 } from '@ndla/button';
13
+ import { HorizontalMenu } from '@ndla/icons/contentType';
14
+ import { Pencil } from '@ndla/icons/action';
15
+ import { DeleteForever } from '@ndla/icons/editor';
12
16
  import ListResource from './ListResource';
17
+ import { defaultParameters } from '../../../../stories/defaults';
18
+
19
+ const StoryResourceMenu = () => (
20
+ <DropdownMenu>
21
+ <DropdownTrigger>
22
+ <IconButtonV2 aria-label="Show more" title="Show more" variant="ghost" colorTheme="light">
23
+ <HorizontalMenu />
24
+ </IconButtonV2>
25
+ </DropdownTrigger>
26
+ <DropdownContent>
27
+ <DropdownItem>
28
+ <ButtonV2 variant="ghost" colorTheme="light" shape="sharp" size="small" fontWeight="normal">
29
+ <Pencil />
30
+ Rediger
31
+ </ButtonV2>
32
+ </DropdownItem>
33
+ <DropdownItem>
34
+ <ButtonV2 variant="ghost" colorTheme="danger" shape="sharp" size="small" fontWeight="normal">
35
+ <DeleteForever />
36
+ Slett
37
+ </ButtonV2>
38
+ </DropdownItem>
39
+ </DropdownContent>
40
+ </DropdownMenu>
41
+ );
13
42
 
14
43
  export default {
15
44
  title: 'Components/Resources/ListResource',
@@ -28,7 +57,7 @@ export default {
28
57
  headingLevel: {
29
58
  control: false,
30
59
  },
31
- menuItems: {
60
+ menu: {
32
61
  control: false,
33
62
  },
34
63
  },
@@ -41,6 +70,7 @@ export default {
41
70
  alt: '',
42
71
  },
43
72
  resourceTypes: [{ id: 'urn:resourcetype:learningPath', name: 'Læringssti' }],
73
+ menu: <StoryResourceMenu />,
44
74
  tags: ['tag', 'tag', 'tag', 'tag'],
45
75
  },
46
76
  } as Meta<typeof ListResource>;
@@ -8,12 +8,13 @@
8
8
 
9
9
  import styled from '@emotion/styled';
10
10
  import { colors, fonts, spacing } from '@ndla/core';
11
- import React, { ReactNode } from 'react';
11
+ import React, { CSSProperties, HTMLAttributes, ReactNode, useMemo } from 'react';
12
12
  import { useTranslation } from 'react-i18next';
13
- import { MenuButton } from '@ndla/button';
14
- import SafeLink from '@ndla/safelink';
15
- import { useNavigate } from 'react-router-dom';
13
+ import { IconButtonV2 } from '@ndla/button';
14
+ import SafeLink, { SafeLinkButton } from '@ndla/safelink';
16
15
  import { HashTag } from '@ndla/icons/common';
16
+ import { css } from '@emotion/react';
17
+ import { DropdownMenu, DropdownContent, DropdownTrigger, DropdownItem } from '@ndla/dropdown-menu';
17
18
  import resourceTypeColor from '../utils/resourceTypeColor';
18
19
  import { resourceEmbedTypeMapping } from '../model/ContentType';
19
20
 
@@ -37,7 +38,11 @@ export const ResourceTitleLink = styled(SafeLink)`
37
38
  }
38
39
  `;
39
40
 
40
- export const ResourceTitle = styled.span`
41
+ const StyledTrigger = styled(IconButtonV2)`
42
+ margin: 0px ${spacing.xsmall};
43
+ `;
44
+
45
+ export const resourceHeadingStyle = css`
41
46
  margin: 0;
42
47
  overflow: hidden;
43
48
  text-overflow: ellipsis;
@@ -113,19 +118,32 @@ const TagCounterWrapper = styled.span`
113
118
  ${fonts.sizes('14px', '14px')};
114
119
  `;
115
120
 
116
- export interface ContentIconProps {
121
+ interface ContentIconProps extends HTMLAttributes<HTMLSpanElement> {
117
122
  contentType: string;
123
+ children?: ReactNode;
118
124
  }
119
125
 
120
- export const StyledContentIconWrapper = styled.span<ContentIconProps>`
126
+ const StyledContentIconWrapper = styled.span`
121
127
  width: 100%;
122
128
  aspect-ratio: 4/3;
123
129
  display: flex;
124
130
  align-items: center;
125
131
  justify-content: center;
126
- background-color: ${({ contentType }) => resourceTypeColor(contentType)};
132
+ background-color: var(--content-background-color);
127
133
  `;
128
134
 
135
+ export const ContentIconWrapper = ({ contentType, children, ...props }: ContentIconProps) => {
136
+ const contentIconWrapperVars = useMemo(
137
+ () => ({ '--content-background-color': resourceTypeColor(contentType) } as unknown as CSSProperties),
138
+ [contentType],
139
+ );
140
+ return (
141
+ <StyledContentIconWrapper {...props} style={contentIconWrapperVars}>
142
+ {children}
143
+ </StyledContentIconWrapper>
144
+ );
145
+ };
146
+
129
147
  interface TagListProps {
130
148
  tags?: string[];
131
149
  tagLinkPrefix?: string;
@@ -159,29 +177,40 @@ interface CompressedTagListProps {
159
177
  }
160
178
 
161
179
  export const CompressedTagList = ({ tags, tagLinkPrefix }: CompressedTagListProps) => {
162
- const navigate = useNavigate();
163
180
  const { t } = useTranslation();
164
- const visibleTags = tags.slice(0, 3);
165
- const remainingTags = tags.slice(3, tags.length).map((tag) => {
166
- return {
167
- icon: <HashTag />,
168
- text: tag,
169
- onClick: () => {
170
- navigate(`${tagLinkPrefix ? tagLinkPrefix : ''}/${encodeURIComponent(tag)}`);
171
- },
172
- };
173
- });
181
+ const visibleTags = useMemo(() => tags.slice(0, 3), [tags]);
182
+ const remainingTags = useMemo(() => tags.slice(3, tags.length), [tags]);
183
+
174
184
  return (
175
185
  <>
176
186
  <TagList tagLinkPrefix={tagLinkPrefix} tags={visibleTags} />
177
187
  {remainingTags.length > 0 && (
178
- <MenuButton
179
- size="small"
180
- menuIcon={<TagCounterWrapper>{`+${remainingTags.length}`}</TagCounterWrapper>}
181
- menuItems={remainingTags}
182
- align="end"
183
- aria-label={t('myNdla.moreTags', { count: remainingTags.length })}
184
- />
188
+ <DropdownMenu>
189
+ <DropdownTrigger>
190
+ <StyledTrigger
191
+ size="xsmall"
192
+ variant="ghost"
193
+ colorTheme="light"
194
+ aria-label={t('myNdla.moreTags', { count: remainingTags.length })}
195
+ >
196
+ {<TagCounterWrapper>{`+${remainingTags.length}`}</TagCounterWrapper>}
197
+ </StyledTrigger>
198
+ </DropdownTrigger>
199
+ <DropdownContent showArrow>
200
+ {remainingTags.map((tag, i) => (
201
+ <DropdownItem key={`tag-${i}`}>
202
+ <SafeLinkButton
203
+ to={`${tagLinkPrefix ?? ''}/${encodeURIComponent(tag)}`}
204
+ variant="ghost"
205
+ colorTheme="light"
206
+ >
207
+ <HashTag />
208
+ {tag}
209
+ </SafeLinkButton>
210
+ </DropdownItem>
211
+ ))}
212
+ </DropdownContent>
213
+ </DropdownMenu>
185
214
  )}
186
215
  </>
187
216
  );
@@ -18,15 +18,13 @@ import { TreeStructureType } from './types';
18
18
  import { arrowNavigation } from './arrowNavigation';
19
19
  import ContentLoader from '../ContentLoader';
20
20
 
21
- interface StyledRowProps {
22
- isOpen: boolean;
23
- }
24
-
25
- const StyledRow = styled.div<StyledRowProps>`
21
+ const StyledRow = styled.div`
26
22
  display: flex;
27
23
  padding: ${spacing.xxsmall};
28
24
  align-items: center;
29
- border-bottom: ${({ isOpen }) => isOpen && `1px solid ${colors.brand.tertiary}`};
25
+ &[data-open='true'] {
26
+ border-bottom: 1px solid ${colors.brand.tertiary};
27
+ }
30
28
  `;
31
29
  const StyledSelectedFolder = styled(Button)`
32
30
  flex: 1;
@@ -110,7 +108,7 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
110
108
 
111
109
  return (
112
110
  <StyledRow
113
- isOpen={showTree}
111
+ data-open={showTree}
114
112
  onMouseDown={(e) => {
115
113
  if (!e.defaultPrevented) {
116
114
  e.preventDefault();
@@ -6,21 +6,21 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { KeyboardEvent, useEffect, useRef } from 'react';
9
+ import React, { CSSProperties, KeyboardEvent, useEffect, useMemo, useRef } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import styled from '@emotion/styled';
12
12
  import { ArrowDropDownRounded } from '@ndla/icons/common';
13
13
  import { FolderOutlined, FolderShared } from '@ndla/icons/contentType';
14
14
  import { Done } from '@ndla/icons/editor';
15
15
  import { ButtonV2 as Button } from '@ndla/button';
16
- import { colors, spacing, animations, spacingUnit, misc, fonts } from '@ndla/core';
16
+ import { colors, spacing, animations, misc, fonts } from '@ndla/core';
17
17
  import SafeLink from '@ndla/safelink';
18
18
  import { IFolder } from '@ndla/types-backend/learningpath-api';
19
19
  import { CommonFolderItemsProps } from './types';
20
20
  import { arrowNavigation } from './arrowNavigation';
21
21
  import { treestructureId } from './helperFunctions';
22
22
 
23
- const OpenButton = styled.span<{ isOpen: boolean }>`
23
+ const OpenButton = styled.span`
24
24
  display: flex;
25
25
  align-items: center;
26
26
  justify-content: center;
@@ -34,7 +34,12 @@ const OpenButton = styled.span<{ isOpen: boolean }>`
34
34
  svg {
35
35
  width: 24px;
36
36
  height: 24px;
37
- transform: rotate(${({ isOpen }) => (isOpen ? '0' : '-90')}deg);
37
+ transform: rotate(-90deg);
38
+ }
39
+ &[data-open='true'] {
40
+ svg {
41
+ transform: rotate(0deg);
42
+ }
38
43
  }
39
44
  `;
40
45
 
@@ -57,57 +62,67 @@ const FolderIconWrapper = styled.div`
57
62
  }
58
63
  `;
59
64
 
60
- const shouldForwardProp = (name: string) => !['selected', 'level', 'focused', 'isCreatingFolder'].includes(name);
61
-
62
- interface FolderNameProps {
63
- selected?: boolean;
64
- level: number;
65
- isCreatingFolder?: boolean;
66
- focused?: boolean;
67
- }
68
-
69
- const FolderName = styled(Button, { shouldForwardProp })<FolderNameProps>`
65
+ const FolderName = styled(Button)`
70
66
  display: grid;
71
67
  grid-template-columns: auto 1fr auto;
72
- padding-left: ${({ level }) => 0.75 * spacingUnit * level}px;
68
+ padding-left: calc(0.75 * ${spacing.normal} * var(--level));
73
69
  gap: ${spacing.xxsmall};
74
70
  border: none;
75
71
  outline: none;
76
- background: ${({ selected, isCreatingFolder, focused }) =>
77
- isCreatingFolder ? 'none' : selected ? colors.brand.lighter : focused && colors.brand.lightest};
78
- color: ${({ isCreatingFolder, focused }) =>
79
- isCreatingFolder && focused ? colors.brand.primary : colors.text.primary};
72
+ color: ${colors.text.primary};
80
73
  transition: ${animations.durations.superFast};
81
74
  word-break: break-word;
82
75
 
83
76
  &:hover {
84
77
  box-shadow: none;
85
78
  outline: none;
86
- background: ${({ selected }) => (selected ? colors.brand.light : colors.brand.lightest)};
79
+ background: ${colors.brand.lightest};
87
80
  color: ${colors.text.primary};
88
81
  }
82
+
83
+ &[data-focused='true'] {
84
+ background: ${colors.brand.lightest};
85
+ }
86
+
87
+ &[data-selected='true'] {
88
+ background: ${colors.brand.lighter};
89
+ &:hover {
90
+ background: ${colors.brand.light};
91
+ }
92
+ }
93
+
94
+ &[data-creating='true'][data-focused='true'] {
95
+ color: ${colors.brand.primary};
96
+ }
97
+
98
+ &[data-creating='true'] {
99
+ background: none;
100
+ }
89
101
  `;
90
102
 
91
103
  const StyledDone = styled(Done)`
92
104
  color: ${colors.support.green};
93
105
  `;
94
106
 
95
- const FolderNameLink = styled(SafeLink, { shouldForwardProp })<FolderNameProps>`
107
+ const FolderNameLink = styled(SafeLink)`
96
108
  display: grid;
97
109
  align-items: center;
98
110
  grid-template-columns: ${spacing.medium} 1fr auto;
99
111
  padding: ${spacing.small} ${spacing.xxsmall};
100
- margin-left: ${({ level }) => 0.75 * spacingUnit * level}px;
112
+ padding-left: calc(0.75 * ${spacing.normal} * var(--level));
101
113
  gap: ${spacing.xxsmall};
102
114
  cursor: pointer;
103
115
 
104
116
  border: none;
105
117
  box-shadow: none;
106
- color: ${({ selected }) => (selected ? colors.brand.primary : colors.text.primary)};
107
- font-weight: ${({ selected }) => selected && fonts.weight.semibold};
118
+ color: ${colors.text.primary};
108
119
  font-size: ${fonts.sizes('16px')};
109
120
  transition: ${animations.durations.superFast};
110
121
  word-break: break-word;
122
+ &[data-selected='true'] {
123
+ color: ${colors.brand.primary};
124
+ font-weight: ${fonts.weight.semibold};
125
+ }
111
126
  &:hover,
112
127
  &:focus {
113
128
  color: ${colors.brand.primary};
@@ -145,6 +160,8 @@ const FolderItem = ({
145
160
  const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
146
161
  const selected = selectedFolder ? selectedFolder.id === id : false;
147
162
 
163
+ const levelVariable = useMemo(() => ({ '--level': level } as unknown as CSSProperties), [level]);
164
+
148
165
  const focused = focusedFolder?.id === id;
149
166
 
150
167
  const handleClickFolder = () => {
@@ -195,7 +212,7 @@ const FolderItem = ({
195
212
  aria-current={selected ? 'page' : undefined}
196
213
  aria-describedby={containsResource ? `alreadyAdded-${folder.id}` : undefined}
197
214
  ref={ref}
198
- level={level}
215
+ style={levelVariable}
199
216
  onKeyDown={(e: KeyboardEvent<HTMLElement>) => {
200
217
  if (e.key === 'Enter') {
201
218
  setSelectedFolder(folder);
@@ -205,7 +222,7 @@ const FolderItem = ({
205
222
  }}
206
223
  to={loading ? '' : linkPath}
207
224
  tabIndex={tabable ? 0 : -1}
208
- selected={selected}
225
+ data-selected={selected}
209
226
  onFocus={() => setFocusedFolder(folder)}
210
227
  onClick={handleClickFolder}
211
228
  >
@@ -213,7 +230,7 @@ const FolderItem = ({
213
230
  <OpenButton
214
231
  aria-hidden
215
232
  tabIndex={-1}
216
- isOpen={isOpen}
233
+ data-open={isOpen}
217
234
  onClick={(e) => {
218
235
  e.stopPropagation();
219
236
  e.preventDefault();
@@ -237,7 +254,7 @@ const FolderItem = ({
237
254
  id={treestructureId(type, folder.id)}
238
255
  aria-expanded={isMaxDepth || emptyFolder ? undefined : isOpen}
239
256
  aria-selected={selected}
240
- focused={focusedFolder?.id === folder.id}
257
+ data-focused={focusedFolder?.id === folder.id}
241
258
  aria-describedby={containsResource ? `alreadyAdded-${folder.id}` : undefined}
242
259
  aria-label={`${name}${folder.status === 'shared' ? `, ${t('myNdla.folder.sharing.shared')}` : ''}`}
243
260
  variant="ghost"
@@ -245,21 +262,19 @@ const FolderItem = ({
245
262
  fontWeight="normal"
246
263
  colorTheme="light"
247
264
  ref={ref}
248
- level={level}
249
- selected={selected}
265
+ style={levelVariable}
266
+ data-selected={selected}
250
267
  disabled={loading}
251
- onFocus={(e) => {
252
- setFocusedFolder(focusedFolder || folder);
253
- }}
268
+ onFocus={() => setFocusedFolder(focusedFolder || folder)}
254
269
  onClick={handleClickFolder}
255
- isCreatingFolder={isCreatingFolder}
270
+ data-creating={isCreatingFolder}
256
271
  >
257
272
  <IconWrapper>
258
273
  {!hideArrow && (
259
274
  <OpenButton
260
275
  aria-hidden
261
276
  tabIndex={-1}
262
- isOpen={isOpen}
277
+ data-open={isOpen}
263
278
  onClick={(e) => {
264
279
  e.stopPropagation();
265
280
  setFocusedFolder(folder);