@ndla/ui 25.2.1 → 26.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 (74) hide show
  1. package/es/Article/ArticleByline.js +17 -7
  2. package/es/Article/ArticleSideBar.js +5 -4
  3. package/es/Breadcrumb/BreadcrumbItem.js +8 -7
  4. package/es/ErrorMessage/ErrorMessage.js +12 -6
  5. package/es/Frontpage/FrontpageHeader.js +7 -9
  6. package/es/LanguageSelector/LanguageSelector.js +12 -7
  7. package/es/LearningPaths/LearningPathInformation.js +8 -5
  8. package/es/Subject/SubjectHeader.js +5 -6
  9. package/es/TreeStructure/FolderItem.js +110 -94
  10. package/es/TreeStructure/FolderItems.js +26 -30
  11. package/es/TreeStructure/FolderNameInput.js +35 -27
  12. package/es/TreeStructure/NavigationLink.js +81 -0
  13. package/es/TreeStructure/TreeStructure.js +169 -45
  14. package/es/locale/messages-en.js +7 -22
  15. package/es/locale/messages-nb.js +8 -23
  16. package/es/locale/messages-nn.js +7 -22
  17. package/es/locale/messages-se.js +697 -712
  18. package/es/locale/messages-sma.js +8 -23
  19. package/lib/Article/ArticleByline.js +17 -7
  20. package/lib/Article/ArticleSideBar.js +5 -4
  21. package/lib/Breadcrumb/BreadcrumbItem.js +8 -7
  22. package/lib/ErrorMessage/ErrorMessage.d.ts +1 -0
  23. package/lib/ErrorMessage/ErrorMessage.js +12 -6
  24. package/lib/Frontpage/FrontpageHeader.d.ts +5 -6
  25. package/lib/Frontpage/FrontpageHeader.js +7 -11
  26. package/lib/LanguageSelector/LanguageSelector.js +13 -7
  27. package/lib/LearningPaths/LearningPathInformation.d.ts +2 -1
  28. package/lib/LearningPaths/LearningPathInformation.js +8 -5
  29. package/lib/Subject/SubjectHeader.js +14 -16
  30. package/lib/TreeStructure/FolderItem.d.ts +2 -3
  31. package/lib/TreeStructure/FolderItem.js +107 -92
  32. package/lib/TreeStructure/FolderItems.d.ts +1 -3
  33. package/lib/TreeStructure/FolderItems.js +26 -29
  34. package/lib/TreeStructure/FolderNameInput.d.ts +2 -1
  35. package/lib/TreeStructure/FolderNameInput.js +33 -26
  36. package/lib/TreeStructure/NavigationLink.d.ts +15 -0
  37. package/lib/TreeStructure/NavigationLink.js +100 -0
  38. package/lib/TreeStructure/TreeStructure.d.ts +1 -2
  39. package/lib/TreeStructure/TreeStructure.js +163 -45
  40. package/lib/TreeStructure/types.d.ts +4 -1
  41. package/lib/locale/messages-en.d.ts +4 -19
  42. package/lib/locale/messages-en.js +7 -22
  43. package/lib/locale/messages-nb.d.ts +4 -19
  44. package/lib/locale/messages-nb.js +8 -23
  45. package/lib/locale/messages-nn.d.ts +4 -19
  46. package/lib/locale/messages-nn.js +7 -22
  47. package/lib/locale/messages-se.d.ts +4 -19
  48. package/lib/locale/messages-se.js +697 -712
  49. package/lib/locale/messages-sma.d.ts +4 -19
  50. package/lib/locale/messages-sma.js +8 -23
  51. package/package.json +14 -14
  52. package/src/Article/ArticleByline.tsx +10 -3
  53. package/src/Article/ArticleSideBar.tsx +1 -0
  54. package/src/Breadcrumb/BreadcrumbItem.tsx +1 -1
  55. package/src/ErrorMessage/ErrorMessage.tsx +6 -0
  56. package/src/Frontpage/FrontpageHeader.tsx +5 -6
  57. package/src/LanguageSelector/LanguageSelector.tsx +4 -1
  58. package/src/LearningPaths/LearningPathInformation.tsx +3 -2
  59. package/src/Subject/SubjectHeader.tsx +1 -2
  60. package/src/TreeStructure/FolderItem.tsx +126 -104
  61. package/src/TreeStructure/FolderItems.tsx +51 -43
  62. package/src/TreeStructure/FolderNameInput.tsx +43 -28
  63. package/src/TreeStructure/NavigationLink.tsx +100 -0
  64. package/src/TreeStructure/TreeStructure.tsx +187 -61
  65. package/src/TreeStructure/types.ts +5 -1
  66. package/src/locale/messages-en.ts +9 -22
  67. package/src/locale/messages-nb.ts +10 -23
  68. package/src/locale/messages-nn.ts +9 -22
  69. package/src/locale/messages-se.ts +724 -738
  70. package/src/locale/messages-sma.ts +10 -23
  71. package/es/TreeStructure/TreeStructureWrapper.js +0 -13
  72. package/lib/TreeStructure/TreeStructureWrapper.d.ts +0 -12
  73. package/lib/TreeStructure/TreeStructureWrapper.js +0 -24
  74. package/src/TreeStructure/TreeStructureWrapper.tsx +0 -31
@@ -8,12 +8,13 @@
8
8
 
9
9
  import React from 'react';
10
10
  import styled from '@emotion/styled';
11
- import { animations, spacing } from '@ndla/core';
11
+ import { animations } from '@ndla/core';
12
12
  import FolderItem from './FolderItem';
13
13
  import FolderNameInput from './FolderNameInput';
14
- import { CommonFolderItemsProps, FolderType } from './types';
14
+ import { CommonFolderItemsProps, FolderType, TreeStructureType } from './types';
15
+ import NavigationLink from './NavigationLink';
15
16
 
16
- const StyledUL = styled.ul<{ firstLevel?: boolean }>`
17
+ const StyledUL = styled.ul`
17
18
  ${animations.fadeInLeft(animations.durations.fast)};
18
19
  animation-fill-mode: forwards;
19
20
  @media (prefers-reduced-motion: reduce) {
@@ -22,18 +23,22 @@ const StyledUL = styled.ul<{ firstLevel?: boolean }>`
22
23
  list-style: none;
23
24
  margin: 0;
24
25
  padding: 0;
25
- margin-left: ${({ firstLevel }) => (firstLevel ? `-${spacing.xsmall}` : spacing.small)};
26
26
  `;
27
27
 
28
- const StyledLI = styled.li`
28
+ interface StyledLiProps {
29
+ type?: TreeStructureType;
30
+ }
31
+
32
+ const StyledLI = styled.li<StyledLiProps>`
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: ${({ type }) => type === 'navigation' && 'flex-start'};
29
36
  margin: 0;
30
37
  padding: 0;
31
38
  `;
32
39
 
33
40
  export interface FolderItemsProps extends CommonFolderItemsProps {
34
41
  folders: FolderType[];
35
- editable?: boolean;
36
- maximumLevelsOfFoldersAllowed: number;
37
42
  newFolderParentId: string | undefined;
38
43
  onCancelNewFolder: () => void;
39
44
  onSaveNewFolder: (name: string, parentId: string) => void;
@@ -41,56 +46,59 @@ export interface FolderItemsProps extends CommonFolderItemsProps {
41
46
  }
42
47
 
43
48
  const FolderItems = ({
44
- editable,
45
49
  folders,
46
50
  level,
47
51
  loading,
48
- maximumLevelsOfFoldersAllowed,
49
52
  newFolderParentId,
50
53
  onCancelNewFolder,
51
54
  onSaveNewFolder,
52
55
  openFolders,
56
+ type,
53
57
  ...rest
54
58
  }: FolderItemsProps) => (
55
- <StyledUL role="group" firstLevel={level === 0}>
59
+ <StyledUL role={level === 0 ? 'tree' : 'group'}>
56
60
  {folders.map((folder) => {
57
61
  const { subfolders, id } = folder;
58
62
  const isOpen = openFolders?.includes(id);
59
63
 
60
64
  return (
61
- <StyledLI key={id} role="treeitem">
62
- <div>
63
- <FolderItem
64
- hideArrow={subfolders?.length === 0 || level > maximumLevelsOfFoldersAllowed}
65
- folder={folder}
66
- isOpen={isOpen}
67
- level={level}
68
- loading={loading}
69
- noPaddingWhenArrowIsHidden={editable && level === 0 && subfolders?.length === 0}
70
- {...rest}
71
- />
72
- </div>
73
- {newFolderParentId === id && (
74
- <FolderNameInput
75
- loading={loading}
76
- onCancelNewFolder={onCancelNewFolder}
77
- onSaveNewFolder={onSaveNewFolder}
78
- parentId={newFolderParentId}
79
- />
80
- )}
81
- {subfolders && isOpen && (
82
- <FolderItems
83
- editable={editable}
84
- folders={subfolders}
85
- level={level + 1}
86
- loading={loading}
87
- maximumLevelsOfFoldersAllowed={maximumLevelsOfFoldersAllowed}
88
- newFolderParentId={newFolderParentId}
89
- onCancelNewFolder={onCancelNewFolder}
90
- onSaveNewFolder={onSaveNewFolder}
91
- openFolders={openFolders}
92
- {...rest}
93
- />
65
+ <StyledLI key={id} role="treeitem" type={type}>
66
+ {folder.isNavigation ? (
67
+ <NavigationLink folder={folder} isOpen={isOpen} level={level} type={type} loading={loading} {...rest} />
68
+ ) : (
69
+ <>
70
+ <FolderItem
71
+ folder={folder}
72
+ isOpen={isOpen}
73
+ level={level}
74
+ loading={loading}
75
+ type={type}
76
+ isCreatingFolder={newFolderParentId === folder.id}
77
+ {...rest}
78
+ />
79
+ {newFolderParentId === id && (
80
+ <FolderNameInput
81
+ loading={loading}
82
+ level={level}
83
+ onCancelNewFolder={onCancelNewFolder}
84
+ onSaveNewFolder={onSaveNewFolder}
85
+ parentId={newFolderParentId}
86
+ />
87
+ )}
88
+ {subfolders && isOpen && (
89
+ <FolderItems
90
+ folders={subfolders}
91
+ level={level + 1}
92
+ loading={loading}
93
+ type={type}
94
+ newFolderParentId={newFolderParentId}
95
+ onCancelNewFolder={onCancelNewFolder}
96
+ onSaveNewFolder={onSaveNewFolder}
97
+ openFolders={openFolders}
98
+ {...rest}
99
+ />
100
+ )}
101
+ </>
94
102
  )}
95
103
  </StyledLI>
96
104
  );
@@ -8,9 +8,7 @@
8
8
 
9
9
  import React, { useEffect, useState, useRef, ChangeEvent, KeyboardEvent } from 'react';
10
10
  import styled from '@emotion/styled';
11
- import { FolderOutlined } from '@ndla/icons/contentType';
12
- import { ArrowDropDown as ArrowDropDownRaw } from '@ndla/icons/common';
13
- import { spacing, colors, misc, animations } from '@ndla/core';
11
+ import { spacing, colors, animations, spacingUnit } from '@ndla/core';
14
12
  import { useTranslation } from 'react-i18next';
15
13
  import { isMobile } from 'react-device-detect';
16
14
  import { Spinner } from '@ndla/icons';
@@ -18,18 +16,22 @@ import { IconButton } from '@ndla/button';
18
16
  import { Cross } from '@ndla/icons/action';
19
17
  import { Done } from '@ndla/icons/editor';
20
18
 
21
- const ArrowRight = styled(ArrowDropDownRaw)`
22
- color: ${colors.text.primary};
23
- transform: rotate(-90deg);
24
- `;
19
+ // Source: https://kovart.github.io/dashed-border-generator/
20
+ 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(
21
+ colors.brand.tertiary,
22
+ )}' stroke-width='2' stroke-dasharray='8%2c8' stroke-dashoffset='4' stroke-linecap='square'/%3e%3c/svg%3e")`;
25
23
 
26
24
  const NewFolderWrapper = styled.div`
27
- padding-left: ${spacing.normal};
28
- ${animations.fadeInLeft(animations.durations.fast)};
29
- animation-fill-mode: forwards;
30
- @media (prefers-reduced-motion: reduce) {
31
- animation: none;
32
- }
25
+ background: linear-gradient(
26
+ to bottom,
27
+ ${colors.white} 0%,
28
+ ${colors.white} 15%,
29
+ ${colors.brand.lighter} 15%,
30
+ ${colors.brand.lighter} 85%,
31
+ ${colors.white} 85%,
32
+ ${colors.white} 100%
33
+ );
34
+ background-size: auto 100%;
33
35
  `;
34
36
 
35
37
  const Row = styled.div`
@@ -38,23 +40,31 @@ const Row = styled.div`
38
40
  padding-right: ${spacing.xsmall};
39
41
  `;
40
42
 
41
- const InputWrapper = styled.div<{ loading?: boolean }>`
43
+ const InputWrapper = styled.div<{ level: number }>`
42
44
  display: flex;
43
45
  margin: ${spacing.xxsmall} 0;
46
+ margin-left: ${({ level }) => 0.75 * spacingUnit * level + 2 * spacingUnit}px;
47
+ margin-right: ${spacing.normal};
44
48
  align-items: center;
45
- border: 1px solid ${({ loading }) => (loading ? colors.brand.lighter : colors.brand.primary)};
46
- border-style: dashed;
47
- border-radius: ${misc.borderRadius};
49
+ background-color: ${colors.white};
50
+ background-image: ${borderStyle};
48
51
  color: ${colors.brand.primary};
52
+
53
+ ${animations.fadeInLeft(animations.durations.fast)};
54
+ animation-fill-mode: forwards;
55
+ @media (prefers-reduced-motion: reduce) {
56
+ animation: none;
57
+ }
49
58
  `;
50
59
 
51
60
  const StyledInput = styled.input`
61
+ padding: ${spacing.small};
52
62
  flex-grow: 1;
53
63
  border: 0;
54
64
  outline: none;
55
65
  min-width: 0;
56
66
  background: transparent;
57
- color: ${colors.text.primary};
67
+ color: ${colors.brand.primary};
58
68
  scroll-margin-top: 100px;
59
69
  `;
60
70
 
@@ -63,11 +73,12 @@ interface FolderNameInputProps {
63
73
  parentId: string;
64
74
  onCancelNewFolder: () => void;
65
75
  loading?: boolean;
76
+ level: number;
66
77
  }
67
78
 
68
- const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading }: FolderNameInputProps) => {
79
+ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading, level }: FolderNameInputProps) => {
69
80
  const { t } = useTranslation();
70
- const [name, setName] = useState<string>(t('treeStructure.newFolder.defaultName'));
81
+ const [name, setName] = useState<string>('');
71
82
  const inputRef = useRef<HTMLInputElement>(null);
72
83
 
73
84
  useEffect(() => {
@@ -75,15 +86,14 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
75
86
  if (isMobile) {
76
87
  inputRef.current?.scrollIntoView({ behavior: 'smooth' });
77
88
  }
78
- }, []);
89
+ return () => {
90
+ onCancelNewFolder();
91
+ };
92
+ }, [onCancelNewFolder]);
79
93
 
80
94
  return (
81
95
  <NewFolderWrapper>
82
- <InputWrapper loading={loading}>
83
- <Row>
84
- <ArrowRight />
85
- <FolderOutlined />
86
- </Row>
96
+ <InputWrapper level={level}>
87
97
  <StyledInput
88
98
  ref={inputRef}
89
99
  autoFocus
@@ -96,6 +106,10 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
96
106
  onCancelNewFolder();
97
107
  } else if (e.key === 'Enter' || e.key === 'Tab') {
98
108
  e.preventDefault();
109
+ if (name === '') {
110
+ onCancelNewFolder();
111
+ return;
112
+ }
99
113
  onSaveNewFolder(name, parentId);
100
114
  }
101
115
  }}
@@ -104,12 +118,13 @@ const FolderNameInput = ({ onSaveNewFolder, parentId, onCancelNewFolder, loading
104
118
  <Row>
105
119
  {!loading ? (
106
120
  <>
107
- <IconButton aria-label={t('close')} size="xsmall" ghostPill onClick={onCancelNewFolder}>
121
+ <IconButton aria-label={t('close')} title={t('close')} size="small" ghostPill onClick={onCancelNewFolder}>
108
122
  <Cross />
109
123
  </IconButton>
110
124
  <IconButton
111
125
  aria-label={t('save')}
112
- size="xsmall"
126
+ title={t('save')}
127
+ size="small"
113
128
  ghostPill
114
129
  onClick={() => onSaveNewFolder(name, parentId)}>
115
130
  <Done />
@@ -0,0 +1,100 @@
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 styled from '@emotion/styled';
10
+ import { colors, fonts, spacing } from '@ndla/core';
11
+ import React, { KeyboardEvent, useEffect, useRef } from 'react';
12
+ import SafeLink from '@ndla/safelink';
13
+ import { arrowNavigation } from './arrowNavigation';
14
+ import { CommonFolderItemsProps, FolderType } from './types';
15
+
16
+ interface StyledProps {
17
+ selected?: boolean;
18
+ }
19
+
20
+ const StyledSafeLink = styled(SafeLink)<StyledProps>`
21
+ display: grid;
22
+ grid-template-columns: ${spacing.medium} 1fr;
23
+ align-items: center;
24
+ padding: ${spacing.xxsmall};
25
+ margin: ${spacing.xsmall} 0;
26
+ gap: ${spacing.xxsmall};
27
+ box-shadow: none;
28
+
29
+ color: ${({ selected }) => (selected ? colors.brand.primary : colors.text.primary)};
30
+ font-weight: ${({ selected }) => (selected ? fonts.weight.semibold : fonts.weight.normal)};
31
+ ${fonts.sizes('16px')};
32
+
33
+ :hover,
34
+ :focus {
35
+ color: ${colors.brand.primary};
36
+ }
37
+ svg {
38
+ height: 26px;
39
+ width: 26px;
40
+ }
41
+ `;
42
+
43
+ const IconWrapper = styled.span`
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ `;
48
+
49
+ interface Props extends CommonFolderItemsProps {
50
+ isOpen: boolean;
51
+ folder: FolderType;
52
+ }
53
+
54
+ const NavigationLink = ({
55
+ loading,
56
+ folder,
57
+ selectedFolder,
58
+ focusedFolderId,
59
+ setSelectedFolder,
60
+ setFocusedId,
61
+ visibleFolders,
62
+ onOpenFolder,
63
+ onCloseFolder,
64
+ }: Props) => {
65
+ const { id, icon, name } = folder;
66
+ const selected = selectedFolder && selectedFolder.id === id;
67
+ const ref = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
68
+ const focused = focusedFolderId === id;
69
+
70
+ const handleClick = () => {
71
+ if (!selected) {
72
+ setSelectedFolder(folder);
73
+ setFocusedId(id);
74
+ }
75
+ };
76
+
77
+ useEffect(() => {
78
+ if (focusedFolderId === id) {
79
+ ref.current?.focus();
80
+ }
81
+ }, [focusedFolderId, ref, id]);
82
+
83
+ return (
84
+ <StyledSafeLink
85
+ ref={ref}
86
+ onKeyDown={(e: KeyboardEvent<HTMLElement>) =>
87
+ arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)
88
+ }
89
+ tabIndex={selected || focused ? 0 : -1}
90
+ selected={selected}
91
+ onFocus={() => setFocusedId(id)}
92
+ onClick={handleClick}
93
+ to={loading ? '' : `/minndla/${id}`}>
94
+ <IconWrapper>{icon}</IconWrapper>
95
+ {name}
96
+ </StyledSafeLink>
97
+ );
98
+ };
99
+
100
+ export default NavigationLink;