Skip to content

Commit

Permalink
Feat(web-react): Introduce Navigation #DS-1524
Browse files Browse the repository at this point in the history
  • Loading branch information
curdaj authored and crishpeen committed Dec 17, 2024
1 parent ffa5374 commit 121d281
Show file tree
Hide file tree
Showing 29 changed files with 776 additions and 9 deletions.
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;
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;
43 changes: 43 additions & 0 deletions packages/web-react/src/components/Navigation/NavigationLink.tsx
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, SpiritNavigationLinkProps } from '../../types';
import { useNavigationLinkProps } from './useNavigationLinkProps';
import { useNavigationLinkStyleProps } from './useNavigationLinkStyleProps';

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

/* We need an exception for components exported with forwardRef */
/* eslint no-underscore-dangle: ['error', { allow: ['_NavigationLink'] }] */
const _NavigationLink = <E extends ElementType = 'a'>(
props: SpiritNavigationLinkProps<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 { navigationLinkProps } = useNavigationLinkProps(propsWithDefaults);
const { classProps, props: modifiedProps } = useNavigationLinkStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

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

const NavigationLink = forwardRef<HTMLElement, SpiritNavigationLinkProps<ElementType>>(_NavigationLink);

export default NavigationLink;
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 links of the application.

It consists of a these parts:

- [Navigation](#navigation)
- [NavigationItem](#navigation-item)
- [NavigationLink](#navigation-link)

## 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 `NavigationLink` components,
it will apply a gap between them.

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

### 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 links.

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

<NavigationItem>{/* Navigation links 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 Link

The `NavigationLink` is component that is styled to be used as a navigation link.

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

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

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

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

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

ℹ️ Please note that in the `isDisabled` state the `NavigationLink` will be an `span` tag.

If the `NavigationLink` 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 NavigationLink |
| `elementType` | `ElementType` | `a` || Type of element used as |
| `href` | `string` | - || URL of the link |
| `isDisabled` | `boolean` | `false` || Whether the link is disabled |
| `isSelected` | `boolean` | `false` || Whether the link 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 NavigationLink components:

```jsx
<Navigation aria-label="Main Navigation">
<NavigationItem>
<NavigationLink href="#" aria-current="page" isSelected>
Selected Link
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink href="#" isDisabled>
Disabled Link
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink href="#">Link</NavigationLink>
</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,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');
});
});
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 NavigationLink from '../NavigationLink';

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

stylePropsTest(NavigationLink);

restPropsTest(NavigationLink, 'a');

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

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

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

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

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

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

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

expect(screen.getByText('Content')).toBeInTheDocument();
});
});
Loading

0 comments on commit 121d281

Please sign in to comment.