@ndla/ui 27.1.1 → 27.1.2

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 (49) hide show
  1. package/es/MyNdla/Resource/FolderInput.js +65 -77
  2. package/es/Resource/resourceComponents.js +13 -13
  3. package/es/TreeStructure/AddFolderButton.js +87 -0
  4. package/es/TreeStructure/ComboboxButton.js +13 -64
  5. package/es/TreeStructure/FolderItem.js +12 -12
  6. package/es/TreeStructure/FolderItems.js +12 -14
  7. package/es/TreeStructure/TreeStructure.js +55 -41
  8. package/es/locale/messages-en.js +1 -1
  9. package/es/locale/messages-nb.js +1 -1
  10. package/es/locale/messages-nn.js +1 -1
  11. package/es/locale/messages-se.js +1 -1
  12. package/es/locale/messages-sma.js +1 -1
  13. package/lib/MyNdla/Resource/FolderInput.d.ts +7 -6
  14. package/lib/MyNdla/Resource/FolderInput.js +65 -73
  15. package/lib/Resource/resourceComponents.js +13 -13
  16. package/lib/TreeStructure/AddFolderButton.d.ts +16 -0
  17. package/lib/TreeStructure/AddFolderButton.js +100 -0
  18. package/lib/TreeStructure/ComboboxButton.d.ts +0 -4
  19. package/lib/TreeStructure/ComboboxButton.js +19 -64
  20. package/lib/TreeStructure/FolderItem.js +12 -12
  21. package/lib/TreeStructure/FolderItems.d.ts +4 -3
  22. package/lib/TreeStructure/FolderItems.js +12 -15
  23. package/lib/TreeStructure/TreeStructure.d.ts +3 -4
  24. package/lib/TreeStructure/TreeStructure.js +55 -41
  25. package/lib/TreeStructure/types.d.ts +7 -1
  26. package/lib/locale/messages-en.js +1 -1
  27. package/lib/locale/messages-nb.js +1 -1
  28. package/lib/locale/messages-nn.js +1 -1
  29. package/lib/locale/messages-se.js +1 -1
  30. package/lib/locale/messages-sma.js +1 -1
  31. package/package.json +6 -6
  32. package/src/.DS_Store +0 -0
  33. package/src/MyNdla/Resource/FolderInput.tsx +69 -73
  34. package/src/Resource/resourceComponents.tsx +4 -6
  35. package/src/TreeStructure/AddFolderButton.tsx +79 -0
  36. package/src/TreeStructure/ComboboxButton.tsx +8 -53
  37. package/src/TreeStructure/FolderItem.tsx +5 -4
  38. package/src/TreeStructure/FolderItems.tsx +8 -15
  39. package/src/TreeStructure/TreeStructure.tsx +42 -33
  40. package/src/TreeStructure/types.ts +13 -1
  41. package/src/locale/messages-en.ts +1 -1
  42. package/src/locale/messages-nb.ts +1 -1
  43. package/src/locale/messages-nn.ts +1 -1
  44. package/src/locale/messages-se.ts +1 -1
  45. package/src/locale/messages-sma.ts +1 -1
  46. package/es/TreeStructure/FolderNameInput.js +0 -143
  47. package/lib/TreeStructure/FolderNameInput.d.ts +0 -19
  48. package/lib/TreeStructure/FolderNameInput.js +0 -159
  49. package/src/TreeStructure/FolderNameInput.tsx +0 -167
@@ -7,110 +7,106 @@
7
7
  */
8
8
 
9
9
  import styled from '@emotion/styled';
10
- import { IconButton } from '@ndla/button';
11
- import { FolderOutlined } from '@ndla/icons/contentType';
10
+ import { IconButton, IconButtonV2 } from '@ndla/button';
12
11
  import { Cross } from '@ndla/icons/action';
13
- import React, { ChangeEvent, KeyboardEvent, useRef, useState } from 'react';
12
+ import React, { ComponentProps, forwardRef, useEffect } from 'react';
13
+ import { isMobile } from 'react-device-detect';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  import { colors, spacing } from '@ndla/core';
16
- import { Input } from '@ndla/forms';
17
- import { css } from '@emotion/core';
16
+ import { InputV2 } from '@ndla/forms';
18
17
  import { Done } from '@ndla/icons/editor';
18
+ import { Spinner } from '@ndla/icons';
19
+ import { useForwardedRef } from '@ndla/util';
20
+
21
+ interface Props extends ComponentProps<typeof InputV2> {
22
+ loading?: boolean;
23
+ onClose?: () => void;
24
+ onSave: () => void;
25
+ }
19
26
 
20
27
  // Source: https://kovart.github.io/dashed-border-generator/
21
- const borderStyle = `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='${encodeURIComponent(
22
- colors.brand.tertiary,
23
- )}' stroke-width='2' stroke-dasharray='8%2c8' stroke-dashoffset='4' stroke-linecap='square'/%3e%3c/svg%3e")`;
28
+ const borderStyle = (error?: boolean) =>
29
+ `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='${encodeURIComponent(
30
+ error ? colors.support.red : colors.brand.tertiary,
31
+ )}' stroke-width='2' stroke-dasharray='8%2c8' stroke-dashoffset='4' stroke-linecap='square'/%3e%3c/svg%3e")`;
24
32
 
25
- const StyledFolderIcon = styled.span`
26
- display: flex;
27
- padding: ${spacing.small};
28
- svg {
29
- color: ${colors.brand.primary};
30
- height: 20px;
31
- width: 20px;
32
- }
33
- `;
33
+ interface StyledInputProps {
34
+ error?: string;
35
+ }
34
36
 
35
- const inputWrapperStyle = css`
36
- padding: ${spacing.small};
37
- background: none;
38
- background-image: ${borderStyle};
37
+ const StyledInput = styled(InputV2)<StyledInputProps>`
38
+ background-color: white;
39
+ background-image: ${({ error }) => borderStyle(!!error)};
39
40
  border: none;
40
- `;
41
-
42
- const StyledInput = styled(Input)`
43
- && {
44
- flex-grow: 1;
45
- flex-basis: 0;
46
- min-width: 0;
41
+ border-radius: 0;
42
+ flex-wrap: nowrap;
43
+ input {
47
44
  line-height: 1.75em;
48
45
  color: ${colors.brand.primary};
46
+ caret-color: ${colors.brand.tertiary};
49
47
  ::selection {
50
48
  background: ${colors.brand.lighter};
51
49
  }
50
+ ::placeholder {
51
+ color: ${colors.brand.tertiary};
52
+ }
52
53
  }
53
54
  `;
54
55
 
55
56
  const Row = styled.div`
56
57
  display: flex;
57
- gap: ${spacing.xsmall};
58
- padding-right: ${spacing.small};
58
+ align-items: center;
59
+ gap: ${spacing.xxsmall};
60
+ padding-right: ${spacing.xsmall};
59
61
  `;
60
62
 
61
- interface Props {
62
- onAddFolder: (name: string) => void;
63
- onClose: () => void;
64
- autoSelect?: boolean;
65
- }
66
-
67
- const FolderInput = ({ onAddFolder, onClose, autoSelect }: Props) => {
63
+ const FolderInput = forwardRef<HTMLInputElement, Props>(({ loading, error, onClose, onSave, ...rest }, ref) => {
68
64
  const { t } = useTranslation();
69
- const newFolderText = t('treeStructure.newFolder.defaultName');
70
- const [input, setInput] = useState<string>(newFolderText);
71
- const inputRef = useRef<HTMLInputElement>(null);
72
-
73
- const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
74
- setInput(e.target.value);
75
- };
65
+ const inputRef = useForwardedRef(ref);
76
66
 
77
- const onKeydown = (e: KeyboardEvent<HTMLInputElement>) => {
78
- if (e.key === 'Enter' && input.trim()) {
79
- e.preventDefault();
80
- onAddFolder(input);
81
- } else if (e.key === 'Escape') {
82
- e.preventDefault();
83
- onClose();
67
+ useEffect(() => {
68
+ if (isMobile) {
69
+ inputRef.current?.scrollIntoView({ behavior: 'smooth' });
84
70
  }
85
- };
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, []);
86
73
 
87
74
  return (
88
75
  <StyledInput
89
- autoSelect={autoSelect}
90
- customCss={inputWrapperStyle}
91
- warningText={!input.trim() ? t('myNdla.folder.missingName') : undefined}
92
- ref={inputRef}
93
- value={input}
94
- onChange={handleInputChange}
95
- onKeyDown={onKeydown}
96
- aria-label={newFolderText}
97
- iconLeft={
98
- <StyledFolderIcon>
99
- <FolderOutlined />
100
- </StyledFolderIcon>
101
- }
102
- iconRight={
76
+ white
77
+ error={error}
78
+ aria-disabled={loading ? true : undefined}
79
+ aria-describedby={'folder-input-spinner'}
80
+ ref={ref}
81
+ after={
103
82
  <Row>
104
- <IconButton aria-label={t('close')} size="small" ghostPill onClick={onClose}>
105
- <Cross />
106
- </IconButton>
107
- <IconButton aria-label={t('save')} size="small" ghostPill onClick={() => onAddFolder(input)}>
108
- <Done />
109
- </IconButton>
83
+ {!loading && (
84
+ <>
85
+ {!error && (
86
+ <IconButtonV2
87
+ variant={'ghost'}
88
+ colorTheme="light"
89
+ tabIndex={0}
90
+ aria-label={t('save')}
91
+ title={t('save')}
92
+ size="small"
93
+ onClick={onSave}>
94
+ <Done />
95
+ </IconButtonV2>
96
+ )}
97
+ <IconButton aria-label={t('close')} title={t('close')} size="small" ghostPill onClick={onClose}>
98
+ <Cross />
99
+ </IconButton>
100
+ </>
101
+ )}
102
+ <div aria-live="assertive">
103
+ {loading && <Spinner size="normal" margin={spacing.small} id="folder-spinner" aria-label={t('loading')} />}
104
+ </div>
110
105
  </Row>
111
106
  }
107
+ {...rest}
112
108
  />
113
109
  );
114
- };
110
+ });
115
111
 
116
112
  export default FolderInput;
@@ -185,12 +185,10 @@ export const TopicList = ({ topics }: TopicListProps) => {
185
185
  return (
186
186
  <StyledTopicList aria-label={t('navigation.topics')}>
187
187
  {topics.map((topic, i) => (
188
- <>
189
- <StyledTopicListElement key={topic}>
190
- {topic}
191
- {i !== topics.length - 1 && <StyledTopicDivider aria-hidden="true">•</StyledTopicDivider>}
192
- </StyledTopicListElement>
193
- </>
188
+ <StyledTopicListElement key={topic}>
189
+ {topic}
190
+ {i !== topics.length - 1 && <StyledTopicDivider aria-hidden="true">•</StyledTopicDivider>}
191
+ </StyledTopicListElement>
194
192
  ))}
195
193
  </StyledTopicList>
196
194
  );
@@ -0,0 +1,79 @@
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, { useRef } from 'react';
10
+ import { useTranslation } from 'react-i18next';
11
+ import Tooltip from '@ndla/tooltip';
12
+ import styled from '@emotion/styled';
13
+ import { ButtonV2 as Button } from '@ndla/button';
14
+ import { Plus } from '@ndla/icons/action';
15
+ import { FolderType } from './types';
16
+
17
+ interface AddFolderButtonProps {
18
+ canAddFolder: boolean;
19
+ focusedFolder?: FolderType;
20
+ setNewFolderParentId: (id?: string) => void;
21
+ }
22
+
23
+ const StyledAddFolderButton = styled(Button)`
24
+ &,
25
+ &:disabled {
26
+ border-color: transparent;
27
+ }
28
+ `;
29
+
30
+ const StyledPlus = styled(Plus)`
31
+ height: 24px;
32
+ width: 24px;
33
+ `;
34
+
35
+ const AddFolderButton = ({ canAddFolder, setNewFolderParentId, focusedFolder }: AddFolderButtonProps) => {
36
+ const { t } = useTranslation();
37
+ const ref = useRef<HTMLButtonElement>(null);
38
+ return (
39
+ <Tooltip
40
+ tooltip={
41
+ canAddFolder
42
+ ? t('myNdla.newFolderUnder', {
43
+ folderName: focusedFolder?.name,
44
+ })
45
+ : t('treeStructure.maxFoldersAlreadyAdded')
46
+ }>
47
+ <StyledAddFolderButton
48
+ ref={ref}
49
+ variant="outline"
50
+ shape="pill"
51
+ disabled={!canAddFolder}
52
+ aria-label={
53
+ canAddFolder
54
+ ? t('myNdla.newFolderUnder', {
55
+ folderName: focusedFolder?.name,
56
+ })
57
+ : t('treeStructure.maxFoldersAlreadyAdded')
58
+ }
59
+ onMouseDown={(e) => {
60
+ e.preventDefault();
61
+ e.stopPropagation();
62
+ ref.current?.focus();
63
+ }}
64
+ onMouseUp={(e) => {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ ref.current?.focus();
68
+ }}
69
+ onClick={(e) => {
70
+ e.currentTarget.focus();
71
+ setNewFolderParentId(focusedFolder?.id);
72
+ }}>
73
+ <StyledPlus /> {t('myNdla.newFolder')}
74
+ </StyledAddFolderButton>
75
+ </Tooltip>
76
+ );
77
+ };
78
+
79
+ export default AddFolderButton;
@@ -7,13 +7,9 @@
7
7
  */
8
8
 
9
9
  import React, { KeyboardEvent } from 'react';
10
- import { useTranslation } from 'react-i18next';
11
10
  import styled from '@emotion/styled';
12
11
  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';
12
+ import { breakpoints, colors, mq, spacing } from '@ndla/core';
17
13
  import { ChevronUp, ChevronDown } from '@ndla/icons/common';
18
14
  import { forwardRef } from 'react';
19
15
  import { ButtonV2 as Button, IconButtonV2 as IconButton } from '@ndla/button';
@@ -27,8 +23,8 @@ interface StyledRowProps {
27
23
 
28
24
  const StyledRow = styled.div<StyledRowProps>`
29
25
  display: flex;
30
- justify-content: space-between;
31
26
  padding: ${spacing.xxsmall};
27
+ align-items: center;
32
28
  border-bottom: ${({ isOpen }) => isOpen && `1px solid ${colors.brand.tertiary}`};
33
29
  `;
34
30
  const StyledSelectedFolder = styled(Button)`
@@ -44,20 +40,13 @@ const StyledSelectedFolder = styled(Button)`
44
40
  :focus-visible {
45
41
  outline: none;
46
42
  }
47
- `;
48
-
49
- const StyledAddFolderButton = styled(Button)`
50
- &,
51
- &:disabled {
52
- border-color: transparent;
43
+ ${mq.range({ until: breakpoints.tablet })} {
44
+ min-height: 4rem;
45
+ max-height: 4rem;
46
+ text-align: start;
53
47
  }
54
48
  `;
55
49
 
56
- const StyledPlus = styled(Plus)`
57
- height: 24px;
58
- width: 24px;
59
- `;
60
-
61
50
  interface Props {
62
51
  showTree: boolean;
63
52
  type: TreeStructureType;
@@ -70,9 +59,6 @@ interface Props {
70
59
  onOpenFolder: (id: string) => void;
71
60
  onCloseFolder: (id: string) => void;
72
61
  setFocusedFolder: (folder?: FolderType) => void;
73
- onNewFolder?: (name: string, parentId: string) => Promise<IFolder>;
74
- maxLevel: number;
75
- setNewFolderParentId: (id?: string) => void;
76
62
  }
77
63
 
78
64
  const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
@@ -89,13 +75,9 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
89
75
  setFocusedFolder,
90
76
  onOpenFolder,
91
77
  onCloseFolder,
92
- onNewFolder,
93
- maxLevel,
94
- setNewFolderParentId,
95
78
  },
96
79
  ref,
97
80
  ) => {
98
- const { t } = useTranslation();
99
81
  const innerRef = useForwardedRef(ref);
100
82
 
101
83
  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
@@ -118,7 +100,6 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
118
100
  }
119
101
  };
120
102
 
121
- const canAddFolder = selectedFolder && selectedFolder?.breadcrumbs.length < (maxLevel || 1);
122
103
  return (
123
104
  <StyledRow isOpen={showTree}>
124
105
  <StyledSelectedFolder
@@ -137,35 +118,11 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
137
118
  shape="sharp"
138
119
  onKeyDown={onKeyDown}
139
120
  onClick={() => {
121
+ innerRef.current?.focus();
140
122
  onToggleTree(!showTree);
141
123
  }}>
142
124
  {selectedFolder?.name}
143
125
  </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
126
  <IconButton
170
127
  aria-hidden
171
128
  aria-label=""
@@ -174,9 +131,7 @@ const ComboboxButton = forwardRef<HTMLButtonElement, Props>(
174
131
  colorTheme="greyLighter"
175
132
  size="small"
176
133
  onClick={() => {
177
- if (!showTree) {
178
- innerRef.current?.focus();
179
- }
134
+ innerRef.current?.focus();
180
135
  onToggleTree(!showTree);
181
136
  }}>
182
137
  {showTree ? <ChevronUp /> : <ChevronDown />}
@@ -37,6 +37,9 @@ const OpenButton = styled.span<{ isOpen: boolean }>`
37
37
  `;
38
38
 
39
39
  const StyledName = styled.span`
40
+ white-space: nowrap;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
40
43
  grid-column-start: 2;
41
44
  text-align: left;
42
45
  `;
@@ -63,7 +66,6 @@ const FolderName = styled(Button, { shouldForwardProp })<FolderNameProps>`
63
66
  color: ${({ isCreatingFolder, focused }) =>
64
67
  isCreatingFolder && focused ? colors.brand.primary : colors.text.primary};
65
68
  transition: ${animations.durations.superFast};
66
- line-height: 1;
67
69
  word-break: break-word;
68
70
 
69
71
  &:hover {
@@ -93,7 +95,6 @@ const FolderNameLink = styled(SafeLink, { shouldForwardProp })<FolderNameProps>`
93
95
  font-weight: ${({ selected }) => selected && fonts.weight.semibold};
94
96
  font-size: ${fonts.sizes('16px')};
95
97
  transition: ${animations.durations.superFast};
96
- line-height: 1;
97
98
  word-break: break-word;
98
99
  &:hover,
99
100
  &:focus {
@@ -230,7 +231,7 @@ const FolderItem = ({
230
231
  selected={selected}
231
232
  disabled={loading}
232
233
  onFocus={(e) => {
233
- setFocusedFolder(focusedFolder || folder, true);
234
+ setFocusedFolder(focusedFolder || folder);
234
235
  }}
235
236
  onClick={handleClickFolder}
236
237
  isCreatingFolder={isCreatingFolder}>
@@ -241,7 +242,7 @@ const FolderItem = ({
241
242
  isOpen={isOpen}
242
243
  onClick={(e) => {
243
244
  e.stopPropagation();
244
- setFocusedFolder(folder, true);
245
+ setFocusedFolder(folder);
245
246
  if (isOpen) {
246
247
  onCloseFolder(id);
247
248
  } else {
@@ -10,8 +10,7 @@ import React, { ReactNode } from 'react';
10
10
  import styled from '@emotion/styled';
11
11
  import { animations } from '@ndla/core';
12
12
  import FolderItem from './FolderItem';
13
- import FolderNameInput from './FolderNameInput';
14
- import { CommonFolderItemsProps, FolderType, TreeStructureType } from './types';
13
+ import { CommonFolderItemsProps, FolderType, NewFolderInputFunc, OnCreatedFunc, TreeStructureType } from './types';
15
14
  import NavigationLink from './NavigationLink';
16
15
  import { treestructureId } from './helperFunctions';
17
16
 
@@ -42,10 +41,11 @@ export interface FolderItemsProps extends CommonFolderItemsProps {
42
41
  folders: FolderType[];
43
42
  newFolderParentId: string | undefined;
44
43
  onCancelNewFolder: () => void;
45
- onSaveNewFolder: (name: string, parentId: string) => void;
46
44
  openFolders: string[];
47
45
  parentFolder?: FolderType;
48
46
  children?: ReactNode;
47
+ onCreate: OnCreatedFunc;
48
+ newFolderInput?: NewFolderInputFunc;
49
49
  }
50
50
 
51
51
  const FolderItems = ({
@@ -54,11 +54,12 @@ const FolderItems = ({
54
54
  loading,
55
55
  newFolderParentId,
56
56
  onCancelNewFolder,
57
- onSaveNewFolder,
58
57
  openFolders,
59
58
  type,
60
59
  parentFolder,
61
60
  children,
61
+ onCreate,
62
+ newFolderInput,
62
63
  ...rest
63
64
  }: FolderItemsProps) => (
64
65
  <StyledUL
@@ -101,20 +102,12 @@ const FolderItems = ({
101
102
  type={type}
102
103
  newFolderParentId={newFolderParentId}
103
104
  onCancelNewFolder={onCancelNewFolder}
104
- onSaveNewFolder={onSaveNewFolder}
105
105
  openFolders={openFolders}
106
+ newFolderInput={newFolderInput}
107
+ onCreate={onCreate}
106
108
  {...rest}>
107
109
  {newFolderParentId === id && (
108
- <li role="none">
109
- <FolderNameInput
110
- loading={loading}
111
- level={level}
112
- onCancelNewFolder={onCancelNewFolder}
113
- onSaveNewFolder={onSaveNewFolder}
114
- parentId={newFolderParentId}
115
- type={type}
116
- />
117
- </li>
110
+ <li role="none">{newFolderInput?.({ parentId: id, onClose: onCancelNewFolder, onCreate })}</li>
118
111
  )}
119
112
  </FolderItems>
120
113
  )}
@@ -14,8 +14,9 @@ import { uniq } from 'lodash';
14
14
  import { IFolder } from '@ndla/types-learningpath-api';
15
15
  import FolderItems from './FolderItems';
16
16
  import { flattenFolders, treestructureId } from './helperFunctions';
17
- import { CommonTreeStructureProps, FolderType, TreeStructureType } from './types';
17
+ import { CommonTreeStructureProps, FolderType, NewFolderInputFunc, TreeStructureType } from './types';
18
18
  import ComboboxButton from './ComboboxButton';
19
+ import AddFolderButton from './AddFolderButton';
19
20
 
20
21
  export const MAX_LEVEL_FOR_FOLDERS = 4;
21
22
 
@@ -29,6 +30,12 @@ const StyledTreeStructure = styled.div`
29
30
  flex-direction: column;
30
31
  `;
31
32
 
33
+ const Row = styled.div`
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ `;
38
+
32
39
  const TreeStructureWrapper = styled.div<{ type: TreeStructureType }>`
33
40
  display: flex;
34
41
  flex-direction: column;
@@ -73,7 +80,7 @@ export interface TreeStructureProps extends CommonTreeStructureProps {
73
80
  folders: FolderType[];
74
81
  label?: string;
75
82
  maxLevel?: number;
76
- onNewFolder?: (name: string, parentId: string) => Promise<IFolder>;
83
+ newFolderInput?: NewFolderInputFunc;
77
84
  }
78
85
 
79
86
  const TreeStructure = ({
@@ -82,10 +89,10 @@ const TreeStructure = ({
82
89
  label,
83
90
  loading,
84
91
  maxLevel = MAX_LEVEL_FOR_FOLDERS,
85
- onNewFolder,
86
92
  onSelectFolder,
87
93
  targetResource,
88
94
  type,
95
+ newFolderInput,
89
96
  }: TreeStructureProps) => {
90
97
  const ref = useRef<HTMLButtonElement>(null);
91
98
 
@@ -147,17 +154,15 @@ const TreeStructure = ({
147
154
  setOpenFolders(uniq(openFolders.concat(id)));
148
155
  };
149
156
 
150
- const onSaveNewFolder = (name: string, parentId: string) => {
151
- onNewFolder?.(name, parentId).then((newFolder) => {
152
- if (newFolder) {
153
- setSelectedFolder(newFolder);
154
- onSelectFolder?.(newFolder.id);
155
- setFocusedFolder(newFolder);
156
- setOpenFolders(uniq(openFolders.concat(parentId)));
157
- setNewFolderParentId?.(undefined);
158
- ref.current?.focus();
159
- }
160
- });
157
+ const onNewFolderCreated = (newFolder: IFolder | undefined, parentId: string) => {
158
+ if (newFolder) {
159
+ setSelectedFolder(newFolder);
160
+ onSelectFolder?.(newFolder.id);
161
+ setFocusedFolder(newFolder);
162
+ setOpenFolders(uniq(openFolders.concat(parentId)));
163
+ setNewFolderParentId?.(undefined);
164
+ ref.current?.focus();
165
+ }
161
166
  };
162
167
 
163
168
  const onCancelNewFolder = () => {
@@ -165,25 +170,31 @@ const TreeStructure = ({
165
170
  ref.current?.focus();
166
171
  };
167
172
 
168
- const setFolderFocus = (folder: FolderType, focus?: boolean) => {
173
+ const setFolderFocus = (folder: FolderType) => {
169
174
  setFocusedFolder(folder);
170
-
171
- if (focus) {
172
- ref.current?.focus();
173
- }
175
+ setNewFolderParentId(undefined);
176
+ ref.current?.focus();
174
177
  };
178
+ const canAddFolder = showTree && selectedFolder && selectedFolder?.breadcrumbs.length < (maxLevel || 1);
175
179
 
176
180
  return (
177
- <StyledTreeStructure>
178
- {label && <StyledLabel id={treestructureId(type, 'label')}>{label}</StyledLabel>}
179
- <TreeStructureWrapper
180
- aria-label={label}
181
- type={type}
182
- onBlur={(e) => {
183
- if (type === 'picker' && !e.currentTarget.contains(e.relatedTarget)) {
184
- onToggleTree(false);
185
- }
186
- }}>
181
+ <StyledTreeStructure
182
+ onBlur={(e) => {
183
+ if (type === 'picker' && !e.currentTarget.contains(e.relatedTarget)) {
184
+ onToggleTree(false);
185
+ }
186
+ }}>
187
+ <Row>
188
+ {label && <StyledLabel id={treestructureId(type, 'label')}>{label}</StyledLabel>}
189
+ {type === 'picker' && (
190
+ <AddFolderButton
191
+ canAddFolder={!!canAddFolder}
192
+ focusedFolder={focusedFolder}
193
+ setNewFolderParentId={setNewFolderParentId}
194
+ />
195
+ )}
196
+ </Row>
197
+ <TreeStructureWrapper aria-label={label} type={type}>
187
198
  {type === 'picker' && (
188
199
  <ComboboxButton
189
200
  ref={ref}
@@ -198,9 +209,6 @@ const TreeStructure = ({
198
209
  flattenedFolders={flattenedFolders}
199
210
  onCloseFolder={onCloseFolder}
200
211
  onOpenFolder={onOpenFolder}
201
- onNewFolder={onNewFolder}
202
- maxLevel={maxLevel}
203
- setNewFolderParentId={setNewFolderParentId}
204
212
  />
205
213
  )}
206
214
  {showTree && (
@@ -216,7 +224,6 @@ const TreeStructure = ({
216
224
  onCancelNewFolder={onCancelNewFolder}
217
225
  onCloseFolder={onCloseFolder}
218
226
  onOpenFolder={onOpenFolder}
219
- onSaveNewFolder={onSaveNewFolder}
220
227
  onSelectFolder={onSelectFolder}
221
228
  openFolders={openFolders}
222
229
  setFocusedFolder={setFolderFocus}
@@ -225,6 +232,8 @@ const TreeStructure = ({
225
232
  visibleFolders={flattenedFolders}
226
233
  type={type}
227
234
  closeTree={() => onToggleTree(false)}
235
+ newFolderInput={newFolderInput}
236
+ onCreate={onNewFolderCreated}
228
237
  />
229
238
  </ScrollableDiv>
230
239
  )}
@@ -17,6 +17,18 @@ export interface FolderType extends IFolder {
17
17
 
18
18
  export type TreeStructureType = 'navigation' | 'picker';
19
19
 
20
+ export type OnCreatedFunc = (folder: IFolder | undefined, parentId: string) => void;
21
+
22
+ export type NewFolderInputFunc = ({
23
+ onClose,
24
+ parentId,
25
+ onCreate,
26
+ }: {
27
+ onClose: () => void;
28
+ parentId: string;
29
+ onCreate: OnCreatedFunc;
30
+ }) => ReactNode;
31
+
20
32
  export interface TreeStructureMenuProps extends Omit<MenuItemProps, 'onClick'> {
21
33
  onClick: (e: MouseEvent<HTMLDivElement> | undefined, folder: FolderType) => void;
22
34
  }
@@ -35,7 +47,7 @@ export interface CommonFolderItemsProps extends CommonTreeStructureProps {
35
47
  selectedFolder?: FolderType;
36
48
  onCloseFolder: (id: string) => void;
37
49
  onOpenFolder: (id: string) => void;
38
- setFocusedFolder: (folder: FolderType, focus?: boolean) => void;
50
+ setFocusedFolder: (folder: FolderType) => void;
39
51
  setSelectedFolder: (folder: FolderType) => void;
40
52
  visibleFolders: FolderType[];
41
53
  closeTree: () => void;