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 NavigationItem subcomponent and showcase Header with Navigation and Dropdown #1840

Open
wants to merge 4 commits into
base: feature/unstyled-button
Choose a base branch
from
Open
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
14 changes: 11 additions & 3 deletions packages/web-react/src/components/Navigation/NavigationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
'use client';

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

const defaultProps: Partial<SpiritNavigationItemProps> = {
isStretchingChildren: false,
};

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

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

return (
<li {...otherProps} className={styleProps.className} style={styleProps.style}>
<li {...otherProps} className={classNames(classProps, styleProps.className)} style={styleProps.style}>
{children}
</li>
);
Expand Down
22 changes: 19 additions & 3 deletions packages/web-react/src/components/Navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,27 @@ import { NavigationItem } from '@lmc-eu/spirit-web-react';
<NavigationItem>{/* Navigation actions go here */}</NavigationItem>;
```

If the children are not `NavigationAction` components or the `NavigationItem` does not have the
`isStretchingChildren` class, it will centre its children vertically.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`isStretchingChildren` class, it will centre its children vertically.
`isStretchingChildren` prop, it will centre its children vertically.

Otherwise better than in web 🙂.


```jsx
<NavigationItem>
<Link href="#">Vertically centered Link</Link>
</NavigationItem>
<NavigationItem isStretchingChildren>
<Link href="#">Stretched Link</Link>
</NavigationItem>
<NavigationItem>
<NavigationAction href="#">Stretched NavigationAction</NavigationAction>
</NavigationItem>
```

### API

| Name | Type | Default | Required | Description |
| ---------- | ----------------------- | ------- | -------- | ----------------------------- |
| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationItem |
| Name | Type | Default | Required | Description |
| ---------------------- | ----------------------- | ------- | -------- | ---------------------------------------- |
| `children` | `string` \| `ReactNode` | `null` | ✓ | Content of the NavigationItem |
| `isStretchingChildren` | `boolean` | `false` | ✕ | Whether the children should be stretched |

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
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
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 { classNamePrefixProviderTest, restPropsTest, stylePropsTest } from '@local/tests';
import NavigationItem from '../NavigationItem';

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

stylePropsTest(NavigationItem);

restPropsTest(NavigationItem, 'li');

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

expect(screen.getByRole('listitem')).toHaveClass('NavigationItem');
});

it('should have stretchChildren classname', () => {
render(<NavigationItem isStretchingChildren>Content</NavigationItem>);

expect(screen.getByRole('listitem')).toHaveClass('NavigationItem--stretchChildren');
});

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { renderHook } from '@testing-library/react';
import { useNavigationItemStyleProps } from '../useNavigationItemStyleProps';

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

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

it('should return stretchChildren class', () => {
const props = { isStretchingChildren: true };
const { result } = renderHook(() => useNavigationItemStyleProps(props));

expect(result.current.classProps).toBe('NavigationItem NavigationItem--stretchChildren');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import classNames from 'classnames';
import { useClassNamePrefix } from '../../hooks';
import { SpiritNavigationItemProps } from '../../types';

export interface NavigationItemStyles {
classProps: string;
props: SpiritNavigationItemProps;
}

export const useNavigationItemStyleProps = (props: SpiritNavigationItemProps): NavigationItemStyles => {
const { isStretchingChildren, ...restProps } = props;
const itemClass = useClassNamePrefix('NavigationItem');
const itemIsStretchingChildrenClass = `${itemClass}--stretchChildren`;

const className = classNames(itemClass, {
[itemIsStretchingChildrenClass]: isStretchingChildren,
});

return {
classProps: className,
props: restProps,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface NavigationStyles {
classProps: string;
}

export const useNavigationStyleProps = () => {
export const useNavigationStyleProps = (): NavigationStyles => {
const navigationClass = useClassNamePrefix('Navigation');

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import React from 'react';
import { SpiritNavigationActionProps } from '../../../types';
import { Button } from '../../Button';
import { ButtonLink } from '../../ButtonLink';
import { Container } from '../../Container';
import { Dropdown, DropdownPopover, DropdownTrigger } from '../../Dropdown';
import { Flex } from '../../Flex';
import { Icon } from '../../Icon';
import { Item } from '../../Item';
import { Navigation, NavigationAction, NavigationItem } from '../../Navigation';
import { ProductLogo } from '../../ProductLogo';
import { defaultSvgLogo } from '../../ProductLogo/demo/ProductLogoDefault';
import { Text } from '../../Text';
import { UNSTABLE_Avatar } from '../../UNSTABLE_Avatar';
import UNSTABLE_Header from '../UNSTABLE_Header';
import UNSTABLE_HeaderLogo from '../UNSTABLE_HeaderLogo';

const NavigationActionAsDropdownTrigger = (props: SpiritNavigationActionProps) => (
<NavigationAction {...props} elementType="button" />
);
const AvatarButtonAsDropdownTrigger = (props: SpiritNavigationActionProps) => (
<button {...props} className="button-unstyled" type="button" />
);

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

return (
<UNSTABLE_Header>
<Container>
<Flex alignmentX="left" alignmentY="center" spacing="space-1000">
<Flex alignmentX="left" alignmentY="stretch" spacing="space-1000">
<UNSTABLE_HeaderLogo href="#">
<ProductLogo>{defaultSvgLogo}</ProductLogo>
</UNSTABLE_HeaderLogo>
Expand All @@ -27,6 +42,35 @@ const HeaderDefault = () => {
Selected
</NavigationAction>
</NavigationItem>
<NavigationItem>
<Dropdown
alignmentX="stretch"
alignmentY="stretch"
id="navigation-action-dropdown"
isOpen={isNavigationActionDropdownOpen}
onToggle={() => setIsNavigationActionDropdownOpen(!isNavigationActionDropdownOpen)}
placement="bottom-end"
>
<DropdownTrigger elementType={NavigationActionAsDropdownTrigger as unknown as HTMLButtonElement}>
Dropdown
<Icon name="chevron-up" boxSize={20} UNSAFE_className="accessibility-open" />
<Icon name="chevron-down" boxSize={20} UNSAFE_className="accessibility-closed" />
</DropdownTrigger>
<DropdownPopover>
<ul className="list-unstyled">
<li>
<Item label="My Account" href="https://www.example.com" />
</li>
<li>
<Item label="Settings" href="https://www.example.com" />
</li>
<li>
<Item label="Log Out" href="https://www.example.com" />
</li>
</ul>
</DropdownPopover>
</Dropdown>
</NavigationItem>
<NavigationItem>
<NavigationAction href="#" isDisabled>
Disabled
Expand All @@ -39,9 +83,45 @@ const HeaderDefault = () => {
<Icon name="search" />
</Button>
</NavigationItem>
<NavigationItem isStretchingChildren>
<Dropdown
alignmentX="stretch"
alignmentY="stretch"
id="avatar-dropdown"
isOpen={isAvatarDropdownOpen}
onToggle={() => setIsAvatarDropdownOpen(!isAvatarDropdownOpen)}
placement="bottom-end"
>
<DropdownTrigger elementType={AvatarButtonAsDropdownTrigger as unknown as HTMLButtonElement}>
<Flex spacingX="space-500" alignmentX="stretch" alignmentY="center">
<UNSTABLE_Avatar isSquare aria-label="Profile of Jiří Bárta">
<Icon name="profile" boxSize={20} />
</UNSTABLE_Avatar>
<Text elementType="span" size="small" emphasis="semibold" UNSAFE_className="text-primary">
My Account
</Text>
<Icon name="chevron-up" boxSize={20} UNSAFE_className="accessibility-open" />
<Icon name="chevron-down" boxSize={20} UNSAFE_className="accessibility-closed" />
</Flex>
</DropdownTrigger>
<DropdownPopover>
<ul className="list-unstyled">
<li>
<Item label="My Account" href="https://www.example.com" />
</li>
<li>
<Item label="Settings" href="https://www.example.com" />
</li>
<li>
<Item label="Log Out" href="https://www.example.com" />
</li>
</ul>
</DropdownPopover>
</Dropdown>
</NavigationItem>
<NavigationItem>
<ButtonLink href="#" color="secondary">
Sign up
Log out
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

</ButtonLink>
</NavigationItem>
<NavigationItem>
Expand Down
4 changes: 3 additions & 1 deletion packages/web-react/src/types/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export type NavigationActionProps<E extends ElementType> = {
elementType?: E;
} & NavigationActionBaseProps;

export interface SpiritNavigationItemProps extends ChildrenProps, StyleProps {}
export interface SpiritNavigationItemProps extends ChildrenProps, StyleProps {
isStretchingChildren?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕺 🙈

hasChildrenStretched / hasStretchedChildren Also not great…

}

export type SpiritNavigationActionProps<E extends ElementType = 'a'> = NavigationActionProps<E> &
SpiritPolymorphicElementPropsWithRef<E, NavigationActionProps<E>>;
Expand Down
26 changes: 26 additions & 0 deletions packages/web/src/scss/components/Navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The `Navigation` component is a container for the navigation actions of the appl
It consists of these parts:

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

## Navigation
Expand All @@ -29,6 +30,31 @@ it will insert a gap between them.

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

## Navigation Item

The `NavigationItem` is a container for a navigation action or any other action.

```html
<li class="NavigationItem">
<!-- action -->
</li>
```

It centers its children vertically if the children are not NavigationAction components or
it does not have the `.NavigationItem--stretchChildren` class.
Comment on lines +43 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would turn this around so it's more comprehensible (hopefully): If there is a NavigationAction inside or the XY modifier is present, it stretches its content vertically. In all other cases, the content is vertically centered.


```html
<li class="NavigationItem">
<a href="#">Vertically centered link</a>
</li>
<li class="NavigationItem NavigationItem--stretchChildren">
<a href="#">Stretched link</a>
</li>
<li class="NavigationItem">
<a href="#" class="NavigationAction">Stretched NavigationAction</a>
</li>
```

## Navigation Action

The `NavigationAction` is component that is styled to be used as a navigation action.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
@use 'theme';

.Navigation,
.Navigation > ul,
.Navigation > ul > li {
.Navigation > ul {
display: flex;
align-items: stretch;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it needed? It works without it for me.

}

.Navigation:has(.NavigationAction) {
Expand All @@ -15,7 +15,6 @@
.Navigation:not(:has(.NavigationAction)),
.Navigation:not(:has(.NavigationAction)) > ul {
gap: theme.$spacing;
align-items: center;
}

.Navigation > ul {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
// 1. We need to set the top border too, otherwise the content won't be aligned properly.

@use '@tokens' as tokens;
@use '../../tools/reset';
@use '../../tools/typography';
@use 'theme';

.NavigationAction:where(button) {
@include reset.button();
}

.NavigationAction {
@include typography.generate(theme.$link-typography);

display: flex;
gap: theme.$link-gap;
align-items: center;
justify-content: center;
padding-inline: theme.$link-spacing;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use '@tokens' as tokens;
@use 'theme';

.NavigationItem {
display: flex;
align-items: center;
}

.NavigationItem--stretchChildren,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just the AlignmentYExtended dictionary alone, isn't it? 🙂 Now it looks a bit strange to me like this, but I think we will be able to get rid of this modifier once we abstract the alignment classes (and people will be able to use also other alignments here if needed).

.NavigationItem:has(.NavigationAction) {
align-items: stretch;
}
1 change: 1 addition & 0 deletions packages/web/src/scss/components/Navigation/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ $spacing: tokens.$space-700;
$height: 50px;

$link-typography: tokens.$body-small-semibold;
$link-gap: tokens.$space-600;
$link-spacing: tokens.$space-600;
$link-color-state-default: tokens.$component-header-item-state-default;
$link-color-state-hover: tokens.$component-header-item-state-hover;
Comment on lines 6 to 10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these all start with action instead of link?

Expand Down
Loading
Loading