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

feat: Added strong PolymorphicProps #18384

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 4 additions & 2 deletions packages/react/src/components/Tag/OperationalTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import classNames from 'classnames';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';
import { PolymorphicProps } from '../../types/common';
import Tag, { SIZES } from './Tag';
import Tag, { SIZES, TagBaseProps } from './Tag';
import { Tooltip } from '../Tooltip';
import { Text } from '../Text';
import { isEllipsisActive } from './isEllipsisActive';
Expand Down 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 Expand Up @@ -119,6 +119,7 @@ const OperationalTag = <T extends React.ElementType>({
onMouseEnter={() => false}
closeOnActivation>
<Tag
as="button"
ref={tagRef}
type={type}
size={size}
Expand All @@ -137,6 +138,7 @@ const OperationalTag = <T extends React.ElementType>({

return (
<Tag
as="button"
ref={tagRef}
type={type}
size={size}
Expand Down
Loading
Loading