From b74949088818b0f703a4f2211d8472d0d4f6551b Mon Sep 17 00:00:00 2001 From: Andrey Azov Date: Tue, 3 Oct 2023 08:57:23 +0100 Subject: [PATCH] Update GenomeSelectorBySearchQuery (#1032) - Disabled search field in GenomeSelectorBySearchQuery if the search brought results - Added content to show if there have been no search results - Added styles and behaviour to disabled ShadedInput component - Added a TextButton component, which uses native html button and thus is better for accessibility than spans or divs --- .../GenomeSelectorBySearchQuery.tsx | 72 ++++++++++--------- .../species-search-field/AddSpecies.tsx | 55 ++++++++++++++ .../SpeciesSearchField.scss | 16 +++-- .../SpeciesSearchField.tsx | 32 +++------ .../SpeciesSearchResultsSummary.scss | 18 +++++ .../SpeciesSearchResultsSummary.tsx | 51 ++++++++++--- .../SpeciesSelectorResultsView.tsx | 10 ++- src/shared/components/input/Input.scss | 4 ++ .../components/input/ShadedInput.test.tsx | 46 +++++++++++- src/shared/components/input/ShadedInput.tsx | 5 +- .../components/text-button/TextButton.scss | 5 ++ .../components/text-button/TextButton.tsx | 40 +++++++++++ .../input/ShadedInputStory.tsx | 14 ++++ .../text-button/TextButton.stories.tsx | 28 ++++++++ 14 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 src/content/app/new-species-selector/components/species-search-field/AddSpecies.tsx create mode 100644 src/shared/components/text-button/TextButton.scss create mode 100644 src/shared/components/text-button/TextButton.tsx create mode 100644 stories/shared-components/text-button/TextButton.stories.tsx diff --git a/src/content/app/new-species-selector/components/genome-selector-by-search-query/GenomeSelectorBySearchQuery.tsx b/src/content/app/new-species-selector/components/genome-selector-by-search-query/GenomeSelectorBySearchQuery.tsx index ead989989c..29b22bf3b5 100644 --- a/src/content/app/new-species-selector/components/genome-selector-by-search-query/GenomeSelectorBySearchQuery.tsx +++ b/src/content/app/new-species-selector/components/genome-selector-by-search-query/GenomeSelectorBySearchQuery.tsx @@ -20,7 +20,8 @@ import { useLazyGetSpeciesSearchResultsQuery } from 'src/content/app/new-species import useSelectableGenomesTable from 'src/content/app/new-species-selector/components/selectable-genomes-table/useSelectableGenomesTable'; -import SpeciesSearchField from 'src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField'; +import AddSpecies from 'src/content/app/new-species-selector/components/species-search-field/AddSpecies'; +import SpeciesSearchField from '../species-search-field/SpeciesSearchField'; import SpeciesSearchResultsSummary from 'src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary'; import SpeciesSearchResultsTable from 'src/content/app/new-species-selector/components/species-search-results-table/SpeciesSearchResultsTable'; @@ -31,19 +32,18 @@ import styles from './GenomeSelectorBySearchQuery.scss'; type Props = { query: string; onSpeciesAdd: (genomes: SpeciesSearchMatch[]) => void; + onClose: () => void; }; const GenomeSelectorBySearchQuery = (props: Props) => { - const { query } = props; - const [hasQueryChangedSinceSubmission, setHasQueryChangedSinceSubmission] = - useState(false); + const { query, onClose } = props; + const [canSubmitSearch, setCanSubmitSearch] = useState(false); const [searchTrigger, result] = useLazyGetSpeciesSearchResultsQuery(); const { currentData } = result; const { genomes, stagedGenomes, - setStagedGenomes, isTableExpanded, onTableExpandToggle, onGenomePreselectToggle @@ -53,44 +53,52 @@ const GenomeSelectorBySearchQuery = (props: Props) => { searchTrigger({ query }); }, []); - const onInput = () => { - setHasQueryChangedSinceSubmission(true); - setStagedGenomes([]); // remove all preselected species because user has changed value of the search field - }; - - const onSearchSubmit = () => { - searchTrigger({ query }); - setHasQueryChangedSinceSubmission(false); + const onSearchInput = () => { + if (!canSubmitSearch) { + setCanSubmitSearch(true); + } }; const onSpeciesAdd = () => { props.onSpeciesAdd(stagedGenomes); }; - const speciesSearchFieldMode = stagedGenomes.length - ? 'species-add' - : 'species-search'; + const onSearchSubmit = () => { + searchTrigger({ query }); + setCanSubmitSearch(false); + }; return (
- - {currentData && !hasQueryChangedSinceSubmission && ( + {currentData?.matches.length ? ( + 0} + onAdd={onSpeciesAdd} + onCancel={onClose} + /> + ) : ( + + )} + + {currentData && ( <> -
- -
+ + {currentData.matches.length > 0 && ( +
+ +
+ )} )}
diff --git a/src/content/app/new-species-selector/components/species-search-field/AddSpecies.tsx b/src/content/app/new-species-selector/components/species-search-field/AddSpecies.tsx new file mode 100644 index 0000000000..04584e96ca --- /dev/null +++ b/src/content/app/new-species-selector/components/species-search-field/AddSpecies.tsx @@ -0,0 +1,55 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import classNames from 'classnames'; + +import ShadedInput from 'src/shared/components/input/ShadedInput'; +import { PrimaryButton } from 'src/shared/components/button/Button'; +import TextButton from 'src/shared/components/text-button/TextButton'; + +import styles from './SpeciesSearchField.scss'; + +export type Props = { + query: string; + canAdd: boolean; + onAdd: () => void; + onCancel: () => void; +}; + +const AddSpecies = (props: Props) => { + const { query, canAdd, onAdd, onCancel } = props; + + return ( +
+ + +
+ + Add + + Cancel +
+
+ ); +}; + +export default AddSpecies; diff --git a/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.scss b/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.scss index 88a17078ba..3582dea257 100644 --- a/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.scss +++ b/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.scss @@ -1,11 +1,11 @@ @import 'src/styles/common'; -.speciesSearchField { +.grid { display: grid; grid-template-areas: 'label .' - 'input button'; - grid-template-columns: 486px 64px; + 'input controls'; + grid-template-columns: 486px max-content; row-gap: 10px; column-gap: 45px; } @@ -19,11 +19,17 @@ grid-area: input; } -.button { - grid-area: button; +.controls { + grid-area: controls; align-self: center; } .submit { --primary-button-color: #{$blue}; } + +.addSpeciesControls { + display: flex; + align-items: center; + gap: 30px; +} diff --git a/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.tsx b/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.tsx index c406143e54..59f3a42b2a 100644 --- a/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.tsx +++ b/src/content/app/new-species-selector/components/species-search-field/SpeciesSearchField.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { type MouseEvent, FormEvent } from 'react'; +import React, { FormEvent } from 'react'; import classNames from 'classnames'; import { useAppDispatch, useAppSelector } from 'src/store'; @@ -27,18 +27,15 @@ import { PrimaryButton } from 'src/shared/components/button/Button'; import styles from './SpeciesSearchField.scss'; -type Mode = 'species-search' | 'species-add'; - export type Props = { onSearchSubmit: () => void; - mode?: Mode; canSubmit?: boolean; onSpeciesAdd?: () => void; onInput?: ((event: FormEvent) => void) | (() => void); }; const SpeciesSearchField = (props: Props) => { - const { mode = 'species-search', canSubmit = true } = props; + const { canSubmit = true } = props; const dispatch = useAppDispatch(); const query = useAppSelector(getSpeciesSearchQuery); @@ -53,13 +50,8 @@ const SpeciesSearchField = (props: Props) => { props.onSearchSubmit(); }; - const onAdd = (event: MouseEvent) => { - event.preventDefault(); // to avoid triggering form submission - props.onSpeciesAdd?.(); - }; - return ( -
+ { help={helpText} minLength={3} /> - {mode === 'species-search' ? ( - - Find - - ) : ( - - Add - - )} + + Find + ); }; diff --git a/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.scss b/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.scss index c8cebb31a8..ddb2b667b9 100644 --- a/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.scss +++ b/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.scss @@ -9,3 +9,21 @@ .searchMatchesCount { font-weight: $bold; } + +.noMatchesMessage { + color: $red; + margin-top: 40px; + margin-bottom: 30px; +} + +.searchHelp p { + margin: 0; +} + +.searchHelp ul { + padding-inline-start: 20px; +} + +.searchHelp li { + list-style: disc; +} diff --git a/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.tsx b/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.tsx index 923687b73d..03b8fc2508 100644 --- a/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.tsx +++ b/src/content/app/new-species-selector/components/species-search-results-summary/SpeciesSearchResultsSummary.tsx @@ -28,22 +28,57 @@ type Props = { // TODO: add a filter component to this section const SpeciesSearchResultsSummary = (props: Props) => { - const searchMatchesCount = props.searchResult?.meta.total_count; + const searchMatchesCount = props.searchResult?.meta.total_count ?? 0; - // TODO: style the contents differently if the results are from a popular species + return searchMatchesCount > 0 ? ( + + ) : ( + + ); +}; + +const SuccessfulSearchResults = (props: { count: number }) => { + const { count } = props; return (
- {searchMatchesCount && searchMatchesCount > 1 && ( + + {count} results + +
+ ); +}; + +const NoResults = () => { + return ( +
+
- - {searchMatchesCount} - {' '} - results + 0 results - )} +
+
+ Sorry, we don’t recognise, or may not have data for this species +
+
); }; +const SearchHelp = () => { + return ( +
+

+ In order to help you find what you’re really looking for, could we + suggest +

+
    +
  • only search for a species
  • +
  • use a full name where possible
  • +
  • try a different name or identifier
  • +
+
+ ); +}; + export default SpeciesSearchResultsSummary; diff --git a/src/content/app/new-species-selector/views/species-selector-results-view/SpeciesSelectorResultsView.tsx b/src/content/app/new-species-selector/views/species-selector-results-view/SpeciesSelectorResultsView.tsx index 4ec2b653f7..cc457bbf7a 100644 --- a/src/content/app/new-species-selector/views/species-selector-results-view/SpeciesSelectorResultsView.tsx +++ b/src/content/app/new-species-selector/views/species-selector-results-view/SpeciesSelectorResultsView.tsx @@ -46,12 +46,12 @@ const SpeciesSelectorResultslView = () => { return ( - + ); }; -const Content = () => { +const Content = (props: { onClose: () => void }) => { const modalView = useAppSelector(getSpeciesSelectorModalView); const query = useAppSelector(getSpeciesSearchQuery); const selectedPopularSpecies = useAppSelector(getSelectedPopularSpecies); @@ -62,7 +62,11 @@ const Content = () => { }; return modalView === 'species-search' ? ( - + ) : selectedPopularSpecies ? ( ', () => { expect(component.classList).toContain('shadedInputWrapperSmall'); }); + it('cannot be interacted with if disabled', async () => { + // before disabling + const { container, rerender } = render(); + const wrapper = container.querySelector( + '.shadedInputWrapper' + ) as HTMLElement; + const inputElement = container.querySelector('input') as HTMLInputElement; + + const inputText = 'Hello world'; + + await userEvent.type(inputElement, inputText); + + expect(inputElement.value).toBe(inputText); + expect(wrapper.classList.contains('shadedInputDisabled')).toBe(false); + + rerender(); + + await userEvent.type(inputElement, 'Goodbye!'); + + // the disabled input element should still be showing the initial text; + // but no additional interactions should be able to modify it + expect(inputElement.value).toBe(inputText); + expect(wrapper.classList.contains('shadedInputDisabled')).toBe(true); + }); + describe('help element', () => { - it('can show help element', () => { + it('appears if help text is provided', () => { // no help element rendered if no help text provided const { container, rerender } = render(); expect(container.querySelector('.rightCorner')).toBeFalsy(); @@ -62,6 +87,18 @@ describe('', () => { container.querySelector('.rightCorner .questionButton') ).toBeTruthy(); }); + + it('does not show up in a disabled shaded input', () => { + // we already know fron another test + // that a help element appears if help text is provided + const { container } = render( + + ); + + expect( + container.querySelector('.rightCorner .questionButton') + ).toBeFalsy(); + }); }); describe('of type search', () => { @@ -103,5 +140,12 @@ describe('', () => { expect(inputElement.value).toBe(''); expect(onInput).toHaveBeenCalledTimes(1); }); + + it('does not show the clear button if input is disabled', async () => { + const { container } = render( + + ); + expect(container.querySelector('.closeButton')).toBeFalsy(); + }); }); }); diff --git a/src/shared/components/input/ShadedInput.tsx b/src/shared/components/input/ShadedInput.tsx index e10f59ad13..f3c663fb36 100644 --- a/src/shared/components/input/ShadedInput.tsx +++ b/src/shared/components/input/ShadedInput.tsx @@ -45,6 +45,7 @@ export type Props = Omit, 'size'> & { const ShadedInput = (props: Props, ref: ForwardedRef) => { const { className: classNameFromProps, + disabled = false, size, help, type = 'text', @@ -64,6 +65,7 @@ const ShadedInput = (props: Props, ref: ForwardedRef) => { styles.shadedInputWrapper, classNameFromProps, { + [styles.shadedInputDisabled]: disabled, [styles.shadedInputWrapperLarge]: size === 'large', [styles.shadedInputWrapperSmall]: size === 'small' } @@ -83,11 +85,12 @@ const ShadedInput = (props: Props, ref: ForwardedRef) => {
- {rightCornerContent && ( + {!disabled && rightCornerContent && (
{rightCornerContent}
)}
diff --git a/src/shared/components/text-button/TextButton.scss b/src/shared/components/text-button/TextButton.scss new file mode 100644 index 0000000000..4606e49820 --- /dev/null +++ b/src/shared/components/text-button/TextButton.scss @@ -0,0 +1,5 @@ +@import 'src/styles/common'; + +.textButton { + color: var(--text-button-color, #{$blue}); +} diff --git a/src/shared/components/text-button/TextButton.tsx b/src/shared/components/text-button/TextButton.tsx new file mode 100644 index 0000000000..d9565d873a --- /dev/null +++ b/src/shared/components/text-button/TextButton.tsx @@ -0,0 +1,40 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + type DetailedHTMLProps, + type ButtonHTMLAttributes +} from 'react'; +import classNames from 'classnames'; + +import styles from './TextButton.scss'; + +type Props = DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +>; + +export const TextButton = (props: Props) => { + const buttonClasses = classNames(styles.textButton, props.className); + + return ( + + ); +}; + +export default TextButton; diff --git a/stories/shared-components/input/ShadedInputStory.tsx b/stories/shared-components/input/ShadedInputStory.tsx index 736389ecc7..4282eb07d3 100644 --- a/stories/shared-components/input/ShadedInputStory.tsx +++ b/stories/shared-components/input/ShadedInputStory.tsx @@ -32,6 +32,7 @@ export const ShadedInputPlayground = () => { const [withPlaceholder, setWithPlaceholder] = useState(false); const [withHelp, setWithHelp] = useState(false); const [isSearch, setIsSearch] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); const wrapperClasses = classNames(styles.shadedInputWrapper, { [styles.greyStage]: isDarkBackground @@ -47,6 +48,7 @@ export const ShadedInputPlayground = () => { minLength={minLength} help={withHelp ? helpText : undefined} type={isSearch ? 'search' : 'text'} + disabled={isDisabled} />
@@ -63,6 +65,8 @@ export const ShadedInputPlayground = () => { setWithHelp={setWithHelp} isSearch={isSearch} setIsSearch={setIsSearch} + isDisabled={isDisabled} + setIsDisabled={setIsDisabled} />
@@ -82,6 +86,8 @@ const Options = (props: { setWithHelp: (x: boolean) => void; isSearch: boolean; setIsSearch: (x: boolean) => void; + isDisabled: boolean; + setIsDisabled: (x: boolean) => void; }) => { const [minLength, setMinLength] = useState(defaultMinLength); @@ -161,6 +167,14 @@ const Options = (props: { onChange={onMinimumLengthChange} /> + ); }; diff --git a/stories/shared-components/text-button/TextButton.stories.tsx b/stories/shared-components/text-button/TextButton.stories.tsx new file mode 100644 index 0000000000..3b2fbe8b8f --- /dev/null +++ b/stories/shared-components/text-button/TextButton.stories.tsx @@ -0,0 +1,28 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import TextButton from 'src/shared/components/text-button/TextButton'; + +export const TextButtonStory = { + name: 'default', + render: () => Click me +}; + +export default { + title: 'Components/Shared Components/TextButton' +};