@ndla/ui 19.1.1 → 19.2.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 (35) hide show
  1. package/es/TreeStructure/FolderItem.js +54 -38
  2. package/es/TreeStructure/FolderItems.js +29 -35
  3. package/es/TreeStructure/FolderNameInput.js +11 -15
  4. package/es/TreeStructure/TreeStructure.js +64 -91
  5. package/es/TreeStructure/arrowNavigation.js +44 -0
  6. package/es/TreeStructure/helperFunctions.js +41 -35
  7. package/lib/TreeStructure/FolderItem.d.ts +6 -3
  8. package/lib/TreeStructure/FolderItem.js +55 -38
  9. package/lib/TreeStructure/FolderItems.d.ts +1 -1
  10. package/lib/TreeStructure/FolderItems.js +29 -35
  11. package/lib/TreeStructure/FolderNameInput.d.ts +3 -2
  12. package/lib/TreeStructure/FolderNameInput.js +11 -15
  13. package/lib/TreeStructure/TreeStructure.d.ts +1 -6
  14. package/lib/TreeStructure/TreeStructure.js +63 -92
  15. package/lib/TreeStructure/TreeStructure.types.d.ts +13 -20
  16. package/lib/TreeStructure/arrowNavigation.d.ts +9 -0
  17. package/lib/TreeStructure/arrowNavigation.js +54 -0
  18. package/lib/TreeStructure/helperFunctions.d.ts +3 -4
  19. package/lib/TreeStructure/helperFunctions.js +45 -35
  20. package/package.json +5 -5
  21. package/src/TreeStructure/FolderItem.tsx +63 -40
  22. package/src/TreeStructure/FolderItems.tsx +26 -19
  23. package/src/TreeStructure/FolderNameInput.tsx +9 -11
  24. package/src/TreeStructure/TreeStructure.tsx +56 -71
  25. package/src/TreeStructure/TreeStructure.types.ts +13 -17
  26. package/src/TreeStructure/arrowNavigation.ts +53 -0
  27. package/src/TreeStructure/helperFunctions.ts +17 -25
  28. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.js +0 -194
  29. package/es/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -0
  30. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.d.ts +0 -11
  31. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.js +0 -198
  32. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.d.ts +0 -26
  33. package/lib/TreeStructure/keyboardNavigation/keyboardNavigation.types.js +0 -1
  34. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.ts +0 -161
  35. package/src/TreeStructure/keyboardNavigation/keyboardNavigation.types.ts +0 -28
@@ -6,13 +6,14 @@
6
6
  *
7
7
  */
8
8
 
9
- import React, { useEffect, useRef } from 'react';
9
+ import React, { KeyboardEvent, useEffect, useRef } from 'react';
10
10
  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
14
  import SafeLink from '@ndla/safelink';
15
15
  import { SetFocusedFolderId, FolderChildFuncType } from './TreeStructure.types';
16
+ import { arrowNavigation } from './arrowNavigation';
16
17
 
17
18
  const OpenButton = styled.button<{ isOpen: boolean }>`
18
19
  background: transparent;
@@ -49,7 +50,10 @@ const WrapperForFolderChild = styled.div<{ marked: boolean }>`
49
50
  }
50
51
  `;
51
52
 
52
- const FolderName = styled.button<{ marked: boolean; noArrow?: boolean }>`
53
+ const FolderName = styled('button', { shouldForwardProp: (name) => !['marked', 'noArrow'].includes(name) })<{
54
+ marked: boolean;
55
+ noArrow?: boolean;
56
+ }>`
53
57
  line-height: 1;
54
58
  background: ${({ marked }) => (marked ? colors.brand.lighter : 'transparent')};
55
59
  color: ${colors.text.primary};
@@ -81,16 +85,19 @@ const FolderNameLink = FolderName.withComponent(SafeLink);
81
85
  interface Props {
82
86
  name: string;
83
87
  id: string;
84
- onToggleOpen: (id: string) => void;
88
+ level: number;
89
+ onCloseFolder: (id: string) => void;
90
+ onOpenFolder: (id: string) => void;
85
91
  onMarkFolder: (id: string) => void;
92
+ onSelectFolder?: (id: string) => void;
86
93
  isOpen: boolean;
87
94
  markedFolderId?: string;
88
95
  focusedFolderId?: string;
96
+ visibleFolders: string[];
89
97
  loading?: boolean;
90
98
  openOnFolderClick?: boolean;
91
99
  hideArrow?: boolean;
92
100
  setFocusedFolderId: SetFocusedFolderId;
93
- url?: string;
94
101
  icon?: React.ReactNode;
95
102
  noPaddingWhenArrowIsHidden?: boolean;
96
103
  folderChild?: FolderChildFuncType;
@@ -99,62 +106,61 @@ interface Props {
99
106
  const FolderItem = ({
100
107
  hideArrow,
101
108
  loading,
109
+ level,
102
110
  name,
103
111
  id,
104
- onToggleOpen,
112
+ visibleFolders,
113
+ onCloseFolder,
114
+ onOpenFolder,
105
115
  onMarkFolder,
116
+ onSelectFolder,
106
117
  isOpen,
107
118
  markedFolderId,
108
119
  focusedFolderId,
109
120
  openOnFolderClick,
110
121
  setFocusedFolderId,
111
122
  icon,
112
- url,
113
123
  noPaddingWhenArrowIsHidden,
114
124
  folderChild,
115
125
  }: Props) => {
116
- const folderNameLinkRef = useRef<HTMLAnchorElement | null>(null);
117
- const folderNameButtonRef = useRef<HTMLButtonElement | null>(null);
126
+ const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
127
+ const marked = markedFolderId === id;
128
+
129
+ const handleMarkFolder = () => {
130
+ onMarkFolder(id);
131
+ if (openOnFolderClick) {
132
+ if (isOpen) {
133
+ onCloseFolder(id);
134
+ } else {
135
+ onOpenFolder(id);
136
+ }
137
+ }
138
+ };
139
+
118
140
  useEffect(() => {
119
141
  if (focusedFolderId === id) {
120
- if (url && folderNameLinkRef.current) {
121
- folderNameLinkRef.current.focus();
122
- } else if (folderNameButtonRef.current) {
123
- folderNameButtonRef.current.focus();
142
+ if (ref.current) {
143
+ ref.current.focus();
124
144
  }
125
145
  }
126
- }, [focusedFolderId, folderNameLinkRef, folderNameButtonRef, url, id]);
127
- const marked = markedFolderId === id;
146
+ }, [focusedFolderId, ref, id]);
147
+
128
148
  return (
129
149
  <FolderItemWrapper>
130
150
  {!hideArrow && (
131
- <OpenButton tabIndex={-1} isOpen={isOpen} disabled={loading} onClick={() => onToggleOpen(id)}>
151
+ <OpenButton
152
+ tabIndex={-1}
153
+ isOpen={isOpen}
154
+ disabled={loading}
155
+ onClick={() => (isOpen ? onCloseFolder(id) : onOpenFolder(id))}>
132
156
  <ArrowDropDown />
133
157
  </OpenButton>
134
158
  )}
135
- {url ? (
136
- <FolderNameLink
137
- ref={folderNameLinkRef}
138
- noArrow={hideArrow}
139
- to={loading ? '' : url}
140
- tabIndex={marked ? 0 : -1}
141
- marked={marked}
142
- onFocus={() => {
143
- setFocusedFolderId(id);
144
- }}
145
- onClick={() => {
146
- onMarkFolder(id);
147
- if (openOnFolderClick) {
148
- onToggleOpen(id);
149
- }
150
- }}>
151
- {icon || <FolderOutlined />}
152
- {name}
153
- </FolderNameLink>
154
- ) : (
159
+ {onSelectFolder ? (
155
160
  <>
156
161
  <FolderName
157
- ref={folderNameButtonRef}
162
+ ref={ref}
163
+ onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)}
158
164
  noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
159
165
  tabIndex={marked ? 0 : -1}
160
166
  marked={marked}
@@ -163,10 +169,8 @@ const FolderItem = ({
163
169
  setFocusedFolderId(id);
164
170
  }}
165
171
  onClick={() => {
166
- onMarkFolder(id);
167
- if (openOnFolderClick) {
168
- onToggleOpen(id);
169
- }
172
+ handleMarkFolder();
173
+ onSelectFolder(id);
170
174
  }}>
171
175
  {icon || <FolderOutlined />}
172
176
  {name}
@@ -177,6 +181,25 @@ const FolderItem = ({
177
181
  </WrapperForFolderChild>
178
182
  )}
179
183
  </>
184
+ ) : (
185
+ <FolderNameLink
186
+ ref={ref}
187
+ onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
188
+ arrowNavigation(e, id, visibleFolders, setFocusedFolderId, onOpenFolder, onCloseFolder)
189
+ }
190
+ noArrow={hideArrow}
191
+ to={loading ? '' : `/minndla/${level > 1 ? 'folders/' : ''}${id}`}
192
+ tabIndex={marked || level === 1 ? 0 : -1}
193
+ marked={marked}
194
+ onFocus={() => {
195
+ setFocusedFolderId(id);
196
+ }}
197
+ onClick={() => {
198
+ handleMarkFolder();
199
+ }}>
200
+ {icon || <FolderOutlined />}
201
+ {name}
202
+ </FolderNameLink>
180
203
  )}
181
204
  </FolderItemWrapper>
182
205
  );
@@ -32,65 +32,73 @@ const StyledLI = styled.li`
32
32
 
33
33
  const FolderItems = ({
34
34
  loading,
35
- data,
36
- idPaths,
35
+ folders,
36
+ level,
37
37
  editable,
38
- onToggleOpen,
38
+ onSelectFolder,
39
+ onCloseFolder,
40
+ onOpenFolder,
39
41
  onCreateNewFolder,
40
42
  onCancelNewFolder,
41
43
  onSaveNewFolder,
42
- newFolder,
44
+ newFolderParentId,
45
+ visibleFolders,
43
46
  openFolders,
44
47
  markedFolderId,
45
48
  onMarkFolder,
46
49
  openOnFolderClick,
47
50
  focusedFolderId,
48
51
  setFocusedFolderId,
49
- firstLevel,
50
52
  folderChild,
51
53
  maximumLevelsOfFoldersAllowed,
52
54
  }: FolderItemsProps) => (
53
- <StyledUL role="group" firstLevel={firstLevel}>
54
- {data.map(({ name, data: dataChildren, id, url, icon }, _index) => {
55
- const newIdPaths = [...idPaths, _index];
55
+ <StyledUL role="group" firstLevel={level === 1}>
56
+ {folders.map(({ name, subfolders, id, icon }, _index) => {
56
57
  const isOpen = openFolders?.includes(id);
57
58
  return (
58
59
  <StyledLI key={id} role="treeitem">
59
60
  <div>
60
61
  <FolderItem
62
+ level={level}
61
63
  icon={icon}
62
- url={url}
64
+ onSelectFolder={onSelectFolder}
63
65
  openOnFolderClick={openOnFolderClick}
64
66
  loading={loading}
65
67
  isOpen={isOpen}
66
68
  id={id}
69
+ visibleFolders={visibleFolders}
67
70
  name={name}
68
71
  markedFolderId={markedFolderId}
69
72
  focusedFolderId={focusedFolderId}
70
- onToggleOpen={onToggleOpen}
71
73
  onMarkFolder={onMarkFolder}
72
- hideArrow={dataChildren?.length === 0 || newIdPaths.length >= maximumLevelsOfFoldersAllowed}
73
- noPaddingWhenArrowIsHidden={editable && firstLevel && dataChildren?.length === 0}
74
+ onCloseFolder={onCloseFolder}
75
+ onOpenFolder={onOpenFolder}
76
+ hideArrow={subfolders?.length === 0 || level > maximumLevelsOfFoldersAllowed}
77
+ noPaddingWhenArrowIsHidden={editable && level === 1 && subfolders?.length === 0}
74
78
  setFocusedFolderId={setFocusedFolderId}
75
79
  folderChild={folderChild}
76
80
  />
77
81
  </div>
78
- {newFolder?.parentId === id && (
82
+ {newFolderParentId === id && (
79
83
  <FolderNameInput
84
+ parentId={newFolderParentId}
80
85
  loading={loading}
81
86
  onCancelNewFolder={onCancelNewFolder}
82
87
  onSaveNewFolder={onSaveNewFolder}
83
88
  />
84
89
  )}
85
- {dataChildren && isOpen && (
90
+ {subfolders && isOpen && (
86
91
  <FolderItems
92
+ onSelectFolder={onSelectFolder}
87
93
  loading={loading}
88
- newFolder={newFolder}
94
+ newFolderParentId={newFolderParentId}
95
+ visibleFolders={visibleFolders}
89
96
  openFolders={openFolders}
90
- idPaths={newIdPaths}
97
+ level={level + 1}
91
98
  editable={editable}
92
- data={dataChildren}
93
- onToggleOpen={onToggleOpen}
99
+ folders={subfolders}
100
+ onCloseFolder={onCloseFolder}
101
+ onOpenFolder={onOpenFolder}
94
102
  onCreateNewFolder={onCreateNewFolder}
95
103
  onSaveNewFolder={onSaveNewFolder}
96
104
  onCancelNewFolder={onCancelNewFolder}
@@ -99,7 +107,6 @@ const FolderItems = ({
99
107
  openOnFolderClick={openOnFolderClick}
100
108
  focusedFolderId={focusedFolderId}
101
109
  setFocusedFolderId={setFocusedFolderId}
102
- firstLevel={false}
103
110
  folderChild={folderChild}
104
111
  maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
105
112
  />
@@ -53,14 +53,15 @@ const StyledInput = styled.input`
53
53
  `;
54
54
 
55
55
  interface FolderNameInputProps {
56
- onSaveNewFolder: (value: string) => void;
56
+ onSaveNewFolder: (name: string, parentId: string) => void;
57
+ parentId: string;
57
58
  onCancelNewFolder: () => void;
58
59
  loading?: boolean;
59
60
  }
60
61
 
61
- const FolderNameInput = ({ onSaveNewFolder, onCancelNewFolder, loading }: FolderNameInputProps) => {
62
+ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading }: FolderNameInputProps) => {
62
63
  const { t } = useTranslation();
63
- const [value, setValue] = useState<string>(t('treeStructure.newFolder.defaultName'));
64
+ const [name, setName] = useState<string>(t('treeStructure.newFolder.defaultName'));
64
65
  const inputRef = useRef<HTMLInputElement>(null);
65
66
 
66
67
  useEffect(() => {
@@ -82,22 +83,19 @@ const FolderNameInput = ({ onSaveNewFolder, onCancelNewFolder, loading }: Folder
82
83
  autoFocus
83
84
  placeholder={t('treeStructure.newFolder.placeholder')}
84
85
  disabled={loading}
85
- value={value}
86
+ value={name}
86
87
  onBlur={() => onCancelNewFolder()}
87
88
  onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
88
89
  if (e.key === 'Escape') {
89
90
  onCancelNewFolder();
90
- return;
91
- }
92
- if (e.key === 'Enter' || e.key === 'Tab') {
93
- onSaveNewFolder(value);
91
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
94
92
  e.preventDefault();
93
+ onSaveNewFolder(name, parentId);
95
94
  }
96
- return;
97
95
  }}
98
96
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
99
- const target = e.target as HTMLInputElement;
100
- setValue(target.value);
97
+ const target = e.target;
98
+ setName(target.value);
101
99
  }}
102
100
  />
103
101
  {loading && <Spinner size="small" />}
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import React, { useEffect, useState, useRef, useMemo } from 'react';
10
- import { uuid } from '@ndla/util';
11
10
  import { AddButton } from '@ndla/button';
12
11
  import Tooltip from '@ndla/tooltip';
13
12
  import { useTranslation } from 'react-i18next';
@@ -16,9 +15,8 @@ import { spacing, fonts } from '@ndla/core';
16
15
  import { uniq } from 'lodash';
17
16
  import TreeStructureStyledWrapper from './TreeStructureWrapper';
18
17
  import FolderItems from './FolderItems';
19
- import { getIdPathsOfFolder, getPathOfFolder, getFolderName } from './helperFunctions';
20
- import keyboardNavigation, { KEYBOARD_KEYS_OF_INTEREST } from './keyboardNavigation/keyboardNavigation';
21
- import { NewFolderProps, TreeStructureProps } from './TreeStructure.types';
18
+ import { getPathOfFolder, getFolderName, flattenFolders } from './helperFunctions';
19
+ import { TreeStructureProps } from './TreeStructure.types';
22
20
 
23
21
  export const MAX_LEVEL_FOR_FOLDERS = 4;
24
22
 
@@ -32,26 +30,32 @@ const AddFolderWrapper = styled.div`
32
30
  `;
33
31
 
34
32
  const TreeStructure = ({
35
- data,
33
+ folders,
36
34
  label,
37
35
  editable,
38
36
  loading,
39
37
  onNewFolder,
38
+ onSelectFolder,
40
39
  openOnFolderClick,
41
40
  framed,
42
41
  folderIdMarkedByDefault,
43
42
  defaultOpenFolders,
44
43
  folderChild,
45
- maximumLevelsOfFoldersAllowed,
44
+ maximumLevelsOfFoldersAllowed = MAX_LEVEL_FOR_FOLDERS,
46
45
  }: TreeStructureProps) => {
47
46
  const { t } = useTranslation();
48
- const [newFolder, setNewFolder] = useState<NewFolderProps | undefined>();
47
+ const [newFolderParentId, setNewFolderParentId] = useState<string | undefined>();
49
48
  const [openFolders, setOpenFolders] = useState<string[]>(defaultOpenFolders || []);
50
49
  const [focusedFolderId, setFocusedFolderId] = useState<string | undefined>();
51
- const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || data[0]?.id);
50
+ const [markedFolderId, setMarkedFolderId] = useState<string | undefined>(folderIdMarkedByDefault || folders[0]?.id);
52
51
  const treestructureRef = useRef<HTMLDivElement>(null);
53
52
  const wrapperRef = useRef<HTMLDivElement>(null);
54
- const rootLevelId = useMemo(() => uuid(), []); // TODO: use useId hook when we update to React 18
53
+ const rootLevelId = 'treestructure-root';
54
+
55
+ const visibleFolders = useMemo(
56
+ () => flattenFolders(folders, openFolders).map((folder) => folder.id),
57
+ [folders, openFolders],
58
+ );
55
59
 
56
60
  useEffect(() => {
57
61
  if (defaultOpenFolders) {
@@ -63,54 +67,51 @@ const TreeStructure = ({
63
67
 
64
68
  useEffect(() => {
65
69
  if (!loading) {
66
- setNewFolder(undefined);
70
+ setNewFolderParentId(undefined);
67
71
  }
68
72
  }, [loading]);
69
73
 
70
- const onToggleOpen = (id: string) => {
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) {
74
+ const onCloseFolder = (id: string) => {
75
+ // Did we just closed a folder with a marked folder inside it?
76
+ // If so, we need to mark the folder we just closed.
77
+ if (markedFolderId) {
78
+ const closingFolderPath = getPathOfFolder(folders, id);
79
+ const markedFolderPath = getPathOfFolder(folders, markedFolderId);
80
+ const markedFolderIsSubPath = closingFolderPath.every(
81
+ (folderId, _index) => markedFolderPath[_index] === folderId,
82
+ );
83
+ if (markedFolderIsSubPath) {
84
+ if (onSelectFolder) {
81
85
  setMarkedFolderId(closingFolderPath[closingFolderPath.length - 1]);
86
+ onSelectFolder(closingFolderPath[closingFolderPath.length - 1]);
87
+ } else {
88
+ setFocusedFolderId(closingFolderPath[closingFolderPath.length - 1]);
82
89
  }
83
90
  }
84
- setOpenFolders(openFolders.filter((folder) => folder !== id));
85
- } else {
86
- setOpenFolders(uniq([...openFolders, id]));
87
91
  }
92
+ setOpenFolders(openFolders.filter((folder) => folder !== id));
88
93
  };
89
94
 
90
- const onCreateNewFolder = (props: { idPaths: number[]; parentId?: string }) => {
91
- setNewFolder(props);
95
+ const onOpenFolder = (id: string) => {
96
+ setOpenFolders(uniq(openFolders.concat(id)));
92
97
  };
93
98
 
94
- const onSaveNewFolder = (value: string) => {
95
- if (newFolder) {
96
- // We would like to create a new folder with the name of value.
97
- // Its location in structure is based on newFolder object
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
- if (newFolder.parentId) {
105
- setOpenFolders(uniq([...openFolders, newFolder.parentId]));
106
- }
107
- }
108
- });
109
- }
99
+ const onCreateNewFolder = (parentId: string) => {
100
+ setNewFolderParentId(parentId);
101
+ };
102
+
103
+ const onSaveNewFolder = (name: string, parentId: string) => {
104
+ onNewFolder(name, parentId).then((newFolderId) => {
105
+ if (newFolderId) {
106
+ setMarkedFolderId(newFolderId);
107
+ setFocusedFolderId(newFolderId);
108
+ setOpenFolders(uniq(openFolders.concat(parentId)));
109
+ }
110
+ });
110
111
  };
111
112
 
112
113
  const onCancelNewFolder = () => {
113
- setNewFolder(undefined);
114
+ setNewFolderParentId(undefined);
114
115
  };
115
116
 
116
117
  const onMarkFolder = (id: string) => {
@@ -118,35 +119,25 @@ const TreeStructure = ({
118
119
  setFocusedFolderId(id);
119
120
  };
120
121
 
121
- const paths = getPathOfFolder(data, markedFolderId || '');
122
+ const paths = getPathOfFolder(folders, markedFolderId || '');
122
123
  const canAddFolder = editable && paths.length < (maximumLevelsOfFoldersAllowed || 1);
123
124
 
124
125
  return (
125
- <div
126
- ref={treestructureRef}
127
- onKeyDown={(e) => {
128
- if (wrapperRef.current?.contains(document.activeElement) && KEYBOARD_KEYS_OF_INTEREST.includes(e.key)) {
129
- keyboardNavigation({
130
- e,
131
- data,
132
- setFocusedFolderId,
133
- focusedFolderId,
134
- onToggleOpen,
135
- openFolders,
136
- });
137
- }
138
- }}>
126
+ <div ref={treestructureRef}>
139
127
  {label && <StyledLabel htmlFor={rootLevelId}>{label}</StyledLabel>}
140
128
  <TreeStructureStyledWrapper ref={wrapperRef} id={rootLevelId} aria-label="Menu tree" role="tree" framed={framed}>
141
129
  <FolderItems
142
- idPaths={[]}
143
- data={data}
130
+ onSelectFolder={onSelectFolder}
131
+ level={1}
132
+ folders={folders}
144
133
  editable={editable}
145
- onToggleOpen={onToggleOpen}
146
- newFolder={newFolder}
134
+ onOpenFolder={onOpenFolder}
135
+ onCloseFolder={onCloseFolder}
136
+ newFolderParentId={newFolderParentId}
147
137
  onCreateNewFolder={onCreateNewFolder}
148
138
  onCancelNewFolder={onCancelNewFolder}
149
139
  onSaveNewFolder={onSaveNewFolder}
140
+ visibleFolders={visibleFolders}
150
141
  openFolders={openFolders}
151
142
  markedFolderId={markedFolderId}
152
143
  onMarkFolder={onMarkFolder}
@@ -154,7 +145,6 @@ const TreeStructure = ({
154
145
  loading={loading}
155
146
  focusedFolderId={focusedFolderId}
156
147
  setFocusedFolderId={setFocusedFolderId}
157
- firstLevel
158
148
  folderChild={folderChild}
159
149
  maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
160
150
  />
@@ -165,16 +155,15 @@ const TreeStructure = ({
165
155
  tooltip={
166
156
  canAddFolder
167
157
  ? t('myNdla.newFolderUnder', {
168
- folderName: getFolderName(data, markedFolderId),
158
+ folderName: getFolderName(folders, markedFolderId),
169
159
  })
170
- : t('myNdla.maxFoldersAlreadyAdded')
160
+ : t('treeStructure.maxFoldersAlreadyAdded')
171
161
  }>
172
162
  <AddButton
173
163
  disabled={!canAddFolder}
174
164
  aria-label={t('myNdla.newFolder')}
175
165
  onClick={() => {
176
- const idPaths = getIdPathsOfFolder(data, markedFolderId || '');
177
- setNewFolder({ idPaths, parentId: paths[paths.length - 1] });
166
+ setNewFolderParentId(markedFolderId);
178
167
  }}>
179
168
  {t('myNdla.newFolder')}
180
169
  </AddButton>
@@ -185,8 +174,4 @@ const TreeStructure = ({
185
174
  );
186
175
  };
187
176
 
188
- TreeStructure.defaultProps = {
189
- maximumLevelsOfFoldersAllowed: MAX_LEVEL_FOR_FOLDERS,
190
- };
191
-
192
177
  export default TreeStructure;
@@ -11,35 +11,29 @@ import React, { ReactNode } from 'react';
11
11
  export interface FolderStructureProps {
12
12
  id: string;
13
13
  name: string;
14
- isOpen?: boolean;
15
- data?: FolderStructureProps[];
14
+ subfolders: FolderStructureProps[];
16
15
  isFavorite?: boolean;
17
16
  status?: string;
18
17
  openAsDefault?: boolean;
19
- url?: string;
20
18
  icon?: ReactNode;
21
19
  }
22
20
 
23
- export interface NewFolderProps {
24
- parentId?: string;
25
- idPaths: number[];
26
- }
27
-
28
21
  interface CommonFolderProps {
29
- data: FolderStructureProps[];
30
22
  editable?: boolean;
31
23
  loading?: boolean;
32
24
  openOnFolderClick?: boolean;
25
+ onSelectFolder?: (id: string) => void;
33
26
  }
34
27
 
35
28
  export interface TreeStructureProps extends CommonFolderProps {
29
+ folders: FolderStructureProps[];
36
30
  framed?: boolean;
37
31
  label?: string;
38
32
  folderIdMarkedByDefault?: string;
39
- onNewFolder: (props: { value: string; parentId?: string; idPaths: number[] }) => Promise<string>;
33
+ onNewFolder: (name: string, parentId: string) => Promise<string>;
40
34
  defaultOpenFolders?: string[];
41
35
  folderChild?: FolderChildFuncType;
42
- maximumLevelsOfFoldersAllowed: number;
36
+ maximumLevelsOfFoldersAllowed?: number;
43
37
  }
44
38
 
45
39
  export type onCreateNewFolderProp = ({
@@ -56,18 +50,20 @@ export type SetFocusedFolderId = React.Dispatch<React.SetStateAction<string | un
56
50
  export type FolderChildFuncType = (id: string, tabIndex: number) => ReactNode;
57
51
 
58
52
  export interface FolderItemsProps extends CommonFolderProps {
59
- onToggleOpen: (id: string) => void;
60
- onSaveNewFolder: (value: string) => void;
53
+ folders: FolderStructureProps[];
54
+ onCloseFolder: (id: string) => void;
55
+ onOpenFolder: (id: string) => void;
56
+ onSaveNewFolder: (name: string, parentId: string) => void;
61
57
  onCancelNewFolder: () => void;
62
- onCreateNewFolder: onCreateNewFolderProp;
63
- newFolder: NewFolderProps | undefined;
58
+ onCreateNewFolder: (parentId: string) => void;
59
+ newFolderParentId: string | undefined;
60
+ visibleFolders: string[];
64
61
  openFolders: string[];
65
62
  markedFolderId?: string;
66
63
  onMarkFolder: (id: string) => void;
67
- idPaths: number[];
64
+ level: number;
68
65
  focusedFolderId: string | undefined;
69
66
  setFocusedFolderId: SetFocusedFolderId;
70
- firstLevel: boolean;
71
67
  keyNavigationFocusIsCreateFolderButton?: boolean;
72
68
  icon?: ReactNode;
73
69
  folderChild?: FolderChildFuncType;
@@ -0,0 +1,53 @@
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 { KeyboardEvent } from 'react';
10
+
11
+ const navigateVertical = (
12
+ visibleFolders: string[],
13
+ folderId: string,
14
+ setFocusedFolderId: (id: string) => void,
15
+ direction: 1 | -1,
16
+ ) => {
17
+ const currentIndex = visibleFolders.findIndex((id) => id === folderId);
18
+ const target = visibleFolders[currentIndex + direction];
19
+ if (target !== undefined) {
20
+ setFocusedFolderId(target);
21
+ }
22
+ };
23
+
24
+ const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
25
+
26
+ export const arrowNavigation = (
27
+ e: KeyboardEvent<HTMLElement>,
28
+ id: string,
29
+ visibleFolders: string[],
30
+ setFocusedFolderId: (id: string) => void,
31
+ onOpen: (id: string) => void,
32
+ onClose: (id: string) => void,
33
+ ) => {
34
+ if (!arrowKeys.includes(e.key)) {
35
+ return;
36
+ }
37
+
38
+ e.preventDefault();
39
+ e.stopPropagation();
40
+
41
+ switch (e.key) {
42
+ case 'ArrowUp':
43
+ return navigateVertical(visibleFolders, id, setFocusedFolderId, -1);
44
+ case 'ArrowDown':
45
+ return navigateVertical(visibleFolders, id, setFocusedFolderId, 1);
46
+ case 'ArrowLeft':
47
+ return onClose(id);
48
+ case 'ArrowRight':
49
+ return onOpen(id);
50
+ default:
51
+ return;
52
+ }
53
+ };