Skip to content

Commit

Permalink
feat: Added strong PolymorphicProps (#18384)
Browse files Browse the repository at this point in the history
* feat: added strong polymorphicProps

* fix: fixed import in button

* fix: fixed ref typescript error

* fix: fixed tags

* fix: fixed tag typescript

* fix: fixed ref in button

* fix: removed console.log

* fix: removed unsed props
  • Loading branch information
guidari authored Jan 27, 2025
1 parent 682c6ab commit 9b9fa47
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 317 deletions.
164 changes: 84 additions & 80 deletions packages/react/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { composeEventHandlers } from '../../tools/events';
import { PolymorphicProps } from '../../types/common';
import { PopoverAlignment } from '../Popover';
import ButtonBase from './ButtonBase';
import {
PolymorphicComponentPropWithRef,
PolymorphicRef,
} from '../../internal/PolymorphicProps';

export const ButtonKinds = [
'primary',
Expand Down Expand Up @@ -102,15 +106,13 @@ export interface ButtonBaseProps
tooltipPosition?: ButtonTooltipPosition;
}

export type ButtonProps<T extends React.ElementType> = PolymorphicProps<
T,
ButtonBaseProps
>;
export type ButtonProps<T extends React.ElementType> =
PolymorphicComponentPropWithRef<T, ButtonBaseProps>;

export type ButtonComponent = <T extends React.ElementType>(
export type ButtonComponent = <T extends React.ElementType = 'button'>(
props: ButtonProps<T>,
context?: any
) => React.ReactElement<any, any> | null;
) => React.ReactElement | any;

function isIconOnlyButton(
hasIconOnly: ButtonBaseProps['hasIconOnly'],
Expand All @@ -123,87 +125,89 @@ function isIconOnlyButton(
return false;
}

const Button = React.forwardRef(function Button<T extends React.ElementType>(
props: ButtonProps<T>,
ref: React.Ref<unknown>
) {
const tooltipRef = useRef(null);
const {
as,
autoAlign = false,
children,
hasIconOnly = false,
iconDescription,
kind = 'primary',
onBlur,
onClick,
onFocus,
onMouseEnter,
onMouseLeave,
renderIcon: ButtonImageElement,
size,
tooltipAlignment = 'center',
tooltipPosition = 'top',
...rest
} = props;

const handleClick = (evt: React.MouseEvent) => {
// Prevent clicks on the tooltip from triggering the button click event
if (evt.target === tooltipRef.current) {
evt.preventDefault();
}
};

const iconOnlyImage = !ButtonImageElement ? null : <ButtonImageElement />;

if (!isIconOnlyButton(hasIconOnly, kind)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { tooltipAlignment, ...propsWithoutTooltipAlignment } = props;
return <ButtonBase ref={ref} {...propsWithoutTooltipAlignment} />;
} else {
let align: PopoverAlignment | undefined = undefined;

if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
if (tooltipAlignment === 'center') {
align = tooltipPosition;
const Button: ButtonComponent = React.forwardRef(
<T extends React.ElementType = 'button'>(
props: ButtonProps<T>,
ref: React.Ref<unknown>
) => {
const tooltipRef = useRef(null);
const {
as,
autoAlign = false,
children,
hasIconOnly = false,
iconDescription,
kind = 'primary',
onBlur,
onClick,
onFocus,
onMouseEnter,
onMouseLeave,
renderIcon: ButtonImageElement,
size,
tooltipAlignment = 'center',
tooltipPosition = 'top',
...rest
} = props;

const handleClick = (evt: React.MouseEvent) => {
// Prevent clicks on the tooltip from triggering the button click event
if (evt.target === tooltipRef.current) {
evt.preventDefault();
}
if (tooltipAlignment === 'end') {
align = `${tooltipPosition}-end`;
};

const iconOnlyImage = !ButtonImageElement ? null : <ButtonImageElement />;

if (!isIconOnlyButton(hasIconOnly, kind)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { tooltipAlignment, ...propsWithoutTooltipAlignment } = props;
return <ButtonBase ref={ref} {...propsWithoutTooltipAlignment} />;
} else {
let align: PopoverAlignment | undefined = undefined;

if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
if (tooltipAlignment === 'center') {
align = tooltipPosition;
}
if (tooltipAlignment === 'end') {
align = `${tooltipPosition}-end`;
}
if (tooltipAlignment === 'start') {
align = `${tooltipPosition}-start`;
}
}
if (tooltipAlignment === 'start') {
align = `${tooltipPosition}-start`;

if (tooltipPosition === 'right' || tooltipPosition === 'left') {
align = tooltipPosition;
}
}

if (tooltipPosition === 'right' || tooltipPosition === 'left') {
align = tooltipPosition;
return (
<IconButton
{...rest}
ref={ref}
as={as}
align={align}
label={iconDescription}
kind={kind}
size={size}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
autoAlign={autoAlign}
onClick={composeEventHandlers([onClick, handleClick])}
renderIcon={iconOnlyImage ? null : ButtonImageElement} // avoid doubling the icon.
>
{iconOnlyImage ?? children}
</IconButton>
);
}

return (
<IconButton
{...rest}
ref={ref}
as={as}
align={align}
label={iconDescription}
kind={kind}
size={size}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
autoAlign={autoAlign}
onClick={composeEventHandlers([onClick, handleClick])}
renderIcon={iconOnlyImage ? null : ButtonImageElement} // avoid doubling the icon.
>
{iconOnlyImage ?? children}
</IconButton>
);
}
});
);

Button.displayName = 'Button';
Button.propTypes = {
(Button as React.FC).displayName = 'Button';
(Button as React.FC).propTypes = {
/**
* Specify how the button itself should be rendered.
* Make sure to apply all props to the root node and render children appropriately
Expand Down
125 changes: 66 additions & 59 deletions packages/react/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import React, {
} from 'react';
import { usePrefix } from '../../internal/usePrefix';
import { PolymorphicProps } from '../../types/common';
import {
PolymorphicComponentPropWithRef,
PolymorphicRef,
} from '../../internal/PolymorphicProps';

export interface LinkBaseProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/**
Expand Down Expand Up @@ -68,67 +72,70 @@ export interface LinkBaseProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
visited?: boolean;
}

export type LinkProps<E extends ElementType> = PolymorphicProps<
E,
LinkBaseProps
>;

const Link = React.forwardRef(function Link<E extends React.ElementType>(
{
as: BaseComponent,
children,
className: customClassName,
href,
disabled = false,
inline = false,
visited = false,
renderIcon: Icon,
size,
target,
...rest
}: LinkProps<E>,
ref
) {
const prefix = usePrefix();
const className = cx(`${prefix}--link`, customClassName, {
[`${prefix}--link--disabled`]: disabled,
[`${prefix}--link--inline`]: inline,
[`${prefix}--link--visited`]: visited,
[`${prefix}--link--${size}`]: size,
});
const rel = target === '_blank' ? 'noopener' : undefined;
const linkProps: AnchorHTMLAttributes<HTMLAnchorElement> = {
className: BaseComponent ? undefined : className,
rel,
target,
};

// Reference for disabled links:
// https://www.scottohara.me/blog/2021/05/28/disabled-links.html
if (!disabled) {
linkProps.href = href;
} else {
linkProps.role = 'link';
linkProps['aria-disabled'] = true;
export type LinkProps<T extends React.ElementType> =
PolymorphicComponentPropWithRef<T, LinkBaseProps>;

type LinkComponent = <T extends React.ElementType = 'a'>(
props: LinkProps<T>
) => React.ReactElement | any;

const Link: LinkComponent = React.forwardRef(
<T extends React.ElementType = 'a'>(
{
as: BaseComponent,
children,
className: customClassName,
href,
disabled = false,
inline = false,
visited = false,
renderIcon: Icon,
size,
target,
...rest
}: LinkProps<T>,
ref: PolymorphicRef<T>
) => {
const prefix = usePrefix();
const className = cx(`${prefix}--link`, customClassName, {
[`${prefix}--link--disabled`]: disabled,
[`${prefix}--link--inline`]: inline,
[`${prefix}--link--visited`]: visited,
[`${prefix}--link--${size}`]: size,
});
const rel = target === '_blank' ? 'noopener' : undefined;
const linkProps: AnchorHTMLAttributes<HTMLAnchorElement> = {
className: BaseComponent ? undefined : className,
rel,
target,
};

// Reference for disabled links:
// https://www.scottohara.me/blog/2021/05/28/disabled-links.html
if (!disabled) {
linkProps.href = href;
} else {
linkProps.role = 'link';
linkProps['aria-disabled'] = true;
}

const BaseComponentAsAny = (BaseComponent ?? 'a') as any;

return (
<BaseComponentAsAny ref={ref} {...linkProps} {...rest}>
{children}
{!inline && Icon && (
<div className={`${prefix}--link__icon`}>
<Icon />
</div>
)}
</BaseComponentAsAny>
);
}
);

const BaseComponentAsAny = (BaseComponent ?? 'a') as any;

return (
<BaseComponentAsAny ref={ref} {...linkProps} {...rest}>
{children}
{!inline && Icon && (
<div className={`${prefix}--link__icon`}>
<Icon />
</div>
)}
</BaseComponentAsAny>
);
});

Link.displayName = 'Link';

Link.propTypes = {
(Link as React.FC).displayName = 'Link';
(Link as React.FC).propTypes = {
/**
* Provide a custom element or component to render the top-level node for the
* component.
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const Modal = React.forwardRef(function Modal(
) {
const prefix = usePrefix();
const button = useRef<HTMLButtonElement>(null);
const secondaryButton = useRef();
const secondaryButton = useRef<HTMLButtonElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const innerModal = useRef<HTMLDivElement>(null);
const startTrap = useRef<HTMLSpanElement>(null);
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/Tag/DismissibleTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const DismissibleTag = <T extends React.ElementType>({
...other
}: DismissibleTagProps<T>) => {
const prefix = usePrefix();
const tagLabelRef = useRef<HTMLElement>();
const tagLabelRef = useRef<HTMLDivElement>(null);
const tagId = id || `tag-${useId()}`;
const tagClasses = classNames(`${prefix}--tag--filter`, className);
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/Tag/OperationalTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const OperationalTag = <T extends React.ElementType>({
...other
}: OperationalTagProps<T>) => {
const prefix = usePrefix();
const tagRef = useRef<HTMLElement>();
const tagRef = useRef<HTMLButtonElement>(null);
const tagId = id || `tag-${useId()}`;
const tagClasses = classNames(`${prefix}--tag--operational`, className);
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/components/Tag/SelectableTag.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, useRef } from 'react';
import React, { useLayoutEffect, useState, useRef, MouseEvent } from 'react';
import classNames from 'classnames';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';
Expand Down Expand Up @@ -46,7 +46,7 @@ export interface SelectableTagBaseProps {
/**
* Provide an optional function to be called when the tag is clicked.
*/
onClick?: (e: Event) => void;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;

/**
* Specify the state of the selectable tag.
Expand Down Expand Up @@ -83,7 +83,7 @@ const SelectableTag = <T extends React.ElementType>({
...other
}: SelectableTagProps<T>) => {
const prefix = usePrefix();
const tagRef = useRef<HTMLElement>();
const tagRef = useRef<HTMLButtonElement>(null);
const tagId = id || `tag-${useId()}`;
const [selectedTag, setSelectedTag] = useState(selected);
const tagClasses = classNames(`${prefix}--tag--selectable`, className, {
Expand All @@ -103,7 +103,7 @@ const SelectableTag = <T extends React.ElementType>({
`${prefix}--tag-label-tooltip`
);

const handleClick = (e: Event) => {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
setSelectedTag(!selectedTag);
onChange?.(!selectedTag);
onClick?.(e);
Expand Down
Loading

0 comments on commit 9b9fa47

Please sign in to comment.