(() => {
+ if (!renderOption) {
+ return undefined;
+ }
+ const toGroupItem = (item: ListboxGroupItem): OptionListProps.OptionGroupItem => ({
+ ...item,
+ option: toPublicGroup(item.option),
+ });
+ return ({ item }) => {
+ if (item.type === 'item') {
+ const parent = item.parent && toGroupItem(item.parent);
+ return renderOption({ item: { ...item, option: toPublicOption(item.option), parent } });
+ }
+ if (item.type === 'group') {
+ return renderOption({ item: toGroupItem(item) });
+ }
+ // The listbox renders only options and group headers; it has no trigger or select-all item.
+ return null;
+ };
+ }, [renderOption, toPublicOption, toPublicGroup]);
+
+ // The root, not the list, clears the highlight when the pointer leaves, so the leave covers the whole component.
+ const { onMouseLeave } = controller.getListProps();
+
+ return (
+
+ {
+ const leafIndex = leafIndexes.get(option);
+ return {
+ ...controller.getOptionProps(option, index),
+ // Inline, there is no dropdown border above the list, so the first item has no top divider.
+ hideTopDivider: index === 0,
+ className: leafIndex === undefined ? testClasses['group-header'] : testClasses.option,
+ // 1-based option position, used by test-utils. Group headers are not counted.
+ 'data-option-index': leafIndex === undefined ? undefined : leafIndex + 1,
+ 'data-value': option.option.value,
+ };
+ }}
+ />
+
+ );
+ }
+);
+
+export default InternalOptionList;
diff --git a/src/option-list/styles.scss b/src/option-list/styles.scss
new file mode 100644
index 00000000000..3149d4ea797
--- /dev/null
+++ b/src/option-list/styles.scss
@@ -0,0 +1,11 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../internal/styles' as styles;
+
+.root {
+ @include styles.styles-reset;
+ display: block;
+}
diff --git a/src/option-list/test-classes/styles.scss b/src/option-list/test-classes/styles.scss
new file mode 100644
index 00000000000..a9e28fbeae3
--- /dev/null
+++ b/src/option-list/test-classes/styles.scss
@@ -0,0 +1,9 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+.root,
+.option,
+.group-header {
+ /* used in test-utils */
+}
diff --git a/src/test-utils/dom/option-list/index.ts b/src/test-utils/dom/option-list/index.ts
new file mode 100644
index 00000000000..1e550353437
--- /dev/null
+++ b/src/test-utils/dom/option-list/index.ts
@@ -0,0 +1,132 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { ComponentWrapper, ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom';
+import { escapeSelector } from '@cloudscape-design/test-utils-core/utils';
+
+import OptionWrapper from '../internal/option.js';
+
+import selectableItemsStyles from '../../../internal/components/selectable-item/styles.selectors.js';
+import styles from '../../../option-list/test-classes/styles.selectors.js';
+
+export class OptionListOptionWrapper extends ComponentWrapper {
+ /**
+ * Returns the option label.
+ */
+ findLabel(): ElementWrapper | null {
+ return this.findOptionContent()?.findLabel() ?? null;
+ }
+
+ /**
+ * Returns the option description.
+ */
+ findDescription(): ElementWrapper | null {
+ return this.findOptionContent()?.findDescription() ?? null;
+ }
+
+ /**
+ * Returns the option label tag.
+ */
+ findLabelTag(): ElementWrapper | null {
+ return this.findOptionContent()?.findLabelTag() ?? null;
+ }
+
+ /**
+ * Returns the option tags.
+ */
+ findTags(): Array {
+ return this.findOptionContent()?.findTags() ?? [];
+ }
+
+ /**
+ * Returns `true` if the option is selected. A group header in multi selection is selected when all its options are selected.
+ */
+ @usesDom
+ isSelected(): boolean {
+ return this.element.classList.contains(selectableItemsStyles.selected);
+ }
+
+ /**
+ * Returns `true` if the option is highlighted.
+ */
+ @usesDom
+ isHighlighted(): boolean {
+ return this.element.classList.contains(selectableItemsStyles.highlighted);
+ }
+
+ /**
+ * Returns `true` if the option is disabled.
+ */
+ @usesDom
+ isDisabled(): boolean {
+ return this.element.classList.contains(selectableItemsStyles.disabled);
+ }
+
+ private findOptionContent(): OptionWrapper | null {
+ return this.findComponent(`.${OptionWrapper.rootSelector}`, OptionWrapper);
+ }
+}
+
+export default class OptionListWrapper extends ComponentWrapper {
+ static rootSelector: string = styles.root;
+
+ /**
+ * Returns all options. Group headers are not included.
+ */
+ findOptions(): Array {
+ return this.findAllByClassName(styles.option).map(wrapper => new OptionListOptionWrapper(wrapper.getElement()));
+ }
+
+ /**
+ * Returns an option for a given index. Group headers are not counted.
+ *
+ * @param index 1-based index of the option to return.
+ */
+ findOptionByIndex(index: number): OptionListOptionWrapper | null {
+ return this.findComponent(`.${styles.option}[data-option-index="${index}"]`, OptionListOptionWrapper);
+ }
+
+ /**
+ * Returns an option for a given value.
+ *
+ * @param value The `value` of the option.
+ */
+ findOptionByValue(value: string): OptionListOptionWrapper | null {
+ const escapedValue = escapeSelector(value);
+ return this.findComponent(`.${styles.option}[data-value="${escapedValue}"]`, OptionListOptionWrapper);
+ }
+
+ /**
+ * Returns all selected options. Group headers are not included.
+ */
+ findSelectedOptions(): Array {
+ return this.findAll(`.${styles.option}.${selectableItemsStyles.selected}`).map(
+ wrapper => new OptionListOptionWrapper(wrapper.getElement())
+ );
+ }
+
+ /**
+ * Returns the highlighted option. Group headers are not included.
+ */
+ findHighlightedOption(): OptionListOptionWrapper | null {
+ return this.findComponent(`.${styles.option}.${selectableItemsStyles.highlighted}`, OptionListOptionWrapper);
+ }
+
+ /**
+ * Returns all option group headers.
+ */
+ findGroupHeaders(): Array {
+ return this.findAllByClassName(styles['group-header']).map(
+ wrapper => new OptionListOptionWrapper(wrapper.getElement())
+ );
+ }
+
+ /**
+ * Returns an option group header for a given value.
+ *
+ * @param value The `value` of the option group.
+ */
+ findGroupHeaderByValue(value: string): OptionListOptionWrapper | null {
+ const escapedValue = escapeSelector(value);
+ return this.findComponent(`.${styles['group-header']}[data-value="${escapedValue}"]`, OptionListOptionWrapper);
+ }
+}