From 02df04c707d4f6930663f6ce9bbc6cfac3ec1b4b Mon Sep 17 00:00:00 2001 From: Kari Salminen Date: Fri, 26 Jan 2024 17:56:42 +0200 Subject: [PATCH] feat(shared): add type for autoComplete property in React related to: https://github.com/City-of-Helsinki/yjdh/pull/2767#pullrequestreview-1845768040 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 autocomplete in HTML specification: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill refs YJDH-689 (PR #2767 comment mentioned possible autoComplete type) --- .../components/application/form/TextInput.tsx | 3 +- .../src/components/youth-form/YouthForm.tsx | 4 +- frontend/shared/src/types/auto-complete.d.ts | 270 ++++++++++++++++++ frontend/shared/src/types/common/subset.d.ts | 8 + frontend/shared/src/types/input-props.d.ts | 3 +- .../src/types/tests/auto-complete.test.ts | 97 +++++++ 6 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 frontend/shared/src/types/auto-complete.d.ts create mode 100644 frontend/shared/src/types/common/subset.d.ts create mode 100644 frontend/shared/src/types/tests/auto-complete.test.ts 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 +];