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 Vertical Navigation #DS-1627 #1861

Merged
merged 6 commits into from
Jan 29, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ const DrawerDefault = () => {
label="Drawer content"
name="content"
id="drawer-content"
helperText="Can contain HTML."
value={content}
onChange={(e) => setContent(e.currentTarget.value)}
/>
Expand Down
12 changes: 9 additions & 3 deletions packages/web-react/src/components/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

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

const defaultProps: Partial<SpiritNavigationProps> = {
direction: Direction.HORIZONTAL,
};

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

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

return (
<nav {...otherProps} className={classNames(classProps.root, styleProps.className)} style={styleProps.style}>
Expand Down
13 changes: 10 additions & 3 deletions packages/web-react/src/components/Navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ It consists of a these parts:

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

The `Navigation` component can be horizontal or vertical. Use `direction` prop to set the orientation. Default direction is `horizontal`.

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

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

It centres its children vertically, and if the children do not include `NavigationAction` components,
Expand All @@ -25,9 +30,10 @@ it will apply a gap between them.

### API

| Name | Type | Default | Required | Description |
| ---------- | --------------------------------------------------------------------------------------- | ------- | -------- | ------------------------- |
| `children` | `ReactElement<HTMLLIElement>` \| `ReactElement<NavigationItem>` \| Array of these types | `null` | ✓ | Content of the Navigation |
| Name | Type | Default | Required | Description |
| ----------- | --------------------------------------------------------------------------------------- | ------------ | -------- | ----------------------------- |
| `children` | `ReactElement<HTMLLIElement>` \| `ReactElement<NavigationItem>` \| Array of these types | `null` | ✓ | Content of the Navigation |
| `direction` | [Direction dictionary][direction-dictionary] | `horizontal` | ✕ | Orientation 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]
Expand Down Expand Up @@ -146,6 +152,7 @@ With Buttons:
</Navigation>
```

[direction-dictionary]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#direction
[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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,53 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { classNamePrefixProviderTest, restPropsTest, stylePropsTest } from '@local/tests';
import { Direction } from '../../../constants';
import Navigation from '../Navigation';

const dataProvider = [
{
direction: Direction.HORIZONTAL,
directionClassName: 'Navigation--horizontal',
},
{
direction: Direction.VERTICAL,
directionClassName: 'Navigation--vertical',
},
{
direction: undefined,
directionClassName: 'Navigation--horizontal',
},
];

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

stylePropsTest(Navigation);

restPropsTest(Navigation, 'nav');

beforeEach(() => {
render(
<Navigation>
<li>Content</li>
</Navigation>,
);
});
describe.each(dataProvider)('when direction is $direction', ({ direction, directionClassName }) => {
beforeEach(() => {
render(
<Navigation direction={direction}>
<li>Content</li>
</Navigation>,
);
});

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

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

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

it('should render children', () => {
expect(screen.getByText('Content')).toBeInTheDocument();
it('should render children', () => {
expect(screen.getByText('Content')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { renderHook } from '@testing-library/react';
import { Direction } from '../../../constants';
import { SpiritNavigationActionProps } from '../../../types';
import { useNavigationStyleProps } from '../useNavigationStyleProps';

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

expect(result.current.classProps.root).toBe('Navigation');
expect(result.current.classProps.root).toBe('Navigation Navigation--horizontal');
expect(result.current.classProps.action).toBe('NavigationAction');
expect(result.current.classProps.item).toBe('NavigationItem NavigationItem--alignmentYCenter');
});
Expand All @@ -31,4 +33,11 @@ describe('useNavigationStyleProps', () => {

expect(result.current.classProps.item).toBe('NavigationItem NavigationItem--alignmentYStretch');
});

it('should return if navigation is vertical', () => {
const props = { direction: Direction.VERTICAL };
const { result } = renderHook(() => useNavigationStyleProps(props));

expect(result.current.classProps.root).toBe('Navigation Navigation--vertical');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import Navigation from '../Navigation';
import NavigationItem from '../NavigationItem';

const NavigationDefault = () => {
const NavigationHorizontal = () => {
return (
<Navigation aria-label="Main Navigation">
<NavigationItem>Item</NavigationItem>
</Navigation>
);
};
export default NavigationDefault;
export default NavigationHorizontal;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Navigation from '../Navigation';
import NavigationAction from '../NavigationAction';
import NavigationItem from '../NavigationItem';

const NavigationDefault = () => {
const NavigationHorizontalWithAction = () => {
return (
<Navigation aria-label="Main Navigation">
<NavigationItem>
Expand All @@ -22,4 +22,4 @@ const NavigationDefault = () => {
</Navigation>
);
};
export default NavigationDefault;
export default NavigationHorizontalWithAction;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ButtonLink } from '../../ButtonLink';
import Navigation from '../Navigation';
import NavigationItem from '../NavigationItem';

const NavigationDefault = () => {
const NavigationHorizontalWithButtons = () => {
return (
<Navigation aria-label="Navigation with Buttons">
<NavigationItem>
Expand All @@ -17,4 +17,4 @@ const NavigationDefault = () => {
</Navigation>
);
};
export default NavigationDefault;
export default NavigationHorizontalWithButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { ElementType, forwardRef } from 'react';
import { PolymorphicRef, SpiritNavigationActionProps } from '../../../types';
import Dropdown from '../../Dropdown/Dropdown';
import DropdownPopover from '../../Dropdown/DropdownPopover';
import DropdownTrigger from '../../Dropdown/DropdownTrigger';
import { Icon } from '../../Icon';
import { Item } from '../../Item';
import Navigation from '../Navigation';
import NavigationAction from '../NavigationAction';
import NavigationItem from '../NavigationItem';

/* We need an exception for components exported with forwardRef */
/* eslint no-underscore-dangle: ['error', { allow: ['_NavigationActionAsDropdownTrigger'] }] */
const _NavigationActionAsDropdownTrigger = <E extends ElementType = 'a'>(
props: SpiritNavigationActionProps<E>,
ref: PolymorphicRef<E>,
): JSX.Element => {
return <NavigationAction ref={ref} {...props} elementType="button" />;
};

const NavigationActionAsDropdownTrigger = forwardRef<HTMLButtonElement, SpiritNavigationActionProps<ElementType>>(
_NavigationActionAsDropdownTrigger,
);

const NavigationHorizontalWithDropdown = () => {
const [isNavigationActionDropdownOpen, setIsNavigationActionDropdownOpen] = React.useState(false);

return (
<Navigation aria-label="Main Navigation">
<NavigationItem>
<NavigationAction href="/">Link</NavigationAction>
</NavigationItem>
<NavigationItem>
<Dropdown
alignmentX="stretch"
alignmentY="stretch"
id="dropdown-navigation"
isOpen={isNavigationActionDropdownOpen}
onToggle={() => setIsNavigationActionDropdownOpen(!isNavigationActionDropdownOpen)}
>
<DropdownTrigger elementType={NavigationActionAsDropdownTrigger as unknown as HTMLButtonElement}>
Dropdown
<Icon name={`chevron-${isNavigationActionDropdownOpen ? 'up' : 'down'}`} boxSize={20} />
</DropdownTrigger>
<DropdownPopover>
<Item elementType="a" href="#" label="My Account" />
<Item elementType="a" href="#" label="Settings" />
<Item elementType="a" href="#" label="Log out" />
</DropdownPopover>
</Dropdown>
</NavigationItem>
</Navigation>
);
};
export default NavigationHorizontalWithDropdown;
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 NavigationVertical = () => {
return (
<Navigation aria-label="Main Navigation" direction="vertical">
<NavigationItem>Item</NavigationItem>
</Navigation>
);
};
export default NavigationVertical;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import Navigation from '../Navigation';
import NavigationAction from '../NavigationAction';
import NavigationItem from '../NavigationItem';

const NavigationVerticalWithAction = () => {
return (
<Navigation aria-label="Main Navigation" direction="vertical">
<NavigationItem>
<NavigationAction href="/">Link</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction href="/" aria-current="page" isSelected>
Selected
</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction href="/" isDisabled>
Disabled
</NavigationAction>
</NavigationItem>
</Navigation>
);
};
export default NavigationVerticalWithAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { ButtonLink } from '../../ButtonLink';
import Navigation from '../Navigation';
import NavigationItem from '../NavigationItem';

const NavigationVerticalWithButtons = () => {
return (
<Navigation aria-label="Navigation with Buttons" direction="vertical">
<NavigationItem>
<ButtonLink href="#">Button</ButtonLink>
</NavigationItem>
<NavigationItem>
<ButtonLink href="#" color="secondary">
Button
</ButtonLink>
</NavigationItem>
</Navigation>
);
};
export default NavigationVerticalWithButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Collapse, useCollapse } from '../../Collapse';
import { Icon } from '../../Icon';
import Navigation from '../Navigation';
import NavigationAction from '../NavigationAction';
import NavigationItem from '../NavigationItem';

const NavigationVerticalWithCollapse = () => {
const { isOpen, toggleHandler } = useCollapse(true);

return (
<Navigation aria-label="Vertical Navigation With Collapse" direction="vertical">
<NavigationItem>
<NavigationAction href="/">Link</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction elementType="button" onClick={toggleHandler} aria-expanded="true" isSelected>
Menu
<Icon name={`chevron-${isOpen ? 'up' : 'down'}`} size="small" />
</NavigationAction>
<Collapse id="collapse-navigation" isOpen={isOpen}>
<ul>
<NavigationItem>
<NavigationAction href="/">Nested Link</NavigationAction>
</NavigationItem>
<NavigationItem>
<NavigationAction href="/" isSelected aria-current="page">
Nested Selected
</NavigationAction>
</NavigationItem>
</ul>
</Collapse>
</NavigationItem>
</Navigation>
);
};
export default NavigationVerticalWithCollapse;
Loading
Loading