Skip to content

Commit 3340304

Browse files
Kilian-Collendertay1orjonesguidari
committed
fix(tag): use refs to handle component access (carbon-design-system#16571)
* fix(tag): use refs to handle component access When using the querySelector it is easily broken if the id has reserved characters. Also the isEllipsisActive helper had no protection for a non element. * fix(tag): cast BaseComponent type * fix(tag): improve deprecation notices * fix: added forwardRef to Tag to grab ref in variants * fix: removed console log * fix: fixed spelling and remove ref from old filter * fix: updated snapshots * fix: fixed onMouseEnter error on console * fix: fixed TS error --------- Co-authored-by: Taylor Jones <[email protected]> Co-authored-by: guidari <[email protected]>
1 parent 574070e commit 3340304

File tree

7 files changed

+87
-70
lines changed

7 files changed

+87
-70
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8234,6 +8234,7 @@ Map {
82348234
},
82358235
},
82368236
"Tag" => Object {
8237+
"$$typeof": Symbol(react.forward_ref),
82378238
"propTypes": Object {
82388239
"as": Object {
82398240
"type": "elementType",
@@ -8299,6 +8300,7 @@ Map {
82998300
"type": "oneOf",
83008301
},
83018302
},
8303+
"render": [Function],
83028304
},
83038305
"TagSkeleton" => Object {
83048306
"propTypes": Object {

packages/react/src/components/Tag/DismissibleTag.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { useLayoutEffect, useState, ReactNode } from 'react';
9+
import React, { useLayoutEffect, useState, ReactNode, useRef } from 'react';
1010
import classNames from 'classnames';
1111
import setupGetInstanceId from '../../tools/setupGetInstanceId';
1212
import { usePrefix } from '../../internal/usePrefix';
@@ -15,6 +15,7 @@ import Tag, { SIZES, TYPES } from './Tag';
1515
import { Close } from '@carbon/icons-react';
1616
import { Tooltip } from '../Tooltip';
1717
import { Text } from '../Text';
18+
import { isEllipsisActive } from './isEllipsisActive';
1819

1920
const getInstanceId = setupGetInstanceId();
2021

@@ -91,22 +92,17 @@ const DismissibleTag = <T extends React.ElementType>({
9192
...other
9293
}: DismissibleTagProps<T>) => {
9394
const prefix = usePrefix();
95+
const tagLabelRef = useRef<HTMLElement>();
9496
const tagId = id || `tag-${getInstanceId()}`;
9597
const tagClasses = classNames(`${prefix}--tag--filter`, className);
9698
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
9799

98-
const isEllipsisActive = (element: any) => {
99-
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
100-
return element.offsetWidth < element.scrollWidth;
101-
};
102-
103100
useLayoutEffect(() => {
104-
const elementTagId = document.querySelector(`#${tagId}`);
105-
const newElement = elementTagId?.getElementsByClassName(
101+
const newElement = tagLabelRef.current?.getElementsByClassName(
106102
`${prefix}--tag__label`
107103
)[0];
108-
isEllipsisActive(newElement);
109-
}, [prefix, tagId]);
104+
setIsEllipsisApplied(isEllipsisActive(newElement));
105+
}, [prefix, tagLabelRef]);
110106
const handleClose = (event: React.MouseEvent<HTMLButtonElement>) => {
111107
if (onClose) {
112108
event.stopPropagation();
@@ -134,7 +130,8 @@ const DismissibleTag = <T extends React.ElementType>({
134130
const dismissLabel = `Dismiss "${text}"`;
135131

136132
return (
137-
<Tag<any>
133+
<Tag
134+
ref={tagLabelRef}
138135
type={type}
139136
size={size}
140137
renderIcon={renderIcon}

packages/react/src/components/Tag/OperationalTag.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import React, {
1111
useLayoutEffect,
1212
useState,
1313
ReactNode,
14+
useRef,
1415
} from 'react';
1516
import classNames from 'classnames';
1617
import setupGetInstanceId from '../../tools/setupGetInstanceId';
@@ -19,6 +20,7 @@ import { PolymorphicProps } from '../../types/common';
1920
import Tag, { SIZES } from './Tag';
2021
import { Tooltip } from '../Tooltip';
2122
import { Text } from '../Text';
23+
import { isEllipsisActive } from './isEllipsisActive';
2224

2325
const getInstanceId = setupGetInstanceId();
2426

@@ -97,23 +99,18 @@ const OperationalTag = <T extends React.ElementType>({
9799
...other
98100
}: OperationalTagProps<T>) => {
99101
const prefix = usePrefix();
102+
const tagRef = useRef<HTMLElement>();
100103
const tagId = id || `tag-${getInstanceId()}`;
101104
const tagClasses = classNames(`${prefix}--tag--operational`, className);
102105
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
103106

104-
const isEllipsisActive = (element: any) => {
105-
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
106-
return element.offsetWidth < element.scrollWidth;
107-
};
108-
109107
useLayoutEffect(() => {
110-
const elementTagId = document.querySelector(`#${tagId}`);
111-
const newElement = elementTagId?.getElementsByClassName(
108+
const newElement = tagRef.current?.getElementsByClassName(
112109
`${prefix}--tag__label`
113110
)[0];
114111

115-
isEllipsisActive(newElement);
116-
}, [prefix, tagId]);
112+
setIsEllipsisApplied(isEllipsisActive(newElement));
113+
}, [prefix, tagRef]);
117114

118115
let normalizedSlug;
119116
if (slug && slug['type']?.displayName === 'Slug') {
@@ -135,9 +132,10 @@ const OperationalTag = <T extends React.ElementType>({
135132
align="bottom"
136133
className={tooltipClasses}
137134
leaveDelayMs={0}
138-
onMouseEnter={false}
135+
onMouseEnter={() => false}
139136
closeOnActivation>
140-
<Tag<any>
137+
<Tag
138+
ref={tagRef}
141139
type={type}
142140
size={size}
143141
renderIcon={renderIcon}
@@ -155,7 +153,8 @@ const OperationalTag = <T extends React.ElementType>({
155153
}
156154

157155
return (
158-
<Tag<any>
156+
<Tag
157+
ref={tagRef}
159158
type={type}
160159
size={size}
161160
renderIcon={renderIcon}

packages/react/src/components/Tag/SelectableTag.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { useLayoutEffect, useState, ReactNode } from 'react';
9+
import React, { useLayoutEffect, useState, ReactNode, useRef } from 'react';
1010
import classNames from 'classnames';
1111
import setupGetInstanceId from '../../tools/setupGetInstanceId';
1212
import { usePrefix } from '../../internal/usePrefix';
1313
import { PolymorphicProps } from '../../types/common';
1414
import Tag, { SIZES } from './Tag';
1515
import { Tooltip } from '../Tooltip';
1616
import { Text } from '../Text';
17+
import { isEllipsisActive } from './isEllipsisActive';
1718

1819
const getInstanceId = setupGetInstanceId();
1920

@@ -78,25 +79,20 @@ const SelectableTag = <T extends React.ElementType>({
7879
...other
7980
}: SelectableTagProps<T>) => {
8081
const prefix = usePrefix();
82+
const tagRef = useRef<HTMLElement>();
8183
const tagId = id || `tag-${getInstanceId()}`;
8284
const [selectedTag, setSelectedTag] = useState(selected);
8385
const tagClasses = classNames(`${prefix}--tag--selectable`, className, {
8486
[`${prefix}--tag--selectable-selected`]: selectedTag,
8587
});
8688
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
8789

88-
const isEllipsisActive = (element: any) => {
89-
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
90-
return element.offsetWidth < element.scrollWidth;
91-
};
92-
9390
useLayoutEffect(() => {
94-
const elementTagId = document.querySelector(`#${tagId}`);
95-
const newElement = elementTagId?.getElementsByClassName(
91+
const newElement = tagRef.current?.getElementsByClassName(
9692
`${prefix}--tag__label`
9793
)[0];
98-
isEllipsisActive(newElement);
99-
}, [prefix, tagId]);
94+
setIsEllipsisApplied(isEllipsisActive(newElement));
95+
}, [prefix, tagRef]);
10096

10197
let normalizedSlug;
10298
if (slug && slug['type']?.displayName === 'Slug') {
@@ -122,8 +118,9 @@ const SelectableTag = <T extends React.ElementType>({
122118
align="bottom"
123119
className={tooltipClasses}
124120
leaveDelayMs={0}
125-
onMouseEnter={false}>
126-
<Tag<any>
121+
onMouseEnter={() => false}>
122+
<Tag
123+
ref={tagRef}
127124
slug={slug}
128125
size={size}
129126
renderIcon={renderIcon}
@@ -142,7 +139,8 @@ const SelectableTag = <T extends React.ElementType>({
142139
}
143140

144141
return (
145-
<Tag<any>
142+
<Tag
143+
ref={tagRef}
146144
slug={slug}
147145
size={size}
148146
renderIcon={renderIcon}

packages/react/src/components/Tag/Tag.tsx

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { useLayoutEffect, useState, ReactNode } from 'react';
9+
import React, {
10+
useLayoutEffect,
11+
useState,
12+
ReactNode,
13+
useRef,
14+
ForwardedRef,
15+
} from 'react';
1016
import classNames from 'classnames';
1117
import { Close } from '@carbon/icons-react';
1218
import setupGetInstanceId from '../../tools/setupGetInstanceId';
@@ -15,6 +21,8 @@ import { PolymorphicProps } from '../../types/common';
1521
import { Text } from '../Text';
1622
import deprecate from '../../prop-types/deprecate';
1723
import { DefinitionTooltip } from '../Tooltip';
24+
import { isEllipsisActive } from './isEllipsisActive';
25+
import { useMergeRefs } from '@floating-ui/react';
1826

1927
const getInstanceId = setupGetInstanceId();
2028
export const TYPES = {
@@ -55,7 +63,7 @@ export interface TagBaseProps {
5563
disabled?: boolean;
5664

5765
/**
58-
* @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.
66+
* @deprecated The `filter` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.
5967
*/
6068
filter?: boolean;
6169

@@ -65,7 +73,7 @@ export interface TagBaseProps {
6573
id?: string;
6674

6775
/**
68-
* @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.
76+
* @deprecated The `onClose` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.
6977
*/
7078
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
7179

@@ -87,7 +95,7 @@ export interface TagBaseProps {
8795
slug?: ReactNode;
8896

8997
/**
90-
* @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.
98+
* @deprecated The `title` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.
9199
*/
92100
title?: string;
93101

@@ -102,37 +110,36 @@ export type TagProps<T extends React.ElementType> = PolymorphicProps<
102110
TagBaseProps
103111
>;
104112

105-
const Tag = <T extends React.ElementType>({
106-
children,
107-
className,
108-
id,
109-
type,
110-
filter, // remove filter in next major release - V12
111-
renderIcon: CustomIconElement,
112-
title = 'Clear filter', // remove title in next major release - V12
113-
disabled,
114-
onClose, // remove onClose in next major release - V12
115-
size,
116-
as: BaseComponent,
117-
slug,
118-
...other
119-
}: TagProps<T>) => {
113+
const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
114+
{
115+
children,
116+
className,
117+
id,
118+
type,
119+
filter, // remove filter in next major release - V12
120+
renderIcon: CustomIconElement,
121+
title = 'Clear filter', // remove title in next major release - V12
122+
disabled,
123+
onClose, // remove onClose in next major release - V12
124+
size,
125+
as: BaseComponent,
126+
slug,
127+
...other
128+
}: TagProps<T>,
129+
forwardRef: ForwardedRef<HTMLElement | undefined>
130+
) {
120131
const prefix = usePrefix();
132+
const tagRef = useRef<HTMLElement>();
133+
const ref = useMergeRefs([forwardRef, tagRef]);
121134
const tagId = id || `tag-${getInstanceId()}`;
122135
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
123136

124-
const isEllipsisActive = (element: any) => {
125-
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
126-
return element.offsetWidth < element.scrollWidth;
127-
};
128-
129137
useLayoutEffect(() => {
130-
const elementTagId = document.querySelector(`#${tagId}`);
131-
const newElement = elementTagId?.getElementsByClassName(
138+
const newElement = tagRef.current?.getElementsByClassName(
132139
`${prefix}--tag__label`
133140
)[0];
134-
isEllipsisActive(newElement);
135-
}, [prefix, tagId]);
141+
setIsEllipsisApplied(isEllipsisActive(newElement));
142+
}, [prefix, tagRef]);
136143

137144
const conditions = [
138145
`${prefix}--tag--selectable`,
@@ -172,7 +179,7 @@ const Tag = <T extends React.ElementType>({
172179
}
173180

174181
if (filter) {
175-
const ComponentTag = BaseComponent ?? 'div';
182+
const ComponentTag = (BaseComponent as React.ElementType) ?? 'div';
176183
return (
177184
<ComponentTag className={tagClasses} id={tagId} {...other}>
178185
{CustomIconElement && size !== 'sm' ? (
@@ -215,6 +222,7 @@ const Tag = <T extends React.ElementType>({
215222

216223
return (
217224
<ComponentTag
225+
ref={ref}
218226
disabled={disabled}
219227
className={tagClasses}
220228
id={tagId}
@@ -254,7 +262,7 @@ const Tag = <T extends React.ElementType>({
254262
{normalizedSlug}
255263
</ComponentTag>
256264
);
257-
};
265+
});
258266

259267
Tag.propTypes = {
260268
/**
@@ -283,7 +291,7 @@ Tag.propTypes = {
283291
*/
284292
filter: deprecate(
285293
PropTypes.bool,
286-
'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.'
294+
'The `filter` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.'
287295
),
288296

289297
/**
@@ -296,7 +304,7 @@ Tag.propTypes = {
296304
*/
297305
onClose: deprecate(
298306
PropTypes.func,
299-
'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.'
307+
'The `onClose` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.'
300308
),
301309

302310
/**
@@ -321,7 +329,7 @@ Tag.propTypes = {
321329
*/
322330
title: deprecate(
323331
PropTypes.string,
324-
'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.'
332+
'The `title` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.'
325333
),
326334

327335
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright IBM Corp. 2024
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 isEllipsisActive = (element: any) => {
9+
if (element) {
10+
return element?.offsetWidth < element?.scrollWidth;
11+
}
12+
return false;
13+
};

packages/react/src/components/Tooltip/Tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function Tooltip<T extends React.ElementType>({
160160

161161
function onMouseEnter() {
162162
// Interactive Tags should not support onMouseEnter
163-
if (!rest?.onMouseEnter?.()) {
163+
if (!rest?.onMouseEnter) {
164164
setIsPointerIntersecting(true);
165165
setOpen(true, enterDelayMs);
166166
}

0 commit comments

Comments
 (0)