diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05ebb6b7f1c..b78217fcb85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,18 @@
## [`master`](https://github.com/elastic/eui/tree/master)
-
- Added `paddingSize` prop to `EuiCard` ([#3638](https://github.com/elastic/eui/pull/3638))
- Added `isClearable` and `placeholder` options to `EuiColorPicker` ([#3689](https://github.com/elastic/eui/pull/3689))
- Added SASS helper files for EUI theme globals ([#3691](https://github.com/elastic/eui/pull/3691))
+**Breaking changes**
+
+- Significant accessibility refactor of `EuiSelectable` ([#3169](https://github.com/elastic/eui/pull/3169))
+ - `react-virtualized` replaced with `react-window`
+ - `virtualizedProps` on `EuiSelectableOptionsList` renamed to `windowProps`
+ - Removed `rootId` and added `makeOptionId`, `listId`, and `setActiveOptionIndex` to `EuiSelectableList`
+ - Added `listId` to `EuiSelectableSearch`
+ - `options` passed into `EuiSelectable` cannot have an `id`
+ - Requires an `onChange` to be passed into `EuiSelectableSearch`
+
## [`26.3.0`](https://github.com/elastic/eui/tree/v26.3.0)
- Expanded `EuiBasicTable`'s default action's name configuration to accept any React node ([#3688](https://github.com/elastic/eui/pull/3688))
diff --git a/package.json b/package.json
index 95cd227f9f8..55b22cebc6a 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,8 @@
"@types/numeral": "^0.0.25",
"@types/react-beautiful-dnd": "^12.1.2",
"@types/react-input-autosize": "^2.0.2",
- "@types/react-virtualized": "^9.18.7",
+ "@types/react-virtualized-auto-sizer": "^1.0.0",
+ "@types/react-window": "^1.8.1",
"chroma-js": "^2.0.4",
"classnames": "^2.2.5",
"highlight.js": "^9.12.0",
@@ -66,7 +67,8 @@
"react-focus-lock": "^1.17.7",
"react-input-autosize": "^2.2.2",
"react-is": "~16.3.0",
- "react-virtualized": "^9.21.2",
+ "react-virtualized-auto-sizer": "^1.0.2",
+ "react-window": "^1.8.5",
"resize-observer-polyfill": "^1.5.0",
"tabbable": "^3.0.0",
"text-diff": "^1.0.1",
@@ -101,8 +103,8 @@
"@typescript-eslint/eslint-plugin": "^3.2.0",
"@typescript-eslint/parser": "^3.2.0",
"autoprefixer": "^7.1.5",
- "axe-core": "^3.3.2",
- "axe-puppeteer": "^1.0.0",
+ "axe-core": "^3.5.4",
+ "axe-puppeteer": "^1.1.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js
index 6358d1d16be..39c9f5378b1 100644
--- a/scripts/a11y-testing.js
+++ b/scripts/a11y-testing.js
@@ -38,7 +38,6 @@ const docsPages = async (root, page) => {
`${root}#/forms/color-selection`,
`${root}#/forms/code-editor`,
`${root}#/forms/date-picker`,
- `${root}#/forms/selectable`,
`${root}#/forms/suggest`,
`${root}#/forms/super-date-picker`,
`${root}#/elastic-charts/creating-charts`,
@@ -102,7 +101,7 @@ const printResult = result =>
{ id: 'color-contrast', enabled: false },
{
id: 'scrollable-region-focusable',
- matches: '[role="grid"]',
+ selector: '[data-skip-axe="scrollable-region-focusable"]',
},
],
})
diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js
index 5e25caa9ee9..4133c4cf74b 100644
--- a/src-docs/src/views/combo_box/combo_box_example.js
+++ b/src-docs/src/views/combo_box/combo_box_example.js
@@ -252,8 +252,8 @@ export const ComboBoxExample = {
text: (
EuiComboBoxList uses{' '}
-
- react-virtualized
+
+ react-window
{' '}
to only render visible options to be super fast no matter how many
options there are.
diff --git a/src-docs/src/views/selectable/data.ts b/src-docs/src/views/selectable/data.ts
index 85b0bda4ad9..67f425d3394 100644
--- a/src-docs/src/views/selectable/data.ts
+++ b/src-docs/src/views/selectable/data.ts
@@ -15,7 +15,6 @@ export const Options: EuiSelectableOption[] = [
},
{
label: 'Dione',
- id: 'id_dione',
},
{
label: 'Iapetus',
diff --git a/src-docs/src/views/selectable/selectable.tsx b/src-docs/src/views/selectable/selectable.tsx
index 29993c2bb17..75977051b5b 100644
--- a/src-docs/src/views/selectable/selectable.tsx
+++ b/src-docs/src/views/selectable/selectable.tsx
@@ -8,6 +8,7 @@ export default () => {
return (
setOptions(newOptions)}>
diff --git a/src-docs/src/views/selectable/selectable_custom_render.js b/src-docs/src/views/selectable/selectable_custom_render.js
index 046b55d5566..0d9debff55d 100644
--- a/src-docs/src/views/selectable/selectable_custom_render.js
+++ b/src-docs/src/views/selectable/selectable_custom_render.js
@@ -1,4 +1,4 @@
-import React, { useState, Fragment } from 'react';
+import React, { useState } from 'react';
import {
EuiBadge,
@@ -13,7 +13,6 @@ import { createDataStore } from '../tables/data_store';
export default () => {
const countries = createDataStore().countries.map(country => {
return {
- id: country.code,
label: `${country.name}`,
prepend: country.flag,
append: {country.code},
@@ -26,7 +25,7 @@ export default () => {
});
const [options, setOptions] = useState(countries);
- const [useCustomContent, setUseCustomContent] = useState(countries);
+ const [useCustomContent, setUseCustomContent] = useState(false);
const onChange = options => {
setOptions(options);
@@ -38,13 +37,13 @@ export default () => {
const renderCountryOption = (option, searchValue) => {
return (
-
+ <>
{option.label}
I am secondary content, I am!
-
+ >
);
};
@@ -61,9 +60,9 @@ export default () => {
}
return (
-
+ <>
@@ -71,17 +70,18 @@ export default () => {
{(list, search) => (
-
+ <>
{search}
{list}
-
+ >
)}
-
+ >
);
};
diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js
index 22389c8567d..7d9c9fb2e53 100644
--- a/src-docs/src/views/selectable/selectable_example.js
+++ b/src-docs/src/views/selectable/selectable_example.js
@@ -104,6 +104,7 @@ export const SelectableExample = {
},
demo: ,
snippet: ` setOptions(newOptions)}
listProps={{ bordered: true }}>
@@ -141,6 +142,7 @@ export const SelectableExample = {
props: { EuiSelectable },
demo: ,
snippet: `,
snippet: `
setOptions(newOptions)}
- singleSelection={true}
- listProps={{ bordered: true }}>
- {list => list}
-
- `,
+ aria-label="Single selection example"
+ options={options}
+ onChange={newOptions => setOptions(newOptions)}
+ singleSelection={true}
+ listProps={{ bordered: true }}>
+ {list => list}
+`,
},
{
title: 'Sizing and containers',
@@ -255,6 +257,7 @@ export const SelectableExample = {
props: { EuiSelectable },
demo: ,
snippet: ` setOptions(newOptions)}>
diff --git a/src-docs/src/views/selectable/selectable_exclusion.tsx b/src-docs/src/views/selectable/selectable_exclusion.tsx
index 1a4c87d27b1..48614971c55 100644
--- a/src-docs/src/views/selectable/selectable_exclusion.tsx
+++ b/src-docs/src/views/selectable/selectable_exclusion.tsx
@@ -8,6 +8,7 @@ export default () => {
return (
setOptions(newOptions)}>
diff --git a/src-docs/src/views/selectable/selectable_messages.tsx b/src-docs/src/views/selectable/selectable_messages.tsx
index 95ec006c00a..26da0f56b65 100644
--- a/src-docs/src/views/selectable/selectable_messages.tsx
+++ b/src-docs/src/views/selectable/selectable_messages.tsx
@@ -30,7 +30,11 @@ export default () => {
checked={isLoading}
/>
-
+
{list => (useCustomMessage && !isLoading ? customMessage : list)}
diff --git a/src-docs/src/views/selectable/selectable_popover.js b/src-docs/src/views/selectable/selectable_popover.js
index c5fa3c3d9dd..dc9afc30529 100644
--- a/src-docs/src/views/selectable/selectable_popover.js
+++ b/src-docs/src/views/selectable/selectable_popover.js
@@ -99,6 +99,7 @@ export default () => {
{isFlyoutVisible && (
{
{}}
style={{ width: 300 }}
diff --git a/src-docs/src/views/selectable/selectable_search.tsx b/src-docs/src/views/selectable/selectable_search.tsx
index 4566e56ab71..6649a7ac9cd 100644
--- a/src-docs/src/views/selectable/selectable_search.tsx
+++ b/src-docs/src/views/selectable/selectable_search.tsx
@@ -9,6 +9,7 @@ export default () => {
return (
{
return (
setOptions(newOptions)}
singleSelection={true}
diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap
index ecab3704429..9eff4a11e59 100644
--- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap
+++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap
@@ -317,14 +317,224 @@ exports[`props options list is rendered 1`] = `
class="euiComboBoxOptionsList__rowWrap"
>
+ style="position: relative; height: 189px; width: 0px; overflow: auto; direction: ltr;"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx
index b7f119a334d..0c5bd260bc1 100644
--- a/src/components/combo_box/combo_box.tsx
+++ b/src/components/combo_box/combo_box.tsx
@@ -575,6 +575,7 @@ export class EuiComboBox extends Component<
relatedTarget &&
this.comboBoxRefInstance &&
this.comboBoxRefInstance.contains(relatedTarget);
+
if (!focusedInOptionsList && !focusedInInput) {
this.closeList();
@@ -693,6 +694,10 @@ export class EuiComboBox extends Component<
if (singleSelection) {
requestAnimationFrame(this.closeList);
+ } else {
+ this.setState({
+ activeOptionIndex: this.state.matchingOptions.indexOf(addedOption),
+ });
}
};
diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
index e1824c6e7e8..6ca32b98af1 100644
--- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
+++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
@@ -24,7 +24,11 @@ import React, {
RefCallback,
} from 'react';
import classNames from 'classnames';
-import { List, ListProps } from 'react-virtualized'; // eslint-disable-line import/named
+import {
+ FixedSizeList,
+ ListProps,
+ ListChildComponentProps,
+} from 'react-window';
import { EuiCode } from '../../../components/code';
import { EuiFlexGroup, EuiFlexItem } from '../../flex';
@@ -103,6 +107,8 @@ export class EuiComboBoxOptionsList extends Component<
EuiComboBoxOptionsListProps
> {
listRefInstance: RefInstance = null;
+ listRef: FixedSizeList | null = null;
+ listBoxRef: HTMLUListElement | null = null;
static defaultProps = {
'data-test-subj': '',
@@ -146,6 +152,10 @@ export class EuiComboBoxOptionsList extends Component<
) {
this.updatePosition();
}
+
+ if (this.listRef && typeof this.props.activeOptionIndex !== 'undefined') {
+ this.listRef.scrollToItem(this.props.activeOptionIndex, 'auto');
+ }
}
componentWillUnmount() {
@@ -173,6 +183,80 @@ export class EuiComboBoxOptionsList extends Component<
this.listRefInstance = ref;
};
+ setListRef = (ref: FixedSizeList | null) => {
+ this.listRef = ref;
+ };
+
+ setListBoxRef = (ref: HTMLUListElement | null) => {
+ this.listBoxRef = ref;
+
+ if (ref) {
+ ref.setAttribute('id', this.props.rootId('listbox'));
+ ref.setAttribute('role', 'listBox');
+ ref.setAttribute('tabIndex', '0');
+ }
+ };
+
+ ListRow = ({ data, index, style }: ListChildComponentProps) => {
+ const option = data[index];
+ const { isGroupLabelOption, label, value, ...rest } = option;
+ const {
+ singleSelection,
+ selectedOptions,
+ onOptionClick,
+ optionRef,
+ activeOptionIndex,
+ renderOption,
+ searchValue,
+ rootId,
+ } = this.props;
+
+ if (isGroupLabelOption) {
+ return (
+
+ {label}
+
+ );
+ }
+
+ let checked: FilterChecked | undefined = undefined;
+ if (
+ singleSelection &&
+ selectedOptions.length &&
+ selectedOptions[0].label === label
+ ) {
+ checked = 'on';
+ }
+
+ return (
+ {
+ if (onOptionClick) {
+ onOptionClick(option);
+ }
+ }}
+ ref={optionRef.bind(this, index)}
+ isFocused={activeOptionIndex === index}
+ checked={checked}
+ showIcons={singleSelection ? true : false}
+ id={rootId(`_option-${index}`)}
+ title={label}
+ {...rest}>
+ {renderOption ? (
+ renderOption(option, searchValue, OPTION_CONTENT_CLASSNAME)
+ ) : (
+
+ {label}
+
+ )}
+
+ );
+ };
+
render() {
const {
'data-test-subj': dataTestSubj,
@@ -311,65 +395,17 @@ export class EuiComboBoxOptionsList extends Component<
const height = numVisibleOptions * rowHeight;
const optionsList = (
- {
- const option = matchingOptions[index];
- const { isGroupLabelOption, label, value, ...rest } = option;
-
- if (isGroupLabelOption) {
- return (
-
- {label}
-
- );
- }
-
- let checked: FilterChecked | undefined = undefined;
- if (
- singleSelection &&
- selectedOptions.length &&
- selectedOptions[0].label === label
- ) {
- checked = 'on';
- }
-
- return (
- {
- if (onOptionClick) {
- onOptionClick(option);
- }
- }}
- ref={optionRef.bind(this, index)}
- isFocused={activeOptionIndex === index}
- checked={checked}
- showIcons={singleSelection ? true : false}
- id={rootId(`_option-${index}`)}
- title={label}
- {...rest}>
- {renderOption ? (
- renderOption(option, searchValue, OPTION_CONTENT_CLASSNAME)
- ) : (
-
- {label}
-
- )}
-
- );
- }}
- role="listbox"
- rowCount={matchingOptions.length}
- rowHeight={rowHeight}
- scrollToIndex={scrollToIndex}
- width={width}
- />
+ itemCount={matchingOptions.length}
+ itemSize={rowHeight}
+ itemData={matchingOptions}
+ ref={this.setListRef}
+ innerRef={this.setListBoxRef}
+ width={width}>
+ {this.ListRow}
+
);
const classes = classNames(
diff --git a/src/components/selectable/__snapshots__/selectable.test.tsx.snap b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
index ec2b5e77f42..7bbd9e21b00 100644
--- a/src/components/selectable/__snapshots__/selectable.test.tsx.snap
+++ b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
@@ -2,7 +2,6 @@
exports[`EuiSelectable is rendered 1`] = `
diff --git a/src/components/selectable/selectable.test.tsx b/src/components/selectable/selectable.test.tsx
index 2638ed03eeb..daaf0ff1745 100644
--- a/src/components/selectable/selectable.test.tsx
+++ b/src/components/selectable/selectable.test.tsx
@@ -38,6 +38,11 @@ const options: EuiSelectableOption[] = [
},
];
+// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
+jest.mock('../../services/accessibility/html_id_generator', () => ({
+ htmlIdGenerator: () => () => 'htmlId',
+}));
+
describe('EuiSelectable', () => {
test('is rendered', () => {
const component = render(
diff --git a/src/components/selectable/selectable.tsx b/src/components/selectable/selectable.tsx
index 61bb338ab84..e0e5c55d335 100644
--- a/src/components/selectable/selectable.tsx
+++ b/src/components/selectable/selectable.tsx
@@ -33,13 +33,11 @@ import { EuiSelectableMessage } from './selectable_message';
import { EuiSelectableList } from './selectable_list';
import { EuiLoadingChart } from '../loading';
import { getMatchingOptions } from './matching_options';
-import { keys } from '../../services';
+import { keys, htmlIdGenerator } from '../../services';
import { EuiI18n } from '../i18n';
import { EuiSelectableOption } from './selectable_option';
-import {
- EuiSelectableOptionsListProps,
- EuiSelectableSingleOptionProps,
-} from './selectable_list/selectable_list';
+import { EuiSelectableOptionsListProps } from './selectable_list/selectable_list';
+import { EuiSelectableSearchProps } from './selectable_search/selectable_search';
type RequiredEuiSelectableOptionsListProps = Omit<
EuiSelectableOptionsListProps,
@@ -65,7 +63,7 @@ type EuiSelectableSearchableProps = ExclusiveUnion<
/**
* Passes props down to the `EuiFieldSearch`
*/
- searchProps?: {};
+ searchProps?: Partial;
}
>;
@@ -88,7 +86,7 @@ export type EuiSelectableProps = Omit<
/**
* Array of EuiSelectableOption objects. See #EuiSelectableOptionProps
*/
- options: EuiSelectableOption[];
+ options: Array>;
/**
* Passes back the altered `options` array with selected options as
*/
@@ -99,7 +97,7 @@ export type EuiSelectableProps = Omit<
* `true`: only allows one selection
* `always`: can and must have only one selection
*/
- singleSelection?: EuiSelectableSingleOptionProps;
+ singleSelection?: EuiSelectableOptionsListProps['singleSelection'];
/**
* Allows marking options as `checked='off'` as well as `'on'`
*/
@@ -129,6 +127,7 @@ export interface EuiSelectableState {
activeOptionIndex?: number;
searchValue: string;
visibleOptions: EuiSelectableOption[];
+ isFocused: boolean;
}
export class EuiSelectable extends Component<
@@ -142,7 +141,7 @@ export class EuiSelectable extends Component<
};
private optionsListRef = createRef();
-
+ rootId = htmlIdGenerator();
constructor(props: EuiSelectableProps) {
super(props);
@@ -165,6 +164,7 @@ export class EuiSelectable extends Component<
activeOptionIndex,
searchValue: initialSearchValue,
visibleOptions,
+ isFocused: false,
};
}
@@ -193,6 +193,27 @@ export class EuiSelectable extends Component<
return this.state.activeOptionIndex != null;
};
+ onFocus = () => {
+ if (!this.state.visibleOptions.length || this.state.activeOptionIndex) {
+ return;
+ }
+
+ const firstSelected = this.state.visibleOptions.findIndex(
+ option => option.checked && !option.disabled && !option.isGroupLabel
+ );
+
+ if (firstSelected > -1) {
+ this.setState({ activeOptionIndex: firstSelected, isFocused: true });
+ } else {
+ this.setState({
+ activeOptionIndex: this.state.visibleOptions.findIndex(
+ option => !option.disabled && !option.isGroupLabel
+ ),
+ isFocused: true,
+ });
+ }
+ };
+
onKeyDown = (event: KeyboardEvent) => {
const optionsList = this.optionsListRef.current;
@@ -210,6 +231,8 @@ export class EuiSelectable extends Component<
break;
case keys.ENTER:
+ case keys.SPACE:
+ event.preventDefault();
event.stopPropagation();
if (this.state.activeOptionIndex != null && optionsList) {
optionsList.onAddOrRemoveOption(
@@ -218,20 +241,23 @@ export class EuiSelectable extends Component<
}
break;
- case keys.TAB:
- // Disallow tabbing when the user is navigating the options.
- // TODO: Can we force the tab to the next sibling element?
- if (this.hasActiveOption()) {
- event.preventDefault();
- event.stopPropagation();
- }
+ case keys.HOME:
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({ activeOptionIndex: 0 });
+ break;
+
+ case keys.END:
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({
+ activeOptionIndex: this.state.visibleOptions.length - 1,
+ });
break;
default:
- if (this.props.onKeyDown) {
- this.props.onKeyDown(event);
- }
- this.clearActiveOption();
+ this.setState({ activeOptionIndex: undefined }, this.onFocus);
+ break;
}
};
@@ -258,10 +284,12 @@ export class EuiSelectable extends Component<
}
}
- // Group titles are included in option list but are not selectable
- // Skip group title options
+ // Group titles and disabled options are included in option list but are not selectable
const direction = amount > 0 ? 1 : -1;
- while (visibleOptions[nextActiveOptionIndex].isGroupLabel) {
+ while (
+ visibleOptions[nextActiveOptionIndex].isGroupLabel ||
+ visibleOptions[nextActiveOptionIndex].disabled
+ ) {
nextActiveOptionIndex = nextActiveOptionIndex + direction;
if (nextActiveOptionIndex < 0) {
@@ -275,29 +303,35 @@ export class EuiSelectable extends Component<
});
};
- clearActiveOption = () => {
- this.setState({
- activeOptionIndex: undefined,
- });
- };
-
onSearchChange = (
visibleOptions: EuiSelectableOption[],
searchValue: string
) => {
- this.setState({
- visibleOptions,
- searchValue,
- });
+ this.setState(
+ {
+ visibleOptions,
+ searchValue,
+ activeOptionIndex: undefined,
+ },
+ () => {
+ if (this.state.isFocused) {
+ this.onFocus();
+ }
+ }
+ );
};
onContainerBlur = () => {
- this.clearActiveOption();
+ this.setState({
+ activeOptionIndex: undefined,
+ isFocused: false,
+ });
};
onOptionClick = (options: EuiSelectableOption[]) => {
this.setState(state => ({
visibleOptions: getMatchingOptions(options, state.searchValue),
+ activeOptionIndex: this.state.activeOptionIndex,
}));
if (this.props.onChange) {
this.props.onChange(options);
@@ -319,12 +353,32 @@ export class EuiSelectable extends Component<
renderOption,
height,
allowExclusions,
+ 'aria-label': ariaLabel,
+ 'aria-describedby': ariaDescribedby,
...rest
} = this.props;
const { searchValue, visibleOptions, activeOptionIndex } = this.state;
- let messageContent;
+ // Some messy destructuring here to remove aria-label/describedby from searchProps and listProps
+ // Made messier by some TS requirements
+ // The aria attributes are then used in getAccessibleName() to place them where they need to go
+ const unknownAccessibleName = {
+ 'aria-label': undefined,
+ 'aria-describedby': undefined,
+ };
+ const {
+ 'aria-label': searchAriaLabel,
+ 'aria-describedby': searchAriaDescribedby,
+ ...cleanedSearchProps
+ } = searchProps || unknownAccessibleName;
+ const {
+ 'aria-label': listAriaLabel,
+ 'aria-describedby': listAriaDescribedby,
+ ...cleanedListProps
+ } = listProps || unknownAccessibleName;
+
+ let messageContent: JSX.Element | undefined;
if (isLoading) {
messageContent = (
@@ -368,36 +422,126 @@ export class EuiSelectable extends Component<
className
);
+ const messageContentId = messageContent && this.rootId('messageContent');
+ const listId = this.rootId('listbox');
+ const makeOptionId = (index: number | undefined) => {
+ if (typeof index === 'undefined') {
+ return '';
+ }
+
+ return `${listId}_option-${index}`;
+ };
+
+ /**
+ * There are lots of ways to add an accessible name
+ * Usually we want the same name for the input and the listbox (which is added by aria-label/describedby)
+ * But you can always override it using searchProps or listProps
+ * This finds the correct name to use
+ *
+ * TODO: This doesn't handle being labelled (