@ndla/ui 22.0.2 → 22.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 (98) hide show
  1. package/es/Article/ArticleByline.js +7 -4
  2. package/es/Article/ArticleNotions.js +10 -6
  3. package/es/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
  4. package/es/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
  5. package/es/CompetenceGoalTab/CompetenceItem.js +14 -12
  6. package/es/CompetenceGoalTab/SearchButton.js +7 -4
  7. package/es/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
  8. package/es/Filter/FilterButtons.js +10 -9
  9. package/es/Footer/FooterPrivacy.js +3 -2
  10. package/es/Masthead/MastheadSearchModal.js +4 -3
  11. package/es/Resource/BlockResource.js +9 -5
  12. package/es/Resource/ListResource.js +8 -6
  13. package/es/Resource/resourceComponents.js +36 -25
  14. package/es/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
  15. package/es/SearchTypeResult/PopupFilter.js +12 -8
  16. package/es/SearchTypeResult/components/ItemContexts.js +8 -7
  17. package/es/Topic/Topic.js +21 -20
  18. package/es/TreeStructure/FolderItem.js +34 -29
  19. package/es/TreeStructure/TreeStructure.js +5 -3
  20. package/es/TreeStructure/TreeStructureWrapper.js +2 -2
  21. package/es/User/AuthModal.js +9 -8
  22. package/es/locale/messages-en.js +12 -4
  23. package/es/locale/messages-nb.js +12 -4
  24. package/es/locale/messages-nn.js +12 -4
  25. package/es/locale/messages-se.js +12 -4
  26. package/es/locale/messages-sma.js +12 -4
  27. package/lib/Article/ArticleByline.js +7 -4
  28. package/lib/Article/ArticleNotions.js +10 -6
  29. package/lib/CompetenceGoalTab/CompetenceGoalItem.d.ts +1 -1
  30. package/lib/CompetenceGoalTab/CompetenceGoalItem.js +12 -10
  31. package/lib/CompetenceGoalTab/CompetenceGoalTab.d.ts +2 -1
  32. package/lib/CompetenceGoalTab/CompetenceGoalTab.js +11 -9
  33. package/lib/CompetenceGoalTab/CompetenceItem.d.ts +2 -1
  34. package/lib/CompetenceGoalTab/CompetenceItem.js +14 -12
  35. package/lib/CompetenceGoalTab/SearchButton.d.ts +2 -1
  36. package/lib/CompetenceGoalTab/SearchButton.js +7 -4
  37. package/lib/CompetenceGoals/CompetenceGoalsDialog.js +8 -4
  38. package/lib/Filter/FilterButtons.js +10 -9
  39. package/lib/Footer/FooterPrivacy.js +3 -2
  40. package/lib/Masthead/MastheadSearchModal.js +4 -3
  41. package/lib/Resource/BlockResource.d.ts +2 -1
  42. package/lib/Resource/BlockResource.js +9 -5
  43. package/lib/Resource/ListResource.d.ts +2 -1
  44. package/lib/Resource/ListResource.js +8 -6
  45. package/lib/Resource/resourceComponents.d.ts +4 -2
  46. package/lib/Resource/resourceComponents.js +38 -19
  47. package/lib/ResourcesWrapper/ResourcesTopicTitle.js +7 -4
  48. package/lib/SearchTypeResult/PopupFilter.js +12 -8
  49. package/lib/SearchTypeResult/components/ItemContexts.js +8 -7
  50. package/lib/Topic/Topic.js +21 -20
  51. package/lib/TreeStructure/FolderItem.d.ts +1 -1
  52. package/lib/TreeStructure/FolderItem.js +34 -29
  53. package/lib/TreeStructure/TreeStructure.d.ts +0 -1
  54. package/lib/TreeStructure/TreeStructure.js +5 -3
  55. package/lib/TreeStructure/TreeStructureWrapper.js +2 -2
  56. package/lib/TreeStructure/types.d.ts +1 -0
  57. package/lib/User/AuthModal.js +9 -8
  58. package/lib/locale/messages-en.d.ts +8 -0
  59. package/lib/locale/messages-en.js +12 -4
  60. package/lib/locale/messages-nb.d.ts +8 -0
  61. package/lib/locale/messages-nb.js +12 -4
  62. package/lib/locale/messages-nn.d.ts +8 -0
  63. package/lib/locale/messages-nn.js +12 -4
  64. package/lib/locale/messages-se.d.ts +8 -0
  65. package/lib/locale/messages-se.js +12 -4
  66. package/lib/locale/messages-sma.d.ts +8 -0
  67. package/lib/locale/messages-sma.js +12 -4
  68. package/lib/types.d.ts +1 -0
  69. package/package.json +8 -7
  70. package/src/Article/ArticleByline.tsx +4 -1
  71. package/src/Article/ArticleNotions.tsx +4 -1
  72. package/src/CompetenceGoalTab/CompetenceGoalItem.tsx +6 -2
  73. package/src/CompetenceGoalTab/CompetenceGoalTab.tsx +5 -4
  74. package/src/CompetenceGoalTab/CompetenceItem.tsx +9 -2
  75. package/src/CompetenceGoalTab/SearchButton.tsx +3 -2
  76. package/src/CompetenceGoals/CompetenceGoalsDialog.jsx +5 -2
  77. package/src/Filter/FilterButtons.tsx +1 -0
  78. package/src/Footer/FooterPrivacy.tsx +4 -1
  79. package/src/Masthead/MastheadSearchModal.tsx +1 -0
  80. package/src/Resource/BlockResource.tsx +4 -2
  81. package/src/Resource/ListResource.tsx +3 -1
  82. package/src/Resource/resourceComponents.tsx +25 -9
  83. package/src/ResourcesWrapper/ResourcesTopicTitle.tsx +5 -1
  84. package/src/SearchTypeResult/PopupFilter.tsx +3 -1
  85. package/src/SearchTypeResult/components/ItemContexts.tsx +1 -0
  86. package/src/Topic/Topic.tsx +1 -0
  87. package/src/TreeStructure/FolderItem.tsx +7 -2
  88. package/src/TreeStructure/TreeStructure.tsx +2 -1
  89. package/src/TreeStructure/TreeStructureWrapper.tsx +9 -12
  90. package/src/TreeStructure/types.ts +1 -0
  91. package/src/User/AuthModal.tsx +2 -1
  92. package/src/locale/messages-en.ts +8 -0
  93. package/src/locale/messages-nb.ts +8 -0
  94. package/src/locale/messages-nn.ts +8 -0
  95. package/src/locale/messages-se.ts +8 -0
  96. package/src/locale/messages-sma.ts +8 -0
  97. package/src/types.ts +1 -0
  98. package/src/.DS_Store +0 -0
@@ -29,6 +29,8 @@ declare const messages: {
29
29
  collectedInfo: string;
30
30
  general: string;
31
31
  topic: string;
32
+ isAuth: string;
33
+ isNotAuth: string;
32
34
  };
33
35
  resource: {
34
36
  accessDenied: string;
@@ -311,6 +313,7 @@ declare const messages: {
311
313
  contextModal: {
312
314
  button: string;
313
315
  heading: string;
316
+ ariaLabel: string;
314
317
  };
315
318
  };
316
319
  subjectPage: {
@@ -356,6 +359,7 @@ declare const messages: {
356
359
  articleErrorDescription: string;
357
360
  topic: string;
358
361
  topics: string;
362
+ imageModal: string;
359
363
  };
360
364
  welcomePage: {
361
365
  search: string;
@@ -413,6 +417,7 @@ declare const messages: {
413
417
  toFrontpage: string;
414
418
  subjectOverview: string;
415
419
  title: string;
420
+ modalLabel: string;
416
421
  subjectPage: string;
417
422
  backToSubjectFrontpage: string;
418
423
  openFilter: string;
@@ -523,6 +528,7 @@ declare const messages: {
523
528
  competenceGoals: {
524
529
  competenceGoal: string;
525
530
  title: string;
531
+ modalText: string;
526
532
  closeCompetenceGoals: string;
527
533
  showCompetenceGoals: string;
528
534
  openCompentenceGoalsFilter: string;
@@ -546,6 +552,8 @@ declare const messages: {
546
552
  competenceGoalItem: {
547
553
  title: string;
548
554
  };
555
+ licenseData: string;
556
+ licenseFrom: string;
549
557
  };
550
558
  subject: {
551
559
  associatedTopics: string;
@@ -172,7 +172,8 @@ var messages = _objectSpread(_objectSpread({
172
172
  },
173
173
  contextModal: {
174
174
  button: '+ {{count}} flere steder',
175
- heading: 'Ressursen er brukt flere steder'
175
+ heading: 'Ressursen er brukt flere steder',
176
+ ariaLabel: 'Se flere kontekster'
176
177
  }
177
178
  },
178
179
  subjectPage: {
@@ -217,7 +218,8 @@ var messages = _objectSpread(_objectSpread({
217
218
  topicPage: {
218
219
  articleErrorDescription: 'Beklager, en feil oppstod under lasting av emnebeskrivelsen.',
219
220
  topic: 'EMNE',
220
- topics: 'Emner'
221
+ topics: 'Emner',
222
+ imageModal: 'Se bildet i full størrelse'
221
223
  },
222
224
  welcomePage: {
223
225
  search: 'Søk',
@@ -275,6 +277,7 @@ var messages = _objectSpread(_objectSpread({
275
277
  toFrontpage: 'Til forsiden',
276
278
  subjectOverview: 'Alle fag',
277
279
  title: 'Innhold',
280
+ modalLabel: 'Velg innhold',
278
281
  subjectPage: 'Fagforside',
279
282
  backToSubjectFrontpage: 'Tilbake til fagforsiden',
280
283
  openFilter: 'Filter',
@@ -361,6 +364,7 @@ var messages = _objectSpread(_objectSpread({
361
364
  competenceGoals: {
362
365
  competenceGoal: 'kompetansemål',
363
366
  title: 'Kompetansemål og læreplan',
367
+ modalText: 'Utforsk læreplankoblinger',
364
368
  closeCompetenceGoals: 'Lukk kompetansemål',
365
369
  showCompetenceGoals: 'Vis kompetansemål',
366
370
  openCompentenceGoalsFilter: 'Filtrer kompetansemål',
@@ -383,7 +387,9 @@ var messages = _objectSpread(_objectSpread({
383
387
  competenceTabCorelabel: 'Kjerneelement',
384
388
  competenceGoalItem: {
385
389
  title: 'Kompetansemål og vurdering'
386
- }
390
+ },
391
+ licenseData: 'Inneholder data under',
392
+ licenseFrom: 'tilgjengeliggjort på'
387
393
  },
388
394
  subject: {
389
395
  associatedTopics: 'Tilhørende emner'
@@ -923,7 +929,9 @@ var messages = _objectSpread(_objectSpread({
923
929
  modal: {
924
930
  collectedInfo: 'Vi har hentet følgende informasjon om deg fra Feide:',
925
931
  general: 'Ressursene som krever pålogging med Feide, vises med ikonet',
926
- topic: 'Logg inn med Feide for å få tilgang til dette emnet.'
932
+ topic: 'Logg inn med Feide for å få tilgang til dette emnet.',
933
+ isAuth: 'Brukerinfo',
934
+ isNotAuth: 'Logg inn med Feide'
927
935
  },
928
936
  resource: {
929
937
  accessDenied: 'Vi beklager, men denne ressursen er bare for lærere innlogget med Feide.'
package/lib/types.d.ts CHANGED
@@ -96,6 +96,7 @@ export declare type CompetenceGoalsItemType = {
96
96
  type: 'LK06' | 'LK20';
97
97
  }[];
98
98
  selected?: boolean;
99
+ isOembed?: boolean;
99
100
  };
100
101
  export declare type NotionMedia = {
101
102
  type: 'video' | 'other';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndla/ui",
3
- "version": "22.0.2",
3
+ "version": "22.2.0",
4
4
  "description": "UI component library for NDLA.",
5
5
  "license": "GPL-3.0",
6
6
  "main": "lib/index.js",
@@ -31,16 +31,16 @@
31
31
  "types"
32
32
  ],
33
33
  "dependencies": {
34
- "@ndla/button": "^3.2.1",
34
+ "@ndla/button": "^3.3.0",
35
35
  "@ndla/carousel": "^1.2.15",
36
36
  "@ndla/core": "^2.3.3",
37
37
  "@ndla/forms": "^3.1.3",
38
38
  "@ndla/hooks": "^1.1.4",
39
39
  "@ndla/icons": "^1.11.3",
40
40
  "@ndla/licenses": "^5.0.6",
41
- "@ndla/modal": "^1.2.16",
42
- "@ndla/notion": "^3.1.27",
43
- "@ndla/safelink": "^2.2.5",
41
+ "@ndla/modal": "^1.2.17",
42
+ "@ndla/notion": "^3.1.29",
43
+ "@ndla/safelink": "^2.2.6",
44
44
  "@ndla/switch": "^0.1.10",
45
45
  "@ndla/tabs": "^1.1.14",
46
46
  "@ndla/tooltip": "^2.1.5",
@@ -68,7 +68,8 @@
68
68
  "i18next": "^20.3.1",
69
69
  "prop-types": "^15.8.1",
70
70
  "react": "^16.8.4 || ^17.0.0",
71
- "react-i18next": "^11.11.0"
71
+ "react-i18next": "^11.11.0",
72
+ "react-router-dom": "^6.3.0"
72
73
  },
73
74
  "devDependencies": {
74
75
  "@babel/plugin-proposal-optional-chaining": "^7.11.0",
@@ -83,5 +84,5 @@
83
84
  "publishConfig": {
84
85
  "access": "public"
85
86
  },
86
- "gitHead": "d26b1f085bcef66dcc1d8ed829072ab1223d28eb"
87
+ "gitHead": "12ed2984919fd75fbb7f1b1e55c1d5903fd51d51"
87
88
  }
@@ -102,6 +102,8 @@ const ArticleByline = ({
102
102
  const showPrimaryContributors = suppliers.length > 0 || authors.length > 0;
103
103
  const showSecondaryContributors = suppliers.length > 0 && authors.length > 0;
104
104
 
105
+ const buttonId = 'popupUseContent';
106
+
105
107
  return (
106
108
  <Wrapper>
107
109
  <div>
@@ -124,8 +126,9 @@ const ArticleByline = ({
124
126
  <ButtonWrapper>
125
127
  {licenseBox && (
126
128
  <Modal
129
+ labelledBy={buttonId}
127
130
  activateButton={
128
- <Button size="small" borderShape="rounded" outline>
131
+ <Button id={buttonId} size="small" borderShape="rounded" outline>
129
132
  {t('article.useContent')}
130
133
  </Button>
131
134
  }
@@ -165,14 +165,17 @@ type ArticleNotionsProps = {
165
165
  export const ArticleNotions = ({ notions, relatedContent = [], buttonOffsetRight, type }: ArticleNotionsProps) => {
166
166
  const { t } = useTranslation();
167
167
  const leftOffset = `${buttonOffsetRight - 32}px`;
168
+ const headingId = 'popupNotionHeading';
169
+
168
170
  return (
169
171
  <ArticleNotionsContainer>
170
172
  <Modal
173
+ labelledBy={headingId}
171
174
  activateButton={
172
175
  <NotionsTrigger role="button" aria-label={t('article.notionsPrompt')} style={{ left: leftOffset }}>
173
176
  <NotionFlip />
174
177
  <Explanation />
175
- <span>{t('article.notionsPrompt')}</span>
178
+ <span id={headingId}>{t('article.notionsPrompt')}</span>
176
179
  </NotionsTrigger>
177
180
  }
178
181
  size="large"
@@ -60,7 +60,7 @@ const GoalSearchWrapper = styled.div`
60
60
  }
61
61
  `;
62
62
 
63
- const CompetenceGoalItem = ({ title, goals }: CompetenceGoalsItemType) => {
63
+ const CompetenceGoalItem = ({ title, goals, isOembed }: CompetenceGoalsItemType) => {
64
64
  const { t } = useTranslation();
65
65
  return (
66
66
  <GoalItem>
@@ -73,7 +73,11 @@ const CompetenceGoalItem = ({ title, goals }: CompetenceGoalsItemType) => {
73
73
  <GoalListInnerTextWrapper>{goal.text}</GoalListInnerTextWrapper>
74
74
  {goal.url && goal.type !== 'LK06' && (
75
75
  <GoalSearchWrapper>
76
- <SearchButton to={goal.url} text={t('competenceGoals.competenceGoalResourceSearchText')} />
76
+ <SearchButton
77
+ to={goal.url}
78
+ text={t('competenceGoals.competenceGoalResourceSearchText')}
79
+ target={isOembed ? '_blank' : '_self'}
80
+ />
77
81
  </GoalSearchWrapper>
78
82
  )}
79
83
  </GoalListElementInnerWrapper>
@@ -19,6 +19,7 @@ import CompetenceItem, { ListItemProp } from './CompetenceItem';
19
19
  type CompetenceProps = {
20
20
  list: ListItemProp[];
21
21
  highlightSearchBox?: boolean;
22
+ isOembed?: boolean;
22
23
  };
23
24
 
24
25
  const Wrapper = styled.div`
@@ -79,7 +80,7 @@ const HighlightText = styled.span`
79
80
  font-family: 'Shadows Into Light Two', cursive;
80
81
  `;
81
82
 
82
- const CompetenceGoalTab = ({ list, highlightSearchBox }: CompetenceProps) => {
83
+ const CompetenceGoalTab = ({ list, highlightSearchBox, isOembed }: CompetenceProps) => {
83
84
  const [currentTabItem, setCurrentTab] = useState(list[0]);
84
85
  const { t } = useTranslation();
85
86
 
@@ -106,15 +107,15 @@ const CompetenceGoalTab = ({ list, highlightSearchBox }: CompetenceProps) => {
106
107
  <ArrowFeatureTips />
107
108
  </HighlightWrapper>
108
109
  )}
109
- <CompetenceItem item={currentTabItem} />
110
+ <CompetenceItem item={currentTabItem} isOembed={isOembed} />
110
111
  <LicenseByline licenseRights={[CC, BY]}>
111
112
  <LicenseIconsTextWrapper>UDIR</LicenseIconsTextWrapper>
112
113
  </LicenseByline>
113
- Inneholder data under{' '}
114
+ {`${t('competenceGoals.licenseData')} `}
114
115
  <SafeLink to="https://data.norge.no/nlod/no" target="_blank">
115
116
  NLOD
116
117
  </SafeLink>
117
- , tilgjengeliggjort på{' '}
118
+ {`, ${t('competenceGoals.licenseFrom')} `}
118
119
  <SafeLink to="https://data.udir.no/" target="_blank">
119
120
  data.udir.no
120
121
  </SafeLink>
@@ -79,9 +79,10 @@ export type ListItemProp = {
79
79
  };
80
80
  export type ListItemProps = {
81
81
  item: ListItemProp;
82
+ isOembed?: boolean;
82
83
  };
83
84
 
84
- const CompetenceItem = ({ item }: ListItemProps) => {
85
+ const CompetenceItem = ({ item, isOembed }: ListItemProps) => {
85
86
  const { t } = useTranslation();
86
87
  const { type, groupedCompetenceGoals, groupedCoreElementItems } = item;
87
88
  switch (type) {
@@ -105,7 +106,13 @@ const CompetenceItem = ({ item }: ListItemProps) => {
105
106
  {group.elements.length > 0 && (
106
107
  <Goals>
107
108
  {group.elements.map((goal) => (
108
- <CompetenceGoalItem key={goal.id} id={goal.id} title={goal.title} goals={goal.goals} />
109
+ <CompetenceGoalItem
110
+ key={goal.id}
111
+ id={goal.id}
112
+ title={goal.title}
113
+ goals={goal.goals}
114
+ isOembed={isOembed}
115
+ />
109
116
  ))}
110
117
  </Goals>
111
118
  )}
@@ -37,11 +37,12 @@ const IconWrapper = styled.span`
37
37
  type Props = {
38
38
  to: string;
39
39
  text: string;
40
+ target?: string;
40
41
  };
41
42
 
42
- const SearchButton = ({ to, text }: Props) => (
43
+ const SearchButton = ({ to, text, target = '_self' }: Props) => (
43
44
  <Wrapper>
44
- <SafeLink to={to}>
45
+ <SafeLink to={to} target={target}>
45
46
  <IconWrapper>
46
47
  <Search style={{ width: '24px', height: '24px' }} />
47
48
  </IconWrapper>
@@ -33,8 +33,11 @@ const HeadingWrapper = styled.h2`
33
33
 
34
34
  export const CompetenceGoalsDialog = ({ children, isOpen, onClose, subjectName, modalProps }) => {
35
35
  const { t } = useTranslation();
36
+ const iconId = 'popupCompetenceGoals';
37
+
36
38
  return (
37
39
  <Modal
40
+ labelledBy={iconId}
38
41
  {...modalProps}
39
42
  controllable
40
43
  isOpen={isOpen}
@@ -48,8 +51,8 @@ export const CompetenceGoalsDialog = ({ children, isOpen, onClose, subjectName,
48
51
  <ModalHeader modifier="menu">
49
52
  <HeaderWrapper>
50
53
  <HeadingWrapper>
51
- <FooterHeaderIcon size="24px" style={{ marginRight: '20px' }} />
52
- {'Utforsk læreplankoblinger'} {subjectName && ` \u2022 ${subjectName}`}
54
+ <FooterHeaderIcon id={iconId} size="24px" style={{ marginRight: '20px' }} />
55
+ {t('competenceGoals.modalText')} {subjectName && ` \u2022 ${subjectName}`}
53
56
  </HeadingWrapper>
54
57
  <ModalCloseButton onClick={close} title={t('competenceGoals.competenceGoalClose')} />
55
58
  </HeaderWrapper>
@@ -128,6 +128,7 @@ export const FilterButtons = ({ heading, items, onFilterToggle, onRemoveAllFilte
128
128
  </StyledButtonElementWrapper>
129
129
  ))}
130
130
  <Modal
131
+ label={t('searchPage.searchFilterMessages.resourceTypeFilter.button')}
131
132
  size="fullscreen"
132
133
  animation="subtle"
133
134
  backgroundColor="white"
@@ -69,7 +69,10 @@ const StyledFooterText = styled.div`
69
69
 
70
70
  const FooterPrivacy = ({ lang, label }: FooterPrivacyProps) => (
71
71
  <StyledFooterText>
72
- <Modal activateButton={<StyledPrivacyButton type="button">{label}</StyledPrivacyButton>} size="fullscreen">
72
+ <Modal
73
+ label={label}
74
+ activateButton={<StyledPrivacyButton type="button">{label}</StyledPrivacyButton>}
75
+ size="fullscreen">
73
76
  {(onClose: () => void) => (
74
77
  <OneColumn cssModifier="medium">
75
78
  <ModalHeader>
@@ -117,6 +117,7 @@ const MastheadSearchModal = ({
117
117
  t,
118
118
  }: Props & WithTranslation) => (
119
119
  <Modal
120
+ label={t('searchPage.searchFieldPlaceholder')}
120
121
  backgroundColor="grey"
121
122
  animation="slide-down"
122
123
  animationDuration={200}
@@ -17,6 +17,7 @@ import ContentLoader from '../ContentLoader';
17
17
 
18
18
  interface BlockResourceProps {
19
19
  link: string;
20
+ tagLinkPrefix?: string;
20
21
  title: string;
21
22
  resourceImage: ResourceImageProps;
22
23
  topics: string[];
@@ -47,7 +48,7 @@ const BlockDescription = styled.p`
47
48
  overflow: hidden;
48
49
  text-overflow: ellipsis;
49
50
  transition: height 0.2s ease-out;
50
- ${() => BlockElementWrapper}:hover & {
51
+ ${() => BlockElementWrapper}:hover &, ${() => BlockElementWrapper}:focus & {
51
52
  // Unfortunate css needed for multi-line text overflow ellipsis.
52
53
  height: 3.1em;
53
54
  -webkit-line-clamp: 2;
@@ -132,6 +133,7 @@ const BlockTopicList = ({ topics, loading }: BlockTopicListProps) => {
132
133
 
133
134
  const BlockResource = ({
134
135
  link,
136
+ tagLinkPrefix,
135
137
  title,
136
138
  tags,
137
139
  resourceImage,
@@ -152,7 +154,7 @@ const BlockResource = ({
152
154
  <BlockTopicList topics={topics} loading={isLoading} />
153
155
  <BlockDescription>{description}</BlockDescription>
154
156
  <RightRow>
155
- {tags && <CompressedTagList tags={tags} />}
157
+ {tags && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
156
158
  {menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
157
159
  </RightRow>
158
160
  </BlockInfoWrapper>
@@ -109,6 +109,7 @@ interface StyledImageProps {
109
109
 
110
110
  export interface ListResourceProps {
111
111
  link: string;
112
+ tagLinkPrefix?: string;
112
113
  title: string;
113
114
  resourceImage: ResourceImageProps;
114
115
  topics: string[];
@@ -184,6 +185,7 @@ const ResourceDescription = ({ description, loading }: ResourceDescriptionProps)
184
185
 
185
186
  const ListResource = ({
186
187
  link,
188
+ tagLinkPrefix,
187
189
  title,
188
190
  tags,
189
191
  resourceImage,
@@ -205,7 +207,7 @@ const ListResource = ({
205
207
  </TopicAndTitleWrapper>
206
208
  {showDescription && <ResourceDescription description={description} loading={isLoading} />}
207
209
  <TagsandActionMenu>
208
- {tags && <CompressedTagList tags={tags} />}
210
+ {tags && <CompressedTagList tagLinkPrefix={tagLinkPrefix} tags={tags} />}
209
211
  {menuItems && menuItems.length > 0 && <MenuButton alignRight size="small" menuItems={menuItems} />}
210
212
  </TagsandActionMenu>
211
213
  </ResourceWrapper>
@@ -6,11 +6,13 @@
6
6
  *
7
7
  */
8
8
 
9
- import React from 'react';
10
9
  import styled from '@emotion/styled';
11
- import { fonts, colors, spacing } from '@ndla/core';
10
+ import { colors, fonts, spacing } from '@ndla/core';
11
+ import React, { MouseEvent } from 'react';
12
12
 
13
13
  import { MenuButton } from '@ndla/button';
14
+ import SafeLink from '@ndla/safelink';
15
+ import { useNavigate } from 'react-router-dom';
14
16
 
15
17
  export interface ResourceImageProps {
16
18
  alt: string;
@@ -42,9 +44,13 @@ const StyledTagList = styled.ul`
42
44
  `;
43
45
 
44
46
  const StyledTagListElement = styled.li`
45
- color: ${colors.brand.grey};
46
47
  margin: 0;
47
48
  ${fonts.sizes(14)};
49
+ `;
50
+
51
+ const StyledSafeLink = styled(SafeLink)`
52
+ box-shadow: none;
53
+ color: ${colors.brand.grey};
48
54
  ::before {
49
55
  content: '#';
50
56
  }
@@ -90,14 +96,20 @@ const TagCounterWrapper = styled.p`
90
96
 
91
97
  interface TagListProps {
92
98
  tags?: string[];
99
+ tagLinkPrefix?: string;
93
100
  }
94
-
95
- export const TagList = ({ tags }: TagListProps) => {
101
+ export const TagList = ({ tags, tagLinkPrefix }: TagListProps) => {
96
102
  if (!tags) return null;
97
103
  return (
98
104
  <StyledTagList>
99
105
  {tags.map((tag, i) => (
100
- <StyledTagListElement key={`tag-${i}`}>{tag}</StyledTagListElement>
106
+ <StyledTagListElement key={`tag-${i}`}>
107
+ <StyledSafeLink
108
+ onClick={(e: MouseEvent<HTMLAnchorElement | HTMLElement>) => e.stopPropagation()}
109
+ to={`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`}>
110
+ {tag}
111
+ </StyledSafeLink>
112
+ </StyledTagListElement>
101
113
  ))}
102
114
  </StyledTagList>
103
115
  );
@@ -105,20 +117,24 @@ export const TagList = ({ tags }: TagListProps) => {
105
117
 
106
118
  interface CompressedTagListProps {
107
119
  tags: string[];
120
+ tagLinkPrefix?: string;
108
121
  }
109
122
 
110
- export const CompressedTagList = ({ tags }: CompressedTagListProps) => {
123
+ export const CompressedTagList = ({ tags, tagLinkPrefix }: CompressedTagListProps) => {
124
+ const navigate = useNavigate();
111
125
  const visibleTags = tags.slice(0, 3);
112
126
  const remainingTags = tags.slice(3, tags.length).map((tag) => {
113
127
  return {
114
128
  text: '#' + tag,
115
- onClick: () => {},
129
+ onClick: () => {
130
+ navigate(`${tagLinkPrefix ? tagLinkPrefix : ''}/${tag}`);
131
+ },
116
132
  };
117
133
  });
118
134
 
119
135
  return (
120
136
  <>
121
- <TagList tags={visibleTags} />
137
+ <TagList tagLinkPrefix={tagLinkPrefix} tags={visibleTags} />
122
138
  {remainingTags.length > 0 && (
123
139
  <MenuButton hideMenuIcon={true} menuItems={remainingTags}>
124
140
  <TagCounterWrapper>{`+${remainingTags.length}`}</TagCounterWrapper>
@@ -66,6 +66,9 @@ const ResourcesTopicTitle = ({
66
66
  } else {
67
67
  heading = <h1 {...classes('topic-title', 'single')}>{messages.label}</h1>;
68
68
  }
69
+
70
+ const tooltipId = 'popupDialogTooltip';
71
+
69
72
  return (
70
73
  <header {...classes('topic-title-wrapper', { invertedStyle })}>
71
74
  <div>
@@ -82,6 +85,7 @@ const ResourcesTopicTitle = ({
82
85
  css={invertedStyle ? invertedSwitchCSS : switchCSS}
83
86
  />
84
87
  <Modal
88
+ labelledBy={tooltipId}
85
89
  narrow
86
90
  wrapperFunctionForButton={(activateButton: ReactNode) => (
87
91
  <TooltipWrapper>
@@ -89,7 +93,7 @@ const ResourcesTopicTitle = ({
89
93
  </TooltipWrapper>
90
94
  )}
91
95
  activateButton={
92
- <TooltipButton aria-label={t('resource.dialogTooltip')}>
96
+ <TooltipButton id={tooltipId} aria-label={t('resource.dialogTooltip')}>
93
97
  <HelpIcon invertedStyle={invertedStyle} />
94
98
  </TooltipButton>
95
99
  }>
@@ -92,9 +92,11 @@ const PopupFilter = ({
92
92
  }: PopupFilterProps) => {
93
93
  const { t } = useTranslation();
94
94
  const [selectedMenu, setSelectedMenu] = useState(MENU_ALL_SUBJECTS);
95
+ const headingId = 'popupFilterHeading';
95
96
 
96
97
  return (
97
98
  <Modal
99
+ labelledBy={headingId}
98
100
  controllable
99
101
  backgroundColor="white"
100
102
  animation="subtle"
@@ -107,7 +109,7 @@ const PopupFilter = ({
107
109
  <ModalWrapper>
108
110
  <ModalContent>
109
111
  <ModalHeaderWrapper>
110
- <ModalHeading>{t('searchPage.searchFilterMessages.filterLabel')}</ModalHeading>
112
+ <ModalHeading id={headingId}>{t('searchPage.searchFilterMessages.filterLabel')}</ModalHeading>
111
113
  <ModalCloseButton onClick={() => onClose()} title={t('searchPage.close')} />
112
114
  </ModalHeaderWrapper>
113
115
  {subjectCategories && programmes && (
@@ -98,6 +98,7 @@ const ItemContexts = ({ contexts, id, title }: ItemContextsType) => {
98
98
  &nbsp;
99
99
  {contexts.length > 1 && (
100
100
  <Modal
101
+ label={t('searchPage.contextModal.ariaLabel')}
101
102
  activateButton={
102
103
  <ModalButton link>
103
104
  {t('searchPage.contextModal.button', {
@@ -268,6 +268,7 @@ const Topic = ({
268
268
  {topic.visualElement ? (
269
269
  <>
270
270
  <Modal
271
+ label={t('topicPage.imageModal')}
271
272
  activateButton={
272
273
  <VisualElementButton
273
274
  stripped
@@ -49,19 +49,22 @@ const WrapperForFolderChild = styled.div`
49
49
  gap: ${spacing.xsmall};
50
50
  `;
51
51
 
52
- const shouldForwardProp = (name: string) => !['selected', 'noArrow'].includes(name);
52
+ const shouldForwardProp = (name: string) => !['selected', 'noArrow', 'fullWidth'].includes(name);
53
53
 
54
54
  interface FolderNameProps {
55
55
  selected?: boolean;
56
56
  noArrow?: boolean;
57
+ fullWidth?: boolean;
57
58
  }
58
59
 
59
60
  const FolderName = styled('button', { shouldForwardProp })<FolderNameProps>`
60
61
  cursor: pointer;
61
62
  padding: ${spacing.xsmall};
62
63
  margin: 0;
64
+ outline-offset: -2px;
65
+ outline-color: ${colors.brand.primary};
63
66
  margin-left: ${({ noArrow }) => (noArrow ? `29px` : `0px`)};
64
- flex-grow: 1;
67
+ flex-grow: ${({ fullWidth }) => fullWidth && 1};
65
68
  display: grid;
66
69
  grid-template-columns: auto 1fr auto;
67
70
  align-items: center;
@@ -116,6 +119,7 @@ const FolderItem = ({
116
119
  setSelectedFolder,
117
120
  targetResource,
118
121
  visibleFolders,
122
+ framed,
119
123
  }: Props) => {
120
124
  const { t } = useTranslation();
121
125
  const { id, icon, name } = folder;
@@ -173,6 +177,7 @@ const FolderItem = ({
173
177
  {onSelectFolder ? (
174
178
  <>
175
179
  <FolderName
180
+ fullWidth={framed}
176
181
  ref={ref}
177
182
  onKeyDown={(e) => arrowNavigation(e, id, visibleFolders, setFocusedId, onOpenFolder, onCloseFolder)}
178
183
  noArrow={hideArrow && !noPaddingWhenArrowIsHidden}
@@ -34,7 +34,6 @@ export interface TreeStructureProps extends CommonTreeStructureProps {
34
34
  defaultOpenFolders?: string[];
35
35
  folders: FolderType[];
36
36
  editable?: boolean;
37
- framed?: boolean;
38
37
  label?: string;
39
38
  maximumLevelsOfFoldersAllowed?: number;
40
39
  onNewFolder?: (name: string, parentId: string) => Promise<IFolder>;
@@ -118,6 +117,7 @@ const TreeStructure = ({
118
117
  onNewFolder?.(name, parentId).then((newFolder) => {
119
118
  if (newFolder) {
120
119
  setSelectedFolder(newFolder);
120
+ onSelectFolder?.(newFolder.id);
121
121
  setFocusedId(newFolder.id);
122
122
  setOpenFolders(uniq(openFolders.concat(parentId)));
123
123
  }
@@ -156,6 +156,7 @@ const TreeStructure = ({
156
156
  setSelectedFolder={setSelectedFolder}
157
157
  targetResource={targetResource}
158
158
  visibleFolders={visibleFolderIds}
159
+ framed={framed}
159
160
  />
160
161
  </TreeStructureStyledWrapper>
161
162
  {editable && (
@@ -13,18 +13,15 @@ import { colors, misc, spacing } from '@ndla/core';
13
13
  const TreeStructureWrapper = styled.div<{ framed?: boolean }>`
14
14
  padding: ${spacing.xsmall};
15
15
  ${({ framed }) =>
16
- framed
17
- ? css`
18
- border: 1px solid ${colors.brand.neutral7};
19
- border-radius: ${misc.borderRadius};
20
- max-height: 400px;
21
- overflow-y: scroll;
22
- scroll-behavior: smooth;
23
- padding: ${spacing.small};
24
- `
25
- : css`
26
- margin-left: -${spacing.medium};
27
- `}
16
+ framed &&
17
+ css`
18
+ border: 1px solid ${colors.brand.neutral7};
19
+ border-radius: ${misc.borderRadius};
20
+ max-height: 400px;
21
+ overflow-y: scroll;
22
+ scroll-behavior: smooth;
23
+ padding: ${spacing.small};
24
+ `}
28
25
  transition: ${misc.transition.default};
29
26
  &:focus-within {
30
27
  border-color: ${colors.brand.primary};