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}>