diff --git a/packages/pluggableWidgets/combobox-web/CHANGELOG.md b/packages/pluggableWidgets/combobox-web/CHANGELOG.md index 3ebf5c60ba..4d86acd76d 100644 --- a/packages/pluggableWidgets/combobox-web/CHANGELOG.md +++ b/packages/pluggableWidgets/combobox-web/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue where combobox would show aria-labelledby even when no label was added. + +- We added the option to fill an aria-label for the combobox. + ## [2.4.2] - 2025-06-10 ### Fixed diff --git a/packages/pluggableWidgets/combobox-web/package.json b/packages/pluggableWidgets/combobox-web/package.json index ecedc64dc5..f0e229cd9d 100644 --- a/packages/pluggableWidgets/combobox-web/package.json +++ b/packages/pluggableWidgets/combobox-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/combobox-web", "widgetName": "Combobox", - "version": "2.4.2", + "version": "2.4.3", "description": "Configurable Combo box widget with suggestions and autocomplete.", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/combobox-web/src/Combobox.tsx b/packages/pluggableWidgets/combobox-web/src/Combobox.tsx index 04c0e56e71..7c4d4da91c 100644 --- a/packages/pluggableWidgets/combobox-web/src/Combobox.tsx +++ b/packages/pluggableWidgets/combobox-web/src/Combobox.tsx @@ -25,6 +25,7 @@ export default function Combobox(props: ComboboxContainerProps): ReactElement { noOptionsText: props.noOptionsText?.value, readOnlyStyle: props.readOnlyStyle, ariaRequired: props.ariaRequired, + ariaLabel: props.ariaLabel?.value, a11yConfig: { ariaLabels: { clearSelection: props.clearButtonAriaLabel?.value ?? "", diff --git a/packages/pluggableWidgets/combobox-web/src/Combobox.xml b/packages/pluggableWidgets/combobox-web/src/Combobox.xml index e35652eaf1..5f87880ab5 100644 --- a/packages/pluggableWidgets/combobox-web/src/Combobox.xml +++ b/packages/pluggableWidgets/combobox-web/src/Combobox.xml @@ -339,6 +339,14 @@ + + Aria label + Used to describe the combo box. + + Combo box + Keuzelijst + + Clear selection button Used to clear all selected values. diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap index 4c3a62d324..e067d4a728 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/MultiSelection.spec.tsx.snap @@ -18,7 +18,6 @@ exports[`Combo box (Association) renders combobox widget 1`] = ` aria-autocomplete="list" aria-controls="downshift-0-menu" aria-expanded="false" - aria-labelledby="comboBox1-label" aria-required="true" autocomplete="off" class="widget-combobox-input" @@ -129,7 +128,6 @@ exports[`Combo box (Association) toggles combobox menu on: input CLICK(focus) / aria-autocomplete="list" aria-controls="downshift-2-menu" aria-expanded="false" - aria-labelledby="comboBox1-label" aria-required="true" autocomplete="off" class="widget-combobox-input" @@ -240,7 +238,6 @@ exports[`Combo box (Association) toggles combobox menu on: input TOGGLE BUTTON 1 aria-autocomplete="list" aria-controls="downshift-6-menu" aria-expanded="false" - aria-labelledby="comboBox1-label" aria-required="true" autocomplete="off" class="widget-combobox-input" diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap index f5c22b8218..21eff47ec7 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap @@ -18,7 +18,6 @@ exports[`Combo box (Association) renders combobox widget 1`] = ` aria-autocomplete="list" aria-controls="downshift-0-menu" aria-expanded="false" - aria-labelledby="comboBox1-label" aria-required="true" autocomplete="off" class="widget-combobox-input" diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap index f933d6aa10..e6188ccc4b 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap @@ -18,7 +18,6 @@ exports[`Combo box (Static values) renders combobox widget 1`] = ` aria-autocomplete="list" aria-controls="downshift-0-menu" aria-expanded="false" - aria-labelledby="comboBox1-label" aria-required="true" autocomplete="off" class="widget-combobox-input" diff --git a/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx b/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx index 1b12725c9c..7e586ba40d 100644 --- a/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx +++ b/packages/pluggableWidgets/combobox-web/src/components/MultiSelection/MultiSelection.tsx @@ -2,7 +2,7 @@ import classNames from "classnames"; import { Fragment, KeyboardEvent, ReactElement, createElement, useMemo, useRef } from "react"; import { ClearButton } from "../../assets/icons"; import { MultiSelector, SelectionBaseProps } from "../../helpers/types"; -import { getSelectedCaptionsPlaceholder } from "../../helpers/utils"; +import { getInputLabel, getSelectedCaptionsPlaceholder } from "../../helpers/utils"; import { useDownshiftMultiSelectProps } from "../../hooks/useDownshiftMultiSelectProps"; import { useLazyLoading } from "../../hooks/useLazyLoading"; import { ComboboxWrapper } from "../ComboboxWrapper"; @@ -37,6 +37,36 @@ export function MultiSelection({ const inputRef = useRef(null); const isSelectedItemsBoxStyle = selector.selectedItemsStyle === "boxes"; const isOptionsSelected = selector.isOptionsSelected(); + const inputLabel = getInputLabel(options.inputId); + const hasLabel = useMemo(() => Boolean(inputLabel), [inputLabel]); + const inputProps = getInputProps({ + ...getDropdownProps( + { + preventKeyAction: isOpen + }, + { suppressRefError: true } + ), + ref: inputRef, + onKeyDown: (event: KeyboardEvent) => { + if ( + (event.key === "Backspace" && inputRef.current?.selectionStart === 0) || + (event.key === "ArrowLeft" && isSelectedItemsBoxStyle && inputRef.current?.selectionStart === 0) + ) { + setActiveIndex(selectedItems.length - 1); + } + if (event.key === " ") { + if (highlightedIndex >= 0) { + toggleSelectedItem(highlightedIndex); + event.preventDefault(); + event.stopPropagation(); + } + } + }, + disabled: selector.readOnly, + readOnly: selector.options.filterType === "none", + "aria-required": ariaRequired.value, + "aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined + }); const memoizedselectedCaptions = useMemo( () => getSelectedCaptionsPlaceholder(selector, selectedItems), @@ -106,35 +136,8 @@ export function MultiSelection({ })} tabIndex={tabIndex} placeholder=" " - {...getInputProps({ - ...getDropdownProps( - { - preventKeyAction: isOpen - }, - { suppressRefError: true } - ), - ref: inputRef, - onKeyDown: (event: KeyboardEvent) => { - if ( - (event.key === "Backspace" && inputRef.current?.selectionStart === 0) || - (event.key === "ArrowLeft" && - isSelectedItemsBoxStyle && - inputRef.current?.selectionStart === 0) - ) { - setActiveIndex(selectedItems.length - 1); - } - if (event.key === " ") { - if (highlightedIndex >= 0) { - toggleSelectedItem(highlightedIndex); - event.preventDefault(); - event.stopPropagation(); - } - } - }, - disabled: selector.readOnly, - readOnly: selector.options.filterType === "none", - "aria-required": ariaRequired.value - })} + {...inputProps} + aria-labelledby={hasLabel ? inputProps["aria-labelledby"] : undefined} /> {memoizedselectedCaptions} diff --git a/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx b/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx index 47d64be1a5..eccabce5b2 100644 --- a/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx +++ b/packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx @@ -2,6 +2,7 @@ import classNames from "classnames"; import { Fragment, ReactElement, createElement, useMemo, useRef } from "react"; import { ClearButton } from "../../assets/icons"; import { SelectionBaseProps, SingleSelector } from "../../helpers/types"; +import { getInputLabel } from "../../helpers/utils"; import { useDownshiftSingleSelectProps } from "../../hooks/useDownshiftSingleSelectProps"; import { useLazyLoading } from "../../hooks/useLazyLoading"; import { ComboboxWrapper } from "../ComboboxWrapper"; @@ -44,6 +45,7 @@ export function SingleSelection({ const selectedItemCaption = useMemo( () => selector.caption.render(selectedItem, "label"), + // eslint-disable-next-line react-hooks/exhaustive-deps [ selectedItem, selector.status, @@ -54,6 +56,20 @@ export function SingleSelection({ ] ); + const inputLabel = getInputLabel(options.inputId); + const hasLabel = useMemo(() => Boolean(inputLabel), [inputLabel]); + + const inputProps = getInputProps( + { + disabled: selector.readOnly, + readOnly: selector.options.filterType === "none", + ref: inputRef, + "aria-required": ariaRequired.value, + "aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined + }, + { suppressRefError: true } + ); + return ( { menuFooterContent?: ReactNode; tabIndex: number; ariaRequired: DynamicValue; + ariaLabel?: string; a11yConfig: { ariaLabels: { clearSelection: string; diff --git a/packages/pluggableWidgets/combobox-web/src/helpers/utils.ts b/packages/pluggableWidgets/combobox-web/src/helpers/utils.ts index 4789e9f8ce..96fd5d8983 100644 --- a/packages/pluggableWidgets/combobox-web/src/helpers/utils.ts +++ b/packages/pluggableWidgets/combobox-web/src/helpers/utils.ts @@ -149,3 +149,7 @@ function sortSelections( } return newValueIds; } + +export function getInputLabel(inputId: string): Element | null { + return document.querySelector(`label[for="${inputId}"]`); +} diff --git a/packages/pluggableWidgets/combobox-web/src/package.xml b/packages/pluggableWidgets/combobox-web/src/package.xml index 414286d64e..5fcc7377bf 100644 --- a/packages/pluggableWidgets/combobox-web/src/package.xml +++ b/packages/pluggableWidgets/combobox-web/src/package.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts b/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts index 72e23b0ab5..d23d3a96ac 100644 --- a/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts +++ b/packages/pluggableWidgets/combobox-web/typings/ComboboxProps.d.ts @@ -90,6 +90,7 @@ export interface ComboboxContainerProps { onEnterEvent?: ActionValue; onLeaveEvent?: ActionValue; ariaRequired: DynamicValue; + ariaLabel?: DynamicValue; clearButtonAriaLabel?: DynamicValue; removeValueAriaLabel?: DynamicValue; a11ySelectedValue?: DynamicValue; @@ -145,6 +146,7 @@ export interface ComboboxPreviewProps { onEnterEvent: {} | null; onLeaveEvent: {} | null; ariaRequired: string; + ariaLabel: string; clearButtonAriaLabel: string; removeValueAriaLabel: string; a11ySelectedValue: string;