Skip to content

Commit 04644a9

Browse files
authored
refactor(Button): convert Button component family to TypeScript (#13811)
* refactor(Button): convert Button component family to TypeScript * fix(ButtonSkeleton): restore small prop * fix(ComposedModal): remove unnecessary expect error annotations
1 parent 47fc5c8 commit 04644a9

11 files changed

Lines changed: 204 additions & 37 deletions

File tree

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,16 @@ Map {
397397
},
398398
"render": [Function],
399399
},
400+
"ButtonKinds" => Object {
401+
"0": "primary",
402+
"1": "secondary",
403+
"2": "danger",
404+
"3": "ghost",
405+
"4": "danger--primary",
406+
"5": "danger--ghost",
407+
"6": "danger--tertiary",
408+
"7": "tertiary",
409+
},
400410
"ButtonSet" => Object {
401411
"$$typeof": Symbol(react.forward_ref),
402412
"propTypes": Object {
@@ -412,6 +422,13 @@ Map {
412422
},
413423
"render": [Function],
414424
},
425+
"ButtonSizes" => Object {
426+
"0": "sm",
427+
"1": "md",
428+
"2": "lg",
429+
"3": "xl",
430+
"4": "2xl",
431+
},
415432
"ButtonSkeleton" => Object {
416433
"propTypes": Object {
417434
"className": Object {
@@ -437,6 +454,17 @@ Map {
437454
},
438455
},
439456
},
457+
"ButtonTooltipAlignments" => Object {
458+
"0": "start",
459+
"1": "center",
460+
"2": "end",
461+
},
462+
"ButtonTooltipPositions" => Object {
463+
"0": "top",
464+
"1": "right",
465+
"2": "bottom",
466+
"3": "left",
467+
},
440468
"Checkbox" => Object {
441469
"$$typeof": Symbol(react.forward_ref),
442470
"defaultProps": Object {

packages/react/src/__tests__/index-test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ describe('Carbon Components React', () => {
2424
"BreadcrumbItem",
2525
"BreadcrumbSkeleton",
2626
"Button",
27+
"ButtonKinds",
2728
"ButtonSet",
29+
"ButtonSizes",
2830
"ButtonSkeleton",
31+
"ButtonTooltipAlignments",
32+
"ButtonTooltipPositions",
2933
"Checkbox",
3034
"CheckboxGroup",
3135
"CheckboxSkeleton",

packages/react/src/components/Button/Button.Skeleton.js renamed to packages/react/src/components/Button/Button.Skeleton.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,42 @@ import PropTypes from 'prop-types';
99
import React from 'react';
1010
import cx from 'classnames';
1111
import { usePrefix } from '../../internal/usePrefix';
12+
import { ButtonSize } from './Button';
1213

13-
const ButtonSkeleton = ({
14+
export interface ButtonSkeletonProps extends React.HTMLAttributes<HTMLElement> {
15+
/**
16+
* Optionally specify an href for your Button to become an `<a>` element
17+
*/
18+
href?: string;
19+
20+
/**
21+
* Specify the size of the button, from a list of available sizes.
22+
*/
23+
size?: ButtonSize;
24+
25+
/**
26+
* @deprecated This property will be removed in the next major Carbon version,
27+
* use size={sm} instead.
28+
*
29+
* Specify whether the Button should be a small variant
30+
*/
31+
small?: boolean;
32+
}
33+
34+
const ButtonSkeleton: React.FC<ButtonSkeletonProps> = ({
1435
className,
1536
small = false,
1637
href,
1738
size = 'lg',
1839
...rest
19-
}) => {
40+
}: ButtonSkeletonProps) => {
2041
const prefix = usePrefix();
2142

2243
const buttonClasses = cx(className, {
2344
[`${prefix}--skeleton`]: true,
2445
[`${prefix}--btn`]: true,
2546
[`${prefix}--btn--sm`]: small || size === 'sm',
26-
[`${prefix}--btn--md`]: size === 'field' || size === 'md',
47+
[`${prefix}--btn--md`]: size === 'md',
2748
[`${prefix}--btn--lg`]: size === 'lg',
2849
[`${prefix}--btn--xl`]: size === 'xl',
2950
[`${prefix}--btn--2xl`]: size === '2xl',
@@ -60,6 +81,9 @@ ButtonSkeleton.propTypes = {
6081
size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl', '2xl']),
6182

6283
/**
84+
* @deprecated This property will be removed in the next major Carbon version,
85+
* use size={sm} instead.
86+
*
6387
* Specify whether the Button should be a small variant
6488
*/
6589
small: PropTypes.bool,

packages/react/src/components/Button/Button.js renamed to packages/react/src/components/Button/Button.tsx

Lines changed: 114 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,113 @@
88
import PropTypes from 'prop-types';
99
import React, { useRef } from 'react';
1010
import classNames from 'classnames';
11-
import { ButtonKinds } from '../../prop-types/types';
1211
import { IconButton } from '../IconButton';
1312
import { composeEventHandlers } from '../../tools/events';
1413
import { usePrefix } from '../../internal/usePrefix';
1514
import { useId } from '../../internal/useId';
15+
import { PolymorphicProps } from '../../types/common';
16+
import { PopoverAlignment } from '../Popover';
1617

17-
const Button = React.forwardRef(function Button(
18+
export const ButtonKinds = [
19+
'primary',
20+
'secondary',
21+
'danger',
22+
'ghost',
23+
'danger--primary',
24+
'danger--ghost',
25+
'danger--tertiary',
26+
'tertiary',
27+
] as const;
28+
29+
export type ButtonKind = (typeof ButtonKinds)[number];
30+
31+
export const ButtonSizes = ['sm', 'md', 'lg', 'xl', '2xl'] as const;
32+
33+
export type ButtonSize = (typeof ButtonSizes)[number];
34+
35+
export const ButtonTooltipAlignments = ['start', 'center', 'end'] as const;
36+
37+
export type ButtonTooltipAlignment = (typeof ButtonTooltipAlignments)[number];
38+
39+
export const ButtonTooltipPositions = ['top', 'right', 'bottom', 'left'];
40+
41+
export type ButtonTooltipPosition = (typeof ButtonTooltipPositions)[number];
42+
43+
interface ButtonBaseProps
44+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
45+
/**
46+
* Specify the message read by screen readers for the danger button variant
47+
*/
48+
dangerDescription?: string;
49+
50+
/**
51+
* Specify if the button is an icon-only button
52+
*/
53+
hasIconOnly?: boolean;
54+
55+
/**
56+
* Optionally specify an href for your Button to become an `<a>` element
57+
*/
58+
href?: string;
59+
60+
/**
61+
* If specifying the `renderIcon` prop, provide a description for that icon that can
62+
* be read by screen readers
63+
*/
64+
iconDescription?: string;
65+
66+
/**
67+
* Specify whether the Button is expressive, or not
68+
*/
69+
isExpressive?: boolean;
70+
71+
/**
72+
* Specify whether the Button is currently selected. Only applies to the Ghost variant.
73+
*/
74+
isSelected?: boolean;
75+
76+
/**
77+
* Specify the kind of Button you want to create
78+
*/
79+
kind?: ButtonKind;
80+
81+
/**
82+
* Optional prop to allow overriding the icon rendering.
83+
* Can be a React component class
84+
*/
85+
renderIcon?: React.ElementType;
86+
87+
/**
88+
* Specify the size of the button, from the following list of sizes:
89+
*/
90+
size?: ButtonSize;
91+
92+
/**
93+
* Specify the alignment of the tooltip to the icon-only button.
94+
* Can be one of: start, center, or end.
95+
*/
96+
tooltipAlignment?: ButtonTooltipAlignment;
97+
98+
/**
99+
* Specify the direction of the tooltip for icon-only buttons.
100+
* Can be either top, right, bottom, or left.
101+
*/
102+
tooltipPosition?: ButtonTooltipPosition;
103+
}
104+
105+
export type ButtonProps<T extends React.ElementType> = PolymorphicProps<
106+
T,
107+
ButtonBaseProps
108+
>;
109+
110+
export interface ButtonComponent {
111+
<T extends React.ElementType>(
112+
props: ButtonProps<T>,
113+
context?: any
114+
): React.ReactElement<any, any> | null;
115+
}
116+
117+
const Button = React.forwardRef(function Button<T extends React.ElementType>(
18118
{
19119
as,
20120
children,
@@ -39,13 +139,13 @@ const Button = React.forwardRef(function Button(
39139
tooltipPosition = 'top',
40140
type = 'button',
41141
...rest
42-
},
43-
ref
142+
}: ButtonProps<T>,
143+
ref: React.Ref<unknown>
44144
) {
45145
const tooltipRef = useRef(null);
46146
const prefix = usePrefix();
47147

48-
const handleClick = (evt) => {
148+
const handleClick = (evt: React.MouseEvent) => {
49149
// Prevent clicks on the tooltip from triggering the button click event
50150
if (evt.target === tooltipRef.current) {
51151
evt.preventDefault();
@@ -85,31 +185,29 @@ const Button = React.forwardRef(function Button(
85185

86186
const dangerButtonVariants = ['danger', 'danger--tertiary', 'danger--ghost'];
87187

88-
let component = 'button';
188+
let component: React.ElementType = 'button';
89189
const assistiveId = useId('danger-description');
90-
let otherProps = {
190+
const { 'aria-pressed': ariaPressed } = rest;
191+
let otherProps: Partial<ButtonBaseProps> = {
91192
disabled,
92193
type,
93194
'aria-describedby': dangerButtonVariants.includes(kind)
94195
? assistiveId
95-
: null,
196+
: undefined,
96197
'aria-pressed':
97-
rest['aria-pressed'] ??
98-
(hasIconOnly && kind === 'ghost' ? isSelected : null),
198+
ariaPressed ?? (hasIconOnly && kind === 'ghost' ? isSelected : undefined),
99199
};
100200
const anchorProps = {
101201
href,
102202
};
103203

104-
let assistiveText;
204+
let assistiveText: JSX.Element | null = null;
105205
if (dangerButtonVariants.includes(kind)) {
106206
assistiveText = (
107207
<span id={assistiveId} className={`${prefix}--visually-hidden`}>
108208
{dangerDescription}
109209
</span>
110210
);
111-
} else {
112-
assistiveText = null;
113211
}
114212

115213
if (as) {
@@ -141,7 +239,7 @@ const Button = React.forwardRef(function Button(
141239
);
142240

143241
if (hasIconOnly) {
144-
let align;
242+
let align: PopoverAlignment | undefined = undefined;
145243

146244
if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
147245
if (tooltipAlignment === 'center') {
@@ -233,7 +331,7 @@ Button.propTypes = {
233331
'renderIcon property specified without also providing an iconDescription property.'
234332
);
235333
}
236-
return undefined;
334+
return null;
237335
},
238336

239337
/**
@@ -320,4 +418,4 @@ Button.propTypes = {
320418
type: PropTypes.oneOf(['button', 'reset', 'submit']),
321419
};
322420

323-
export default Button;
421+
export default Button as ButtonComponent;

packages/react/src/components/Button/index.js renamed to packages/react/src/components/Button/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ import Button from './Button';
99

1010
export default Button;
1111
export { Button };
12+
export * from './Button';
1213
export { default as ButtonSkeleton } from './Button.Skeleton';

packages/react/src/components/ButtonSet/ButtonSet.js renamed to packages/react/src/components/ButtonSet/ButtonSet.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,31 @@ import React from 'react';
99
import PropTypes from 'prop-types';
1010
import classNames from 'classnames';
1111
import { usePrefix } from '../../internal/usePrefix';
12+
import { ForwardRefReturn } from '../../types/common';
1213

13-
const ButtonSet = React.forwardRef(function ButtonSet(
14-
{ children, className, stacked, ...rest },
15-
ref
16-
) {
17-
const prefix = usePrefix();
18-
const buttonSetClasses = classNames(className, `${prefix}--btn-set`, {
19-
[`${prefix}--btn-set--stacked`]: stacked,
14+
export interface ButtonSetProps extends React.HTMLAttributes<HTMLDivElement> {
15+
/**
16+
* Specify the button arrangement of the set (vertically stacked or
17+
* horizontal)
18+
*/
19+
stacked?: boolean;
20+
}
21+
22+
const ButtonSet: ForwardRefReturn<HTMLDivElement, ButtonSetProps> =
23+
React.forwardRef(function ButtonSet(
24+
{ children, className, stacked, ...rest }: ButtonSetProps,
25+
ref: React.Ref<HTMLDivElement>
26+
) {
27+
const prefix = usePrefix();
28+
const buttonSetClasses = classNames(className, `${prefix}--btn-set`, {
29+
[`${prefix}--btn-set--stacked`]: stacked,
30+
});
31+
return (
32+
<div {...rest} className={buttonSetClasses} ref={ref}>
33+
{children}
34+
</div>
35+
);
2036
});
21-
return (
22-
<div {...rest} className={buttonSetClasses} ref={ref}>
23-
{children}
24-
</div>
25-
);
26-
});
2737

2838
ButtonSet.displayName = 'ButtonSet';
2939
ButtonSet.propTypes = {
File renamed without changes.

packages/react/src/components/ComposedModal/ModalFooter.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ function SecondaryButtonSet({
3131

3232
if (Array.isArray(secondaryButtons) && secondaryButtons.length <= 2) {
3333
return secondaryButtons.map(({ buttonText, onClick: onButtonClick }, i) => (
34-
// @ts-expect-error: Invalid derived type, will be fine once explicit types are added
3534
<Button
3635
key={`${buttonText}-${i}`}
3736
className={secondaryClassName}
@@ -43,7 +42,6 @@ function SecondaryButtonSet({
4342
}
4443
if (secondaryButtonText) {
4544
return (
46-
// @ts-expect-error: Invalid derived type, will be fine once explicit types are added
4745
<Button
4846
className={secondaryClassName}
4947
onClick={handleRequestClose}
@@ -204,7 +202,6 @@ export const ModalFooter = React.forwardRef<HTMLElement, ModalFooterProps>(
204202
{/* @ts-expect-error: Invalid derived types, will be fine once explicit types are added */}
205203
<SecondaryButtonSet {...secondaryButtonProps} />
206204
{primaryButtonText && (
207-
// @ts-expect-error: Invalid derived types, will be fine once explicit types are added
208205
<Button
209206
onClick={onRequestSubmit}
210207
className={primaryClassName}

0 commit comments

Comments
 (0)