From 78620aa742a354360a9603e622583dba819cb418 Mon Sep 17 00:00:00 2001 From: ariellalgilmore Date: Thu, 25 Jul 2024 17:17:15 -0700 Subject: [PATCH 1/2] refactor(action-bar): convert to typescript --- .../ActionBar/{ActionBar.js => ActionBar.tsx} | 107 +++++++++++++++--- .../{ActionBarItem.js => ActionBarItem.tsx} | 48 +++++++- ...lowItems.js => ActionBarOverflowItems.tsx} | 40 ++++++- 3 files changed, 173 insertions(+), 22 deletions(-) rename packages/ibm-products/src/components/ActionBar/{ActionBar.js => ActionBar.tsx} (73%) rename packages/ibm-products/src/components/ActionBar/{ActionBarItem.js => ActionBarItem.tsx} (71%) rename packages/ibm-products/src/components/ActionBar/{ActionBarOverflowItems.js => ActionBarOverflowItems.tsx} (76%) diff --git a/packages/ibm-products/src/components/ActionBar/ActionBar.js b/packages/ibm-products/src/components/ActionBar/ActionBar.tsx similarity index 73% rename from packages/ibm-products/src/components/ActionBar/ActionBar.js rename to packages/ibm-products/src/components/ActionBar/ActionBar.tsx index 279410b6ce..559b954a1a 100644 --- a/packages/ibm-products/src/components/ActionBar/ActionBar.js +++ b/packages/ibm-products/src/components/ActionBar/ActionBar.tsx @@ -6,7 +6,13 @@ // // Import portions of React that are needed. -import React, { useEffect, useState, useRef } from 'react'; +import React, { + useEffect, + useState, + useRef, + PropsWithChildren, + Ref, +} from 'react'; // Other standard imports. import PropTypes from 'prop-types'; @@ -20,6 +26,8 @@ import uuidv4 from '../../global/js/utils/uuidv4'; import { prepareProps } from '../../global/js/utils/props-helper'; import { ActionBarItem } from './ActionBarItem'; import { ActionBarOverflowItems } from './ActionBarOverflowItems'; +import { ButtonProps } from '@carbon/type'; +import { CarbonIconType } from '@carbon/icons-react/lib/CarbonIcon'; // The block part of our conventional BEM class names (blockClass__E--M). const blockClass = `${pkg.prefix}--action-bar`; @@ -30,6 +38,59 @@ const defaults = { actions: Object.freeze([]), }; +interface Action extends ButtonProps { + id?: string; + key: string; + iconDescription: string; + label: string; + onClick: () => void; + renderIcon: CarbonIconType; +} + +interface ActionBarProps extends PropsWithChildren { + /** + * Specifies the action bar items. Each item is specified as an object + * with required fields: key for array rendering, renderIcon, iconDescription and + * label to provide the icon to display, + * and optional 'onClick' to receive notifications when the button is clicked. + * Additional fields in the object will be passed to the + * Button component, and these can include 'disabled', 'ref', 'className', + * and any other Button props. + * + * Note that the Button props 'kind', 'size', + * 'tooltipPosition', 'tooltipAlignment' and 'type' are ignored, as these + * cannot be used for an action bar item. + * + * Carbon Button API https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api + */ + actions?: readonly Action[]; + // expects action bar item as array or in fragment, + /** + * className + */ + className?: string; + /** + * maxVisible : Maximum action bar items visible before going into the overflow menu + */ + maxVisible?: number; + /** + * class name applied to the overflow options + */ + menuOptionsClass?: string; + /** + * onItemCountChange - event reporting maxWidth + */ + onWidthChange?: (sizes: { minWidth: number; maxWidth: number }) => void; + /** + * overflowAriaLabel label for open close button overflow used for action bar items that do nto fit. + */ + overflowAriaLabel: string; + /** + * align tags to right of available space + */ + rightAlign?: boolean; +} + // NOTE: the component SCSS is not imported here: it is rolled up separately. /** @@ -50,18 +111,19 @@ export let ActionBar = React.forwardRef( // Collect any other property values passed in. ...rest - }, - ref + }: ActionBarProps, + ref: Ref ) => { const [displayCount, setDisplayCount] = useState(0); - const [displayedItems, setDisplayedItems] = useState([]); - const [hiddenSizingItems, setHiddenSizingItems] = useState([]); + const [displayedItems, setDisplayedItems] = useState([]); + const [hiddenSizingItems, setHiddenSizingItems] = + useState(null); const internalId = useRef(uuidv4()); - const refDisplayedItems = useRef(null); - const sizingRef = useRef(null); - const sizes = useRef({}); + const refDisplayedItems = useRef(null); + const sizingRef = useRef(null); + const sizes = useRef<{ minWidth?: number; maxWidth?: number }>({}); - const backupRef = useRef(); + const backupRef = useRef(null); const localRef = ref || backupRef; // create hidden sizing items @@ -125,8 +187,8 @@ export let ActionBar = React.forwardRef( const checkFullyVisibleItems = () => { /* istanbul ignore if */ if (sizingRef.current) { - let sizingItems = Array.from( - sizingRef.current.querySelectorAll( + const sizingItems = Array.from( + sizingRef.current.querySelectorAll( `.${blockClass}__hidden-sizing-item` ) ); @@ -135,31 +197,36 @@ export let ActionBar = React.forwardRef( const overflowItem = sizingItems.shift(); // determine how many will fit - let spaceAvailable = refDisplayedItems.current.offsetWidth; + let spaceAvailable = refDisplayedItems.current?.offsetWidth; let willFit = 0; let maxVisibleWidth = 0; - let fitLimit = maxVisible + const fitLimit = maxVisible ? Math.min(maxVisible, sizingItems.length) : sizingItems.length; // loop checking the space available for (let i = 0; i < fitLimit; i++) { - const newSpaceAvailable = spaceAvailable - sizingItems[i].offsetWidth; + const newSpaceAvailable = + spaceAvailable && spaceAvailable - sizingItems[i].offsetWidth; // update maxVisibleWidth for later use by onWidthChange maxVisibleWidth += sizingItems[i].offsetWidth; - if (newSpaceAvailable >= 0) { + if (newSpaceAvailable && newSpaceAvailable >= 0) { spaceAvailable = newSpaceAvailable; willFit += 1; } } // if not enough space for all items then make room for the overflow - const overflowWidth = overflowItem.offsetWidth; + const overflowWidth = overflowItem!.offsetWidth; if (willFit < sizingItems.length) { // Check space for overflow - while (willFit > 0 && spaceAvailable < overflowWidth) { + while ( + willFit > 0 && + spaceAvailable && + spaceAvailable < overflowWidth + ) { willFit -= 1; // Highly unlikely that any action bar item is narrower than the overflow item @@ -178,7 +245,8 @@ export let ActionBar = React.forwardRef( sizes.current.maxWidth = maxVisibleWidth; // emit onWidthChange onWidthChange({ - ...sizes.current, + minWidth: overflowWidth, + maxWidth: maxVisibleWidth, }); } if (willFit < 1) { @@ -242,8 +310,10 @@ ActionBar.propTypes = { * * Carbon Button API https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api */ + /**@ts-ignore */ actions: PropTypes.arrayOf( PropTypes.shape({ + /**@ts-ignore */ ...prepareProps(Button.propTypes, [ // props not desired from Button.propTypes 'kind', @@ -256,6 +326,7 @@ ActionBar.propTypes = { // Redefine as form different to Button and a key prop used by ActionBarItems iconDescription: PropTypes.string.isRequired, label: PropTypes.string.isRequired, + /**@ts-ignore */ renderIcon: Button.propTypes.renderIcon.isRequired, // We duplicate onClick here to improve DocGen in Storybook onClick: PropTypes.func, diff --git a/packages/ibm-products/src/components/ActionBar/ActionBarItem.js b/packages/ibm-products/src/components/ActionBar/ActionBarItem.tsx similarity index 71% rename from packages/ibm-products/src/components/ActionBar/ActionBarItem.js rename to packages/ibm-products/src/components/ActionBar/ActionBarItem.tsx index e179ed0051..965b684413 100644 --- a/packages/ibm-products/src/components/ActionBar/ActionBarItem.js +++ b/packages/ibm-products/src/components/ActionBar/ActionBarItem.tsx @@ -6,7 +6,7 @@ // // Import portions of React that are needed. -import React from 'react'; +import React, { ForwardedRef, PropsWithChildren } from 'react'; // Other standard imports. import PropTypes from 'prop-types'; @@ -15,18 +15,56 @@ import { pkg } from '../../settings'; // Carbon and package components we use. import { IconButton } from '@carbon/react'; +import { CarbonIconType } from '@carbon/icons-react/lib/CarbonIcon'; // The block part of our conventional BEM class names (blockClass__E--M). const componentName = 'ActionBarItem'; const blockClass = `${pkg.prefix}--action-bar-item`; +interface ActionBarItemProps extends PropsWithChildren { + /** + * Specify an optional className to be added to your Button + * + * (inherited from Carbon Button) + */ + className?: string; + /** + * If specifying the `renderIcon` prop, provide a description for that icon that can + * be read by screen readers + * + * (inherited from Carbon Button) + */ + label?: string; + /** + * Optional click handler + * + * (inherited from Carbon Button) + */ + onClick?: () => void; + /** + * Optional prop to allow overriding the icon rendering. + * Can be a React component class + * + * (inherited from Carbon Button) + */ + renderIcon?: CarbonIconType; + + /** + * Optional tab index + */ + tabIndex?: number; +} + // NOTE: the component SCSS is not imported here: it is rolled up separately. /** * The ActionBarItem is used in the page header to populate the action bar */ export let ActionBarItem = React.forwardRef( - ({ label, className, renderIcon, ...rest }, ref) => { + ( + { label, className, renderIcon, ...rest }: ActionBarItemProps, + ref: ForwardedRef + ) => { const Icon = renderIcon; return ( @@ -98,5 +136,11 @@ ActionBarItem.propTypes = { * * (inherited from Carbon Button) */ + /**@ts-ignore */ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** + * Optional tab index + */ + tabIndex: PropTypes.number, }; diff --git a/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.js b/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx similarity index 76% rename from packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.js rename to packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx index 369a7070b7..b707967a90 100644 --- a/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.js +++ b/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx @@ -6,7 +6,7 @@ // // Import portions of React that are needed. -import React, { useRef } from 'react'; +import React, { useRef, PropsWithChildren, ReactElement } from 'react'; // Other standard imports. import PropTypes from 'prop-types'; @@ -15,18 +15,49 @@ import cx from 'classnames'; // Carbon and package components we use. import { OverflowMenu, OverflowMenuItem } from '@carbon/react'; import uuidv4 from '../../global/js/utils/uuidv4'; +import { CarbonIconType } from '@carbon/icons-react/lib/CarbonIcon'; // The block part of our conventional BEM class names (blockClass__E--M). import { pkg } from '../../settings'; const blockClass = `${pkg.prefix}--action-bar-overflow-items`; const componentName = 'ActionBar'; +type OverflowItem = { + label: string; + onClick: () => void; + renderIcon: CarbonIconType; +}; + +interface ActionBarOverflowItemProps extends PropsWithChildren { + /** + * className + */ + className?: string; + /** + * class name applied to the overflow options + */ + menuOptionsClass?: string; + /** + * overflowAriaLabel label for open close button overflow used for action bar items that do nto fit. + */ + overflowAriaLabel?: string; + /** + * overflowItems: items to bre shown in the ActionBar overflow menu + */ + overflowItems: ReactElement[]; + + /** + * Optional tab index + */ + tabIndex?: number; +} + export const ActionBarOverflowItems = ({ className, menuOptionsClass, overflowItems, overflowAriaLabel, -}) => { +}: ActionBarOverflowItemProps) => { const internalId = useRef(uuidv4()); return ( Date: Mon, 29 Jul 2024 09:58:22 -0700 Subject: [PATCH 2/2] fix(comments): update --- .../src/components/ActionBar/ActionBar.tsx | 3 ++- .../components/ActionBar/ActionBarOverflowItems.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/ibm-products/src/components/ActionBar/ActionBar.tsx b/packages/ibm-products/src/components/ActionBar/ActionBar.tsx index 559b954a1a..2c735954ef 100644 --- a/packages/ibm-products/src/components/ActionBar/ActionBar.tsx +++ b/packages/ibm-products/src/components/ActionBar/ActionBar.tsx @@ -43,7 +43,7 @@ interface Action extends ButtonProps { key: string; iconDescription: string; label: string; - onClick: () => void; + onClick?: () => void; renderIcon: CarbonIconType; } @@ -321,6 +321,7 @@ ActionBar.propTypes = { 'tooltipPosition', 'tooltipAlignment', ]), + id: PropTypes.string, // Additional props key: PropTypes.string.isRequired, // Redefine as form different to Button and a key prop used by ActionBarItems diff --git a/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx b/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx index b707967a90..27f84535cd 100644 --- a/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx +++ b/packages/ibm-products/src/components/ActionBar/ActionBarOverflowItems.tsx @@ -44,7 +44,7 @@ interface ActionBarOverflowItemProps extends PropsWithChildren { /** * overflowItems: items to bre shown in the ActionBar overflow menu */ - overflowItems: ReactElement[]; + overflowItems?: ReactElement[]; /** * Optional tab index @@ -72,11 +72,11 @@ export const ActionBarOverflowItems = ({ // This uses a copy of a menu item option // NOTE: Cannot use a real Tooltip icon below as it uses a