Skip to content

Commit

Permalink
feat: Kebab Menu w/ popover (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriellsh committed Apr 28, 2020
1 parent b593875 commit 0e4a50d
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 12 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions packages/fuselage/src/components/Box/Position/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const top = (top) => ({ top });
const left = (left) => ({ left });
const right = (right) => ({ right });

function offset(el) {
function getOffset(el) {
return el.getBoundingClientRect();
}

Expand Down Expand Up @@ -55,7 +55,7 @@ const throttle = (func, limit) => {
};
};

export const Position = ({ anchor, width = 'stretch', style, className, children, placement = 'bottom center' }) => {
export const Position = ({ anchor, width = 'stretch', style, className, children, placement = 'bottom center'/* , offset*/ }) => {
const [position, setPosition] = useState();
const ref = useRef();

Expand All @@ -69,8 +69,8 @@ export const Position = ({ anchor, width = 'stretch', style, className, children
const [vertical, horizontal] = placement.split(' ');

const handlePosition = throttle(() => {
const anchorPosition = offset(anchor.current);
const elementPosition = offset(ref.current.parentElement);
const anchorPosition = getOffset(anchor.current);
const elementPosition = getOffset(ref.current.parentElement);

setPosition({
...width === 'stretch' && anchor.current && {
Expand Down Expand Up @@ -103,15 +103,15 @@ export const Position = ({ anchor, width = 'stretch', style, className, children
window.removeEventListener('resize', handlePosition);
resizer.current && resizer.current.unobserve(current);
};
}, [anchor.current, placement, offsetWidth]);
}, [anchor, placement, offsetWidth, width]);

const portalContainer = useMemo(() => {
const element = document.createElement('div');
document.body.appendChild(element);
return element;
}, []);

useEffect(() => () => document.body.removeChild(portalContainer), []);
useEffect(() => () => document.body.removeChild(portalContainer), [portalContainer]);

return ReactDOM.createPortal(
React.cloneElement(children, {
Expand All @@ -131,6 +131,6 @@ export const Position = ({ anchor, width = 'stretch', style, className, children
);
};

export const PositionAnimated = ({ width, placement, visible, children, ...props }) => (
<AnimatedVisibility visibility={visible}><Position placement={placement} width={width} {...props}>{children}</Position></AnimatedVisibility>
export const PositionAnimated = ({ width, offset, placement, visible, children, ...props }) => (
<AnimatedVisibility visibility={visible}><Position offset={offset} placement={placement} width={width} {...props}>{children}</Position></AnimatedVisibility>
);
67 changes: 67 additions & 0 deletions packages/fuselage/src/components/Menu/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useRef, useCallback } from 'react';

import {
Button,
PositionAnimated,
Options,
Icon,
useCursor,
} from '..';

const menuAction = ([selected], options) => {
options[selected].action();
};

const mapOptions = (options) => Object.entries(options).map(([value, { label }]) => [value, label]);

export const Menu = ({
options,
optionWidth = '240px',
placement = 'bottom right',
...props }) => {
const mappedOptions = mapOptions(options);
const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] = useCursor(-1, mappedOptions, (args, [, hide]) => {
menuAction(args, options);
reset();
hide();
});

const ref = useRef();
const onClick = useCallback(() => ref.current.focus() & show(), [show]);

const handleSelection = useCallback((args) => {
menuAction(args, options);
reset();
hide();
}, [hide, reset, options]);

return (
<>
<Button
ref={ref}
small
ghost
onClick={onClick}
onBlur={hide}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
{...props}
>
<Icon name='menu' size={20} />
</Button>
<PositionAnimated
width='auto'
visible={visible}
anchor={ref}
placement={placement}
>
<Options
width={optionWidth}
onSelect={handleSelection}
options={mappedOptions}
cursor={cursor}
/>
</PositionAnimated>
</>
);
};
32 changes: 32 additions & 0 deletions packages/fuselage/src/components/Menu/stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { action } from '@storybook/addon-actions';
import { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks';
// import LinkTo from '@storybook/addon-links/react';
// import { PropsVariationSection } from '../../../.storybook/helpers';
import { Box, Icon, Menu } from '../'
const options = {
'makeAdmin': {
label: <Box display='flex' alignItems='center'><Icon mie='x4' name='key' size='x16'/>Make Admin</Box>,
action: () => console.log('[...] is now admin'),
},
'delete': {
label: <Box display='flex' alignItems='center' textColor='danger'><Icon mie='x4' name='trash' size='x16'/>Delete</Box>,
action: () => console.log('[...] no longer exists'),
}
}

<Meta title='Misc/Menu' parameters={{ jest: ['Menu/spec'] }} />

# Menu

Kebab Menu

<Preview>
<Story name='Default'>
<Box style={{ position: 'relative', maxWidth: 250 }} >
<Menu options={options} />
</Box>
</Story>
</Preview>

<Props of={Menu} />

11 changes: 7 additions & 4 deletions packages/fuselage/src/components/Options/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useLayoutEffect, useState, forwardRef } from 'react';
import React, { useCallback, useLayoutEffect, useState, forwardRef, useMemo } from 'react';


import { AnimatedVisibility, Box, Flex, Margins, Scrollable } from '../Box';
Expand Down Expand Up @@ -42,6 +42,7 @@ export const OptionAvatar = React.memo(({ id, value, children: label, focus, sel

export const Options = React.forwardRef(({
maxHeight = '144px',
width = '240px',
multiple,
renderEmpty: EmptyComponent = Empty,
options,
Expand All @@ -59,14 +60,16 @@ export const Options = React.forwardRef(({
if (li.offsetTop + li.clientHeight > current.scrollTop + current.clientHeight || li.offsetTop - li.clientHeight < current.scrollTop) {
current.scrollTop = li.offsetTop;
}
}, [cursor]);
}, [cursor, ref]);

const optionsMemoized = useMemo(() => options.map(([value, label, selected], i) => <OptionComponent role='option' onMouseDown={(e) => prevent(e) & onSelect([value, label]) && false} key={value} value={value} selected={selected || (multiple !== true && null)} focus={cursor === i || null}>{label}</OptionComponent>), [options, multiple, cursor, onSelect]);
return <Box rcx-options is='div' {...props}>
<Tile padding='x8' elevation='2'>
<Scrollable vertical smooth>
<Margins blockStart='x4'>
<Tile ref={ref} elevation='0' padding='none' maxHeight={maxHeight} onMouseDown={prevent} onClick={prevent} is='ol' aria-multiselectable={multiple} role='listbox' aria-multiselectable='true' aria-activedescendant={options && options[cursor] && options[cursor][0]}>
<Tile ref={ref} elevation='0' padding='none' width={width} maxHeight={maxHeight} onMouseDown={prevent} onClick={prevent} is='ol' aria-multiselectable={multiple} role='listbox' aria-multiselectable='true' aria-activedescendant={options && options[cursor] && options[cursor][0]}>
{!options.length && <EmptyComponent/>}
{options.map(([value, label, selected], i) => <OptionComponent role='option' onMouseDown={(e) => prevent(e) & onSelect([value, label]) && false} key={value} value={value} selected={selected || (multiple !== true && null)} focus={cursor === i || null}>{label}</OptionComponent>)}
{optionsMemoized}
</Tile>
</Margins>
</Scrollable>
Expand Down
1 change: 1 addition & 0 deletions packages/fuselage/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ export * from './Tile';
export * from './ToggleSwitch';
export * from './Tooltip';
export * from './UrlInput';
export * from './Menu';

0 comments on commit 0e4a50d

Please sign in to comment.