mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #18103 from overleaf/ii-bs5-forms
[web] Bootstrap 5 form elements GitOrigin-RevId: 7d031bed07007d0aa00a43f06d25bfb7384dee87
This commit is contained in:
parent
b6093848af
commit
482bd7fb9c
28 changed files with 1478 additions and 154 deletions
|
@ -1,5 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getUserFacingMessage,
|
||||
|
@ -11,6 +10,10 @@ import useAsync from '../../../shared/hooks/use-async'
|
|||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/wrappers/notification-wrapper'
|
||||
import FormGroupWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-group-wrapper'
|
||||
import FormLabelWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-label-wrapper'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
function AccountInfoSection() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -104,20 +107,20 @@ function AccountInfoSection() {
|
|||
required={false}
|
||||
/>
|
||||
{isSuccess ? (
|
||||
<FormGroup>
|
||||
<FormGroupWrapper>
|
||||
<NotificationWrapper
|
||||
type="success"
|
||||
content={t('thanks_settings_updated')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroupWrapper>
|
||||
) : null}
|
||||
{isError ? (
|
||||
<FormGroup>
|
||||
<FormGroupWrapper>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={getUserFacingMessage(error) ?? ''}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroupWrapper>
|
||||
) : null}
|
||||
{canUpdateEmail || canUpdateNames ? (
|
||||
<ButtonWrapper
|
||||
|
@ -159,14 +162,12 @@ function ReadOrWriteFormGroup({
|
|||
}: ReadOrWriteFormGroupProps) {
|
||||
const [validationMessage, setValidationMessage] = useState('')
|
||||
|
||||
const handleInvalid = (
|
||||
event: React.InvalidEvent<HTMLInputElement & FormControl>
|
||||
) => {
|
||||
const handleInvalid = (event: React.InvalidEvent<HTMLInputElement>) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleChangeAndValidity = (
|
||||
event: React.ChangeEvent<HTMLInputElement & FormControl>
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
handleChange(event)
|
||||
setValidationMessage(event.target.validationMessage)
|
||||
|
@ -174,18 +175,17 @@ function ReadOrWriteFormGroup({
|
|||
|
||||
if (!canEdit) {
|
||||
return (
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor={id}>{label}</ControlLabel>
|
||||
<FormControl id={id} type="text" readOnly value={value} />
|
||||
</FormGroup>
|
||||
<FormGroupWrapper controlId={id}>
|
||||
<FormLabelWrapper>{label}</FormLabelWrapper>
|
||||
<FormControlWrapper type="text" readOnly value={value} />
|
||||
</FormGroupWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor={id}>{label}</ControlLabel>
|
||||
<FormControl
|
||||
id={id}
|
||||
<FormGroupWrapper controlId={id}>
|
||||
<FormLabelWrapper>{label}</FormLabelWrapper>
|
||||
<FormControlWrapper
|
||||
type={type}
|
||||
required={required}
|
||||
value={value}
|
||||
|
@ -193,10 +193,8 @@ function ReadOrWriteFormGroup({
|
|||
onChange={handleChangeAndValidity}
|
||||
onInvalid={handleInvalid}
|
||||
/>
|
||||
{validationMessage ? (
|
||||
<span className="small text-danger">{validationMessage}</span>
|
||||
) : null}
|
||||
</FormGroup>
|
||||
{validationMessage && <FormText isError>{validationMessage}</FormText>}
|
||||
</FormGroupWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useCombobox } from 'downshift'
|
|||
import classnames from 'classnames'
|
||||
import countries, { CountryCode } from '../../../data/countries-list'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
|
||||
type CountryInputProps = {
|
||||
setValue: React.Dispatch<React.SetStateAction<CountryCode | null>>
|
||||
|
@ -62,7 +63,7 @@ function Downshift({ setValue, inputRef }: CountryInputProps) {
|
|||
>
|
||||
{t('country')}
|
||||
</label>
|
||||
<input
|
||||
<FormControlWrapper
|
||||
{...getInputProps({
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value)
|
||||
|
@ -74,8 +75,6 @@ function Downshift({ setValue, inputRef }: CountryInputProps) {
|
|||
},
|
||||
ref: inputRef,
|
||||
})}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder={t('country')}
|
||||
/>
|
||||
<i className="caret" />
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getJSON } from '../../../../../infrastructure/fetch-json'
|
|||
import useAbortController from '../../../../../shared/hooks/use-abort-controller'
|
||||
import domainBlocklist from '../../../domain-blocklist'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
|
||||
const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
|
||||
|
||||
|
@ -48,7 +49,7 @@ type InputProps = {
|
|||
function Input({ onChange, handleAddNewEmail }: InputProps) {
|
||||
const { signal } = useAbortController()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [suggestion, setSuggestion] = useState<string | null>(null)
|
||||
const [inputValue, setInputValue] = useState<string | null>(null)
|
||||
const [matchedDomain, setMatchedDomain] = useState<DomainInfo | null>(null)
|
||||
|
@ -161,15 +162,16 @@ function Input({ onChange, handleAddNewEmail }: InputProps) {
|
|||
|
||||
return (
|
||||
<div className="input-suggestions">
|
||||
<div className="form-control input-suggestions-shadow">
|
||||
<div className="input-suggestions-shadow-suggested">
|
||||
{suggestion || ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
<FormControlWrapper
|
||||
data-testid="affiliations-email-shadow"
|
||||
readOnly
|
||||
className="input-suggestions-shadow"
|
||||
value={suggestion || ''}
|
||||
/>
|
||||
<FormControlWrapper
|
||||
id="affiliations-email"
|
||||
className="form-control input-suggestions-main"
|
||||
data-testid="affiliations-email"
|
||||
className="input-suggestions-main"
|
||||
type="email"
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDownEvent}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { DomainInfo } from './input'
|
|||
import { getJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import useAsync from '../../../../../shared/hooks/use-async'
|
||||
import UniversityName from './university-name'
|
||||
import FormGroupWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-group-wrapper'
|
||||
|
||||
type InstitutionFieldsProps = {
|
||||
countryCode: CountryCode | null
|
||||
|
@ -151,17 +152,15 @@ function InstitutionFields({
|
|||
) : (
|
||||
// Display the country and university fields
|
||||
<>
|
||||
<div className="form-group mb-2">
|
||||
<FormGroupWrapper className="mb-2">
|
||||
<CountryInput
|
||||
id="new-email-country-input"
|
||||
setValue={setCountryCode}
|
||||
ref={countryRef}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`form-group ${
|
||||
isRoleAndDepartmentVisible ? 'mb-2' : 'mb-0'
|
||||
}`}
|
||||
</FormGroupWrapper>
|
||||
<FormGroupWrapper
|
||||
className={isRoleAndDepartmentVisible ? 'mb-2' : 'mb-0'}
|
||||
>
|
||||
<DownshiftInput
|
||||
items={getUniversityItems()}
|
||||
|
@ -171,12 +170,12 @@ function InstitutionFields({
|
|||
setValue={setUniversityName}
|
||||
disabled={!countryCode}
|
||||
/>
|
||||
</div>
|
||||
</FormGroupWrapper>
|
||||
</>
|
||||
)}
|
||||
{isRoleAndDepartmentVisible && (
|
||||
<>
|
||||
<div className="form-group mb-2">
|
||||
<FormGroupWrapper className="mb-2">
|
||||
<DownshiftInput
|
||||
items={[...defaultRoles]}
|
||||
inputValue={role}
|
||||
|
@ -184,8 +183,8 @@ function InstitutionFields({
|
|||
label={t('role')}
|
||||
setValue={setRole}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-0">
|
||||
</FormGroupWrapper>
|
||||
<FormGroupWrapper className="mb-0">
|
||||
<DownshiftInput
|
||||
items={departments}
|
||||
inputValue={department}
|
||||
|
@ -193,7 +192,7 @@ function InstitutionFields({
|
|||
label={t('department')}
|
||||
setValue={setDepartment}
|
||||
/>
|
||||
</div>
|
||||
</FormGroupWrapper>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useCombobox } from 'downshift'
|
|||
import classnames from 'classnames'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
|
||||
type DownshiftInputProps = {
|
||||
highlightMatches?: boolean
|
||||
|
@ -97,7 +98,7 @@ function Downshift({
|
|||
>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
<FormControlWrapper
|
||||
{...getInputProps({
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(event.target.value)
|
||||
|
@ -109,8 +110,6 @@ function Downshift({
|
|||
},
|
||||
ref: inputRef,
|
||||
})}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
@ -10,6 +10,7 @@ import defaultRoles from '../../data/roles'
|
|||
import defaultDepartments from '../../data/departments'
|
||||
import { University } from '../../../../../../types/university'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import FormGroupWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-group-wrapper'
|
||||
|
||||
type InstitutionAndRoleProps = {
|
||||
userEmailData: UserEmailData
|
||||
|
@ -120,7 +121,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
|
|||
) : (
|
||||
<div className="affiliation-change-container small">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group mb-2">
|
||||
<FormGroupWrapper className="mb-2">
|
||||
<DownshiftInput
|
||||
items={[...defaultRoles]}
|
||||
inputValue={role}
|
||||
|
@ -129,8 +130,8 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
|
|||
setValue={setRole}
|
||||
ref={roleRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-2">
|
||||
</FormGroupWrapper>
|
||||
<FormGroupWrapper className="mb-2">
|
||||
<DownshiftInput
|
||||
items={departments}
|
||||
inputValue={department}
|
||||
|
@ -138,7 +139,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
|
|||
label={t('department')}
|
||||
setValue={setDepartment}
|
||||
/>
|
||||
</div>
|
||||
</FormGroupWrapper>
|
||||
<ButtonWrapper
|
||||
size="small"
|
||||
variant="primary"
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
||||
import { Checkbox, ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { postJSON, FetchError } from '../../../../infrastructure/fetch-json'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import LeaveModalFormError from './modal-form-error'
|
||||
import { useLocation } from '../../../../shared/hooks/use-location'
|
||||
import FormGroupWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-group-wrapper'
|
||||
import FormLabelWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-label-wrapper'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
import FormCheckboxWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-checkbox-wrapper'
|
||||
|
||||
export type LeaveModalFormProps = {
|
||||
setInFlight: Dispatch<SetStateAction<boolean>>
|
||||
|
@ -26,15 +29,11 @@ function LeaveModalForm({
|
|||
const [confirmation, setConfirmation] = useState(false)
|
||||
const [error, setError] = useState<FetchError | null>(null)
|
||||
|
||||
const handleEmailChange = (
|
||||
event: React.ChangeEvent<HTMLFormElement & FormControl>
|
||||
) => {
|
||||
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handlePasswordChange = (
|
||||
event: React.ChangeEvent<HTMLFormElement & FormControl>
|
||||
) => {
|
||||
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(event.target.value)
|
||||
}
|
||||
|
||||
|
@ -74,43 +73,43 @@ function LeaveModalForm({
|
|||
|
||||
return (
|
||||
<form id="leave-form" onSubmit={handleSubmit}>
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor="email-input">{t('email')}</ControlLabel>
|
||||
<FormControl
|
||||
id="email-input"
|
||||
<FormGroupWrapper controlId="email-input">
|
||||
<FormLabelWrapper>{t('email')}</FormLabelWrapper>
|
||||
<FormControlWrapper
|
||||
type="text"
|
||||
placeholder={t('email')}
|
||||
required
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor="password-input">{t('password')}</ControlLabel>
|
||||
<FormControl
|
||||
id="password-input"
|
||||
</FormGroupWrapper>
|
||||
<FormGroupWrapper controlId="password-input">
|
||||
<FormLabelWrapper>{t('password')}</FormLabelWrapper>
|
||||
<FormControlWrapper
|
||||
type="password"
|
||||
placeholder={t('password')}
|
||||
required
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Checkbox
|
||||
</FormGroupWrapper>
|
||||
<FormCheckboxWrapper
|
||||
id="confirm-account-deletion"
|
||||
required
|
||||
checked={confirmation}
|
||||
onChange={handleConfirmationChange}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="delete_account_confirmation_label"
|
||||
components={[<i />]} // eslint-disable-line react/jsx-key
|
||||
values={{
|
||||
userDefaultEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</Checkbox>
|
||||
label={
|
||||
<Trans
|
||||
i18nKey="delete_account_confirmation_label"
|
||||
components={[<i />]} // eslint-disable-line react/jsx-key
|
||||
values={{
|
||||
userDefaultEmail,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{error ? <LeaveModalFormError error={error} /> : null}
|
||||
</form>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getUserFacingMessage,
|
||||
|
@ -12,6 +11,10 @@ import { PasswordStrengthOptions } from '../../../../../types/password-strength-
|
|||
import useAsync from '../../../shared/hooks/use-async'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/wrappers/notification-wrapper'
|
||||
import FormGroupWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-group-wrapper'
|
||||
import FormLabelWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-label-wrapper'
|
||||
import FormControlWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-control-wrapper'
|
||||
import FormTextWrapper from '@/features/ui/components/bootstrap-5/wrappers/form-text-wrapper'
|
||||
|
||||
type PasswordUpdateResult = {
|
||||
message?: {
|
||||
|
@ -156,12 +159,12 @@ function PasswordForm() {
|
|||
autoComplete="new-password"
|
||||
/>
|
||||
{isSuccess && data?.message?.text ? (
|
||||
<FormGroup>
|
||||
<FormGroupWrapper>
|
||||
<NotificationWrapper type="success" content={data.message.text} />
|
||||
</FormGroup>
|
||||
</FormGroupWrapper>
|
||||
) : null}
|
||||
{isError ? (
|
||||
<FormGroup>
|
||||
<FormGroupWrapper>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={
|
||||
|
@ -195,7 +198,7 @@ function PasswordForm() {
|
|||
)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroupWrapper>
|
||||
) : null}
|
||||
<ButtonWrapper
|
||||
form="password-change-form"
|
||||
|
@ -235,25 +238,26 @@ function PasswordFormGroup({
|
|||
const [validationMessage, setValidationMessage] = useState('')
|
||||
const [hadInteraction, setHadInteraction] = useState(false)
|
||||
|
||||
const handleInvalid = (
|
||||
event: React.InvalidEvent<HTMLInputElement & FormControl>
|
||||
) => {
|
||||
const handleInvalid = (event: React.InvalidEvent<HTMLInputElement>) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleChangeAndValidity = (
|
||||
event: React.ChangeEvent<HTMLInputElement & FormControl>
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
handleChange(event)
|
||||
setHadInteraction(true)
|
||||
setValidationMessage(event.target.validationMessage)
|
||||
}
|
||||
|
||||
const isInvalid = Boolean(
|
||||
hadInteraction && (parentValidationMessage || validationMessage)
|
||||
)
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor={id}>{label}</ControlLabel>
|
||||
<FormControl
|
||||
id={id}
|
||||
<FormGroupWrapper controlId={id}>
|
||||
<FormLabelWrapper>{label}</FormLabelWrapper>
|
||||
<FormControlWrapper
|
||||
type="password"
|
||||
placeholder="*********"
|
||||
autoComplete={autoComplete}
|
||||
|
@ -263,13 +267,14 @@ function PasswordFormGroup({
|
|||
onInvalid={handleInvalid}
|
||||
required={hadInteraction}
|
||||
minLength={minLength}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
{hadInteraction && (parentValidationMessage || validationMessage) ? (
|
||||
<span className="small text-danger">
|
||||
{isInvalid && (
|
||||
<FormTextWrapper isError>
|
||||
{parentValidationMessage || validationMessage}
|
||||
</span>
|
||||
) : null}
|
||||
</FormGroup>
|
||||
</FormTextWrapper>
|
||||
)}
|
||||
</FormGroupWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import { MergeAndOverride } from '../../../../../../../types/utils'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classnames from 'classnames'
|
||||
|
||||
type FormTextProps = MergeAndOverride<
|
||||
React.ComponentProps<(typeof Form)['Text']>,
|
||||
| {
|
||||
isInfo?: boolean
|
||||
isError?: never
|
||||
isWarning?: never
|
||||
isSuccess?: never
|
||||
}
|
||||
| {
|
||||
isInfo?: never
|
||||
isError?: boolean
|
||||
isWarning?: never
|
||||
isSuccess?: never
|
||||
}
|
||||
| {
|
||||
isInfo?: never
|
||||
isError?: never
|
||||
isWarning?: boolean
|
||||
isSuccess?: never
|
||||
}
|
||||
| {
|
||||
isInfo?: never
|
||||
isError?: never
|
||||
isWarning?: never
|
||||
isSuccess?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
export const getFormTextColor = ({
|
||||
isError,
|
||||
isSuccess,
|
||||
isWarning,
|
||||
}: {
|
||||
isError?: boolean
|
||||
isSuccess?: boolean
|
||||
isWarning?: boolean
|
||||
}) => ({
|
||||
'text-danger': isError,
|
||||
'text-success': isSuccess,
|
||||
'text-warning': isWarning,
|
||||
})
|
||||
|
||||
function FormText({
|
||||
isInfo,
|
||||
isError,
|
||||
isWarning,
|
||||
isSuccess,
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: FormTextProps) {
|
||||
return (
|
||||
<Form.Text
|
||||
className={classnames(
|
||||
className,
|
||||
getFormTextColor({ isError, isSuccess, isWarning })
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{isInfo && <MaterialIcon type="info" className="text-info" />}
|
||||
{isError && <MaterialIcon type="error" />}
|
||||
{isWarning && <MaterialIcon type="warning" />}
|
||||
{isSuccess && <MaterialIcon type="check_circle" />}
|
||||
{children}
|
||||
</Form.Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormText
|
|
@ -0,0 +1,32 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import { Checkbox as BS3Checkbox } from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type FormCheckboxWrapperProps = React.ComponentProps<(typeof Form)['Check']> & {
|
||||
bs3Props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function FormCheckboxWrapper(props: FormCheckboxWrapperProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3FormLabelProps: React.ComponentProps<typeof BS3Checkbox> = {
|
||||
children: rest.label,
|
||||
checked: rest.checked,
|
||||
required: rest.required,
|
||||
readOnly: rest.readOnly,
|
||||
disabled: rest.disabled,
|
||||
inline: rest.inline,
|
||||
title: rest.title,
|
||||
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3Checkbox {...bs3FormLabelProps} />}
|
||||
bs5={<Form.Check {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormCheckboxWrapper
|
|
@ -0,0 +1,71 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import { FormControl as BS3FormControl } from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type FormControlWrapperProps = React.ComponentProps<
|
||||
(typeof Form)['Control']
|
||||
> & {
|
||||
bs3Props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
const FormControlWrapper = forwardRef<
|
||||
HTMLInputElement,
|
||||
FormControlWrapperProps
|
||||
>((props, ref) => {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
let bs3FormControlProps: React.ComponentProps<typeof BS3FormControl> = {
|
||||
id: rest.id,
|
||||
className: rest.className,
|
||||
style: rest.style,
|
||||
type: rest.type,
|
||||
value: rest.value,
|
||||
required: rest.required,
|
||||
disabled: rest.disabled,
|
||||
placeholder: rest.placeholder,
|
||||
readOnly: rest.readOnly,
|
||||
autoComplete: rest.autoComplete,
|
||||
minLength: rest.minLength,
|
||||
maxLength: rest.maxLength,
|
||||
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
|
||||
onKeyDown: rest.onKeyDown as (e: React.KeyboardEvent<unknown>) => void,
|
||||
onFocus: rest.onFocus as (e: React.FocusEvent<unknown>) => void,
|
||||
onInvalid: rest.onInvalid as (e: React.InvalidEvent<unknown>) => void,
|
||||
inputRef: (inputElement: HTMLInputElement) => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(inputElement)
|
||||
} else if (ref) {
|
||||
ref.current = inputElement
|
||||
}
|
||||
},
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
// get all `aria-*` and `data-*` attributes
|
||||
const extraProps = Object.entries(rest).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key.startsWith('aria-') || key.startsWith('data-')) {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
|
||||
bs3FormControlProps = {
|
||||
...bs3FormControlProps,
|
||||
...extraProps,
|
||||
'data-ol-dirty': rest['data-ol-dirty'],
|
||||
} as typeof bs3FormControlProps & Record<string, unknown>
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3FormControl {...bs3FormControlProps} />}
|
||||
bs5={<Form.Control ref={ref} {...rest} />}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControlWrapper.displayName = 'FormControlWrapper'
|
||||
|
||||
export default FormControlWrapper
|
|
@ -0,0 +1,29 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import { FormGroup as BS3FormGroup } from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type FormGroupWrapperProps = React.ComponentProps<(typeof Form)['Group']> & {
|
||||
bs3Props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function FormGroupWrapper(props: FormGroupWrapperProps) {
|
||||
const { bs3Props, className, ...rest } = props
|
||||
|
||||
const classNames = className ?? 'mb-3'
|
||||
|
||||
const bs3FormGroupProps: React.ComponentProps<typeof BS3FormGroup> = {
|
||||
children: rest.children,
|
||||
controlId: rest.controlId,
|
||||
className,
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3FormGroup {...bs3FormGroupProps} />}
|
||||
bs5={<Form.Group className={classNames} {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormGroupWrapper
|
|
@ -0,0 +1,27 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import { ControlLabel as BS3FormLabel } from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type FormLabelWrapperProps = React.ComponentProps<(typeof Form)['Label']> & {
|
||||
bs3Props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function FormLabelWrapper(props: FormLabelWrapperProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3FormLabelProps: React.ComponentProps<typeof BS3FormLabel> = {
|
||||
children: rest.children,
|
||||
htmlFor: rest.htmlFor,
|
||||
srOnly: rest.visuallyHidden,
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3FormLabel {...bs3FormLabelProps} />}
|
||||
bs5={<Form.Label {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormLabelWrapper
|
|
@ -0,0 +1,38 @@
|
|||
import FormText, {
|
||||
getFormTextColor,
|
||||
} from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
import PolymorphicComponent from '@/shared/components/polymorphic-component'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import classnames from 'classnames'
|
||||
|
||||
type FormTextWrapperProps = React.ComponentProps<typeof FormText> & {
|
||||
bs3Props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function FormTextWrapper(props: FormTextWrapperProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3HelpBlockProps = {
|
||||
children: rest.children,
|
||||
className: classnames(
|
||||
'small',
|
||||
rest.className,
|
||||
getFormTextColor({
|
||||
isError: rest.isError,
|
||||
isSuccess: rest.isSuccess,
|
||||
isWarning: rest.isWarning,
|
||||
})
|
||||
),
|
||||
as: 'span',
|
||||
...bs3Props,
|
||||
} as const satisfies React.ComponentProps<typeof PolymorphicComponent>
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<PolymorphicComponent {...bs3HelpBlockProps} />}
|
||||
bs5={<FormText {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormTextWrapper
|
|
@ -0,0 +1,65 @@
|
|||
import { useRef, useLayoutEffect } from 'react'
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<(typeof Form)['Check']> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / Form',
|
||||
component: Form.Check,
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
argTypes: {
|
||||
id: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
defaultChecked: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Check']>
|
||||
|
||||
export const Checkbox: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const CheckboxChecked: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
defaultChecked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const CheckboxIndeterminate = (args: Story['args']) => {
|
||||
const ref = useRef<HTMLInputElement>()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.indeterminate = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Form.Check ref={ref} {...args} />
|
||||
}
|
||||
CheckboxIndeterminate.args = {
|
||||
id: 'id-2',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
}
|
206
services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
Normal file
206
services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
const meta: Meta<(typeof Form)['Control']> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / Form / Input',
|
||||
component: Form.Control,
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Control']>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control defaultValue="Large input" size="lg" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control defaultValue="Regular input" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control defaultValue="Small input" size="sm" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<(typeof Form)['Check']> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / Form',
|
||||
component: Form.Check,
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
argTypes: {
|
||||
id: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
defaultChecked: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Check']>
|
||||
|
||||
export const Radio: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
type: 'radio',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const RadioChecked: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
type: 'radio',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
defaultChecked: true,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
const meta: Meta<(typeof Form)['Select']> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / Form / Select',
|
||||
component: Form.Select,
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Select']>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" isInvalid {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select isInvalid {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" isInvalid {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" isSuccess {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select isSuccess {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" isSuccess {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
const meta: Meta<(typeof Form)['Control']> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / Form / Textarea',
|
||||
component: Form.Control,
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Control']>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control as="textarea" defaultValue="Regular input" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText>Helper</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isInfo>Info</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText isError>Error</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isWarning>Warning</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<Form.Group controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
<Form.Group controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText isSuccess>Success</FormText>
|
||||
</Form.Group>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
$prefix: bs-;
|
||||
|
||||
// Options
|
||||
//
|
||||
// Quickly modify global styling by enabling or disabling optional features.
|
||||
|
||||
$enable-validation-icons: false;
|
||||
|
||||
// Fonts
|
||||
$font-family-sans-serif: 'Noto Sans', sans-serif;
|
||||
$font-family-serif: 'Merriweather', serif;
|
||||
|
@ -10,6 +16,15 @@ $font-size-base: 1rem;
|
|||
$font-size-sm: var(--font-size-02);
|
||||
$line-height-base: 1.5;
|
||||
|
||||
// Components
|
||||
//
|
||||
// Define common padding and border radius sizes and more.
|
||||
|
||||
$focus-ring-width: 2px;
|
||||
$focus-ring-opacity: 1;
|
||||
$focus-ring-color: $blue-30;
|
||||
$focus-ring-blur: 0;
|
||||
|
||||
// Buttons
|
||||
$btn-font-family: $font-family-sans-serif;
|
||||
$btn-font-weight: 700;
|
||||
|
@ -20,9 +35,105 @@ $btn-border-radius-lg: $border-radius-full;
|
|||
$btn-border-radius-sm: $border-radius-full;
|
||||
$btn-white-space: nowrap;
|
||||
|
||||
// Forms
|
||||
|
||||
// form-text-variables
|
||||
$form-text-margin-top: $spacing-04;
|
||||
$form-text-font-size: var(--font-size-02);
|
||||
$form-text-color: var(--content-secondary);
|
||||
|
||||
// form-label-variables
|
||||
$form-label-margin-bottom: $spacing-02;
|
||||
$form-label-font-size: var(--font-size-02);
|
||||
$form-label-font-weight: 600;
|
||||
$form-label-color: var(--content-secondary);
|
||||
|
||||
// form-input-variables
|
||||
$input-padding-y: $spacing-03;
|
||||
$input-padding-x: $spacing-04;
|
||||
$input-font-size: var(--font-size-03);
|
||||
$input-line-height: 1.5; // equivalent of var(--line-height-03) - BS expects a unitless value for further calculations
|
||||
|
||||
$input-padding-y-sm: $spacing-01;
|
||||
$input-padding-x-sm: $spacing-03;
|
||||
$input-font-size-sm: var(--font-size-02);
|
||||
$input-line-height-sm: 1.25; // equivalent of var(--line-height-02) - BS expects a unitless value for further calculations
|
||||
|
||||
$input-padding-y-lg: $spacing-05;
|
||||
$input-padding-x-lg: $spacing-05;
|
||||
$input-font-size-lg: var(--font-size-03);
|
||||
|
||||
$input-bg: $bg-light-primary;
|
||||
$input-disabled-color: var(--content-disabled);
|
||||
$input-disabled-bg: $bg-light-disabled;
|
||||
$input-disabled-border-color: var(--border-disabled);
|
||||
|
||||
$input-color: $content-primary;
|
||||
$input-border-color: $neutral-60;
|
||||
$input-border-width: var(--bs-border-width);
|
||||
|
||||
$input-border-radius: $border-radius-base;
|
||||
$input-border-radius-sm: $border-radius-base;
|
||||
$input-border-radius-lg: $border-radius-base;
|
||||
|
||||
$input-focus-border-color: var(--border-active);
|
||||
|
||||
$input-placeholder-color: var(--content-placeholder);
|
||||
$input-plaintext-color: $content-primary;
|
||||
|
||||
$input-height-border: calc(
|
||||
#{$input-border-width} * 2
|
||||
); // stylelint-disable-line function-disallowed-list
|
||||
|
||||
$input-height: add(
|
||||
$input-line-height * 1em,
|
||||
add($input-padding-y * 2, $input-height-border, false)
|
||||
);
|
||||
$input-height-sm: add(
|
||||
$input-line-height-sm * 1em,
|
||||
add($input-padding-y-sm * 2, $input-height-border, false)
|
||||
);
|
||||
$input-height-lg: add(
|
||||
$input-line-height * 1em,
|
||||
add($input-padding-y-lg * 2, $input-height-border, false)
|
||||
);
|
||||
|
||||
// form-check-variables
|
||||
$form-check-label-color: var(--content-primary);
|
||||
|
||||
$form-check-input-border: var(--bs-border-width) solid var(--border-primary);
|
||||
$form-check-input-border-radius: $border-radius-base;
|
||||
$form-check-input-focus-border: var(--border-primary);
|
||||
|
||||
$form-check-input-disabled-opacity: 1;
|
||||
|
||||
// Form validation
|
||||
|
||||
// form-feedback-variables
|
||||
$form-feedback-invalid-color: $bg-danger-01;
|
||||
$form-feedback-icon-invalid: null;
|
||||
|
||||
// form-validation-colors
|
||||
$form-invalid-color: $form-feedback-invalid-color;
|
||||
$form-invalid-border-color: $bg-danger-01;
|
||||
|
||||
// form-validation-states
|
||||
$form-validation-states: (
|
||||
'invalid': (
|
||||
'color': $bg-danger-01,
|
||||
'icon': $form-feedback-icon-invalid,
|
||||
'tooltip-color': #fff,
|
||||
'tooltip-bg-color': var(--bs-danger),
|
||||
'focus-box-shadow': 0 0 $focus-ring-blur $focus-ring-width
|
||||
rgba($red-30, $focus-ring-opacity),
|
||||
'border-color': var(--#{$prefix}form-invalid-border-color),
|
||||
),
|
||||
);
|
||||
|
||||
// Colors
|
||||
$primary: $bg-accent-01;
|
||||
$secondary: $bg-light-primary;
|
||||
$success: $bg-accent-01;
|
||||
$info: $bg-info-01;
|
||||
$warning: $bg-warning-01;
|
||||
$danger: $bg-danger-01;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.grecaptcha-badge {
|
||||
visibility: hidden;
|
||||
height: 0 !important; // Prevent layout shift
|
||||
}
|
|
@ -26,6 +26,7 @@
|
|||
@import 'bootstrap-5/scss/images';
|
||||
@import 'bootstrap-5/scss/containers';
|
||||
@import 'bootstrap-5/scss/grid';
|
||||
@import 'bootstrap-5/scss/forms';
|
||||
@import 'bootstrap-5/scss/buttons';
|
||||
@import 'bootstrap-5/scss/dropdown';
|
||||
@import 'bootstrap-5/scss/badge';
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
@import 'tooltip';
|
||||
@import 'card';
|
||||
@import 'badge';
|
||||
@import 'form';
|
||||
@import 'input-suggestions';
|
||||
@import 'footer';
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
.form-check-input {
|
||||
@function form-check-box-svg($color) {
|
||||
@return url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill='none' stroke='#{$color}' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/></svg>");
|
||||
}
|
||||
@function form-check-radio-svg($color) {
|
||||
@return url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='2' fill='#{$color}'/></svg>");
|
||||
}
|
||||
@function form-check-indeterminate-svg($color) {
|
||||
@return url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill='none' stroke='#{$color}' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/></svg>");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(:disabled) {
|
||||
border-color: var(--border-hover);
|
||||
|
||||
&:checked,
|
||||
&[type='checkbox']:indeterminate {
|
||||
background-color: var(--bg-accent-02);
|
||||
border-color: var(--bg-accent-02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@mixin input-disabled-styles {
|
||||
background-color: $input-disabled-bg;
|
||||
border-color: $input-disabled-border-color;
|
||||
}
|
||||
|
||||
@include input-disabled-styles;
|
||||
|
||||
&[type='checkbox']:indeterminate {
|
||||
@include input-disabled-styles;
|
||||
|
||||
background-image: escape-svg(form-check-indeterminate-svg($neutral-40));
|
||||
}
|
||||
}
|
||||
|
||||
// Use disabled attribute in addition of :disabled pseudo-class
|
||||
// See: https://github.com/twbs/bootstrap/issues/28247
|
||||
&[disabled],
|
||||
&:disabled {
|
||||
~ .form-check-label {
|
||||
color: $input-disabled-color;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
&[type='checkbox'] {
|
||||
background-image: escape-svg(form-check-box-svg($neutral-40));
|
||||
}
|
||||
|
||||
&[type='radio'] {
|
||||
background-image: escape-svg(form-check-radio-svg($neutral-40));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
&[disabled],
|
||||
&:disabled {
|
||||
&::placeholder {
|
||||
color: var(--content-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-text {
|
||||
display: inline-flex;
|
||||
gap: $spacing-02;
|
||||
line-height: var(--line-height-02);
|
||||
|
||||
.form-control[disabled] ~ & {
|
||||
color: var(--content-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
&:has(+ .form-control[disabled]) {
|
||||
color: var(--content-disabled);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.input-suggestions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-suggestions-main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.input-suggestions-shadow {
|
||||
background-color: $input-bg;
|
||||
color: var(--content-placeholder);
|
||||
|
||||
& + .input-suggestions-main {
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ $is-overleaf-light: false;
|
|||
|
||||
// Link styles
|
||||
@import 'base/links';
|
||||
@import 'base/base';
|
||||
|
||||
// Page layout that isn't related to a particular component or page
|
||||
@import 'base/layout';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.input-suggestions {
|
||||
position: relative;
|
||||
height: @input-height-base;
|
||||
}
|
||||
|
||||
.input-suggestions-main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -9,13 +9,6 @@
|
|||
}
|
||||
|
||||
.input-suggestions-shadow {
|
||||
background-color: @input-bg;
|
||||
padding-top: @input-suggestion-v-offset;
|
||||
}
|
||||
.input-suggestions-shadow-existing {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.input-suggestions-shadow-suggested {
|
||||
background-color: @input-bg !important;
|
||||
color: lighten(@input-color, 25%);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
@ -55,19 +50,19 @@ describe('<AddEmailInput/>', function () {
|
|||
handleAddNewEmail={handleAddNewEmailStub}
|
||||
/>
|
||||
)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed', function () {
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId('affiliations-email') as HTMLInputElement
|
||||
expect(input.value).to.equal('user')
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event on every stroke', function () {
|
||||
expect(onChangeStub.calledWith('user')).to.equal(true)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 's' },
|
||||
})
|
||||
expect(onChangeStub.calledWith('s')).to.equal(true)
|
||||
|
@ -78,18 +73,22 @@ describe('<AddEmailInput/>', function () {
|
|||
})
|
||||
|
||||
it('should submit on Enter if email looks valid', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@domain.com' },
|
||||
})
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
expect(handleAddNewEmailStub.calledWith()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should not submit on Enter if email does not look valid', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@' },
|
||||
})
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
expect(handleAddNewEmailStub.calledWith()).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
@ -105,13 +104,15 @@ describe('<AddEmailInput/>', function () {
|
|||
describe('when there are no matches', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.get('express:/institutions/domains', 200)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed', function () {
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@d')
|
||||
})
|
||||
})
|
||||
|
@ -119,15 +120,22 @@ describe('<AddEmailInput/>', function () {
|
|||
describe('when there is a domain match', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed along with the suggestion', async function () {
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@d')
|
||||
await screen.findByText('user@domain.edu')
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event with the typed text', function () {
|
||||
|
@ -135,7 +143,7 @@ describe('<AddEmailInput/>', function () {
|
|||
})
|
||||
|
||||
it('should dispatch a `change` event with institution data when the typed email contains the institution domain', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@domain.edu' },
|
||||
})
|
||||
await fetchMock.flush(true)
|
||||
|
@ -148,8 +156,13 @@ describe('<AddEmailInput/>', function () {
|
|||
})
|
||||
|
||||
it('should clear the suggestion when the potential domain match is completely deleted', async function () {
|
||||
await screen.findByText('user@domain.edu')
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: '' },
|
||||
})
|
||||
expect(screen.queryByText('user@domain.edu')).to.be.null
|
||||
|
@ -157,12 +170,22 @@ describe('<AddEmailInput/>', function () {
|
|||
|
||||
describe('when there is a suggestion and "Tab" key is pressed', function () {
|
||||
beforeEach(async function () {
|
||||
await screen.findByText('user@domain.edu') // wait until autocompletion available
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' })
|
||||
// wait until autocompletion available
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Tab',
|
||||
})
|
||||
})
|
||||
|
||||
it('it should autocomplete the input', async function () {
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
|
@ -178,12 +201,22 @@ describe('<AddEmailInput/>', function () {
|
|||
|
||||
describe('when there is a suggestion and "Enter" key is pressed', function () {
|
||||
beforeEach(async function () {
|
||||
await screen.findByText('user@domain.edu') // wait until autocompletion available
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
|
||||
// wait until autocompletion available
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
})
|
||||
|
||||
it('it should autocomplete the input', async function () {
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
|
@ -201,17 +234,22 @@ describe('<AddEmailInput/>', function () {
|
|||
fetchMock.reset()
|
||||
|
||||
// clear input
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: '' },
|
||||
})
|
||||
// type a hint to trigger the domain search
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
|
||||
expect(fetchMock.called()).to.be.false
|
||||
expect(onChangeStub.calledWith('user@d')).to.equal(true)
|
||||
await screen.findByText('user@domain.edu')
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -228,7 +266,7 @@ describe('<AddEmailInput/>', function () {
|
|||
{ university: { id: 1 }, hostname: blockedDomain },
|
||||
]
|
||||
fetchMock.get('express:/institutions/domains', blockedInstitution)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: `user@${blockedDomain.split('.')[0]}` },
|
||||
})
|
||||
await fetchMock.flush(true)
|
||||
|
@ -243,7 +281,7 @@ describe('<AddEmailInput/>', function () {
|
|||
},
|
||||
]
|
||||
fetchMock.get('express:/institutions/domains', blockedInstitution)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: {
|
||||
value: `user@subdomain.${blockedDomain.split('.')[0]}`,
|
||||
},
|
||||
|
@ -257,10 +295,15 @@ describe('<AddEmailInput/>', function () {
|
|||
beforeEach(async function () {
|
||||
// type an initial suggestion
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
await screen.findByText('user@domain.edu')
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
// make sure the next suggestions are delayed
|
||||
clearDomainCache()
|
||||
|
@ -269,17 +312,25 @@ describe('<AddEmailInput/>', function () {
|
|||
})
|
||||
|
||||
it('should keep the suggestion if the hint matches the previously matched domain', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@do' },
|
||||
})
|
||||
screen.getByText('user@domain.edu')
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
it('should remove the suggestion if the hint does not match the previously matched domain', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@foo' },
|
||||
})
|
||||
expect(screen.queryByText('user@domain.edu')).to.be.null
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -292,10 +343,15 @@ describe('<AddEmailInput/>', function () {
|
|||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
onChangeStub = sinon.stub()
|
||||
render(<Input {...defaultProps} onChange={onChangeStub} />)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
await screen.findByText('user@domain.edu')
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
// subsequent requests fail
|
||||
fetchMock.reset()
|
||||
|
@ -303,16 +359,19 @@ describe('<AddEmailInput/>', function () {
|
|||
})
|
||||
|
||||
it('should clear suggestions', async function () {
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@dom' },
|
||||
})
|
||||
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement
|
||||
const input = screen.getByTestId('affiliations-email') as HTMLInputElement
|
||||
expect(input.value).to.equal('user@dom')
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.queryByText('user@domain.edu')
|
||||
)
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
|
||||
expect(fetchMock.called()).to.be.true // ensures `domainCache` hasn't been hit
|
||||
})
|
||||
|
@ -322,11 +381,14 @@ describe('<AddEmailInput/>', function () {
|
|||
it('should clear suggestion', async function () {
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
render(<Input {...defaultProps} onChange={sinon.stub()} />)
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@other' },
|
||||
})
|
||||
await fetchMock.flush(true)
|
||||
expect(screen.queryByText('user@domain.edu')).to.not.exist
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue