Skip to content

Commit

Permalink
feat(component): tooltip for dropdown item (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-maksym-konohorov authored and chanceaclark committed Nov 6, 2019
1 parent 3769123 commit 4e5fc50
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 33 deletions.
4 changes: 2 additions & 2 deletions packages/big-design-theme/src/system/z-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export const zIndex: ZIndex = {
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
tooltip: 1060,
popover: 1070,
popover: 1060,
tooltip: 1070,
};
60 changes: 42 additions & 18 deletions packages/big-design/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { FlexItem } from '../Flex/Item';
import { Link } from '../Link';
import { List } from '../List';
import { ListItem } from '../List/Item';
import { Tooltip, TooltipProps } from '../Tooltip';

import { DropdownItem, DropdownLinkItem, DropdownProps } from './types';
import { DropdownLinkItem, DropdownOption, DropdownProps } from './types';

interface DropdownState {
highlightedItem: HTMLLIElement | null;
Expand All @@ -25,6 +26,10 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
private listRef: HTMLUListElement | null = null;
private triggerRef: HTMLElement | null = null;

private readonly tooltipModifiers: TooltipProps['modifiers'] = {
preventOverflow: { enabled: true, escapeWithReference: true },
offset: { offset: '0, 20' },
};
private readonly uniqueDropdownId = uniqueId('dropdown_');
private readonly uniqueTriggerId = uniqueId('trigger_');

Expand Down Expand Up @@ -92,25 +97,44 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
ref={ref}
role="option"
>
{option.type === 'link' && !option.disabled ? (
<Link href={option.url} target={option.target}>
<Flex alignItems="center" flexDirection="row">
{icon && <FlexItem paddingRight="xSmall">{this.renderIcon(option, isHighlighted)}</FlexItem>}
{content}
</Flex>
</Link>
) : (
<Flex alignItems="center" flexDirection="row">
{icon && <FlexItem paddingRight="xSmall">{this.renderIcon(option, isHighlighted)}</FlexItem>}
{content}
</Flex>
)}
{this.getContent(option, isHighlighted)}
</ListItem>
);
})
);
}

private wrapInLink(option: DropdownLinkItem<T>, content: React.ReactChild) {
return (
<Link href={option.url} target={option.target}>
{content}
</Link>
);
}

private getContent(option: DropdownOption<T>, isHighlighted: boolean) {
const { disabled, icon, tooltip } = option;

const baseContent = (
<Flex alignItems="center" flexDirection="row">
{icon && <FlexItem paddingRight="xSmall">{this.renderIcon(option, isHighlighted)}</FlexItem>}
{option.content}
</Flex>
);

const content = option.type === 'link' && !disabled ? this.wrapInLink(option, baseContent) : baseContent;

return disabled && tooltip ? this.wrapInTooltip(tooltip, content) : content;
}

private wrapInTooltip(tooltip: DropdownOption<T>['tooltip'], trigger: React.ReactChild) {
return (
<Tooltip placement="left" trigger={trigger} modifiers={this.tooltipModifiers} inline={false}>
{tooltip}
</Tooltip>
);
}

private renderTrigger(ref: RefHandler) {
const { trigger } = this.props;

Expand All @@ -129,7 +153,7 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
);
}

private renderIcon(item: DropdownItem<T> | DropdownLinkItem<T>, isHighlighted: boolean) {
private renderIcon(item: DropdownOption<T>, isHighlighted: boolean) {
return (
React.isValidElement(item.icon) &&
React.cloneElement(item.icon, {
Expand All @@ -139,7 +163,7 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
);
}

private iconColor(item: DropdownItem<T> | DropdownLinkItem<T>, isHighlighted: boolean) {
private iconColor(item: DropdownOption<T>, isHighlighted: boolean) {
if (item.disabled) {
return 'secondary40';
}
Expand Down Expand Up @@ -179,7 +203,7 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
return id || this.uniqueDropdownId;
}

private getItemId(item: DropdownItem<T> | DropdownLinkItem<T>, index: number) {
private getItemId(item: DropdownOption<T>, index: number) {
const { id } = item;

return id || `${this.getDropdownId()}-item-${index}`;
Expand Down Expand Up @@ -220,7 +244,7 @@ export class Dropdown<T extends any> extends React.PureComponent<DropdownProps<T
this.toggleList();
};

private handleOnItemClick = (item: DropdownItem<T> | DropdownLinkItem<T>) => {
private handleOnItemClick = (item: DropdownOption<T>) => {
if (item.disabled) {
return;
}
Expand Down
53 changes: 53 additions & 0 deletions packages/big-design/src/components/Dropdown/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,56 @@ test('does not forward styles', () => {
expect(container.getElementsByClassName('test').length).toBe(0);
expect(getByRole('listbox')).not.toHaveStyle('background: red');
});

test('renders tooltip with disabled item', () => {
const tooltipContent = 'Option with tooltip';
const tooltipText = 'This is tooltip message';
const { getByRole, getByText } = render(
<Dropdown
onClick={onClick}
options={[
{ content: 'Option 1', type: 'string', value: '0' },
{
content: tooltipContent,
tooltip: tooltipText,
disabled: true,
type: 'string',
},
{ content: 'Option 3', type: 'string', value: '2', actionType: 'destructive' },
]}
trigger={<Button>Button</Button>}
/>,
);
const trigger = getByRole('button');

fireEvent.click(trigger);
fireEvent.mouseEnter(getByText(tooltipContent));

expect(getByText(tooltipText)).toBeInTheDocument();
});

test("doesn't render tooltip on enabled item", () => {
const tooltipContent = 'Option with tooltip';
const tooltipText = 'This is tooltip message';
const { getByRole, getByText, queryByText } = render(
<Dropdown
onClick={onClick}
options={[
{ content: 'Option 1', type: 'string', value: '0' },
{
content: tooltipContent,
tooltip: tooltipText,
type: 'string',
},
{ content: 'Option 3', type: 'string', value: '2', actionType: 'destructive' },
]}
trigger={<Button>Button</Button>}
/>,
);
const trigger = getByRole('button');

fireEvent.click(trigger);
fireEvent.mouseEnter(getByText(tooltipContent));

expect(queryByText(tooltipText)).not.toBeInTheDocument();
});
7 changes: 5 additions & 2 deletions packages/big-design/src/components/Dropdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { Placement } from 'popper.js';

import { ListItemProps } from '../List/Item';

export type DropdownOption<T> = DropdownItem<T> | DropdownLinkItem<T>;

export interface DropdownProps<T> extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
maxHeight?: number;
options: Array<DropdownItem<T> | DropdownLinkItem<T>>;
options: Array<DropdownOption<T>>;
placement?: Placement;
trigger: React.ReactElement;
}

interface BaseItem<T> extends Omit<ListItemProps, 'children' | 'content' | 'onClick' | 'value'> {
content: string;
icon?: React.ReactElement;
tooltip?: string;
value?: T;
onClick?(item: DropdownItem<T> | DropdownLinkItem<T>): void;
onClick?(item: DropdownOption<T>): void;
}

export interface DropdownItem<T> extends BaseItem<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ exports[`render pagination component 1`] = `
outline: none;
overflow-y: scroll;
padding: 0.5rem 0;
z-index: 1070;
z-index: 1060;
}
.c8 {
Expand Down Expand Up @@ -596,7 +596,7 @@ exports[`render pagination component with invalid page info 1`] = `
outline: none;
overflow-y: scroll;
padding: 0.5rem 0;
z-index: 1070;
z-index: 1060;
}
.c8 {
Expand Down Expand Up @@ -1092,7 +1092,7 @@ exports[`render pagination component with invalid range info 1`] = `
outline: none;
overflow-y: scroll;
padding: 0.5rem 0;
z-index: 1070;
z-index: 1060;
}
.c8 {
Expand Down Expand Up @@ -1589,7 +1589,7 @@ exports[`render pagination component with no items 1`] = `
outline: none;
overflow-y: scroll;
padding: 0.5rem 0;
z-index: 1070;
z-index: 1060;
}
.c8 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ exports[`renders a pagination component 1`] = `
outline: none;
overflow-y: scroll;
padding: 0.5rem 0;
z-index: 1070;
z-index: 1060;
}
.c11 {
Expand Down
13 changes: 10 additions & 3 deletions packages/big-design/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { Small } from '../Typography';

import { StyledTooltip, StyledTooltipTrigger } from './styled';

export interface TooltipProps {
export interface TooltipProps extends React.HTMLAttributes<HTMLDivElement> {
placement: PopperProps['placement'];
trigger: React.ReactChild;
modifiers?: PopperProps['modifiers'];
inline?: boolean;
}

interface State {
Expand All @@ -18,6 +20,7 @@ interface State {
export class Tooltip extends React.PureComponent<TooltipProps, State> {
static defaultProps: Partial<TooltipProps> = {
placement: 'top',
inline: true,
};

state = {
Expand All @@ -38,13 +41,14 @@ export class Tooltip extends React.PureComponent<TooltipProps, State> {
}

render() {
const { children, trigger } = this.props;
const { children, trigger, inline } = this.props;

return (
<Manager>
<Reference>
{({ ref }) => (
<StyledTooltipTrigger
inline={inline}
onBlur={this.hideTooltip}
onFocus={this.showTooltip}
onKeyDown={this.onKeyDown}
Expand All @@ -58,7 +62,10 @@ export class Tooltip extends React.PureComponent<TooltipProps, State> {
</Reference>
{this.tooltipContainer
? createPortal(
<Popper placement={this.props.placement} modifiers={{ offset: { offset: '0, 8' } }}>
<Popper
placement={this.props.placement}
modifiers={{ offset: { offset: '0, 8' }, ...this.props.modifiers }}
>
{({ placement, ref, style }) =>
this.state.visible && (
<StyledTooltip ref={ref} style={style} data-placement={placement}>
Expand Down
11 changes: 9 additions & 2 deletions packages/big-design/src/components/Tooltip/styled.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { theme as defaultTheme } from '@bigcommerce/big-design-theme';
import styled from 'styled-components';
import styled, { css } from 'styled-components';

export const StyledTooltipTrigger = styled.div`
export const StyledTooltipTrigger = styled.div<{ inline?: boolean }>`
display: inline-block;
${({ inline }) =>
!inline &&
css`
display: block;
flex-grow: 1;
`}
`;

export const StyledTooltip = styled.div`
Expand Down
14 changes: 14 additions & 0 deletions packages/docs/PropTables/DropdownPropTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ const dropdownItemProps: Prop[] = [
types: '(item: DropdownItem): void',
description: 'Returns the item object.',
},
{
name: 'tooltip',
types: 'string',
description: (
<>
Adds tooltip for disabled item. Default placement is set to <Code highlight={false}>right</Code>.
</>
),
},
{
name: 'type',
types: "'string'",
Expand Down Expand Up @@ -136,6 +145,11 @@ const dropdownLinkProps: Prop[] = [
</>
),
},
{
name: 'tooltip',
types: "{ message: string, placement?: 'left' | 'right' }",
description: "Adds tooltip for disabled item. Placement is optional, if not passed - 'left' is set.",
},
{
name: 'type',
types: "'link'",
Expand Down
8 changes: 7 additions & 1 deletion packages/docs/pages/Dropdown/DropdownPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default () => (
value: 'copy',
icon: <AssignmentIcon />,
disabled: true,
tooltip: 'You cannot copy this item...',
},
{
content: 'Delete',
Expand All @@ -43,7 +44,12 @@ export default () => (
icon: <DeleteIcon />,
actionType: 'destructive',
},
{ content: 'Link', icon: <OpenInNewIcon />, type: 'link', url: '#' },
{
content: 'Link',
icon: <OpenInNewIcon />,
type: 'link',
url: '#',
},
]}
placement="bottom-start"
trigger={<Button>Open Menu</Button>}
Expand Down

0 comments on commit 4e5fc50

Please sign in to comment.