Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions bulma-ui/src/form/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Field, FieldProps } from './Field';
import { Control, ControlBaseProps } from './Control';
import { DateInputBase, DateInputBaseProps } from './DateInputBase';
import { useInsideField, useInsideControl } from './FormContext';
import { useAutoLabelId } from './useAutoLabelId';

/**
* Props for the DateInput convenience wrapper. Extends `DateInputBaseProps`
Expand All @@ -15,11 +16,11 @@ import { useInsideField, useInsideControl } from './FormContext';
* @extraProp {React.Ref<HTMLInputElement>} [ref] - Forwarded to the underlying `<input>`.
*/
export interface DateInputProps extends DateInputBaseProps {
/** Field label (component auto-wraps in a `Field` if not already inside). */
/** Field label (component auto-wraps in a `Field` if not already inside). Automatically associated with the input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Not wired in `inline` mode (no visible input to label) and dropped inside an outer `Field`. */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: FieldProps['labelSize'];
/** Props for the label element. */
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: FieldProps['labelProps'];
/** Render the field with horizontal layout. */
horizontal?: boolean;
Expand Down Expand Up @@ -98,6 +99,13 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
) => {
const insideField = useInsideField();
const insideControl = useInsideControl();
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: baseProps.id,
labelProps,
// Inline mode renders a bare calendar with no input to label.
rendersLabel: !insideField && !baseProps.inline,
});
const helpClass = usePrefixedClassNames('help', {
[`is-${messageColor}`]: !!messageColor,
});
Expand All @@ -107,6 +115,7 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
let content: React.ReactNode = (
<DateInputBase
ref={ref}
id={controlId}
{...baseProps}
triggerIcon={baseProps.triggerIcon ?? !isLoading}
/>
Expand Down Expand Up @@ -142,7 +151,7 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
15 changes: 12 additions & 3 deletions bulma-ui/src/form/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Field, FieldProps } from './Field';
import { Control, ControlBaseProps } from './Control';
import { DateTimeInputBase, DateTimeInputBaseProps } from './DateTimeInputBase';
import { useInsideField, useInsideControl } from './FormContext';
import { useAutoLabelId } from './useAutoLabelId';

/**
* Props for the DateTimeInput convenience wrapper. Extends
Expand All @@ -13,11 +14,11 @@ import { useInsideField, useInsideControl } from './FormContext';
* @extraProp {boolean} [required=false] - Marks the field as required for native HTML form validation.
*/
export interface DateTimeInputProps extends DateTimeInputBaseProps {
/** Field label. */
/** Field label. Automatically associated with the input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Not wired in `inline` mode (no visible input to label) and dropped inside an outer `Field`. */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: FieldProps['labelSize'];
/** Props for the label element. */
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: FieldProps['labelProps'];
/** Render the field with horizontal layout. */
horizontal?: boolean;
Expand Down Expand Up @@ -96,6 +97,13 @@ export const DateTimeInput = forwardRef<HTMLInputElement, DateTimeInputProps>(
) => {
const insideField = useInsideField();
const insideControl = useInsideControl();
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: baseProps.id,
labelProps,
// Inline mode renders a bare picker with no input to label.
rendersLabel: !insideField && !baseProps.inline,
});
const helpClass = usePrefixedClassNames('help', {
[`is-${messageColor}`]: !!messageColor,
});
Expand All @@ -105,6 +113,7 @@ export const DateTimeInput = forwardRef<HTMLInputElement, DateTimeInputProps>(
let content: React.ReactNode = (
<DateTimeInputBase
ref={ref}
id={controlId}
{...baseProps}
triggerIcon={baseProps.triggerIcon ?? !isLoading}
/>
Expand Down Expand Up @@ -140,7 +149,7 @@ export const DateTimeInput = forwardRef<HTMLInputElement, DateTimeInputProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
4 changes: 2 additions & 2 deletions bulma-ui/src/form/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export interface FieldProps
hasAddons?: boolean | 'centered' | 'right';
/** Constrains the field to its content's width (used inside horizontal field bodies). */
narrow?: boolean;
/** Field label, rendered above the widget. */
/** Field label, rendered above the widget. `Field` itself does not associate it with any control — pass `labelProps={{ htmlFor }}` and a matching `id` on your control (the single-control convenience inputs do this automatically when they render their own `Field`). */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: 'small' | 'normal' | 'medium' | 'large';
/** Props for the label element. */
/** Props for the label element — where `htmlFor` goes when composing. */
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement> & {
[key: string]: unknown;
};
Expand Down
16 changes: 15 additions & 1 deletion bulma-ui/src/form/File.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useConfig } from '../helpers/Config';
import { useInsideField } from './FormContext';
import { Field } from './Field';
import { FormFieldProps } from './fieldProps';
import { useAutoLabelId } from './useAutoLabelId';

/**
* Props for the File component.
Expand All @@ -21,6 +22,12 @@ export interface FileProps
>,
Omit<BulmaClassesProps, 'color'>,
FormFieldProps {
/** Field label. Automatically associated with the file input via `htmlFor` — uses your `id` when provided, otherwise a generated one. The input then has two labels (this one plus the wrapping `file-label`); assistive tech reads both. Dropped inside an outer `Field` (label that `Field` yourself). */
label?: React.ReactNode;
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement> & {
[key: string]: unknown;
};
/** Bulma color modifier for the file input. */
color?:
| 'primary'
Expand Down Expand Up @@ -96,6 +103,12 @@ export const File = forwardRef<HTMLInputElement, FileProps>(
ref
) => {
const insideField = useInsideField();
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: props.id,
labelProps,
rendersLabel: !insideField,
});
const { classPrefix } = useConfig();
const { bulmaHelperClasses, rest } = useBulmaClasses({
color,
Expand Down Expand Up @@ -142,6 +155,7 @@ export const File = forwardRef<HTMLInputElement, FileProps>(
inputClassName
)}
type="file"
id={controlId}
{...rest}
/>
<span className={usePrefixedClassNames('file-cta')}>
Expand Down Expand Up @@ -173,7 +187,7 @@ export const File = forwardRef<HTMLInputElement, FileProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
15 changes: 11 additions & 4 deletions bulma-ui/src/form/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Field, FieldProps } from './Field';
import { Control, ControlBaseProps } from './Control';
import { InputBase, InputBaseProps } from './InputBase';
import { useInsideField, useInsideControl } from './FormContext';
import { useAutoLabelId } from './useAutoLabelId';

/**
* Props for the Input component.
Expand All @@ -13,11 +14,11 @@ import { useInsideField, useInsideControl } from './FormContext';
* Control-level (icons, loading) props.
*/
export interface InputProps extends InputBaseProps {
/** Field label. */
/** Field label. Automatically associated with the input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Dropped inside an outer `Field` (label that `Field` yourself). */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: FieldProps['labelSize'];
/** Props for the label element. */
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: FieldProps['labelProps'];
/** Horizontal field layout. */
horizontal?: boolean;
Expand Down Expand Up @@ -105,11 +106,17 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
) => {
const insideField = useInsideField();
const insideControl = useInsideControl();
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: inputProps.id,
labelProps,
rendersLabel: !insideField,
});
const helpClass = usePrefixedClassNames('help', {
[`is-${messageColor}`]: !!messageColor,
});

let content = <InputBase ref={ref} {...inputProps} />;
let content = <InputBase ref={ref} id={controlId} {...inputProps} />;

if (!insideControl) {
content = (
Expand Down Expand Up @@ -139,7 +146,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
20 changes: 18 additions & 2 deletions bulma-ui/src/form/Numberinput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';
import { useInsideField } from './FormContext';
import { Field } from './Field';
import { FormFieldProps } from './fieldProps';
import { useAutoLabelId } from './useAutoLabelId';

/** Valid sizes for the Numberinput component. */
export type NumberinputSize = 'small' | 'medium' | 'large';
Expand Down Expand Up @@ -43,6 +44,12 @@ export interface NumberinputProps
>,
Omit<BulmaClassesProps, 'color' | 'backgroundColor' | 'size'>,
FormFieldProps {
/** Field label. Automatically associated with the number input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Not wired when no Field wrapper is rendered (inside a `Field`, or plusminus `bare`). */
label?: React.ReactNode;
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement> & {
[key: string]: unknown;
};
/** Controlled value. */
value?: number;
/** Default value for uncontrolled usage. */
Expand Down Expand Up @@ -175,6 +182,14 @@ export const Numberinput = forwardRef<HTMLInputElement, NumberinputProps>(
const isAddons = compact || isStepper;
const effectiveControlsPosition = isStepper ? 'right' : controlsPosition;

// The plusminus bare branch renders no Field and no label at all.
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: props.id,
labelProps,
rendersLabel: !insideField && (isStepper || !effectiveBare),
});

// Clamp value to min/max
const clampValue = useCallback(
(val: number): number => {
Expand Down Expand Up @@ -324,6 +339,7 @@ export const Numberinput = forwardRef<HTMLInputElement, NumberinputProps>(
aria-valuenow={currentValue ?? undefined}
aria-valuemin={min}
aria-valuemax={max}
id={controlId}
{...rest}
/>
</div>
Expand Down Expand Up @@ -366,7 +382,7 @@ export const Numberinput = forwardRef<HTMLInputElement, NumberinputProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down Expand Up @@ -494,7 +510,7 @@ export const Numberinput = forwardRef<HTMLInputElement, NumberinputProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
15 changes: 11 additions & 4 deletions bulma-ui/src/form/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Field, FieldProps } from './Field';
import { Control, ControlBaseProps } from './Control';
import { SelectBase, SelectBaseProps } from './SelectBase';
import { useInsideField, useInsideControl } from './FormContext';
import { useAutoLabelId } from './useAutoLabelId';

/**
* Props for the Select component.
Expand All @@ -13,11 +14,11 @@ import { useInsideField, useInsideControl } from './FormContext';
* Control-level (icons, loading) props.
*/
export interface SelectProps extends SelectBaseProps {
/** Field label. */
/** Field label. Automatically associated with the select via `htmlFor` — uses your `id` when provided, otherwise a generated one. Dropped inside an outer `Field` (label that `Field` yourself). */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: FieldProps['labelSize'];
/** Props for the label element. */
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: FieldProps['labelProps'];
/** Horizontal field layout. */
horizontal?: boolean;
Expand Down Expand Up @@ -89,11 +90,17 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
) => {
const insideField = useInsideField();
const insideControl = useInsideControl();
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: selectProps.id,
labelProps,
rendersLabel: !insideField,
});
const helpClass = usePrefixedClassNames('help', {
[`is-${messageColor}`]: !!messageColor,
});

let content = <SelectBase ref={ref} {...selectProps} />;
let content = <SelectBase ref={ref} id={controlId} {...selectProps} />;

if (!insideControl) {
content = (
Expand All @@ -118,7 +125,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
19 changes: 18 additions & 1 deletion bulma-ui/src/form/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Field } from './Field';
import { Control } from './Control';
import { FormFieldProps } from './fieldProps';
import { useAutoLabelId } from './useAutoLabelId';

/** Valid sizes for the Slider component. */
export type SliderSize = 'small' | 'medium' | 'large';
Expand Down Expand Up @@ -52,6 +53,12 @@
>,
Omit<BulmaClassesProps, 'color' | 'backgroundColor' | 'size'>,
FormFieldProps {
/** Field label. Automatically associated with the slider input via `htmlFor` — uses your `id` when provided, otherwise a generated one. In range mode the label targets the low (minimum) thumb. Dropped inside an outer `Field` (label that `Field` yourself). */
label?: React.ReactNode;
/** Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). */
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement> & {
[key: string]: unknown;
};
/** Minimum value. */
min?: number;
/** Maximum value. */
Expand Down Expand Up @@ -293,6 +300,14 @@
const insideField = useInsideField();
const insideControl = useInsideControl();
const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
// In range mode the low thumb carries `rest` (and any user id), so the
// label targets it; each thumb keeps its own aria-label for AT.
const { controlId, fieldLabelProps } = useAutoLabelId({
label,
id: restProps.id,
labelProps,
rendersLabel: !insideField,
});

// Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
const tooltipMode: SliderTooltip =
Expand Down Expand Up @@ -321,7 +336,7 @@
const isControlled = controlledValue !== undefined;

// Current values
const currentRange: [number, number] = range

Check warning on line 339 in bulma-ui/src/form/Slider.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

The 'currentRange' conditional could make the dependencies of useCallback Hook (at line 405) change on every render. To fix this, wrap the initialization of 'currentRange' in its own useMemo() Hook

Check warning on line 339 in bulma-ui/src/form/Slider.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

The 'currentRange' conditional could make the dependencies of useCallback Hook (at line 394) change on every render. To fix this, wrap the initialization of 'currentRange' in its own useMemo() Hook
? isControlled
? (controlledValue as [number, number])
: internalRange
Expand Down Expand Up @@ -537,7 +552,7 @@
[]
);

useLayoutEffect(() => {

Check warning on line 555 in bulma-ui/src/form/Slider.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

React Hook useLayoutEffect contains a call to 'setFlipped'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [isVertical, range, progressLow, progressSingle, getThumbSizePx, computeNudge] as a second argument to the useLayoutEffect Hook
if (isVertical) return;
const wrapper = wrapperRef.current;
const el = outputRef.current;
Expand All @@ -551,7 +566,7 @@
}
});

useLayoutEffect(() => {

Check warning on line 569 in bulma-ui/src/form/Slider.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

React Hook useLayoutEffect contains a call to 'setFlippedHigh'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [isVertical, range, getThumbSizePx, computeNudge, progressHigh] as a second argument to the useLayoutEffect Hook
if (isVertical || !range) return;
const wrapper = wrapperRef.current;
const el = outputHighRef.current;
Expand All @@ -565,7 +580,7 @@
});

// Detect vertical tooltip overflow: flip to left if no space on right
useLayoutEffect(() => {

Check warning on line 583 in bulma-ui/src/form/Slider.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

React Hook useLayoutEffect contains a call to 'setVerticalFlippedLeft'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [isVertical, getThumbSizePx] as a second argument to the useLayoutEffect Hook
if (!isVertical) return;
const wrapper = wrapperRef.current;
const el = outputRef.current;
Expand Down Expand Up @@ -661,6 +676,7 @@
aria-orientation={isVertical ? 'vertical' : undefined}
aria-label={ariaLabel as string | undefined}
{...getAriaProps(currentSingle)}
id={controlId}
{...rest}
/>
{tooltipMode !== 'hidden' && (
Expand Down Expand Up @@ -742,6 +758,7 @@
(ariaLabel as [string, string] | undefined)?.[0] ?? 'Minimum value'
}
{...getAriaProps(currentRange[0])}
id={controlId}
{...rest}
{...(nameLow !== undefined ? { name: nameLow } : {})}
/>
Expand Down Expand Up @@ -863,7 +880,7 @@
<Field
label={label}
labelSize={labelSize}
labelProps={labelProps}
labelProps={fieldLabelProps}
horizontal={horizontal}
className={fieldClassName}
>
Expand Down
Loading
Loading