diff --git a/src/components/data/attributetable/attributetable.tsx b/src/components/data/attributetable/attributetable.tsx index 68507aa0..6ba6f20c 100644 --- a/src/components/data/attributetable/attributetable.tsx +++ b/src/components/data/attributetable/attributetable.tsx @@ -62,6 +62,7 @@ export const AttributeTable = ({ const renderTable = () => { return editable ? ( > + showRequiredExplanation={false} fieldsetClassName="mykn-attributetable__body" showActions={isFormOpenState} secondaryActions={[ diff --git a/src/components/data/datagrid/datagridtoolbar.tsx b/src/components/data/datagrid/datagridtoolbar.tsx index 64b488f7..7551d02c 100644 --- a/src/components/data/datagrid/datagridtoolbar.tsx +++ b/src/components/data/datagrid/datagridtoolbar.tsx @@ -147,6 +147,7 @@ export const DataGridToolbar = < }, ]} labelSubmit={ucFirst(_labelSaveFieldSelection)} + showRequiredExplanation={false} onSubmit={(e) => { const form = e.target as HTMLFormElement; const data = serializeForm(form, false); diff --git a/src/components/data/kanban/kanban.stories.tsx b/src/components/data/kanban/kanban.stories.tsx index 9367dd0a..7c258bcc 100644 --- a/src/components/data/kanban/kanban.stories.tsx +++ b/src/components/data/kanban/kanban.stories.tsx @@ -107,6 +107,7 @@ export const WithToolbar: Story = { direction: "horizontal", label: "Sorteren", required: true, + showRequiredIndicator: false, options: [ { label: "Nieuwste eerst", value: "-pk", selected: true }, { label: "Oudste eerst", value: "pk", selected: true }, diff --git a/src/components/form/form/form.tsx b/src/components/form/form/form.tsx index 6a63aace..3a2d5d83 100644 --- a/src/components/form/form/form.tsx +++ b/src/components/form/form/form.tsx @@ -19,9 +19,11 @@ import { import { forceArray } from "../../../lib"; import { ButtonProps } from "../../button"; import { Toolbar, ToolbarItem, ToolbarProps } from "../../toolbar"; +import { P } from "../../typography"; import { ErrorMessage } from "../errormessage"; import { FormControl } from "../formcontrol"; import "./form.scss"; +import { TRANSLATIONS } from "./translations"; export type FormProps = Omit< React.ComponentProps<"form">, @@ -63,6 +65,21 @@ export type FormProps = Omit< /** Whether to show the form actions. */ showActions?: boolean; + /** Whether to show a required indicator (*) when a field is required. */ + showRequiredIndicator?: boolean; + + /** The required indicator (*). */ + requiredIndicator?: string; + + /** The required explanation text. */ + requiredExplanation?: string; + + /** The required (accessible) label. */ + labelRequired?: string; + + /** Whether to show a text describing the meaning of * when one or more fields are required. */ + showRequiredExplanation?: boolean; + /** Props to pass to Toolbar. */ toolbarProps?: Partial; @@ -126,6 +143,11 @@ export const Form = ({ onChange, onSubmit, showActions = true, + showRequiredIndicator = true, + requiredIndicator, + labelRequired, + showRequiredExplanation = true, + requiredExplanation, toolbarProps, useTypedResults = false, validate = validateForm, @@ -151,13 +173,17 @@ export const Form = ({ const intl = useIntl(); - const _labelSubmit = labelSubmit - ? labelSubmit - : intl.formatMessage({ - id: "mykn.components.Form.labelSubmit", - description: "mykn.components.Form: The submit form label", - defaultMessage: "verzenden", - }); + const _requiredIndicator = + requiredIndicator || intl.formatMessage(TRANSLATIONS.REQUIRED_INDICATOR); + + const _requiredExplanation = + requiredExplanation || + intl.formatMessage(TRANSLATIONS.REQUIRED_EXPLANATION, { + requiredIndicator: _requiredIndicator, + }); + + const _labelSubmit = + labelSubmit || intl.formatMessage(TRANSLATIONS.LABEL_SUBMIT); /** * Revalidate on state change. @@ -237,6 +263,8 @@ export const Form = ({ )} + {showRequiredExplanation &&

{_requiredExplanation}

} + {Boolean(fields?.length) && (
{fields.map((field, index) => { @@ -249,12 +277,7 @@ export const Form = ({ const _labelValidationErrorRequired = labelValidationErrorRequired ? labelValidationErrorRequired : intl.formatMessage( - { - id: "mykn.components.Form.labelValidationErrorRequired", - description: - 'mykn.components.Form: The "required" validation error', - defaultMessage: "Veld {label} is verplicht", - }, + TRANSLATIONS.LABEL_VALIDATION_ERROR_REQUIRED, { ...field, label, value }, ); @@ -271,6 +294,9 @@ export const Form = ({ error={message} forceShowError={!validateOnChange} justify={justify} + showRequiredIndicator={showRequiredIndicator} + requiredIndicator={requiredIndicator} + labelRequired={labelRequired} value={value} onChange={defaultOnChange} {...field} diff --git a/src/components/form/form/translations.ts b/src/components/form/form/translations.ts new file mode 100644 index 00000000..6cf24afc --- /dev/null +++ b/src/components/form/form/translations.ts @@ -0,0 +1,34 @@ +// Define the structure of a single message descriptor +import { defineMessages } from "../../../lib"; + +export const TRANSLATIONS = defineMessages({ + REQUIRED_EXPLANATION: { + id: "mykn.components.Form.requiredExplanation", + description: "mykn.components.Form: The required explanation text", + defaultMessage: + "Verplichte velden zijn gemarkeerd met een sterretje ({requiredIndicator})", + }, + REQUIRED_INDICATOR: { + id: "mykn.components.Form.requiredIndicator", + description: "mykn.components.Form: The required indicator (*)", + defaultMessage: "*", + }, + + LABEL_REQUIRED: { + id: "mykn.components.Form.labelRequired", + description: "mykn.components.Form: The required (accessible) label", + defaultMessage: "Veld {label} is verplicht", + }, + + LABEL_VALIDATION_ERROR_REQUIRED: { + id: "mykn.components.Form.labelValidationErrorRequired", + description: 'mykn.components.Form: The "required" validation error', + defaultMessage: "Veld {label} is verplicht", + }, + + LABEL_SUBMIT: { + id: "mykn.components.Form.labelSubmit", + description: "mykn.components.Form: The submit form label", + defaultMessage: "verzenden", + }, +}); diff --git a/src/components/form/formcontrol/formcontrol.scss b/src/components/form/formcontrol/formcontrol.scss index 939c018e..6941ec2c 100644 --- a/src/components/form/formcontrol/formcontrol.scss +++ b/src/components/form/formcontrol/formcontrol.scss @@ -18,4 +18,8 @@ width: 100%; } } + + &__required-inidicator { + text-decoration: none; + } } diff --git a/src/components/form/formcontrol/formcontrol.stories.tsx b/src/components/form/formcontrol/formcontrol.stories.tsx index ffa4fa85..f5009f1d 100644 --- a/src/components/form/formcontrol/formcontrol.stories.tsx +++ b/src/components/form/formcontrol/formcontrol.stories.tsx @@ -12,16 +12,18 @@ type Story = StoryObj; export const InputFormControl: Story = { args: { - error: 'Field "school year" does not contain a valid e-mail address', + error: 'Field "e-mail" does not contain a valid e-mail address', + forceShowError: true, // Make sure the Story shows error. label: "Enter your e-mail address", name: "e-mail", - value: "johndoen#example.com", + value: "johndoe@example.com", }, }; export const SelectFormControl: Story = { args: { error: 'Field "school year" is required', + forceShowError: true, // Make sure the Story shows error. options: [ { label: "Freshman" }, { label: "Sophomore" }, @@ -34,3 +36,13 @@ export const SelectFormControl: Story = { value: "Junior", }, }; + +export const Required: Story = { + args: { + error: "Dit veld is verplicht", + forceShowError: true, // Make sure the Story shows error. + label: "Enter your e-mail address", + name: "e-mail", + required: true, + }, +}; diff --git a/src/components/form/formcontrol/formcontrol.tsx b/src/components/form/formcontrol/formcontrol.tsx index 32d16da4..89847cb1 100644 --- a/src/components/form/formcontrol/formcontrol.tsx +++ b/src/components/form/formcontrol/formcontrol.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import React, { useId, useState } from "react"; +import { useIntl } from "../../../lib"; import { FormField, isCheckbox, @@ -19,6 +20,7 @@ import { DateInput } from "../dateinput"; import { DatePicker } from "../datepicker"; import { DateRangeInput } from "../daterangeinput"; import { ErrorMessage } from "../errormessage"; +import { TRANSLATIONS } from "../form/translations"; import { Input, InputProps } from "../input"; import { Label } from "../label"; import { Radio } from "../radio"; @@ -36,6 +38,15 @@ export type FormControlProps = FormField & { /** Whether to forcefully show the error (if set), this skips the dirty check. */ forceShowError?: boolean; + + /** Whether to show a required indicator (*) when a field is required. */ + showRequiredIndicator?: boolean; + + /** The required indicator (*). */ + requiredIndicator?: string; + + /** The required (accessible) label. */ + labelRequired?: string; }; /** @@ -45,6 +56,9 @@ export type FormControlProps = FormField & { * @param justify * @param error * @param forceShowError + * @param showRequiredLabel + * @param requiredIndicator + * @param labelRequired * @param props * @constructor */ @@ -53,6 +67,9 @@ export const FormControl: React.FC = ({ justify = "baseline", error = "", forceShowError, + showRequiredIndicator = true, + requiredIndicator, + labelRequired, ...props }) => { const [isDirty, setIsDirty] = useState(false); @@ -63,6 +80,15 @@ export const FormControl: React.FC = ({ isCheckboxGroup(props) || isRadioGroup(props) ? `${_id}-choice-0` : _id; const idError = `${id}_error`; + const intl = useIntl(); + + const _requiredIndicator = + requiredIndicator || intl.formatMessage(TRANSLATIONS.REQUIRED_INDICATOR); + + const _labelRequired = + labelRequired || + intl.formatMessage(TRANSLATIONS.LABEL_REQUIRED, { label: props.label }); + return (
= ({ `mykn-form-control--justify-${justify}`, )} > - {props.label && } + {props.label && ( + + )} + = ({ props.onChange?.(e); }} /> + {(isDirty || forceShowError) && error && ( {error} )} diff --git a/src/components/form/select/select.tsx b/src/components/form/select/select.tsx index 463a34df..56b65e09 100644 --- a/src/components/form/select/select.tsx +++ b/src/components/form/select/select.tsx @@ -223,8 +223,8 @@ export const Select: React.FC = ({ )} tabIndex={0} ref={refs.setReference} - title={label || undefined} aria-autocomplete="none" + aria-label={label || undefined} aria-hidden={hidden} onBlur={handleBlur} {...getReferenceProps()} diff --git a/src/components/modal/modal.stories.tsx b/src/components/modal/modal.stories.tsx index 125da605..809c36ac 100644 --- a/src/components/modal/modal.stories.tsx +++ b/src/components/modal/modal.stories.tsx @@ -39,6 +39,7 @@ export const ModalComponent: Story = { }, ]} showActions={false} + showRequiredExplanation={false} /> ), diff --git a/src/hooks/dialog/useprompt.tsx b/src/hooks/dialog/useprompt.tsx index e137d66c..976106e0 100644 --- a/src/hooks/dialog/useprompt.tsx +++ b/src/hooks/dialog/useprompt.tsx @@ -56,7 +56,9 @@ export const usePrompt = () => { }, onCancel, modalProps, - undefined, + { + showRequiredExplanation: false, + }, autofocus, ); }; diff --git a/src/lib/i18n/compiled/en.json b/src/lib/i18n/compiled/en.json index 48209f22..f9501b23 100644 --- a/src/lib/i18n/compiled/en.json +++ b/src/lib/i18n/compiled/en.json @@ -28,8 +28,11 @@ "mykn.components.DatePicker.labelWeekPrefix": "Week", "mykn.components.DateRangeInput.labelEndDate": "end date", "mykn.components.DateRangeInput.labelStartDate": "start date", + "mykn.components.Form.labelRequired": "Field {label} is required", "mykn.components.Form.labelSubmit": "submit", "mykn.components.Form.labelValidationErrorRequired": "Field {label} is required", + "mykn.components.Form.requiredExplanation": "Required fields are marked with an asterisk ({requiredIndicator})", + "mykn.components.Form.requiredIndicator": "*", "mykn.components.Kanban.labelMoveObject": "change position of item", "mykn.components.Kanban.labelSelectColumn": "move item to column", "mykn.components.Logo.hrefLabel": "Go to \"{href}\"", diff --git a/src/lib/i18n/compiled/nl.json b/src/lib/i18n/compiled/nl.json index c4e6d4f7..d6954392 100644 --- a/src/lib/i18n/compiled/nl.json +++ b/src/lib/i18n/compiled/nl.json @@ -28,8 +28,11 @@ "mykn.components.DatePicker.labelWeekPrefix": "week", "mykn.components.DateRangeInput.labelEndDate": "einddatum", "mykn.components.DateRangeInput.labelStartDate": "startdatum", + "mykn.components.Form.labelRequired": "Veld {label} is verplicht", "mykn.components.Form.labelSubmit": "verzenden", "mykn.components.Form.labelValidationErrorRequired": "Veld {label} is verplicht", + "mykn.components.Form.requiredExplanation": "Verplichte velden zijn gemarkeerd met een sterretje ({requiredIndicator})", + "mykn.components.Form.requiredIndicator": "*", "mykn.components.Kanban.labelMoveObject": "wijzig positie van onderdeel", "mykn.components.Kanban.labelSelectColumn": "verplaats onderdeel naar kolom", "mykn.components.Logo.hrefLabel": "Navigeer naar \"{href}\"", diff --git a/src/lib/i18n/messages/en.json b/src/lib/i18n/messages/en.json index e2470ab1..225d9565 100644 --- a/src/lib/i18n/messages/en.json +++ b/src/lib/i18n/messages/en.json @@ -144,6 +144,11 @@ "description": "mykn.components.DateRangeInput: The start date (accessible) label", "originalDefault": "startdatum" }, + "mykn.components.Form.labelRequired": { + "defaultMessage": "Field {label} is required", + "description": "mykn.components.Form: The required (accessible) label", + "originalDefault": "Veld {label} is verplicht" + }, "mykn.components.Form.labelSubmit": { "defaultMessage": "submit", "description": "mykn.components.Form: The submit form label", @@ -154,6 +159,16 @@ "description": "mykn.components.Form: The \"required\" validation error", "originalDefault": "Veld {label} is verplicht" }, + "mykn.components.Form.requiredExplanation": { + "defaultMessage": "Required fields are marked with an asterisk ({requiredIndicator})", + "description": "mykn.components.Form: The required explanation text", + "originalDefault": "Verplichte velden zijn gemarkeerd met een sterretje ({requiredIndicator})" + }, + "mykn.components.Form.requiredIndicator": { + "defaultMessage": "*", + "description": "mykn.components.Form: The required indicator (*)", + "originalDefault": "*" + }, "mykn.components.Kanban.labelMoveObject": { "defaultMessage": "change position of item", "description": "mykn.components.Kanban: The kanban \"move object position\" (accessible) label", diff --git a/src/lib/i18n/messages/nl.json b/src/lib/i18n/messages/nl.json index 42743724..b8f84e26 100644 --- a/src/lib/i18n/messages/nl.json +++ b/src/lib/i18n/messages/nl.json @@ -144,6 +144,11 @@ "description": "mykn.components.DateRangeInput: The start date (accessible) label", "originalDefault": "startdatum" }, + "mykn.components.Form.labelRequired": { + "defaultMessage": "Veld {label} is verplicht", + "description": "mykn.components.Form: The required (accessible) label", + "originalDefault": "Veld {label} is verplicht" + }, "mykn.components.Form.labelSubmit": { "defaultMessage": "verzenden", "description": "mykn.components.Form: The submit form label", @@ -154,6 +159,16 @@ "description": "mykn.components.Form: The \"required\" validation error", "originalDefault": "Veld {label} is verplicht" }, + "mykn.components.Form.requiredExplanation": { + "defaultMessage": "Verplichte velden zijn gemarkeerd met een sterretje ({requiredIndicator})", + "description": "mykn.components.Form: The required explanation text", + "originalDefault": "Verplichte velden zijn gemarkeerd met een sterretje ({requiredIndicator})" + }, + "mykn.components.Form.requiredIndicator": { + "defaultMessage": "*", + "description": "mykn.components.Form: The required indicator (*)", + "originalDefault": "*" + }, "mykn.components.Kanban.labelMoveObject": { "defaultMessage": "wijzig positie van onderdeel", "description": "mykn.components.Kanban: The kanban \"move object position\" (accessible) label", diff --git a/src/templates/base/cardBase.tsx b/src/templates/base/cardBase.tsx index 08a210e2..83d46848 100644 --- a/src/templates/base/cardBase.tsx +++ b/src/templates/base/cardBase.tsx @@ -105,6 +105,7 @@ export const CardBaseTemplate: React.FC = ({ {formProps && (
diff --git a/src/templates/login/login.tsx b/src/templates/login/login.tsx index 6386f3e9..4ac4dc3e 100644 --- a/src/templates/login/login.tsx +++ b/src/templates/login/login.tsx @@ -86,6 +86,7 @@ export const LoginTemplate = < justify="stretch" labelSubmit={ucFirst(_labelLogin)} secondaryActions={secondaryActions} + showRequiredExplanation={false} {...formProps} />