@ndla/ui 23.0.0 → 24.1.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.
Files changed (148) hide show
  1. package/es/Article/ArticleAuthorContent.js +2 -4
  2. package/es/Article/ArticleFavoritesButton.js +2 -2
  3. package/es/AuthorInfo/AuthorInfo.js +29 -16
  4. package/es/ContentCard/ContentCard.js +66 -25
  5. package/es/FileList/File.js +34 -8
  6. package/es/FileList/FileList.js +29 -3
  7. package/es/InfoBox/InfoBox.js +10 -3
  8. package/es/InfoWidget/InfoWidget.js +67 -22
  9. package/es/MyNdla/Resource/FolderInput.js +19 -6
  10. package/es/NoContentBox/NoContentBox.js +1 -6
  11. package/es/Portrait/Portrait.js +19 -13
  12. package/es/Resource/BlockResource.js +7 -6
  13. package/es/Resource/ListResource.js +8 -7
  14. package/es/Search/ActiveFilterContent.js +4 -14
  15. package/es/Search/ActiveFilters.js +8 -19
  16. package/es/Search/SearchField.js +31 -52
  17. package/es/Search/SearchResult.js +113 -136
  18. package/es/Search/ToggleSearchButton.js +34 -43
  19. package/es/Search/index.js +2 -8
  20. package/es/TagSelector/SuggestionInput.js +9 -19
  21. package/es/TreeStructure/FolderItems.js +3 -3
  22. package/es/TreeStructure/FolderNameInput.js +33 -14
  23. package/es/TreeStructure/TreeStructure.js +3 -2
  24. package/es/all.css +1 -1
  25. package/es/index-javascript.js +0 -1
  26. package/es/index.js +2 -1
  27. package/es/locale/messages-en.js +11 -5
  28. package/es/locale/messages-nb.js +9 -3
  29. package/es/locale/messages-nn.js +11 -5
  30. package/es/locale/messages-se.js +29 -23
  31. package/es/locale/messages-sma.js +43 -37
  32. package/lib/Article/ArticleAuthorContent.js +9 -4
  33. package/lib/Article/ArticleFavoritesButton.js +2 -2
  34. package/lib/AuthorInfo/AuthorInfo.d.ts +1 -11
  35. package/lib/AuthorInfo/AuthorInfo.js +36 -20
  36. package/lib/ContentCard/ContentCard.d.ts +1 -15
  37. package/lib/ContentCard/ContentCard.js +60 -28
  38. package/lib/FileList/File.js +36 -12
  39. package/lib/FileList/FileList.js +28 -5
  40. package/lib/InfoBox/InfoBox.js +12 -5
  41. package/lib/InfoWidget/InfoWidget.js +61 -25
  42. package/lib/MediaList/MediaList.d.ts +1 -1
  43. package/lib/MyNdla/Resource/FolderInput.js +18 -5
  44. package/lib/NoContentBox/NoContentBox.js +1 -8
  45. package/lib/Portrait/Portrait.js +19 -14
  46. package/lib/Resource/BlockResource.js +7 -6
  47. package/lib/Resource/ListResource.js +8 -7
  48. package/lib/Search/ActiveFilterContent.d.ts +13 -0
  49. package/lib/Search/ActiveFilterContent.js +4 -15
  50. package/lib/Search/ActiveFilters.d.ts +13 -0
  51. package/lib/Search/ActiveFilters.js +8 -20
  52. package/lib/Search/SearchField.d.ts +19 -0
  53. package/lib/Search/SearchField.js +32 -56
  54. package/lib/Search/SearchResult.d.ts +36 -0
  55. package/lib/Search/SearchResult.js +116 -159
  56. package/lib/Search/ToggleSearchButton.d.ts +16 -0
  57. package/lib/Search/ToggleSearchButton.js +36 -46
  58. package/lib/Search/index.d.ts +12 -0
  59. package/lib/Search/index.js +0 -54
  60. package/lib/SearchTypeResult/SearchTypeHeader.d.ts +1 -1
  61. package/lib/TagSelector/SuggestionInput.js +9 -19
  62. package/lib/TreeStructure/FolderItems.js +3 -3
  63. package/lib/TreeStructure/FolderNameInput.js +35 -14
  64. package/lib/TreeStructure/TreeStructure.js +3 -2
  65. package/lib/TreeStructure/types.d.ts +1 -1
  66. package/lib/all.css +1 -1
  67. package/lib/index-javascript.js +0 -74
  68. package/lib/index.d.ts +1 -0
  69. package/lib/index.js +38 -1
  70. package/lib/locale/messages-en.d.ts +7 -1
  71. package/lib/locale/messages-en.js +11 -5
  72. package/lib/locale/messages-nb.d.ts +6 -0
  73. package/lib/locale/messages-nb.js +9 -3
  74. package/lib/locale/messages-nn.d.ts +7 -1
  75. package/lib/locale/messages-nn.js +11 -5
  76. package/lib/locale/messages-se.d.ts +7 -1
  77. package/lib/locale/messages-se.js +29 -23
  78. package/lib/locale/messages-sma.d.ts +12 -6
  79. package/lib/locale/messages-sma.js +43 -37
  80. package/package.json +10 -10
  81. package/src/Article/ArticleAuthorContent.tsx +1 -1
  82. package/src/Article/ArticleFavoritesButton.tsx +2 -2
  83. package/src/AuthorInfo/AuthorInfo.tsx +53 -19
  84. package/src/ContentCard/ContentCard.tsx +127 -35
  85. package/src/FileList/File.tsx +47 -17
  86. package/src/FileList/FileList.tsx +37 -8
  87. package/src/InfoBox/InfoBox.tsx +24 -4
  88. package/src/InfoWidget/InfoWidget.tsx +83 -34
  89. package/src/MediaList/MediaList.tsx +1 -1
  90. package/src/MyNdla/Resource/FolderInput.tsx +18 -3
  91. package/src/NoContentBox/NoContentBox.tsx +2 -7
  92. package/src/Portrait/Portrait.tsx +25 -10
  93. package/src/Resource/BlockResource.tsx +1 -1
  94. package/src/Resource/ListResource.tsx +3 -1
  95. package/src/Search/{ActiveFilterContent.jsx → ActiveFilterContent.tsx} +11 -12
  96. package/src/Search/{ActiveFilters.jsx → ActiveFilters.tsx} +20 -17
  97. package/src/Search/{SearchField.jsx → SearchField.tsx} +58 -68
  98. package/src/Search/SearchResult.tsx +360 -0
  99. package/src/Search/ToggleSearchButton.tsx +73 -0
  100. package/src/Search/component.search.scss +0 -4
  101. package/src/Search/index.ts +16 -0
  102. package/src/SectionHeading/SectionHeading.tsx +1 -0
  103. package/src/TagSelector/SuggestionInput.tsx +0 -9
  104. package/src/TreeStructure/FolderItems.tsx +1 -1
  105. package/src/TreeStructure/FolderNameInput.tsx +34 -9
  106. package/src/TreeStructure/TreeStructure.tsx +1 -0
  107. package/src/TreeStructure/types.ts +1 -1
  108. package/src/all.scss +0 -1
  109. package/src/index-javascript.js +0 -15
  110. package/src/index.ts +2 -0
  111. package/src/locale/messages-en.ts +10 -4
  112. package/src/locale/messages-nb.ts +9 -3
  113. package/src/locale/messages-nn.ts +10 -4
  114. package/src/locale/messages-se.ts +29 -23
  115. package/src/locale/messages-sma.ts +41 -35
  116. package/src/main.scss +0 -7
  117. package/es/Search/SearchFilter.js +0 -72
  118. package/es/Search/SearchFilterList.js +0 -115
  119. package/es/Search/SearchOverlay.js +0 -39
  120. package/es/Search/SearchPage.js +0 -178
  121. package/es/Search/SearchPopoverFilter.js +0 -152
  122. package/es/Search/SearchResultAuthor.js +0 -51
  123. package/lib/Search/SearchFilter.js +0 -88
  124. package/lib/Search/SearchFilterList.js +0 -137
  125. package/lib/Search/SearchOverlay.js +0 -62
  126. package/lib/Search/SearchPage.js +0 -207
  127. package/lib/Search/SearchPopoverFilter.js +0 -172
  128. package/lib/Search/SearchResultAuthor.js +0 -60
  129. package/src/AuthorInfo/component.author-info.scss +0 -54
  130. package/src/ContentCard/component.content-card.scss +0 -109
  131. package/src/FileList/component.file-list.scss +0 -102
  132. package/src/InfoBox/component.info-box.scss +0 -21
  133. package/src/InfoWidget/component.info-widget.scss +0 -52
  134. package/src/NoContentBox/component.no-content-box.scss +0 -17
  135. package/src/Portrait/component.portrait.scss +0 -29
  136. package/src/Search/SearchFilter.jsx +0 -82
  137. package/src/Search/SearchFilterList.jsx +0 -110
  138. package/src/Search/SearchOverlay.jsx +0 -38
  139. package/src/Search/SearchPage.jsx +0 -178
  140. package/src/Search/SearchPopoverFilter.jsx +0 -109
  141. package/src/Search/SearchResult.jsx +0 -239
  142. package/src/Search/SearchResultAuthor.jsx +0 -54
  143. package/src/Search/ToggleSearchButton.jsx +0 -64
  144. package/src/Search/component.search-filter.scss +0 -67
  145. package/src/Search/component.search-overlay.scss +0 -103
  146. package/src/Search/component.search-page.scss +0 -125
  147. package/src/Search/component.search-result-author.scss +0 -65
  148. package/src/Search/index.js +0 -34
@@ -6,27 +6,30 @@
6
6
  *
7
7
  */
8
8
 
9
- import React from 'react';
10
- import PropTypes from 'prop-types';
9
+ import React, { FocusEvent, MouseEvent, RefObject } from 'react';
10
+ import { useTranslation } from 'react-i18next';
11
11
  import BEMHelper from 'react-bem-helper';
12
- import { Search as SearchIcon } from '@ndla/icons/common';
13
12
  import { css } from '@emotion/core';
13
+ import styled from '@emotion/styled';
14
+ import { Search as SearchIcon } from '@ndla/icons/common';
14
15
  import { colors, spacing, mq, breakpoints, misc, fonts } from '@ndla/core';
15
- import { useTranslation } from 'react-i18next';
16
16
 
17
17
  import ActiveFilters from './ActiveFilters';
18
18
  import LoadingWrapper from './LoadingWrapper';
19
19
 
20
- import { ContentTypeResultShape } from '../shapes';
21
-
22
20
  const classes = new BEMHelper('c-search-field');
23
21
 
24
- const inputStyle = (frontPageSearch) => css`
22
+ interface StyledInputProps {
23
+ frontPageSearch?: boolean;
24
+ hasFilters?: boolean;
25
+ }
26
+
27
+ const StyledInput = styled.input<StyledInputProps>`
25
28
  width: 100%;
26
29
  height: 48px;
27
30
  line-height: 28px;
28
31
  border: 1px solid ${colors.brand.greyLight};
29
- border-radius: ${frontPageSearch ? '100px' : misc.borderRadius};
32
+ border-radius: ${(p) => (p.frontPageSearch ? '100px' : misc.borderRadius)};
30
33
  padding-right: ${spacing.large};
31
34
  padding-left: ${spacing.normal};
32
35
  flex-grow: 1;
@@ -41,61 +44,71 @@ const inputStyle = (frontPageSearch) => css`
41
44
  line-height: 58px;
42
45
  ${fonts.sizes('18px', '24px')};
43
46
  }
44
- `;
45
47
 
46
- const filterStyle = css`
47
- ${mq.range({ from: breakpoints.desktop })} {
48
- padding-left: ${spacing.normal};
49
- }
50
- padding-left: 0;
51
- border-left: 0;
52
- border-top-left-radius: 0;
53
- border-bottom-left-radius: 0;
48
+ ${(p) =>
49
+ p.hasFilters &&
50
+ css`
51
+ ${mq.range({ from: breakpoints.desktop })} {
52
+ padding-left: ${spacing.normal};
53
+ }
54
+ padding-left: 0;
55
+ border-left: 0;
56
+ border-top-left-radius: 0;
57
+ border-bottom-left-radius: 0;
54
58
 
55
- &:focus {
56
- border: 1px solid ${colors.brand.primary};
57
- border-left: 0;
59
+ &:focus {
60
+ border: 1px solid ${colors.brand.primary};
61
+ border-left: 0;
58
62
 
59
- & + .c-search-field__filters {
60
- border: 1px solid ${colors.brand.primary};
61
- border-right: 0;
62
- }
63
- }
63
+ & + .c-search-field__filters {
64
+ border: 1px solid ${colors.brand.primary};
65
+ border-right: 0;
66
+ }
67
+ }
68
+ `};
64
69
  `;
65
70
 
71
+ interface Props {
72
+ value: string;
73
+ placeholder: string;
74
+ onChange: (value: string) => void;
75
+ filters?: { value: string; title: string }[];
76
+ onFilterRemove?: (value: string, filterName?: string) => void;
77
+ onFocus?: (event?: FocusEvent<HTMLInputElement>) => void;
78
+ onBlur?: () => void;
79
+ onClick?: (event: MouseEvent<HTMLInputElement>) => void;
80
+ loading?: boolean;
81
+ inputRef?: RefObject<HTMLInputElement>;
82
+ frontPageSearch?: boolean;
83
+ }
84
+
66
85
  const SearchField = ({
67
86
  placeholder,
68
87
  value,
69
88
  onChange,
70
89
  filters,
71
- small,
72
90
  onClick,
73
- onFocus = () => {},
74
- onBlur = () => {},
91
+ onFocus,
92
+ onBlur,
75
93
  loading,
76
94
  onFilterRemove,
77
95
  inputRef,
78
96
  frontPageSearch = false,
79
- }) => {
97
+ }: Props) => {
80
98
  const { t } = useTranslation();
81
- const handleOnFilterRemove = (value, filterName) => {
82
- onFilterRemove(value, filterName);
83
- if (inputRef) {
84
- inputRef.current.focus();
85
- }
86
- onFocus();
99
+ const handleOnFilterRemove = (value: string, filterName?: string) => {
100
+ onFilterRemove?.(value, filterName);
101
+ inputRef?.current?.focus();
102
+ onFocus?.();
87
103
  };
88
- const hasFilters = filters && filters.length > 0;
89
104
  return (
90
105
  <div {...classes('input-wrapper')}>
91
106
  {loading && <LoadingWrapper value={value} />}
92
- <input
107
+ <StyledInput
108
+ frontPageSearch={frontPageSearch}
109
+ hasFilters={!!filters?.length}
93
110
  ref={inputRef}
94
111
  type="search"
95
- css={css`
96
- ${inputStyle(frontPageSearch)};
97
- ${hasFilters && filterStyle};
98
- `}
99
112
  aria-autocomplete="list"
100
113
  autoComplete="off"
101
114
  id="search"
@@ -108,7 +121,7 @@ const SearchField = ({
108
121
  onFocus={onFocus}
109
122
  onClick={onClick}
110
123
  />
111
- {hasFilters && (
124
+ {filters && filters.length > 0 && (
112
125
  <div {...classes('filters')}>
113
126
  <ActiveFilters filters={filters} onFilterRemove={handleOnFilterRemove} />
114
127
  </div>
@@ -119,41 +132,18 @@ const SearchField = ({
119
132
  type="button"
120
133
  onClick={() => {
121
134
  onChange('');
122
- onFocus();
123
- if (inputRef) {
124
- inputRef.current.focus();
125
- }
135
+ onFocus?.();
136
+ inputRef?.current?.focus();
126
137
  }}
127
138
  onBlur={onBlur}>
128
139
  {t('welcomePage.resetSearch')}
129
140
  </button>
130
141
  )}
131
- <button tabIndex="-1" {...classes('button', 'searchIcon')} type="submit" value="Search">
142
+ <button tabIndex={-1} {...classes('button', 'searchIcon')} type="submit" value="Search">
132
143
  <SearchIcon />
133
144
  </button>
134
145
  </div>
135
146
  );
136
147
  };
137
148
 
138
- SearchField.propTypes = {
139
- value: PropTypes.string.isRequired,
140
- placeholder: PropTypes.string.isRequired,
141
- onChange: PropTypes.func.isRequired,
142
- filters: PropTypes.arrayOf(
143
- PropTypes.shape({
144
- value: PropTypes.string.isRequired,
145
- title: PropTypes.string.isRequired,
146
- }),
147
- ),
148
- searchResult: PropTypes.arrayOf(ContentTypeResultShape),
149
- allResultUrl: PropTypes.string,
150
- onFilterRemove: PropTypes.func,
151
- small: PropTypes.bool,
152
- onNavigate: PropTypes.func,
153
- onFocus: PropTypes.func,
154
- onBlur: PropTypes.func,
155
- onClick: PropTypes.func,
156
- loading: PropTypes.bool,
157
- };
158
-
159
149
  export default SearchField;
@@ -0,0 +1,360 @@
1
+ import React, { ElementType, Fragment, ReactNode } from 'react';
2
+ import { ChevronRight } from '@ndla/icons/common';
3
+ import { uuid } from '@ndla/util';
4
+ import { useTranslation } from 'react-i18next';
5
+ import Tooltip from '@ndla/tooltip';
6
+ import SafeLink from '@ndla/safelink';
7
+ import { Spinner } from '@ndla/icons';
8
+ import styled from '@emotion/styled';
9
+ import { fonts, mq, spacing, breakpoints, colors, spacingUnit } from '@ndla/core';
10
+
11
+ interface ItemType {
12
+ id: string | number;
13
+ title?: string;
14
+ url?: string | { href: string };
15
+ breadcrumb?: string[];
16
+ subjects?: {
17
+ title: string;
18
+ url?: string | { href: string };
19
+ breadcrumb?: string[];
20
+ }[];
21
+ additional?: boolean;
22
+ image?: ReactNode;
23
+ ingress: string;
24
+ contentTypeIcon?: string;
25
+ contentTypeLabel?: string;
26
+ children?: ReactNode;
27
+ type?: string;
28
+ }
29
+
30
+ interface SearchResultItemProps {
31
+ children?: ReactNode;
32
+ subjectsLabel: string;
33
+ additionalContentTooltip: string;
34
+ item: ItemType;
35
+ }
36
+
37
+ const StyledHeader = styled.header`
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ align-items: center;
41
+
42
+ > * {
43
+ margin: 0 ${spacing.small} ${spacing.small} 0;
44
+ }
45
+
46
+ ${mq.range({ until: breakpoints.tablet })} {
47
+ h1 {
48
+ width: 100%;
49
+ }
50
+ }
51
+ `;
52
+
53
+ const ContentTypeWrapper = styled.div`
54
+ margin: 0 ${spacing.small} ${spacing.small} 0;
55
+ display: flex;
56
+ align-items: center;
57
+ `;
58
+
59
+ const PillsWrapper = styled.div`
60
+ background: ${colors.brand.greyLightest};
61
+ margin-right: ${spacing.small};
62
+ padding: 0 ${spacingUnit / 3}px;
63
+ border-radius: 50%;
64
+ ${fonts.sizes('12px', '20px')};
65
+ font-weight: ${fonts.weight.semibold};
66
+ `;
67
+
68
+ const StyledContent = styled.div`
69
+ display: flex;
70
+ align-items: flex-start;
71
+ justify-content: space-between;
72
+ & > img,
73
+ & > picture > img {
74
+ width: 75px;
75
+ height: auto;
76
+ flex-shrink: 0;
77
+ margin: 0 0 ${spacing.small} ${spacing.small};
78
+
79
+ ${mq.range({ from: breakpoints.tabletWide })} {
80
+ width: 80px;
81
+ margin-left: ${spacing.normal};
82
+ margin-right: ${spacing.small};
83
+ }
84
+ }
85
+ `;
86
+
87
+ const StyledIngress = styled.p`
88
+ margin: 0 0 ${spacing.normal} 0;
89
+ width: 100%;
90
+ flex-grow: 1;
91
+ ${fonts.sizes('14px', '22px')};
92
+
93
+ ${mq.range({ from: breakpoints.tablet })} {
94
+ ${fonts.sizes('16px', '24px')};
95
+ max-width: 550px;
96
+ }
97
+
98
+ ${mq.range({ from: breakpoints.desktop })} {
99
+ ${fonts.sizes('18px', '26px')};
100
+ max-width: 600px;
101
+ }
102
+ `;
103
+
104
+ const StyledSubjects = styled.div`
105
+ display: flex;
106
+ align-items: center;
107
+ flex-wrap: wrap;
108
+ margin-bottom: ${spacing.normal};
109
+ ${fonts.sizes('14px', '16px')};
110
+
111
+ ${mq.range({ from: breakpoints.desktop })} {
112
+ ${fonts.sizes('16px', '18px')};
113
+ }
114
+
115
+ & > span {
116
+ text-transform: uppercase;
117
+ color: ${colors.text.light};
118
+ padding-right: ${spacing.small};
119
+ ${fonts.sizes('14px', '16px')};
120
+ }
121
+
122
+ ul {
123
+ display: flex;
124
+ align-items: center;
125
+ flex-wrap: wrap;
126
+ list-style: none;
127
+ margin: 0;
128
+ padding: 0;
129
+ }
130
+
131
+ li {
132
+ margin: 0;
133
+ position: relative;
134
+ line-height: 1rem;
135
+
136
+ padding: 0 ${spacing.xsmall} 0 0;
137
+ margin-right: ${spacing.xsmall};
138
+
139
+ ${mq.range({ from: breakpoints.desktop })} {
140
+ padding: 0 ${spacing.small} 0 0;
141
+ margin-right: ${spacing.small};
142
+ }
143
+
144
+ &::after {
145
+ content: '';
146
+ width: 1px;
147
+ height: 15px;
148
+ background: ${colors.brand.greyLight};
149
+ display: block;
150
+ position: absolute;
151
+ right: 0;
152
+ top: 3px;
153
+ }
154
+ &:first-child {
155
+ font-weight: 600;
156
+ }
157
+
158
+ &:last-child {
159
+ &:after {
160
+ display: none;
161
+ }
162
+
163
+ margin-right: 0;
164
+ }
165
+ }
166
+ `;
167
+
168
+ const StyledSearchResultItem = styled.li`
169
+ border-bottom: 1px solid ${colors.brand.greyLight};
170
+ padding: 0 ${spacing.normal} 0 ${spacing.normal};
171
+
172
+ ${mq.range({ from: breakpoints.desktop })} {
173
+ padding: 0;
174
+ }
175
+
176
+ margin: 0 0 ${spacing.normal};
177
+
178
+ &:first-child {
179
+ margin-top: ${spacing.normal};
180
+ }
181
+
182
+ &:last-child {
183
+ border-bottom: 0;
184
+ margin-bottom: 0;
185
+ }
186
+
187
+ h1 {
188
+ font-weight: 600;
189
+ ${fonts.sizes('16px', '20px')};
190
+
191
+ ${mq.range({ from: breakpoints.desktop })} {
192
+ ${fonts.sizes('22px', '28px')};
193
+ }
194
+
195
+ a {
196
+ color: ${colors.brand.dark};
197
+
198
+ &:hover,
199
+ &:focus,
200
+ &:active {
201
+ color: ${colors.brand.dark};
202
+ }
203
+ }
204
+ }
205
+ `;
206
+
207
+ const StyledBreadcrumb = styled.div`
208
+ list-style: none;
209
+ display: flex;
210
+ align-items: center;
211
+ flex-wrap: wrap;
212
+ color: ${colors.text.light};
213
+ margin-bottom: ${spacing.normal};
214
+ ${fonts.sizes('14px', '18px')};
215
+ ${mq.range({ from: breakpoints.desktop })} {
216
+ ${fonts.sizes('16px', '24px')};
217
+ }
218
+ .c-icon {
219
+ width: 12px;
220
+ height: 12px;
221
+ margin: 0 ${spacingUnit / 3}px;
222
+
223
+ ${mq.range({ from: breakpoints.desktop })} {
224
+ margin: 0 ${spacing.xsmall};
225
+ }
226
+ }
227
+ `;
228
+
229
+ export const SearchResultItem = ({
230
+ item,
231
+ subjectsLabel,
232
+ additionalContentTooltip,
233
+ children,
234
+ }: SearchResultItemProps) => {
235
+ const itemBreadcrumb = (breadcrumbs: string[] = [], itemBreadcrumb?: boolean) => {
236
+ const Breadcrumb: ElementType = itemBreadcrumb ? StyledBreadcrumb : 'div';
237
+ if (breadcrumbs.length > 0) {
238
+ return (
239
+ <Breadcrumb>
240
+ {breadcrumbs.map((breadcrumbItem, index) => {
241
+ let icon = null;
242
+ if (index !== (item.breadcrumb?.length || 0) - 1) {
243
+ icon = <ChevronRight />;
244
+ }
245
+ return (
246
+ <Fragment key={uuid()}>
247
+ <span>{breadcrumbItem}</span>
248
+ {icon}
249
+ </Fragment>
250
+ );
251
+ })}
252
+ </Breadcrumb>
253
+ );
254
+ }
255
+ };
256
+ return (
257
+ <StyledSearchResultItem key={item.id}>
258
+ <article>
259
+ <StyledHeader>
260
+ <h1>
261
+ {item.url && typeof item.url !== 'string' ? (
262
+ <a {...item.url}>{item.title}</a>
263
+ ) : (
264
+ <SafeLink to={item.url ?? ''}>{item.title}</SafeLink>
265
+ )}
266
+ </h1>
267
+ <ContentTypeWrapper>{item.contentTypeIcon}</ContentTypeWrapper>
268
+ {item.contentTypeLabel && <PillsWrapper>{item.contentTypeLabel}</PillsWrapper>}
269
+ {item.type && <PillsWrapper>{item.type}</PillsWrapper>}
270
+ {item.additional && <PillsWrapper>{additionalContentTooltip}</PillsWrapper>}
271
+ {children}
272
+ </StyledHeader>
273
+ <StyledContent>
274
+ <StyledIngress dangerouslySetInnerHTML={{ __html: item.ingress }} />
275
+ {item.image}
276
+ </StyledContent>
277
+ {(!item.subjects || item.subjects.length === 0) && itemBreadcrumb(item.breadcrumb, true)}
278
+ {item.subjects && item.subjects.length !== 0 && (
279
+ <StyledSubjects>
280
+ <span>{subjectsLabel}</span>
281
+ <ul>
282
+ {item.subjects.map((subject) => (
283
+ <li key={uuid()}>
284
+ <Tooltip tooltip={itemBreadcrumb(subject.breadcrumb)}>
285
+ {subject.url && typeof subject.url !== 'string' ? (
286
+ <a {...subject.url}>{subject.title}</a>
287
+ ) : (
288
+ <SafeLink to={subject.url ?? ''}>{subject.title}</SafeLink>
289
+ )}
290
+ </Tooltip>
291
+ </li>
292
+ ))}
293
+ </ul>
294
+ </StyledSubjects>
295
+ )}
296
+ </article>
297
+ </StyledSearchResultItem>
298
+ );
299
+ };
300
+
301
+ interface SearchResultListProps {
302
+ loading?: boolean;
303
+ results?: ItemType[];
304
+ }
305
+
306
+ const EmptyResultList = styled.article`
307
+ margin-top: ${spacing.large};
308
+ h1 {
309
+ ${fonts.sizes('22px', '32px')};
310
+ font-weight: ${fonts.weight.bold};
311
+ margin: 0 0 ${spacing.small};
312
+ }
313
+
314
+ p {
315
+ ${fonts.sizes('16px', '22px')};
316
+ margin: 0;
317
+ font-family: ${fonts.serif};
318
+
319
+ ${mq.range({ from: breakpoints.desktop })} {
320
+ ${fonts.sizes('18px', '32px')};
321
+ }
322
+ }
323
+ `;
324
+
325
+ const ResultList = styled.ul`
326
+ list-style: none;
327
+ margin: 0;
328
+ padding: 0;
329
+ ${mq.range({ from: breakpoints.desktop })} {
330
+ margin: ${spacing.medium} 0 0 0;
331
+ }
332
+ `;
333
+
334
+ export const SearchResultList = ({ results, loading }: SearchResultListProps) => {
335
+ const { t } = useTranslation();
336
+ if (loading) {
337
+ return <Spinner />;
338
+ }
339
+ if (!results) {
340
+ return <EmptyResultList />;
341
+ }
342
+ return results.length === 0 ? (
343
+ <EmptyResultList>
344
+ <h1>{t('searchPage.searchResultListMessages.noResultHeading')}</h1>
345
+ <p>{t('searchPage.searchResultListMessages.noResultDescription')}</p>
346
+ </EmptyResultList>
347
+ ) : (
348
+ <ResultList>
349
+ {results.map((item) => (
350
+ <SearchResultItem
351
+ key={`search_result_item_${typeof item.url === 'object' ? item.url.href : item.url}`}
352
+ item={item}
353
+ additionalContentTooltip={t('resource.tooltipAdditionalTopic')}
354
+ subjectsLabel={t('searchPage.searchResultListMessages.subjectsLabel')}>
355
+ {item.children}
356
+ </SearchResultItem>
357
+ ))}
358
+ </ResultList>
359
+ );
360
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright (c) 2018-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 from 'react';
10
+ import { spacing, spacingUnit, breakpoints, mq, misc, fonts, colors } from '@ndla/core';
11
+ import { Search } from '@ndla/icons/common';
12
+ import Button, { ButtonProps } from '@ndla/button';
13
+ import styled from '@emotion/styled';
14
+
15
+ interface Props extends ButtonProps {
16
+ hideOnNarrowScreen?: boolean;
17
+ hideOnWideScreen?: boolean;
18
+ ndlaFilm?: boolean;
19
+ }
20
+
21
+ interface StyledButtonProps {
22
+ hideOnNarrowScreen?: boolean;
23
+ hideOnWideScreen?: boolean;
24
+ ndlaFilm?: boolean;
25
+ }
26
+
27
+ const StyledButton = styled(Button)<StyledButtonProps>`
28
+ background: ${(p) => (p.ndlaFilm ? colors.ndlaFilm.filmColorBright : colors.brand.greyLighter)};
29
+ border-radius: ${misc.borderRadius};
30
+ border: 0;
31
+ display: ${(p) => (p.hideOnNarrowScreen ? 'none' : 'flex')};
32
+ color: ${(p) => (p.ndlaFilm ? '#fff' : colors.brand.primary)};
33
+ padding: ${spacing.small} ${spacingUnit * 0.75}px ${spacing.small} ${spacing.normal};
34
+ align-items: center;
35
+ margin-left: ${spacing.normal};
36
+
37
+ .c-icon {
38
+ height: 24px;
39
+ width: 24px;
40
+ }
41
+
42
+ ${fonts.sizes('16px', '32px')};
43
+
44
+ ${mq.range({ from: breakpoints.desktop })} {
45
+ display: ${(p) => (p.hideOnWideScreen ? 'none' : 'flex')};
46
+ margin-right: ${spacing.medium};
47
+ padding: ${spacing.small} ${spacing.normal};
48
+ }
49
+ &:hover,
50
+ &:focus,
51
+ &:active {
52
+ border: 0;
53
+ }
54
+ `;
55
+
56
+ const StyledSpan = styled.span`
57
+ margin-right: ${spacing.normal};
58
+ font-weight: ${fonts.weight.normal};
59
+ `;
60
+
61
+ const ToggleSearchButton = ({ children, ndlaFilm, hideOnNarrowScreen, hideOnWideScreen, ...rest }: Props) => (
62
+ <StyledButton
63
+ ndlaFilm={ndlaFilm}
64
+ hideOnNarrowScreen={hideOnNarrowScreen}
65
+ hideOnWideScreen={hideOnWideScreen}
66
+ type="button"
67
+ {...rest}>
68
+ <StyledSpan>{children}</StyledSpan>
69
+ <Search />
70
+ </StyledButton>
71
+ );
72
+
73
+ export default ToggleSearchButton;
@@ -1,6 +1,2 @@
1
1
  @import './component.search-field';
2
- @import './component.search-overlay';
3
- @import './component.search-page';
4
2
  @import './component.search-result';
5
- @import './component.search-result-author';
6
- @import './component.search-filter';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright (c) 2017-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 SearchField from './SearchField';
10
+
11
+ import { SearchResultList, SearchResultItem } from './SearchResult';
12
+
13
+ import ActiveFilters from './ActiveFilters';
14
+ export { default as ToggleSearchButton } from './ToggleSearchButton';
15
+
16
+ export { SearchField, SearchResultItem, SearchResultList, ActiveFilters };
@@ -9,6 +9,7 @@ interface Props {
9
9
  large?: boolean;
10
10
  className?: string;
11
11
  }
12
+
12
13
  const SectionHeading = ({ children, large = false, className }: Props) => {
13
14
  const Wrapper: ElementType = large ? 'h1' : 'h2';
14
15
  return <Wrapper {...classes('', { large: !!large }, className)}>{children}</Wrapper>;
@@ -128,7 +128,6 @@ const SuggestionInput = ({
128
128
  const { t } = useTranslation();
129
129
  const [currentHighlightedIndex, setCurrentHighlightedIndex] = useState(0);
130
130
  const [hasFocus, setHasFocus] = useState(false);
131
- const initalRender = useRef(true);
132
131
  const inputRef = useRef<HTMLInputElement>(null);
133
132
  const containerRef = useRef<HTMLDivElement>(null);
134
133
  const suggestionIdRef = useRef<string>(uuid());
@@ -138,14 +137,6 @@ const SuggestionInput = ({
138
137
  setCurrentHighlightedIndex(0);
139
138
  }, [suggestions]);
140
139
 
141
- useEffect(() => {
142
- if (!initalRender.current) {
143
- inputRef.current?.focus();
144
- } else {
145
- initalRender.current = false;
146
- }
147
- }, [addedTags]);
148
-
149
140
  useEffect(() => {
150
141
  const selectedSuggestionElement = document
151
142
  .getElementById(suggestionIdRef.current)
@@ -22,7 +22,7 @@ const StyledUL = styled.ul<{ firstLevel?: boolean }>`
22
22
  list-style: none;
23
23
  margin: 0;
24
24
  padding: 0;
25
- margin-left: ${({ firstLevel }) => (firstLevel ? `-${spacing.xsmall}` : spacing.normal)};
25
+ margin-left: ${({ firstLevel }) => (firstLevel ? `-${spacing.xsmall}` : spacing.small)};
26
26
  `;
27
27
 
28
28
  const StyledLI = styled.li`