Skip to content

Commit e8c5872

Browse files
authored
fix: update ref handling in ListBoxMenuItem useIsTruncated hook (#18611)
* fix: update ref handling in ListBoxMenuItem useIsTruncated hook * refactor: merge forwarded and local refs in useIsTruncated hook * refactor: update useIsTruncated to use useMergedRefs
1 parent 25f0b35 commit e8c5872

File tree

3 files changed

+72
-23
lines changed

3 files changed

+72
-23
lines changed

packages/react/src/components/ListBox/ListBoxMenuItem.tsx

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -8,26 +8,52 @@
88
import cx from 'classnames';
99
import React, {
1010
ForwardedRef,
11+
forwardRef,
1112
ReactNode,
1213
useEffect,
1314
useRef,
1415
useState,
16+
type MutableRefObject,
17+
type Ref,
1518
} from 'react';
1619
import PropTypes from 'prop-types';
1720
import { usePrefix } from '../../internal/usePrefix';
1821
import { ForwardRefReturn, ReactAttr } from '../../types/common';
22+
import { useMergedRefs } from '../../internal/useMergedRefs';
1923

20-
function useIsTruncated(ref) {
24+
/**
25+
* Determines if the content of an element is truncated.
26+
*
27+
* Merges a forwarded ref with a local ref to check the element's dimensions.
28+
*
29+
* @template T
30+
* @param forwardedRef - A ref passed from the parent component.
31+
* @param deps - Dependencies to re-run the truncation check.
32+
* @returns An object containing the truncation state and the merged ref.
33+
*/
34+
const useIsTruncated = <T extends HTMLElement>(
35+
forwardedRef?: Ref<T>,
36+
deps: any[] = []
37+
) => {
38+
const localRef = useRef<T>(null);
39+
const mergedRef = useMergedRefs([
40+
...(forwardedRef ? [forwardedRef] : []),
41+
localRef,
42+
]);
2143
const [isTruncated, setIsTruncated] = useState(false);
2244

2345
useEffect(() => {
24-
const element = ref.current;
25-
const { offsetWidth, scrollWidth } = element;
26-
setIsTruncated(offsetWidth < scrollWidth);
27-
}, [ref, setIsTruncated]);
46+
const element = localRef.current;
2847

29-
return isTruncated;
30-
}
48+
if (element) {
49+
const { offsetWidth, scrollWidth } = element;
50+
51+
setIsTruncated(offsetWidth < scrollWidth);
52+
}
53+
}, [localRef, ...deps]);
54+
55+
return { isTruncated, ref: mergedRef };
56+
};
3157

3258
export interface ListBoxMenuItemProps extends ReactAttr<HTMLLIElement> {
3359
/**
@@ -58,7 +84,7 @@ export interface ListBoxMenuItemProps extends ReactAttr<HTMLLIElement> {
5884

5985
export type ListBoxMenuItemForwardedRef =
6086
| (ForwardedRef<HTMLLIElement> & {
61-
menuItemOptionRef?: React.Ref<HTMLDivElement>;
87+
menuItemOptionRef?: Ref<HTMLDivElement>;
6288
})
6389
| null;
6490

@@ -72,20 +98,26 @@ export type ListBoxMenuItemComponent = ForwardRefReturn<
7298
* name, alongside any classes for any corresponding states, for a generic list
7399
* box menu item.
74100
*/
75-
const ListBoxMenuItem = React.forwardRef<HTMLLIElement, ListBoxMenuItemProps>(
76-
function ListBoxMenuItem(
77-
{
78-
children,
79-
isActive = false,
80-
isHighlighted = false,
81-
title,
82-
...rest
83-
}: ListBoxMenuItemProps,
84-
forwardedRef: ListBoxMenuItemForwardedRef
85-
) {
101+
const ListBoxMenuItem = forwardRef<HTMLLIElement, ListBoxMenuItemProps>(
102+
(
103+
{ children, isActive = false, isHighlighted = false, title, ...rest },
104+
forwardedRef
105+
) => {
86106
const prefix = usePrefix();
87-
const ref = useRef(null);
88-
const isTruncated = useIsTruncated(forwardedRef?.menuItemOptionRef || ref);
107+
108+
const menuItemOptionRefProp =
109+
forwardedRef && typeof forwardedRef !== 'function'
110+
? (
111+
forwardedRef as MutableRefObject<HTMLLIElement | null> & {
112+
menuItemOptionRef?: Ref<HTMLDivElement>;
113+
}
114+
).menuItemOptionRef
115+
: undefined;
116+
117+
const { isTruncated, ref: menuItemOptionRef } = useIsTruncated(
118+
menuItemOptionRefProp,
119+
[children]
120+
);
89121
const className = cx(`${prefix}--list-box__menu-item`, {
90122
[`${prefix}--list-box__menu-item--active`]: isActive,
91123
[`${prefix}--list-box__menu-item--highlighted`]: isHighlighted,
@@ -98,7 +130,7 @@ const ListBoxMenuItem = React.forwardRef<HTMLLIElement, ListBoxMenuItemProps>(
98130
title={isTruncated ? title : undefined}>
99131
<div
100132
className={`${prefix}--list-box__menu-item__option`}
101-
ref={forwardedRef?.menuItemOptionRef || ref}>
133+
ref={menuItemOptionRef}>
102134
{children}
103135
</div>
104136
</li>

packages/react/src/components/ListBox/__tests__/ListBoxMenuItem-test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,18 @@ describe('ListBoxMenuItem', () => {
2828
const { container } = render(<ListBox.MenuItem data-testid="test" />);
2929
expect(container.firstChild).toHaveAttribute('data-testid', 'test');
3030
});
31+
32+
it('should forward `menuItemOptionRef` to the inner `div` element', () => {
33+
let innerDivNode = null;
34+
const menuItemOptionRefCallback = jest.fn((node) => {
35+
innerDivNode = node;
36+
});
37+
const forwardedRef = { menuItemOptionRef: menuItemOptionRefCallback };
38+
const { container } = render(<ListBox.MenuItem ref={forwardedRef} />);
39+
const expectedDiv = container.querySelector(
40+
'.cds--list-box__menu-item__option'
41+
);
42+
43+
expect(innerDivNode).toBe(expectedDiv);
44+
});
3145
});

packages/react/src/internal/useMergedRefs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import { useCallback, Ref, ForwardedRef } from 'react';
99

10+
// TODO: Investigate updating this hook based on the following code from
11+
// https://github.com/carbon-design-system/carbon/pull/18611:
12+
// https://github.com/adamalston/carbon/blob/dd403b6b10de3d8a6ccd8d2e21174c908c1e890a/packages/react/src/components/ListBox/ListBoxMenuItem.tsx#L23-L46
1013
/**
1114
* Combine multiple refs into a single ref. This use useful when you have two
1215
* refs from both `React.forwardRef` and `useRef` that you would like to add to

0 commit comments

Comments
 (0)