Skip to content

Commit ae67e80

Browse files
authored
refactor: consolidate defaultItemToString implementations (#20402)
1 parent 8219772 commit ae67e80

File tree

9 files changed

+112
-88
lines changed

9 files changed

+112
-88
lines changed

packages/react/src/components/ComboBox/ComboBox.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { autoUpdate, flip, hide, useFloating } from '@floating-ui/react';
4848
import { TranslateWithId } from '../../types/common';
4949
import { useFeatureFlag } from '../FeatureFlags';
5050
import { AILabel } from '../AILabel';
51-
import { isComponentElement } from '../../internal';
51+
import { defaultItemToString, isComponentElement } from '../../internal';
5252

5353
const {
5454
InputBlur,
@@ -63,24 +63,6 @@ const {
6363
FunctionSelectItem,
6464
} = useCombobox.stateChangeTypes;
6565

66-
const defaultItemToString = <ItemType,>(item: ItemType | null) => {
67-
if (typeof item === 'string') {
68-
return item;
69-
}
70-
if (typeof item === 'number') {
71-
return `${item}`;
72-
}
73-
if (
74-
item !== null &&
75-
typeof item === 'object' &&
76-
'label' in item &&
77-
typeof item['label'] === 'string'
78-
) {
79-
return item['label'];
80-
}
81-
return '';
82-
};
83-
8466
const defaultShouldFilterItem = () => true;
8567

8668
const autocompleteCustomFilter = ({

packages/react/src/components/Dropdown/Dropdown.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,31 +57,13 @@ import {
5757
} from '@floating-ui/react';
5858
import { useFeatureFlag } from '../FeatureFlags';
5959
import { AILabel } from '../AILabel';
60-
import { isComponentElement } from '../../internal';
60+
import { defaultItemToString, isComponentElement } from '../../internal';
6161

6262
const { ItemMouseMove, MenuMouseLeave } =
6363
useSelect.stateChangeTypes as UseSelectInterface['stateChangeTypes'] & {
6464
ToggleButtonClick: UseSelectStateChangeTypes.ToggleButtonClick;
6565
};
6666

67-
const defaultItemToString = <ItemType,>(item?: ItemType | null): string => {
68-
if (typeof item === 'string') {
69-
return item;
70-
}
71-
if (typeof item === 'number') {
72-
return `${item}`;
73-
}
74-
if (
75-
item !== null &&
76-
typeof item === 'object' &&
77-
'label' in item &&
78-
typeof item['label'] === 'string'
79-
) {
80-
return item['label'];
81-
}
82-
return '';
83-
};
84-
8567
type ExcludedAttributes = 'id' | 'onChange';
8668

8769
export interface OnChangeData<ItemType> {

packages/react/src/components/Menu/MenuItem.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Menu } from './Menu';
4040
import { MenuContext } from './MenuContext';
4141
import { useLayoutDirection } from '../LayoutDirection';
4242
import { Text } from '../Text';
43+
import { defaultItemToString } from '../../internal';
4344

4445
export interface MenuItemProps extends LiHTMLAttributes<HTMLLIElement> {
4546
/**
@@ -480,8 +481,6 @@ MenuItemGroup.propTypes = {
480481
label: PropTypes.string.isRequired,
481482
};
482483

483-
const defaultItemToString = (item) => item.toString();
484-
485484
export interface MenuItemRadioGroupProps<Item>
486485
extends Omit<ComponentProps<'ul'>, 'onChange'> {
487486
/**
@@ -495,7 +494,7 @@ export interface MenuItemRadioGroupProps<Item>
495494
defaultSelectedItem?: Item;
496495

497496
/**
498-
* Provide a function to convert an item to the string that will be rendered. Defaults to item.toString().
497+
* Converts an item into a string for display.
499498
*/
500499
itemToString?: (item: Item) => string;
501500

@@ -586,7 +585,7 @@ MenuItemRadioGroup.propTypes = {
586585
defaultSelectedItem: PropTypes.any,
587586

588587
/**
589-
* Provide a function to convert an item to the string that will be rendered. Defaults to item.toString().
588+
* Converts an item into a string for display.
590589
*/
591590
itemToString: PropTypes.func,
592591

packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import ListBox, {
5050
import Checkbox from '../Checkbox';
5151
import { ListBoxTrigger, ListBoxSelection } from '../ListBox/next';
5252
import { match, keys } from '../../internal/keyboard';
53-
import { defaultItemToString } from './tools/itemToString';
5453
import mergeRefs from '../../tools/mergeRefs';
5554
import { deprecate } from '../../prop-types/deprecate';
5655
import { useId } from '../../internal/useId';
@@ -67,7 +66,7 @@ import {
6766
} from '@floating-ui/react';
6867
import { TranslateWithId } from '../../types/common';
6968
import { AILabel } from '../AILabel';
70-
import { isComponentElement } from '../../internal';
69+
import { defaultItemToString, isComponentElement } from '../../internal';
7170

7271
const {
7372
InputBlur,

packages/react/src/components/MultiSelect/MultiSelect.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import {
5858
} from '@floating-ui/react';
5959
import { useFeatureFlag } from '../FeatureFlags';
6060
import { AILabel } from '../AILabel';
61-
import { isComponentElement } from '../../internal';
61+
import { defaultItemToString, isComponentElement } from '../../internal';
6262

6363
const {
6464
ItemClick,
@@ -78,24 +78,6 @@ const {
7878
ToggleButtonClick: UseSelectStateChangeTypes.ToggleButtonClick;
7979
};
8080

81-
const defaultItemToString = <ItemType,>(item?: ItemType): string => {
82-
if (typeof item === 'string') {
83-
return item;
84-
}
85-
if (typeof item === 'number') {
86-
return `${item}`;
87-
}
88-
if (
89-
item !== null &&
90-
typeof item === 'object' &&
91-
'label' in item &&
92-
typeof item['label'] === 'string'
93-
) {
94-
return item['label'];
95-
}
96-
return '';
97-
};
98-
9981
interface selectedItemType {
10082
text: string;
10183
}

packages/react/src/components/MultiSelect/tools/itemToString.js

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { defaultItemToString } from '../defaultItemToString';
9+
10+
describe('defaultItemToString', () => {
11+
it('should return strings as is', () => {
12+
expect(defaultItemToString('hello')).toBe('hello');
13+
expect(defaultItemToString('')).toBe('');
14+
});
15+
16+
it('should stringify numbers', () => {
17+
expect(defaultItemToString(0)).toBe('0');
18+
expect(defaultItemToString(42)).toBe('42');
19+
expect(defaultItemToString(-7)).toBe('-7');
20+
});
21+
22+
it('should handle nullish values', () => {
23+
expect(defaultItemToString(null)).toBe('');
24+
expect(defaultItemToString(undefined)).toBe('');
25+
});
26+
27+
it('should return the label when the object has a string label', () => {
28+
expect(defaultItemToString({ label: 'Option A' })).toBe('Option A');
29+
expect(defaultItemToString({ label: '' })).toBe('');
30+
});
31+
32+
it('should ignore non-string labels', () => {
33+
expect(defaultItemToString({ label: 123 })).toBe('');
34+
expect(defaultItemToString({ label: false })).toBe('');
35+
expect(defaultItemToString({ label: null })).toBe('');
36+
expect(defaultItemToString({ label: undefined })).toBe('');
37+
});
38+
39+
it('should ignore objects without labels', () => {
40+
expect(defaultItemToString({})).toBe('');
41+
expect(defaultItemToString({ name: 'nope' })).toBe('');
42+
});
43+
44+
it('should ignore arrays unless they have a string label property', () => {
45+
const arr = [];
46+
47+
expect(defaultItemToString(arr)).toBe('');
48+
49+
arr.label = 'from array';
50+
51+
expect(defaultItemToString(arr)).toBe('from array');
52+
});
53+
54+
it('should read inherited labels from prototype chain', () => {
55+
const proto = { label: 'from proto' };
56+
const obj = Object.create(proto);
57+
58+
expect(defaultItemToString(obj)).toBe('from proto');
59+
});
60+
61+
it('should return empty strings for functions, bigints, and symbols', () => {
62+
expect(defaultItemToString(() => {})).toBe('');
63+
expect(defaultItemToString(10n)).toBe('');
64+
expect(defaultItemToString(Symbol('x'))).toBe('');
65+
});
66+
67+
it('should return empty string for boxed primitives (Number, String, Boolean)', () => {
68+
expect(defaultItemToString(new Number(5))).toBe('');
69+
expect(defaultItemToString(new String('hello'))).toBe('');
70+
expect(defaultItemToString(new Boolean(true))).toBe('');
71+
});
72+
73+
it('should handle objects with non-enumerable string label', () => {
74+
const object = {};
75+
76+
Object.defineProperty(object, 'label', {
77+
value: 'hidden label',
78+
enumerable: false,
79+
});
80+
81+
expect(defaultItemToString(object)).toBe('hidden label');
82+
});
83+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export const defaultItemToString = <ItemType>(item: ItemType | null) => {
9+
if (typeof item === 'string') return item;
10+
if (typeof item === 'number') return `${item}`;
11+
if (
12+
item &&
13+
typeof item === 'object' &&
14+
'label' in item &&
15+
typeof item.label === 'string'
16+
) {
17+
return item.label;
18+
}
19+
20+
return '';
21+
};

packages/react/src/internal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
export * from './defaultItemToString';
89
export * from './utils';

0 commit comments

Comments
 (0)