diff --git a/packages/react/src/components/Dropdown/Dropdown.js b/packages/react/src/components/Dropdown/Dropdown.tsx similarity index 64% rename from packages/react/src/components/Dropdown/Dropdown.js rename to packages/react/src/components/Dropdown/Dropdown.tsx index 190fd320db96..38a87925e2b0 100644 --- a/packages/react/src/components/Dropdown/Dropdown.js +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useRef, useContext, useState } from 'react'; -import { useSelect } from 'downshift'; +import React, { useRef, useContext, useState, FocusEvent, ForwardedRef, MouseEvent, ReactNode } from 'react'; +import { useSelect, UseSelectProps, UseSelectState } from 'downshift'; import cx from 'classnames'; import PropTypes from 'prop-types'; import { @@ -14,22 +14,176 @@ import { WarningAltFilled, WarningFilled, } from '@carbon/icons-react'; -import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox'; +import ListBox, { ListBoxSize, ListBoxType, PropTypes as ListBoxPropTypes } from '../ListBox'; import mergeRefs from '../../tools/mergeRefs'; import deprecate from '../../prop-types/deprecate'; import { useFeatureFlag } from '../FeatureFlags'; import { usePrefix } from '../../internal/usePrefix'; import { FormContext } from '../FluidForm'; +import { ReactAttr } from '../../types/common'; -const defaultItemToString = (item) => { +const defaultItemToString = <ItemType,>(item?: ItemType): string => { if (typeof item === 'string') { return item; } - - return item ? item.label : ''; + if (typeof item === 'number') { + return `${item}` + } + if (item !== null && typeof item === 'object' + && 'label' in item && typeof item['label'] === 'string') { + return item['label'] + } + return ''; }; -const Dropdown = React.forwardRef(function Dropdown( +type ExcludedAttributes = 'id' | 'onChange'; + +export interface OnChangeData<ItemType> { + selectedItem: ItemType | null; +} + +export interface DropdownProps<ItemType> + extends Omit<ReactAttr<HTMLDivElement>, ExcludedAttributes> { + + /** + * 'aria-label' of the ListBox component. + */ + ariaLabel?: string; + + /** + * Specify the direction of the dropdown. Can be either top or bottom. + */ + direction?: 'top' | 'bottom'; + + /** + * Disable the control + */ + disabled?: boolean; + + /** + * Additional props passed to Downshift + */ + downshiftProps?: Partial<UseSelectProps<ItemType>>; + + /** + * Provide helper text that is used alongside the control label for + * additional help + */ + helperText?: React.ReactNode; + + /** + * Specify whether the title text should be hidden or not + */ + hideLabel?: boolean; + + /** + * Specify a custom `id` + */ + id: string; + + /** + * Allow users to pass in an arbitrary item or a string (in case their items are an array of strings) + * from their collection that are pre-selected + */ + initialSelectedItem?: ItemType; + + /** + * Specify if the currently selected value is invalid. + */ + invalid?: boolean; + + /** + * Message which is displayed if the value is invalid. + */ + invalidText?: React.ReactNode; + + /** + * Function to render items as custom components instead of strings. + * Defaults to null and is overridden by a getter + */ + itemToElement?: React.JSXElementConstructor<ItemType> | null; + + /** + * Helper function passed to downshift that allows the library to render a + * given item to a string label. By default, it extracts the `label` field + * from a given item to serve as the item label in the list. + */ + itemToString?(item: ItemType): string; + + /** + * We try to stay as generic as possible here to allow individuals to pass + * in a collection of whatever kind of data structure they prefer + */ + items: ItemType[]; + + /** + * Generic `label` that will be used as the textual representation of what + * this field is for + */ + label: NonNullable<ReactNode>; + + /** + * `true` to use the light version. + * @deprecated The `light` prop for `Dropdown` has been deprecated + * in favor of the new `Layer` component. It will be removed in the next major release. + */ + light?: boolean; + + /** + * `onChange` is a utility for this controlled component to communicate to a + * consuming component what kind of internal state changes are occurring. + */ + onChange?(data: OnChangeData<ItemType>): void; + + /** + * Whether or not the Dropdown is readonly + */ + readOnly?: boolean; + + /** + * An optional callback to render the currently selected item as a react element instead of only + * as a string. + */ + renderSelectedItem?(item: ItemType): string; + + /** + * In the case you want to control the dropdown selection entirely. + */ + selectedItem?: ItemType; + + /** + * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option. + */ + size?: ListBoxSize; + + /** + * Provide the title text that will be read by a screen reader when + * visiting this control + */ + titleText?: React.ReactNode; + + /** + * Callback function for translating ListBoxMenuIcon SVG title + */ + translateWithId?(messageId: string, args?: Record<string, unknown>): string; + + /** + * The dropdown type, `default` or `inline` + */ + type?: ListBoxType; + + /** + * Specify whether the control is currently in warning state + */ + warn?: boolean; + + /** + * Provide the text that is displayed when the control is in warning state + */ + warnText?: React.ReactNode; +} + +const Dropdown = React.forwardRef(<ItemType,>( { className: containerClassName, disabled, @@ -37,7 +191,7 @@ const Dropdown = React.forwardRef(function Dropdown( items, label, ariaLabel, - itemToString, + itemToString = defaultItemToString, itemToElement, renderSelectedItem, type, @@ -58,12 +212,12 @@ const Dropdown = React.forwardRef(function Dropdown( downshiftProps, readOnly, ...other - }, - ref -) { + }: DropdownProps<ItemType>, + ref: ForwardedRef<HTMLButtonElement> +) => { const prefix = usePrefix(); const { isFluid } = useContext(FormContext); - const selectProps = { + const selectProps: UseSelectProps<ItemType> = { ...downshiftProps, items, itemToString, @@ -141,28 +295,32 @@ const Dropdown = React.forwardRef(function Dropdown( <div className={helperClasses}>{helperText}</div> ) : null; - function onSelectedItemChange({ selectedItem }) { + function onSelectedItemChange({ selectedItem }: Partial<UseSelectState<ItemType>>) { setIsFocused(false); if (onChange) { - onChange({ selectedItem }); + onChange({ selectedItem: selectedItem ?? null }); } } const menuItemOptionRefs = useRef(items.map((_) => React.createRef())); - const handleFocus = (evt) => { + const handleFocus = (evt: FocusEvent<HTMLDivElement>) => { setIsFocused(evt.type === 'focus' ? true : false); }; + const mergedRef = mergeRefs(toggleButtonProps.ref, ref); + const readOnlyEventHandlers = readOnly ? { - onClick: (evt) => { + onClick: (evt: MouseEvent<HTMLButtonElement>) => { // NOTE: does not prevent click evt.preventDefault(); // focus on the element as per readonly input behavior - evt.target.focus(); + if (mergedRef.current !== undefined) { + mergedRef.current.focus(); + } }, - onKeyDown: (evt) => { + onKeyDown: (evt: React.KeyboardEvent<HTMLButtonElement>) => { const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter']; // This prevents the select from opening for the above keys if (selectAccessKeys.includes(evt.key)) { @@ -205,10 +363,10 @@ const Dropdown = React.forwardRef(function Dropdown( className={`${prefix}--list-box__field`} disabled={disabled} aria-disabled={readOnly ? true : undefined} // aria-disabled to remain focusable - title={selectedItem ? itemToString(selectedItem) : label} + title={selectedItem && itemToString !== undefined ? itemToString(selectedItem) : label} {...toggleButtonProps} {...readOnlyEventHandlers} - ref={mergeRefs(toggleButtonProps.ref, ref)}> + ref={mergedRef}> <span className={`${prefix}--list-box__label`}> {selectedItem ? renderSelectedItem @@ -221,12 +379,14 @@ const Dropdown = React.forwardRef(function Dropdown( <ListBox.Menu {...getMenuProps()}> {isOpen && items.map((item, index) => { + const isObject = item !== null && typeof item === 'object'; + const disabled = isObject && 'disabled' in item && item.disabled === true; const itemProps = getItemProps({ item, index, - disabled: item.disabled, + disabled, }); - const title = itemToElement ? item.text : itemToString(item); + const title = isObject && 'text' in item && itemToElement ? item.text : itemToString(item); return ( <ListBox.MenuItem key={itemProps.id} @@ -239,7 +399,8 @@ const Dropdown = React.forwardRef(function Dropdown( menuItemOptionRef: menuItemOptionRefs.current[index], }} {...itemProps}> - {itemToElement ? ( + {typeof item === 'object' && ItemToElement !== undefined + && ItemToElement !== null ? ( <ItemToElement key={itemProps.id} {...item} /> ) : ( itemToString(item) @@ -259,6 +420,14 @@ const Dropdown = React.forwardRef(function Dropdown( ); }); +type DropdownComponentProps<ItemType> = + React.PropsWithoutRef<React.PropsWithChildren<DropdownProps<ItemType>> + & React.RefAttributes<HTMLButtonElement>> + +interface DropdownComponent { + <ItemType>(props: DropdownComponentProps<ItemType>): React.ReactElement | null +} + Dropdown.displayName = 'Dropdown'; Dropdown.propTypes = { /** @@ -284,7 +453,7 @@ Dropdown.propTypes = { /** * Additional props passed to Downshift */ - downshiftProps: PropTypes.object, + downshiftProps: PropTypes.object as React.Validator<UseSelectProps<unknown>>, /** * Provide helper text that is used alongside the control label for @@ -422,6 +591,6 @@ Dropdown.defaultProps = { titleText: '', helperText: '', direction: 'bottom', -}; +} as DropdownProps<unknown>; -export default Dropdown; +export default Dropdown as DropdownComponent; diff --git a/packages/react/src/components/Dropdown/index.js b/packages/react/src/components/Dropdown/index.ts similarity index 77% rename from packages/react/src/components/Dropdown/index.js rename to packages/react/src/components/Dropdown/index.ts index d4a911edc773..b51047b15d72 100644 --- a/packages/react/src/components/Dropdown/index.js +++ b/packages/react/src/components/Dropdown/index.ts @@ -5,8 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import Dropdown from './Dropdown'; +import Dropdown, { OnChangeData } from './Dropdown'; +export type { OnChangeData }; +export { Dropdown }; export { default as DropdownSkeleton } from './Dropdown.Skeleton'; + export default Dropdown; -export { Dropdown }; diff --git a/packages/react/src/components/FluidForm/FluidForm.js b/packages/react/src/components/FluidForm/FluidForm.tsx similarity index 79% rename from packages/react/src/components/FluidForm/FluidForm.js rename to packages/react/src/components/FluidForm/FluidForm.tsx index 7e6a7df4aced..e2179c276280 100644 --- a/packages/react/src/components/FluidForm/FluidForm.js +++ b/packages/react/src/components/FluidForm/FluidForm.tsx @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. @@ -11,8 +11,15 @@ import classnames from 'classnames'; import Form from '../Form'; import { FormContext } from './FormContext'; import { usePrefix } from '../../internal/usePrefix'; +import { ReactAttr } from '../../types/common'; -function FluidForm({ className, children, ...other }) { +export type FluidFormProps = ReactAttr<HTMLFormElement> + +const FluidForm: React.FC<FluidFormProps> = ({ + className, + children, + ...other +}: FluidFormProps) => { const prefix = usePrefix(); const classNames = classnames(`${prefix}--form--fluid`, className); diff --git a/packages/react/src/components/FluidForm/FormContext.js b/packages/react/src/components/FluidForm/FormContext.ts similarity index 57% rename from packages/react/src/components/FluidForm/FormContext.js rename to packages/react/src/components/FluidForm/FormContext.ts index c99698b123e9..9e52f9fcd202 100644 --- a/packages/react/src/components/FluidForm/FormContext.js +++ b/packages/react/src/components/FluidForm/FormContext.ts @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. @@ -7,6 +7,9 @@ import { createContext } from 'react'; -export const FormContext = createContext({ +export interface FormContextProps { + isFluid?: boolean; +} +export const FormContext = createContext<FormContextProps>({ isFluid: false, }); diff --git a/packages/react/src/components/FluidForm/index.js b/packages/react/src/components/FluidForm/index.ts similarity index 89% rename from packages/react/src/components/FluidForm/index.js rename to packages/react/src/components/FluidForm/index.ts index 26d7982218a7..24ac2bf4cad6 100644 --- a/packages/react/src/components/FluidForm/index.js +++ b/packages/react/src/components/FluidForm/index.ts @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. diff --git a/packages/react/src/components/ListBox/ListBox.js b/packages/react/src/components/ListBox/ListBox.tsx similarity index 65% rename from packages/react/src/components/ListBox/ListBox.js rename to packages/react/src/components/ListBox/ListBox.tsx index 447487962360..d857421502d2 100644 --- a/packages/react/src/components/ListBox/ListBox.js +++ b/packages/react/src/components/ListBox/ListBox.tsx @@ -1,39 +1,93 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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 React, { useContext } from 'react'; +import React, { KeyboardEvent, MouseEvent, useContext} from 'react'; import PropTypes from 'prop-types'; import deprecate from '../../prop-types/deprecate'; import { ListBoxType, ListBoxSize } from './ListBoxPropTypes'; import { usePrefix } from '../../internal/usePrefix'; -import ListBoxField from './ListBoxField'; -import ListBoxMenu from './ListBoxMenu'; -import ListBoxMenuIcon from './ListBoxMenuIcon'; -import ListBoxMenuItem from './ListBoxMenuItem'; -import ListBoxSelection from './ListBoxSelection'; import { FormContext } from '../FluidForm'; +import { ForwardRefReturn, ReactAttr } from '../../types/common'; -const handleOnKeyDown = (event) => { +const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { if (event.keyCode === 27) { event.stopPropagation(); } }; -const handleClick = (event) => { +const handleClick = (event: MouseEvent<HTMLDivElement>) => { event.preventDefault(); event.stopPropagation(); }; +type ExcludedAttributes = 'onKeyDown' | 'onKeyPress' | 'ref' + +export interface ListBoxProps + extends Omit<ReactAttr<HTMLDivElement>, ExcludedAttributes> { + + /** + * Specify whether the ListBox is currently disabled + */ + disabled?: boolean; + + /** + * Specify whether the control is currently invalid + */ + invalid?: boolean; + + /** + * Specify the text to be displayed when the control is invalid + */ + invalidText?: React.ReactNode; + + /** + * Specify if the control should render open + */ + isOpen?: boolean; + + /** + * `true` to use the light version. For use on $ui-01 backgrounds only. + * Don't use this to make tile background color same as container background color. + * + * @deprecated The `light` prop for `ListBox` has been deprecated in favor of + * the new `Layer` component. It will be removed in the next major release. + */ + light?: boolean; + + /** + * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option. + */ + size?: ListBoxSize; + + /** + * Specify the "type" of the ListBox. Currently supports either `default` or + * `inline` as an option. + */ + type?: ListBoxType; + + /** + * Specify whether the control is currently in warning state + */ + warn?: boolean; + + /** + * Provide the text that is displayed when the control is in warning state + */ + warnText?: React.ReactNode; +} + +export type ListBoxComponent = ForwardRefReturn<HTMLDivElement, ListBoxProps> + /** * `ListBox` is a generic container component that handles creating the * container class name in response to certain props. */ -const ListBox = React.forwardRef(function ListBox( +const ListBox: ListBoxComponent = React.forwardRef(function ListBox( { children, className: containerClassName, @@ -47,15 +101,15 @@ const ListBox = React.forwardRef(function ListBox( light, isOpen, ...rest - }, - ref + }: ListBoxProps, + ref: React.LegacyRef<HTMLDivElement> ) { const prefix = usePrefix(); const { isFluid } = useContext(FormContext); const showWarning = !invalid && warn; const className = cx({ - [containerClassName]: !!containerClassName, + ...(containerClassName && {[containerClassName]: true}), [`${prefix}--list-box`]: true, [`${prefix}--list-box--${size}`]: size, [`${prefix}--list-box--inline`]: type === 'inline', @@ -157,10 +211,4 @@ ListBox.defaultProps = { type: 'default', }; -ListBox.Field = ListBoxField; -ListBox.Menu = ListBoxMenu; -ListBox.MenuIcon = ListBoxMenuIcon; -ListBox.MenuItem = ListBoxMenuItem; -ListBox.Selection = ListBoxSelection; - export default ListBox; diff --git a/packages/react/src/components/ListBox/ListBoxField.js b/packages/react/src/components/ListBox/ListBoxField.tsx similarity index 76% rename from packages/react/src/components/ListBox/ListBoxField.js rename to packages/react/src/components/ListBox/ListBoxField.tsx index d81075f17997..47912db6a641 100644 --- a/packages/react/src/components/ListBox/ListBoxField.js +++ b/packages/react/src/components/ListBox/ListBoxField.tsx @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. @@ -8,16 +8,26 @@ import React from 'react'; import PropTypes from 'prop-types'; import { usePrefix } from '../../internal/usePrefix'; +import { ReactAttr } from '../../types/common'; // No longer used, left export for backward-compatibility export const translationIds = {}; +export interface ListBoxFieldProps extends ReactAttr<HTMLDivElement> { + + /** + * Specify if the parent <ListBox> is disabled + */ + disabled?: boolean; + +} + /** * `ListBoxField` is responsible for creating the containing node for valid * elements inside of a field. It also provides a11y-related attributes like * `role` to make sure a user can focus the given field. */ -function ListBoxField({ children, disabled, tabIndex, ...rest }) { +function ListBoxField({ children, disabled, tabIndex, ...rest }: ListBoxFieldProps) { const prefix = usePrefix(); return ( @@ -59,4 +69,6 @@ ListBoxField.propTypes = { tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; -export default ListBoxField; +export type ListBoxFieldComponent = React.FC<ListBoxFieldProps> + +export default ListBoxField as ListBoxFieldComponent; diff --git a/packages/react/src/components/ListBox/ListBoxMenu.js b/packages/react/src/components/ListBox/ListBoxMenu.tsx similarity index 66% rename from packages/react/src/components/ListBox/ListBoxMenu.js rename to packages/react/src/components/ListBox/ListBoxMenu.tsx index ce0ef0d324c5..957f58c142b4 100644 --- a/packages/react/src/components/ListBox/ListBoxMenu.js +++ b/packages/react/src/components/ListBox/ListBoxMenu.tsx @@ -1,23 +1,37 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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 React from 'react'; +import React, { ForwardedRef } from 'react'; import { usePrefix } from '../../internal/usePrefix'; import PropTypes from 'prop-types'; import ListBoxMenuItem from './ListBoxMenuItem'; +import { ForwardRefReturn, ReactAttr } from '../../types/common'; + +type ExcludedAttributes = 'id'; + +export interface ListBoxMenuProps + extends Omit<ReactAttr<HTMLDivElement>, ExcludedAttributes> { + + /** + * Specify a custom `id` + */ + id: string; +} + +export type ListBoxMenuComponent = ForwardRefReturn<HTMLDivElement, ListBoxMenuProps> /** * `ListBoxMenu` is a simple container node that isolates the `list-box__menu` * class into a single component. It is also being used to validate given * `children` components. */ -const ListBoxMenu = React.forwardRef(function ListBoxMenu( - { children, id, ...rest }, - ref +const ListBoxMenu: ListBoxMenuComponent = React.forwardRef(function ListBoxMenu( + { children, id, ...rest }: ListBoxMenuProps, + ref: ForwardedRef<HTMLDivElement>, ) { const prefix = usePrefix(); return ( @@ -39,7 +53,7 @@ ListBoxMenu.propTypes = { */ children: PropTypes.oneOfType([ PropTypes.node, - PropTypes.arrayOf(ListBoxMenuItem), + PropTypes.arrayOf(PropTypes.oneOf([ListBoxMenuItem])), /** * allow single item using the workaround for functional components * https://github.com/facebook/react/issues/2979#issuecomment-222379916 diff --git a/packages/react/src/components/ListBox/ListBoxMenuIcon.js b/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx similarity index 65% rename from packages/react/src/components/ListBox/ListBoxMenuIcon.js rename to packages/react/src/components/ListBox/ListBoxMenuIcon.tsx index a9c4c4c70410..206d5bc4104a 100644 --- a/packages/react/src/components/ListBox/ListBoxMenuIcon.js +++ b/packages/react/src/components/ListBox/ListBoxMenuIcon.tsx @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. @@ -21,11 +21,33 @@ const defaultTranslations = { [translationIds['open.menu']]: 'Open menu', }; +const defaultTranslateWithId = (id: string) => defaultTranslations[id] + +export interface ListBoxMenuIconProps { + /** + * Specify whether the menu is currently open, which will influence the + * direction of the menu icon + */ + isOpen: boolean; + + /** + * i18n hook used to provide the appropriate description for the given menu + * icon. This function takes in an id defined in `translationIds` and should + * return a string message for that given message id. + */ + translateWithId?(messageId: string, args?: Record<string, unknown>): string; +} + +export type ListBoxMenuIconComponent = React.FC<ListBoxMenuIconProps>; + /** * `ListBoxMenuIcon` is used to orient the icon up or down depending on the * state of the menu for a given `ListBox` */ -const ListBoxMenuIcon = ({ isOpen, translateWithId: t }) => { +const ListBoxMenuIcon: ListBoxMenuIconComponent = ({ + isOpen, + translateWithId: t = defaultTranslateWithId, +}: ListBoxMenuIconProps) => { const prefix = usePrefix(); const className = cx(`${prefix}--list-box__menu-icon`, { [`${prefix}--list-box__menu-icon--open`]: isOpen, @@ -56,7 +78,7 @@ ListBoxMenuIcon.propTypes = { }; ListBoxMenuIcon.defaultProps = { - translateWithId: (id) => defaultTranslations[id], + translateWithId: defaultTranslateWithId, }; export default ListBoxMenuIcon; diff --git a/packages/react/src/components/ListBox/ListBoxMenuItem.js b/packages/react/src/components/ListBox/ListBoxMenuItem.tsx similarity index 66% rename from packages/react/src/components/ListBox/ListBoxMenuItem.js rename to packages/react/src/components/ListBox/ListBoxMenuItem.tsx index 8287dcbcc296..0ef179668def 100644 --- a/packages/react/src/components/ListBox/ListBoxMenuItem.js +++ b/packages/react/src/components/ListBox/ListBoxMenuItem.tsx @@ -1,14 +1,15 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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 React, { useEffect, useRef, useState } from 'react'; +import React, { ForwardedRef, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { usePrefix } from '../../internal/usePrefix'; +import { ForwardRefReturn, ReactAttr } from '../../types/common'; function useIsTruncated(ref) { const [isTruncated, setIsTruncated] = useState(false); @@ -21,14 +22,34 @@ function useIsTruncated(ref) { return isTruncated; } +export interface ListBoxMenuItemProps extends ReactAttr<HTMLDivElement> { + + /** + * Specify whether the current menu item is "active". + */ + isActive?: boolean; + + /** + * Specify whether the current menu item is "highlighted". + */ + isHighlighted?: boolean; + +} + +export type ListBoxMenuItemForwardedRef = ForwardedRef<HTMLDivElement> & { + menuItemOptionRef?: React.Ref<HTMLDivElement>; + } | null; + +export type ListBoxMenuItemComponent = ForwardRefReturn<ListBoxMenuItemForwardedRef, ListBoxMenuItemProps>; + /** * `ListBoxMenuItem` is a helper component for managing the container class * name, alongside any classes for any corresponding states, for a generic list * box menu item. */ -const ListBoxMenuItem = React.forwardRef(function ListBoxMenuItem( - { children, isActive, isHighlighted, title, ...rest }, - forwardedRef +const ListBoxMenuItem = React.forwardRef<HTMLDivElement, ListBoxMenuItemProps>(function ListBoxMenuItem( + { children, isActive, isHighlighted, title, ...rest }: ListBoxMenuItemProps, + forwardedRef: ListBoxMenuItemForwardedRef ) { const prefix = usePrefix(); const ref = useRef(null); @@ -43,7 +64,7 @@ const ListBoxMenuItem = React.forwardRef(function ListBoxMenuItem( {...rest} className={className} title={isTruncated ? title : undefined} - tabIndex="-1"> + tabIndex={-1}> <div className={`${prefix}--list-box__menu-item__option`} ref={forwardedRef?.menuItemOptionRef || ref}> @@ -82,4 +103,4 @@ ListBoxMenuItem.defaultProps = { isHighlighted: false, }; -export default ListBoxMenuItem; +export default ListBoxMenuItem as ListBoxMenuItemComponent; diff --git a/packages/react/src/components/ListBox/ListBoxPropTypes.js b/packages/react/src/components/ListBox/ListBoxPropTypes.js deleted file mode 100644 index 767eded95c81..000000000000 --- a/packages/react/src/components/ListBox/ListBoxPropTypes.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * 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 PropTypes from 'prop-types'; - -export const ListBoxType = PropTypes.oneOf(['default', 'inline']); -export const ListBoxSize = PropTypes.oneOf(['sm', 'md', 'lg']); diff --git a/packages/react/src/components/ListBox/ListBoxPropTypes.ts b/packages/react/src/components/ListBox/ListBoxPropTypes.ts new file mode 100644 index 000000000000..5bc72b729fef --- /dev/null +++ b/packages/react/src/components/ListBox/ListBoxPropTypes.ts @@ -0,0 +1,17 @@ +/** + * Copyright IBM Corp. 2016, 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 PropTypes from 'prop-types'; + +const listBoxTypes = ['default', 'inline'] as const; +const listBoxSizes = ['sm', 'md', 'lg'] as const; + +export type ListBoxType = typeof listBoxTypes[number]; +export type ListBoxSize = typeof listBoxSizes[number]; + +export const ListBoxType = PropTypes.oneOf<ListBoxType>(listBoxTypes); +export const ListBoxSize = PropTypes.oneOf<ListBoxSize>(listBoxSizes); diff --git a/packages/react/src/components/ListBox/ListBoxSelection.js b/packages/react/src/components/ListBox/ListBoxSelection.tsx similarity index 68% rename from packages/react/src/components/ListBox/ListBoxSelection.js rename to packages/react/src/components/ListBox/ListBoxSelection.tsx index da6f19a3e12f..f3c32675d41a 100644 --- a/packages/react/src/components/ListBox/ListBoxSelection.js +++ b/packages/react/src/components/ListBox/ListBoxSelection.tsx @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2018 + * Copyright IBM Corp. 2016, 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. @@ -11,26 +11,66 @@ import PropTypes from 'prop-types'; import { Close } from '@carbon/icons-react'; import { match, keys } from '../../internal/keyboard'; import { usePrefix } from '../../internal/usePrefix'; +import { KeyboardEvent, MouseEvent } from 'react'; + +export interface ListBoxSelectionProps { + /** + * Specify a function to be invoked when a user interacts with the clear + * selection element. + */ + clearSelection(event: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>): void; + + /** + * Specify whether or not the clear selection element should be disabled + */ + disabled?: boolean; + + /** + * Specify an optional `onClearSelection` handler that is called when the underlying + * element is cleared + */ + onClearSelection?(event: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>): void; + + /** + * Whether or not the Dropdown is readonly + */ + readOnly?: boolean; + + /** + * Specify an optional `selectionCount` value that will be used to determine + * whether the selection should display a badge or a single clear icon. + */ + selectionCount?: number; + + /** + * i18n hook used to provide the appropriate description for the given menu + * icon. This function takes in an id defined in `translationIds` and should + * return a string message for that given message id. + */ + translateWithId(messageId: string, args?: Record<string, unknown>): string; +} + +export type ListBoxSelectionComponent = React.FC<ListBoxSelectionProps> /** * `ListBoxSelection` is used to provide controls for clearing a selection, in * addition to conditionally rendering a badge if the control has more than one * selection. */ -function ListBoxSelection({ +const ListBoxSelection: ListBoxSelectionComponent = ({ clearSelection, selectionCount, translateWithId: t, disabled, onClearSelection, readOnly, -}) { +}: ListBoxSelectionProps) => { const prefix = usePrefix(); const className = cx(`${prefix}--list-box__selection`, { [`${prefix}--tag--filter`]: selectionCount, [`${prefix}--list-box__selection--multi`]: selectionCount, }); - const handleOnClick = (event) => { + const handleOnClick = (event: MouseEvent<HTMLDivElement>) => { event.stopPropagation(); if (disabled || readOnly) { return; @@ -40,14 +80,14 @@ function ListBoxSelection({ onClearSelection(event); } }; - const handleOnKeyDown = (event) => { + const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { event.stopPropagation(); if (disabled || readOnly) { return; } // When a user hits ENTER, we'll clear the selection - if (match(event, keys.Enter)) { + if (match(event.code, keys.Enter)) { clearSelection(event); if (onClearSelection) { onClearSelection(event); @@ -65,7 +105,7 @@ function ListBoxSelection({ ); return selectionCount ? ( <div className={tagClasses}> - <span className={`${prefix}--tag__label`} title={selectionCount}> + <span className={`${prefix}--tag__label`} title={`${selectionCount}`}> {selectionCount} </span> <div @@ -74,7 +114,6 @@ function ListBoxSelection({ className={`${prefix}--tag__close-icon`} onClick={handleOnClick} onKeyDown={handleOnKeyDown} - disabled={disabled} aria-label={t('clear.all')} title={description} aria-disabled={readOnly ? true : undefined}> @@ -124,18 +163,6 @@ ListBoxSelection.propTypes = { */ onClearSelection: PropTypes.func, - /** - * Specify an optional `onClick` handler that is called when the underlying - * clear selection element is clicked - */ - onClick: PropTypes.func, - - /** - * Specify an optional `onKeyDown` handler that is called when the underlying - * clear selection element fires a keydown event - */ - onKeyDown: PropTypes.func, - /** * Whether or not the Dropdown is readonly */ @@ -156,7 +183,7 @@ ListBoxSelection.propTypes = { }; ListBoxSelection.defaultProps = { - translateWithId: (id) => defaultTranslations[id], + translateWithId: (id: string) => defaultTranslations[id], }; export default ListBoxSelection; diff --git a/packages/react/src/components/ListBox/index.js b/packages/react/src/components/ListBox/index.js deleted file mode 100644 index 1bf90ffcfcac..000000000000 --- a/packages/react/src/components/ListBox/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -export * as PropTypes from './ListBoxPropTypes'; - -import ListBox from './ListBox'; -import ListBoxField from './ListBoxField'; -import ListBoxMenu from './ListBoxMenu'; -import ListBoxMenuIcon from './ListBoxMenuIcon'; -import ListBoxMenuItem from './ListBoxMenuItem'; -import ListBoxSelection from './ListBoxSelection'; - -ListBox.Field = ListBoxField; -ListBox.Menu = ListBoxMenu; -ListBox.MenuIcon = ListBoxMenuIcon; -ListBox.MenuItem = ListBoxMenuItem; -ListBox.Selection = ListBoxSelection; - -export default ListBox; diff --git a/packages/react/src/components/ListBox/index.ts b/packages/react/src/components/ListBox/index.ts new file mode 100644 index 000000000000..321bc9dd5374 --- /dev/null +++ b/packages/react/src/components/ListBox/index.ts @@ -0,0 +1,38 @@ +/** + * Copyright IBM Corp. 2016, 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. + */ + +export * as PropTypes from './ListBoxPropTypes'; +export * from './ListBoxPropTypes'; + +import ListBoxInternal, { + ListBoxComponent as ListBoxPartialComponent, +} from './ListBox'; +import ListBoxField, { ListBoxFieldComponent } from './ListBoxField'; +import ListBoxMenu, { ListBoxMenuComponent } from './ListBoxMenu'; +import ListBoxMenuIcon, { ListBoxMenuIconComponent } from './ListBoxMenuIcon'; +import ListBoxMenuItem, { ListBoxMenuItemComponent } from './ListBoxMenuItem'; +import ListBoxSelection, { + ListBoxSelectionComponent, +} from './ListBoxSelection'; + +export interface ListBoxComponent extends ListBoxPartialComponent { + readonly Field: ListBoxFieldComponent; + readonly Menu: ListBoxMenuComponent; + readonly MenuIcon: ListBoxMenuIconComponent; + readonly MenuItem: ListBoxMenuItemComponent; + readonly Selection: ListBoxSelectionComponent; +} + +const ListBox: ListBoxComponent = Object.assign(ListBoxInternal, { + Field: ListBoxField, + Menu: ListBoxMenu, + MenuIcon: ListBoxMenuIcon, + MenuItem: ListBoxMenuItem, + Selection: ListBoxSelection, +}); + +export default ListBox;