@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.
- package/es/Article/ArticleByline.js +17 -7
- package/es/Article/ArticleSideBar.js +5 -4
- package/es/Breadcrumb/BreadcrumbItem.js +8 -7
- package/es/ErrorMessage/ErrorMessage.js +12 -6
- package/es/Frontpage/FrontpageHeader.js +7 -9
- package/es/LanguageSelector/LanguageSelector.js +12 -7
- package/es/LearningPaths/LearningPathInformation.js +8 -5
- package/es/Subject/SubjectHeader.js +5 -6
- package/es/TreeStructure/FolderItem.js +110 -94
- package/es/TreeStructure/FolderItems.js +26 -30
- package/es/TreeStructure/FolderNameInput.js +35 -27
- package/es/TreeStructure/NavigationLink.js +81 -0
- package/es/TreeStructure/TreeStructure.js +169 -45
- package/es/locale/messages-en.js +7 -22
- package/es/locale/messages-nb.js +8 -23
- package/es/locale/messages-nn.js +7 -22
- package/es/locale/messages-se.js +697 -712
- package/es/locale/messages-sma.js +8 -23
- package/lib/Article/ArticleByline.js +17 -7
- package/lib/Article/ArticleSideBar.js +5 -4
- package/lib/Breadcrumb/BreadcrumbItem.js +8 -7
- package/lib/ErrorMessage/ErrorMessage.d.ts +1 -0
- package/lib/ErrorMessage/ErrorMessage.js +12 -6
- package/lib/Frontpage/FrontpageHeader.d.ts +5 -6
- package/lib/Frontpage/FrontpageHeader.js +7 -11
- package/lib/LanguageSelector/LanguageSelector.js +13 -7
- package/lib/LearningPaths/LearningPathInformation.d.ts +2 -1
- package/lib/LearningPaths/LearningPathInformation.js +8 -5
- package/lib/Subject/SubjectHeader.js +14 -16
- package/lib/TreeStructure/FolderItem.d.ts +2 -3
- package/lib/TreeStructure/FolderItem.js +107 -92
- package/lib/TreeStructure/FolderItems.d.ts +1 -3
- package/lib/TreeStructure/FolderItems.js +26 -29
- package/lib/TreeStructure/FolderNameInput.d.ts +2 -1
- package/lib/TreeStructure/FolderNameInput.js +33 -26
- package/lib/TreeStructure/NavigationLink.d.ts +15 -0
- package/lib/TreeStructure/NavigationLink.js +100 -0
- package/lib/TreeStructure/TreeStructure.d.ts +1 -2
- package/lib/TreeStructure/TreeStructure.js +163 -45
- package/lib/TreeStructure/types.d.ts +4 -1
- package/lib/locale/messages-en.d.ts +4 -19
- package/lib/locale/messages-en.js +7 -22
- package/lib/locale/messages-nb.d.ts +4 -19
- package/lib/locale/messages-nb.js +8 -23
- package/lib/locale/messages-nn.d.ts +4 -19
- package/lib/locale/messages-nn.js +7 -22
- package/lib/locale/messages-se.d.ts +4 -19
- package/lib/locale/messages-se.js +697 -712
- package/lib/locale/messages-sma.d.ts +4 -19
- package/lib/locale/messages-sma.js +8 -23
- package/package.json +14 -14
- package/src/Article/ArticleByline.tsx +10 -3
- package/src/Article/ArticleSideBar.tsx +1 -0
- package/src/Breadcrumb/BreadcrumbItem.tsx +1 -1
- package/src/ErrorMessage/ErrorMessage.tsx +6 -0
- package/src/Frontpage/FrontpageHeader.tsx +5 -6
- package/src/LanguageSelector/LanguageSelector.tsx +4 -1
- package/src/LearningPaths/LearningPathInformation.tsx +3 -2
- package/src/Subject/SubjectHeader.tsx +1 -2
- package/src/TreeStructure/FolderItem.tsx +126 -104
- package/src/TreeStructure/FolderItems.tsx +51 -43
- package/src/TreeStructure/FolderNameInput.tsx +43 -28
- package/src/TreeStructure/NavigationLink.tsx +100 -0
- package/src/TreeStructure/TreeStructure.tsx +187 -61
- package/src/TreeStructure/types.ts +5 -1
- package/src/locale/messages-en.ts +9 -22
- package/src/locale/messages-nb.ts +10 -23
- package/src/locale/messages-nn.ts +9 -22
- package/src/locale/messages-se.ts +724 -738
- package/src/locale/messages-sma.ts +10 -23
- package/es/TreeStructure/TreeStructureWrapper.js +0 -13
- package/lib/TreeStructure/TreeStructureWrapper.d.ts +0 -12
- package/lib/TreeStructure/TreeStructureWrapper.js +0 -24
- 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
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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<{
|
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
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>(
|
|
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
|
|
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="
|
|
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
|
-
|
|
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;
|