From bc3efbfe06f321032ce62f7c83fdb203daf767d0 Mon Sep 17 00:00:00 2001 From: Pavel Klibani Date: Wed, 29 Nov 2023 21:29:53 +0100 Subject: [PATCH 1/2] Fix(web-react): Tablink and HeaderLink now accept a NextLink #DS-1018 - TabLink and HeaderLink now accept a NextLink and have forwardRef support --- .../src/components/Header/HeaderLink.tsx | 25 +++++++--- .../web-react/src/components/Header/README.md | 46 +++++++++++++------ .../Header/__tests__/HeaderLink.test.tsx | 11 +++++ .../web-react/src/components/Tabs/README.md | 10 ++-- .../web-react/src/components/Tabs/TabLink.tsx | 22 ++++----- .../Tabs/__tests__/TabLink.test.tsx | 11 +++++ .../Tabs/stories/TabLink.stories.tsx | 4 +- packages/web-react/src/types/header.ts | 17 ++++++- packages/web-react/src/types/tabs.ts | 20 ++++++++ 9 files changed, 126 insertions(+), 40 deletions(-) diff --git a/packages/web-react/src/components/Header/HeaderLink.tsx b/packages/web-react/src/components/Header/HeaderLink.tsx index 377615aab5..cc3afc69ed 100644 --- a/packages/web-react/src/components/Header/HeaderLink.tsx +++ b/packages/web-react/src/components/Header/HeaderLink.tsx @@ -1,20 +1,31 @@ -import React from 'react'; +import React, { ElementType, forwardRef } from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { HeaderLinkProps } from '../../types'; +import { PolymorphicRef, SpiritHeaderLinkProps } from '../../types'; import { useHeaderStyleProps } from './useHeaderStyleProps'; -const HeaderLink = (props: HeaderLinkProps) => { - const { children, isCurrent, ...restProps } = props; - +/* We need an exception for components exported with forwardRef */ +/* eslint no-underscore-dangle: ['error', { allow: ['_HeaderLink'] }] */ +const _HeaderLink = ( + props: SpiritHeaderLinkProps, + ref: PolymorphicRef, +): JSX.Element => { + const { elementType: ElementTag = 'a', children, isCurrent, ...restProps } = props; const { classProps } = useHeaderStyleProps({ isCurrentLink: isCurrent }); const { styleProps, props: otherProps } = useStyleProps(restProps); return ( - + {children} - + ); }; +export const HeaderLink = forwardRef>(_HeaderLink); + export default HeaderLink; diff --git a/packages/web-react/src/components/Header/README.md b/packages/web-react/src/components/Header/README.md index 0284ec350e..5e780c62d4 100644 --- a/packages/web-react/src/components/Header/README.md +++ b/packages/web-react/src/components/Header/README.md @@ -6,19 +6,38 @@ your specific design goals. The Header is a composition of several subcomponents: -- [Header](#minimal-header) - - [HeaderMobileActions](#mobile-only-actions) - - [HeaderDesktopActions](#desktop-only-actions) - - [HeaderNav](#navigation) - - [HeaderNavItem](#navigation) - - [HeaderLink](#navigation) -- [HeaderDialog](#header-dialog) - - [HeaderDialogCloseButton](#close-button) - - [HeaderDialogActions](#primary-and-secondary-actions) - - [HeaderDialogNav](#navigation-1) - - [HeaderDialogNavItem](#navigation-1) - - [HeaderDialogLink](#navigation-1) - - [HeaderDialogText](#navigation-1) +- [Header and HeaderDialog](#header-and-headerdialog) + - [Accessibility Guidelines](#accessibility-guidelines) + - [Minimal Header](#minimal-header) + - [Color Variants](#color-variants) + - [Simple Header](#simple-header) + - [API](#api) + - [Supported Content](#supported-content) + - [Header](#header) + - [Mobile-Only Actions](#mobile-only-actions) + - [Custom Mobile Actions](#custom-mobile-actions) + - [API](#api-1) + - [Desktop-Only Actions](#desktop-only-actions) + - [API](#api-2) + - [Navigation](#navigation) + - [Other Content](#other-content) + - [HeaderNav API](#headernav-api) + - [HeaderNavItem API](#headernavitem-api) + - [HeaderLink API](#headerlink-api) + - [HeaderButton API](#headerbutton-api) + - [Header Dialog](#header-dialog) + - [API](#api-3) + - [Close Button](#close-button) + - [API](#api-4) + - [Primary and Secondary Actions](#primary-and-secondary-actions) + - [API](#api-5) + - [Navigation](#navigation-1) + - [HeaderDialogNav API](#headerdialognav-api) + - [HeaderDialogNavItem API](#headerdialognavitem-api) + - [HeaderDialogLink API](#headerdialoglink-api) + - [HeaderDialogButton API](#headerdialogbutton-api) + - [HeaderDialogText API](#headerdialogtext-api) + - [Composition](#composition) ## Accessibility Guidelines @@ -243,6 +262,7 @@ The component further inherits properties from the [`
  • `][mdn-li-element] elem | Name | Type | Default | Required | Description | | ------------------ | --------------- | ------- | -------- | -------------------- | | `children` | `ReactNode` | — | ✕ | Children node | +| `elementType` | `ElementType` | `a` | ✕ | Type of element | | `isCurrent` | `bool` | `false` | ✕ | Mark link as current | | `UNSAFE_className` | `string` | — | ✕ | Custom class name | | `UNSAFE_style` | `CSSProperties` | — | ✕ | Custom style | diff --git a/packages/web-react/src/components/Header/__tests__/HeaderLink.test.tsx b/packages/web-react/src/components/Header/__tests__/HeaderLink.test.tsx index c97059f26c..12af786ec4 100644 --- a/packages/web-react/src/components/Header/__tests__/HeaderLink.test.tsx +++ b/packages/web-react/src/components/Header/__tests__/HeaderLink.test.tsx @@ -19,4 +19,15 @@ describe('HeaderLink', () => { const element = dom.container.querySelector('a') as HTMLElement; expect(element.textContent).toBe('Hello World'); }); + + it('should render button element', () => { + const dom = render( + + Hello World + , + ); + + const element = dom.container.querySelector('button') as HTMLElement; + expect(element.textContent).toBe('Hello World'); + }); }); diff --git a/packages/web-react/src/components/Tabs/README.md b/packages/web-react/src/components/Tabs/README.md index ca5f450eae..d887da7886 100644 --- a/packages/web-react/src/components/Tabs/README.md +++ b/packages/web-react/src/components/Tabs/README.md @@ -114,11 +114,11 @@ Tab list link #### API -| Name | Type | Default | Required | Description | -| ----------- | ---------------------------- | ------- | -------- | ----------------------------- | -| `children` | `any` | — | ✕ | Child component | -| `href` | `string` | — | ✔ | External link | -| `itemProps` | `StyleProps & HTMLLIElement` | — | ✕ | Props for parent list element | +| Name | Type | Default | Required | Description | +| ------------- | ---------------------------- | ------- | -------- | ----------------------------- | +| `children` | `any` | — | ✕ | Child component | +| `elementType` | `ElementType` | `a` | ✕ | Type of element | +| `itemProps` | `StyleProps & HTMLLIElement` | — | ✕ | Props for parent list element | ### TabContent diff --git a/packages/web-react/src/components/Tabs/TabLink.tsx b/packages/web-react/src/components/Tabs/TabLink.tsx index 5e0ecd46a5..4ebb4f52d0 100644 --- a/packages/web-react/src/components/Tabs/TabLink.tsx +++ b/packages/web-react/src/components/Tabs/TabLink.tsx @@ -1,29 +1,27 @@ -import React from 'react'; +import React, { ElementType, forwardRef } from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { ChildrenProps, StyleProps } from '../../types'; +import { PolymorphicRef, SpiritTabLinkProps } from '../../types'; import { useTabsStyleProps } from './useTabsStyleProps'; -export type TabLinkItemProps = StyleProps & React.HTMLProps; - -export interface TabLinkProps extends ChildrenProps { - href: string; - itemProps?: TabLinkItemProps; -} - -const TabLink = ({ children, href, itemProps = {}, ...restProps }: TabLinkProps): JSX.Element => { +/* We need an exception for components exported with forwardRef */ +/* eslint no-underscore-dangle: ['error', { allow: ['_TabLink'] }] */ +const _TabLink = (props: SpiritTabLinkProps, ref: PolymorphicRef): JSX.Element => { + const { elementType: ElementTag = 'a', children, itemProps = {}, ...restProps } = props; const { classProps } = useTabsStyleProps(); const { styleProps: itemStyleProps, props: itemTransferProps } = useStyleProps(itemProps); return (
  • - + {children} - +
  • ); }; +export const TabLink = forwardRef>(_TabLink); + TabLink.defaultProps = { itemProps: {}, }; diff --git a/packages/web-react/src/components/Tabs/__tests__/TabLink.test.tsx b/packages/web-react/src/components/Tabs/__tests__/TabLink.test.tsx index 3f6cb316fa..d471dd732f 100644 --- a/packages/web-react/src/components/Tabs/__tests__/TabLink.test.tsx +++ b/packages/web-react/src/components/Tabs/__tests__/TabLink.test.tsx @@ -16,4 +16,15 @@ describe('TabLink', () => { const element = dom.container.querySelector('a') as HTMLElement; expect(element).toHaveClass('Tabs__link'); }); + + it('should render button element', () => { + const dom = render( + + Hello World + , + ); + + const element = dom.container.querySelector('button') as HTMLElement; + expect(element.textContent).toBe('Hello World'); + }); }); diff --git a/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx b/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx index 2fdd2cefe7..af0739e5d3 100644 --- a/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx +++ b/packages/web-react/src/components/Tabs/stories/TabLink.stories.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { TabId } from '../../../types'; -import { TabContent, TabItem, TabLink, TabLinkProps, TabList, TabPane, Tabs } from '..'; +import { TabId, TabLinkProps } from '../../../types'; +import { TabContent, TabItem, TabLink, TabList, TabPane, Tabs } from '..'; const meta: Meta = { title: 'Components/Tabs', diff --git a/packages/web-react/src/types/header.ts b/packages/web-react/src/types/header.ts index c589a57f7b..d642fe3279 100644 --- a/packages/web-react/src/types/header.ts +++ b/packages/web-react/src/types/header.ts @@ -1,3 +1,4 @@ +import { ElementType } from 'react'; import { ChildrenProps, ClickEvent, @@ -6,8 +7,11 @@ import { SpiritDialogElementProps, SpiritElementProps, SpiritLItemElementProps, + SpiritPolymorphicElementPropsWithRef, SpiritSpanElementProps, SpiritUListElementProps, + StyleProps, + TransferProps, } from './shared'; export type HeaderActionsColorType = 'primary' | 'secondary'; @@ -58,10 +62,21 @@ export interface HeaderDialogNavItemProps extends SpiritLItemElementProps, Child export interface HeaderDialogTextProps extends SpiritSpanElementProps, ChildrenProps {} -export interface HeaderLinkProps extends SpiritAnchorElementProps, ChildrenProps { +export interface HeaderLinkBaseProps extends ChildrenProps, StyleProps, TransferProps { isCurrent?: boolean; } +export type HeaderLinkProps = { + /** + * The HTML element or React element used to render the Link, e.g. 'a'. + * @default 'a' + */ + elementType?: E; +} & HeaderLinkBaseProps; + +export type SpiritHeaderLinkProps = HeaderLinkProps & + SpiritPolymorphicElementPropsWithRef>; + export interface HeaderMobileActionsProps extends SpiritElementProps, HeaderMobileActionsHandlingProps, ChildrenProps { dialogId: string; menuToggleLabel?: string; diff --git a/packages/web-react/src/types/tabs.ts b/packages/web-react/src/types/tabs.ts index 9e99ee9c5f..2b617601a6 100644 --- a/packages/web-react/src/types/tabs.ts +++ b/packages/web-react/src/types/tabs.ts @@ -1,3 +1,6 @@ +import { ElementType } from 'react'; +import { ChildrenProps, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; + export type TabId = string | number; export interface SpiritTabsProps { @@ -8,3 +11,20 @@ export interface SpiritTabsProps { /** Identification of affected pane */ forTab?: TabId; } + +export type TabLinkItemProps = StyleProps & React.HTMLProps; + +export interface TabLinkBaseProps extends ChildrenProps, StyleProps, TransferProps { + itemProps?: TabLinkItemProps; +} + +export type TabLinkProps = { + /** + * The HTML element or React element used to render the Link, e.g. 'a'. + * @default 'a' + */ + elementType?: E; +} & TabLinkBaseProps; + +export type SpiritTabLinkProps = TabLinkProps & + SpiritPolymorphicElementPropsWithRef>; From f6a0862208aaebfbcb3da7e1f2dc2760aa05f278 Mon Sep 17 00:00:00 2001 From: Pavel Klibani Date: Wed, 29 Nov 2023 23:25:16 +0100 Subject: [PATCH 2/2] Fix(web-react): HeaderDialogLink now accept a NextLink #DS-1003 --- .../components/Header/HeaderDialogLink.tsx | 23 +++++++--- .../web-react/src/components/Header/README.md | 46 ++++++------------- .../__tests__/HeaderDialogLink.test.tsx | 11 +++++ packages/web-react/src/types/header.ts | 14 +++++- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/packages/web-react/src/components/Header/HeaderDialogLink.tsx b/packages/web-react/src/components/Header/HeaderDialogLink.tsx index 2785f50610..07a70a61b5 100644 --- a/packages/web-react/src/components/Header/HeaderDialogLink.tsx +++ b/packages/web-react/src/components/Header/HeaderDialogLink.tsx @@ -1,24 +1,33 @@ -import React from 'react'; +import React, { ElementType, forwardRef } from 'react'; import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; -import { HeaderDialogLinkProps } from '../../types'; +import { PolymorphicRef, SpiritDialogHeaderLinkProps } from '../../types'; import { useHeaderStyleProps } from './useHeaderStyleProps'; -const HeaderDialogLink = (props: HeaderDialogLinkProps) => { - const { children, isCurrent, ...restProps } = props; - +/* We need an exception for components exported with forwardRef */ +/* eslint no-underscore-dangle: ['error', { allow: ['_HeaderDialogLink'] }] */ +const _HeaderDialogLink = ( + props: SpiritDialogHeaderLinkProps, + ref: PolymorphicRef, +): JSX.Element => { + const { elementType: ElementTag = 'a', children, isCurrent, ...restProps } = props; const { classProps } = useHeaderStyleProps({ isCurrentLink: isCurrent }); const { styleProps, props: otherProps } = useStyleProps(restProps); return ( - {children} - + ); }; +export const HeaderDialogLink = forwardRef>( + _HeaderDialogLink, +); + export default HeaderDialogLink; diff --git a/packages/web-react/src/components/Header/README.md b/packages/web-react/src/components/Header/README.md index 5e780c62d4..c909713725 100644 --- a/packages/web-react/src/components/Header/README.md +++ b/packages/web-react/src/components/Header/README.md @@ -6,38 +6,19 @@ your specific design goals. The Header is a composition of several subcomponents: -- [Header and HeaderDialog](#header-and-headerdialog) - - [Accessibility Guidelines](#accessibility-guidelines) - - [Minimal Header](#minimal-header) - - [Color Variants](#color-variants) - - [Simple Header](#simple-header) - - [API](#api) - - [Supported Content](#supported-content) - - [Header](#header) - - [Mobile-Only Actions](#mobile-only-actions) - - [Custom Mobile Actions](#custom-mobile-actions) - - [API](#api-1) - - [Desktop-Only Actions](#desktop-only-actions) - - [API](#api-2) - - [Navigation](#navigation) - - [Other Content](#other-content) - - [HeaderNav API](#headernav-api) - - [HeaderNavItem API](#headernavitem-api) - - [HeaderLink API](#headerlink-api) - - [HeaderButton API](#headerbutton-api) - - [Header Dialog](#header-dialog) - - [API](#api-3) - - [Close Button](#close-button) - - [API](#api-4) - - [Primary and Secondary Actions](#primary-and-secondary-actions) - - [API](#api-5) - - [Navigation](#navigation-1) - - [HeaderDialogNav API](#headerdialognav-api) - - [HeaderDialogNavItem API](#headerdialognavitem-api) - - [HeaderDialogLink API](#headerdialoglink-api) - - [HeaderDialogButton API](#headerdialogbutton-api) - - [HeaderDialogText API](#headerdialogtext-api) - - [Composition](#composition) +- [Header](#minimal-header) + - [HeaderMobileActions](#mobile-only-actions) + - [HeaderDesktopActions](#desktop-only-actions) + - [HeaderNav](#navigation) + - [HeaderNavItem](#navigation) + - [HeaderLink](#navigation) +- [HeaderDialog](#header-dialog) + - [HeaderDialogCloseButton](#close-button) + - [HeaderDialogActions](#primary-and-secondary-actions) + - [HeaderDialogNav](#navigation-1) + - [HeaderDialogNavItem](#navigation-1) + - [HeaderDialogLink](#navigation-1) + - [HeaderDialogText](#navigation-1) ## Accessibility Guidelines @@ -418,6 +399,7 @@ The component further inherits properties from the [`
  • `][mdn-li-element] elem | Name | Type | Default | Required | Description | | ------------------ | --------------- | ------- | -------- | -------------------- | | `children` | `ReactNode` | — | ✕ | Children node | +| `elementType` | `ElementType` | `a` | ✕ | Type of element | | `isCurrent` | `bool` | `false` | ✕ | Mark link as current | | `UNSAFE_className` | `string` | — | ✕ | Custom class name | | `UNSAFE_style` | `CSSProperties` | — | ✕ | Custom style | diff --git a/packages/web-react/src/components/Header/__tests__/HeaderDialogLink.test.tsx b/packages/web-react/src/components/Header/__tests__/HeaderDialogLink.test.tsx index 0ad904ecd9..bfe5787a31 100644 --- a/packages/web-react/src/components/Header/__tests__/HeaderDialogLink.test.tsx +++ b/packages/web-react/src/components/Header/__tests__/HeaderDialogLink.test.tsx @@ -22,4 +22,15 @@ describe('HeaderDialogLink', () => { const element = dom.container.querySelector('a') as HTMLElement; expect(element.textContent).toBe('Hello World'); }); + + it('should render button element', () => { + const dom = render( + + Hello World + , + ); + + const element = dom.container.querySelector('button') as HTMLElement; + expect(element.textContent).toBe('Hello World'); + }); }); diff --git a/packages/web-react/src/types/header.ts b/packages/web-react/src/types/header.ts index d642fe3279..a68fc98b5f 100644 --- a/packages/web-react/src/types/header.ts +++ b/packages/web-react/src/types/header.ts @@ -2,7 +2,6 @@ import { ElementType } from 'react'; import { ChildrenProps, ClickEvent, - SpiritAnchorElementProps, SpiritButtonElementProps, SpiritDialogElementProps, SpiritElementProps, @@ -52,10 +51,21 @@ export interface HeaderDialogCloseButtonProps extends Omit = { + /** + * The HTML element or React element used to render the Link, e.g. 'a'. + * @default 'a' + */ + elementType?: E; +} & BaseHeaderDialogLinkProps; + +export type SpiritDialogHeaderLinkProps = HeaderDialogLinkProps & + SpiritPolymorphicElementPropsWithRef>; + export interface HeaderDialogNavProps extends SpiritUListElementProps, ChildrenProps {} export interface HeaderDialogNavItemProps extends SpiritLItemElementProps, ChildrenProps {}