✨(frontend) new Form components
- improve the Form component to abstract the few things we'll certainly do all the time (data parsing, action buttons rendering) - add a Field component, the main way to render form fields. It mainly wraps react aria components with our styling. The Checkbox component is a bit tricky to go around some current limitations with react aria
This commit is contained in:
@@ -233,7 +233,7 @@ const config: Config = {
|
||||
paragraph: { value: '{spacing.1}' },
|
||||
heading: { value: '{spacing.1}' },
|
||||
gutter: { value: '{spacing.1}' },
|
||||
textfield: { value: '{spacing.0.5}' },
|
||||
textfield: { value: '{spacing.1}' },
|
||||
},
|
||||
}),
|
||||
textStyles: defineTextStyles({
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { navigate } from 'wouter/use-browser-location'
|
||||
import { Form } from 'react-aria-components'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import {
|
||||
Button,
|
||||
P,
|
||||
@@ -10,11 +8,13 @@ import {
|
||||
H,
|
||||
VerticallyOffCenter,
|
||||
Dialog,
|
||||
TextField,
|
||||
Form,
|
||||
Field,
|
||||
Ul,
|
||||
} from '@/primitives'
|
||||
import { useCloseDialog } from '@/primitives/Dialog'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { authUrl, useUser } from '@/features/auth'
|
||||
import { navigateToNewRoom } from '@/features/rooms'
|
||||
import { isRoomValid, navigateToNewRoom } from '@/features/rooms'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
|
||||
export const Home = () => {
|
||||
@@ -59,44 +59,33 @@ export const Home = () => {
|
||||
|
||||
const JoinMeetingDialogContent = () => {
|
||||
const { t } = useTranslation('home')
|
||||
const closeDialog = useCloseDialog()
|
||||
const fieldOk = /^.*([a-z]{3}-[a-z]{4}-[a-z]{3})$/
|
||||
return (
|
||||
<Div>
|
||||
<Form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const roomInput = document.getElementById(
|
||||
'join-meeting-input'
|
||||
) as HTMLInputElement
|
||||
const value = roomInput.value
|
||||
const matches = value.match(fieldOk)
|
||||
if (matches) {
|
||||
navigate(`/${matches[1]}`)
|
||||
}
|
||||
onSubmit={(data) => {
|
||||
navigate(`/${(data.roomId as string).trim()}`)
|
||||
}}
|
||||
submitLabel={t('joinInputSubmit')}
|
||||
>
|
||||
<TextField
|
||||
id="join-meeting-input"
|
||||
<Field
|
||||
type="text"
|
||||
name="roomId"
|
||||
label={t('joinInputLabel')}
|
||||
description={t('joinInputExample', {
|
||||
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
|
||||
})}
|
||||
validate={(value) => {
|
||||
if (!fieldOk.test(value)) {
|
||||
return t('joinInputError')
|
||||
}
|
||||
return null
|
||||
return !isRoomValid(value.trim()) ? (
|
||||
<>
|
||||
<p>{t('joinInputError')}</p>
|
||||
<Ul>
|
||||
<li>{window.location.origin}/uio-azer-jkl</li>
|
||||
<li>uio-azer-jkl</li>
|
||||
</Ul>
|
||||
</>
|
||||
) : null
|
||||
}}
|
||||
/>
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{t('joinInputSubmit')}
|
||||
</Button>
|
||||
<Button variant="primary" outline onPress={closeDialog}>
|
||||
{t('cancel', { ns: 'global' })}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
<H lvl={2}>{t('joinMeetingTipHeading')}</H>
|
||||
<P last>{t('joinMeetingTipContent')}</P>
|
||||
|
||||
166
src/frontend/src/primitives/Checkbox.tsx
Normal file
166
src/frontend/src/primitives/Checkbox.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { type ReactNode, useId, useState } from 'react'
|
||||
import {
|
||||
type CheckboxProps as RACCheckboxProps,
|
||||
Checkbox as RACCheckbox,
|
||||
CheckboxContext,
|
||||
} from 'react-aria-components'
|
||||
import { type StyledVariantProps } from '@/styled-system/types'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { FieldErrors } from './FieldErrors'
|
||||
import { FieldDescription } from './FieldDescription'
|
||||
|
||||
// styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html
|
||||
export const StyledCheckbox = styled(RACCheckbox, {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.375,
|
||||
forcedColorAdjust: 'none',
|
||||
width: 'fit-content',
|
||||
'& .mt-Checkbox-checkbox': {
|
||||
borderColor: 'control.border',
|
||||
flexShrink: 0,
|
||||
width: '1.375rem',
|
||||
height: '1.375rem',
|
||||
border: '1px solid',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 200ms',
|
||||
},
|
||||
'& svg': {
|
||||
stroke: 'primary.text',
|
||||
width: '0.875rem',
|
||||
height: '0.875rem',
|
||||
flexShrink: 0,
|
||||
fill: 'none',
|
||||
strokeWidth: '3px',
|
||||
strokeDasharray: '22px',
|
||||
strokeDashoffset: '66',
|
||||
transition: 'all 200ms',
|
||||
},
|
||||
'&[data-pressed] .mt-Checkbox-checkbox': {
|
||||
borderColor: 'focusRing',
|
||||
},
|
||||
'&[data-focus-visible] .mt-Checkbox-checkbox': {
|
||||
outline: '2px solid!',
|
||||
outlineColor: 'focusRing!',
|
||||
outlineOffset: '2px!',
|
||||
},
|
||||
'&[data-selected] .mt-Checkbox-checkbox': {
|
||||
borderColor: 'primary',
|
||||
backgroundColor: 'primary',
|
||||
},
|
||||
'&[data-selected][data-pressed] .mt-Checkbox-checkbox': {
|
||||
borderColor: 'primary.active',
|
||||
backgroundColor: 'primary.active',
|
||||
},
|
||||
'&[data-selected] svg': {
|
||||
strokeDashoffset: '44',
|
||||
},
|
||||
'&[data-mt-checkbox-invalid="true"] .mt-Checkbox-checkbox': {
|
||||
borderColor: 'danger',
|
||||
},
|
||||
'&[data-selected][data-mt-checkbox-invalid="true"] .mt-Checkbox-checkbox': {
|
||||
backgroundColor: 'danger',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
base: {},
|
||||
'& .mt-Checkbox-checkbox': {
|
||||
width: '1.125rem',
|
||||
height: '1.125rem',
|
||||
},
|
||||
'& svg': {
|
||||
width: '0.625rem',
|
||||
height: '0.625rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type CheckboxProps = StyledVariantProps<typeof StyledCheckbox> &
|
||||
RACCheckboxProps & { description?: ReactNode }
|
||||
|
||||
/**
|
||||
* RAC Checkbox wrapper that adds support for description/error messages
|
||||
*
|
||||
* This is because description and error messages are not supported for
|
||||
* standalone checkboxes (see https://github.com/adobe/react-spectrum/issues/6192)
|
||||
*
|
||||
* We do some trickery to go around a few things so that we can trigger
|
||||
* the error message with the `validate` prop like other fields.
|
||||
*
|
||||
* Used internally by checkbox fields and checkbox group fields.
|
||||
*/
|
||||
export const Checkbox = ({
|
||||
isInvalid,
|
||||
description,
|
||||
children,
|
||||
...props
|
||||
}: CheckboxProps) => {
|
||||
const [error, setError] = useState<ReactNode | null>(null)
|
||||
const errorId = useId()
|
||||
const descriptionId = useId()
|
||||
|
||||
if (isInvalid !== undefined) {
|
||||
console.error(
|
||||
'Checkbox: passing isInvalid is not supported, use the validate prop instead'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CheckboxContext.Provider
|
||||
value={{
|
||||
'aria-describedby': [
|
||||
!!description && descriptionId,
|
||||
!!error && errorId,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
// @ts-expect-error Any html attribute is actually valid
|
||||
'data-mt-checkbox-invalid': !!error,
|
||||
}}
|
||||
>
|
||||
<StyledCheckbox {...props}>
|
||||
{(renderProps) => {
|
||||
renderProps.isInvalid && !!props.validate
|
||||
? setError(props.validate(renderProps.isSelected))
|
||||
: setError(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-Checkbox-checkbox">
|
||||
<svg
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 18 18"
|
||||
aria-hidden="true"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
>
|
||||
<polyline points="1 9 7 14 15 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
{typeof children === 'function'
|
||||
? children(renderProps)
|
||||
: children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</StyledCheckbox>
|
||||
</CheckboxContext.Provider>
|
||||
{!!description && (
|
||||
<FieldDescription id={descriptionId}>{description}</FieldDescription>
|
||||
)}
|
||||
{!!error && <FieldErrors id={errorId} errors={[error]} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -118,6 +118,9 @@ export const Dialog = ({ title, children, ...dialogProps }: DialogProps) => {
|
||||
}
|
||||
|
||||
export const useCloseDialog = () => {
|
||||
const dialogState = useContext(OverlayTriggerStateContext)!
|
||||
return dialogState.close
|
||||
const dialogState = useContext(OverlayTriggerStateContext)
|
||||
if (dialogState) {
|
||||
return dialogState.close
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
187
src/frontend/src/primitives/Field.tsx
Normal file
187
src/frontend/src/primitives/Field.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { type ReactNode } from 'react'
|
||||
import {
|
||||
Label,
|
||||
TextField as RACTextField,
|
||||
FieldError as RACFieldError,
|
||||
CheckboxGroup,
|
||||
RadioGroup,
|
||||
type TextFieldProps,
|
||||
type CheckboxProps,
|
||||
type CheckboxGroupProps,
|
||||
type RadioGroupProps,
|
||||
} from 'react-aria-components'
|
||||
import { FieldDescription } from './FieldDescription'
|
||||
import { FieldErrors } from './FieldErrors'
|
||||
import { Input } from './Input'
|
||||
import { Radio } from './Radio'
|
||||
import { Checkbox } from './Checkbox'
|
||||
import { Div } from './Div'
|
||||
|
||||
const FieldWrapper = styled('div', {
|
||||
base: {
|
||||
marginBottom: 'textfield',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledLabel = styled(Label, {
|
||||
base: {
|
||||
display: 'block',
|
||||
},
|
||||
})
|
||||
|
||||
type OmittedRACProps = 'type' | 'label' | 'items' | 'description' | 'validate'
|
||||
type Items = { items: Array<{ value: string; label: ReactNode }> }
|
||||
type PartialTextFieldProps = Omit<TextFieldProps, OmittedRACProps>
|
||||
type PartialCheckboxProps = Omit<CheckboxProps, OmittedRACProps>
|
||||
type PartialCheckboxGroupProps = Omit<CheckboxGroupProps, OmittedRACProps>
|
||||
type PartialRadioGroupProps = Omit<RadioGroupProps, OmittedRACProps>
|
||||
type FieldProps = (
|
||||
| ({
|
||||
type: 'text'
|
||||
items?: never
|
||||
validate?: (
|
||||
value: string
|
||||
) => ReactNode | ReactNode[] | true | null | undefined
|
||||
} & PartialTextFieldProps)
|
||||
| ({
|
||||
type: 'checkbox'
|
||||
validate?: (
|
||||
value: boolean
|
||||
) => ReactNode | ReactNode[] | true | null | undefined
|
||||
items?: never
|
||||
} & PartialCheckboxProps)
|
||||
| ({
|
||||
type: 'checkboxGroup'
|
||||
validate?: (
|
||||
value: string[]
|
||||
) => ReactNode | ReactNode[] | true | null | undefined
|
||||
} & Items &
|
||||
PartialCheckboxGroupProps)
|
||||
| ({
|
||||
type: 'radioGroup'
|
||||
validate?: (
|
||||
value: string | null
|
||||
) => ReactNode | ReactNode[] | true | null | undefined
|
||||
} & Items &
|
||||
PartialRadioGroupProps)
|
||||
) & {
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Form field.
|
||||
*
|
||||
* This is the only component you should need when creating forms, besides the wrapping Form component.
|
||||
*
|
||||
* It has a specific type: a text input, a select, a checkbox, a checkbox group or a radio group.
|
||||
* It can have a `description`, a help text shown below the label.
|
||||
* On submit, it shows the errors that the `validate` prop returns based on the field value.
|
||||
* You can render React nodes as error messages if needed, but you usually return strings.
|
||||
*
|
||||
* You can directly pass HTML input props if needed (like required, pattern, etc)
|
||||
*/
|
||||
export const Field = ({
|
||||
type,
|
||||
label,
|
||||
description,
|
||||
items,
|
||||
validate,
|
||||
...props
|
||||
}: FieldProps) => {
|
||||
const LabelAndDescription = (
|
||||
<>
|
||||
<StyledLabel>{label}</StyledLabel>
|
||||
<FieldDescription slot="description">{description}</FieldDescription>
|
||||
</>
|
||||
)
|
||||
const RACFieldErrors = (
|
||||
<RACFieldError>
|
||||
{({ validationErrors }) => {
|
||||
return <FieldErrors errors={validationErrors} />
|
||||
}}
|
||||
</RACFieldError>
|
||||
)
|
||||
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<RACTextField
|
||||
validate={validate as unknown as TextFieldProps['validate']}
|
||||
{...(props as PartialTextFieldProps)}
|
||||
>
|
||||
{LabelAndDescription}
|
||||
<Input />
|
||||
{RACFieldErrors}
|
||||
</RACTextField>
|
||||
</FieldWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<Checkbox
|
||||
validate={validate as unknown as CheckboxProps['validate']}
|
||||
description={description}
|
||||
{...(props as PartialCheckboxProps)}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</FieldWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'checkboxGroup') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<CheckboxGroup
|
||||
validate={validate as unknown as CheckboxGroupProps['validate']}
|
||||
{...(props as PartialCheckboxGroupProps)}
|
||||
>
|
||||
{LabelAndDescription}
|
||||
<Div marginTop={0.25}>
|
||||
{items.map((item, index) => (
|
||||
<FieldItem last={index === items.length - 1} key={item.value}>
|
||||
<Checkbox size="sm" value={item.value}>
|
||||
{item.label}
|
||||
</Checkbox>
|
||||
</FieldItem>
|
||||
))}
|
||||
</Div>
|
||||
{RACFieldErrors}
|
||||
</CheckboxGroup>
|
||||
</FieldWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'radioGroup') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<RadioGroup
|
||||
validate={validate as unknown as RadioGroupProps['validate']}
|
||||
{...(props as PartialRadioGroupProps)}
|
||||
>
|
||||
{LabelAndDescription}
|
||||
{items.map((item, index) => (
|
||||
<FieldItem last={index === items.length - 1} key={item.value}>
|
||||
<Radio value={item.value}>{item.label}</Radio>
|
||||
</FieldItem>
|
||||
))}
|
||||
{RACFieldErrors}
|
||||
</RadioGroup>
|
||||
</FieldWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const FieldItem = ({
|
||||
children,
|
||||
last,
|
||||
}: {
|
||||
children: ReactNode
|
||||
last?: boolean
|
||||
}) => {
|
||||
return <Div {...(!last ? { marginBottom: 0.25 } : {})}>{children}</Div>
|
||||
}
|
||||
20
src/frontend/src/primitives/FieldDescription.tsx
Normal file
20
src/frontend/src/primitives/FieldDescription.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { Text, TextProps } from 'react-aria-components'
|
||||
|
||||
const StyledDescription = styled(Text, {
|
||||
base: {
|
||||
display: 'block',
|
||||
textStyle: 'sm',
|
||||
color: 'default.subtle-text',
|
||||
marginBottom: 0.125,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Styled field description.
|
||||
*
|
||||
* Used internally by Fields.
|
||||
*/
|
||||
export const FieldDescription = (props: TextProps) => {
|
||||
return <StyledDescription {...props} />
|
||||
}
|
||||
36
src/frontend/src/primitives/FieldErrors.tsx
Normal file
36
src/frontend/src/primitives/FieldErrors.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { type ReactNode, Fragment } from 'react'
|
||||
|
||||
const StyledErrors = styled('div', {
|
||||
base: {
|
||||
display: 'block',
|
||||
textStyle: 'sm',
|
||||
color: 'danger',
|
||||
marginTop: 0.125,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Styled list of given errors.
|
||||
*
|
||||
* Used internally by Fields.
|
||||
*/
|
||||
export const FieldErrors = ({
|
||||
id,
|
||||
errors,
|
||||
}: {
|
||||
id?: string
|
||||
errors: ReactNode[]
|
||||
}) => {
|
||||
return (
|
||||
<StyledErrors id={id}>
|
||||
{errors.map((error, i) => {
|
||||
return typeof error === 'string' ? (
|
||||
<p key={error}>{error}</p>
|
||||
) : (
|
||||
<Fragment key={i}>{error}</Fragment>
|
||||
)
|
||||
})}
|
||||
</StyledErrors>
|
||||
)
|
||||
}
|
||||
61
src/frontend/src/primitives/Form.tsx
Normal file
61
src/frontend/src/primitives/Form.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { type FormEvent } from 'react'
|
||||
import { Form as RACForm, type FormProps } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { Button, useCloseDialog } from '@/primitives'
|
||||
|
||||
/**
|
||||
* From wrapper that exposes form data on submit and adds submit/cancel buttons
|
||||
*
|
||||
* Wrap all your Fields in this component.
|
||||
* If the form is in a dialog, the cancel button closes the dialog unless you pass a custom onCancelButtonPress handler.
|
||||
*/
|
||||
export const Form = ({
|
||||
onSubmit,
|
||||
submitLabel,
|
||||
withCancelButton = true,
|
||||
onCancelButtonPress,
|
||||
children,
|
||||
...props
|
||||
}: Omit<FormProps, 'onSubmit'> & {
|
||||
onSubmit?: (
|
||||
data: {
|
||||
[k: string]: FormDataEntryValue
|
||||
},
|
||||
event: FormEvent<HTMLFormElement>
|
||||
) => void
|
||||
submitLabel: string
|
||||
withCancelButton?: boolean
|
||||
onCancelButtonPress?: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const closeDialog = useCloseDialog()
|
||||
const onCancel = withCancelButton
|
||||
? onCancelButtonPress || closeDialog
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<RACForm
|
||||
{...props}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const formData = Object.fromEntries(new FormData(event.currentTarget))
|
||||
if (onSubmit) {
|
||||
onSubmit(formData, event)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{!!onCancel && (
|
||||
<Button variant="primary" outline onPress={() => onCancel()}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</RACForm>
|
||||
)
|
||||
}
|
||||
19
src/frontend/src/primitives/Input.tsx
Normal file
19
src/frontend/src/primitives/Input.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { Input as RACInput } from 'react-aria-components'
|
||||
|
||||
/**
|
||||
* Styled RAC Input.
|
||||
*
|
||||
* Used internally by Fields.
|
||||
*/
|
||||
export const Input = styled(RACInput, {
|
||||
base: {
|
||||
width: 'full',
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.25,
|
||||
border: '1px solid',
|
||||
borderColor: 'control.border',
|
||||
color: 'control.text',
|
||||
borderRadius: 4,
|
||||
},
|
||||
})
|
||||
89
src/frontend/src/primitives/Radio.tsx
Normal file
89
src/frontend/src/primitives/Radio.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
type RadioProps as RACRadioProps,
|
||||
Radio as RACRadio,
|
||||
} from 'react-aria-components'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { type StyledVariantProps } from '@/styled-system/types'
|
||||
|
||||
// styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html and changed for round radios
|
||||
export const StyledRadio = styled(RACRadio, {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.375,
|
||||
forcedColorAdjust: 'none',
|
||||
width: 'fit-content',
|
||||
'& .mt-Radio': {
|
||||
borderRadius: 'full',
|
||||
flexShrink: 0,
|
||||
width: '1.125rem',
|
||||
height: '1.125rem',
|
||||
border: '1px solid {colors.control.border}',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 200ms',
|
||||
},
|
||||
'& .mt-Radio-check': {
|
||||
width: '0.5rem',
|
||||
height: '0.5rem',
|
||||
borderRadius: 'full',
|
||||
backgroundColor: 'transparent',
|
||||
transition: 'all 200ms',
|
||||
},
|
||||
'&[data-pressed] .mt-Radio': {
|
||||
borderColor: 'primary.active',
|
||||
},
|
||||
'&[data-focus-visible] .mt-Radio': {
|
||||
outline: '2px solid!',
|
||||
outlineColor: 'focusRing!',
|
||||
outlineOffset: '2px!',
|
||||
},
|
||||
'&[data-selected] .mt-Radio': {
|
||||
borderColor: 'primary',
|
||||
},
|
||||
'&[data-selected] .mt-Radio-check': {
|
||||
backgroundColor: 'primary',
|
||||
},
|
||||
'&[data-selected][data-pressed] .mt-Radio-check': {
|
||||
backgroundColor: 'primary.active',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
base: {},
|
||||
'& .radio': {
|
||||
width: '1.125rem',
|
||||
height: '1.125rem',
|
||||
},
|
||||
'& svg': {
|
||||
width: '0.625rem',
|
||||
height: '0.625rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type RadioProps = StyledVariantProps<typeof StyledRadio> & RACRadioProps
|
||||
|
||||
/**
|
||||
* Styled radio button.
|
||||
*
|
||||
* Used internally by RadioGroups in Fields.
|
||||
*/
|
||||
export const Radio = ({ children, ...props }: RadioProps) => {
|
||||
return (
|
||||
<StyledRadio {...props}>
|
||||
{(renderProps) => (
|
||||
<>
|
||||
<div className="mt-Radio" aria-hidden="true">
|
||||
<div className="mt-Radio-check" />
|
||||
</div>
|
||||
{typeof children === 'function' ? children(renderProps) : children}
|
||||
</>
|
||||
)}
|
||||
</StyledRadio>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
TextField as RACTextField,
|
||||
Text,
|
||||
FieldError,
|
||||
} from 'react-aria-components'
|
||||
|
||||
const StyledRACTextField = styled(RACTextField, {
|
||||
base: {
|
||||
marginBottom: 'textfield',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledLabel = styled(Label, {
|
||||
base: {
|
||||
display: 'block',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledInput = styled(Input, {
|
||||
base: {
|
||||
width: 'full',
|
||||
paddingY: 0.125,
|
||||
paddingX: 0.25,
|
||||
border: '1px solid',
|
||||
borderColor: 'control.border',
|
||||
color: 'control.text',
|
||||
borderRadius: 4,
|
||||
},
|
||||
})
|
||||
|
||||
const StyledDescription = styled(Text, {
|
||||
base: {
|
||||
display: 'block',
|
||||
textStyle: 'sm',
|
||||
color: 'default.subtle-text',
|
||||
marginTop: 0.125,
|
||||
},
|
||||
})
|
||||
|
||||
const StyledFieldError = styled(FieldError, {
|
||||
base: {
|
||||
display: 'block',
|
||||
textStyle: 'sm',
|
||||
color: 'danger',
|
||||
marginTop: 0.125,
|
||||
},
|
||||
})
|
||||
|
||||
export const TextField = ({ validate, label, description, ...inputProps }) => {
|
||||
const labelFor = inputProps.id
|
||||
return (
|
||||
<StyledRACTextField validate={validate}>
|
||||
<StyledLabel htmlFor={labelFor}>{label}</StyledLabel>
|
||||
<StyledInput {...inputProps} />
|
||||
<StyledDescription slot="description">{description}</StyledDescription>
|
||||
<StyledFieldError />
|
||||
</StyledRACTextField>
|
||||
)
|
||||
}
|
||||
8
src/frontend/src/primitives/Ul.tsx
Normal file
8
src/frontend/src/primitives/Ul.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { styled } from '../styled-system/jsx'
|
||||
|
||||
export const Ul = styled('ul', {
|
||||
base: {
|
||||
listStyle: 'disc',
|
||||
paddingLeft: 1,
|
||||
},
|
||||
})
|
||||
@@ -4,8 +4,10 @@ export { Badge } from './Badge'
|
||||
export { Bold } from './Bold'
|
||||
export { Box } from './Box'
|
||||
export { Button } from './Button'
|
||||
export { Dialog } from './Dialog'
|
||||
export { Dialog, useCloseDialog } from './Dialog'
|
||||
export { Div } from './Div'
|
||||
export { Field } from './Field'
|
||||
export { Form } from './Form'
|
||||
export { H } from './H'
|
||||
export { Hr } from './Hr'
|
||||
export { Italic } from './Italic'
|
||||
@@ -14,5 +16,5 @@ export { P } from './P'
|
||||
export { Popover } from './Popover'
|
||||
export { PopoverList } from './PopoverList'
|
||||
export { Text } from './Text'
|
||||
export { TextField } from './TextField'
|
||||
export { Ul } from './Ul'
|
||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||
|
||||
Reference in New Issue
Block a user