From 24b32c62cfab845680766fa297dfd8a3ed08c232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C4=8Curda?= Date: Mon, 2 Dec 2024 11:27:20 +0100 Subject: [PATCH] Feat(web-react): Introduce UNSTABLE_Header #DS-1524 --- .../src/components/UNSTABLE_Header/README.md | 45 +++++++++++++++ .../UNSTABLE_Header/UNSTABLE_Header.tsx | 28 +++++++++ .../UNSTABLE_Header/UNSTABLE_HeaderLogo.tsx | 43 ++++++++++++++ .../__tests__/UNSTABLE_Header.test.tsx | 23 ++++++++ .../__tests__/UNSTABLE_HeaderLogo.test.tsx | 24 ++++++++ .../__tests__/useHeaderStyleProps.test.ts | 12 ++++ .../UNSTABLE_Header/demo/HeaderDefault.tsx | 22 +++++++ .../UNSTABLE_Header/demo/HeaderFluid.tsx | 23 ++++++++ .../UNSTABLE_Header/demo/HeaderMinimal.tsx | 20 +++++++ .../demo/HeaderWithNavigation.tsx | 57 +++++++++++++++++++ .../components/UNSTABLE_Header/demo/index.tsx | 32 +++++++++++ .../src/components/UNSTABLE_Header/index.html | 1 + .../src/components/UNSTABLE_Header/index.ts | 5 ++ .../stories/UNSTABLE_Header.stories.tsx | 48 ++++++++++++++++ .../stories/UNSTABLE_HeaderLogo.stories.tsx | 27 +++++++++ .../UNSTABLE_Header/useHeaderStyleProps.ts | 23 ++++++++ packages/web-react/src/types/index.ts | 1 + .../web-react/src/types/unstableHeader.ts | 28 +++++++++ 18 files changed, 462 insertions(+) create mode 100644 packages/web-react/src/components/UNSTABLE_Header/README.md create mode 100644 packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_Header.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_HeaderLogo.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_Header.test.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_HeaderLogo.test.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/__tests__/useHeaderStyleProps.test.ts create mode 100644 packages/web-react/src/components/UNSTABLE_Header/demo/HeaderDefault.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/demo/HeaderFluid.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/demo/HeaderMinimal.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/demo/HeaderWithNavigation.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/demo/index.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/index.html create mode 100644 packages/web-react/src/components/UNSTABLE_Header/index.ts create mode 100644 packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_Header.stories.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_HeaderLogo.stories.tsx create mode 100644 packages/web-react/src/components/UNSTABLE_Header/useHeaderStyleProps.ts create mode 100644 packages/web-react/src/types/unstableHeader.ts diff --git a/packages/web-react/src/components/UNSTABLE_Header/README.md b/packages/web-react/src/components/UNSTABLE_Header/README.md new file mode 100644 index 0000000000..bb23846e62 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/README.md @@ -0,0 +1,45 @@ +# UNSTABLE Header + +> ⚠️ This component is UNSTABLE. It may significantly change at any point in the future. +> Please use it with caution. + +```jsx +import { UNSTABLE_Header } from '@lmc-eu/spirit-web-react'; + +{/* Content go here */}; +``` + +## Header + +Without any modifier, Header is ready to contain necessary blocks in a classic +left-to-right layout (in LTR documents). + +```jsx + + + + + + Spirit + + + + + +``` + +## API + +| Name | Type | Default | Required | Description | +| ---------- | ----------------------- | ------- | -------- | -------------------------------------------- | +| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the UNSTABLE_Header | +| `isFluid` | `boolean` | `false` | ✕ | UNSTABLE_Header without container | +| `isSimple` | `bool` | `false` | ✕ | Shorter, centered version of UNSTABLE_Header | + +The components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes +[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches +[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props diff --git a/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_Header.tsx b/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_Header.tsx new file mode 100644 index 0000000000..f632efc958 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_Header.tsx @@ -0,0 +1,28 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritHeaderProps } from '../../types/unstableHeader'; +import { useHeaderStyleProps } from './useHeaderStyleProps'; + +const defaultProps: Partial = { + isFluid: false, + isSimple: false, +}; + +const UNSTABLE_Header = (props: SpiritHeaderProps): JSX.Element => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { children, ...restProps } = propsWithDefaults; + + const { classProps, props: modifiedProps } = useHeaderStyleProps(restProps); + const { styleProps, props: otherProps } = useStyleProps(modifiedProps); + + return ( +
+ {children} +
+ ); +}; + +export default UNSTABLE_Header; diff --git a/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_HeaderLogo.tsx b/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_HeaderLogo.tsx new file mode 100644 index 0000000000..925ac9738e --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/UNSTABLE_HeaderLogo.tsx @@ -0,0 +1,43 @@ +'use client'; + +import classNames from 'classnames'; +import React, { ElementType, forwardRef } from 'react'; +import { useStyleProps } from '../../hooks'; +import { PolymorphicRef, SpiritHeaderLogoProps } from '../../types'; +import { useHeaderStyleProps } from './useHeaderStyleProps'; + +const defaultProps: Partial = { + elementType: 'a', +}; + +/* We need an exception for components exported with forwardRef */ +/* eslint no-underscore-dangle: ['error', { allow: ['_HeaderLogo'] }] */ +const _HeaderLogo = ( + props: SpiritHeaderLogoProps, + ref: PolymorphicRef, +): JSX.Element => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { + elementType: ElementTag = defaultProps.elementType as ElementType, + children, + ...restProps + } = propsWithDefaults; + const { classProps, props: modifiedProps } = useHeaderStyleProps(restProps); + const { styleProps, props: otherProps } = useStyleProps(modifiedProps); + + return ( + + {children} + + ); +}; + +const UNSTABLE_HeaderLogo = forwardRef>(_HeaderLogo); + +export default UNSTABLE_HeaderLogo; diff --git a/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_Header.test.tsx b/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_Header.test.tsx new file mode 100644 index 0000000000..29194763ce --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_Header.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import UNSTABLE_Header from '../UNSTABLE_Header'; + +describe('UNSTABLE_Header', () => { + classNamePrefixProviderTest(UNSTABLE_Header, 'UNSTABLE_Header'); + + stylePropsTest(UNSTABLE_Header); + + restPropsTest(UNSTABLE_Header, 'header'); + + it('should have default classname', () => { + render(Content); + + const header = screen.getByRole('banner'); + + expect(header).toHaveClass('UNSTABLE_Header'); + }); +}); diff --git a/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_HeaderLogo.test.tsx b/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_HeaderLogo.test.tsx new file mode 100644 index 0000000000..e6dbf01e78 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/__tests__/UNSTABLE_HeaderLogo.test.tsx @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +describe('UNSTABLE_HeaderLogo', () => { + stylePropsTest(UNSTABLE_HeaderLogo); + + restPropsTest(UNSTABLE_HeaderLogo, 'a'); + + it('should have default classname', () => { + render(Content); + + expect(screen.getByRole('link')).toHaveClass('UNSTABLE_HeaderLogo'); + }); + + it('should render children', () => { + render(Content); + + expect(screen.getByText('Content')).toBeInTheDocument(); + }); +}); diff --git a/packages/web-react/src/components/UNSTABLE_Header/__tests__/useHeaderStyleProps.test.ts b/packages/web-react/src/components/UNSTABLE_Header/__tests__/useHeaderStyleProps.test.ts new file mode 100644 index 0000000000..97835de5a3 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/__tests__/useHeaderStyleProps.test.ts @@ -0,0 +1,12 @@ +import { renderHook } from '@testing-library/react'; +import { useHeaderStyleProps } from '../useHeaderStyleProps'; + +describe('useHeaderStyleProps', () => { + it('should return defaults', () => { + const props = {}; + const { result } = renderHook(() => useHeaderStyleProps(props)); + + expect(result.current.classProps.root).toBe('UNSTABLE_Header'); + expect(result.current.classProps.logo).toBe('UNSTABLE_HeaderLogo'); + }); +}); diff --git a/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderDefault.tsx b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderDefault.tsx new file mode 100644 index 0000000000..f46600c58c --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderDefault.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Container } from '../../Container'; +import { Flex } from '../../Flex'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import UNSTABLE_Header from '../UNSTABLE_Header'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const HeaderDefault = () => { + return ( + + + + + {defaultSvgLogo} + + + + + ); +}; +export default HeaderDefault; diff --git a/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderFluid.tsx b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderFluid.tsx new file mode 100644 index 0000000000..d62b966e8b --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderFluid.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Container } from '../../Container'; +import { Flex } from '../../Flex'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import UNSTABLE_Header from '../UNSTABLE_Header'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const HeaderFluid = () => { + return ( + + + + + {defaultSvgLogo} + + + + + ); +}; + +export default HeaderFluid; diff --git a/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderMinimal.tsx b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderMinimal.tsx new file mode 100644 index 0000000000..ae670e0fb9 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderMinimal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Flex } from '../../Flex'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import UNSTABLE_Header from '../UNSTABLE_Header'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const HeaderMinimal = () => { + return ( + + + + {defaultSvgLogo} + + + + ); +}; + +export default HeaderMinimal; diff --git a/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderWithNavigation.tsx b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderWithNavigation.tsx new file mode 100644 index 0000000000..867a12f673 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/demo/HeaderWithNavigation.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import { Flex } from '../../Flex'; +import { Icon } from '../../Icon'; +import { Navigation, NavigationItem, NavigationLink } from '../../Navigation'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import UNSTABLE_Header from '../UNSTABLE_Header'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const HeaderDefault = () => { + return ( + + + + + {defaultSvgLogo} + + + + Link + + + + Selected + + + + + Disabled + + + + + + + + + + + + + + + + + + ); +}; +export default HeaderDefault; diff --git a/packages/web-react/src/components/UNSTABLE_Header/demo/index.tsx b/packages/web-react/src/components/UNSTABLE_Header/demo/index.tsx new file mode 100644 index 0000000000..2eff55e20a --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/demo/index.tsx @@ -0,0 +1,32 @@ +// Because there is no `dist` directory during the CI run +/* eslint-disable import/no-extraneous-dependencies, import/extensions, import/no-unresolved */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment, import/extensions, import/no-unresolved +// @ts-ignore: No declaration file -- @see https://jira.almacareer.tech/browse/DS-561 +import icons from '@lmc-eu/spirit-icons/icons'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import DocsSection from '../../../../docs/DocsSections'; +import { IconsProvider } from '../../../context'; +import HeaderDefault from './HeaderDefault'; +import HeaderFluid from './HeaderFluid'; +import HeaderMinimal from './HeaderMinimal'; +import HeaderWithNavigation from './HeaderWithNavigation'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + + + + + + , +); diff --git a/packages/web-react/src/components/UNSTABLE_Header/index.html b/packages/web-react/src/components/UNSTABLE_Header/index.html new file mode 100644 index 0000000000..39c220fe8c --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/index.html @@ -0,0 +1 @@ +{{> web-react/demo title="Header" parentPageName="Components" isUnstable=true }} diff --git a/packages/web-react/src/components/UNSTABLE_Header/index.ts b/packages/web-react/src/components/UNSTABLE_Header/index.ts new file mode 100644 index 0000000000..2649e6b17d --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/index.ts @@ -0,0 +1,5 @@ +'use client'; + +export { default as UNSTABLE_Header } from './UNSTABLE_Header'; +export { default as UNSTABLE_HeaderLogo } from './UNSTABLE_HeaderLogo'; +export * as useUnstableHeaderStyleProps from './useHeaderStyleProps'; diff --git a/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_Header.stories.tsx b/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_Header.stories.tsx new file mode 100644 index 0000000000..bef302d083 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_Header.stories.tsx @@ -0,0 +1,48 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import ReadMe from '../README.md'; +import UNSTABLE_Header from '../UNSTABLE_Header'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const meta: Meta = { + title: 'Experimental/UNSTABLE_Header', + component: UNSTABLE_Header, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + isSimple: { + control: 'boolean', + table: { + defaultValue: { summary: 'false' }, + }, + }, + isFluid: { + control: 'boolean', + table: { + defaultValue: { summary: 'false' }, + }, + }, + }, + args: { + children: ( + + {defaultSvgLogo} + + ), + isFluid: false, + isSimple: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + name: 'UNSTABLE_Header', +}; diff --git a/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_HeaderLogo.stories.tsx b/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_HeaderLogo.stories.tsx new file mode 100644 index 0000000000..e53ba3b0a1 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/stories/UNSTABLE_HeaderLogo.stories.tsx @@ -0,0 +1,27 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { ProductLogo } from '../../ProductLogo'; +import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault'; +import ReadMe from '../README.md'; +import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo'; + +const meta: Meta = { + title: 'Experimental/UNSTABLE_Header', + component: UNSTABLE_HeaderLogo, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + args: { + children: {defaultSvgLogo}, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + name: 'UNSTABLE_HeaderLogo', +}; diff --git a/packages/web-react/src/components/UNSTABLE_Header/useHeaderStyleProps.ts b/packages/web-react/src/components/UNSTABLE_Header/useHeaderStyleProps.ts new file mode 100644 index 0000000000..e68af054c3 --- /dev/null +++ b/packages/web-react/src/components/UNSTABLE_Header/useHeaderStyleProps.ts @@ -0,0 +1,23 @@ +import { useClassNamePrefix } from '../../hooks'; +import { SpiritHeaderProps } from './UNSTABLE_Header'; + +export interface HeaderStyles { + classProps: { + root: string; + logo: string; + }; + props: T; +} + +export const useHeaderStyleProps = (props: SpiritHeaderProps): HeaderStyles => { + const headerClass = useClassNamePrefix('UNSTABLE_Header'); + const headerLogoClass = 'UNSTABLE_HeaderLogo'; + + return { + classProps: { + root: headerClass, + logo: headerLogoClass, + }, + props, + }; +}; diff --git a/packages/web-react/src/types/index.ts b/packages/web-react/src/types/index.ts index 45c359fc65..84dcbe757b 100644 --- a/packages/web-react/src/types/index.ts +++ b/packages/web-react/src/types/index.ts @@ -38,4 +38,5 @@ export * from './textFieldBase'; export * from './toast'; export * from './toggle'; export * from './tooltip'; +export * from './unstableHeader'; export * from './visuallyHidden'; diff --git a/packages/web-react/src/types/unstableHeader.ts b/packages/web-react/src/types/unstableHeader.ts new file mode 100644 index 0000000000..3b08b4390a --- /dev/null +++ b/packages/web-react/src/types/unstableHeader.ts @@ -0,0 +1,28 @@ +import { ChildrenProps, SpiritPolymorphicElementPropsWithRef, StyleProps, TransferProps } from './shared'; +import { LinkTarget } from './link'; +import { ElementType } from 'react'; + +export type SpiritHeaderProps = { + isFluid?: boolean; + isSimple?: boolean; +} & StyleProps & + ChildrenProps; + +export interface HeaderLogoBaseProps extends ChildrenProps, StyleProps, TransferProps { + /** NavigationLink's href attribute */ + href?: string; + /** NavigationLink's target attribute */ + target?: LinkTarget; +} + +export type HeaderLogoProps = { + /** + * The HTML element or React element used to render the button, e.g. 'div', 'a', or `RouterLink`. + * + * @default 'a' + */ + elementType?: E; +} & HeaderLogoBaseProps; + +export type SpiritHeaderLogoProps = HeaderLogoProps & + SpiritPolymorphicElementPropsWithRef>;