Skip to content

Commit

Permalink
fix(tag): use refs to handle component access (carbon-design-system#1…
Browse files Browse the repository at this point in the history
…6571)

      * 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]>
  • Loading branch information
3 people committed May 24, 2024
1 parent e12dd6f commit df8c92a
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 70 deletions.
2 changes: 2 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
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

0 comments on commit df8c92a

Please sign in to comment.