@ndla/ui 18.0.0 → 19.0.1

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 (58) hide show
  1. package/es/Masthead/MastheadAuthModal.js +8 -3
  2. package/es/MyNdla/Resource/Folder.js +11 -10
  3. package/es/Resource/BlockResource.js +14 -8
  4. package/es/Resource/ListResource.js +15 -9
  5. package/es/Resource/resourceComponents.js +12 -11
  6. package/es/TreeStructure/FolderItem.js +7 -6
  7. package/es/TreeStructure/FolderItems.js +8 -7
  8. package/es/TreeStructure/TreeStructure.js +65 -80
  9. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +23 -11
  10. package/es/locale/messages-en.js +2 -0
  11. package/es/locale/messages-nb.js +2 -0
  12. package/es/locale/messages-nn.js +2 -0
  13. package/es/locale/messages-se.js +2 -0
  14. package/es/locale/messages-sma.js +2 -0
  15. package/lib/Masthead/MastheadAuthModal.js +14 -7
  16. package/lib/MyNdla/Resource/Folder.d.ts +4 -3
  17. package/lib/MyNdla/Resource/Folder.js +11 -10
  18. package/lib/Resource/BlockResource.d.ts +4 -3
  19. package/lib/Resource/BlockResource.js +14 -7
  20. package/lib/Resource/ListResource.d.ts +4 -3
  21. package/lib/Resource/ListResource.js +15 -8
  22. package/lib/Resource/resourceComponents.d.ts +4 -1
  23. package/lib/Resource/resourceComponents.js +14 -13
  24. package/lib/TreeStructure/FolderItem.js +8 -6
  25. package/lib/TreeStructure/FolderItems.d.ts +1 -1
  26. package/lib/TreeStructure/FolderItems.js +8 -8
  27. package/lib/TreeStructure/TreeStructure.d.ts +6 -1
  28. package/lib/TreeStructure/TreeStructure.js +66 -80
  29. package/lib/TreeStructure/TreeStructure.types.d.ts +5 -3
  30. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +23 -11
  31. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +1 -1
  32. package/lib/locale/messages-en.d.ts +2 -0
  33. package/lib/locale/messages-en.js +2 -0
  34. package/lib/locale/messages-nb.d.ts +2 -0
  35. package/lib/locale/messages-nb.js +2 -0
  36. package/lib/locale/messages-nn.d.ts +2 -0
  37. package/lib/locale/messages-nn.js +2 -0
  38. package/lib/locale/messages-se.d.ts +2 -0
  39. package/lib/locale/messages-se.js +2 -0
  40. package/lib/locale/messages-sma.d.ts +2 -0
  41. package/lib/locale/messages-sma.js +2 -0
  42. package/package.json +5 -5
  43. package/src/Masthead/MastheadAuthModal.tsx +9 -0
  44. package/src/MyNdla/Resource/Folder.tsx +6 -6
  45. package/src/Resource/BlockResource.tsx +7 -6
  46. package/src/Resource/ListResource.tsx +7 -6
  47. package/src/Resource/resourceComponents.tsx +8 -1
  48. package/src/TreeStructure/FolderItem.tsx +3 -2
  49. package/src/TreeStructure/FolderItems.tsx +4 -3
  50. package/src/TreeStructure/TreeStructure.tsx +54 -42
  51. package/src/TreeStructure/TreeStructure.types.ts +5 -3
  52. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +7 -7
  53. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +1 -1
  54. package/src/locale/messages-en.ts +2 -0
  55. package/src/locale/messages-nb.ts +2 -0
  56. package/src/locale/messages-nn.ts +2 -0
  57. package/src/locale/messages-se.ts +2 -0
  58. package/src/locale/messages-sma.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndla/ui",
3
- "version": "18.0.0",
3
+ "version": "19.0.1",
4
4
  "description": "UI component library for NDLA.",
5
5
  "license": "GPL-3.0",
6
6
  "main": "lib/index.js",
@@ -31,15 +31,15 @@
31
31
  "types"
32
32
  ],
33
33
  "dependencies": {
34
- "@ndla/button": "^2.6.0",
34
+ "@ndla/button": "^3.0.0",
35
35
  "@ndla/carousel": "^1.2.11",
36
36
  "@ndla/core": "^2.3.0",
37
37
  "@ndla/hooks": "^1.1.4",
38
38
  "@ndla/icons": "^1.10.0",
39
39
  "@ndla/licenses": "^5.0.2",
40
40
  "@ndla/modal": "^1.2.12",
41
- "@ndla/notion": "^3.1.17",
42
- "@ndla/safelink": "^2.0.8",
41
+ "@ndla/notion": "^3.1.19",
42
+ "@ndla/safelink": "^2.1.0",
43
43
  "@ndla/switch": "^0.1.7",
44
44
  "@ndla/tabs": "^1.1.10",
45
45
  "@ndla/tooltip": "^2.1.2",
@@ -81,5 +81,5 @@
81
81
  "publishConfig": {
82
82
  "access": "public"
83
83
  },
84
- "gitHead": "917673dcc652e2cbc9106b8b4c30cd393c00cff5"
84
+ "gitHead": "8c51904170c455fc66f1c624b02829efe15c1482"
85
85
  }
@@ -10,6 +10,8 @@ import React from 'react';
10
10
  import styled from '@emotion/styled';
11
11
  import Button from '@ndla/button';
12
12
  import { Feide } from '@ndla/icons/common';
13
+ import { colors, fonts, spacing } from '@ndla/core';
14
+ import { useTranslation } from 'react-i18next';
13
15
 
14
16
  import AuthModal, { AuthModalProps } from '../User/AuthModal';
15
17
 
@@ -18,7 +20,12 @@ type FeideWrapperProps = {
18
20
  };
19
21
 
20
22
  const StyledButton = styled(Button)<FeideWrapperProps>`
23
+ font-weight: ${fonts.weight.semibold};
24
+ display: flex;
25
+ align-items: center;
21
26
  .feide-icon svg {
27
+ margin-left: ${spacing.xsmall};
28
+ fill: ${colors.brand.primary};
22
29
  color: ${(props) => (props.inverted ? `#ffffff` : `#000000`)};
23
30
  width: 22px;
24
31
  height: 22px;
@@ -35,11 +42,13 @@ interface Props extends AuthModalProps {
35
42
  }
36
43
 
37
44
  const MastheadAuthModal = ({ inverted, ...rest }: Props) => {
45
+ const { t } = useTranslation();
38
46
  return (
39
47
  <AuthModal
40
48
  {...rest}
41
49
  activateButton={
42
50
  <StyledButton inverted={inverted} ghostPill={!inverted} ghostPillInverted={inverted} aria-label="Feide">
51
+ {t('myNdla.myNDLA')}
43
52
  <span className="feide-icon">
44
53
  <Feide />
45
54
  </span>
@@ -7,14 +7,14 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import React, { ReactNode } from 'react';
10
+ import React from 'react';
11
11
  import { FolderOutlined } from '@ndla/icons/contentType';
12
12
  import { FileDocumentOutline } from '@ndla/icons/common';
13
13
  import { fonts, spacing, colors, mq, breakpoints } from '@ndla/core';
14
14
  import { css } from '@emotion/core';
15
15
  import { useTranslation } from 'react-i18next';
16
16
  import SafeLink from '@ndla/safelink';
17
- import { MenuButton } from '@ndla/button';
17
+ import { MenuButton, MenuItemProps } from '@ndla/button';
18
18
 
19
19
  interface FolderIconWrapperProps {
20
20
  type?: LayoutType;
@@ -84,7 +84,7 @@ interface Props {
84
84
  description?: string;
85
85
  link: string;
86
86
  type: LayoutType;
87
- actionMenu?: ReactNode;
87
+ menuItems?: MenuItemProps[];
88
88
  }
89
89
 
90
90
  interface IconCountProps {
@@ -124,14 +124,14 @@ const IconCount = ({ type, count, layoutType }: IconCountProps) => {
124
124
  return (
125
125
  <IconCountWrapper type={layoutType}>
126
126
  <Icon aria-label={t(`myNdla.${type}s`)} />
127
- <span>{layoutType === 'block' ? count : t(`myNdla.${type}s`, { count: 3 })}</span>
127
+ <span>{layoutType === 'block' ? count : t(`myNdla.${type}s`, { count })}</span>
128
128
  </IconCountWrapper>
129
129
  );
130
130
  };
131
131
 
132
132
  type LayoutType = 'list' | 'block';
133
133
 
134
- const Folder = ({ link, title, subFolders, subResources, type = 'list', actionMenu }: Props) => {
134
+ const Folder = ({ link, title, subFolders, subResources, type = 'list', menuItems }: Props) => {
135
135
  const { t } = useTranslation();
136
136
  return (
137
137
  <FolderWrapper to={link}>
@@ -141,7 +141,7 @@ const Folder = ({ link, title, subFolders, subResources, type = 'list', actionMe
141
141
  <FolderTitle>{title}</FolderTitle>
142
142
  <IconCount layoutType={type} type={'folder'} count={subFolders} />
143
143
  <IconCount layoutType={type} type={'resource'} count={subResources} />
144
- <MenuButton size="small" />
144
+ {menuItems && menuItems.length > 0 && <MenuButton size="small" menuItems={menuItems} />}
145
145
  </FolderWrapper>
146
146
  );
147
147
  };
@@ -7,11 +7,12 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import React, { ReactNode } from 'react';
10
+ import React from 'react';
11
11
  import SafeLink from '@ndla/safelink';
12
12
  import { colors, fonts, spacing } from '@ndla/core';
13
+ import { MenuButton, MenuItemProps } from '@ndla/button';
13
14
  import Image from '../Image';
14
- import { CompressedTags, ResourceImageProps, ResourceTitle, Row, TopicList } from './resourceComponents';
15
+ import { CompressedTagList, ResourceImageProps, ResourceTitle, Row, TopicList } from './resourceComponents';
15
16
 
16
17
  interface BlockResourceProps {
17
18
  link: string;
@@ -20,7 +21,7 @@ interface BlockResourceProps {
20
21
  topics: string[];
21
22
  tags?: string[];
22
23
  description?: string;
23
- actionMenu?: ReactNode;
24
+ menuItems?: MenuItemProps[];
24
25
  }
25
26
 
26
27
  const BlockElementWrapper = styled(SafeLink)`
@@ -77,7 +78,7 @@ const ImageWrapper = styled.div`
77
78
  }
78
79
  `;
79
80
 
80
- const BlockResource = ({ link, title, tags, resourceImage, topics, description, actionMenu }: BlockResourceProps) => {
81
+ const BlockResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: BlockResourceProps) => {
81
82
  return (
82
83
  <BlockElementWrapper to={link}>
83
84
  <ImageWrapper>
@@ -90,8 +91,8 @@ const BlockResource = ({ link, title, tags, resourceImage, topics, description,
90
91
  <TopicList topics={topics} />
91
92
  <BlockDescription>{description}</BlockDescription>
92
93
  <RightRow>
93
- {tags && CompressedTags(tags)}
94
- {actionMenu}
94
+ {tags && <CompressedTagList tags={tags} />}
95
+ {menuItems && menuItems.length > 0 && <MenuButton size="small" menuItems={menuItems} />}
95
96
  </RightRow>
96
97
  </BlockInfoWrapper>
97
98
  </BlockElementWrapper>
@@ -7,11 +7,12 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import React, { ReactNode } from 'react';
10
+ import React from 'react';
11
11
  import SafeLink from '@ndla/safelink';
12
12
  import { fonts, spacing, colors, breakpoints, mq } from '@ndla/core';
13
+ import { MenuButton, MenuItemProps } from '@ndla/button';
13
14
  import Image from '../Image';
14
- import { CompressedTags, ResourceImageProps, ResourceTitle, TopicList } from './resourceComponents';
15
+ import { CompressedTagList, ResourceImageProps, ResourceTitle, TopicList } from './resourceComponents';
15
16
 
16
17
  const ResourceDescription = styled.p`
17
18
  grid-area: description;
@@ -109,10 +110,10 @@ export interface ListResourceProps {
109
110
  topics: string[];
110
111
  tags?: string[];
111
112
  description?: string;
112
- actionMenu?: ReactNode;
113
+ menuItems?: MenuItemProps[];
113
114
  }
114
115
 
115
- const ListResource = ({ link, title, tags, resourceImage, topics, description, actionMenu }: ListResourceProps) => {
116
+ const ListResource = ({ link, title, tags, resourceImage, topics, description, menuItems }: ListResourceProps) => {
116
117
  const showDescription = description !== undefined;
117
118
 
118
119
  return (
@@ -126,8 +127,8 @@ const ListResource = ({ link, title, tags, resourceImage, topics, description, a
126
127
  </TopicAndTitle>
127
128
  {showDescription && <ResourceDescription>{description}</ResourceDescription>}
128
129
  <TagsandActionMenu>
129
- {tags && CompressedTags(tags)}
130
- {actionMenu}
130
+ {tags && <CompressedTagList tags={tags} />}
131
+ {menuItems && menuItems.length > 0 && <MenuButton size="small" menuItems={menuItems} />}
131
132
  </TagsandActionMenu>
132
133
  </ResourceWrapper>
133
134
  );
@@ -48,6 +48,9 @@ const StyledTagListElement = styled.li`
48
48
  ::before {
49
49
  content: '#';
50
50
  }
51
+ &:hover {
52
+ color: ${colors.brand.primary};
53
+ }
51
54
  `;
52
55
 
53
56
  const StyledTopicList = styled.ul`
@@ -100,7 +103,11 @@ export const TagList = ({ tags }: TagListProps) => {
100
103
  );
101
104
  };
102
105
 
103
- export const CompressedTags = (tags: string[]) => {
106
+ interface CompressedTagListProps {
107
+ tags: string[];
108
+ }
109
+
110
+ export const CompressedTagList = ({ tags }: CompressedTagListProps) => {
104
111
  const visibleTags = tags.slice(0, 3);
105
112
  const remainingTags = tags.slice(3, tags.length).map((tag) => {
106
113
  return {
@@ -11,6 +11,7 @@ import styled from '@emotion/styled';
11
11
  import { ArrowDropDown } from '@ndla/icons/common';
12
12
  import { FolderOutlined } from '@ndla/icons/contentType';
13
13
  import { colors, spacing, misc, animations } from '@ndla/core';
14
+ import SafeLink from '@ndla/safelink';
14
15
  import { SetFocusedFolderId, FolderChildFuncType } from './TreeStructure.types';
15
16
 
16
17
  const OpenButton = styled.button<{ isOpen: boolean }>`
@@ -75,7 +76,7 @@ const FolderName = styled.button<{ marked: boolean; noArrow?: boolean }>`
75
76
  text-align: left;
76
77
  `;
77
78
 
78
- const FolderNameLink = FolderName.withComponent('a');
79
+ const FolderNameLink = FolderName.withComponent(SafeLink);
79
80
 
80
81
  interface Props {
81
82
  name: string;
@@ -135,9 +136,9 @@ const FolderItem = ({
135
136
  <FolderNameLink
136
137
  ref={folderNameLinkRef}
137
138
  noArrow={hideArrow}
139
+ to={loading ? '' : url}
138
140
  tabIndex={marked ? 0 : -1}
139
141
  marked={marked}
140
- href={loading ? undefined : url}
141
142
  onFocus={() => {
142
143
  setFocusedFolderId(id);
143
144
  }}
@@ -12,7 +12,6 @@ import { animations, spacing } from '@ndla/core';
12
12
  import FolderItem from './FolderItem';
13
13
  import FolderNameInput from './FolderNameInput';
14
14
  import { FolderItemsProps } from './TreeStructure.types';
15
- import { MAX_LEVEL_FOR_FOLDERS } from './TreeStructure';
16
15
 
17
16
  const StyledUL = styled.ul<{ firstLevel?: boolean }>`
18
17
  ${animations.fadeInLeft(animations.durations.fast)};
@@ -49,11 +48,12 @@ const FolderItems = ({
49
48
  setFocusedFolderId,
50
49
  firstLevel,
51
50
  folderChild,
51
+ maximumLevelsOfFoldersAllowed,
52
52
  }: FolderItemsProps) => (
53
53
  <StyledUL role="group" firstLevel={firstLevel}>
54
54
  {data.map(({ name, data: dataChildren, id, url, icon }, _index) => {
55
55
  const newIdPaths = [...idPaths, _index];
56
- const isOpen = openFolders?.has(id);
56
+ const isOpen = openFolders?.includes(id);
57
57
  return (
58
58
  <StyledLI key={id} role="treeitem">
59
59
  <div>
@@ -69,7 +69,7 @@ const FolderItems = ({
69
69
  focusedFolderId={focusedFolderId}
70
70
  onToggleOpen={onToggleOpen}
71
71
  onMarkFolder={onMarkFolder}
72
- hideArrow={dataChildren?.length === 0 || newIdPaths.length >= MAX_LEVEL_FOR_FOLDERS}
72
+ hideArrow={dataChildren?.length === 0 || newIdPaths.length >= maximumLevelsOfFoldersAllowed}
73
73
  noPaddingWhenArrowIsHidden={editable && firstLevel && dataChildren?.length === 0}
74
74
  setFocusedFolderId={setFocusedFolderId}
75
75
  folderChild={folderChild}
@@ -101,6 +101,7 @@ const FolderItems = ({
101
101
  setFocusedFolderId={setFocusedFolderId}
102
102
  firstLevel={false}
103
103
  folderChild={folderChild}
104
+ maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
104
105
  />
105
106
  )}
106
107
  </StyledLI>
@@ -13,6 +13,7 @@ import Tooltip from '@ndla/tooltip';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import styled from '@emotion/styled';
15
15
  import { spacing, fonts } from '@ndla/core';
16
+ import { uniq } from 'lodash';
16
17
  import TreeStructureStyledWrapper from './TreeStructureWrapper';
17
18
  import FolderItems from './FolderItems';
18
19
  import { getIdPathsOfFolder, getPathOfFolder, getFolderName } from './helperFunctions';
@@ -41,21 +42,23 @@ const TreeStructure = ({
41
42
  folderIdMarkedByDefault,
42
43
  defaultOpenFolders,
43
44
  folderChild,
45
+ maximumLevelsOfFoldersAllowed,
44
46
  }: TreeStructureProps) => {
45
47
  const { t } = useTranslation();
46
48
  const [newFolder, setNewFolder] = useState<NewFolderProps | undefined>();
47
- const [openFolders, setOpenFolders] = useState<Set<string>>(new Set(defaultOpenFolders || []));
49
+ const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
48
50
  const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
49
- const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0].id);
51
+ const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0]?.id);
50
52
  const treestructureRef = useRef<HTMLDivElement>(null);
51
53
  const wrapperRef = useRef<HTMLDivElement>(null);
52
54
  const rootLevelId = useMemo(() => uuid(), []); // TODO: use useId hook when we update to React 18
53
55
 
54
56
  useEffect(() => {
55
- setOpenFolders((prev) => {
56
- defaultOpenFolders?.forEach((id) => prev.add(id));
57
- return new Set(prev);
58
- });
57
+ if (defaultOpenFolders) {
58
+ setOpenFolders((prev) => {
59
+ return uniq([...defaultOpenFolders, ...prev]);
60
+ });
61
+ }
59
62
  }, [defaultOpenFolders]);
60
63
 
61
64
  useEffect(() => {
@@ -65,48 +68,44 @@ const TreeStructure = ({
65
68
  }, [loading]);
66
69
 
67
70
  const onToggleOpen = (id: string) => {
68
- setOpenFolders((prev) => {
69
- if (prev.has(id)) {
70
- prev.delete(id);
71
- // Did we just closed a folder with a marked folder inside it?
72
- // If so, we need to mark the folder we just closed.
73
- if (markedFolderId) {
74
- const closingFolderPath = getPathOfFolder(data, id);
75
- const markedFolderPath = getPathOfFolder(data, markedFolderId);
76
- const markedFolderIsSubPath = closingFolderPath.every(
77
- (folderId, _index) => markedFolderPath[_index] === folderId,
78
- );
79
- if (markedFolderIsSubPath) {
80
- setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
81
- }
71
+ if (openFolders.includes(id)) {
72
+ // Did we just closed a folder with a marked folder inside it?
73
+ // If so, we need to mark the folder we just closed.
74
+ if (markedFolderId) {
75
+ const closingFolderPath = getPathOfFolder(data, id);
76
+ const markedFolderPath = getPathOfFolder(data, markedFolderId);
77
+ const markedFolderIsSubPath = closingFolderPath.every(
78
+ (folderId, _index) => markedFolderPath[_index] === folderId,
79
+ );
80
+ if (markedFolderIsSubPath) {
81
+ setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
82
82
  }
83
- } else {
84
- prev.add(id);
85
83
  }
86
- return new Set(prev);
87
- });
84
+ setOpenFolders(openFolders.filter((folder) => folder !== id));
85
+ } else {
86
+ setOpenFolders(uniq([...openFolders, id]));
87
+ }
88
88
  };
89
89
 
90
90
  const onCreateNewFolder = (props: { idPaths: number[]; parentId?: string }) => {
91
91
  setNewFolder(props);
92
92
  };
93
93
 
94
- const onSaveNewFolder = async (value: string) => {
94
+ const onSaveNewFolder = (value: string) => {
95
95
  if (newFolder) {
96
96
  // We would like to create a new folder with the name of value.
97
97
  // Its location in structure is based on newFolder object
98
- const newFolderId = await onNewFolder({ ...newFolder, value });
99
- if (newFolderId) {
100
- setMarkedFolderId(newFolderId);
101
- setFocusedFolderId(newFolderId);
102
- // Open current folder in case it was closed..
103
- setOpenFolders((prev) => {
98
+ onNewFolder({ ...newFolder, value }).then((newFolderId) => {
99
+ if (newFolderId) {
100
+ setMarkedFolderId(newFolderId);
101
+ setFocusedFolderId(newFolderId);
102
+ // Open current folder in case it was closed..
103
+
104
104
  if (newFolder.parentId) {
105
- prev.add(newFolder.parentId);
105
+ setOpenFolders(uniq([...openFolders, newFolder.parentId]));
106
106
  }
107
- return new Set(prev);
108
- });
109
- }
107
+ }
108
+ });
110
109
  }
111
110
  };
112
111
 
@@ -119,6 +118,9 @@ const TreeStructure = ({
119
118
  setFocusedFolderId(id);
120
119
  };
121
120
 
121
+ const paths = getPathOfFolder(data, markedFolderId || '');
122
+ const canAddFolder = editable && paths.length < (maximumLevelsOfFoldersAllowed || 1);
123
+
122
124
  return (
123
125
  <div
124
126
  ref={treestructureRef}
@@ -134,7 +136,7 @@ const TreeStructure = ({
134
136
  });
135
137
  }
136
138
  }}>
137
- <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>
139
+ {label && <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>}
138
140
  <TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
139
141
  <FolderItems
140
142
  idPaths={[]}
@@ -154,22 +156,28 @@ const TreeStructure = ({
154
156
  setFocusedFolderId={setFocusedFolderId}
155
157
  firstLevel
156
158
  folderChild={folderChild}
159
+ maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
157
160
  />
158
161
  </TreeStructureStyledWrapper>
159
162
  {editable && (
160
163
  <AddFolderWrapper>
161
164
  <Tooltip
162
- tooltip={t('myNdla.newFolderUnder', {
163
- folderName: getFolderName(data, markedFolderId),
164
- })}>
165
+ tooltip={
166
+ canAddFolder
167
+ ? t('myNdla.newFolderUnder', {
168
+ folderName: getFolderName(data, markedFolderId),
169
+ })
170
+ : t('myNdla.maxFoldersAlreadyAdded')
171
+ }>
165
172
  <AddButton
173
+ disabled={!canAddFolder}
166
174
  aria-label={t('myNdla.newFolder')}
167
175
  onClick={() => {
168
- const paths = getPathOfFolder(data, markedFolderId || '');
169
176
  const idPaths = getIdPathsOfFolder(data, markedFolderId || '');
170
177
  setNewFolder({ idPaths, parentId: paths[paths.length - 1] });
171
- }}
172
- />
178
+ }}>
179
+ {t('myNdla.newFolder')}
180
+ </AddButton>
173
181
  </Tooltip>
174
182
  </AddFolderWrapper>
175
183
  )}
@@ -177,4 +185,8 @@ const TreeStructure = ({
177
185
  );
178
186
  };
179
187
 
188
+ TreeStructure.defaultProps = {
189
+ maximumLevelsOfFoldersAllowed: MAX_LEVEL_FOR_FOLDERS,
190
+ };
191
+
180
192
  export default TreeStructure;
@@ -34,11 +34,12 @@ interface CommonFolderProps {
34
34
 
35
35
  export interface TreeStructureProps extends CommonFolderProps {
36
36
  framed?: boolean;
37
- label: string;
37
+ label?: string;
38
38
  folderIdMarkedByDefault?: string;
39
39
  onNewFolder: (props: { value: string; parentId?: string; idPaths: number[] }) => Promise<string>;
40
40
  defaultOpenFolders?: string[];
41
41
  folderChild?: FolderChildFuncType;
42
+ maximumLevelsOfFoldersAllowed: number;
42
43
  }
43
44
 
44
45
  export type onCreateNewFolderProp = ({
@@ -49,7 +50,7 @@ export type onCreateNewFolderProp = ({
49
50
  parentId: string | undefined;
50
51
  }) => void;
51
52
 
52
- export type SetOpenFolderProp = React.Dispatch<React.SetStateAction<Set<string>>>;
53
+ export type SetOpenFolderProp = React.Dispatch<React.SetStateAction<string[]>>;
53
54
  export type SetFocusedFolderId = React.Dispatch<React.SetStateAction<string | undefined>>;
54
55
 
55
56
  export type FolderChildFuncType = (id: string, tabIndex: number) => ReactNode;
@@ -60,7 +61,7 @@ export interface FolderItemsProps extends CommonFolderProps {
60
61
  onCancelNewFolder: () => void;
61
62
  onCreateNewFolder: onCreateNewFolderProp;
62
63
  newFolder: NewFolderProps | undefined;
63
- openFolders: Set<string>;
64
+ openFolders: string[];
64
65
  markedFolderId?: string;
65
66
  onMarkFolder: (id: string) => void;
66
67
  idPaths: number[];
@@ -70,4 +71,5 @@ export interface FolderItemsProps extends CommonFolderProps {
70
71
  keyNavigationFocusIsCreateFolderButton?: boolean;
71
72
  icon?: ReactNode;
72
73
  folderChild?: FolderChildFuncType;
74
+ maximumLevelsOfFoldersAllowed: number;
73
75
  }
@@ -66,7 +66,7 @@ const keyboardNavigation = ({
66
66
  if (dataId === id) {
67
67
  elementWithKeyFocus.paths = paths;
68
68
  elementWithKeyFocus.index = _index;
69
- elementWithKeyFocus.isOpen = openFolders.has(dataId) && childData && childData.length > 0;
69
+ elementWithKeyFocus.isOpen = openFolders.includes(dataId) && childData && childData.length > 0;
70
70
  elementWithKeyFocus.data = childData;
71
71
  elementWithKeyFocus.parent = parent;
72
72
  elementWithKeyFocus.parentId = parentId;
@@ -78,7 +78,7 @@ const keyboardNavigation = ({
78
78
  if (!updatePathToElementWithKeyFocus(data, [], data)) {
79
79
  // Couldn't find its location in the tree.
80
80
  // This should not happen, reset its value to root.
81
- setFocusedFolderId(e.key === 'ArrowDown' ? data[0].id : undefined);
81
+ setFocusedFolderId(e.key === 'ArrowDown' ? data[0]?.id : undefined);
82
82
  return;
83
83
  }
84
84
  e.preventDefault();
@@ -113,7 +113,7 @@ const keyboardNavigation = ({
113
113
  }
114
114
 
115
115
  if (!id && e.key === 'ArrowDown') {
116
- setFocusedFolderId(data[0].id);
116
+ setFocusedFolderId(data[0]?.id);
117
117
  return;
118
118
  }
119
119
  if (!id) {
@@ -124,7 +124,7 @@ const keyboardNavigation = ({
124
124
  if (elementWithKeyFocus.index > 0) {
125
125
  // Move upwards to the parent folder
126
126
  setFocusedFolderId(
127
- elementWithKeyFocus.parent ? elementWithKeyFocus.parent[elementWithKeyFocus.index - 1].id : undefined,
127
+ elementWithKeyFocus.parent ? elementWithKeyFocus.parent[elementWithKeyFocus.index - 1]?.id : undefined,
128
128
  );
129
129
  } else if (elementWithKeyFocus.paths.length > 0) {
130
130
  elementWithKeyFocus.paths.pop();
@@ -133,14 +133,14 @@ const keyboardNavigation = ({
133
133
  findParent = findParent[index].data as FolderStructureProps[];
134
134
  });
135
135
  const parentsCurrentIndex = findParent.findIndex(({ id }) => id === elementWithKeyFocus.parentId);
136
- setFocusedFolderId(findParent[parentsCurrentIndex].id);
136
+ setFocusedFolderId(findParent[parentsCurrentIndex]?.id);
137
137
  }
138
138
  return;
139
139
  }
140
140
 
141
141
  if (elementWithKeyFocus.isOpen) {
142
142
  if (elementWithKeyFocus.data?.length) {
143
- setFocusedFolderId(elementWithKeyFocus.data[0].id);
143
+ setFocusedFolderId(elementWithKeyFocus.data[0]?.id);
144
144
  } else {
145
145
  // move to next child of parent if any... need new traverse :-/
146
146
  traverseUpwards(data, setFocusedFolderId, elementWithKeyFocus.paths, elementWithKeyFocus.index);
@@ -150,7 +150,7 @@ const keyboardNavigation = ({
150
150
 
151
151
  if (elementWithKeyFocus.parent && elementWithKeyFocus.index < elementWithKeyFocus.parent?.length - 1) {
152
152
  // Move downwards to the next child
153
- setFocusedFolderId(elementWithKeyFocus.parent[elementWithKeyFocus.index + 1].id);
153
+ setFocusedFolderId(elementWithKeyFocus.parent[elementWithKeyFocus.index + 1]?.id);
154
154
  return;
155
155
  }
156
156
 
@@ -12,7 +12,7 @@ export interface KeyboardNavigationProps {
12
12
  e: React.KeyboardEvent<HTMLElement>;
13
13
  data: FolderStructureProps[];
14
14
  setFocusedFolderId: SetFocusedFolderId;
15
- openFolders: Set<string>;
15
+ openFolders: string[];
16
16
  onToggleOpen: (id: string) => void;
17
17
  focusedFolderId: string | undefined;
18
18
  }
@@ -20,6 +20,7 @@ const messages = {
20
20
  delete: 'Delete',
21
21
  },
22
22
  createFolder: 'Create folder',
23
+ maxFoldersAlreadyAdded: 'Maximum subfolders reached',
23
24
  newFolder: {
24
25
  placeholder: 'Add foldername',
25
26
  defaultName: 'New folder',
@@ -983,6 +984,7 @@ const messages = {
983
984
  close: 'Close fact box',
984
985
  },
985
986
  myNdla: {
987
+ myNDLA: 'My NDLA',
986
988
  resources: '{{count}} Resource',
987
989
  resources_plural: '{{count}} Resources',
988
990
  folders: '{{count}} Folder',
@@ -20,6 +20,7 @@ const messages = {
20
20
  delete: 'Slett',
21
21
  },
22
22
  createFolder: 'Lag mappe',
23
+ maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
23
24
  newFolder: {
24
25
  placeholder: 'Skriv navn på mappe',
25
26
  defaultName: 'Ny mappe',
@@ -981,6 +982,7 @@ const messages = {
981
982
  close: 'Lukk faktaboks',
982
983
  },
983
984
  myNdla: {
985
+ myNDLA: 'Min NDLA',
984
986
  resources: '{{count}} ressurs',
985
987
  resources_plural: '{{count}} ressurser',
986
988
  folders: '{{count}} mappe',
@@ -20,6 +20,7 @@ const messages = {
20
20
  delete: 'Slett',
21
21
  },
22
22
  createFolder: 'Lag mappe',
23
+ maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
23
24
  newFolder: {
24
25
  placeholder: 'Skriv namn på mappe',
25
26
  defaultName: 'Ny mappe',
@@ -982,6 +983,7 @@ const messages = {
982
983
  close: 'Lukk faktaboks',
983
984
  },
984
985
  myNdla: {
986
+ myNDLA: 'Min NDLA',
985
987
  resources: '{{count}} ressurs',
986
988
  resources_plural: '{{count}} ressursar',
987
989
  folders: '{{count}} mappe',
@@ -20,6 +20,7 @@ const messages = {
20
20
  delete: 'Slett',
21
21
  },
22
22
  createFolder: 'Lag mappe',
23
+ maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
23
24
  newFolder: {
24
25
  placeholder: 'Skriv navn på mappe',
25
26
  defaultName: 'Ny mappe',
@@ -981,6 +982,7 @@ const messages = {
981
982
  close: 'Lukk faktaboks',
982
983
  },
983
984
  myNdla: {
985
+ myNDLA: 'Min NDLA',
984
986
  resources: '{{count}} ressurs',
985
987
  resources_plural: '{{count}} ressurser',
986
988
  folders: '{{count}} mappe',
@@ -20,6 +20,7 @@ const messages = {
20
20
  delete: 'Slett',
21
21
  },
22
22
  createFolder: 'Lag mappe',
23
+ maxFoldersAlreadyAdded: 'Maks nivå av undermapper nådd',
23
24
  newFolder: {
24
25
  placeholder: 'Skriv navn på mappe',
25
26
  defaultName: 'Ny mappe',
@@ -981,6 +982,7 @@ const messages = {
981
982
  close: 'Lukk faktaboks',
982
983
  },
983
984
  myNdla: {
985
+ myNDLA: 'Min NDLA',
984
986
  resources: '{{count}} ressurs',
985
987
  resources_plural: '{{count}} ressurser',
986
988
  folders: '{{count}} mappe',