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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: Combobox/Dropdown multiselect checkbox+label pattern with role=option results in duplicate text",
"packageName": "@fluentui/react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
29 changes: 23 additions & 6 deletions packages/react/src/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1487,22 +1487,25 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
);
}

private _renderCheckboxLabel(item: IComboBoxOption): JSX.Element | null {
const { onRenderOption = this._onRenderMultiselectOptionContent } = this.props;
return onRenderOption(item, this._onRenderMultiselectOptionContent);
}

private _renderOption = (item: IComboBoxOption): JSX.Element => {
const { onRenderOption = this._onRenderOptionContent } = this.props;
const id = this._id;
const id = item.id ?? this._id + '-list' + item.index;
const isSelected: boolean = this._isOptionSelected(item.index);
const isChecked: boolean = this._isOptionChecked(item.index);
const isIndeterminate: boolean = this._isOptionIndeterminate(item.index);
const optionStyles = this._getCurrentOptionStyles(item);
const optionClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item));
const title = item.title;

const onRenderCheckboxLabel = () => onRenderOption(item, this._onRenderOptionContent);

const getOptionComponent = () => {
return !this.props.multiSelect ? (
<CommandButton
id={item.id ?? id + '-list' + item.index}
id={id}
key={item.key}
data-index={item.index}
styles={optionStyles}
Expand All @@ -1529,8 +1532,9 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
</CommandButton>
) : (
<Checkbox
id={item.id ?? id + '-list' + item.index}
id={id}
ariaLabel={item.ariaLabel}
ariaLabelledBy={item.ariaLabel ? undefined : id + '-label'}
key={item.key}
styles={optionStyles}
className={'ms-ComboBox-option'}
Expand All @@ -1541,7 +1545,7 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
title={title}
disabled={item.disabled}
// eslint-disable-next-line react/jsx-no-bind
onRenderLabel={onRenderCheckboxLabel}
onRenderLabel={this._renderCheckboxLabel.bind(this, { ...item, id: id + '-label' })}
inputProps={{
// aria-selected should only be applied to checked items, not hovered items
'aria-selected': isChecked ? 'true' : 'false',
Expand Down Expand Up @@ -1750,6 +1754,19 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
return <span className={optionClassNames.optionText}>{item.text}</span>;
};

/*
* Render content of a multiselect item label.
* Text within the label is aria-hidden, to prevent duplicate input/label exposure
*/
private _onRenderMultiselectOptionContent = (item: IComboBoxOption): JSX.Element => {
const optionClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item));
return (
<span id={item.id} aria-hidden="true" className={optionClassNames.optionText}>
{item.text}
</span>
);
};

/**
* Click handler for the menu items
* to select the item and also close the menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
}
>
<input
aria-labelledby="ComboBox0-list1-label"
aria-selected="false"
class=

Expand Down Expand Up @@ -1742,6 +1743,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
</i>
</div>
<span
aria-hidden="true"
class=
ms-ComboBox-optionText
{
Expand All @@ -1754,6 +1756,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
white-space: nowrap;
word-wrap: break-word;
}
id="ComboBox0-list1-label"
>
Option 1
</span>
Expand Down Expand Up @@ -1854,6 +1857,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
}
>
<input
aria-labelledby="ComboBox0-list3-label"
aria-selected="true"
checked=""
class=
Expand Down Expand Up @@ -1959,6 +1963,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
</i>
</div>
<span
aria-hidden="true"
class=
ms-ComboBox-optionText
{
Expand All @@ -1971,6 +1976,7 @@ exports[`ComboBox Renders correctly when opened in multi-select mode 1`] = `
white-space: nowrap;
word-wrap: break-word;
}
id="ComboBox0-list3-label"
>
Option 2
</span>
Expand Down
31 changes: 24 additions & 7 deletions packages/react/src/components/Dropdown/Dropdown.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
isRenderingPlaceholder: !selectedOptions.length,
panelClassName: panelProps ? panelProps.className : undefined,
calloutClassName: calloutProps ? calloutProps.className : undefined,
calloutRenderEdge: calloutRenderEdge,
calloutRenderEdge,
});

const hasErrorMessage: boolean = !!errorMessage && errorMessage.length > 0;
Expand Down Expand Up @@ -796,6 +796,10 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown

const { title } = item;

// define the id and label id (for multiselect checkboxes)
const id = this._listId + item.index;
const labelId = item.id ?? (id + '-label');

const multiSelectItemStyles = this._classNames.subComponentStyles
? (this._classNames.subComponentStyles.multiSelectItem as IStyleFunctionOrObject<
ICheckboxStyleProps,
Expand All @@ -805,7 +809,7 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown

return !this.props.multiSelect ? (
<CommandButton
id={this._listId + item.index}
id={id}
key={item.key}
data-index={item.index}
data-is-focusable={!item.disabled}
Expand All @@ -829,7 +833,7 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
</CommandButton>
) : (
<Checkbox
id={this._listId + item.index}
id={id}
key={item.key}
disabled={item.disabled}
onChange={this._onItemClick(item)}
Expand All @@ -847,13 +851,14 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
label={item.text}
title={title}
// eslint-disable-next-line react/jsx-no-bind
onRenderLabel={this._onRenderItemLabel.bind(this, item)}
onRenderLabel={this._onRenderItemLabel.bind(this, { ...item, id: labelId })}
className={css(itemClassName, 'is-multi-select')}
checked={isItemSelected}
styles={multiSelectItemStyles}
ariaPositionInSet={!item.hidden ? this._sizePosCache.positionInSet(item.index) : undefined}
ariaSetSize={!item.hidden ? this._sizePosCache.optionSetSize : undefined}
ariaLabel={item.ariaLabel}
ariaLabelledBy={item.ariaLabel ? undefined : labelId}
/>
);
};
Expand All @@ -863,10 +868,22 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
return <span className={this._classNames.dropdownOptionText}>{item.text}</span>;
};

/** Render custom label for drop down item */
/*
* Render content of a multiselect item label.
* Text within the label is aria-hidden, to prevent duplicate input/label exposure
*/
private _onRenderMultiselectOption = (item: IDropdownOption): JSX.Element => {
return (
<span id={item.id} aria-hidden="true" className={this._classNames.dropdownOptionText}>
{item.text}
</span>
);
};

/** Render custom label for multiselect checkbox items */
private _onRenderItemLabel = (item: IDropdownOption): JSX.Element | null => {
const { onRenderOption = this._onRenderOption } = this.props;
return onRenderOption(item, this._onRenderOption);
const { onRenderOption = this._onRenderMultiselectOption } = this.props;
return onRenderOption(item, this._onRenderMultiselectOption);
};

private _onPositioned = (positions?: ICalloutPositionedInfo): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
}
>
<input
aria-labelledby="Dropdown0-list1-label"
aria-posinset="1"
aria-selected="true"
aria-setsize="2"
Expand Down Expand Up @@ -582,6 +583,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
</i>
</div>
<span
aria-hidden="true"
class=
ms-Dropdown-optionText
{
Expand All @@ -597,6 +599,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
white-space: nowrap;
word-wrap: break-word;
}
id="Dropdown0-list1-label"
>
1
</span>
Expand Down Expand Up @@ -714,6 +717,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
title="test"
>
<input
aria-labelledby="Dropdown0-list3-label"
aria-posinset="2"
aria-selected="false"
aria-setsize="2"
Expand Down Expand Up @@ -825,6 +829,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
</i>
</div>
<span
aria-hidden="true"
class=
ms-Dropdown-optionText
{
Expand All @@ -840,6 +845,7 @@ exports[`Dropdown multi-select Renders correctly when open 1`] = `
white-space: nowrap;
word-wrap: break-word;
}
id="Dropdown0-list3-label"
>
2
</span>
Expand Down