[Shared-UX] Migrate Toolbar component from presentation to shared-ux (#137224)

This commit is contained in:
Rachel Shen 2022-08-29 14:22:38 -06:00 committed by GitHub
parent 927fd674ce
commit 1ebfd79daf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 542 additions and 9 deletions

View file

@ -1,9 +1,23 @@
---
id: sharedUX/ButtonToolbar
slug: /shared-ux/button-toolbar
title: Button Toolbar
description:
tags: ['shared-ux', 'component']
date: 2022-06-14
id: sharedUX/Components/Toolbar
slug: /shared-ux/components/button_toolbar
title: Toolbar
summary: An implementation of the popover, primary button, icon button group and add from library button
tags: ['shared-ux', 'component', 'toolbar']
date: 2022-07-28
---
This `toolbar` component accepts a `children` prop. Children can include a `popover` or a generic `button`. It can also include the `IconButtonGroup` and `AddFromLibrary` component for soltuions.
Styling of the popover and button follow the primary styles.
```jsx
<Toolbar>
{{
primaryButton,
iconButtonGroup,
extraButtons,
addFromLibraryButton,
}}
</Toolbar>
```

View file

@ -21,7 +21,7 @@ export default {
},
};
const quickButtons = [
const iconButtons = [
{
label: 'Text',
onClick: action('onTextClick'),
@ -65,7 +65,7 @@ const argTypes = {
type Params = Record<keyof typeof argTypes, any>;
export const IconButtonGroup = ({ buttonCount }: Params) => {
return <Component legend="Example icon group" buttons={quickButtons.slice(0, buttonCount)} />;
return <Component legend="Example icon group" buttons={iconButtons.slice(0, buttonCount)} />;
};
IconButtonGroup.argTypes = argTypes;

View file

@ -16,3 +16,7 @@ export type {
export { ToolbarPopover } from './popover';
export type { ToolbarPopoverProps } from './popover';
export { Toolbar } from './toolbar';
export type { ToolbarProps } from './toolbar';
export type { ToolbarButton } from './toolbar';

View file

@ -7,5 +7,5 @@ tags: ['shared-ux', 'component']
date: 2022-03-28
---
This component is a thing wrapper around `EuiPopover` that handles open and close state. Its open and close state is controlled by a `ToolbarButton` button component.
This component is a thin wrapper around `EuiPopover` that handles open and close state. Its open and close state is controlled by a `ToolbarButton` button component.
This popover requires a label and children.

View file

@ -0,0 +1,200 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Toolbar /> is rendered 1`] = `
<Toolbar
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
>
<EuiFlexGroup
gutterSize="s"
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<PrimaryButton
label="Create chart"
onClick={[Function]}
>
<EuiButton
color="primary"
fill={true}
iconSide="left"
onClick={[Function]}
size="m"
>
<EuiButtonDisplay
baseClassName="euiButton"
color="primary"
disabled={false}
element="button"
fill={true}
iconSide="left"
isDisabled={false}
onClick={[Function]}
size="m"
type="button"
>
<button
className="euiButton euiButton--primary euiButton--fill"
disabled={false}
onClick={[Function]}
style={
Object {
"minWidth": undefined,
}
}
type="button"
>
<EuiButtonContentDeprecated
className="euiButton__content"
iconSide="left"
textProps={
Object {
"className": "euiButton__text",
}
}
>
<span
className="euiButtonContent euiButton__content"
>
<span
className="euiButton__text"
>
Create chart
</span>
</span>
</EuiButtonContentDeprecated>
</button>
</EuiButtonDisplay>
</EuiButton>
</PrimaryButton>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
responsive={false}
wrap={true}
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--wrap"
/>
</EuiFlexGroup>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</Toolbar>
`;

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { Toolbar } from './toolbar';
export type { Props as ToolbarProps } from './toolbar';
export type { ToolbarButton } from './toolbar';

View file

@ -0,0 +1,201 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Story } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { EuiContextMenu } from '@elastic/eui';
import { Toolbar } from './toolbar';
import { AddFromLibraryButton, IconButtonGroup, PrimaryButton } from '../buttons';
import { ToolbarPopover } from '../popover';
const iconButtons = [
{
label: 'Text',
onClick: action('onTextClick'),
iconType: 'visText',
title: 'Text as markdown',
},
{
label: 'Control',
onClick: action('onControlClick'),
iconType: 'controlsHorizontal',
},
{
label: 'Link',
onClick: action('onLinkClick'),
iconType: 'link',
},
{
label: 'Image',
onClick: action('onImageClick'),
iconType: 'image',
},
{
label: 'Markup',
onClick: action('onMarkupClick'),
iconType: 'visVega',
},
];
const primaryButtonConfigs = {
Generic: <PrimaryButton label="Primary Action" iconType="apps" onClick={action('generic')} />,
Canvas: (
<ToolbarPopover label="Add element" iconType="plusInCircle" panelPaddingSize="none">
{() => (
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
)}
</ToolbarPopover>
),
Dashboard: (
<PrimaryButton label="Create chart" iconType="plusInCircle" onClick={action('dashboard')} />
),
};
const extraButtonConfigs = {
Generic: undefined,
Canvas: undefined,
Dashboard: [
<ToolbarPopover iconType="visualizeApp" label="All editors" panelPaddingSize="none">
{() => (
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
)}
</ToolbarPopover>,
],
};
export default {
title: 'Toolbar',
description: 'A universal toolbar for solutions.',
component: Toolbar,
argTypes: {
iconButtonCount: {
defaultValue: 2,
control: {
type: 'number',
min: 0,
max: 5,
step: 1,
},
},
showAddFromLibraryButton: {
defaultValue: true,
control: {
type: 'boolean',
},
},
solution: {
table: {
disable: true,
},
},
},
// https://github.com/storybookjs/storybook/issues/11543#issuecomment-684130442
parameters: {
docs: {
source: {
type: 'code',
},
},
},
};
const Template: Story<{
solution: 'Generic' | 'Canvas' | 'Dashboard';
iconButtonCount: number;
showAddFromLibraryButton: boolean;
}> = ({ iconButtonCount, solution, showAddFromLibraryButton }) => {
const primaryButton = primaryButtonConfigs[solution];
const extraButtons = extraButtonConfigs[solution];
let iconButtonGroup;
let addFromLibraryButton;
if (iconButtonCount > 0) {
iconButtonGroup = (
<IconButtonGroup buttons={iconButtons.slice(0, iconButtonCount)} legend="example" />
);
}
if (showAddFromLibraryButton) {
addFromLibraryButton = <AddFromLibraryButton onClick={action('addFromLibrary')} />;
}
return (
<Toolbar>
{{
primaryButton,
iconButtonGroup,
extraButtons,
addFromLibraryButton,
}}
</Toolbar>
);
};
export const Generic = Template.bind({});
Generic.args = {
...Template.args,
solution: 'Generic',
};
export const Canvas = Template.bind({});
Canvas.args = {
...Template.args,
solution: 'Canvas',
};
export const Dashboard = Template.bind({});
Dashboard.args = {
...Template.args,
solution: 'Dashboard',
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { Toolbar } from './toolbar';
import { PrimaryButton } from '../buttons';
describe('<Toolbar />', () => {
test('is rendered', () => {
const primaryButton = <PrimaryButton label="Create chart" onClick={() => 'click'} />;
const children = { primaryButton };
const component = mountWithIntl(<Toolbar children={children} />);
expect(component).toMatchSnapshot();
});
test('onClick works as expected when the primary button is clicked', () => {
const mockClickHandler = jest.fn();
const primaryButton = <PrimaryButton label="Create chart" onClick={mockClickHandler} />;
const children = { primaryButton };
const component = mountWithIntl(<Toolbar children={children} />);
component.find('button').simulate('click');
expect(mockClickHandler).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ReactElement } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IconButtonGroup, PrimaryButton } from '../buttons';
import { ToolbarPopover } from '../popover';
/** type for cases with both button or a popover could be used */
export type ToolbarButton = typeof PrimaryButton | typeof ToolbarPopover;
/** Specific type for the toolbar children in its props */
interface NamedSlots {
primaryButton: ReactElement<ToolbarButton>;
iconButtonGroup?: ReactElement<typeof IconButtonGroup>;
extraButtons?: Array<ReactElement<ToolbarButton>> | undefined;
}
/**
* Props for a generic toolbar component
*/
export interface Props {
children: NamedSlots;
}
const errorText = i18n.translate('sharedUXPackages.buttonToolbar.toolbar.errorToolbarText', {
defaultMessage:
'There are over 120 extra buttons. Please consider limiting the number of buttons.',
});
/**
*
* @param children of the toolbar such as a popover or button
* @returns Toolbar component
*/
export const Toolbar = ({ children }: Props) => {
const { primaryButton, iconButtonGroup, extraButtons = [] } = children;
if (extraButtons.length > 120) {
throw new Error(errorText);
}
const extra = extraButtons.map((button, index) =>
button ? (
<EuiFlexItem grow={false} key={`button-${index}`}>
{button}
</EuiFlexItem>
) : null
);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>{primaryButton}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup wrap={true} responsive={false} alignItems="center" gutterSize="xs">
{iconButtonGroup ? <EuiFlexItem grow={false}>{iconButtonGroup}</EuiFlexItem> : null}
{extra}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};