Commit bb649489 by Peter Holmberg Committed by GitHub

grafana/ui: Create Tabs component (#21328)

* create tabs component

* replace tabs in pageheader

* splitting two different types of tabitems

* fix index and conditionals in tabs

* redo tabs to not render anchor links

* rename to className and use cx to combine classes

* reverting back to a simpler use case

* moving type to types file

* fix import

* redoing Tabs to simpler composition components

* pr feedback

* update snapshot

* use icon component, added knob for storybook
parent bf187044
import React, { FC } from 'react';
import { cx } from 'emotion';
import { Icon } from '..';
import { useTheme } from '../../themes';
import { IconType } from '../Icon/types';
import { getTabsStyle } from './styles';
export interface TabProps {
label: string;
active?: boolean;
icon?: IconType;
onChangeTab: () => void;
}
export const Tab: FC<TabProps> = ({ label, active, icon, onChangeTab }) => {
const theme = useTheme();
const tabsStyles = getTabsStyle(theme);
return (
<li className={cx(tabsStyles.tabItem, active && tabsStyles.activeStyle)} onClick={onChangeTab}>
{icon && <Icon name={icon} />}
{label}
</li>
);
};
import { Props } from '@storybook/addon-docs/blocks';
import { TabsBar } from './TabsBar'
# TabBar
A composition component for rendering a TabBar with Tabs for navigation
<Props of={TabBar} />
import React from 'react';
import { select } from '@storybook/addon-knobs';
import { TabsBar } from './TabsBar';
import { Tab } from './Tab';
import { UseState } from '../../utils/storybook/UseState';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './TabsBar.mdx';
export default {
title: 'UI/Tabs/TabsBar',
component: TabsBar,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const tabs = [
{ label: '1st child', key: 'first', hide: false, active: true },
{ label: '2nd child', key: 'second', hide: false, active: false },
{ label: '3rd child', key: 'third', hide: false, active: false },
];
export const Simple = () => {
const VISUAL_GROUP = 'Visual options';
// ---
const icon = select('Icon', { None: undefined, Heart: 'heart', Star: 'star', User: 'user' }, undefined, VISUAL_GROUP);
return (
<UseState initialState={tabs}>
{(state, updateState) => {
return (
<TabsBar>
{state.map((tab, index) => {
return (
<Tab
key={index}
label={tab.label}
active={tab.active}
icon={icon}
onChangeTab={() => updateState(state.map((tab, idx) => ({ ...tab, active: idx === index })))}
/>
);
})}
</TabsBar>
);
}}
</UseState>
);
};
import React, { FC, ReactNode } from 'react';
import { useTheme } from '../../themes';
import { getTabsStyle } from './styles';
export interface Props {
/** Children should be a single <Tab /> or an array of <Tab /> */
children: ReactNode;
}
export const TabsBar: FC<Props> = ({ children }) => {
const theme = useTheme();
const tabsStyles = getTabsStyle(theme);
return <ul className={tabsStyles.tabs}>{children}</ul>;
};
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
import { selectThemeVariant, stylesFactory } from '../../themes';
export const getTabsStyle = stylesFactory((theme: GrafanaTheme) => {
const colors = theme.colors;
const tabBorderColor = selectThemeVariant({ dark: colors.dark9, light: colors.gray5 }, theme.type);
return {
tabs: css`
position: relative;
top: 1px;
display: flex;
`,
tabItem: css`
list-style: none;
padding: 10px 15px 9px;
margin-right: ${theme.spacing.md};
position: relative;
display: block;
border: solid transparent;
border-width: 0 1px 1px;
border-radius: ${theme.border.radius.md} ${theme.border.radius.md} 0 0;
color: ${colors.text};
cursor: pointer;
i {
margin-right: ${theme.spacing.sm};
}
.gicon {
position: relative;
top: -2px;
}
&:hover,
&:focus {
color: ${colors.linkHover};
}
`,
activeStyle: css`
border-color: ${colors.orange} ${tabBorderColor} transparent;
background: ${colors.pageBg};
color: ${colors.link};
overflow: hidden;
cursor: not-allowed;
&::before {
display: block;
content: ' ';
position: absolute;
left: 0;
right: 0;
height: 2px;
top: 0;
background-image: linear-gradient(to right, #f05a28 30%, #fbca0a 99%);
}
`,
};
});
...@@ -48,6 +48,8 @@ export { SetInterval } from './SetInterval/SetInterval'; ...@@ -48,6 +48,8 @@ export { SetInterval } from './SetInterval/SetInterval';
export { Table } from './Table/Table'; export { Table } from './Table/Table';
export { TableInputCSV } from './TableInputCSV/TableInputCSV'; export { TableInputCSV } from './TableInputCSV/TableInputCSV';
export { TabsBar } from './Tabs/TabsBar';
export { Tab } from './Tabs/Tab';
// Visualizations // Visualizations
export { export {
......
import React, { FormEvent } from 'react'; import React, { FormEvent } from 'react';
import classNames from 'classnames'; import { Tab, TabsBar } from '@grafana/ui';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { NavModel, NavModelItem, NavModelBreadcrumb } from '@grafana/data'; import { NavModel, NavModelItem, NavModelBreadcrumb } from '@grafana/data';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
...@@ -45,37 +45,30 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string ...@@ -45,37 +45,30 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
); );
}; };
const Tabs = ({ main, customCss }: { main: NavModelItem; customCss: string }) => {
return (
<ul className={`gf-tabs ${customCss}`}>
{main.children.map((tab, idx) => {
if (tab.hideFromTabs) {
return null;
}
const tabClasses = classNames({
'gf-tabs-link': true,
active: tab.active,
});
return (
<li className="gf-tabs-item" key={tab.url}>
<a className={tabClasses} target={tab.target} href={tab.url}>
<i className={tab.icon} />
{tab.text}
</a>
</li>
);
})}
</ul>
);
};
const Navigation = ({ main }: { main: NavModelItem }) => { const Navigation = ({ main }: { main: NavModelItem }) => {
const goToUrl = (index: number) => {
main.children.forEach((child, i) => {
if (i === index) {
appEvents.emit(CoreEvents.locationChange, { href: child.url });
}
});
};
return ( return (
<nav> <nav>
<SelectNav customCss="page-header__select-nav" main={main} /> <SelectNav customCss="page-header__select-nav" main={main} />
<Tabs customCss="page-header__tabs" main={main} /> <TabsBar>
{main.children.map((child, index) => {
return (
<Tab
label={child.text}
active={child.active}
key={`${child.url}-${index}`}
onChangeTab={() => goToUrl(index)}
/>
);
})}
</TabsBar>
</nav> </nav>
); );
}; };
......
...@@ -94,20 +94,13 @@ exports[`ServerStats Should render table with stats 1`] = ` ...@@ -94,20 +94,13 @@ exports[`ServerStats Should render table with stats 1`] = `
</select> </select>
</div> </div>
<ul <ul
className="gf-tabs page-header__tabs" className="css-13jkosq"
> >
<li <li
className="gf-tabs-item" className="css-b418eg"
onClick={[Function]}
> >
<a Admin
className="gf-tabs-link active"
href="Admin"
>
<i
className="icon"
/>
Admin
</a>
</li> </li>
</ul> </ul>
</nav> </nav>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment