Skip to content

Commit 9b9fa47

Browse files
authored
feat: Added strong PolymorphicProps (#18384)
* 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
1 parent 682c6ab commit 9b9fa47

File tree

8 files changed

+340
-317
lines changed

8 files changed

+340
-317
lines changed

packages/react/src/components/Button/Button.tsx

Lines changed: 84 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { composeEventHandlers } from '../../tools/events';
1212
import { PolymorphicProps } from '../../types/common';
1313
import { PopoverAlignment } from '../Popover';
1414
import ButtonBase from './ButtonBase';
15+
import {
16+
PolymorphicComponentPropWithRef,
17+
PolymorphicRef,
18+
} from '../../internal/PolymorphicProps';
1519

1620
export const ButtonKinds = [
1721
'primary',
@@ -102,15 +106,13 @@ export interface ButtonBaseProps
102106
tooltipPosition?: ButtonTooltipPosition;
103107
}
104108

105-
export type ButtonProps<T extends React.ElementType> = PolymorphicProps<
106-
T,
107-
ButtonBaseProps
108-
>;
109+
export type ButtonProps<T extends React.ElementType> =
110+
PolymorphicComponentPropWithRef<T, ButtonBaseProps>;
109111

110-
export type ButtonComponent = <T extends React.ElementType>(
112+
export type ButtonComponent = <T extends React.ElementType = 'button'>(
111113
props: ButtonProps<T>,
112114
context?: any
113-
) => React.ReactElement<any, any> | null;
115+
) => React.ReactElement | any;
114116

115117
function isIconOnlyButton(
116118
hasIconOnly: ButtonBaseProps['hasIconOnly'],
@@ -123,87 +125,89 @@ function isIconOnlyButton(
123125
return false;
124126
}
125127

126-
const Button = React.forwardRef(function Button<T extends React.ElementType>(
127-
props: ButtonProps<T>,
128-
ref: React.Ref<unknown>
129-
) {
130-
const tooltipRef = useRef(null);
131-
const {
132-
as,
133-
autoAlign = false,
134-
children,
135-
hasIconOnly = false,
136-
iconDescription,
137-
kind = 'primary',
138-
onBlur,
139-
onClick,
140-
onFocus,
141-
onMouseEnter,
142-
onMouseLeave,
143-
renderIcon: ButtonImageElement,
144-
size,
145-
tooltipAlignment = 'center',
146-
tooltipPosition = 'top',
147-
...rest
148-
} = props;
149-
150-
const handleClick = (evt: React.MouseEvent) => {
151-
// Prevent clicks on the tooltip from triggering the button click event
152-
if (evt.target === tooltipRef.current) {
153-
evt.preventDefault();
154-
}
155-
};
156-
157-
const iconOnlyImage = !ButtonImageElement ? null : <ButtonImageElement />;
158-
159-
if (!isIconOnlyButton(hasIconOnly, kind)) {
160-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
161-
const { tooltipAlignment, ...propsWithoutTooltipAlignment } = props;
162-
return <ButtonBase ref={ref} {...propsWithoutTooltipAlignment} />;
163-
} else {
164-
let align: PopoverAlignment | undefined = undefined;
165-
166-
if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
167-
if (tooltipAlignment === 'center') {
168-
align = tooltipPosition;
128+
const Button: ButtonComponent = React.forwardRef(
129+
<T extends React.ElementType = 'button'>(
130+
props: ButtonProps<T>,
131+
ref: React.Ref<unknown>
132+
) => {
133+
const tooltipRef = useRef(null);
134+
const {
135+
as,
136+
autoAlign = false,
137+
children,
138+
hasIconOnly = false,
139+
iconDescription,
140+
kind = 'primary',
141+
onBlur,
142+
onClick,
143+
onFocus,
144+
onMouseEnter,
145+
onMouseLeave,
146+
renderIcon: ButtonImageElement,
147+
size,
148+
tooltipAlignment = 'center',
149+
tooltipPosition = 'top',
150+
...rest
151+
} = props;
152+
153+
const handleClick = (evt: React.MouseEvent) => {
154+
// Prevent clicks on the tooltip from triggering the button click event
155+
if (evt.target === tooltipRef.current) {
156+
evt.preventDefault();
169157
}
170-
if (tooltipAlignment === 'end') {
171-
align = `${tooltipPosition}-end`;
158+
};
159+
160+
const iconOnlyImage = !ButtonImageElement ? null : <ButtonImageElement />;
161+
162+
if (!isIconOnlyButton(hasIconOnly, kind)) {
163+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
164+
const { tooltipAlignment, ...propsWithoutTooltipAlignment } = props;
165+
return <ButtonBase ref={ref} {...propsWithoutTooltipAlignment} />;
166+
} else {
167+
let align: PopoverAlignment | undefined = undefined;
168+
169+
if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
170+
if (tooltipAlignment === 'center') {
171+
align = tooltipPosition;
172+
}
173+
if (tooltipAlignment === 'end') {
174+
align = `${tooltipPosition}-end`;
175+
}
176+
if (tooltipAlignment === 'start') {
177+
align = `${tooltipPosition}-start`;
178+
}
172179
}
173-
if (tooltipAlignment === 'start') {
174-
align = `${tooltipPosition}-start`;
180+
181+
if (tooltipPosition === 'right' || tooltipPosition === 'left') {
182+
align = tooltipPosition;
175183
}
176-
}
177184

178-
if (tooltipPosition === 'right' || tooltipPosition === 'left') {
179-
align = tooltipPosition;
185+
return (
186+
<IconButton
187+
{...rest}
188+
ref={ref}
189+
as={as}
190+
align={align}
191+
label={iconDescription}
192+
kind={kind}
193+
size={size}
194+
onMouseEnter={onMouseEnter}
195+
onMouseLeave={onMouseLeave}
196+
onFocus={onFocus}
197+
onBlur={onBlur}
198+
autoAlign={autoAlign}
199+
onClick={composeEventHandlers([onClick, handleClick])}
200+
renderIcon={iconOnlyImage ? null : ButtonImageElement} // avoid doubling the icon.
201+
>
202+
{iconOnlyImage ?? children}
203+
</IconButton>
204+
);
180205
}
181-
182-
return (
183-
<IconButton
184-
{...rest}
185-
ref={ref}
186-
as={as}
187-
align={align}
188-
label={iconDescription}
189-
kind={kind}
190-
size={size}
191-
onMouseEnter={onMouseEnter}
192-
onMouseLeave={onMouseLeave}
193-
onFocus={onFocus}
194-
onBlur={onBlur}
195-
autoAlign={autoAlign}
196-
onClick={composeEventHandlers([onClick, handleClick])}
197-
renderIcon={iconOnlyImage ? null : ButtonImageElement} // avoid doubling the icon.
198-
>
199-
{iconOnlyImage ?? children}
200-
</IconButton>
201-
);
202206
}
203-
});
207+
);
204208

205-
Button.displayName = 'Button';
206-
Button.propTypes = {
209+
(Button as React.FC).displayName = 'Button';
210+
(Button as React.FC).propTypes = {
207211
/**
208212
* Specify how the button itself should be rendered.
209213
* Make sure to apply all props to the root node and render children appropriately

packages/react/src/components/Link/Link.tsx

Lines changed: 66 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import React, {
1616
} from 'react';
1717
import { usePrefix } from '../../internal/usePrefix';
1818
import { PolymorphicProps } from '../../types/common';
19+
import {
20+
PolymorphicComponentPropWithRef,
21+
PolymorphicRef,
22+
} from '../../internal/PolymorphicProps';
1923

2024
export interface LinkBaseProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
2125
/**
@@ -68,67 +72,70 @@ export interface LinkBaseProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
6872
visited?: boolean;
6973
}
7074

71-
export type LinkProps<E extends ElementType> = PolymorphicProps<
72-
E,
73-
LinkBaseProps
74-
>;
75-
76-
const Link = React.forwardRef(function Link<E extends React.ElementType>(
77-
{
78-
as: BaseComponent,
79-
children,
80-
className: customClassName,
81-
href,
82-
disabled = false,
83-
inline = false,
84-
visited = false,
85-
renderIcon: Icon,
86-
size,
87-
target,
88-
...rest
89-
}: LinkProps<E>,
90-
ref
91-
) {
92-
const prefix = usePrefix();
93-
const className = cx(`${prefix}--link`, customClassName, {
94-
[`${prefix}--link--disabled`]: disabled,
95-
[`${prefix}--link--inline`]: inline,
96-
[`${prefix}--link--visited`]: visited,
97-
[`${prefix}--link--${size}`]: size,
98-
});
99-
const rel = target === '_blank' ? 'noopener' : undefined;
100-
const linkProps: AnchorHTMLAttributes<HTMLAnchorElement> = {
101-
className: BaseComponent ? undefined : className,
102-
rel,
103-
target,
104-
};
105-
106-
// Reference for disabled links:
107-
// https://www.scottohara.me/blog/2021/05/28/disabled-links.html
108-
if (!disabled) {
109-
linkProps.href = href;
110-
} else {
111-
linkProps.role = 'link';
112-
linkProps['aria-disabled'] = true;
75+
export type LinkProps<T extends React.ElementType> =
76+
PolymorphicComponentPropWithRef<T, LinkBaseProps>;
77+
78+
type LinkComponent = <T extends React.ElementType = 'a'>(
79+
props: LinkProps<T>
80+
) => React.ReactElement | any;
81+
82+
const Link: LinkComponent = React.forwardRef(
83+
<T extends React.ElementType = 'a'>(
84+
{
85+
as: BaseComponent,
86+
children,
87+
className: customClassName,
88+
href,
89+
disabled = false,
90+
inline = false,
91+
visited = false,
92+
renderIcon: Icon,
93+
size,
94+
target,
95+
...rest
96+
}: LinkProps<T>,
97+
ref: PolymorphicRef<T>
98+
) => {
99+
const prefix = usePrefix();
100+
const className = cx(`${prefix}--link`, customClassName, {
101+
[`${prefix}--link--disabled`]: disabled,
102+
[`${prefix}--link--inline`]: inline,
103+
[`${prefix}--link--visited`]: visited,
104+
[`${prefix}--link--${size}`]: size,
105+
});
106+
const rel = target === '_blank' ? 'noopener' : undefined;
107+
const linkProps: AnchorHTMLAttributes<HTMLAnchorElement> = {
108+
className: BaseComponent ? undefined : className,
109+
rel,
110+
target,
111+
};
112+
113+
// Reference for disabled links:
114+
// https://www.scottohara.me/blog/2021/05/28/disabled-links.html
115+
if (!disabled) {
116+
linkProps.href = href;
117+
} else {
118+
linkProps.role = 'link';
119+
linkProps['aria-disabled'] = true;
120+
}
121+
122+
const BaseComponentAsAny = (BaseComponent ?? 'a') as any;
123+
124+
return (
125+
<BaseComponentAsAny ref={ref} {...linkProps} {...rest}>
126+
{children}
127+
{!inline && Icon && (
128+
<div className={`${prefix}--link__icon`}>
129+
<Icon />
130+
</div>
131+
)}
132+
</BaseComponentAsAny>
133+
);
113134
}
135+
);
114136

115-
const BaseComponentAsAny = (BaseComponent ?? 'a') as any;
116-
117-
return (
118-
<BaseComponentAsAny ref={ref} {...linkProps} {...rest}>
119-
{children}
120-
{!inline && Icon && (
121-
<div className={`${prefix}--link__icon`}>
122-
<Icon />
123-
</div>
124-
)}
125-
</BaseComponentAsAny>
126-
);
127-
});
128-
129-
Link.displayName = 'Link';
130-
131-
Link.propTypes = {
137+
(Link as React.FC).displayName = 'Link';
138+
(Link as React.FC).propTypes = {
132139
/**
133140
* Provide a custom element or component to render the top-level node for the
134141
* component.

packages/react/src/components/Modal/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ const Modal = React.forwardRef(function Modal(
264264
) {
265265
const prefix = usePrefix();
266266
const button = useRef<HTMLButtonElement>(null);
267-
const secondaryButton = useRef();
267+
const secondaryButton = useRef<HTMLButtonElement>(null);
268268
const contentRef = useRef<HTMLDivElement>(null);
269269
const innerModal = useRef<HTMLDivElement>(null);
270270
const startTrap = useRef<HTMLSpanElement>(null);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const DismissibleTag = <T extends React.ElementType>({
104104
...other
105105
}: DismissibleTagProps<T>) => {
106106
const prefix = usePrefix();
107-
const tagLabelRef = useRef<HTMLElement>();
107+
const tagLabelRef = useRef<HTMLDivElement>(null);
108108
const tagId = id || `tag-${useId()}`;
109109
const tagClasses = classNames(`${prefix}--tag--filter`, className);
110110
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const OperationalTag = <T extends React.ElementType>({
9191
...other
9292
}: OperationalTagProps<T>) => {
9393
const prefix = usePrefix();
94-
const tagRef = useRef<HTMLElement>();
94+
const tagRef = useRef<HTMLButtonElement>(null);
9595
const tagId = id || `tag-${useId()}`;
9696
const tagClasses = classNames(`${prefix}--tag--operational`, className);
9797
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);

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

Lines changed: 4 additions & 4 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, useRef } from 'react';
9+
import React, { useLayoutEffect, useState, useRef, MouseEvent } from 'react';
1010
import classNames from 'classnames';
1111
import { useId } from '../../internal/useId';
1212
import { usePrefix } from '../../internal/usePrefix';
@@ -46,7 +46,7 @@ export interface SelectableTagBaseProps {
4646
/**
4747
* Provide an optional function to be called when the tag is clicked.
4848
*/
49-
onClick?: (e: Event) => void;
49+
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
5050

5151
/**
5252
* Specify the state of the selectable tag.
@@ -83,7 +83,7 @@ const SelectableTag = <T extends React.ElementType>({
8383
...other
8484
}: SelectableTagProps<T>) => {
8585
const prefix = usePrefix();
86-
const tagRef = useRef<HTMLElement>();
86+
const tagRef = useRef<HTMLButtonElement>(null);
8787
const tagId = id || `tag-${useId()}`;
8888
const [selectedTag, setSelectedTag] = useState(selected);
8989
const tagClasses = classNames(`${prefix}--tag--selectable`, className, {
@@ -103,7 +103,7 @@ const SelectableTag = <T extends React.ElementType>({
103103
`${prefix}--tag-label-tooltip`
104104
);
105105

106-
const handleClick = (e: Event) => {
106+
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
107107
setSelectedTag(!selectedTag);
108108
onChange?.(!selectedTag);
109109
onClick?.(e);

0 commit comments

Comments
 (0)