diff --git a/packages/components/assets/icons/cross-close.svg b/packages/components/assets/icons/cross-close.svg index f519fe135c..ae5eca3f4b 100644 --- a/packages/components/assets/icons/cross-close.svg +++ b/packages/components/assets/icons/cross-close.svg @@ -1 +1 @@ -cross close \ No newline at end of file +cross close \ No newline at end of file diff --git a/packages/components/assets/icons/eye.svg b/packages/components/assets/icons/eye.svg index 6d0729f94e..7b3dd19111 100644 --- a/packages/components/assets/icons/eye.svg +++ b/packages/components/assets/icons/eye.svg @@ -1,14 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/src/FieldInput/FieldInput.tsx b/packages/components/src/FieldInput/FieldInput.tsx index 2cd156ded2..3602afc967 100644 --- a/packages/components/src/FieldInput/FieldInput.tsx +++ b/packages/components/src/FieldInput/FieldInput.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Flex, Input, Text } from 'theme-ui' +import { Box, Flex, Input, Text } from 'theme-ui' import { CharacterCount } from '../CharacterCount/CharacterCount' @@ -14,6 +14,7 @@ export interface Props extends FieldProps { showCharacterCount?: boolean 'data-cy'?: string customOnBlur?: (event: any) => void + endAdornment?: any } type InputModifiers = { @@ -44,37 +45,62 @@ export const FieldInput = ({ showCharacterCount, minLength, maxLength, + endAdornment, ...rest }: Props) => { const [curLength, setLength] = useState(input?.value?.length ?? 0) + const InputElement = ( + { + if (modifiers) { + e.target.value = processInputModifiers(e.target.value, modifiers) + input.onChange(e) + } + if (customOnBlur) { + customOnBlur(e) + } + input.onBlur() + }} + onChange={(ev) => { + showCharacterCount && setLength(ev.target.value.length) + input.onChange(ev) + }} + /> + ) + return ( {meta.error && meta.touched && ( {meta.error} )} - { - if (modifiers) { - e.target.value = processInputModifiers(e.target.value, modifiers) - input.onChange(e) - } - if (customOnBlur) { - customOnBlur(e) - } - input.onBlur() - }} - onChange={(ev) => { - showCharacterCount && setLength(ev.target.value.length) - input.onChange(ev) - }} - /> + {endAdornment ? ( + + {InputElement} + + {endAdornment} + + + ) : ( + InputElement + )} {showCharacterCount && maxLength && ( , volunteer: , website: , + search: , } diff --git a/packages/components/src/Icon/types.ts b/packages/components/src/Icon/types.ts index eb7e8ef070..bbef14adfc 100644 --- a/packages/components/src/Icon/types.ts +++ b/packages/components/src/Icon/types.ts @@ -68,5 +68,6 @@ export type availableGlyphs = | 'view' | 'volunteer' | 'website' + | 'search' export type IGlyphs = { [k in availableGlyphs]: JSX.Element } diff --git a/packages/components/src/OsmGeocoding/OsmGeocoding.tsx b/packages/components/src/OsmGeocoding/OsmGeocoding.tsx index 7fc285106b..949415b241 100644 --- a/packages/components/src/OsmGeocoding/OsmGeocoding.tsx +++ b/packages/components/src/OsmGeocoding/OsmGeocoding.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' -import { Input } from 'theme-ui' import { useDebouncedCallback } from 'use-debounce' +import { SearchField } from '../SearchField/SearchField' import { OsmGeocodingLoader } from './OsmGeocodingLoader' import { OsmGeocodingResultsList } from './OsmGeocodingResultsList' @@ -94,16 +94,26 @@ export const OsmGeocoding = ({ ref={mainContainerRef} style={{ width: '100%' }} > - { + setQueryLocationService(true) + setSearchValue(value) + }} + onClickDelete={() => { + setSearchValue('') + setQueryLocationService(false) + }} + onClickSearch={() => { + setQueryLocationService(true) + setSearchValue(searchValue) + }} + additionalStyle={{ background: 'white', fontFamily: 'Varela Round', fontSize: '14px', @@ -114,11 +124,6 @@ export const OsmGeocoding = ({ showResultsListing || showLoader ? '5px 5px 0 0' : '5px', marginBottom: 0, }} - onClick={() => setShowResults(true)} - onChange={(event) => { - setQueryLocationService(true) - setSearchValue(event.target.value) - }} /> {showLoader && } {showResultsListing && ( diff --git a/packages/components/src/SearchField/SearchField.stories.tsx b/packages/components/src/SearchField/SearchField.stories.tsx new file mode 100644 index 0000000000..4a825e2299 --- /dev/null +++ b/packages/components/src/SearchField/SearchField.stories.tsx @@ -0,0 +1,25 @@ +import { useState } from 'react' + +import { SearchField } from './SearchField' + +import type { Meta, StoryFn } from '@storybook/react' + +export default { + title: 'Forms/SearchField', + component: SearchField, +} as Meta + +export const Default: StoryFn = () => { + const [searchValue, setSearchValue] = useState('') + + return ( + setSearchValue(value)} + onClickDelete={() => setSearchValue('')} + onClickSearch={() => {}} + /> + ) +} diff --git a/packages/components/src/SearchField/SearchField.tsx b/packages/components/src/SearchField/SearchField.tsx new file mode 100644 index 0000000000..49e7101f21 --- /dev/null +++ b/packages/components/src/SearchField/SearchField.tsx @@ -0,0 +1,96 @@ +import { Box, Input } from 'theme-ui' + +import { Icon } from '../Icon/Icon' + +import type { ThemeUIStyleObject } from 'theme-ui' + +export type Props = { + autoComplete?: string + name?: string + id?: string + dataCy: string + placeHolder: string + value: string + onChange: (value: string) => void + onClickDelete: () => void + onClickSearch: () => void + additionalStyle?: ThemeUIStyleObject +} + +export const SearchField = (props: Props) => { + const { + autoComplete = 'on', + name = 'rand-name', + id = 'rand-id', + dataCy, + placeHolder, + value, + onChange, + onClickDelete, + onClickSearch, + additionalStyle = {}, + } = props + + return ( + + onChange(e.target.value)} + sx={{ + paddingRight: 11, + '::-webkit-search-cancel-button': { + display: 'none', + }, + '::-ms-clear': { + display: 'none', + }, + ...additionalStyle, + }} + /> + + {value && ( + + )} + + + + ) +} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index daac4d9d34..989f8c3066 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -56,6 +56,7 @@ export { OsmGeocoding } from './OsmGeocoding/OsmGeocoding' export { PinProfile } from './PinProfile/PinProfile' export { ProfileLink } from './ProfileLink/ProfileLink' export { ResearchEditorOverview } from './ResearchEditorOverview/ResearchEditorOverview' +export { SearchField } from './SearchField/SearchField' export { Select } from './Select/Select' export { SettingsFormWrapper } from './SettingsFormWrapper/SettingsFormWrapper' export { SiteFooter } from './SiteFooter/SiteFooter' @@ -69,7 +70,5 @@ export { UserEngagementWrapper } from './UserEngagementWrapper/UserEngagementWra export { Username } from './Username/Username' export { UserStatistics } from './UserStatistics/UserStatistics' export { VideoPlayer } from './VideoPlayer/VideoPlayer' - -// export { IImageGalleryItem } from './ImageGallery/ImageGallery' export type { availableGlyphs } from './Icon/types' export type { ITab } from './SettingsFormWrapper/SettingsFormTab' diff --git a/src/assets/icons/cross-close.svg b/src/assets/icons/cross-close.svg index f519fe135c..ff2033b1bb 100644 --- a/src/assets/icons/cross-close.svg +++ b/src/assets/icons/cross-close.svg @@ -1 +1,4 @@ -cross close \ No newline at end of file + + + + \ No newline at end of file diff --git a/src/assets/icons/icon-search.svg b/src/assets/icons/icon-search.svg index dab0ec1793..4c8bd884a9 100644 --- a/src/assets/icons/icon-search.svg +++ b/src/assets/icons/icon-search.svg @@ -1 +1,10 @@ -icon search \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/src/common/Form/PasswordField.tsx b/src/common/Form/PasswordField.tsx index 49339b9697..0a84d787a6 100644 --- a/src/common/Form/PasswordField.tsx +++ b/src/common/Form/PasswordField.tsx @@ -1,34 +1,32 @@ import { useState } from 'react' import { Field } from 'react-final-form' import { Icon } from 'oa-components' -import { Box } from 'theme-ui' export const PasswordField = ({ name, component, ...rest }) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false) return ( - - - + setIsPasswordVisible(!isPasswordVisible)} size="25" - > - - + /> + } + required + /> ) } diff --git a/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx b/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx index 12e85d1738..7e973a7173 100644 --- a/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx +++ b/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import debounce from 'debounce' -import { Select } from 'oa-components' +import { SearchField, Select } from 'oa-components' import { FieldContainer } from 'src/common/Form/FieldContainer' -import { Flex, Input } from 'theme-ui' +import { Flex } from 'theme-ui' import { CategoriesSelectV2 } from '../../../common/Category/CategoriesSelectV2' import { howtoService, HowtosSearchParams } from '../../howto.service' @@ -15,6 +15,7 @@ import type { HowtoSortOption } from './HowtoSortOptions' export const HowtoFilterHeader = () => { const [categories, setCategories] = useState([]) + const [searchString, setSearchString] = useState('') const [searchParams, setSearchParams] = useSearchParams() const categoryParam = searchParams.get(HowtosSearchParams.category) @@ -23,11 +24,17 @@ export const HowtoFilterHeader = () => { const sort = searchParams.get(HowtosSearchParams.sort) as HowtoSortOption const _inputStyle = { - width: ['100%', '100%', '200px'], + width: ['100%', '100%', '230px'], mr: [0, 0, 2], mb: [3, 3, 0], } + useEffect(() => { + if (q && q.length > 0) { + setSearchString(q) + } + }, [q]) + useEffect(() => { const initCategories = async () => { const categories = (await howtoService.getHowtoCategories()) || [] @@ -57,22 +64,26 @@ export const HowtoFilterHeader = () => { const onSearchInputChange = useCallback( debounce((value: string) => { - const params = new URLSearchParams(searchParams.toString()) - params.set(HowtosSearchParams.q, value) - - if (value.length > 0 && sort !== 'MostRelevant') { - params.set(HowtosSearchParams.sort, 'MostRelevant') - } - - if (value.length === 0 || !value) { - params.set(HowtosSearchParams.sort, 'Newest') - } - - setSearchParams(params) + searchValue(value) }, 500), [searchParams], ) + const searchValue = (value: string) => { + const params = new URLSearchParams(searchParams.toString()) + params.set(HowtosSearchParams.q, value) + + if (value.length > 0 && sort !== 'MostRelevant') { + params.set(HowtosSearchParams.sort, 'MostRelevant') + } + + if (value.length === 0 || !value) { + params.set(HowtosSearchParams.sort, 'Newest') + } + + setSearchParams(params) + } + return ( { - onSearchInputChange(e.target.value)} + { + setSearchString(value) + onSearchInputChange(value) + }} + onClickDelete={() => { + setSearchString('') + searchValue('') + }} + onClickSearch={() => searchValue(searchString + 'asd')} /> diff --git a/src/pages/Question/QuestionFilterHeader.tsx b/src/pages/Question/QuestionFilterHeader.tsx index 798326293c..65fe2ece95 100644 --- a/src/pages/Question/QuestionFilterHeader.tsx +++ b/src/pages/Question/QuestionFilterHeader.tsx @@ -1,13 +1,13 @@ import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import debounce from 'debounce' -import { Select } from 'oa-components' +import { SearchField, Select } from 'oa-components' import { FieldContainer } from 'src/common/Form/FieldContainer' import { QuestionSearchParams, questionService, } from 'src/pages/Question/question.service' -import { Flex, Input } from 'theme-ui' +import { Flex } from 'theme-ui' import { CategoriesSelectV2 } from '../common/Category/CategoriesSelectV2' import { listing } from './labels' @@ -18,6 +18,7 @@ import type { QuestionSortOption } from './QuestionSortOptions' export const QuestionFilterHeader = () => { const [categories, setCategories] = useState([]) + const [searchString, setSearchString] = useState('') const [searchParams, setSearchParams] = useSearchParams() const categoryParam = searchParams.get(QuestionSearchParams.category) @@ -26,7 +27,7 @@ export const QuestionFilterHeader = () => { const sort = searchParams.get(QuestionSearchParams.sort) as QuestionSortOption const _inputStyle = { - width: ['100%', '100%', '200px'], + width: ['100%', '100%', '230px'], mr: [0, 0, 2], mb: [3, 3, 0], } @@ -59,22 +60,26 @@ export const QuestionFilterHeader = () => { const onSearchInputChange = useCallback( debounce((value: string) => { - const params = new URLSearchParams(searchParams.toString()) - params.set('q', value) - - if (value.length > 0 && sort !== 'MostRelevant') { - params.set('sort', 'MostRelevant') - } - - if (value.length === 0 || !value) { - params.set('sort', 'Newest') - } - - setSearchParams(params) + searchValue(value) }, 500), [searchParams], ) + const searchValue = (value: string) => { + const params = new URLSearchParams(searchParams.toString()) + params.set('q', value) + + if (value.length > 0 && sort !== 'MostRelevant') { + params.set('sort', 'MostRelevant') + } + + if (value.length === 0 || !value) { + params.set('sort', 'Newest') + } + + setSearchParams(params) + } + return ( { - onSearchInputChange(e.target.value)} + { + setSearchString(value) + onSearchInputChange(value) + }} + onClickDelete={() => { + setSearchString('') + searchValue('') + }} + onClickSearch={() => searchValue(searchString + 'asd')} /> diff --git a/src/pages/Research/Content/ResearchFilterHeader.tsx b/src/pages/Research/Content/ResearchFilterHeader.tsx index cb7ea7dcfd..049a546166 100644 --- a/src/pages/Research/Content/ResearchFilterHeader.tsx +++ b/src/pages/Research/Content/ResearchFilterHeader.tsx @@ -1,10 +1,10 @@ import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import debounce from 'debounce' -import { Select } from 'oa-components' +import { SearchField, Select } from 'oa-components' import { ResearchStatus } from 'oa-shared' import { FieldContainer } from 'src/common/Form/FieldContainer' -import { Flex, Input } from 'theme-ui' +import { Flex } from 'theme-ui' import { CategoriesSelectV2 } from '../../common/Category/CategoriesSelectV2' import { listing } from '../labels' @@ -25,8 +25,9 @@ const researchStatusOptions = [ export const ResearchFilterHeader = () => { const [categories, setCategories] = useState([]) - const [searchParams, setSearchParams] = useSearchParams() + const [searchString, setSearchString] = useState('') + const [searchParams, setSearchParams] = useSearchParams() const categoryParam = searchParams.get(ResearchSearchParams.category) const category = categories?.find((x) => x.value === categoryParam) ?? null const q = searchParams.get(ResearchSearchParams.q) @@ -36,7 +37,7 @@ export const ResearchFilterHeader = () => { // TODO: create a library component for this const _inputStyle = { - width: ['100%', '100%', '200px'], + width: ['100%', '100%', '230px'], mr: [0, 0, 2], mb: [3, 3, 0], } @@ -69,22 +70,26 @@ export const ResearchFilterHeader = () => { const onSearchInputChange = useCallback( debounce((value: string) => { - const params = new URLSearchParams(searchParams.toString()) - params.set(ResearchSearchParams.q, value) - - if (value.length > 0 && sort !== 'MostRelevant') { - params.set(ResearchSearchParams.sort, 'MostRelevant') - } - - if (value.length === 0 || !value) { - params.set(ResearchSearchParams.sort, 'LatestUpdated') - } - - setSearchParams(params) + searchValue(value) }, 1000), [searchParams], ) + const searchValue = (value: string) => { + const params = new URLSearchParams(searchParams.toString()) + params.set(ResearchSearchParams.q, value) + + if (value.length > 0 && sort !== 'MostRelevant') { + params.set(ResearchSearchParams.sort, 'MostRelevant') + } + + if (value.length === 0 || !value) { + params.set(ResearchSearchParams.sort, 'LatestUpdated') + } + + setSearchParams(params) + } + return ( { {/* Text search */} - onSearchInputChange(e.target.value)} + { + setSearchString(value) + onSearchInputChange(value) + }} + onClickDelete={() => { + setSearchString('') + searchValue('') + }} + onClickSearch={() => searchValue(searchString + 'asd')} />