Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/tricky-hoops-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@bigcommerce/catalyst-core": minor
---

Add default optional text to form input labels for inputs that are not required.

## Migration

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.
3 changes: 3 additions & 0 deletions core/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -607,5 +607,8 @@
}
}
}
},
"Form": {
"optional": "optional"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍹Should punctuation e.g., (, ) be included in next-intl strings? Wondering if other languages use a different convention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered about that. Not that I know of. I also wouldn't think the automation we have set up would have an automatic/appropriate translation if that was the case. I wouldn't think our automatic translations would understand the use case just based on the text enough to make a decision?

}
}
14 changes: 12 additions & 2 deletions core/vibes/soul/form/button-radio-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,24 @@ export const ButtonRadioGroup = React.forwardRef<
}
>(
(
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
{
label,
options,
errors,
className,
onOptionMouseEnter,
colorScheme = 'light',
required,
...rest
},
ref,
) => {
const id = React.useId();

return (
<div className={clsx('button-radio-group space-y-2', className)}>
{label !== undefined && label !== '' && (
<Label colorScheme={colorScheme} id={id}>
<Label colorScheme={colorScheme} id={id} required={required}>
{label}
</Label>
)}
Expand All @@ -65,6 +74,7 @@ export const ButtonRadioGroup = React.forwardRef<
aria-labelledby={id}
className="flex flex-wrap gap-2"
ref={ref}
required={required}
>
{options.map((option) => (
<RadioGroupPrimitive.Item
Expand Down
21 changes: 18 additions & 3 deletions core/vibes/soul/form/card-radio-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,34 @@ export const CardRadioGroup = React.forwardRef<
}
>(
(
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
{
label,
options,
errors,
className,
onOptionMouseEnter,
colorScheme = 'light',
required,
...rest
},
ref,
) => {
const id = React.useId();

return (
<div className={clsx('space-y-2', className)}>
{label !== undefined && label !== '' && (
<Label colorScheme={colorScheme} id={id}>
<Label colorScheme={colorScheme} id={id} required={required}>
{label}
</Label>
)}
<RadioGroupPrimitive.Root {...rest} aria-labelledby={id} className="space-y-2" ref={ref}>
<RadioGroupPrimitive.Root
{...rest}
aria-labelledby={id}
className="space-y-2"
ref={ref}
required={required}
>
{options.map((option) => (
<RadioGroupPrimitive.Item
aria-label={option.label}
Expand Down
4 changes: 3 additions & 1 deletion core/vibes/soul/form/checkbox-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Props {
value: string[];
onValueChange: (value: string[]) => void;
colorScheme?: 'light' | 'dark';
required?: boolean;
}

export function CheckboxGroup({
Expand All @@ -32,13 +33,14 @@ export function CheckboxGroup({
value,
onValueChange,
colorScheme,
required,
}: Props) {
const id = React.useId();

return (
<div className={clsx('space-y-2', className)}>
{label !== undefined && label !== '' && (
<Label colorScheme={colorScheme} id={id}>
<Label colorScheme={colorScheme} id={id} required={required}>
{label}
</Label>
)}
Expand Down
1 change: 1 addition & 0 deletions core/vibes/soul/form/checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function Checkbox({
id={id !== undefined ? `${id}-label` : `${generatedId}-label`}
>
{label}
{!props.required && <span className="ml-1 normal-case">(optional)</span>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not using the translated labels

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this text from checkbox since inherently it doesn't make sense. Checkboxes are inherently optinoal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checkbox fields can still be required by the Bigcommerce GQL api.

Thinking of general cases, a ToS checkbox would be required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a difference though between requiring a value and requiring that the box be checked right? Technically, an unchecked box is still a valid user input? Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't an unckecked checkbox looked at as a boolean false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorgemoya updated to pass field.required to CheckboxGroup which handles displaying the label using the Label component for a group of individual Checkbox components. This matches the Stencil functionality. The difference is that we display optional text instead of required text.

CleanShot 2026-01-12 at 14 49 01@2x

CleanShot 2026-01-12 at 14 49 53@2x

</LabelPrimitive.Root>
)}
</div>
Expand Down
3 changes: 2 additions & 1 deletion core/vibes/soul/form/input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const Input = React.forwardRef<
return (
<div className={clsx('w-full space-y-2', className)}>
{label != null && label !== '' && (
<Label colorScheme={colorScheme} htmlFor={id ?? generatedId}>
<Label colorScheme={colorScheme} htmlFor={id ?? generatedId} required={required}>
{label}
</Label>
)}
Expand Down Expand Up @@ -81,6 +81,7 @@ export const Input = React.forwardRef<
)}
id={id ?? generatedId}
ref={ref}
required={required}
/>
</div>
{errors?.map((error) => (
Expand Down
17 changes: 15 additions & 2 deletions core/vibes/soul/form/label/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use client';

import * as LabelPrimitive from '@radix-ui/react-label';
import { clsx } from 'clsx';
import { useTranslations } from 'next-intl';
import { ComponentPropsWithoutRef } from 'react';

// eslint-disable-next-line valid-jsdoc
Expand All @@ -17,8 +20,15 @@ import { ComponentPropsWithoutRef } from 'react';
export function Label({
className,
colorScheme = 'light',
required,
children,
...rest
}: ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { colorScheme?: 'light' | 'dark' }) {
}: ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {
colorScheme?: 'light' | 'dark';
required?: boolean;
}) {
const t = useTranslations('Form');

return (
<LabelPrimitive.Root
{...rest}
Expand All @@ -30,6 +40,9 @@ export function Label({
}[colorScheme],
className,
)}
/>
>
{children}
{!required && <span className="ml-1 normal-case">({t('optional')})</span>}
</LabelPrimitive.Root>
);
}
3 changes: 2 additions & 1 deletion core/vibes/soul/form/number-input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const NumberInput = React.forwardRef<
return (
<div className={clsx('space-y-2', className)}>
{label != null && label !== '' && (
<Label colorScheme={colorScheme} htmlFor={id}>
<Label colorScheme={colorScheme} htmlFor={id} required={required}>
{label}
</Label>
)}
Expand Down Expand Up @@ -130,6 +130,7 @@ export const NumberInput = React.forwardRef<
disabled={disabled}
id={id}
ref={ref}
required={required}
type="number"
/>
<button
Expand Down
25 changes: 22 additions & 3 deletions core/vibes/soul/form/radio-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,34 @@ export const RadioGroup = React.forwardRef<
}
>(
(
{ label, options, errors, className, onOptionMouseEnter, colorScheme = 'light', ...rest },
{
label,
options,
errors,
className,
onOptionMouseEnter,
colorScheme = 'light',
required,
...rest
},
ref,
) => {
const id = React.useId();

return (
<div className={clsx('space-y-2', className)}>
{label !== undefined && label !== '' && <Label id={id}>{label}</Label>}
<RadioGroupPrimitive.Root {...rest} aria-labelledby={id} className="space-y-2" ref={ref}>
{label !== undefined && label !== '' && (
<Label colorScheme={colorScheme} id={id} required={required}>
{label}
</Label>
)}
<RadioGroupPrimitive.Root
{...rest}
aria-labelledby={id}
className="space-y-2"
ref={ref}
required={required}
>
{options.map((option, index) => (
<RadioGroupItem
colorScheme={colorScheme}
Expand Down
4 changes: 3 additions & 1 deletion core/vibes/soul/form/rating-radio-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const RatingRadioGroup = React.forwardRef<
onOptionMouseEnter,
onOptionMouseLeave,
colorScheme = 'light',
required,
...rest
},
ref,
Expand Down Expand Up @@ -74,7 +75,7 @@ export const RatingRadioGroup = React.forwardRef<

return (
<div className={clsx('rating-radio-group space-y-2', className)}>
<Label colorScheme={colorScheme} id={groupId}>
<Label colorScheme={colorScheme} id={groupId} required={required}>
{label}
</Label>

Expand All @@ -86,6 +87,7 @@ export const RatingRadioGroup = React.forwardRef<
onMouseLeave={handleMouseLeave}
onMouseUp={handleMouseUp}
ref={ref}
required={required}
>
<div className="flex items-center gap-1">
{Array.from({ length: max }, (_, i) => {
Expand Down
4 changes: 3 additions & 1 deletion core/vibes/soul/form/select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function Select({
onBlur,
onOptionMouseEnter,
value,
required,
...rest
}: Props) {
const id = React.useId();
Expand All @@ -91,13 +92,14 @@ export function Select({
className={clsx(hideLabel && 'sr-only', 'mb-2')}
colorScheme={colorScheme}
htmlFor={id}
required={required}
>
{label}
</Label>
)}
{/* Workaround for https://github.com/radix-ui/primitives/issues/3198, remove when fixed */}
<input name={name} type="hidden" value={value} />
<SelectPrimitive.Root {...rest} name={`${name}_display`} value={value}>
<SelectPrimitive.Root {...rest} name={`${name}_display`} required={required} value={value}>
<SelectPrimitive.Trigger
aria-label={label}
className={clsx(
Expand Down
14 changes: 12 additions & 2 deletions core/vibes/soul/form/swatch-radio-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,24 @@ export const SwatchRadioGroup = React.forwardRef<
}
>(
(
{ label, options, errors, className, colorScheme = 'light', onOptionMouseEnter, ...rest },
{
label,
options,
errors,
className,
colorScheme = 'light',
onOptionMouseEnter,
required,
...rest
},
ref,
) => {
const id = React.useId();

return (
<div className={clsx('space-y-2', className)}>
{label !== undefined && label !== '' && (
<Label colorScheme={colorScheme} id={id}>
<Label colorScheme={colorScheme} id={id} required={required}>
{label}
</Label>
)}
Expand All @@ -75,6 +84,7 @@ export const SwatchRadioGroup = React.forwardRef<
aria-labelledby={id}
className="flex flex-wrap gap-1"
ref={ref}
required={required}
>
{options.map((option) => (
<RadioGroupPrimitive.Item
Expand Down
3 changes: 2 additions & 1 deletion core/vibes/soul/form/textarea/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const Textarea = React.forwardRef<
return (
<div className={clsx('space-y-2', className)}>
{label != null && label !== '' && (
<Label colorScheme={colorScheme} htmlFor={id}>
<Label colorScheme={colorScheme} htmlFor={id} required={required}>
{label}
</Label>
)}
Expand All @@ -65,6 +65,7 @@ export const Textarea = React.forwardRef<
)}
id={id}
ref={ref}
required={required}
/>
{errors?.map((error) => (
<FieldError key={error}>{error}</FieldError>
Expand Down