Dropdown: New dropdown component (#52684)

* Progress

* Example usage in application

* Add FocusScope

* REmove unused type

* simplify it by removing trigger property

* Use new Menu.Item way to access component
This commit is contained in:
Torkel Ödegaard 2022-07-26 14:19:30 +02:00 committed by GitHub
parent 21dab47293
commit 6af5f71dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 4 deletions

View File

@ -0,0 +1,35 @@
import { Meta, Props } from '@storybook/addon-docs/blocks';
import { Dropdown } from './Dropdown';
<Meta title="MDX|Dropdown" component={Dropdown} />
# Dropdown
Hook up a menu or other overlay to any trigger.
### When to use
When you want a button, link or icon button to open a Menu or Popover. By default the trigger is click but it can be changed to show on hover using the
trigger property.
### Usage
```tsx
import { Dropdown, Menu, Button } from '@grafana/ui';
const menu = (
<Menu>
<Menu.Item label="Google" />
<Menu.Item label="Filter" />
</Menu>
);
return (
<Dropdown overlay={menu}>
<Button icon="bars" />
</Dropdown>
);
```
<Props of={Dropdown} />

View File

@ -0,0 +1,60 @@
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { StoryExample } from '../../utils/storybook/StoryExample';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Button } from '../Button';
import { VerticalGroup } from '../Layout/Layout';
import { Menu } from '../Menu/Menu';
import { Dropdown } from './Dropdown';
import mdx from './Dropdown.mdx';
const meta: ComponentMeta<typeof Dropdown> = {
title: 'Overlays/Dropdown',
component: Dropdown,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
controls: {
exclude: ['className'],
},
},
};
export function Examples() {
const menu = (
<Menu>
<Menu.Item label="View settings" tabIndex={0} />
<Menu.Item label="Edit actions" tabIndex={1} />
<Menu.Item label="Share" tabIndex={2} />
<Menu.Item label="Delete" tabIndex={3} />
</Menu>
);
return (
<VerticalGroup>
<StoryExample name="Button + defaults">
<Dropdown overlay={menu}>
<Button variant="secondary">Button</Button>
</Dropdown>
</StoryExample>
<StoryExample name="Icon button, placement=bottom-start">
<Dropdown overlay={menu} placement="bottom-start">
<Button variant="secondary" icon="bars" />
</Dropdown>
</StoryExample>
</VerticalGroup>
);
}
Examples.parameters = {
controls: {
hideNoControlsWarning: true,
include: [],
},
};
export default meta;

View File

@ -0,0 +1,81 @@
import { css } from '@emotion/css';
import { FocusScope } from '@react-aria/focus';
import React, { useState } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { CSSTransition } from 'react-transition-group';
import { ReactUtils } from '../../utils';
import { Portal } from '../Portal/Portal';
import { TooltipPlacement } from '../Tooltip/types';
export interface Props {
overlay: React.ReactElement | (() => React.ReactElement);
placement?: TooltipPlacement;
children: React.ReactElement;
}
export const Dropdown = React.memo(({ children, overlay, placement }: Props) => {
const [show, setShow] = useState(false);
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
visible: show,
placement: placement,
onVisibleChange: setShow,
interactive: true,
delayHide: 0,
delayShow: 0,
offset: [0, 8],
trigger: ['click'],
});
const animationDuration = 150;
const animationStyles = getStyles(animationDuration);
const onOverlayClicked = () => {
setShow(false);
};
return (
<>
{React.cloneElement(children, {
ref: setTriggerRef,
})}
{visible && (
<Portal>
<FocusScope autoFocus>
<div ref={setTooltipRef} {...getTooltipProps()} onClick={onOverlayClicked}>
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
<CSSTransition
appear={true}
in={true}
timeout={{ appear: animationDuration, exit: 0, enter: 0 }}
classNames={animationStyles}
>
{ReactUtils.renderOrCallToRender(overlay)}
</CSSTransition>
</div>
</FocusScope>
</Portal>
)}
</>
);
});
Dropdown.displayName = 'Dropdown';
const getStyles = (duration: number) => {
return {
appear: css`
opacity: 0;
position: relative;
transform: scaleY(0.5);
transform-origin: top;
`,
appearActive: css`
opacity: 1;
transform: scaleY(1);
transition: transform ${duration}ms cubic-bezier(0.2, 0, 0.2, 1),
opacity ${duration}ms cubic-bezier(0.2, 0, 0.2, 1);
`,
};
};

View File

@ -228,6 +228,7 @@ export { Card, Props as CardProps, getCardStyles } from './Card/Card';
export { CardContainer, CardContainerProps } from './Card/CardContainer';
export { FormattedValueDisplay } from './FormattedValueDisplay/FormattedValueDisplay';
export { ButtonSelect } from './Dropdown/ButtonSelect';
export { Dropdown } from './Dropdown/Dropdown';
export { PluginSignatureBadge, PluginSignatureBadgeProps } from './PluginSignatureBadge/PluginSignatureBadge';
// Export this until we've figured out a good approach to inline form styles.

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { FilterInput, Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { Dropdown, FilterInput, Icon, Menu, MenuItem, Tooltip, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
@ -36,15 +36,30 @@ export function TopSearchBar() {
</button>
</Tooltip>
<Tooltip placement="bottom" content="User profile (todo)">
<button className={styles.actionItem}>
<img src={contextSrv.user.gravatarUrl} />
</button>
<Dropdown overlay={ProfileMenu}>
<button className={styles.actionItem}>
<img src={contextSrv.user.gravatarUrl} />
</button>
</Dropdown>
</Tooltip>
</div>
</div>
);
}
/**
* This is just temporary, needs syncing with the backend option like DisableSignoutMenu
*/
export function ProfileMenu() {
return (
<Menu>
<MenuItem url="profile" label="Your profile" />
<MenuItem url="profile/notifications" label="Your notifications" />
<MenuItem url="logout" label="Sign out" />
</Menu>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
searchBar: css({