Ajout du selecteur radio par block

pull/2077/head
Jérémy Rialland 2022-03-08 15:00:08 +01:00 committed by Johan Girod
parent 4eff86a18a
commit d285b56ccf
5 changed files with 121 additions and 10 deletions

View File

@ -3,6 +3,8 @@ secteur:
une possibilité:
choix obligatoire: oui
possibilités: [S1, S1bis, S2]
metadata:
component: ToggleRadioBlock
secteur . S1:
valeur: secteur = 'S1'

View File

@ -3,6 +3,7 @@ import Emoji from '@/components/utils/Emoji'
import { Markdown } from '@/components/utils/markdown'
import ButtonHelp from '@/design-system/buttons/ButtonHelp'
import { Radio, RadioGroup, ToggleGroup } from '@/design-system/field'
import { RadioBlock } from '@/design-system/field/Radio/Radio'
import { Spacing } from '@/design-system/layout'
import { H4 } from '@/design-system/typography/heading'
import { DottedName } from 'modele-social'
@ -48,20 +49,28 @@ export const HiddenOptionContext = createContext<Array<DottedName>>([])
export function MultipleAnswerInput<Names extends string = DottedName>({
choice,
type = 'radio',
inline,
...props
}: { choice: Choice } & InputProps<Names>) {
}: {
choice: Choice
type?: 'radio' | 'toggle'
inline?: boolean
} & InputProps<Names>) {
// seront stockées ainsi dans le state :
// [parent object path]: dotted fieldName relative to parent
const { handleChange, defaultValue, currentSelection } = useSelection(props)
const Component = type === 'toggle' ? ToggleGroup : RadioGroup
return (
<RadioGroup onChange={handleChange} value={currentSelection ?? undefined}>
<Component onChange={handleChange} value={currentSelection ?? undefined}>
<RadioChoice
autoFocus={defaultValue}
choice={choice}
rootDottedName={props.dottedName}
inline={inline}
/>
</RadioGroup>
</Component>
)
}
@ -69,10 +78,12 @@ function RadioChoice<Names extends string = DottedName>({
choice,
autoFocus,
rootDottedName,
inline,
}: {
choice: Choice
autoFocus?: string
rootDottedName: Names
inline?: boolean
}) {
const relativeDottedName = (radioDottedName: string) =>
radioDottedName.split(rootDottedName + ' . ')[1]
@ -96,10 +107,14 @@ function RadioChoice<Names extends string = DottedName>({
<H4 id={node.dottedName + '-legend'}>{node.title}</H4>
<Spacing lg />
<StyledSubRadioGroup>
<RadioChoice choice={node} rootDottedName={rootDottedName} />
<RadioChoice
inline={inline}
choice={node}
rootDottedName={rootDottedName}
/>
</StyledSubRadioGroup>
</div>
) : (
) : inline ? (
<span>
<Radio
autoFocus={
@ -116,6 +131,16 @@ function RadioChoice<Names extends string = DottedName>({
</ButtonHelp>
)}
</span>
) : (
<RadioBlock
autoFocus={
autoFocus === `'${relativeDottedName(node.dottedName)}'`
}
value={`'${relativeDottedName(node.dottedName)}'`}
title={node.title}
emoji={node.rawNode.icônes}
description={node.rawNode.description}
/>
)}
</Fragment>
))}

View File

@ -9,6 +9,7 @@ import Engine, {
Evaluation,
PublicodesExpression,
reduceAST,
Rule,
RuleNode,
} from 'publicodes'
import React, { useContext } from 'react'
@ -59,11 +60,22 @@ export const binaryQuestion = [
{ value: 'non', label: 'Non' },
] as const
interface RuleWithMetadata<T> extends Rule {
metadata: T
}
const isMetadata = <T extends RuleWithMetadata<unknown>>(
rule: Rule
): rule is T => 'metadata' in rule
// This function takes the unknown rule and finds which React component should
// be displayed to get a user input through successive if statements
// That's not great, but we won't invest more time until we have more diverse
// input components and a better type system.
export default function RuleInput<Names extends string = DottedName>({
export default function RuleInput<
Names extends string = DottedName,
Metadata extends { component?: string } = { component?: string }
>({
dottedName,
onChange,
showSuggestions = true,
@ -90,11 +102,25 @@ export default function RuleInput<Names extends string = DottedName>({
suggestions: showSuggestions ? rule.suggestions : {},
...props,
}
if (
isMetadata<RuleWithMetadata<Metadata>>(rule.rawNode) &&
rule.rawNode.metadata.component === 'ToggleRadioBlock'
) {
return (
<MultipleAnswerInput
{...commonProps}
choice={buildVariantTree(engine, dottedName)}
type="toggle"
/>
)
}
if (getVariant(engine.getRule(dottedName))) {
return (
<MultipleAnswerInput
{...commonProps}
choice={buildVariantTree(engine, dottedName)}
inline
/>
)
}

View File

@ -5,17 +5,25 @@ import { FocusStyle } from '@/design-system/global-style'
import { Body } from '@/design-system/typography/paragraphs'
import React, { createContext, useContext, useRef } from 'react'
import styled, { css } from 'styled-components'
import Emoji from '@/components/utils/Emoji'
import { Strong } from '@/design-system/typography'
import { Markdown } from '@/components/utils/markdown'
const RadioContext = createContext<RadioGroupState | null>(null)
export function Radio(props: AriaRadioProps) {
const { children } = props
export function Radio(
props: AriaRadioProps & {
LabelBodyAs?: Parameters<typeof LabelBody>['0']['as']
}
) {
const { LabelBodyAs: bodyType, ...ariaProps } = props
const { children } = ariaProps
const state = useContext(RadioContext)
if (!state) {
throw new Error("Radio can't be instanciated outside a RadioContext")
}
const ref = useRef(null)
const { inputProps } = useRadio(props, state, ref)
const { inputProps } = useRadio(ariaProps, state, ref)
return (
<label>
@ -25,7 +33,7 @@ export function Radio(props: AriaRadioProps) {
<OutsideCircle />
<InsideCircle />
</RadioButton>
<LabelBody>{children}</LabelBody>
<LabelBody as={bodyType}>{children}</LabelBody>
</VisibleRadio>
</label>
)
@ -99,6 +107,54 @@ const VisibleRadio = styled.div`
}
`
const RadioLabel = styled.p`
margin: ${({ theme }) => theme.spacings.sm} 0;
font-style: italic;
`
const RadioWrapper = styled.span`
flex: 0 0 100%;
${VisibleRadio} {
width: 100%;
border-radius: var(--radius) !important;
margin-bottom: ${({ theme }) => theme.spacings.xs} !important;
}
${RadioButton} {
align-self: baseline;
margin-top: 0.2rem;
}
`
export function RadioBlock({
value,
title,
emoji,
description,
autoFocus,
}: RadioGroupProps & {
value: string
title: string
emoji?: string
description?: string
autoFocus?: boolean
}) {
return (
<RadioWrapper>
<Radio autoFocus={autoFocus} value={value} LabelBodyAs={'div'}>
<Strong>
{title} {emoji && <Emoji emoji={emoji} />}
</Strong>
{description && (
<Markdown as={RadioLabel}>{description ?? ''}</Markdown>
)}
</Radio>
</RadioWrapper>
)
}
const LabelBody = styled(Body)`
margin: ${({ theme }) => theme.spacings.xs} 0px;
margin-left: ${({ theme }) => theme.spacings.xxs};
@ -149,6 +205,7 @@ export function ToggleGroup(
const ToggleGroupContainer = styled.div<{ hideRadio: boolean }>`
--radius: 0.25rem;
display: inline-flex;
flex-wrap: wrap;
${VisibleRadio} {
position: relative;

View File

@ -24,6 +24,7 @@ export default function ExonérationCovid() {
return (
<>
<EngineProvider value={covidEngine}>
<H3>{covidEngine.getRule('secteur').rawNode.question}</H3>
<RuleInput
dottedName={'secteur'}
onChange={(value) => updateSituation('secteur', value)}