diff --git a/changelogs/upcoming/7392.md b/changelogs/upcoming/7392.md
new file mode 100644
index 00000000000..4b87c9f6f8b
--- /dev/null
+++ b/changelogs/upcoming/7392.md
@@ -0,0 +1,3 @@
+**Bug fixes**
+
+- Fixed a bug with `EuiSelectable`s with custom `truncationProps`, where scrollbar widths were not being accounted for
diff --git a/src/components/selectable/selectable.spec.tsx b/src/components/selectable/selectable.spec.tsx
index 30cb60cb987..6c43f3a6957 100644
--- a/src/components/selectable/selectable.spec.tsx
+++ b/src/components/selectable/selectable.spec.tsx
@@ -284,6 +284,23 @@ describe('EuiSelectable', () => {
);
});
+ it('correctly accounts for scrollbar width', () => {
+ const multipleOptions = Array.from({ length: 5 }).map(
+ () => sharedProps.options[0]
+ );
+ cy.realMount(
+
+ );
+
+ cy.get('[data-test-subj="truncatedText"]')
+ .first()
+ .should('have.text', 'Lorem ipsum …iscing elit.');
+ });
+
it('correctly accounts for the keyboard focus badge', () => {
cy.realMount();
diff --git a/src/components/selectable/selectable_list/selectable_list.test.tsx b/src/components/selectable/selectable_list/selectable_list.test.tsx
index 140fa0ce454..847ed388948 100644
--- a/src/components/selectable/selectable_list/selectable_list.test.tsx
+++ b/src/components/selectable/selectable_list/selectable_list.test.tsx
@@ -362,6 +362,13 @@ describe('EuiSelectableListItem', () => {
});
describe('truncation performance optimization', () => {
+ // Mock requestAnimationFrame
+ beforeEach(() => {
+ jest
+ .spyOn(window, 'requestAnimationFrame')
+ .mockImplementation((cb: Function) => cb());
+ });
+
it('does not render EuiTextTruncate if not virtualized and text is wrapping', () => {
const { container } = render(
{
});
it('attempts to use a default optimized option width calculated from the wrapping EuiAutoSizer', () => {
+ // jsdom doesn't return valid element offsetWidths, so we have to mock it here
+ Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
+ configurable: true,
+ value: 600,
+ });
+
const { container } = render(
{
expect(
container.querySelector('[data-resize-observer]')
).not.toBeInTheDocument();
+
+ // Reset jsdom mock
+ Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 0 });
});
it('falls back to individual resize observers if options have append/prepend nodes', () => {
diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx
index 7e907e050e9..9812d3a9e77 100644
--- a/src/components/selectable/selectable_list/selectable_list.tsx
+++ b/src/components/selectable/selectable_list/selectable_list.tsx
@@ -479,15 +479,23 @@ export class EuiSelectableList extends Component<
const checkedIconOffset = this.props.showIcons === false ? 0 : 28; // Defaults to true
this.focusBadgeOffset = this.props.onFocusBadge === false ? 0 : 46;
- this.setState({
- defaultOptionWidth: containerWidth - paddingOffset - checkedIconOffset,
- });
+ // Wait a tick for the listbox ref to update before proceeding
+ requestAnimationFrame(() => {
+ const scrollbarOffset = this.listBoxRef
+ ? containerWidth - this.listBoxRef.offsetWidth
+ : 0;
- // Potentially force list rows to rerender on dynamic resize as well,
- // but try to do it as lightly as possible
- if (truncationProps || (searchable && searchValue)) {
- this.forceVirtualizedListRowRerender();
- }
+ this.setState({
+ defaultOptionWidth:
+ containerWidth - scrollbarOffset - paddingOffset - checkedIconOffset,
+ });
+
+ // Potentially force list rows to rerender on dynamic resize as well,
+ // but try to do it as lightly as possible
+ if (truncationProps || (searchable && searchValue)) {
+ this.forceVirtualizedListRowRerender();
+ }
+ });
};
getTruncationProps = (option: EuiSelectableOption, isFocused: boolean) => {