Skip to content

Commit fe17633

Browse files
committed
WIP: Add autocompletion
1 parent 203a469 commit fe17633

File tree

6 files changed

+205
-54
lines changed

6 files changed

+205
-54
lines changed

components/search/forms/simple.jsx

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,89 @@
1-
import React from 'react'
2-
import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap'
1+
import React, { useRef } from 'react'
2+
import { Select } from '@oacore/design'
33

4-
const SearchField = ({
5-
size = '',
6-
id = 'search-form-field',
7-
label = 'Search in CORE',
8-
...fieldProps
9-
}) => (
10-
<>
11-
<label className="sr-only" htmlFor={id}>
12-
{label}
13-
</label>
14-
<InputGroup size={size}>
15-
<Input type="search" id={id} {...fieldProps} />
16-
<InputGroupAddon addonType="append">
17-
<Button color="primary">Search</Button>
18-
</InputGroupAddon>
19-
</InputGroup>
20-
</>
21-
)
4+
import styles from './styles.module.css'
5+
6+
const options = [
7+
{ id: 1, icon: '#magnify', value: 'Option A' },
8+
{ id: 2, icon: '#magnify', value: 'Option B' },
9+
{ id: 3, icon: '#magnify', value: 'Option C' },
10+
{ id: 4, icon: '#magnify', value: 'Option D' },
11+
{ id: 5, icon: '#magnify', value: 'Option E' },
12+
]
13+
14+
const SearchAutocompletion = ({ formRef, ...passProps }) => {
15+
const [suggestions, setSuggestions] = React.useState(options)
16+
const [value, setValue] = React.useState('')
17+
18+
const handleOnChange = data => {
19+
if (data.value === '') return
20+
formRef.current.submit()
21+
}
22+
23+
const handleOnInput = data => {
24+
// if id doesn't exists it means user type own text
25+
// and didn't use suggestion
26+
if (!data.id) {
27+
setSuggestions(
28+
options.slice(0, Math.max(0, options.length - data.value.length))
29+
)
30+
}
31+
32+
setValue(data.value)
33+
}
34+
35+
return (
36+
<Select
37+
id="search-select"
38+
value={value}
39+
onChange={handleOnChange}
40+
onInput={handleOnInput}
41+
prependIcon="#magnify"
42+
className={styles.searchBox}
43+
clearButtonClassName={styles.clearButton}
44+
selectMenuClassName={styles.selectMenu}
45+
changeOnBlur={false}
46+
{...passProps}
47+
>
48+
{suggestions.map(el => (
49+
<Select.Option
50+
key={el.id}
51+
id={el.id}
52+
value={el.value}
53+
icon={el.icon}
54+
className={styles.option}
55+
>
56+
{el.value}
57+
</Select.Option>
58+
))}
59+
{value !== '' && (
60+
<Select.Option
61+
key={6}
62+
id={6}
63+
value={value}
64+
icon="#magnify"
65+
className={`${styles.option} ${styles.lastOption}`}
66+
>
67+
{`All results for "${value}"`}
68+
</Select.Option>
69+
)}
70+
</Select>
71+
)
72+
}
2273

2374
const SearchForm = ({
2475
action,
2576
method,
2677
onSubmit,
2778
id = 'search-form',
2879
...fieldProps
29-
}) => (
30-
<form id={id} action={action} method={method} onSubmit={onSubmit}>
31-
<SearchField id={`${id}-field`} size="lg" {...fieldProps} />
32-
</form>
33-
)
80+
}) => {
81+
const ref = useRef(null)
82+
return (
83+
<form ref={ref} id={id} action={action} method={method} onSubmit={onSubmit}>
84+
<SearchAutocompletion id={`${id}-field`} {...fieldProps} formRef={ref} />
85+
</form>
86+
)
87+
}
3488

3589
export default SearchForm
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
.search-box {
2+
--form-control-corner-radius: 0.3rem;
3+
--form-control-color: var(--gray-500);
4+
--form-label-color: var(--gray-500);
5+
--select-option-color: var(--gray-700);
6+
--select-option-icon-color: var(--gray-700);
7+
8+
border-bottom: 1px solid transparent;
9+
}
10+
11+
.search-box:focus-within {
12+
--form-control-color: var(--gray-700);
13+
--form-control-padding-x: 0rem;
14+
}
15+
16+
.search-box:focus-within input,
17+
.search-box:focus-within .option span:nth-child(2){
18+
padding-left: 1rem;
19+
padding-right: 1rem;
20+
}
21+
22+
.search-box:focus-within,
23+
.search-box .option {
24+
padding-left: 1rem;
25+
padding-right: 1rem;
26+
}
27+
28+
.search-box:focus-within div:nth-child(2) {
29+
border-bottom: 1px solid var(--primary);
30+
}
31+
32+
.search-box:focus-within .clear-button{
33+
margin-right: -0.5rem;
34+
margin-left: -0.5rem;
35+
}
36+
37+
.search-box:focus-within div:nth-child(2) span,
38+
.search-box:focus-within .option span:first-child {
39+
margin-left: -0.25rem;
40+
margin-right: -0.25rem;
41+
}
42+
43+
.search-box:focus-within {
44+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.25);
45+
}
46+
47+
.search-box:focus-within div:nth-child(2) > * {
48+
border-color: transparent;
49+
}
50+
51+
.search-box:focus-within .select-menu {
52+
color: var(--gray-500)
53+
}
54+
55+
.search-box:focus-within .last-option {
56+
position: relative;
57+
}
58+
.search-box:focus-within .last-option::after {
59+
content: '';
60+
left: 1rem;
61+
right: 1rem;
62+
position: absolute;
63+
border-top: 1px solid var(--gray-300);
64+
}

design.config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const path = require('path')
2+
3+
const icons = ['magnify']
4+
5+
const iconsRoot = path.join(
6+
path.dirname(require.resolve('@mdi/svg/package.json')),
7+
'./svg'
8+
)
9+
10+
const config = {
11+
icons: {
12+
path: iconsRoot,
13+
files: icons,
14+
},
15+
16+
output: {
17+
path: path.join(__dirname, 'public/design'),
18+
publicPath: '/design',
19+
icons: {
20+
files: 'icons',
21+
sprite: 'icons.svg',
22+
},
23+
},
24+
}
25+
26+
module.exports = config

next.config.js

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,38 @@ const nextConfig = {
1818
// This is the array of webpack rules that:
1919
// - is inside a 'oneOf' block
2020
// - contains a rule that matches 'file.css'
21-
const styleRules = (
22-
rules.find(
23-
m => m.oneOf && m.oneOf.find(({ test: reg }) => reg.test('file.scss'))
24-
) || {}
25-
).oneOf
26-
if (!styleRules) return config
27-
// Find all the webpack rules that handle CSS modules
28-
// Look for rules that match '.module.css'
29-
// but aren't being used to generate
30-
// error messages.
31-
const cssModuleRules = [
32-
styleRules.find(
33-
({ test: reg, use }) =>
34-
reg.test('file.module.scss') && use.loader !== 'error-loader'
35-
),
36-
].filter(n => n) // remove 'undefined' values
37-
// Add the 'localsConvention' config option to the CSS loader config
38-
// in each of these rules.
39-
cssModuleRules.forEach(cmr => {
40-
// Find the item inside the 'use' list that defines css-loader
41-
const cssLoaderConfig = cmr.use.find(({ loader }) =>
42-
loader.includes('css-loader')
43-
)
44-
if (cssLoaderConfig && cssLoaderConfig.options) {
45-
// Patch it with the new config
46-
cssLoaderConfig.options.localsConvention = 'camelCase'
47-
}
21+
const suffixes = ['css', 'scss']
22+
suffixes.forEach(suffix => {
23+
const styleRules = (
24+
rules.find(
25+
m =>
26+
m.oneOf &&
27+
m.oneOf.find(({ test: reg }) => reg.test(`file.${suffix}`))
28+
) || {}
29+
).oneOf
30+
if (!styleRules) return
31+
// Find all the webpack rules that handle CSS modules
32+
// Look for rules that match '.module.css'
33+
// but aren't being used to generate
34+
// error messages.
35+
const cssModuleRules = [
36+
styleRules.find(
37+
({ test: reg, use }) =>
38+
reg.test(`file.module.${suffix}`) && use.loader !== 'error-loader'
39+
),
40+
].filter(n => n) // remove 'undefined' values
41+
// Add the 'localsConvention' config option to the CSS loader config
42+
// in each of these rules.
43+
cssModuleRules.forEach(cmr => {
44+
// Find the item inside the 'use' list that defines css-loader
45+
const cssLoaderConfig = cmr.use.find(({ loader }) =>
46+
loader.includes('css-loader')
47+
)
48+
if (cssLoaderConfig && cssLoaderConfig.options) {
49+
// Patch it with the new config
50+
cssLoaderConfig.options.localsConvention = 'camelCase'
51+
}
52+
})
4853
})
4954

5055
Object.assign(config.resolve.alias, {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"test-staging": "blc https://${CORE_STAGING_AREA}core.ac.uk -ro --exclude='/browse' --exclude='/search' --exclude='/public'",
7373
"test": "npm run test-dev",
7474
"dev": "next",
75-
"build": "next build",
75+
"build": "node ./node_modules/.bin/design build icons && next build",
7676
"export": "next build && next export",
7777
"start": "next start"
7878
}

pages/index.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ const IndexPage = () => (
8585
<SearchForm
8686
action="/search"
8787
name="q"
88-
placeholder={patchStats(page.searchPlaceholder, page.statistics)}
88+
label={patchStats(page.searchPlaceholder, page.statistics)}
89+
placeholder="e.g. article title or author name"
90+
variant="pure"
8991
/>
9092
<SearchIntro>
9193
<Markdown>{page.covid19Notice}</Markdown>

0 commit comments

Comments
 (0)