Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): add info button component #1287

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages_rs/nextclade-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"types:generate": "yarn run:node:prod tools/generateTypes.js -i src/gen/_SchemaRoot.json -o src/gen/_SchemaRoot.d.ts"
},
"dependencies": {
"@floating-ui/react": "0.26.1",
"animate.css": "4.1.1",
"auspice": "2.51.0",
"autoprefixer": "10.4.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface CardCollapsibleProps extends PropsWithChildren<CardProps> {
}

export function CardCollapsible({ header, children, defaultCollapsed = true, ...restProps }: CardCollapsibleProps) {
const [collapsed, toggleCollapsed] = useToggle(defaultCollapsed)
const { state: collapsed, toggle: toggleCollapsed } = useToggle(defaultCollapsed)
const isOpen = useMemo(() => !collapsed && !!children, [children, collapsed])
const Icon = useMemo(
() => <ArrowIcon color={children ? '#999' : 'transparent'} size={22} $rotated={isOpen} />,
Expand Down
87 changes: 87 additions & 0 deletions packages_rs/nextclade-web/src/components/Common/InfoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { PropsWithChildren } from 'react'
import { Button as ButtonBase, CardBody } from 'reactstrap'
import styled, { useTheme } from 'styled-components'
import { FaInfo as InfoIcon } from 'react-icons/fa6'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { useToggle } from 'src/hooks/useToggle'
import {
useFloating,
useInteractions,
useFocus,
useDismiss,
useRole,
FloatingPortal,
autoPlacement,
} from '@floating-ui/react'

export interface InfoButtonProps {
size?: number
}

export function InfoButton({ size = 18, children }: PropsWithChildren<InfoButtonProps>) {
const { t } = useTranslationSafe()
const theme = useTheme()

const { state: isOpen, toggle, setState: setIsOpen } = useToggle(false)
const { refs, context, floatingStyles } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [autoPlacement()],
})
const dismiss = useDismiss(context)
const focus = useFocus(context)
const role = useRole(context)
const { getReferenceProps, getFloatingProps } = useInteractions([focus, dismiss, role])

return (
<>
<Button
innerRef={refs.setReference} // NOTE: using `innerRef` to pass ref to `reactstrap` component underneath.
as={ButtonBase} // NOTE: this works with styled-components v5, but "as" prop is removed in v6.
title={t('Click to get help information')}
$size={size}
{...getReferenceProps({ onClick: toggle })}
>
<Icon color={theme.primary} size={size * 0.66} />
</Button>
{isOpen && (
<FloatingPortal>
<Floating ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>
<Card>
<CardBody>{children}</CardBody>
</Card>
</Floating>
</FloatingPortal>
)}
</>
)
}

const Floating = styled.div`
z-index: 1005;
width: 500px;
max-width: 80vw;
max-height: 80vh;
`

const Card = styled.div`
overflow-y: auto;
box-shadow: 1px 1px 10px 5px #0005;
border-radius: 5px;
height: 100%;
background-color: ${(props) => props.theme.bodyBg};
`

const Button = styled.button<{ $size?: number }>`
display: flex;
width: ${(props) => props.$size}px;
height: ${(props) => props.$size}px;
border-radius: ${(props) => props.$size}px;
padding: 0 !important;
margin: auto 0.5rem;
`

const Icon = styled(InfoIcon)`
padding: 0 !important;
margin: auto !important;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { InfoButton } from 'src/components/Common/InfoButton'
import { LinkExternal } from 'src/components/Link/LinkExternal'

export function SelectDatasetHelp() {
const { t } = useTranslationSafe()

return (
<InfoButton>
<p>
{t(
'Nextclade software is built to be agnostic to pathogens it analyzes. The information about concrete pathogens is provided in the form of so-called Nextclade datasets.',
)}
</p>

<p>
{t(
'Datasets vary by the pathogen, strain and other attributes. Each dataset is based on a particular reference sequence. Certain datasets only have enough information for basic analysis, others - more information to allow for more in-depth analysis and checks. Dataset authors periodically update and improve their datasets.',
)}
</p>

<p>
{t(
'You can select one of the datasets manually or to use automatic dataset suggestion function. Automatic suggestion will attempt to guess the most appropriate dataset from your sequence data.',
)}
</p>

<p>
{t(
"If you don't find a dataset for a pathogen or a strain you need, then you can create your own dataset. You can also publish it to our community collection, so that other people can use it too.",
)}
</p>

<p>
{t('Learn more about Nextclade datasets in the {{documentation}}')}
<LinkExternal href="https://docs.nextstrain.org/projects/nextclade/en/stable/user/datasets.html">
{t('documentation')}
</LinkExternal>
{t('.')}
</p>
</InfoButton>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useRecoilState } from 'recoil'
import { Container as ContainerBase } from 'reactstrap'
import { SelectDatasetHelp } from 'src/components/Help/SelectDatasetHelp'
import { DatasetSelectorList } from 'src/components/Main/DatasetSelectorList'
import { SuggestionPanel } from 'src/components/Main/SuggestionPanel'
import { useDatasetSuggestionResults } from 'src/hooks/useRunSeqAutodetect'
Expand Down Expand Up @@ -56,7 +57,10 @@ export function DatasetSelectorImpl({
return (
<Container>
<Header>
<Title>{t('Select dataset')}</Title>
<Title>
<H4Inline>{t('Select dataset')}</H4Inline>
<SelectDatasetHelp />
</Title>

<SearchBox searchTitle={t('Search datasets')} searchTerm={searchTerm} onSearchTermChange={setSearchTerm} />
</Header>
Expand Down Expand Up @@ -104,6 +108,11 @@ const Main = styled.div`
`

const Title = styled.h4`
display: flex;
flex: 1;
`

const H4Inline = styled.h4`
display: inline-flex;
margin: auto 0;
`
23 changes: 20 additions & 3 deletions packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useRouter } from 'next/router'
import React, { useCallback, useEffect, useMemo } from 'react'
import { isNil } from 'lodash'
import { useRecoilState, useRecoilValue } from 'recoil'
import styled from 'styled-components'
import { SelectDatasetHelp } from 'src/components/Help/SelectDatasetHelp'
import { ButtonRun } from 'src/components/Main/ButtonRun'
import { SuggestionPanel } from 'src/components/Main/SuggestionPanel'
import { useRunAnalysis } from 'src/hooks/useRunAnalysis'
import { AutodetectRunState, autodetectRunStateAtom } from 'src/state/autodetect.state'
import { shouldSuggestDatasetsOnDatasetPageAtom } from 'src/state/settings.state'
import styled from 'styled-components'
import { hasRequiredInputsAtom } from 'src/state/inputs.state'
import { datasetCurrentAtom } from 'src/state/dataset.state'
import { DatasetCurrentSummary } from 'src/components/Main/DatasetCurrentSummary'
Expand Down Expand Up @@ -133,7 +134,10 @@ function DatasetCurrentOrSelectButton({ toDatasetSelection }: DatasetCurrentOrSe
return (
<Container>
<Header>
<h4>{text}</h4>
<Title>
<H4Inline>{text}</H4Inline>
<SelectDatasetHelp />
</Title>
</Header>

<Main>
Expand All @@ -150,7 +154,10 @@ function DatasetCurrentOrSelectButton({ toDatasetSelection }: DatasetCurrentOrSe
return (
<Container>
<Header>
<h4>{text}</h4>
<Title>
<H4Inline>{text}</H4Inline>
<SelectDatasetHelp />
</Title>
</Header>

<Main>
Expand All @@ -168,3 +175,13 @@ function DatasetCurrentOrSelectButton({ toDatasetSelection }: DatasetCurrentOrSe
</Container>
)
}

const Title = styled.h4`
display: flex;
flex: 1;
`

const H4Inline = styled.h4`
display: inline-flex;
margin: auto 0;
`
4 changes: 2 additions & 2 deletions packages_rs/nextclade-web/src/hooks/useToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { RecoilState, useRecoilState } from 'recoil'

export type VoidFunc = () => void

export function useToggle(initialState = false): [boolean, VoidFunc, VoidFunc, VoidFunc] {
export function useToggle(initialState = false) {
const [state, setState] = useState(initialState)
const toggle = useCallback(() => setState((state) => !state), [])
const enable = useCallback(() => setState(true), [])
const disable = useCallback(() => setState(false), [])
return [state, toggle, enable, disable]
return { state, setState, toggle, enable, disable }
}

export function useRecoilToggle(recoilState: RecoilState<boolean>) {
Expand Down
41 changes: 41 additions & 0 deletions packages_rs/nextclade-web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,42 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"

"@floating-ui/core@^1.4.2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
dependencies:
"@floating-ui/utils" "^0.1.3"

"@floating-ui/dom@^1.5.1":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
dependencies:
"@floating-ui/core" "^1.4.2"
"@floating-ui/utils" "^0.1.3"

"@floating-ui/react-dom@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20"
integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==
dependencies:
"@floating-ui/dom" "^1.5.1"

"@floating-ui/[email protected]":
version "0.26.1"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.1.tgz#a790bd3cff5f334d9e87d3ec132a3dc39d937800"
integrity sha512-5gyJIJ2tZOPMgmZ/vEcVhdmQiy75b7LPO71sYIiDsxGcZ4hxLuygQWCuT0YXHqppt//Eese+L6t5KnX/gZ3tVA==
dependencies:
"@floating-ui/react-dom" "^2.0.2"
"@floating-ui/utils" "^0.1.5"
tabbable "^6.0.1"

"@floating-ui/utils@^0.1.3", "@floating-ui/utils@^0.1.5":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==

"@google-cloud/common@^3.0.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.10.0.tgz#454d1155bb512109cd83c6183aabbd39f9aabda7"
Expand Down Expand Up @@ -15425,6 +15461,11 @@ synesthesia@^1.0.1:
dependencies:
css-color-names "0.0.3"

tabbable@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==

table@^6.6.0, table@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
Expand Down