Skip to content
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/combobox-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where validation was not connected to combobox, causing issues for screenreaders.

## [2.6.0] - 2025-09-26

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ interface ComboboxWrapperProps extends PropsWithChildren {
validation?: string;
isLoading: boolean;
isMultiselectActive?: boolean;
errorId?: string;
}

export const ComboboxWrapper = forwardRef(
(props: ComboboxWrapperProps, ref: RefObject<HTMLDivElement>): ReactElement => {
const {
Expand All @@ -26,7 +26,8 @@ export const ComboboxWrapper = forwardRef(
validation,
children,
isLoading,
isMultiselectActive
isMultiselectActive,
errorId
} = props;
const { id, onClick } = getToggleButtonProps();

Expand Down Expand Up @@ -56,7 +57,7 @@ export const ComboboxWrapper = forwardRef(
</div>
)}
</div>
{validation && <ValidationAlert>{validation}</ValidationAlert>}
{validation && <ValidationAlert id={errorId}>{validation}</ValidationAlert>}
</Fragment>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import classNames from "classnames";
import { createElement, Fragment, KeyboardEvent, ReactElement, useMemo, useRef } from "react";
import { ClearButton } from "../../assets/icons";
import { MultiSelector, SelectionBaseProps } from "../../helpers/types";
import { getInputLabel, getSelectedCaptionsPlaceholder } from "../../helpers/utils";
import { getInputLabel, getSelectedCaptionsPlaceholder, getValidationErrorId } from "../../helpers/utils";
import { useDownshiftMultiSelectProps } from "../../hooks/useDownshiftMultiSelectProps";
import { useLazyLoading } from "../../hooks/useLazyLoading";
import { ComboboxWrapper } from "../ComboboxWrapper";
Expand Down Expand Up @@ -38,6 +38,7 @@ export function MultiSelection({
const isSelectedItemsBoxStyle = selector.selectedItemsStyle === "boxes";
const isOptionsSelected = selector.isOptionsSelected();
const inputLabel = getInputLabel(options.inputId);
const errorId = getValidationErrorId(options.inputId);
const hasLabel = useMemo(() => Boolean(inputLabel), [inputLabel]);
const inputProps = getInputProps({
...getDropdownProps(
Expand Down Expand Up @@ -96,6 +97,7 @@ export function MultiSelection({
validation={selector.validation}
isLoading={lazyLoading && selector.options.isLoading}
isMultiselectActive={selectedItems?.length > 0}
errorId={errorId}
>
<div
className={classNames(
Expand Down Expand Up @@ -139,6 +141,8 @@ export function MultiSelection({
placeholder=" "
{...inputProps}
aria-labelledby={hasLabel ? inputProps["aria-labelledby"] : undefined}
aria-describedby={selector.validation ? errorId : undefined}
aria-invalid={selector.validation ? true : undefined}
/>
<InputPlaceholder isEmpty={selectedItems.length <= 0}>{memoizedselectedCaptions}</InputPlaceholder>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import classNames from "classnames";
import { createElement, Fragment, ReactElement, useMemo, useRef } from "react";
import { ClearButton } from "../../assets/icons";
import { SelectionBaseProps, SingleSelector } from "../../helpers/types";
import { getInputLabel } from "../../helpers/utils";
import { getInputLabel, getValidationErrorId } from "../../helpers/utils";
import { useDownshiftSingleSelectProps } from "../../hooks/useDownshiftSingleSelectProps";
import { useLazyLoading } from "../../hooks/useLazyLoading";
import { ComboboxWrapper } from "../ComboboxWrapper";
Expand Down Expand Up @@ -57,6 +57,7 @@ export function SingleSelection({
);

const inputLabel = getInputLabel(options.inputId);
const errorId = getValidationErrorId(options.inputId);
const hasLabel = useMemo(() => Boolean(inputLabel), [inputLabel]);

const inputProps = getInputProps(
Expand All @@ -69,7 +70,6 @@ export function SingleSelection({
},
{ suppressRefError: true }
);

return (
<Fragment>
<ComboboxWrapper
Expand All @@ -79,6 +79,7 @@ export function SingleSelection({
getToggleButtonProps={getToggleButtonProps}
validation={selector.validation}
isLoading={lazyLoading && selector.options.isLoading}
errorId={errorId}
>
<div
className={classNames("widget-combobox-selected-items", {
Expand All @@ -93,6 +94,8 @@ export function SingleSelection({
{...inputProps}
placeholder=" "
aria-labelledby={hasLabel ? inputProps["aria-labelledby"] : undefined}
aria-describedby={selector.validation ? errorId : undefined}
aria-invalid={selector.validation ? true : undefined}
/>
<InputPlaceholder
isEmpty={!selector.currentId || !selector.caption.render(selectedItem, "label")}
Expand Down
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/combobox-web/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,7 @@ function sortSelections(
export function getInputLabel(inputId: string): Element | null {
return document.querySelector(`label[for="${inputId}"]`);
}

export function getValidationErrorId(inputId?: string): string | undefined {
return inputId ? inputId + "-validation-message" : undefined;
}
10 changes: 6 additions & 4 deletions packages/shared/widget-plugin-component-kit/src/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ export interface AlertProps {
children?: ReactNode;
className?: string;
bootstrapStyle: "default" | "primary" | "success" | "info" | "warning" | "danger";
id?: string;
role?: string;
}

export interface ValidationAlertProps {
children?: ReactNode;
className?: string;
id?: string;
}

// cloning from https://gitlab.rnd.mendix.com/appdev/appdev/-/blob/master/client/src/widgets/web/helpers/Alert.tsx
export const ValidationAlert = ({ className, children }: ValidationAlertProps): ReactElement => (
<Alert className={classNames("mx-validation-message", className)} bootstrapStyle="danger" role="alert">
export const ValidationAlert = ({ className, children, id }: ValidationAlertProps): ReactElement => (
<Alert className={classNames("mx-validation-message", className)} bootstrapStyle="danger" role="alert" id={id}>
{children}
</Alert>
);

export const Alert = ({ className, bootstrapStyle, children, role }: AlertProps): ReactNode =>
export const Alert = ({ className, bootstrapStyle, children, role, id }: AlertProps): ReactNode =>
Children.count(children) > 0 ? (
<div className={classNames(`alert alert-${bootstrapStyle}`, className)} role={role}>
<div className={classNames(`alert alert-${bootstrapStyle}`, className)} role={role} id={id}>
{children}
</div>
) : null;
Expand Down
Loading