Skip to content

Commit

Permalink
[ui] TopNavigation (#213)
Browse files Browse the repository at this point in the history
* [ui] TopNavigation: initial commit

* [ui] TopNavigation: stub stories with output

* [ui] TopNavigation, Item: render as anchors, buttons, or plain

* [ui] TopNavigation: begin adding styles

* [ui] TopNavigation: add item styles, parent tests

* [ui] TopNavigation, Item: export components

* [ui] TopNavigationItem: add tests, add aria-label prop, remove obsolete

* [ui] bump version to 0.7.2
  • Loading branch information
franzheidl authored Oct 25, 2022
1 parent 5deac97 commit 6b13f3d
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 1 deletion.
2 changes: 1 addition & 1 deletion libs/juno-ui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"module": "lib/index.js",
"source": "src/index.js",
"style": "lib/esm/styles.css",
"version": "0.7.3",
"version": "0.7.4",
"files": [
"src",
"lib",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Stack } from "../Stack/index"
import { Icon } from "../Icon/index.js";

const topNavigationStyles = `
jn-gap-[3.9375rem]
`

/**
A generic horizontal top level navigation component. To be placed below the application header but above application content.
Place TopNavigationItem components as children.
*/
export const TopNavigation = ({
children,
className,
...props
}) => {
return (
<Stack className={`juno-topnavigation ${topNavigationStyles} ${className}`} role="navigation" {...props} >
{ children }
</Stack>
)
}

TopNavigation.propTypes = {
/** The children of the Navigation. Typically these should be TopNavigationItem(s) */
children: PropTypes.node,
/** Passa custom classname. */
className: PropTypes.string,
}

TopNavigation.defaultProps = {
children: null,
className: "",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react"
import { TopNavigation } from "./index.js"
import { TopNavigationItem } from "../TopNavigationItem/TopNavigationItem.component"
import { Default as TopNavigationItemStory } from "../TopNavigationItem/TopNavigationItem.stories"

export default {
title: "WiP/TopNavigation/TopNavigation",
component: TopNavigation,
argTypes: {},
}

const Template = ({items, ...args}) => (
<TopNavigation {...args}>
{items.map((item, i) => (
<TopNavigationItem key={i} {...item} />
))}
</TopNavigation>
)

export const Default = Template.bind({})
Default.args = {
items: [
{...TopNavigationItemStory.args},
{...TopNavigationItemStory.args},
{...TopNavigationItemStory.args, icon: "warning"},
{...TopNavigationItemStory.args, active: true},
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TopNavigation } from './index';

describe('TopNavigation', () => {
test('render a ToppNavigation', async () => {
render(<TopNavigation />);
expect(screen.getByRole('navigation')).toBeInTheDocument();
expect(screen.getByRole('navigation')).toHaveClass("juno-topnavigation");
});

test("renders children as passed", async () => {
render(<TopNavigation><button>Test</button></TopNavigation>)
expect(screen.getByRole('button', {name: "Test"})).toBeInTheDocument();
})

test('renders custom classNames as passed', async () => {
render(<TopNavigation className='my-custom-class' />);
expect(screen.getByRole("navigation")).toHaveClass('my-custom-class');
});

test('renders all props as passed', async () => {
render(<TopNavigation data-lol='Prop goes here' />);
expect(screen.getByRole('navigation')).toHaveAttribute('data-lol', 'Prop goes here');
});
});
2 changes: 2 additions & 0 deletions libs/juno-ui-components/src/components/TopNavigation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TopNavigation } from "./TopNavigation.component";

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Icon } from "../Icon/index.js";
import { knownIcons } from "../Icon/Icon.component.js"
import "./topNavigationItem.scss"

/**
A top level navigation item. Top be placed inside TopNavigation.
*/

export const TopNavigationItem = ({
icon,
label,
ariaLabel,
href,
active,
onClick,
children,
className,
...props
}) => {

const icn = icon ? <Icon icon={icon} size="18" color="jn-text-theme-default" className={ label && label.length ? "jn-mr-1" : "" } /> : null

const content = label || children

const handleButtonClick = (event) => {
onClick && onClick(event)
}

const anchor = (
<a className={`juno-topnavigation-item ${ active ? "juno-topnavigation-item-active" : ""} ${className}`}
href={href}
aria-label={ariaLabel}
{...props}
>
{ icn }
{ content }
</a>
)

const button = (
<button
className={`juno-topnavigation-item ${ active ? "juno-topnavigation-item-active" : ""} ${className}`}
onClick={handleButtonClick}
aria-label={ariaLabel}
{...props}
>
{ icn }
{ content }
</button>
)

const plain = (
<div className={`juno-topnavigation-item ${ active ? "juno-topnavigation-item-active" : ""} ${className}`}
aria-label={ariaLabel}
{...props}
>
{ icn }
{ label || children }
</div>
)

return href ? anchor : onClick ? button : plain
}

TopNavigationItem.propTypes = {
/** pass an icon name */
icon: PropTypes.oneOf(knownIcons),
/** The label of the item */
label: PropTypes.string,
/** Children of the item. Will overwrite label when passed */
children: PropTypes.node,
/** Pass a custom className */
className: PropTypes.string,
/** The aria label of the item */
ariaLabel: PropTypes.string,
/** The link the item should point to. Will render the item as an anchor if passed */
href: PropTypes.string,
/** Whether the item is the currently active item */
active: PropTypes.bool,
/** A handler to execute once the item is clicked. Will render the item as a button element if passed */
onClick: PropTypes.func,
}

TopNavigationItem.defaultProps = {
icon: null,
label: "",
children: null,
className: "",
ariaLabel: "",
href: "",
active: false,
onClick: undefined,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react"
import { TopNavigationItem } from "./index.js"

export default {
title: "WiP/TopNavigation/TopNavigationItem",
component: TopNavigationItem,
argTypes: {},
parameters: { actions: { argTypesRegex: null } }
}

const Template = (args) => <TopNavigationItem {...args} />

export const Default = Template.bind({})
Default.args = {
label: "Navigation Item"
}

export const WithIcon = Template.bind({})
WithIcon.args = {
label: "Navigation Item",
icon: "warning"
}

export const AsAnchor = Template.bind({})
AsAnchor.args = {
label: "Navigation Item",
href: "#"
}

export const AsButton = Template.bind({})
AsButton.args = {
label: "Navigation Item",
onClick: () => {console.log("clicked")}
}

export const Active = Template.bind({})
Active.args = {
label: "Navigation Item",
active: true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TopNavigationItem } from './index';

describe('TopNavigation', () => {

test('renders a ToppNavigationItem', async () => {
render(<TopNavigationItem data-testid="top-nav-item" />);
expect(screen.getByTestId('top-nav-item')).toBeInTheDocument();
expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item");
});

test("renders an icon as passed", async () => {
render(<TopNavigationItem icon="warning" />)
expect(screen.getByRole("img")).toBeInTheDocument();
expect(screen.getByRole("img")).toHaveAttribute("alt", "warning");
})

test("renders a plain, non-interactive item when no href or onClick are passed", async () => {
render(<TopNavigationItem data-testid="top-nav-item" />);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
})

test("renders as a link when a href prop is passed", async () => {
render(<TopNavigationItem href="#"/>);
expect(screen.getByRole("link")).toBeInTheDocument();
expect(screen.getByRole("link")).toHaveClass("juno-topnavigation-item");
})

test("renders as a button when an onClick prop is passed", async () => {
render(<TopNavigationItem onClick={()=>{console.log("click")}} />);
expect(screen.getByRole("button")).toBeInTheDocument();
expect(screen.getByRole("button")).toHaveClass("juno-topnavigation-item");
})

test('renders an active ToppNavigationItem as passed', async () => {
render(<TopNavigationItem data-testid="top-nav-item" active />);
expect(screen.getByTestId('top-nav-item')).toBeInTheDocument();
expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item");
expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item-active");
});

test('renders an aria-label as passed', async () => {
render(<TopNavigationItem href="#" ariaLabel="hey nav item!" />);
expect(screen.getByRole('link')).toHaveAttribute('aria-label', 'hey nav item!');
});

test("renders children as passed", async () => {
render(<TopNavigationItem data-testid="top-nav-item" >Test</TopNavigationItem>)
expect(screen.getByTestId('top-nav-item')).toBeInTheDocument();
expect(screen.getByTestId('top-nav-item')).toHaveTextContent("Test");
})

test("onClick handler is called as passed", () => {
const onClickSpy = jest.fn()
render(<TopNavigationItem onClick={onClickSpy} />)
screen.getByRole("button").click()
expect(onClickSpy).toHaveBeenCalled()
})

test('renders custom classNames as passed', async () => {
render(<TopNavigationItem data-testid="top-nav-item" className='my-custom-class' />);
expect(screen.getByTestId('top-nav-item')).toHaveClass('my-custom-class');
});

test('renders all props as passed', async () => {
render(<TopNavigationItem data-testid="top-nav-item" data-lol='Prop goes here' />);
expect(screen.getByTestId('top-nav-item')).toHaveAttribute('data-lol', 'Prop goes here');
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TopNavigationItem } from "./TopNavigationItem.component";

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.juno-topnavigation-item {
color: var(--color-text-default);
font-weight: bold;
display: inline-flex;
align-items: center;
gap: .5rem;
border-bottom: 3px solid transparent;
padding-top: .125rem;
padding-bottom: 0.3125rem;

&:hover {
color: var(--color-text-high);
border-bottom: 3px solid var(--color-accent);

.juno-icon {
color: var(--color-text-high);
}

}

&:active,
&.juno-topnavigation-item-active {
color: var(--color-navigation-active);
border-bottom: 3px solid var(--color-navigation-active);

.juno-icon {
color: var(--color-navigation-active);
}

}

}
4 changes: 4 additions & 0 deletions libs/juno-ui-components/src/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@
--color-filter-pill-key-bg: var(--color-background-lvl-3-raw);
// LT Modal
--color-modal-backdrop-bg: var(--color-sap-white-raw);
// LT Navigation
--color-navigation-active: var(--color-black);
}

/* ----- LIGHT THEME END -----*/
Expand Down Expand Up @@ -758,6 +760,8 @@
--color-filter-pill-key-bg: var(--color-background-lvl-4-raw);
// DT Modal
--color-modal-backdrop-bg: 13, 20, 28;
// DT Navigation
--color-navigation-active: var(--color-white);
}

/* ----- DARK THEME END ----- */
Expand Down
2 changes: 2 additions & 0 deletions libs/juno-ui-components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export { TextInput } from "./components/TextInput/index.js"
export { TextInputRow } from "./components/TextInputRow/index.js"
export { Toast } from "./components/Toast/index.js"
export { Tooltip } from "./components/Tooltip/index.js"
export { TopNavigation } from "./components/TopNavigation/index.js"
export { TopNavigationItem } from "./components/TopNavigationItem/index.js"
export * from "./components/Form/index.js"

import { StyleProvider } from "./components/StyleProvider/index.js"
Expand Down

0 comments on commit 6b13f3d

Please sign in to comment.