Skip to content

Commit 52ee85e

Browse files
authored
Feat: add required indicators to form fields (#2815)
* feat(core): add required prop to Label component and update form elements - Introduced a `required` prop to the Label component to indicate mandatory fields. - Updated Input, Textarea, NumberInput, Select, Checkbox, RadioGroup, and other form components to utilize the new `required` prop for better accessibility and user experience. * refactor(core): format required indicator in Label component for improved readability * display optional text for form fields instead of required asterisk * fix linting * add internationalization and translated messages for optional text * remove formatting from da.json file * removed formatting from language files * removed formatting from language files * added changeset * remove 'optional' field from multiple language files * ensure required prop is passed down in form input components * pass required prop to RadioGroupPrimitive.Root * fix linting * removed optional text from checkbox * Pass field.required to CheckboxGroup and Checkbox so that optional text can be displayed
1 parent 74e4dd1 commit 52ee85e

14 files changed

Lines changed: 108 additions & 19 deletions

File tree

.changeset/tricky-hoops-hammer.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Add default optional text to form input labels for inputs that are not required.
6+
7+
## Migration
8+
9+
The new required props are optional, so they are backwards compatible. However, this does mean that the `(optional)` text will now show up on fields that aren't explicitly marked as required by passing the required prop to the Label component.

core/messages/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,5 +607,8 @@
607607
}
608608
}
609609
}
610+
},
611+
"Form": {
612+
"optional": "optional"
610613
}
611614
}

core/vibes/soul/form/button-radio-group/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,24 @@ export const ButtonRadioGroup = React.forwardRef<
4848
}
4949
>(
5050
(
51-
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
51+
{
52+
label,
53+
options,
54+
errors,
55+
className,
56+
onOptionMouseEnter,
57+
colorScheme = 'light',
58+
required,
59+
...rest
60+
},
5261
ref,
5362
) => {
5463
const id = React.useId();
5564

5665
return (
5766
<div className={clsx('button-radio-group space-y-2', className)}>
5867
{label !== undefined && label !== '' && (
59-
<Label colorScheme={colorScheme} id={id}>
68+
<Label colorScheme={colorScheme} id={id} required={required}>
6069
{label}
6170
</Label>
6271
)}
@@ -65,6 +74,7 @@ export const ButtonRadioGroup = React.forwardRef<
6574
aria-labelledby={id}
6675
className="flex flex-wrap gap-2"
6776
ref={ref}
77+
required={required}
6878
>
6979
{options.map((option) => (
7080
<RadioGroupPrimitive.Item

core/vibes/soul/form/card-radio-group/index.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,34 @@ export const CardRadioGroup = React.forwardRef<
5050
}
5151
>(
5252
(
53-
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
53+
{
54+
label,
55+
options,
56+
errors,
57+
className,
58+
onOptionMouseEnter,
59+
colorScheme = 'light',
60+
required,
61+
...rest
62+
},
5463
ref,
5564
) => {
5665
const id = React.useId();
5766

5867
return (
5968
<div className={clsx('space-y-2', className)}>
6069
{label !== undefined && label !== '' && (
61-
<Label colorScheme={colorScheme} id={id}>
70+
<Label colorScheme={colorScheme} id={id} required={required}>
6271
{label}
6372
</Label>
6473
)}
65-
<RadioGroupPrimitive.Root {...rest} aria-labelledby={id} className="space-y-2" ref={ref}>
74+
<RadioGroupPrimitive.Root
75+
{...rest}
76+
aria-labelledby={id}
77+
className="space-y-2"
78+
ref={ref}
79+
required={required}
80+
>
6681
{options.map((option) => (
6782
<RadioGroupPrimitive.Item
6883
aria-label={option.label}

core/vibes/soul/form/checkbox-group/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface Props {
2121
value: string[];
2222
onValueChange: (value: string[]) => void;
2323
colorScheme?: 'light' | 'dark';
24+
required?: boolean;
2425
}
2526

2627
export function CheckboxGroup({
@@ -32,13 +33,14 @@ export function CheckboxGroup({
3233
value,
3334
onValueChange,
3435
colorScheme,
36+
required,
3537
}: Props) {
3638
const id = React.useId();
3739

3840
return (
3941
<div className={clsx('space-y-2', className)}>
4042
{label !== undefined && label !== '' && (
41-
<Label colorScheme={colorScheme} id={id}>
43+
<Label colorScheme={colorScheme} id={id} required={required}>
4244
{label}
4345
</Label>
4446
)}

core/vibes/soul/form/dynamic-form/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ function DynamicFormField({
276276
onBlur={controls.blur}
277277
onCheckedChange={(value) => controls.change(String(value))}
278278
onFocus={controls.focus}
279-
required={formField.required}
279+
required={field.required}
280280
value={controls.value}
281281
/>
282282
);
@@ -290,6 +290,7 @@ function DynamicFormField({
290290
name={formField.name}
291291
onValueChange={controls.change}
292292
options={field.options}
293+
required={field.required}
293294
value={Array.isArray(controls.value) ? controls.value : []}
294295
/>
295296
);

core/vibes/soul/form/input/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const Input = React.forwardRef<
3939
return (
4040
<div className={clsx('w-full space-y-2', className)}>
4141
{label != null && label !== '' && (
42-
<Label colorScheme={colorScheme} htmlFor={id ?? generatedId}>
42+
<Label colorScheme={colorScheme} htmlFor={id ?? generatedId} required={required}>
4343
{label}
4444
</Label>
4545
)}
@@ -81,6 +81,7 @@ export const Input = React.forwardRef<
8181
)}
8282
id={id ?? generatedId}
8383
ref={ref}
84+
required={required}
8485
/>
8586
</div>
8687
{errors?.map((error) => (

core/vibes/soul/form/label/index.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
'use client';
2+
13
import * as LabelPrimitive from '@radix-ui/react-label';
24
import { clsx } from 'clsx';
5+
import { useTranslations } from 'next-intl';
36
import { ComponentPropsWithoutRef } from 'react';
47

58
// eslint-disable-next-line valid-jsdoc
@@ -17,8 +20,15 @@ import { ComponentPropsWithoutRef } from 'react';
1720
export function Label({
1821
className,
1922
colorScheme = 'light',
23+
required,
24+
children,
2025
...rest
21-
}: ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { colorScheme?: 'light' | 'dark' }) {
26+
}: ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {
27+
colorScheme?: 'light' | 'dark';
28+
required?: boolean;
29+
}) {
30+
const t = useTranslations('Form');
31+
2232
return (
2333
<LabelPrimitive.Root
2434
{...rest}
@@ -30,6 +40,9 @@ export function Label({
3040
}[colorScheme],
3141
className,
3242
)}
33-
/>
43+
>
44+
{children}
45+
{!required && <span className="ml-1 normal-case">({t('optional')})</span>}
46+
</LabelPrimitive.Root>
3447
);
3548
}

core/vibes/soul/form/number-input/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const NumberInput = React.forwardRef<
6262
return (
6363
<div className={clsx('space-y-2', className)}>
6464
{label != null && label !== '' && (
65-
<Label colorScheme={colorScheme} htmlFor={id}>
65+
<Label colorScheme={colorScheme} htmlFor={id} required={required}>
6666
{label}
6767
</Label>
6868
)}
@@ -130,6 +130,7 @@ export const NumberInput = React.forwardRef<
130130
disabled={disabled}
131131
id={id}
132132
ref={ref}
133+
required={required}
133134
type="number"
134135
/>
135136
<button

core/vibes/soul/form/radio-group/index.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,34 @@ export const RadioGroup = React.forwardRef<
2323
}
2424
>(
2525
(
26-
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
26+
{
27+
label,
28+
options,
29+
errors,
30+
className,
31+
onOptionMouseEnter,
32+
colorScheme = 'light',
33+
required,
34+
...rest
35+
},
2736
ref,
2837
) => {
2938
const id = React.useId();
3039

3140
return (
3241
<div className={clsx('space-y-2', className)}>
33-
{label !== undefined && label !== '' && <Label id={id}>{label}</Label>}
34-
<RadioGroupPrimitive.Root {...rest} aria-labelledby={id} className="space-y-2" ref={ref}>
42+
{label !== undefined && label !== '' && (
43+
<Label colorScheme={colorScheme} id={id} required={required}>
44+
{label}
45+
</Label>
46+
)}
47+
<RadioGroupPrimitive.Root
48+
{...rest}
49+
aria-labelledby={id}
50+
className="space-y-2"
51+
ref={ref}
52+
required={required}
53+
>
3554
{options.map((option, index) => (
3655
<RadioGroupItem
3756
colorScheme={colorScheme}

0 commit comments

Comments
 (0)