Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Introduce UNSTABLE Header & Navigation #1808

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const entryPoints = [
{ dirs: ['components', 'Item'] },
{ dirs: ['components', 'Link'] },
{ dirs: ['components', 'Modal'] },
{ dirs: ['components', 'Navigation'] },
{ dirs: ['components', 'NoSsr'] },
{ dirs: ['components', 'Pagination'] },
{ dirs: ['components', 'PartnerLogo'] },
Expand All @@ -54,6 +55,7 @@ const entryPoints = [
{ dirs: ['components', 'UNSTABLE_ActionLayout'] },
{ dirs: ['components', 'UNSTABLE_Avatar'] },
{ dirs: ['components', 'UNSTABLE_EmptyState'] },
{ dirs: ['components', 'UNSTABLE_Header'] },
{ dirs: ['components', 'UNSTABLE_Slider'] },
{ dirs: ['components', 'UNSTABLE_Toggle'] },
{ dirs: ['components', 'UNSTABLE_Truncate'] },
Expand Down
22 changes: 22 additions & 0 deletions packages/web-react/src/components/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import classNames from 'classnames';
import React from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritNavigationProps } from '../../types';
import { useNavigationStyleProps } from './useNavigationStyleProps';

const Navigation = (props: SpiritNavigationProps): JSX.Element => {
const { children, ...restProps } = props;

const { classProps } = useNavigationStyleProps();
const { styleProps, props: otherProps } = useStyleProps(restProps);

return (
<nav {...otherProps} className={classNames(classProps, styleProps.className)} style={styleProps.style}>
<ul>{children}</ul>
</nav>
);
};

export default Navigation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import classNames from 'classnames';
import React, { ElementType, forwardRef } from 'react';
import { useStyleProps } from '../../hooks';
import { PolymorphicRef, SpiritNavigationActionProps } from '../../types';
import { useNavigationActionProps } from './useNavigationActionProps';
import { useNavigationActionStyleProps } from './useNavigationActionStyleProps';

const defaultProps: Partial<SpiritNavigationActionProps> = {
elementType: 'a',
};

/* We need an exception for components exported with forwardRef */
/* eslint no-underscore-dangle: ['error', { allow: ['_NavigationAction'] }] */
const _NavigationAction = <E extends ElementType = 'a'>(
props: SpiritNavigationActionProps<E>,
ref: PolymorphicRef<E>,
): JSX.Element => {
const propsWithDefaults = { ...defaultProps, ...props };
const { elementType = defaultProps.elementType as ElementType, children, ...restProps } = propsWithDefaults;
const ElementTag = propsWithDefaults.isDisabled ? 'span' : elementType;

const { navigationActionProps } = useNavigationActionProps(propsWithDefaults);
const { classProps, props: modifiedProps } = useNavigationActionStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

return (
<ElementTag
{...otherProps}
{...styleProps}
{...navigationActionProps}
className={classNames(classProps, styleProps.className)}
ref={ref}
>
{children}
</ElementTag>
);
};

const NavigationAction = forwardRef<HTMLElement, SpiritNavigationActionProps<ElementType>>(_NavigationAction);

export default NavigationAction;
19 changes: 19 additions & 0 deletions packages/web-react/src/components/Navigation/NavigationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import React from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritNavigationItemProps } from '../../types';

const NavigationItem = (props: SpiritNavigationItemProps): JSX.Element => {
const { children, ...restProps } = props;

const { styleProps, props: otherProps } = useStyleProps(restProps);

return (
<li {...otherProps} className={styleProps.className} style={styleProps.style}>
{children}
</li>
);
};

export default NavigationItem;
134 changes: 134 additions & 0 deletions packages/web-react/src/components/Navigation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Navigation

The `Navigation` component is a container for the navigation actions of the application.

It consists of a these parts:

- [Navigation](#navigation)
- [NavigationItem](#navigation-item)
- [NavigationAction](#navigation-action)

## Navigation

The `Navigation` is a `nav` wrapper for navigation items.

```jsx
import { Navigation } from '@lmc-eu/spirit-web-react';

<Navigation aria-label="Main Navigation">{/* Navigation items go here */}</Navigation>;
```

It centres its children vertically, and if the children do not include `NavigationAction` components,
it will apply a gap between them.

ℹ️ Don't forget to add the `aria-label` attribute to the `Navigation` component for correct accessible title.

### API

| Name | Type | Default | Required | Description |
| ---------- | --------------------------------------------------------------------------------------- | ------- | -------- | ------------------------- |
| `children` | `ReactElement<HTMLLIElement>` \| `ReactElement<NavigationItem>` \| Array of these types | `null` | ✓ | Content of the Navigation |

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].

## Navigation Item

The `NavigationItem` is a container for navigation actions.

```jsx
import { NavigationItem } from '@lmc-eu/spirit-web-react';

<NavigationItem>{/* Navigation actions go here */}</NavigationItem>;
```

### API

| Name | Type | Default | Required | Description |
| ---------- | ----------------------- | ------- | -------- | ----------------------------- |
| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationItem |

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].

## Navigation Action

The `NavigationAction` is component that is styled to be used as a navigation action.

```jsx
import { NavigationAction } from '@lmc-eu/spirit-web-react';

<NavigationAction href="#">Link</NavigationAction>;
```

It can obtain `isSelected` or `isDisabled` states by adding the respective props.

```jsx
<NavigationAction href="#" aria-current="page" isSelected>Selected Link</NavigationAction>
<NavigationAction href="#" isDisabled>Disabled Link</NavigationAction>
```

ℹ️ Don't forget to add the `aria-current="page"` attribute for correct accessible state if selected.

ℹ️ Please note that in the `isDisabled` state the `NavigationAction` will be a `span` tag.

If the `NavigationAction` is inside a [`UNSTABLE_Header`][web-react-unstable-header] component, it will
inherit the height of the `Header`.

### API

| Name | Type | Default | Required | Description |
| ------------- | --------------------------------- | ------- | -------- | ------------------------------- |
| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationAction |
| `elementType` | `ElementType` | `a` | ✕ | Type of element used as |
| `href` | `string` | - | ✕ | URL of the link |
| `isDisabled` | `boolean` | `false` | ✕ | Whether the action is disabled |
| `isSelected` | `boolean` | `false` | ✕ | Whether the action is selected |
| `ref` | `ForwardedRef<HTMLAnchorElement>` | — | ✕ | Anchor element reference |
| `target` | `string` | `null` | ✕ | Link target |

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].

### Full Example

With NavigationAction components:

```jsx
<Navigation aria-label="Main Navigation">
<NavigationItem>
<NavigationAction href="#" aria-current="page" isSelected>
Selected Link
</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction href="#" isDisabled>
Disabled Link
</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction href="#">Link</NavigationAction>
</NavigationItem>
</Navigation>
```

With Buttons:

```jsx
<Navigation aria-label="Secondary Navigation">
<NavigationItem>
<ButtonLink href="#">Button</ButtonLink>
</NavigationItem>
<NavigationItem>
<ButtonLink href="#" color="secondary">Button</Button>
</NavigationItem>
</Navigation>
```

[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
[web-react-unstable-header]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/UNSTABLE_Header/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 Navigation from '../Navigation';

describe('Navigation', () => {
classNamePrefixProviderTest(Navigation, 'Navigation');

stylePropsTest(Navigation);

restPropsTest(Navigation, 'nav');

beforeEach(() => {
render(
<Navigation>
<li>Content</li>
</Navigation>,
);
});

it('should have default classname', () => {
expect(screen.getByRole('navigation')).toHaveClass('Navigation');
});

it('should render list and children', () => {
expect(screen.getByRole('list')).toBeInTheDocument();
});

it('should render children', () => {
expect(screen.getByText('Content')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 NavigationAction from '../NavigationAction';

describe('NavigationAction', () => {
classNamePrefixProviderTest(NavigationAction, 'NavigationAction');

stylePropsTest(NavigationAction);

restPropsTest(NavigationAction, 'a');

it('should have default classname', () => {
render(<NavigationAction href="/">Content</NavigationAction>);

expect(screen.getByRole('link')).toHaveClass('NavigationAction');
});

it('should have selected classname', () => {
render(
<NavigationAction href="/" isSelected>
Content
</NavigationAction>,
);

expect(screen.getByRole('link')).toHaveClass('NavigationAction NavigationAction--selected');
});

it('should have disabled classname and correct elementType', () => {
render(
<NavigationAction href="/" isDisabled>
Content
</NavigationAction>,
);

expect(screen.getByText('Content')).toHaveClass('NavigationAction NavigationAction--disabled');
expect(screen.getByText('Content')).toContainHTML('span');
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});

it('should render children', () => {
render(<NavigationAction href="/">Content</NavigationAction>);

expect(screen.getByText('Content')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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 NavigationItem from '../NavigationItem';

describe('NavigationItem', () => {
stylePropsTest(NavigationItem);

restPropsTest(NavigationItem, 'li');

it('should have correct role', () => {
render(<NavigationItem>Content</NavigationItem>);

expect(screen.getByRole('listitem')).toBeInTheDocument();
});

it('should render children', () => {
render(<NavigationItem>Content</NavigationItem>);

expect(screen.getByRole('listitem')).toHaveTextContent('Content');
});
});
Loading
Loading