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 committed Dec 4, 2024
1 parent 24b32c6 commit 7eaa5ed
Show file tree
Hide file tree
Showing 26 changed files with 619 additions and 4 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, props: modifiedProps } = useNavigationStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

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 { 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: ElementTag = defaultProps.elementType as ElementType,
children,
...restProps
} = propsWithDefaults;
const { classProps, props: modifiedProps } = useNavigationLinkStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

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

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

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

The `Navigation` component provides a versatile and accessible way to build navigation menus.

The Navigation component consists of the following building blocks:

- [Navigation](#navigation)
- [NavigationItem](#navigationitem)
- [NavigationLink](#navigationlink)

## Navigation

The `Navigation` component is a container for navigation items.

### Usage

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

<Navigation>
<NavigationItem>
<NavigationLink href="/" isSelected>
Home
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink href="/about">About Us</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink href="/contact">Contact</NavigationLink>
</NavigationItem>
</Navigation>;
```

### API

| Name | Type | Default | Required | Description |
| ---------- | ----------------------- | ------- | -------- | ------------------------- |
| `children` | `string` \| `ReactNode` | `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].

## NavigationItem

The `NavigationItem` component is a container for navigation links.

### Usage

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

<NavigationItem>
<NavigationLink href="/" isSelected>
Home
</NavigationLink>
</NavigationItem>;
```

### API

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

## NavigationLink

The `NavigationLink` component is a link in the navigation.

### Usage

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

<NavigationLink href="/" isSelected>
Home
</NavigationLink>;
```

### Selected

The `isSelected` prop is used to indicate that the link is selected.

```jsx
<NavigationLink href="/" isSelected>
Home
</NavigationLink>
```

### Disabled

The `isDisabled` prop is used to indicate that the link is disabled.

```jsx
<NavigationLink href="/" isDisabled>
Home
</NavigationLink>
```

### 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 |
| `isSelected` | `boolean` | `false` || Whether the link is selected |
| `isDisabled` | `boolean` | `false` || Whether the link is disabled |
| `ref` | `ForwardedRef<HTMLAnchorElement>` ||| Anchor element reference |
| `target` | `string` | `null` || Link target |

[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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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');

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

expect(screen.getByRole('navigation')).toHaveClass('Navigation');
});

it('should render list and children', () => {
render(<Navigation>Content</Navigation>);

expect(screen.getByRole('list')).toBeInTheDocument();
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,47 @@
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', () => {
render(
<NavigationLink href="/" isDisabled>
Content
</NavigationLink>,
);

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

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

expect(screen.getByText('Content')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { renderHook } from '@testing-library/react';
import { SpiritNavigationLinkProps } from '../../../types';
import { useNavigationLinkStyleProps } from '../useNavigationLinkStyleProps';

describe('useNavigationLinkStyleProps', () => {
it('should return defaults', () => {
const props = {};
const { result } = renderHook(() => useNavigationLinkStyleProps(props));

expect(result.current.classProps).toBe('NavigationLink');
});

it('should return disabled class', () => {
const props: SpiritNavigationLinkProps = { isDisabled: true };
const { result } = renderHook(() => useNavigationLinkStyleProps(props));

expect(result.current.classProps).toBe('NavigationLink NavigationLink--disabled');
});

it('should return selected class', () => {
const props = { isSelected: true };
const { result } = renderHook(() => useNavigationLinkStyleProps(props));

expect(result.current.classProps).toBe('NavigationLink NavigationLink--selected');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { renderHook } from '@testing-library/react';
import { useNavigationStyleProps } from '../useNavigationStyleProps';

describe('useNavigationStyleProps', () => {
it('should return defaults', () => {
const props = {};
const { result } = renderHook(() => useNavigationStyleProps(props));

expect(result.current.classProps).toBe('Navigation');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { Button } from '../../Button';
import Navigation from '../Navigation';
import NavigationItem from '../NavigationItem';

const NavigationDefault = () => {
return (
<div style={{ '--spirit-header-height': '72px' } as React.CSSProperties}>
<Navigation>
<NavigationItem>
<Button elementType="a" href="#">
Button
</Button>
</NavigationItem>
<NavigationItem>
<Button elementType="a" href="#" color="secondary">
Button
</Button>
</NavigationItem>
</Navigation>
</div>
);
};
export default NavigationDefault;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import Navigation from '../Navigation';
import NavigationItem from '../NavigationItem';

const NavigationDefault = () => {
return (
<Navigation>
<NavigationItem>Item</NavigationItem>
</Navigation>
);
};
export default NavigationDefault;
Loading

0 comments on commit 7eaa5ed

Please sign in to comment.