From ab30e224922d297b0599510f9f977a4d3a89c003 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk <112623876+anton-tsymuk-viacomcbs@users.noreply.github.com> Date: Fri, 16 Jun 2023 17:18:11 +0200 Subject: [PATCH] Add TypeScript types to ContentSwitcher, Switch (#13993) * refactor: add types for ContentSwitcher, Layout and Switch components * docs: update contributors list * refactor: density and size props typing --- .all-contributorsrc | 9 + README.md | 1 + ...ContentSwitcher.js => ContentSwitcher.tsx} | 121 ++++++++-- packages/react/src/components/Layout/index.js | 138 ----------- .../react/src/components/Layout/index.tsx | 217 ++++++++++++++++++ .../Switch/{Switch.js => Switch.tsx} | 85 ++++++- 6 files changed, 401 insertions(+), 170 deletions(-) rename packages/react/src/components/ContentSwitcher/{ContentSwitcher.js => ContentSwitcher.tsx} (59%) delete mode 100644 packages/react/src/components/Layout/index.js create mode 100644 packages/react/src/components/Layout/index.tsx rename packages/react/src/components/Switch/{Switch.js => Switch.tsx} (56%) diff --git a/.all-contributorsrc b/.all-contributorsrc index 70247ee49780..72800ebc0447 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1177,6 +1177,15 @@ "contributions": [ "code" ] + }, + { + "login": "anton-tsymuk-viacomcbs", + "name": "Anton Tsymuk", + "avatar_url": "https://avatars.githubusercontent.com/u/112623876?v=4", + "profile": "https://github.com/anton-tsymuk-viacomcbs", + "contributions": [ + "code" + ] } ], "commitConvention": "none" diff --git a/README.md b/README.md index 994570980868..42426e782e88 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Steven Black

💻 ️️️️♿️
Mark Judy

💻 +
Anton Tsymuk

💻 diff --git a/packages/react/src/components/ContentSwitcher/ContentSwitcher.js b/packages/react/src/components/ContentSwitcher/ContentSwitcher.tsx similarity index 59% rename from packages/react/src/components/ContentSwitcher/ContentSwitcher.js rename to packages/react/src/components/ContentSwitcher/ContentSwitcher.tsx index ee085656c515..cb3bfeb6c94d 100644 --- a/packages/react/src/components/ContentSwitcher/ContentSwitcher.js +++ b/packages/react/src/components/ContentSwitcher/ContentSwitcher.tsx @@ -6,7 +6,7 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { HTMLAttributes, ReactElement } from 'react'; import classNames from 'classnames'; import deprecate from '../../prop-types/deprecate'; import { LayoutConstraint } from '../Layout'; @@ -14,15 +14,73 @@ import { composeEventHandlers } from '../../tools/events'; import { getNextIndex, matches, keys } from '../../internal/keyboard'; import { PrefixContext } from '../../internal/usePrefix'; -export default class ContentSwitcher extends React.Component { +interface SwitchEventHandlersParams { + index?: number; + name?: string | number; + text?: string; + key?: string | number; +} + +export interface ContentSwitcherProps + extends Omit, 'onChange'> { + /** + * Pass in Switch components to be rendered in the ContentSwitcher + */ + children?: ReactElement[]; + + /** + * Specify an optional className to be added to the container node + */ + className?: string; + + /** + * `true` to use the light version. + * + * @deprecated The `light` prop for `ContentSwitcher` has + * been deprecated in favor of the new `Layer` component. It will be removed in the next major release. + */ + light?: boolean; + + /** + * Specify an `onChange` handler that is called whenever the ContentSwitcher + * changes which item is selected + */ + onChange: (params: SwitchEventHandlersParams) => void; + + /** + * Specify a selected index for the initially selected content + */ + selectedIndex: number; + + /** + * Choose whether or not to automatically change selection on focus + */ + selectionMode: 'automatic' | 'manual'; + + /** + * Specify the size of the Content Switcher. Currently supports either `sm`, 'md' (default) or 'lg` as an option. + */ + size: 'sm' | 'md' | 'lg'; +} + +interface ContentSwitcherState { + selectedIndex?: number; +} + +export default class ContentSwitcher extends React.Component< + ContentSwitcherProps, + ContentSwitcherState +> { /** * The DOM references of child ``. * @type {Array} * @private */ - _switchRefs = []; + _switchRefs: HTMLButtonElement[] = []; - state = {}; + state = { + selectedIndex: undefined, + }; static propTypes = { /** @@ -97,10 +155,14 @@ export default class ContentSwitcher extends React.Component { const { key } = data; if (matches(data, [keys.ArrowRight, keys.ArrowLeft])) { - const nextIndex = getNextIndex(key, index, this.props.children.length); + const nextIndex = getNextIndex( + key, + index, + this.props.children?.length as number + ); const children = React.Children.toArray(this.props.children); if (selectionMode === 'manual') { - const switchRef = this._switchRefs[nextIndex]; + const switchRef = this._switchRefs[nextIndex as number]; switchRef && switchRef.focus(); } else { this.setState( @@ -108,7 +170,11 @@ export default class ContentSwitcher extends React.Component { selectedIndex: nextIndex, }, () => { - const child = children[this.state.selectedIndex]; + if (typeof this.state.selectedIndex !== 'number') { + return; + } + + const child = children[this.state.selectedIndex] as ReactElement; const switchRef = this._switchRefs[this.state.selectedIndex]; switchRef && switchRef.focus(); this.props.onChange({ @@ -135,15 +201,20 @@ export default class ContentSwitcher extends React.Component { children, className, light, + // eslint-disable-next-line @typescript-eslint/no-unused-vars selectedIndex, // eslint-disable-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars selectionMode, // eslint-disable-line no-unused-vars size, ...other } = this.props; - const isIconOnly = React.Children.map(children, (child) => { - return child.type.displayName === 'IconSwitch'; - }).every((val) => val === true); + const isIconOnly = React.Children?.map(children, (child) => { + return ( + (child as { type: { displayName: string } }).type.displayName === + 'IconSwitch' + ); + })?.every((val) => val === true); const classes = classNames(`${prefix}--content-switcher`, className, { [`${prefix}--content-switcher--light`]: light, @@ -157,20 +228,22 @@ export default class ContentSwitcher extends React.Component { size={{ default: 'md', min: 'sm', max: 'lg' }} {...other} className={classes} - role="tablist"> - {React.Children.map(children, (child, index) => - React.cloneElement(child, { - index, - onClick: composeEventHandlers([ - this.handleChildChange, - child.props.onClick, - ]), - onKeyDown: this.handleChildChange, - selected: index === this.state.selectedIndex, - ref: this.handleItemRef(index), - size, - }) - )} + role="tablist" + onChange={undefined}> + {children && + React.Children.map(children, (child: ReactElement, index) => + React.cloneElement(child, { + index, + onClick: composeEventHandlers([ + this.handleChildChange, + child.props.onClick, + ]), + onKeyDown: this.handleChildChange, + selected: index === this.state.selectedIndex, + ref: this.handleItemRef(index), + size, + }) + )} ); } diff --git a/packages/react/src/components/Layout/index.js b/packages/react/src/components/Layout/index.js deleted file mode 100644 index 81e49190c908..000000000000 --- a/packages/react/src/components/Layout/index.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright IBM Corp. 2023 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import cx from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { usePrefix } from '../../internal/usePrefix'; - -const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl']; - -const densities = ['condensed', 'normal']; - -const Layout = React.forwardRef(function Layout( - { as: BaseComponent = 'div', children, className, density, size, ...rest }, - forwardRef -) { - const prefix = usePrefix(); - - const classes = cx(className, `${prefix}--layout`, { - [`${prefix}--layout--size-${size}`]: sizes.includes(size), - [`${prefix}--layout--density-${density}`]: densities.includes(density), - }); - - return ( - - {children} - - ); -}); - -Layout.propTypes = { - /** - * Specify a custom component or element to be rendered as the top-level - * element in the component - */ - as: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.string, - PropTypes.elementType, - ]), - - /** - * Provide child elements to be rendered inside of `Layout` - */ - children: PropTypes.node, - - /** - * Provide a custom class name to be used on the outermost element rendered by - * the component - */ - className: PropTypes.string, - - /** - * Specify the desired density of components within this layout - */ - density: PropTypes.oneOf(densities), - - /** - * Specify the desired size of components within this layout - */ - size: PropTypes.oneOf(sizes), -}; - -const LayoutConstraint = React.forwardRef(function Layout( - { as: BaseComponent = 'div', children, className, density, size, ...rest }, - forwardRef -) { - const prefix = usePrefix(); - - const classes = cx( - className, - Object.entries({ - size, - density, - }).map(([group, constraints]) => ({ - [`${prefix}--layout-constraint--${group}:default-${constraints?.default}`]: - constraints?.default, - [`${prefix}--layout-constraint--${group}:min-${constraints?.min}`]: - constraints?.min, - [`${prefix}--layout-constraint--${group}:max-${constraints?.max}`]: - constraints?.max, - })) - ); - - return ( - - {children} - - ); -}); - -LayoutConstraint.propTypes = { - /** - * Specify a custom component or element to be rendered as the top-level - * element in the component - */ - as: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.string, - PropTypes.elementType, - ]), - - /** - * Provide child elements to be rendered inside of `LayoutConstraint` - */ - children: PropTypes.node, - - /** - * Provide a custom class name to be used on the outermost element rendered by - * the component - */ - className: PropTypes.string, - - /** - * Specify the desired layout density constraints of this element's children - */ - density: PropTypes.shape({ - min: PropTypes.oneOf(densities), - default: PropTypes.oneOf(densities), - max: PropTypes.oneOf(densities), - }), - - /** - * Specify the desired layout size constraints of this element's children - */ - size: PropTypes.shape({ - min: PropTypes.oneOf(sizes), - default: PropTypes.oneOf(sizes), - max: PropTypes.oneOf(sizes), - }), -}; - -export { Layout, LayoutConstraint }; diff --git a/packages/react/src/components/Layout/index.tsx b/packages/react/src/components/Layout/index.tsx new file mode 100644 index 000000000000..0f29f8dea656 --- /dev/null +++ b/packages/react/src/components/Layout/index.tsx @@ -0,0 +1,217 @@ +/** + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React, { ElementType, HTMLAttributes, ReactNode } from 'react'; + +import { usePrefix } from '../../internal/usePrefix'; + +const sizes: Size[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl']; + +const densities: Density[] = ['condensed', 'normal']; + +/** + * Density of components within this layout + */ +type Density = 'condensed' | 'normal'; + +/** + * Size of components within this layout + */ +type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; + +export interface LayoutProps extends HTMLAttributes { + /** + * Specify a custom component or element to be rendered as the top-level + * element in the component + */ + as?: (() => ReactNode) | string | ElementType; + + /** + * Provide child elements to be rendered inside of `Layout` + */ + children?: ReactNode; + + /** + * Provide a custom class name to be used on the outermost element rendered by + * the component + */ + className?: string; + + /** + * Specify the desired density of components within this layout + */ + density?: Density; + + /** + * Specify the desired size of components within this layout + */ + size?: Size; +} + +const Layout = React.forwardRef(function Layout( + { as: BaseComponent = 'div', children, className, density, size, ...rest }, + forwardRef +) { + const prefix = usePrefix(); + + const classes = cx(className, `${prefix}--layout`, { + [`${prefix}--layout--size-${size}`]: size && sizes.includes(size), + [`${prefix}--layout--density-${density}`]: + density && densities.includes(density), + }); + + return ( + + {children} + + ); +}); + +Layout.propTypes = { + /** + * Specify a custom component or element to be rendered as the top-level + * element in the component + */ + as: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + PropTypes.elementType, + ]), + + /** + * Provide child elements to be rendered inside of `Layout` + */ + children: PropTypes.node, + + /** + * Provide a custom class name to be used on the outermost element rendered by + * the component + */ + className: PropTypes.string, + + /** + * Specify the desired density of components within this layout + */ + density: PropTypes.oneOf(densities), + + /** + * Specify the desired size of components within this layout + */ + size: PropTypes.oneOf(sizes), +}; + +export interface LayoutConstraintProps extends HTMLAttributes { + /** + * Specify a custom component or element to be rendered as the top-level + * element in the component + */ + as?: (() => ReactNode) | string | ElementType; + + /** + * Provide child elements to be rendered inside of `LayoutConstraint` + */ + children?: ReactNode; + + /** + * Provide a custom class name to be used on the outermost element rendered by + * the component + */ + className?: string; + + /** + * Specify the desired layout density constraints of this element's children + */ + density?: { + min?: Density | null; + default?: Density | null; + max?: Density | null; + } | null; + + /** + * Specify the desired layout size constraints of this element's children + */ + size?: { + min?: Size | null; + default?: Size | null; + max?: Size | null; + } | null; +} + +const LayoutConstraint = React.forwardRef( + function Layout( + { as: BaseComponent = 'div', children, className, density, size, ...rest }, + forwardRef + ) { + const prefix = usePrefix(); + + const classes = cx( + className, + Object.entries({ + size, + density, + }).map(([group, constraints]) => ({ + [`${prefix}--layout-constraint--${group}:default-${constraints?.default}`]: + constraints?.default, + [`${prefix}--layout-constraint--${group}:min-${constraints?.min}`]: + constraints?.min, + [`${prefix}--layout-constraint--${group}:max-${constraints?.max}`]: + constraints?.max, + })) + ); + + return ( + + {children} + + ); + } +); + +LayoutConstraint.propTypes = { + /** + * Specify a custom component or element to be rendered as the top-level + * element in the component + */ + as: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + PropTypes.elementType, + ]), + + /** + * Provide child elements to be rendered inside of `LayoutConstraint` + */ + children: PropTypes.node, + + /** + * Provide a custom class name to be used on the outermost element rendered by + * the component + */ + className: PropTypes.string, + + /** + * Specify the desired layout density constraints of this element's children + */ + density: PropTypes.shape({ + min: PropTypes.oneOf(densities), + default: PropTypes.oneOf(densities), + max: PropTypes.oneOf(densities), + }), + + /** + * Specify the desired layout size constraints of this element's children + */ + size: PropTypes.shape({ + min: PropTypes.oneOf(sizes), + default: PropTypes.oneOf(sizes), + max: PropTypes.oneOf(sizes), + }), +}; + +export { Layout, LayoutConstraint }; diff --git a/packages/react/src/components/Switch/Switch.js b/packages/react/src/components/Switch/Switch.tsx similarity index 56% rename from packages/react/src/components/Switch/Switch.js rename to packages/react/src/components/Switch/Switch.tsx index e4c201d04598..03e73b56a630 100644 --- a/packages/react/src/components/Switch/Switch.js +++ b/packages/react/src/components/Switch/Switch.tsx @@ -6,11 +6,80 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { + ButtonHTMLAttributes, + KeyboardEvent, + MouseEvent, + ReactNode, +} from 'react'; import classNames from 'classnames'; import { usePrefix } from '../../internal/usePrefix'; -const Switch = React.forwardRef(function Switch(props, tabRef) { +interface SwitchEventHandlersParams { + index?: number; + name?: string | number; + text?: string; + key?: string | number; +} + +export interface SwitchProps + extends Omit< + ButtonHTMLAttributes, + 'name' | 'onClick' | 'onKeyDown' + > { + /** + * Provide child elements to be rendered inside of the Switch + */ + children?: ReactNode; + + /** + * Specify an optional className to be added to your Switch + */ + className?: string; + + /** + * Specify whether or not the Switch should be disabled + */ + disabled?: boolean; + + /** + * The index of your Switch in your ContentSwitcher that is used for event handlers. + * Reserved for usage in ContentSwitcher + */ + index?: number; + + /** + * Provide the name of your Switch that is used for event handlers + */ + name?: string | number; + + /** + * A handler that is invoked when a user clicks on the control. + * Reserved for usage in ContentSwitcher + */ + onClick?: (params: SwitchEventHandlersParams) => void; + + /** + * A handler that is invoked on the key down event for the control. + * Reserved for usage in ContentSwitcher + */ + onKeyDown?: (params: SwitchEventHandlersParams) => void; + + /** + * Whether your Switch is selected. Reserved for usage in ContentSwitcher + */ + selected?: boolean; + + /** + * Provide the contents of your Switch + */ + text?: string; +} + +const Switch = React.forwardRef(function Switch( + props: SwitchProps, + tabRef +) { const { children, className, @@ -19,21 +88,21 @@ const Switch = React.forwardRef(function Switch(props, tabRef) { name, onClick, onKeyDown, - selected, + selected = false, text, ...other } = props; const prefix = usePrefix(); - const handleClick = (e) => { + const handleClick = (e: MouseEvent) => { e.preventDefault(); - onClick({ index, name, text }); + onClick?.({ index, name, text }); }; - const handleKeyDown = (event) => { + const handleKeyDown = (event: KeyboardEvent) => { const key = event.key || event.which; - onKeyDown({ index, name, text, key }); + onKeyDown?.({ index, name, text, key }); }; const classes = classNames(className, `${prefix}--content-switcher-btn`, { @@ -52,7 +121,7 @@ const Switch = React.forwardRef(function Switch(props, tabRef) { type="button" ref={tabRef} role="tab" - tabIndex={selected ? '0' : '-1'} + tabIndex={selected ? 0 : -1} aria-selected={selected} {...other} {...commonProps}>