Skip to content

Commit

Permalink
[ui] SideNavigation (#226)
Browse files Browse the repository at this point in the history
* [ui] SideNavigation: add stub components

* [ui] SideNavigation, -Item: update styles

* [ui] bump version to 0.8.6
  • Loading branch information
franzheidl authored Nov 10, 2022
1 parent 0150497 commit 7cecdfc
Show file tree
Hide file tree
Showing 10 changed files with 335 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.8.5",
"version": "0.8.6",
"files": [
"src",
"lib",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Stack } from "../Stack/index"


/**
A generic vertical side navigation component.
Place TopNavigationItem components as children.
*/
export const SideNavigation = ({
children,
className,
...props
}) => {
return (
<Stack direction="vertical" className={`juno-sidenavigation ${className}`} role="navigation" {...props} >
{ children }
</Stack>
)
}

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

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

export default {
title: "Layout/SideNavigation/SideNavigation",
component: SideNavigation,
argTypes: {},
}

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

export const Default = Template.bind({})
Default.args = {
items: [
{...SideNavigationItemStory.args},
{...SideNavigationItemStory.args},
{...SideNavigationItemStory.args, icon: "warning"},
{...SideNavigationItemStory.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 { SideNavigation } from './index';

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

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

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

test('renders all props as passed', async () => {
render(<SideNavigation data-lol='Prop goes here' />);
expect(screen.getByRole('navigation')).toHaveAttribute('data-lol', 'Prop goes here');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SideNavigation } from "./SideNavigation.component";
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react";
import PropTypes from "prop-types";
import { Icon } from "../Icon/index.js";
import { knownIcons } from "../Icon/Icon.component.js"
import "./sideNavigationItem.scss"

/**
A SideNavigation item. Top be placed inside SideNavigation.
*/

export const SideNavigationItem = ({
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-sidenavigation-item ${ active ? "juno-sidenavigation-item-active" : ""} ${className}`}
href={href}
aria-label={ariaLabel}
{...props}
>
{ icn }
{ content }
</a>
)

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

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

return href ? anchor : onClick ? button : plain
}

SideNavigationItem.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,
}

SideNavigationItem.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 { SideNavigationItem } from "./index.js"

export default {
title: "Layout/SideNavigation/SideNavigationItem",
component: SideNavigationItem,
argTypes: {},
parameters: { actions: { argTypesRegex: null } }
}

const Template = (args) => <SideNavigationItem {...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 { SideNavigationItem } from './index';

describe('SideNavigation', () => {

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

test("renders an icon as passed", async () => {
render(<SideNavigationItem 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(<SideNavigationItem data-testid="side-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(<SideNavigationItem href="#"/>);
expect(screen.getByRole("link")).toBeInTheDocument();
expect(screen.getByRole("link")).toHaveClass("juno-sidenavigation-item");
})

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

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

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

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

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

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

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

});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SideNavigationItem } from "./SideNavigationItem.component";
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.juno-sidenavigation-item {
color: var(--color-text-default);
font-weight: bold;
display: inline-flex;
align-items: center;
border-left: 3px solid transparent;
padding-left: 1.75rem;
padding-top: .375rem;
padding-bottom: 0.375rem;

.juno-icon {
margin-right: .5rem;
}

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

.juno-icon {
color: var(--color-text-high);
margin-right: .5rem;
}

}

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

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

}

}

0 comments on commit 7cecdfc

Please sign in to comment.