diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 191aab706618..e19028fd0965 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -3913,6 +3913,9 @@ Map { "HeaderMenuItem" => Object { "$$typeof": Symbol(react.forward_ref), "propTypes": Object { + "as": Object { + "type": "elementType", + }, "children": Object { "isRequired": true, "type": "node", @@ -3920,9 +3923,7 @@ Map { "className": Object { "type": "string", }, - "element": Object { - "type": "elementType", - }, + "element": [Function], "isActive": Object { "type": "bool", }, @@ -3944,6 +3945,9 @@ Map { "prefix": "IBM", }, "propTypes": Object { + "as": Object { + "type": "elementType", + }, "children": Object { "isRequired": true, "type": "node", @@ -3951,9 +3955,7 @@ Map { "className": Object { "type": "string", }, - "element": Object { - "type": "elementType", - }, + "element": [Function], "href": Object { "type": "string", }, @@ -6993,6 +6995,9 @@ Map { "SideNavLink" => Object { "$$typeof": Symbol(react.forward_ref), "propTypes": Object { + "as": Object { + "type": "elementType", + }, "children": Object { "isRequired": true, "type": "node", @@ -7000,9 +7005,7 @@ Map { "className": Object { "type": "string", }, - "element": Object { - "type": "elementType", - }, + "element": [Function], "isActive": Object { "type": "bool", }, diff --git a/packages/react/src/components/UIShell/HeaderMenuItem.js b/packages/react/src/components/UIShell/HeaderMenuItem.tsx similarity index 73% rename from packages/react/src/components/UIShell/HeaderMenuItem.js rename to packages/react/src/components/UIShell/HeaderMenuItem.tsx index 998734c25c16..d5f968febd3a 100644 --- a/packages/react/src/components/UIShell/HeaderMenuItem.js +++ b/packages/react/src/components/UIShell/HeaderMenuItem.tsx @@ -6,13 +6,30 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { + type ComponentProps, + type ForwardedRef, + forwardRef, + type ReactNode, + ElementType, + WeakValidationMap, +} from 'react'; import cx from 'classnames'; -import Link, { LinkPropTypes } from './Link'; +import Link, { LinkProps, LinkPropTypes } from './Link'; import { usePrefix } from '../../internal/usePrefix'; import deprecate from '../../prop-types/deprecate'; -const HeaderMenuItem = React.forwardRef(function HeaderMenuItem( +type HeaderMenuItemProps = LinkProps & { + className?: string | undefined; + isActive?: boolean | undefined; + isCurrentPage?: boolean | undefined; + 'aria-current'?: string | undefined; + children: ReactNode; + role?: ComponentProps<'li'>['role']; + tabIndex?: number | undefined; +}; + +function HeaderMenuItemRenderFunction( { className, isActive, @@ -22,8 +39,8 @@ const HeaderMenuItem = React.forwardRef(function HeaderMenuItem( role, tabIndex = 0, ...rest - }, - ref + }: HeaderMenuItemProps, + ref: ForwardedRef ) { const prefix = usePrefix(); if (isCurrentPage) { @@ -36,7 +53,6 @@ const HeaderMenuItem = React.forwardRef(function HeaderMenuItem( [`${prefix}--header__menu-item--current`]: isActive && ariaCurrent !== 'page', }); - return (
  • ); -}); +} + +const HeaderMenuItem = forwardRef(HeaderMenuItemRenderFunction) as (< + E extends ElementType = 'a' +>( + props: HeaderMenuItemProps +) => JSX.Element) & { + displayName?: string; + propTypes?: WeakValidationMap>; +}; HeaderMenuItem.displayName = 'HeaderMenuItem'; HeaderMenuItem.propTypes = { diff --git a/packages/react/src/components/UIShell/Link.js b/packages/react/src/components/UIShell/Link.js deleted file mode 100644 index 24e1863ebf2f..000000000000 --- a/packages/react/src/components/UIShell/Link.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * 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'; -import React from 'react'; - -/** - * Link is a custom component that allows us to supporting rendering elements - * other than `a` in our markup. The goal is to allow users to support passing - * in their own components to support use-cases like `react-router` or - * `@reach/router` - */ -const Link = React.forwardRef(function Link(props, ref) { - const { - element, - isSideNavExpanded, // eslint-disable-line no-unused-vars - ...rest - } = props; - return React.createElement(element, { ...rest, ref }); -}); - -const LinkPropTypes = { - /** - * The base element to use to build the link. Defaults to `a`, can also accept - * alternative tag names or custom components like `Link` from `react-router`. - */ - element: PropTypes.elementType, - - /** - * Property to indicate if the side nav container is open (or not). Use to - * keep local state and styling in step with the SideNav expansion state. - */ - isSideNavExpanded: PropTypes.bool, -}; - -Link.displayName = 'Link'; -Link.propTypes = LinkPropTypes; -Link.defaultProps = { - element: 'a', -}; - -export { LinkPropTypes }; -export default Link; diff --git a/packages/react/src/components/UIShell/Link.tsx b/packages/react/src/components/UIShell/Link.tsx new file mode 100644 index 000000000000..b7d32d62ff4a --- /dev/null +++ b/packages/react/src/components/UIShell/Link.tsx @@ -0,0 +1,93 @@ +/** + * 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'; +import React, { + type ElementType, + type ForwardedRef, + forwardRef, + type WeakValidationMap, + type Ref, +} from 'react'; +import deprecate from '../../prop-types/deprecate'; +import { type PolymorphicProps } from '../../types/common'; + +// Note: Maybe we should use `as` instead of `element`? `as` appears to be +// standard and is used in other places in this project. + +type LinkBaseProps = { + /** + * @deprecated Use `as` instead + */ + element?: E | undefined; + ref?: Ref; +}; + +export type LinkProps = PolymorphicProps< + E, + LinkBaseProps +>; + +function LinkRenderFunction( + { + element, + as: BaseComponent, + // Captured here to prevent it from being passed into the created element. + // See https://github.com/carbon-design-system/carbon/issues/3970 + isSideNavExpanded: _isSideNavExpanded, + ...rest + }: LinkProps, + ref: ForwardedRef +) { + const BaseComponentAsAny = (BaseComponent ?? element ?? 'a') as any; + return ; +} + +/** + * Link is a custom component that allows us to supporting rendering elements + * other than `a` in our markup. The goal is to allow users to support passing + * in their own components to support use-cases like `react-router` or + * `@reach/router` + */ +const Link = forwardRef(LinkRenderFunction) as (( + props: LinkProps +) => JSX.Element) & { + displayName?: string; + propTypes?: WeakValidationMap>; +}; + +const LinkPropTypes = { + /** + * Provide a custom element or component to render the top-level node for the + * component. + */ + as: PropTypes.elementType, + + /** + * The base element to use to build the link. Defaults to `a`, can also accept + * alternative tag names or custom components like `Link` from `react-router`. + * @deprecated Use `as` instead + * + */ + element: deprecate( + PropTypes.elementType, + 'The `element` prop for `Link` has been deprecated. Please use `as` ' + + 'instead. This will be removed in the next major release.' + ), + + /** + * Property to indicate if the side nav container is open (or not). Use to + * keep local state and styling in step with the SideNav expansion state. + */ + isSideNavExpanded: PropTypes.bool, +}; + +Link.displayName = 'Link'; +Link.propTypes = LinkPropTypes; + +export { LinkPropTypes }; +export default Link;