diff --git a/frontend/kesaseteli/employer/src/components/application/form/TextInput.tsx b/frontend/kesaseteli/employer/src/components/application/form/TextInput.tsx index 976afee6b8..b2397d5770 100644 --- a/frontend/kesaseteli/employer/src/components/application/form/TextInput.tsx +++ b/frontend/kesaseteli/employer/src/components/application/form/TextInput.tsx @@ -8,6 +8,7 @@ import TextInputBase, { } from 'shared/components/forms/inputs/TextInput'; import { GridCellProps } from 'shared/components/forms/section/FormSection.sc'; import ApplicationFormData from 'shared/types/application-form-data'; +import AutoComplete from 'shared/types/auto-complete'; export type TextInputProps = { validation?: RegisterOptions; @@ -16,7 +17,7 @@ export type TextInputProps = { placeholder?: string; helperFormat?: string; onChange?: (value: string) => void; - autoComplete?: string; + autoComplete?: AutoComplete; } & GridCellProps; const TextInput: React.FC = ({ diff --git a/frontend/kesaseteli/youth/src/components/youth-form/YouthForm.tsx b/frontend/kesaseteli/youth/src/components/youth-form/YouthForm.tsx index 553b10a038..9716cd4c33 100644 --- a/frontend/kesaseteli/youth/src/components/youth-form/YouthForm.tsx +++ b/frontend/kesaseteli/youth/src/components/youth-form/YouthForm.tsx @@ -9,6 +9,7 @@ import SaveFormButton from 'shared/components/forms/buttons/SaveFormButton'; import Heading from 'shared/components/forms/heading/Heading'; import FormSection from 'shared/components/forms/section/FormSection'; import { $GridCell } from 'shared/components/forms/section/FormSection.sc'; +import AutoComplete from 'shared/types/auto-complete'; const YouthForm: React.FC = () => { const { t } = useTranslation(); @@ -17,11 +18,12 @@ const YouthForm: React.FC = () => { const { handleSaveSuccess, handleErrorResponse, submitError } = useHandleYouthApplicationSubmit(); const showForceSubmitLink = submitError?.type === 'please_recheck_data'; + const formAutoComplete: AutoComplete = 'off'; return ( <> -
+ <$GridCell $colSpan={2}> {submitError ? ( diff --git a/frontend/shared/src/types/auto-complete.d.ts b/frontend/shared/src/types/auto-complete.d.ts new file mode 100644 index 0000000000..a9522d51b7 --- /dev/null +++ b/frontend/shared/src/types/auto-complete.d.ts @@ -0,0 +1,270 @@ +type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; + +type Rank = 'primary' | 'secondary' | 'tertiary' | 'fallback'; + +type Level = 'debug' | 'info' | 'warning' | 'error' | 'critical' | 'emergency'; + +/** + * Color type to make examples from HTML specification work, e.g. "section-blue", + * see examples in HTML specification: + * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill + */ +type Color = + | 'black' + | 'blue' + | 'brown' + | 'gray' + | 'green' + | 'orange' + | 'pink' + | 'purple' + | 'red' + | 'white' + | 'yellow'; + +type Direction = 'up' | 'down' | 'left' | 'right'; + +type Position = + | 'top' + | 'bottom' + | 'center' + | 'left' + | 'right' + | 'first' + | 'last' + | 'middle' + | 'start' + | 'end' + | 'header' + | 'column' + | 'row' + | 'table' + | 'footer' + | 'side'; + +type Context = + | 'local' + | 'global' + | 'internal' + | 'external' + | 'personal' + | 'home' + | 'work'; + +type ActionVerb = + | 'add' + | 'remove' + | 'delete' + | 'create' + | 'update' + | 'modify' + | 'accept' + | 'reject' + | 'forbid' + | 'deny' + | 'confirm' + | 'notify' + | 'register' + | 'search'; + +type ActionNoun = + | 'application' + | 'confirmation' + | 'login' + | 'notification' + | 'registration'; + +type Age = 'adult' | 'child' | 'senior' | 'youth' | 'new' | 'old'; + +type Role = + | 'guardian' + | 'parent' + | 'employee' + | 'employer' + | 'applicant' + | 'handler' + | 'recipient' + | 'sender' + | 'admin' + | 'administrator' + | 'superuser' + | 'user'; + +type ContactPerson = + | 'contact-person' + | 'employee-contact-person' + | 'employer-contact-person' + | 'applicant-contact-person'; + +type ContactInfo = + | 'contact-info' + | 'employee-contact-info' + | 'employer-contact-info' + | 'applicant-contact-info'; + +type YjdhRelatedWords = + | 'ahjo' + | 'dvv' + | 'helsinki-profile' + | 'talpa' + | 'vtj' + | 'ytj'; + +type MiscellaneousNouns = + | 'chat' + | 'city' + | 'company' + | 'contact' + | 'department' + | 'event' + | 'feedback' + | 'group' + | 'hobby' + | 'invoice' + | 'location' + | 'message' + | 'organization' + | 'other' + | 'payment' + | 'profile' + | 'queue' + | 'sport' + | 'team' + | 'ticket' + | 'venue' + | 'voucher'; + +/** + * Arbitrary selection of section named groups. + * + * @note HTML specification allows for any named groups but that doesn't easily + * translate to string literal types so hardcoding an arbitrary selection of + * supported named group for sections, e.g. "section-9" or "section-info". + */ +type SectionNamedGroup = + | Digit + | Rank + | ActionVerb + | ActionNoun + | Color + | Direction + | Position + | Level + | Age + | Role + | ContactPerson + | ContactInfo + | Context + | YjdhRelatedWords + | MiscellaneousNouns; + +type SectionToken = `section-${SectionNamedGroup}`; + +type BillingShippingToken = 'billing' | 'shipping'; + +type AutofillFieldToken = + | 'name' + | 'honorific-prefix' + | 'given-name' + | 'additional-name' + | 'family-name' + | 'honorific-suffix' + | 'nickname' + | 'username' + | 'new-password' + | 'current-password' + | 'one-time-code' + | 'organization-title' + | 'organization' + | 'street-address' + | 'address-line1' + | 'address-line2' + | 'address-line3' + | 'address-level4' + | 'address-level3' + | 'address-level2' + | 'address-level1' + | 'country' + | 'country-name' + | 'postal-code' + | 'cc-name' + | 'cc-given-name' + | 'cc-additional-name' + | 'cc-family-name' + | 'cc-number' + | 'cc-exp' + | 'cc-exp-month' + | 'cc-exp-year' + | 'cc-csc' + | 'cc-type' + | 'transaction-currency' + | 'transaction-amount' + | 'language' + | 'bday' + | 'bday-day' + | 'bday-month' + | 'bday-year' + | 'sex' + | 'url' + | 'photo'; + +type ContactInfoCategoryToken = 'home' | 'work' | 'mobile' | 'fax' | 'pager'; + +type ContactInfoWithoutCategoryToken = + | 'tel' + | 'tel-country-code' + | 'tel-national' + | 'tel-area-code' + | 'tel-local' + | 'tel-local-prefix' + | 'tel-local-suffix' + | 'tel-extension' + | 'email' + | 'impp'; + +type ContactInfoWithCategoryToken = + `${ContactInfoCategoryToken} ${ContactInfoWithoutCategoryToken}`; + +type WebAuthenticationToken = 'webauthn'; + +type OnOff = 'on' | 'off'; + +/** + * Makes input string literal union type optional and + * usable as prefix token in a set of space-separated tokens. + * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#space-separated-tokens + * @example OptionalPrefix<'a' | 'b'> == '' | 'a ' | 'b '; + */ +type OptionalPrefix = '' | `${T} `; + +/** + * Makes input string literal union type optional and + * usable as suffix token in a set of space-separated tokens. + * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#space-separated-tokens + * @example OptionalSuffix<'a' | 'b'> == '' | ' a' | ' b'; + */ +type OptionalSuffix = '' | ` ${T}`; + +type AutofillDetailTokens = + | `${OptionalPrefix}${OptionalPrefix}${ + | AutofillFieldToken + | ContactInfoWithoutCategoryToken + | ContactInfoWithCategoryToken}${OptionalSuffix}`; + +/** + * Type for autoComplete property in React (i.e. autocomplete attribute in HTML) + * + * Caveats: + * - Not exact (i.e. allows combinations not allowed by HTML specification) + * - e.g. "name address-line1" is allowed here but should not be + * - "inappropriate for a control" clause in HTML specification is not handled, + * which means that this type allows all combinations for all control groups, + * i.e. for Date, Month, Multiline, Numeric, Password, Tel, Text, URL and Username. + * - Not complete (i.e. does not allow all combinations allowed by HTML specification) + * - e.g. "section-arbitrary-named-group-name-123" is not allowed here but should be + * + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill + */ +type AutoComplete = OnOff | AutofillDetailTokens; + +export default AutoComplete; diff --git a/frontend/shared/src/types/common/subset.d.ts b/frontend/shared/src/types/common/subset.d.ts new file mode 100644 index 0000000000..c76b203122 --- /dev/null +++ b/frontend/shared/src/types/common/subset.d.ts @@ -0,0 +1,8 @@ +/** + * Check at compile time that Subtype is a subset of Supertype or not. + * @warning This works with string literal types, but not necessarily with all types + * @return Subtype if Subtype is a subset of Supertype, otherwise raise an error. + */ +type Subset = Subtype; + +export default Subset; diff --git a/frontend/shared/src/types/input-props.d.ts b/frontend/shared/src/types/input-props.d.ts index 48cc7d0491..4ca6b34b72 100644 --- a/frontend/shared/src/types/input-props.d.ts +++ b/frontend/shared/src/types/input-props.d.ts @@ -1,4 +1,5 @@ import { RegisterOptions } from 'react-hook-form'; +import AutoComplete from 'shared/types/auto-complete'; import Id from 'shared/types/id'; type InputProps = { @@ -10,7 +11,7 @@ type InputProps = { errorText?: string; placeholder?: string; disabled?: boolean; - autoComplete?: string; + autoComplete?: AutoComplete; }; export default InputProps; diff --git a/frontend/shared/src/types/tests/auto-complete.test.ts b/frontend/shared/src/types/tests/auto-complete.test.ts new file mode 100644 index 0000000000..6ded820cd8 --- /dev/null +++ b/frontend/shared/src/types/tests/auto-complete.test.ts @@ -0,0 +1,97 @@ +import AutoComplete from '../auto-complete'; +import Subset from '../common/subset'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type CompileTimeTypeTests = [ + // @ts-expect-error 'off' should not be combined with anything + Subset, + // @ts-expect-error 'on' should not be combined with anything + Subset, + // @ts-expect-error No trailing whitespace is allowed + Subset, + // @ts-expect-error No leading whitespace is allowed + Subset, + // @ts-expect-error No leading & trailing whitespace is allowed + Subset, + // @ts-expect-error 'false' is not a valid autocomplete value + Subset, + // @ts-expect-error 'true' is not a valid autocomplete value + Subset, + // @ts-expect-error An empty string is not a valid autocomplete value + Subset, + // @ts-expect-error '0' is not a valid autocomplete value + Subset, + // @ts-expect-error '1' is not a valid autocomplete value + Subset, + // @ts-expect-error 'offx' is not a valid autocomplete value + Subset, + // @ts-expect-error Wrong order of tokens ('home' & 'billing' should be swapped) + Subset, + // On & Off + Subset, + Subset, + // Autofill field names + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + // Combinations of 2 tokens + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + // Combinations of 3 tokens + Subset, + Subset, + Subset, + Subset, + Subset, + Subset, + // Combinations of 4 tokens + Subset, + Subset, + // Combinations of 5 tokens + Subset +];