Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tag): use refs to handle component access #16571

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -8240,6 +8240,7 @@ Map {
},
},
"Tag" => Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"as": Object {
"type": "elementType",
Expand Down Expand Up @@ -8305,6 +8306,7 @@ Map {
"type": "oneOf",
},
},
"render": [Function],
},
"TagSkeleton" => Object {
"propTypes": Object {
Expand Down
19 changes: 8 additions & 11 deletions packages/react/src/components/Tag/DismissibleTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import PropTypes from 'prop-types';
import React, { useLayoutEffect, useState, ReactNode } from 'react';
import React, { useLayoutEffect, useState, ReactNode, useRef } from 'react';
import classNames from 'classnames';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { usePrefix } from '../../internal/usePrefix';
Expand All @@ -15,6 +15,7 @@ import Tag, { SIZES, TYPES } from './Tag';
import { Close } from '@carbon/icons-react';
import { Tooltip } from '../Tooltip';
import { Text } from '../Text';
import { isEllipsisActive } from './isEllipsisActive';

const getInstanceId = setupGetInstanceId();

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

const isEllipsisActive = (element: any) => {
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
return element.offsetWidth < element.scrollWidth;
};

useLayoutEffect(() => {
const elementTagId = document.querySelector(`#${tagId}`);
const newElement = elementTagId?.getElementsByClassName(
const newElement = tagLabelRef.current?.getElementsByClassName(
`${prefix}--tag__label`
)[0];
isEllipsisActive(newElement);
}, [prefix, tagId]);
setIsEllipsisApplied(isEllipsisActive(newElement));
}, [prefix, tagLabelRef]);
const handleClose = (event: React.MouseEvent<HTMLButtonElement>) => {
if (onClose) {
event.stopPropagation();
Expand Down Expand Up @@ -134,7 +130,8 @@ const DismissibleTag = <T extends React.ElementType>({
const dismissLabel = `Dismiss "${text}"`;

return (
<Tag<any>
<Tag
ref={tagLabelRef}
type={type}
size={size}
renderIcon={renderIcon}
Expand Down
23 changes: 11 additions & 12 deletions packages/react/src/components/Tag/OperationalTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React, {
useLayoutEffect,
useState,
ReactNode,
useRef,
} from 'react';
import classNames from 'classnames';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
Expand All @@ -19,6 +20,7 @@ import { PolymorphicProps } from '../../types/common';
import Tag, { SIZES } from './Tag';
import { Tooltip } from '../Tooltip';
import { Text } from '../Text';
import { isEllipsisActive } from './isEllipsisActive';

const getInstanceId = setupGetInstanceId();

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

const isEllipsisActive = (element: any) => {
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
return element.offsetWidth < element.scrollWidth;
};

useLayoutEffect(() => {
const elementTagId = document.querySelector(`#${tagId}`);
const newElement = elementTagId?.getElementsByClassName(
const newElement = tagRef.current?.getElementsByClassName(
`${prefix}--tag__label`
)[0];

isEllipsisActive(newElement);
}, [prefix, tagId]);
setIsEllipsisApplied(isEllipsisActive(newElement));
}, [prefix, tagRef]);

let normalizedSlug;
if (slug && slug['type']?.displayName === 'Slug') {
Expand All @@ -135,9 +132,10 @@ const OperationalTag = <T extends React.ElementType>({
align="bottom"
className={tooltipClasses}
leaveDelayMs={0}
onMouseEnter={false}
onMouseEnter={() => false}
closeOnActivation>
<Tag<any>
<Tag
ref={tagRef}
type={type}
size={size}
renderIcon={renderIcon}
Expand All @@ -155,7 +153,8 @@ const OperationalTag = <T extends React.ElementType>({
}

return (
<Tag<any>
<Tag
ref={tagRef}
type={type}
size={size}
renderIcon={renderIcon}
Expand Down
24 changes: 11 additions & 13 deletions packages/react/src/components/Tag/SelectableTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
*/

import PropTypes from 'prop-types';
import React, { useLayoutEffect, useState, ReactNode } from 'react';
import React, { useLayoutEffect, useState, ReactNode, useRef } from 'react';
import classNames from 'classnames';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { usePrefix } from '../../internal/usePrefix';
import { PolymorphicProps } from '../../types/common';
import Tag, { SIZES } from './Tag';
import { Tooltip } from '../Tooltip';
import { Text } from '../Text';
import { isEllipsisActive } from './isEllipsisActive';

const getInstanceId = setupGetInstanceId();

Expand Down Expand Up @@ -78,25 +79,20 @@ const SelectableTag = <T extends React.ElementType>({
...other
}: SelectableTagProps<T>) => {
const prefix = usePrefix();
const tagRef = useRef<HTMLElement>();
const tagId = id || `tag-${getInstanceId()}`;
const [selectedTag, setSelectedTag] = useState(selected);
const tagClasses = classNames(`${prefix}--tag--selectable`, className, {
[`${prefix}--tag--selectable-selected`]: selectedTag,
});
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);

const isEllipsisActive = (element: any) => {
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
return element.offsetWidth < element.scrollWidth;
};

useLayoutEffect(() => {
const elementTagId = document.querySelector(`#${tagId}`);
const newElement = elementTagId?.getElementsByClassName(
const newElement = tagRef.current?.getElementsByClassName(
`${prefix}--tag__label`
)[0];
isEllipsisActive(newElement);
}, [prefix, tagId]);
setIsEllipsisApplied(isEllipsisActive(newElement));
}, [prefix, tagRef]);

let normalizedSlug;
if (slug && slug['type']?.displayName === 'Slug') {
Expand All @@ -122,8 +118,9 @@ const SelectableTag = <T extends React.ElementType>({
align="bottom"
className={tooltipClasses}
leaveDelayMs={0}
onMouseEnter={false}>
<Tag<any>
onMouseEnter={() => false}>
<Tag
ref={tagRef}
slug={slug}
size={size}
renderIcon={renderIcon}
Expand All @@ -142,7 +139,8 @@ const SelectableTag = <T extends React.ElementType>({
}

return (
<Tag<any>
<Tag
ref={tagRef}
slug={slug}
size={size}
renderIcon={renderIcon}
Expand Down
74 changes: 41 additions & 33 deletions packages/react/src/components/Tag/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
*/

import PropTypes from 'prop-types';
import React, { useLayoutEffect, useState, ReactNode } from 'react';
import React, {
useLayoutEffect,
useState,
ReactNode,
useRef,
ForwardedRef,
} from 'react';
import classNames from 'classnames';
import { Close } from '@carbon/icons-react';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
Expand All @@ -15,6 +21,8 @@ import { PolymorphicProps } from '../../types/common';
import { Text } from '../Text';
import deprecate from '../../prop-types/deprecate';
import { DefinitionTooltip } from '../Tooltip';
import { isEllipsisActive } from './isEllipsisActive';
import { useMergeRefs } from '@floating-ui/react';

const getInstanceId = setupGetInstanceId();
export const TYPES = {
Expand Down Expand Up @@ -55,7 +63,7 @@ export interface TagBaseProps {
disabled?: boolean;

/**
* @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.
* @deprecated The `filter` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.
*/
filter?: boolean;

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

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

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

/**
* @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.
* @deprecated The `title` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.
*/
title?: string;

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

const Tag = <T extends React.ElementType>({
children,
className,
id,
type,
filter, // remove filter in next major release - V12
renderIcon: CustomIconElement,
title = 'Clear filter', // remove title in next major release - V12
disabled,
onClose, // remove onClose in next major release - V12
size,
as: BaseComponent,
slug,
...other
}: TagProps<T>) => {
const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
{
children,
className,
id,
type,
filter, // remove filter in next major release - V12
renderIcon: CustomIconElement,
title = 'Clear filter', // remove title in next major release - V12
disabled,
onClose, // remove onClose in next major release - V12
size,
as: BaseComponent,
slug,
...other
}: TagProps<T>,
forwardRef: ForwardedRef<HTMLElement | undefined>
) {
const prefix = usePrefix();
const tagRef = useRef<HTMLElement>();
const ref = useMergeRefs([forwardRef, tagRef]);
const tagId = id || `tag-${getInstanceId()}`;
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);

const isEllipsisActive = (element: any) => {
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
return element.offsetWidth < element.scrollWidth;
};

useLayoutEffect(() => {
const elementTagId = document.querySelector(`#${tagId}`);
const newElement = elementTagId?.getElementsByClassName(
const newElement = tagRef.current?.getElementsByClassName(
`${prefix}--tag__label`
)[0];
isEllipsisActive(newElement);
}, [prefix, tagId]);
setIsEllipsisApplied(isEllipsisActive(newElement));
}, [prefix, tagRef]);

const conditions = [
`${prefix}--tag--selectable`,
Expand Down Expand Up @@ -172,7 +179,7 @@ const Tag = <T extends React.ElementType>({
}

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

return (
<ComponentTag
ref={ref}
disabled={disabled}
className={tagClasses}
id={tagId}
Expand Down Expand Up @@ -254,7 +262,7 @@ const Tag = <T extends React.ElementType>({
{normalizedSlug}
</ComponentTag>
);
};
});

Tag.propTypes = {
/**
Expand Down Expand Up @@ -283,7 +291,7 @@ Tag.propTypes = {
*/
filter: deprecate(
PropTypes.bool,
'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.'
'The `filter` prop has been deprecated and will be removed in the next major version. Use DismissibleTag instead.'
),

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

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

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/components/Tag/isEllipsisActive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright IBM Corp. 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

export const isEllipsisActive = (element: any) => {
if (element) {
return element?.offsetWidth < element?.scrollWidth;
}
return false;
};
2 changes: 1 addition & 1 deletion packages/react/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function Tooltip<T extends React.ElementType>({

function onMouseEnter() {
// Interactive Tags should not support onMouseEnter
if (!rest?.onMouseEnter?.()) {
if (!rest?.onMouseEnter) {
setIsPointerIntersecting(true);
setOpen(true, enterDelayMs);
}
Expand Down
Loading