@ndla/ui 47.2.1 → 47.3.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.
@@ -8,19 +8,27 @@
8
8
 
9
9
  import styled from '@emotion/styled';
10
10
  import { breakpoints, colors, fonts, mq, spacing } from '@ndla/core';
11
- import { InformationOutline, HumanMaleBoard, Forward, WarningOutline } from '@ndla/icons/common';
12
-
11
+ import { Forward } from '@ndla/icons/common';
13
12
  import { CloseButton } from '@ndla/button';
14
13
  import { css } from '@emotion/react';
15
14
  import { ReactNode } from 'react';
16
15
 
17
16
  type MessageBoxType = 'ghost' | 'danger';
18
17
 
19
- interface StyledProps {
18
+ interface LinkProps {
19
+ href?: string;
20
+ text?: string;
21
+ }
22
+
23
+ interface MessageBoxProps {
20
24
  type?: MessageBoxType;
25
+ children?: ReactNode;
26
+ links?: LinkProps[];
27
+ showCloseButton?: boolean;
28
+ onClose?: () => void;
21
29
  }
22
30
 
23
- const MessageBoxWrapper = styled.div<StyledProps>`
31
+ const MessageBoxWrapper = styled.div`
24
32
  display: flex;
25
33
  padding: ${spacing.small};
26
34
  font-family: ${fonts.sans};
@@ -32,21 +40,14 @@ const MessageBoxWrapper = styled.div<StyledProps>`
32
40
  ${mq.range({ until: breakpoints.tabletWide })} {
33
41
  ${fonts.sizes('16px')};
34
42
  }
35
-
36
- ${({ type }) =>
37
- type === 'ghost' &&
38
- css`
39
- background: transparent;
40
- border: 1px solid ${colors.brand.neutral7};
41
- color: ${colors.brand.greyDark};
42
- `}
43
-
44
- ${({ type }) =>
45
- type === 'danger' &&
46
- css`
47
- background: ${colors.support.redLightest};
48
- color: ${colors.text.primary};
49
- `}
43
+ &[data-type='ghost'] {
44
+ background: transparent;
45
+ color: ${colors.brand.greyDark};
46
+ }
47
+ &[data-type='danger'] {
48
+ background: ${colors.support.redLightest};
49
+ color: ${colors.text.primary};
50
+ }
50
51
  `;
51
52
 
52
53
  const InfoWrapper = styled.div`
@@ -54,23 +55,14 @@ const InfoWrapper = styled.div`
54
55
  flex-direction: row;
55
56
  flex: 1;
56
57
  padding: ${spacing.small};
57
- padding-right: 0;
58
58
  `;
59
59
 
60
- const TextWrapper = styled.div`
61
- & p {
62
- margin: 0;
63
- }
64
- `;
65
-
66
- const IconWrapper = styled.div`
60
+ const ChildrenWrapper = styled.div`
67
61
  display: flex;
68
- align-items: flex-start;
69
- padding-right: ${spacing.small};
70
-
62
+ gap: ${spacing.small};
71
63
  svg {
72
- width: 24px;
73
- height: 24px;
64
+ min-width: 24px;
65
+ min-height: 24px;
74
66
  }
75
67
  `;
76
68
 
@@ -79,7 +71,7 @@ const LinkWrapper = styled.div`
79
71
  flex-wrap: wrap;
80
72
  gap: ${spacing.normal};
81
73
  padding-top: ${spacing.nsmall};
82
-
74
+ padding-left: ${spacing.mediumlarge};
83
75
  svg {
84
76
  flex-shrink: 0;
85
77
  }
@@ -97,38 +89,12 @@ const StyledClosebutton = styled(CloseButton)`
97
89
  padding: 0;
98
90
  `;
99
91
 
100
- interface LinkProps {
101
- href?: string;
102
- text?: string;
103
- }
104
-
105
- interface Props {
106
- type?: MessageBoxType;
107
- children?: ReactNode;
108
- links?: LinkProps[];
109
- showCloseButton?: boolean;
110
- onClose?: () => void;
111
- }
112
-
113
- const Icon = ({ type }: StyledProps) => {
114
- if (type === 'ghost') {
115
- return <HumanMaleBoard />;
116
- }
117
- if (type === 'danger') {
118
- return <WarningOutline />;
119
- }
120
- return <InformationOutline />;
121
- };
122
-
123
- export const MessageBox = ({ type, children = '', links, showCloseButton, onClose }: Props) => {
92
+ export const MessageBox = ({ type, children, links, showCloseButton, onClose }: MessageBoxProps) => {
124
93
  return (
125
- <MessageBoxWrapper type={type}>
94
+ <MessageBoxWrapper data-type={type}>
126
95
  <InfoWrapper>
127
- <IconWrapper>
128
- <Icon type={type} />
129
- </IconWrapper>
130
96
  <div>
131
- <TextWrapper>{children}</TextWrapper>
97
+ <ChildrenWrapper>{children}</ChildrenWrapper>
132
98
  {links && (
133
99
  <LinkWrapper>
134
100
  {links.map((x) => (
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Copyright (c) 2023-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 { Meta, StoryObj } from '@storybook/react';
10
+ import { FavoriteButton } from '@ndla/button';
11
+ import ResourceItem from './ResourceItem';
12
+ import { defaultParameters } from '../../../../stories/defaults';
13
+
14
+ export default {
15
+ title: 'Components/ResourceItem',
16
+ tags: ['autodocs'],
17
+ component: ResourceItem,
18
+ parameters: {
19
+ inlineStories: true,
20
+ ...defaultParameters,
21
+ },
22
+ args: {
23
+ id: 'urn:resource:a7a49c0a-32ea-4343-8b11-bd6d65c24f87',
24
+ name: 'Refleksjonsoppgave om ideer og idéutvikling',
25
+ path: '',
26
+ contentType: 'subject-material',
27
+ additional: false,
28
+ heartButton: () => <FavoriteButton />,
29
+ access: undefined,
30
+ },
31
+ } as Meta<typeof ResourceItem>;
32
+
33
+ export const Default: StoryObj<typeof ResourceItem> = {};
34
+
35
+ export const WithCoreOrAdditionalIndicator: StoryObj<typeof ResourceItem> = {
36
+ args: {
37
+ contentTypeName: 'Fagstoff',
38
+ contentTypeDescription: 'Kjernestoff',
39
+ showAdditionalResources: true,
40
+ },
41
+ };
42
+
43
+ export const WithCoreOrAdditionalIndicatorAdditional: StoryObj<typeof ResourceItem> = {
44
+ args: {
45
+ additional: true,
46
+ contentTypeName: 'Fagstoff',
47
+ contentTypeDescription: 'Tilleggsstoff',
48
+ showAdditionalResources: true,
49
+ },
50
+ };
51
+
52
+ export const RelevanceIndicatorWithoutLabel: StoryObj<typeof ResourceItem> = {
53
+ args: {
54
+ contentTypeDescription: 'Kjernestoff',
55
+ showAdditionalResources: true,
56
+ },
57
+ };
58
+
59
+ export const OnlyAvailableForTeachers: StoryObj<typeof ResourceItem> = {
60
+ args: {
61
+ access: 'teacher',
62
+ },
63
+ };
64
+
65
+ export const CurrentPage: StoryObj<typeof ResourceItem> = {
66
+ args: { active: true },
67
+ };
68
+
69
+ export const SubjectMaterial: StoryObj<typeof ResourceItem> = {
70
+ args: {
71
+ contentType: 'subject-material',
72
+ },
73
+ };
74
+
75
+ export const TasksAndActivities: StoryObj<typeof ResourceItem> = {
76
+ args: {
77
+ contentType: 'tasks-and-activities',
78
+ },
79
+ };
80
+
81
+ export const AssessmentResource: StoryObj<typeof ResourceItem> = {
82
+ args: {
83
+ contentType: 'assessment-resources',
84
+ },
85
+ };
86
+
87
+ export const ExternalLearningResource: StoryObj<typeof ResourceItem> = {
88
+ args: {
89
+ contentType: 'external-learning-resources',
90
+ },
91
+ };
92
+
93
+ export const SourceMaterial: StoryObj<typeof ResourceItem> = {
94
+ args: {
95
+ contentType: 'source-material',
96
+ },
97
+ };
98
+
99
+ export const LearningPath: StoryObj<typeof ResourceItem> = {
100
+ args: {
101
+ contentType: 'learning-path',
102
+ },
103
+ };
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Copyright (c) 2023-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 { Dispatch, SetStateAction, useEffect, useState } from 'react';
10
+ import { Meta, StoryFn } from '@storybook/react';
11
+ import styled from '@emotion/styled';
12
+ import { IFolder } from '@ndla/types-backend/learningpath-api';
13
+ import { colors, spacing } from '@ndla/core';
14
+ import { uuid } from '@ndla/util';
15
+ import TreeStructure, { TreeStructureProps } from './TreeStructure';
16
+ import { defaultParameters } from '../../../../stories/defaults';
17
+ import { flattenFolders } from './helperFunctions';
18
+ import { FolderInput } from '../MyNdla';
19
+
20
+ const MY_FOLDERS_ID = 'folders';
21
+
22
+ const Container = styled.div`
23
+ display: flex;
24
+ margin-top: 40px;
25
+ max-width: 600px;
26
+ &[data-type='picker'] {
27
+ height: 250px;
28
+ }
29
+ `;
30
+
31
+ const StyledFolderInput = styled(FolderInput)`
32
+ border-left: ${spacing.xsmall} solid ${colors.brand.light};
33
+ border-right: ${spacing.xsmall} solid ${colors.brand.light};
34
+ &:focus-within {
35
+ border-color: ${colors.brand.light};
36
+ }
37
+ /* Not good practice, but necessary to give error message same padding as caused by border. */
38
+ & + span {
39
+ padding: 0 ${spacing.xsmall};
40
+ }
41
+ `;
42
+
43
+ const targetResource: TreeStructureProps['targetResource'] = {
44
+ id: 'test-resource',
45
+ resourceId: '123',
46
+ resourceType: 'type',
47
+ tags: [],
48
+ path: '',
49
+ created: '',
50
+ };
51
+
52
+ const STRUCTURE_EXAMPLE: IFolder[] = [
53
+ {
54
+ id: '1',
55
+ name: 'Mine favoritter',
56
+ status: 'private',
57
+ breadcrumbs: [{ id: '1', name: 'Mine Favoritter' }],
58
+ resources: [targetResource],
59
+ created: '2023-03-03T08:40:23.444Z',
60
+ updated: '2023-03-03T08:40:23.444Z',
61
+ subfolders: [
62
+ {
63
+ id: '2',
64
+ name: 'Eksamen',
65
+ status: 'shared',
66
+ breadcrumbs: [
67
+ { id: '1', name: 'Mine Favoritter' },
68
+ { id: '2', name: 'Eksamen' },
69
+ ],
70
+ created: '2023-03-03T08:40:23.444Z',
71
+ updated: '2023-03-03T08:40:23.444Z',
72
+ resources: [],
73
+ subfolders: [
74
+ {
75
+ id: '3',
76
+ name: 'Eksamens oppgaver',
77
+ status: 'shared',
78
+ breadcrumbs: [
79
+ { id: '1', name: 'Mine Favoritter' },
80
+ { id: '2', name: 'Eksamen' },
81
+ { id: '3', name: 'Eksamens oppgaver' },
82
+ ],
83
+ resources: [],
84
+ subfolders: [],
85
+ created: '2023-03-03T08:40:23.444Z',
86
+ updated: '2023-03-03T08:40:23.444Z',
87
+ },
88
+ {
89
+ id: '4',
90
+ name: 'Eksamen 2022',
91
+ status: 'private',
92
+ breadcrumbs: [
93
+ { id: '1', name: 'Mine Favoritter' },
94
+ { id: '2', name: 'Eksamen' },
95
+ { id: '4', name: 'Eksamen 2022' },
96
+ ],
97
+ resources: [],
98
+ subfolders: [],
99
+ created: '2023-03-03T08:40:23.444Z',
100
+ updated: '2023-03-03T08:40:23.444Z',
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ id: '5',
106
+ name: 'Oppgaver',
107
+ status: 'shared',
108
+ breadcrumbs: [
109
+ { id: '1', name: 'Mine Favoritter' },
110
+ { id: '5', name: 'Oppgaver' },
111
+ ],
112
+ resources: [],
113
+ subfolders: [],
114
+ created: '2023-03-03T08:40:23.444Z',
115
+ updated: '2023-03-03T08:40:23.444Z',
116
+ },
117
+ ],
118
+ },
119
+ ];
120
+
121
+ const FOLDER_TREE_STRUCTURE: IFolder[] = [
122
+ {
123
+ id: MY_FOLDERS_ID,
124
+ name: 'Mine mapper',
125
+ status: 'private',
126
+ breadcrumbs: [],
127
+ resources: [],
128
+ subfolders: [...STRUCTURE_EXAMPLE],
129
+ created: '2023-03-03T08:40:23.444Z',
130
+ updated: '2023-03-03T08:40:23.444Z',
131
+ },
132
+ ];
133
+
134
+ export default {
135
+ title: 'Components/TreeStructure',
136
+ tags: ['autodocs'],
137
+ component: TreeStructure,
138
+ parameters: {
139
+ inlineStories: true,
140
+ ...defaultParameters,
141
+ },
142
+ args: {
143
+ defaultOpenFolders: [MY_FOLDERS_ID],
144
+ targetResource: targetResource,
145
+ label: 'Velg mappe',
146
+ maxLevel: 5,
147
+ type: 'picker',
148
+ // eslint-disable-next-line no-console
149
+ onSelectFolder: console.log,
150
+ },
151
+ argTypes: {
152
+ folders: { control: false },
153
+ },
154
+ } as Meta<typeof TreeStructure>;
155
+
156
+ export const Default: StoryFn<typeof TreeStructure> = ({ ...args }) => {
157
+ const [structure, setStructure] = useState<IFolder[]>(
158
+ args.type === 'picker' ? FOLDER_TREE_STRUCTURE : STRUCTURE_EXAMPLE,
159
+ );
160
+
161
+ useEffect(() => {
162
+ setStructure(args.type === 'picker' ? FOLDER_TREE_STRUCTURE : STRUCTURE_EXAMPLE);
163
+ }, [args.type]);
164
+
165
+ return (
166
+ <Container data-type={args.type}>
167
+ <TreeStructure
168
+ {...args}
169
+ folders={structure}
170
+ newFolderInput={({ parentId, onClose, onCreate }) => (
171
+ <NewFolder
172
+ structure={structure}
173
+ setStructure={setStructure}
174
+ parentId={parentId}
175
+ onClose={onClose}
176
+ onCreate={onCreate}
177
+ />
178
+ )}
179
+ />
180
+ </Container>
181
+ );
182
+ };
183
+
184
+ interface NewFolderProps {
185
+ parentId: string;
186
+ structure: IFolder[];
187
+ setStructure: Dispatch<SetStateAction<IFolder[]>>;
188
+ onClose?: () => void;
189
+ onCreate?: (folder: IFolder, parentId: string) => void;
190
+ }
191
+
192
+ const generateNewFolder = (name: string, id: string, breadcrumbs: { id: string; name: string }[]): IFolder => ({
193
+ id,
194
+ name,
195
+ status: 'private',
196
+ subfolders: [],
197
+ breadcrumbs: breadcrumbs.concat({ name, id }),
198
+ resources: [],
199
+ created: '2023-03-03T08:40:23.444Z',
200
+ updated: '2023-03-03T08:40:23.444Z',
201
+ });
202
+
203
+ const NewFolder = ({ parentId, onClose, structure, setStructure, onCreate }: NewFolderProps) => {
204
+ const [name, setName] = useState('');
205
+ const [loading, setLoading] = useState(false);
206
+ const [error, setError] = useState('');
207
+
208
+ const onSave = async () => {
209
+ if (error) {
210
+ return;
211
+ }
212
+ if (name === '') {
213
+ return;
214
+ }
215
+ setLoading(true);
216
+ await new Promise((resolve) => setTimeout(resolve, 3000));
217
+ setLoading(false);
218
+ const flattenedStructure = flattenFolders(structure);
219
+ const targetFolder = flattenedStructure.find((folder) => folder.id === parentId);
220
+ const newFolderId = uuid();
221
+ const newFolder = generateNewFolder(name, newFolderId, targetFolder?.breadcrumbs ?? []);
222
+ if (targetFolder) {
223
+ setStructure((oldStructure) => {
224
+ targetFolder.subfolders.unshift(newFolder);
225
+ return oldStructure;
226
+ });
227
+ } else {
228
+ setStructure((old) => [newFolder].concat(old));
229
+ }
230
+ onCreate?.(newFolder, parentId);
231
+ onClose?.();
232
+ };
233
+
234
+ useEffect(() => {
235
+ if (name.length === 0) {
236
+ setError('Navn er påkrevd');
237
+ } else {
238
+ setError('');
239
+ }
240
+ }, [name]);
241
+
242
+ return (
243
+ <StyledFolderInput
244
+ // eslint-disable-next-line jsx-a11y/no-autofocus
245
+ autoFocus
246
+ labelHidden
247
+ name="name"
248
+ label={'Mine mapper'}
249
+ placeholder={'Skriv inn mappenavn'}
250
+ loading={loading}
251
+ onClose={onClose}
252
+ onSave={onSave}
253
+ error={error}
254
+ value={name}
255
+ onChange={(e) => {
256
+ if (!loading) {
257
+ setName(e.currentTarget.value);
258
+ }
259
+ }}
260
+ onKeyDown={(e) => {
261
+ if (e.key === 'Escape') {
262
+ e.preventDefault();
263
+ onClose?.();
264
+ } else if (e.key === 'Enter') {
265
+ e.preventDefault();
266
+ onSave();
267
+ }
268
+ }}
269
+ />
270
+ );
271
+ };