@defra/docusaurus-theme-govuk 0.0.7-alpha → 0.0.9-alpha
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/README.md +110 -1
- package/index.js +17 -1
- package/package.json +1 -1
- package/src/css/components.scss +192 -0
- package/src/theme/DocItem/Content/index.js +9 -1
- package/src/theme/DocItem/Metadata/index.js +7 -1
- package/src/theme/Header/index.js +170 -0
- package/src/theme/Homepage/index.js +4 -35
- package/src/theme/Layout/index.js +55 -3
- package/src/theme/SearchBar/index.js +206 -0
package/README.md
CHANGED
|
@@ -103,6 +103,13 @@ module.exports = {
|
|
|
103
103
|
{ text: 'GitHub', href: 'https://github.com/your-org/your-repo' },
|
|
104
104
|
],
|
|
105
105
|
},
|
|
106
|
+
|
|
107
|
+
homepage: {
|
|
108
|
+
// Path to link the "Get started" button on the homepage masthead
|
|
109
|
+
getStartedHref: '/getting-started',
|
|
110
|
+
// Short description rendered below the heading
|
|
111
|
+
description: 'A short summary of what your service does.',
|
|
112
|
+
},
|
|
106
113
|
},
|
|
107
114
|
},
|
|
108
115
|
};
|
|
@@ -110,6 +117,17 @@ module.exports = {
|
|
|
110
117
|
|
|
111
118
|
### Configuration Reference
|
|
112
119
|
|
|
120
|
+
#### `themeConfig.govuk.homepage`
|
|
121
|
+
|
|
122
|
+
Controls the homepage masthead — a full-width blue banner rendered between the service navigation and the page body on the homepage only. The banner shares the GOV.UK rebrand blue (`#1d70b8`) with the header and service navigation, so all three elements appear as one unified block.
|
|
123
|
+
|
|
124
|
+
The masthead displays `siteConfig.tagline` as the heading and `homepage.description` as the lead paragraph. A GOV.UK "Start" button links to the configured `getStartedHref`.
|
|
125
|
+
|
|
126
|
+
| Property | Type | Default | Description |
|
|
127
|
+
|----------|------|---------|-------------|
|
|
128
|
+
| `getStartedHref` | `string` | `'/getting-started'` | Path the "Get started" button links to |
|
|
129
|
+
| `description` | `string` | — | Short description rendered below the heading |
|
|
130
|
+
|
|
113
131
|
#### `themeConfig.govuk.header`
|
|
114
132
|
|
|
115
133
|
| Property | Type | Description |
|
|
@@ -205,6 +223,62 @@ The sidebar is resolved once at build time and serialised into the site configur
|
|
|
205
223
|
- The document must be in the `docs/` directory at the root of your Docusaurus site.
|
|
206
224
|
- Heading IDs set via the `{#custom-id}` syntax are not yet respected — the generated anchor will use the slugified heading text.
|
|
207
225
|
|
|
226
|
+
## Search
|
|
227
|
+
|
|
228
|
+
The theme includes a built-in search bar rendered in the header. It uses [`@easyops-cn/docusaurus-search-local`](https://github.com/easyops-cn/docusaurus-search-local) to generate a local [lunr](https://lunrjs.com/) index at build time, and [alphagov's `accessible-autocomplete`](https://github.com/alphagov/accessible-autocomplete) as the accessible suggestion UI. No external service or API key is required.
|
|
229
|
+
|
|
230
|
+
### Dependencies
|
|
231
|
+
|
|
232
|
+
Install both packages in your Docusaurus site:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm install @easyops-cn/docusaurus-search-local accessible-autocomplete
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Configuration
|
|
239
|
+
|
|
240
|
+
The `@easyops-cn/docusaurus-search-local` theme must appear **before** `docusaurus-theme-govuk` in the `themes` array. This order ensures the search index is generated by the easyops plugin while the GOV.UK theme's `SearchBar` component wins the slot and controls the UI.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
themes: [
|
|
244
|
+
[
|
|
245
|
+
require.resolve('@easyops-cn/docusaurus-search-local'),
|
|
246
|
+
{
|
|
247
|
+
// Match your docs routeBasePath — '/' for docs-only mode
|
|
248
|
+
docsRouteBasePath: '/',
|
|
249
|
+
indexBlog: false,
|
|
250
|
+
indexPages: false,
|
|
251
|
+
// Hashed filenames allow long-term browser caching of the search index
|
|
252
|
+
hashed: 'filename',
|
|
253
|
+
highlightSearchTermsOnTargetPage: true,
|
|
254
|
+
searchResultContextMaxLength: 60,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
'docusaurus-theme-govuk',
|
|
258
|
+
],
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
You must also add `docs.versionPersistence` to `themeConfig`. Without it, the `@easyops-cn/docusaurus-search-local` plugin throws a runtime error during static site generation (`Cannot read properties of undefined (reading 'versionPersistence')`):
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
themeConfig: {
|
|
265
|
+
docs: {
|
|
266
|
+
versionPersistence: 'localStorage',
|
|
267
|
+
},
|
|
268
|
+
// ...
|
|
269
|
+
},
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Search index coverage
|
|
273
|
+
|
|
274
|
+
By default, all content under `docsRouteBasePath` is indexed. Blog and custom pages are excluded in the example above (`indexBlog: false`, `indexPages: false`). Adjust these to match your site structure.
|
|
275
|
+
|
|
276
|
+
The search index is generated at build time. Run `npm run build` (or `docs:build`) before serving — the search bar will return no results in development mode (`docusaurus start`).
|
|
277
|
+
|
|
278
|
+
### Result links
|
|
279
|
+
|
|
280
|
+
The search bar deep-links directly to the matching heading within a page using anchor IDs derived from heading text, using the same slugifier as Docusaurus itself. Results show a breadcrumb context trail (`Page › Heading`) to help users identify where a match appears.
|
|
281
|
+
|
|
208
282
|
## Overriding Components
|
|
209
283
|
|
|
210
284
|
You can override any theme component by creating a file at the same path in your project's `src/theme/` directory. For example, to override the 404 page:
|
|
@@ -225,4 +299,39 @@ Common components to override:
|
|
|
225
299
|
|
|
226
300
|
- **Docusaurus**: ^3.0.0
|
|
227
301
|
- **React**: ^18.0.0 || ^19.0.0
|
|
228
|
-
- **Node.js**: 18+
|
|
302
|
+
- **Node.js**: 18+
|
|
303
|
+
|
|
304
|
+
## Open Source Attribution
|
|
305
|
+
|
|
306
|
+
This theme includes a modified version of the [`@not-govuk/header`](https://github.com/daniel-ac-martin/NotGovUK/tree/master/packages/components/header) component from the [NotGovUK](https://github.com/daniel-ac-martin/NotGovUK) project.
|
|
307
|
+
|
|
308
|
+
The modification adds a `children` prop so that additional content (the search bar) can be rendered as a flex sibling inside the header container. All other behaviour is unchanged.
|
|
309
|
+
|
|
310
|
+
**Original licence:**
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
The MIT License (MIT)
|
|
314
|
+
|
|
315
|
+
Copyright (C) 2019, 2020, 2021 Crown Copyright
|
|
316
|
+
Copyright (C) 2019, 2020, 2021 Daniel A.C. Martin
|
|
317
|
+
|
|
318
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
319
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
320
|
+
the Software without restriction, including without limitation the rights to use,
|
|
321
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
322
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
323
|
+
subject to the following conditions:
|
|
324
|
+
|
|
325
|
+
The above copyright notice and this permission notice shall be included in all
|
|
326
|
+
copies or substantial portions of the Software.
|
|
327
|
+
|
|
328
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
329
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
330
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
331
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
332
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
333
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
334
|
+
SOFTWARE.
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The modified source is at [`src/theme/Header/index.js`](src/theme/Header/index.js).
|
package/index.js
CHANGED
|
@@ -215,6 +215,18 @@ module.exports = function themeGovuk(context, options) {
|
|
|
215
215
|
// When installed from npm the theme ships no node_modules of its own,
|
|
216
216
|
// so we must point webpack at the copy already present in the site.
|
|
217
217
|
'@mdx-js/react': resolveFromSite('@mdx-js/react'),
|
|
218
|
+
// @not-govuk/header restricts subpath imports via its `exports` field.
|
|
219
|
+
// Our forked Header component imports logos and the SCSS by their dist paths.
|
|
220
|
+
// Alias them to absolute paths to bypass the exports field restriction.
|
|
221
|
+
...((() => {
|
|
222
|
+
const headerDir = findPkgDir('@not-govuk/header', [siteDir, __dirname]);
|
|
223
|
+
return {
|
|
224
|
+
'@not-govuk/header/dist/CrownLogo$': path.join(headerDir, 'dist/CrownLogo.js'),
|
|
225
|
+
'@not-govuk/header/dist/CrownLogoOld$': path.join(headerDir, 'dist/CrownLogoOld.js'),
|
|
226
|
+
'@not-govuk/header/dist/CoatLogo$': path.join(headerDir, 'dist/CoatLogo.js'),
|
|
227
|
+
'@not-govuk/header/assets/Header.scss$': path.join(headerDir, 'assets/Header.scss'),
|
|
228
|
+
};
|
|
229
|
+
})()),
|
|
218
230
|
},
|
|
219
231
|
},
|
|
220
232
|
plugins: [
|
|
@@ -275,7 +287,6 @@ module.exports = function themeGovuk(context, options) {
|
|
|
275
287
|
);
|
|
276
288
|
}
|
|
277
289
|
),
|
|
278
|
-
// Null out the font SCSS wrappers from @not-govuk/page.
|
|
279
290
|
// GovUKPage.scss imports gds-transport.css, and NotGovUKPage.scss
|
|
280
291
|
// imports roboto.css. These contain @font-face rules with relative
|
|
281
292
|
// URLs that, when inlined by sass and processed by css-loader,
|
|
@@ -294,6 +305,11 @@ module.exports = function themeGovuk(context, options) {
|
|
|
294
305
|
rules: [
|
|
295
306
|
{
|
|
296
307
|
test: /\.m?js$/,
|
|
308
|
+
// `javascript/auto` lets webpack use its default JS pipeline (including
|
|
309
|
+
// Docusaurus's babel-loader rule) instead of treating every .mjs file
|
|
310
|
+
// as strict ESM. Without this, files like tslib.es6.mjs cause
|
|
311
|
+
// "Module parse failed: Unexpected token" because no loader is matched.
|
|
312
|
+
type: 'javascript/auto',
|
|
297
313
|
resolve: {
|
|
298
314
|
fullySpecified: false,
|
|
299
315
|
},
|
package/package.json
CHANGED
package/src/css/components.scss
CHANGED
|
@@ -98,3 +98,195 @@
|
|
|
98
98
|
.app-text-secondary {
|
|
99
99
|
color: #505a5f;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
// Ensure govuk-service-navigation--inverse styles apply in our rebranded context.
|
|
103
|
+
// govuk-frontend compiles these inside @include _govuk-rebrand which may not
|
|
104
|
+
// resolve correctly depending on SCSS load order — duplicate the rules explicitly.
|
|
105
|
+
.govuk-template--rebranded .govuk-service-navigation--inverse {
|
|
106
|
+
background-color: #1d70b8;
|
|
107
|
+
border-bottom: none;
|
|
108
|
+
color: #fff;
|
|
109
|
+
|
|
110
|
+
.govuk-width-container {
|
|
111
|
+
border-width: 1px 0;
|
|
112
|
+
border-style: solid;
|
|
113
|
+
border-color: #8eb8dc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.govuk-service-navigation__link {
|
|
117
|
+
color: #fff;
|
|
118
|
+
|
|
119
|
+
&:link,
|
|
120
|
+
&:visited {
|
|
121
|
+
color: #fff;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&:hover {
|
|
125
|
+
color: #fff;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.govuk-service-navigation__item,
|
|
130
|
+
.govuk-service-navigation__service-name {
|
|
131
|
+
border-color: #fff;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Homepage masthead — a blue welcome band that appears between the service
|
|
136
|
+
// navigation and the main content on the homepage only. The background matches
|
|
137
|
+
// the GOV.UK rebrand header/service-navigation blue (#1d70b8 = $govuk-brand-colour)
|
|
138
|
+
// so that all three elements read as one unified block.
|
|
139
|
+
.app-masthead {
|
|
140
|
+
background-color: #1d70b8;
|
|
141
|
+
// Kill any top border that service-navigation might leave
|
|
142
|
+
border-top: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.app-masthead__container {
|
|
146
|
+
padding-top: 40px; // govuk-spacing(7)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.app-masthead__title {
|
|
150
|
+
color: #fff;
|
|
151
|
+
|
|
152
|
+
// Override the default dark govuk heading colour
|
|
153
|
+
&.govuk-heading-xl {
|
|
154
|
+
color: #fff;
|
|
155
|
+
margin-bottom: 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.app-masthead__description {
|
|
160
|
+
color: #fff;
|
|
161
|
+
font-size: 1.5rem;
|
|
162
|
+
line-height: 1.25;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Search bar rendered as a flex sibling inside govuk-header__container.
|
|
166
|
+
// govuk-frontend's container uses a clearfix float layout; we switch it to
|
|
167
|
+
// flex so children line up in a row and can be vertically centred.
|
|
168
|
+
// margin-left:auto on .app-header-search pushes it to the right edge.
|
|
169
|
+
.govuk-header__container {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
flex-wrap: nowrap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.app-header-search {
|
|
176
|
+
margin-left: auto;
|
|
177
|
+
flex-shrink: 0;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
z-index: 1;
|
|
181
|
+
|
|
182
|
+
// accessible-autocomplete wrapper
|
|
183
|
+
.autocomplete__wrapper {
|
|
184
|
+
position: relative;
|
|
185
|
+
width: 300px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Input field — always white background with search icon on the left
|
|
189
|
+
.autocomplete__input {
|
|
190
|
+
width: 100%;
|
|
191
|
+
padding: 4px 8px 4px 40px;
|
|
192
|
+
border: 2px solid #fff;
|
|
193
|
+
border-radius: 0;
|
|
194
|
+
background-color: #fff;
|
|
195
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0c0c' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
|
|
196
|
+
background-repeat: no-repeat;
|
|
197
|
+
background-position: 10px center;
|
|
198
|
+
background-size: 20px 20px;
|
|
199
|
+
color: #0b0c0c;
|
|
200
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
201
|
+
font-size: 0.875rem;
|
|
202
|
+
box-sizing: border-box;
|
|
203
|
+
|
|
204
|
+
&::placeholder {
|
|
205
|
+
color: #505a5f;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&:focus {
|
|
209
|
+
outline: 3px solid #fd0;
|
|
210
|
+
outline-offset: 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Dropdown menu
|
|
215
|
+
.autocomplete__menu {
|
|
216
|
+
position: absolute;
|
|
217
|
+
top: 100%;
|
|
218
|
+
left: 0;
|
|
219
|
+
right: 0;
|
|
220
|
+
background: #fff;
|
|
221
|
+
border: 2px solid #0b0c0c;
|
|
222
|
+
border-radius: 0;
|
|
223
|
+
margin: 0;
|
|
224
|
+
padding: 0;
|
|
225
|
+
list-style: none;
|
|
226
|
+
max-height: 342px;
|
|
227
|
+
overflow-y: auto;
|
|
228
|
+
z-index: 10;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Inline menu variant (not used, but scoped just in case)
|
|
232
|
+
.autocomplete__menu--inline {
|
|
233
|
+
position: relative;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.autocomplete__option {
|
|
237
|
+
padding: 8px 12px;
|
|
238
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
239
|
+
font-size: 0.875rem;
|
|
240
|
+
color: #0b0c0c;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
border-bottom: 1px solid #f3f2f1;
|
|
243
|
+
|
|
244
|
+
&:last-child {
|
|
245
|
+
border-bottom: none;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.autocomplete__option--focused,
|
|
250
|
+
.autocomplete__option:hover {
|
|
251
|
+
background: #1d70b8;
|
|
252
|
+
color: #fff;
|
|
253
|
+
outline: none;
|
|
254
|
+
|
|
255
|
+
.app-search__context {
|
|
256
|
+
color: rgba(255, 255, 255, 0.8);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Two-line suggestion layout
|
|
261
|
+
.app-search__title {
|
|
262
|
+
display: block;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.app-search__context {
|
|
266
|
+
display: block;
|
|
267
|
+
font-size: 0.75rem;
|
|
268
|
+
color: #505a5f;
|
|
269
|
+
margin-top: 2px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.autocomplete__option--no-results {
|
|
273
|
+
padding: 8px 12px;
|
|
274
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
275
|
+
font-size: 0.875rem;
|
|
276
|
+
color: #505a5f;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Status / hint text (visually hidden assistive text)
|
|
280
|
+
.autocomplete__status {
|
|
281
|
+
position: absolute;
|
|
282
|
+
width: 1px;
|
|
283
|
+
height: 1px;
|
|
284
|
+
padding: 0;
|
|
285
|
+
margin: -1px;
|
|
286
|
+
overflow: hidden;
|
|
287
|
+
clip: rect(0, 0, 0, 0);
|
|
288
|
+
white-space: nowrap;
|
|
289
|
+
border: 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {useDoc} from '../docContext';
|
|
3
3
|
import MDXContent from '@theme/MDXContent';
|
|
4
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
5
|
|
|
5
6
|
// Render a synthetic title if needed
|
|
6
7
|
function useSyntheticTitle() {
|
|
7
8
|
const {metadata, frontMatter, contentTitle} = useDoc();
|
|
9
|
+
const {siteConfig} = useDocusaurusContext();
|
|
8
10
|
const shouldRender =
|
|
9
|
-
!frontMatter.hide_title &&
|
|
11
|
+
!frontMatter.hide_title &&
|
|
12
|
+
typeof contentTitle === 'undefined' &&
|
|
13
|
+
metadata.slug !== '/';
|
|
10
14
|
if (!shouldRender) {
|
|
11
15
|
return null;
|
|
12
16
|
}
|
|
17
|
+
// For the root index page, use the Docusaurus site title instead of the filename
|
|
18
|
+
if (metadata.title?.toLowerCase() === 'index') {
|
|
19
|
+
return siteConfig.title;
|
|
20
|
+
}
|
|
13
21
|
return metadata.title;
|
|
14
22
|
}
|
|
15
23
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Head from '@docusaurus/Head';
|
|
3
3
|
import {useDoc} from '../docContext';
|
|
4
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
5
|
|
|
5
6
|
export default function DocItemMetadata() {
|
|
6
7
|
const {metadata} = useDoc();
|
|
7
|
-
const {
|
|
8
|
+
const {siteConfig} = useDocusaurusContext();
|
|
9
|
+
const {description} = metadata;
|
|
10
|
+
// Use the Docusaurus site title for root index pages instead of the filename
|
|
11
|
+
const title = metadata.title?.toLowerCase() === 'index'
|
|
12
|
+
? siteConfig.title
|
|
13
|
+
: metadata.title;
|
|
8
14
|
|
|
9
15
|
return (
|
|
10
16
|
<Head>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forked from @not-govuk/header (MIT Licence)
|
|
3
|
+
* Copyright (C) 2019–2021 Crown Copyright
|
|
4
|
+
* Copyright (C) 2019–2021 Daniel A.C. Martin
|
|
5
|
+
*
|
|
6
|
+
* Modifications: added `children` prop rendered as a flex sibling inside the
|
|
7
|
+
* header container, allowing content (e.g. a search bar) to be injected into
|
|
8
|
+
* the right-hand side of `govuk-header__container`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import {classBuilder} from '@react-foundry/component-helpers';
|
|
13
|
+
import {Link} from '@not-govuk/link';
|
|
14
|
+
import {WidthContainer} from '@not-govuk/width-container';
|
|
15
|
+
import {CrownLogo} from '@not-govuk/header/dist/CrownLogo';
|
|
16
|
+
import {CrownLogoOld} from '@not-govuk/header/dist/CrownLogoOld';
|
|
17
|
+
import {CoatLogo} from '@not-govuk/header/dist/CoatLogo';
|
|
18
|
+
import '@not-govuk/header/assets/Header.scss';
|
|
19
|
+
|
|
20
|
+
const departmentMap = {
|
|
21
|
+
'home-office': 'Home Office',
|
|
22
|
+
'department-for-communities-and-local-government': 'DCLG',
|
|
23
|
+
'department-for-culture-media-sport': 'DCMS',
|
|
24
|
+
'department-for-environment-food-rural-affairs': 'DEFRA',
|
|
25
|
+
'department-for-work-pensions': 'DWP',
|
|
26
|
+
'foreign-commonwealth-development-office': 'FCDO',
|
|
27
|
+
'foreign-commonwealth-office': 'FCO',
|
|
28
|
+
'hm-revenue-customs': 'HMRC',
|
|
29
|
+
'hm-treasury': 'HM Treasury',
|
|
30
|
+
'ministry-of-justice': 'MoJ',
|
|
31
|
+
'office-of-the-leader-of-the-house-of-lords': '',
|
|
32
|
+
'scotland-office': 'Scotland Office',
|
|
33
|
+
'wales-office': 'Wales Office',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const departmentText = (d) => {
|
|
37
|
+
if (!d) return null;
|
|
38
|
+
return (
|
|
39
|
+
departmentMap[d] ||
|
|
40
|
+
d
|
|
41
|
+
.split('-')
|
|
42
|
+
.map((e) => {
|
|
43
|
+
switch (e) {
|
|
44
|
+
case 'and': return '';
|
|
45
|
+
case 'hm': return 'HM';
|
|
46
|
+
case 'for': return '';
|
|
47
|
+
case 'of': return 'o';
|
|
48
|
+
case 'the': return '';
|
|
49
|
+
default: return e.charAt(0).toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join('')
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const Header = ({
|
|
57
|
+
children,
|
|
58
|
+
classBlock,
|
|
59
|
+
classModifiers: _classModifiers = [],
|
|
60
|
+
className,
|
|
61
|
+
department,
|
|
62
|
+
govUK = false,
|
|
63
|
+
maxContentsWidth,
|
|
64
|
+
navigation = [],
|
|
65
|
+
organisationHref,
|
|
66
|
+
organisationText,
|
|
67
|
+
rebrand = false,
|
|
68
|
+
serviceHref = '/',
|
|
69
|
+
serviceName,
|
|
70
|
+
signOutHref,
|
|
71
|
+
signOutText = 'Sign out',
|
|
72
|
+
logo: _logo,
|
|
73
|
+
...attrs
|
|
74
|
+
}) => {
|
|
75
|
+
const classModifiers = Array.isArray(_classModifiers)
|
|
76
|
+
? _classModifiers
|
|
77
|
+
: [_classModifiers];
|
|
78
|
+
|
|
79
|
+
const classes = classBuilder(
|
|
80
|
+
'govuk-header',
|
|
81
|
+
classBlock,
|
|
82
|
+
[...classModifiers, department],
|
|
83
|
+
className,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const A = (props) => (
|
|
87
|
+
<Link classBlock={classes('link')} {...props} />
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const orgHref = organisationHref || (govUK ? 'https://www.gov.uk/' : '/');
|
|
91
|
+
const orgText =
|
|
92
|
+
organisationText || (govUK ? 'GOV.UK' : departmentText(department));
|
|
93
|
+
|
|
94
|
+
const navLinks = !signOutHref
|
|
95
|
+
? navigation
|
|
96
|
+
: [...navigation, {href: signOutHref, text: signOutText, forceExternal: true}];
|
|
97
|
+
|
|
98
|
+
const logo =
|
|
99
|
+
_logo !== undefined
|
|
100
|
+
? _logo
|
|
101
|
+
: govUK
|
|
102
|
+
? rebrand
|
|
103
|
+
? <CrownLogo focusable="false" className={classes('logotype')} height="30" width="162" />
|
|
104
|
+
: <CrownLogoOld focusable="false" className={classes('logotype')} height="30" width="148" />
|
|
105
|
+
: <CoatLogo aria-hidden="true" focusable="false" className={classes('logotype', ['coat'])} height="30" width="36" />;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<header {...attrs} className={classes()} data-module="govuk-header">
|
|
109
|
+
<WidthContainer maxWidth={maxContentsWidth} className={classes('container')}>
|
|
110
|
+
<div className={classes('logo')}>
|
|
111
|
+
<A
|
|
112
|
+
href={orgHref}
|
|
113
|
+
classModifiers={[
|
|
114
|
+
'homepage',
|
|
115
|
+
orgText && orgText.length > 9 ? 'small' : undefined,
|
|
116
|
+
]}
|
|
117
|
+
>
|
|
118
|
+
{logo}
|
|
119
|
+
{govUK ? null : (
|
|
120
|
+
<span className={classes('logotype-text')}>{orgText}</span>
|
|
121
|
+
)}
|
|
122
|
+
</A>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{(serviceName || navLinks.length) ? (
|
|
126
|
+
<div className={classes('content')}>
|
|
127
|
+
{serviceName && (
|
|
128
|
+
<A href={serviceHref} className={classes('service-name')}>
|
|
129
|
+
{serviceName}
|
|
130
|
+
</A>
|
|
131
|
+
)}
|
|
132
|
+
{navLinks.length ? (
|
|
133
|
+
<nav className={classes('navigation')} aria-label="Menu">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className={classes(
|
|
137
|
+
'menu-button',
|
|
138
|
+
undefined,
|
|
139
|
+
'govuk-js-header-toggle',
|
|
140
|
+
)}
|
|
141
|
+
aria-controls="navigation"
|
|
142
|
+
hidden
|
|
143
|
+
>
|
|
144
|
+
Menu
|
|
145
|
+
</button>
|
|
146
|
+
<ul id="navigation" className={classes('navigation-list')}>
|
|
147
|
+
{navLinks.map(({active, text, ...linkAttrs}, i) => (
|
|
148
|
+
<li
|
|
149
|
+
key={i}
|
|
150
|
+
className={classes(
|
|
151
|
+
'navigation-item',
|
|
152
|
+
active ? 'active' : undefined,
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<A {...linkAttrs}>{text}</A>
|
|
156
|
+
</li>
|
|
157
|
+
))}
|
|
158
|
+
</ul>
|
|
159
|
+
</nav>
|
|
160
|
+
) : null}
|
|
161
|
+
</div>
|
|
162
|
+
) : null}
|
|
163
|
+
|
|
164
|
+
{children}
|
|
165
|
+
</WidthContainer>
|
|
166
|
+
</header>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default Header;
|
|
@@ -1,47 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
3
|
-
import useBaseUrl from '@docusaurus/useBaseUrl';
|
|
4
3
|
import Layout from '@theme/Layout';
|
|
5
4
|
|
|
5
|
+
// Homepage component for consumer sites that use a custom page (src/pages/index.js)
|
|
6
|
+
// rather than a docs-based root. The masthead is rendered automatically by Layout
|
|
7
|
+
// when the current path is '/' and themeConfig.govuk.homepage is configured.
|
|
6
8
|
export default function Homepage() {
|
|
7
9
|
const {siteConfig} = useDocusaurusContext();
|
|
8
|
-
const baseUrl = useBaseUrl('/');
|
|
9
10
|
|
|
10
11
|
return (
|
|
11
12
|
<Layout title={siteConfig.title} description={siteConfig.tagline}>
|
|
12
|
-
|
|
13
|
-
<div className="govuk-grid-column-two-thirds">
|
|
14
|
-
<h1 className="govuk-heading-xl govuk-!-margin-top-8">
|
|
15
|
-
{siteConfig.title}
|
|
16
|
-
</h1>
|
|
17
|
-
|
|
18
|
-
{siteConfig.tagline && (
|
|
19
|
-
<p className="govuk-body-l">
|
|
20
|
-
{siteConfig.tagline}
|
|
21
|
-
</p>
|
|
22
|
-
)}
|
|
23
|
-
|
|
24
|
-
<a
|
|
25
|
-
href={baseUrl}
|
|
26
|
-
role="button"
|
|
27
|
-
draggable="false"
|
|
28
|
-
className="govuk-button govuk-button--start govuk-!-margin-top-4"
|
|
29
|
-
>
|
|
30
|
-
Get started
|
|
31
|
-
<svg
|
|
32
|
-
className="govuk-button__start-icon"
|
|
33
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
-
width="17.5"
|
|
35
|
-
height="19"
|
|
36
|
-
viewBox="0 0 33 40"
|
|
37
|
-
aria-hidden="true"
|
|
38
|
-
focusable="false"
|
|
39
|
-
>
|
|
40
|
-
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z" />
|
|
41
|
-
</svg>
|
|
42
|
-
</a>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
13
|
+
{/* Masthead is injected by Layout on the root path. This is because we need to adjust the styling for the header/nav, so it must live in the global Layout. */}
|
|
45
14
|
</Layout>
|
|
46
15
|
);
|
|
47
16
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import '../../css/theme.scss';
|
|
3
|
-
import {SkipLink,
|
|
3
|
+
import {SkipLink, Footer, PhaseBanner, ServiceNavigation} from '@not-govuk/simple-components';
|
|
4
|
+
import Header from '../Header';
|
|
4
5
|
import SidebarNav from '../SidebarNav';
|
|
6
|
+
import SearchBar from '@theme/SearchBar';
|
|
5
7
|
import {useLocation} from '@docusaurus/router';
|
|
6
8
|
import Head from '@docusaurus/Head';
|
|
7
9
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
10
|
+
import useBaseUrl from '@docusaurus/useBaseUrl';
|
|
8
11
|
import LayoutProvider from '@theme/Layout/Provider';
|
|
9
12
|
import AnnouncementBar from '@theme/AnnouncementBar';
|
|
10
13
|
|
|
@@ -110,9 +113,15 @@ export default function Layout(props) {
|
|
|
110
113
|
const header = govukConfig.header || {};
|
|
111
114
|
const phaseBanner = govukConfig.phaseBanner;
|
|
112
115
|
const footer = govukConfig.footer || {};
|
|
116
|
+
const homepageConfig = govukConfig.homepage;
|
|
113
117
|
|
|
114
118
|
// Strip baseUrl so sidebar matching works regardless of deployment path
|
|
115
119
|
const pathname = stripBaseUrl(location.pathname, siteConfig.baseUrl);
|
|
120
|
+
|
|
121
|
+
const isHomepage = pathname === '/';
|
|
122
|
+
const getStartedHref = useBaseUrl(
|
|
123
|
+
homepageConfig?.getStartedHref || '/getting-started',
|
|
124
|
+
);
|
|
116
125
|
const baseUrl = siteConfig.baseUrl.endsWith('/')
|
|
117
126
|
? siteConfig.baseUrl.slice(0, -1)
|
|
118
127
|
: siteConfig.baseUrl;
|
|
@@ -169,14 +178,57 @@ export default function Layout(props) {
|
|
|
169
178
|
rebrand
|
|
170
179
|
organisationText={header.organisationText}
|
|
171
180
|
organisationHref={header.organisationHref}
|
|
172
|
-
|
|
181
|
+
>
|
|
182
|
+
<div className="app-header-search">
|
|
183
|
+
<SearchBar />
|
|
184
|
+
</div>
|
|
185
|
+
</Header>
|
|
173
186
|
|
|
174
|
-
<ServiceNavigation
|
|
187
|
+
<ServiceNavigation
|
|
175
188
|
items={serviceNavItems}
|
|
176
189
|
serviceName={header.serviceName}
|
|
177
190
|
serviceHref={withBase(header.serviceHref || '/')}
|
|
191
|
+
classModifiers={isHomepage && homepageConfig ? 'inverse' : undefined}
|
|
178
192
|
/>
|
|
179
193
|
|
|
194
|
+
{isHomepage && homepageConfig && (
|
|
195
|
+
<div className="app-masthead">
|
|
196
|
+
<div className="govuk-width-container app-masthead__container">
|
|
197
|
+
<div className="govuk-grid-row">
|
|
198
|
+
<div className="govuk-grid-column-two-thirds">
|
|
199
|
+
<h1 className="govuk-heading-xl app-masthead__title">
|
|
200
|
+
{siteConfig.tagline || siteConfig.title}
|
|
201
|
+
</h1>
|
|
202
|
+
{homepageConfig.description && (
|
|
203
|
+
<p className="app-masthead__description">
|
|
204
|
+
{homepageConfig.description}
|
|
205
|
+
</p>
|
|
206
|
+
)}
|
|
207
|
+
<a
|
|
208
|
+
href={getStartedHref}
|
|
209
|
+
role="button"
|
|
210
|
+
draggable="false"
|
|
211
|
+
className="govuk-button govuk-button--start govuk-button--inverse"
|
|
212
|
+
>
|
|
213
|
+
Get started
|
|
214
|
+
<svg
|
|
215
|
+
className="govuk-button__start-icon"
|
|
216
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
217
|
+
width="17.5"
|
|
218
|
+
height="19"
|
|
219
|
+
viewBox="0 0 33 40"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
focusable="false"
|
|
222
|
+
>
|
|
223
|
+
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z" />
|
|
224
|
+
</svg>
|
|
225
|
+
</a>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
180
232
|
<div className="govuk-width-container">
|
|
181
233
|
{phaseBanner && (
|
|
182
234
|
<PhaseBanner phase={phaseBanner.phase}>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import BrowserOnly from '@docusaurus/BrowserOnly';
|
|
3
|
+
import { useHistory } from '@docusaurus/router';
|
|
4
|
+
import useBaseUrl from '@docusaurus/useBaseUrl';
|
|
5
|
+
// github-slugger is Docusaurus's own heading-anchor dependency.
|
|
6
|
+
// Importing it directly ensures our anchor derivation never drifts from
|
|
7
|
+
// the IDs Docusaurus writes into the built HTML.
|
|
8
|
+
import GithubSlugger from 'github-slugger';
|
|
9
|
+
// @generated module emitted by @easyops-cn/docusaurus-search-local at build time;
|
|
10
|
+
// contains the hashed URL of the search index JSON (e.g. "search-index-fa7ba571.json").
|
|
11
|
+
// eslint-disable-next-line import/no-unresolved
|
|
12
|
+
import { searchIndexUrl } from '@generated/@easyops-cn/docusaurus-search-local/default/generated-constants.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the template URL emitted by easyops.
|
|
16
|
+
* The URL contains a `{dir}` placeholder for the doc root / locale directory.
|
|
17
|
+
* For a single-locale site with docsRouteBasePath '/' the dir is empty.
|
|
18
|
+
*/
|
|
19
|
+
function resolveSearchIndexUrl(template) {
|
|
20
|
+
// Strip the {dir} token (single-locale / single-docs-plugin case).
|
|
21
|
+
return template.replace('{dir}', '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the easyops-generated lunr document list.
|
|
26
|
+
*
|
|
27
|
+
* Document schema (all fields present depend on type):
|
|
28
|
+
* b,i,t,u — page root: t = page title, b = breadcrumbs (empty)
|
|
29
|
+
* h,i,p,t,u — heading: t = heading text, h = anchor hash, p = parent page id
|
|
30
|
+
* i,p,s,t,u — paragraph: t = body text, s = section heading / page title
|
|
31
|
+
* h,i,p,s,t,u — para+anchor: t = body text, s = section heading, h = anchor hash
|
|
32
|
+
*/
|
|
33
|
+
async function fetchSearchDocs(url) {
|
|
34
|
+
const res = await fetch(url);
|
|
35
|
+
if (!res.ok) throw new Error(`Failed to load search index: ${res.status}`);
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
return Object.values(data).flatMap((bucket) => bucket?.documents ?? []);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Minimal HTML escaping for values injected via innerHTML in suggestion templates. */
|
|
41
|
+
function escapeHtml(str) {
|
|
42
|
+
return String(str)
|
|
43
|
+
.replace(/&/g, '&')
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/"/g, '"');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function SearchBarInner() {
|
|
50
|
+
const history = useHistory();
|
|
51
|
+
const indexUrl = useBaseUrl(resolveSearchIndexUrl(searchIndexUrl));
|
|
52
|
+
const rootUrl = useBaseUrl('');
|
|
53
|
+
const [docs, setDocs] = useState([]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
fetchSearchDocs(indexUrl)
|
|
57
|
+
.then(setDocs)
|
|
58
|
+
.catch((err) => console.warn('[SearchBar] Could not load search index', err));
|
|
59
|
+
}, [indexUrl]);
|
|
60
|
+
|
|
61
|
+
// Build URL → page title lookup from page-root entries (those with `b` field).
|
|
62
|
+
const pageByUrl = useMemo(() => {
|
|
63
|
+
const map = new Map();
|
|
64
|
+
for (const doc of docs) {
|
|
65
|
+
if ('b' in doc) map.set(doc.u, doc.t);
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}, [docs]);
|
|
69
|
+
|
|
70
|
+
// Derive the site root URL and its title directly from the index — the
|
|
71
|
+
// page-root entry with the shortest URL is always the site home page.
|
|
72
|
+
// This is more reliable than useBaseUrl('') which can return '' in some
|
|
73
|
+
// rendering contexts, causing the root title to leak into context trails.
|
|
74
|
+
const { rootPageUrl, rootPageTitle } = useMemo(() => {
|
|
75
|
+
let shortest = null;
|
|
76
|
+
for (const [url, title] of pageByUrl) {
|
|
77
|
+
if (shortest === null || url.length < shortest.url.length) {
|
|
78
|
+
shortest = { url, title };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { rootPageUrl: shortest?.url ?? rootUrl, rootPageTitle: shortest?.title ?? null };
|
|
82
|
+
}, [pageByUrl, rootUrl]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk the URL path upwards collecting ancestor page titles.
|
|
86
|
+
* Stops at (and excludes) the site root so the home-page title is never shown.
|
|
87
|
+
* e.g. "/interactive-map/api/button-definition"
|
|
88
|
+
* → "/interactive-map/api" → "API reference"
|
|
89
|
+
* → "/interactive-map" → skipped (site root)
|
|
90
|
+
*/
|
|
91
|
+
const getAncestorPages = useCallback(
|
|
92
|
+
(docUrl, currentPageTitle) => {
|
|
93
|
+
const ancestors = [];
|
|
94
|
+
const normalizedRoot = rootPageUrl.replace(/\/$/, '');
|
|
95
|
+
let url = docUrl.replace(/\/$/, '');
|
|
96
|
+
while (url.lastIndexOf('/') > 0) {
|
|
97
|
+
url = url.substring(0, url.lastIndexOf('/'));
|
|
98
|
+
if (url === normalizedRoot || url + '/' === rootPageUrl) break;
|
|
99
|
+
const title = pageByUrl.get(url) ?? pageByUrl.get(url + '/');
|
|
100
|
+
if (title && title !== currentPageTitle) {
|
|
101
|
+
ancestors.unshift(title);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return ancestors;
|
|
105
|
+
},
|
|
106
|
+
[pageByUrl, rootPageUrl],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const source = useCallback(
|
|
110
|
+
(query, populateResults) => {
|
|
111
|
+
if (!query || query.length < 2) {
|
|
112
|
+
populateResults([]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const q = query.toLowerCase();
|
|
116
|
+
// `s` = the heading/section label; `t` = body text (can be a full paragraph).
|
|
117
|
+
// Match against headings only so body paragraphs don't pollute results.
|
|
118
|
+
// Deduplicate by URL so each page appears at most once.
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const doc of docs) {
|
|
122
|
+
const label = doc.s ?? doc.t;
|
|
123
|
+
if (!label?.toLowerCase().includes(q)) continue;
|
|
124
|
+
if (seen.has(doc.u)) continue;
|
|
125
|
+
seen.add(doc.u);
|
|
126
|
+
const currentPage = pageByUrl.get(doc.u);
|
|
127
|
+
const ancestors = getAncestorPages(doc.u, currentPage);
|
|
128
|
+
// Build a context trail: ancestor pages → current page.
|
|
129
|
+
// Exclude the result label itself and the site root title (home page
|
|
130
|
+
// title is uninformative — user already knows which site they're on).
|
|
131
|
+
const contextParts = [
|
|
132
|
+
...ancestors,
|
|
133
|
+
...(currentPage &&
|
|
134
|
+
currentPage !== label &&
|
|
135
|
+
currentPage !== rootPageTitle
|
|
136
|
+
? [currentPage]
|
|
137
|
+
: []),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Derive the anchor using github-slugger — the same library Docusaurus
|
|
141
|
+
// uses when writing heading IDs into the built HTML, so they always match.
|
|
142
|
+
// Page-root entries (have `b`) live at the page URL with no hash.
|
|
143
|
+
// Heading/paragraph entries (have `h` field, even when empty) deep-link.
|
|
144
|
+
const anchor = ('h' in doc && !('b' in doc))
|
|
145
|
+
? new GithubSlugger().slug(label)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
results.push({
|
|
149
|
+
...doc,
|
|
150
|
+
_label: label,
|
|
151
|
+
_context: contextParts.length ? contextParts.join(' › ') : null,
|
|
152
|
+
_url: anchor ? `${doc.u}#${anchor}` : doc.u,
|
|
153
|
+
});
|
|
154
|
+
if (results.length === 8) break;
|
|
155
|
+
}
|
|
156
|
+
populateResults(results);
|
|
157
|
+
},
|
|
158
|
+
[docs, pageByUrl, getAncestorPages, rootPageTitle],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const onConfirm = useCallback(
|
|
162
|
+
(selected) => {
|
|
163
|
+
if (selected?._url) {
|
|
164
|
+
history.push(selected._url);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
[history],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const Autocomplete = require('accessible-autocomplete/react').default;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Autocomplete
|
|
174
|
+
id="govuk-search"
|
|
175
|
+
source={source}
|
|
176
|
+
templates={{
|
|
177
|
+
inputValue: (result) => (result ? result._label : ''),
|
|
178
|
+
suggestion: (result) => {
|
|
179
|
+
if (!result) return '';
|
|
180
|
+
const title = escapeHtml(result._label);
|
|
181
|
+
const context = result._context
|
|
182
|
+
? `<span class="app-search__context">${escapeHtml(result._context)}</span>`
|
|
183
|
+
: '';
|
|
184
|
+
return `<span class="app-search__title">${title}</span>${context}`;
|
|
185
|
+
},
|
|
186
|
+
}}
|
|
187
|
+
displayMenu="overlay"
|
|
188
|
+
minLength={2}
|
|
189
|
+
placeholder="Search"
|
|
190
|
+
onConfirm={onConfirm}
|
|
191
|
+
tNoResults={() => 'No results found'}
|
|
192
|
+
tAssistiveHint={() =>
|
|
193
|
+
'When autocomplete results are available use up and down arrows to review and enter to select.'
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* SearchBar rendered inside the GOV.UK header.
|
|
201
|
+
* BrowserOnly ensures accessible-autocomplete (which uses DOM APIs) is never
|
|
202
|
+
* evaluated during server-side generation.
|
|
203
|
+
*/
|
|
204
|
+
export default function SearchBar() {
|
|
205
|
+
return <BrowserOnly>{() => <SearchBarInner />}</BrowserOnly>;
|
|
206
|
+
}
|