diff --git a/src/components/dropdown-menu/DropdownMenu.tsx b/src/components/dropdown-menu/DropdownMenu.tsx deleted file mode 100644 index 87913b9c..00000000 --- a/src/components/dropdown-menu/DropdownMenu.tsx +++ /dev/null @@ -1,160 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error -// @ts-ignore This import fails when compiling to CJS. -import { Menu } from '@headlessui/react'; -import type { Placement } from '@popperjs/core'; -import { ReactNode, useRef, ElementType, ComponentProps } from 'react'; - -import { useModifiedPopper } from '../hooks/useModifiedPopper'; -import { useOnClickOutside } from '../hooks/useOnClickOutside'; -import { useOnOff } from '../hooks/useOnOff'; -import { Portal } from '../root-layout/Portal'; - -import { MenuItems, MenuOption, MenuOptions } from './MenuItems'; -import { useContextMenuPlacement } from './useContextMenuPlacement'; - -interface DropdownMenuBaseProps { - /** - * Placement for react-popper - */ - placement?: Placement; - - options: MenuOptions; - onSelect: (selected: MenuOption) => void; -} - -type ElementProps = E extends ElementType - ? ComponentProps - : never; - -interface DropdownMenuClickProps extends DropdownMenuBaseProps { - /** - * Node to be inside the Button - */ - children: ReactNode; - trigger: 'click'; -} - -interface DropdownMenuContextProps extends DropdownMenuBaseProps { - trigger: 'contextMenu'; - children: ReactNode; - as?: E; -} - -export type DropdownMenuProps = - | DropdownMenuContextProps - | DropdownMenuClickProps; - -export function DropdownMenu( - props: DropdownMenuProps & - Omit, keyof DropdownMenuProps>, -) { - const { trigger, ...otherProps } = props; - - if (trigger === 'contextMenu') { - return {...props} />; - } - return ( - {props.children} - ); -} - -function DropdownContextMenu( - props: Omit, 'trigger'> & - Omit, keyof DropdownMenuProps>, -) { - const { - children, - onSelect, - as: Wrapper = 'div', - placement = 'right-start', - options, - ...otherProps - } = props; - - const { - isPopperElementOpen, - closePopperElement, - handleContextMenu, - setPopperElement, - styles, - attributes, - } = useContextMenuPlacement(placement); - - const ref = useRef(null); - useOnClickOutside(ref, closePopperElement); - - const { style = {}, ...otherWrapperProps } = otherProps as ElementProps; - - return ( - - <> - {isPopperElementOpen && ( - -
-
- - { - closePopperElement(); - onSelect(selected); - }} - options={options} - /> - -
-
-
- )} - - {children} - -
- ); -} - -function DropdownClickMenu( - props: Omit, 'trigger'> & { children: ReactNode }, -) { - const { placement = 'bottom-start', onSelect, ...otherProps } = props; - - const [isOpened, , closeItems, toggle] = useOnOff(false); - - const ref = useRef(null); - useOnClickOutside(ref, closeItems); - - const { setReferenceElement, setPopperElement, popperProps } = - useModifiedPopper({ placement, offset: 6 }); - - return ( - - - {props.children} - - {isOpened && ( - -
-
- { - closeItems(); - onSelect(selected); - }} - {...otherProps} - /> -
-
-
- )} -
- ); -} diff --git a/src/components/dropdown-menu/MenuItems.tsx b/src/components/dropdown-menu/MenuItems.tsx deleted file mode 100644 index 258711c1..00000000 --- a/src/components/dropdown-menu/MenuItems.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import styled from '@emotion/styled'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error -// @ts-ignore This import fails when compiling to CJS. -import { Menu } from '@headlessui/react'; -import type { ReactNode } from 'react'; - -export interface MenuOption { - type: 'option'; - label: ReactNode; - icon?: ReactNode; - disabled?: boolean; - data?: T; -} - -export interface MenuDivider { - type: 'divider'; -} - -export type MenuOptions = Array | MenuDivider>; - -const ItemDiv = styled.div<{ - disabled: boolean; - active: boolean; -}>` - display: contents; - cursor: ${(props) => (props.disabled ? 'default' : 'pointer')}; - font-size: 0.875rem; - color: ${(props) => (!props.disabled ? 'black' : 'rgb(163, 163, 163)')}; - & > div { - padding-top: 2px; - padding-bottom: 2px; - ${(props) => props.active && 'background-color: rgb(243, 244, 246);'} - } - - ${(props) => - !props.disabled && - ` - &:hover > div { - background-color: rgb(243, 244, 246); - } - `} -`; - -const Divider = styled.hr` - width: 100%; - color: rgb(229, 229, 229); - margin-top: 5px; - margin-bottom: 5px; - grid-column: 1 / -1; -`; - -const ItemsDiv = styled.div<{ - hasOneIconOrMore: boolean; -}>` - display: grid; - grid-template-columns: [icon-start] ${(props) => - props.hasOneIconOrMore ? '40px' : '0px'} [label-start] auto; - width: fit-content; - align-items: center; - border-radius: 6px; - background-color: white; - box-shadow: - rgba(0, 0, 0, 0.3) 0px 19px 38px, - rgba(0, 0, 0, 0.22) 0px 5px 12px; - padding-top: 5px; - padding-bottom: 5px; - --cell-padding: 16px; -`; - -const LabelDiv = styled.div` - grid-column-start: label-start; - padding-right: var(--cell-padding); - padding-left: var(--cell-padding); -`; - -const IconDiv = styled.div` - grid-column-start: icon-start; - width: 100%; - height: 100%; - padding-left: var(--cell-padding); - display: flex; - justify-content: center; - align-items: center; -`; - -export interface MenuItemsProps { - options: MenuOptions; - onSelect: (selected: MenuOption) => void; - itemsStatic?: boolean; -} - -export function MenuItems(props: MenuItemsProps) { - const { options, onSelect, itemsStatic } = props; - const hasOneOrMoreIcon = options.some( - (option) => option.type === 'option' && option.icon, - ); - - return ( - - {options.map((option, index) => ( - - ))} - - ); -} - -interface ItemProps { - option: MenuOptions[number]; - onSelect: MenuItemsProps['onSelect']; -} - -function Item(props: ItemProps) { - const { option, onSelect } = props; - const isDivider = option.type === 'divider'; - - if (isDivider) { - return ; - } - - return ( - - {({ active }) => ( - { - event.stopPropagation(); - onSelect(option); - }} - active={active} - disabled={option.disabled || false} - > - {option.icon} - {option.label} - - )} - - ); -} diff --git a/src/components/dropdown-menu/index.ts b/src/components/dropdown-menu/index.ts deleted file mode 100644 index 123eed9a..00000000 --- a/src/components/dropdown-menu/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './DropdownMenu'; -export type { MenuOptions, MenuOption, MenuDivider } from './MenuItems'; diff --git a/src/components/dropdown-menu/useContextMenuPlacement.ts b/src/components/dropdown-menu/useContextMenuPlacement.ts deleted file mode 100644 index 687d1b5d..00000000 --- a/src/components/dropdown-menu/useContextMenuPlacement.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Placement } from '@popperjs/core'; -import React, { useCallback, useMemo, useState } from 'react'; -import { usePopper } from 'react-popper'; - -interface PositionState { - clientX: number; - clientY: number; - pageX: number; - pageY: number; -} - -export function useContextMenuPlacement(placement: Placement) { - const [positionState, setPositionState] = useState( - null, - ); - - const handleContextMenu = useCallback( - (event: React.MouseEvent) => { - const { clientX, clientY, pageX, pageY } = event; - event.preventDefault(); - - setPositionState({ - clientX, - clientY, - pageX, - pageY, - }); - }, - [], - ); - - const boundingClientRect = useMemo(() => { - return { - top: positionState?.clientY || 0, - left: positionState?.clientX || 0, - x: positionState?.pageX || 0, - y: positionState?.pageY || 0, - - bottom: 0, - right: 0, - height: 0, - width: 0, - toJSON: () => '', - }; - }, [positionState]); - - const virtualElement = useMemo(() => { - return { - getBoundingClientRect: () => boundingClientRect, - }; - }, [boundingClientRect]); - - const [popperElement, setPopperElement] = useState( - null, - ); - - const { styles, attributes, state } = usePopper( - virtualElement, - popperElement, - { - placement, - }, - ); - - return { - setPopperElement, - styles, - attributes, - popperState: state, - isPopperElementOpen: positionState !== null, - handleContextMenu, - closePopperElement: () => { - setPositionState(null); - }, - }; -} diff --git a/src/components/index.ts b/src/components/index.ts index b6c11aa1..959850aa 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,7 +2,6 @@ export * from './accordion/index'; export * from './button/index'; export * from './color-picker/index'; export * from './drop-zone/index'; -export * from './dropdown-menu/index'; export * from './forms/index'; export * from './fullscreen/index'; export * from './header/index'; diff --git a/stories/components/context-menu.stories.tsx b/stories/components/context-menu.stories.tsx new file mode 100644 index 00000000..2746aac9 --- /dev/null +++ b/stories/components/context-menu.stories.tsx @@ -0,0 +1,162 @@ +/** @jsxImportSource @emotion/react */ +import { ContextMenu, Menu, MenuDivider, MenuItem } from '@blueprintjs/core'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { Table, ValueRenderers } from '../../src/components/index'; +import data from '../data/table.json'; + +export default { + title: 'Components / ContextMenu', +}; + +export function ContextDropdown() { + const content = ( + + + + + + + + + + ); + + return ( + +

Hello, World!

+
+ ); +} + +export function TableWithHeaderDropDownMenu() { + const content = ( + + + + + + + + + + + + + ); + + return ( + + + + + + + + + + + + + + {data + .slice(0, 2) + .map(({ id, name, rn, mw, em, isExpensive, color }) => ( + + + + + + + + + + ))} + +
+ ); +} + +function ColumnWithDropdownMenu({ + value, + content, +}: { + value: string; + content: JSX.Element; +}) { + return ( + + {value} + + ); +} + +const TableWithContext = styled.table` + border: 0.55px solid gray; + th, + td { + border: 0.55px solid gray; + padding: 0.4em; + } +`; + +export function TableWithContextMenu() { + const headerContent = ( + + + + + ); + const content = ( + + + + + + + + + + + + + ); + + return ( + + + + id + name + rn + mw + em + isExpensive + + + + {data.slice(0, 2).map(({ id, name, rn, mw, em, isExpensive }) => ( + + {id} + {name} + {rn} + {mw} + {em} + {isExpensive} + + ))} + + + ); +} diff --git a/stories/components/dropdown.stories.tsx b/stories/components/dropdown.stories.tsx index 4c2a40ff..ce0f6491 100644 --- a/stories/components/dropdown.stories.tsx +++ b/stories/components/dropdown.stories.tsx @@ -1,359 +1,31 @@ -import styled from '@emotion/styled'; -import { Menu } from '@headlessui/react'; -import { useMemo } from 'react'; -import { - FaMeteor, - FaAddressBook, - FaAccessibleIcon, - Fa500Px, - FaAccusoft, - FaAcquisitionsIncorporated, - FaAd, - FaAddressCard, - FaAdjust, -} from 'react-icons/fa'; +import { Menu, MenuDivider, MenuItem, Popover } from '@blueprintjs/core'; -import { - MenuItems, - MenuOptions, -} from '../../src/components/dropdown-menu/MenuItems'; -import { - DropdownMenu, - Table, - ValueRenderers, -} from '../../src/components/index'; -import data from '../data/table.json'; +import { Button } from '../../src/components/index'; export default { title: 'Components / DropdownMenu', }; -const defaultOptions: MenuOptions = [ - { label: 'Default workspace', type: 'option' }, -]; - -function noop() { - // do nothing -} - -const ButtonStyled = styled.div` - box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - color: white; - font-weight: 600; - font-size: 0.875rem; - line-height: 1.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - background-color: rgb(107 114 128); - border-color: transparent; - border-radius: 0.375rem; - border-width: 1px; -`; - export function Dropdown() { - const options = useMemo>(() => { - return [ - { label: 'Default workspace', type: 'option' }, - { label: 'Exercise', type: 'option', icon: }, - { type: 'divider' }, - { label: 'Test', type: 'option', disabled: true }, - { label: 'Test 2', type: 'option' }, - ]; - }, []); - - return ( - - Default workspace - - ); -} - -const DivContextDropdown = styled.div` - height: 500px; - width: 500px; - border: 1px solid black; - display: flex; - justify-content: center; - align-items: center; -`; - -export function ContextDropdown() { - const options = useMemo>(() => { - return [ - { label: 'Default workspace', type: 'option' }, - { label: 'Exercise', type: 'option', icon: }, - { type: 'divider' }, - { label: 'Test', type: 'option', disabled: true }, - { label: 'Test 2', type: 'option' }, - ]; - }, []); - - return ( - - -

Hello, World!

-
-
- ); -} - -export function WithIcon() { - const options = useMemo>(() => { - return [ - ...defaultOptions, - { label: 'Exercise', type: 'option', icon: }, - ]; - }, []); - - return ( - - - - ); -} - -export function WithoutIcon() { - const options = useMemo>(() => { - return [...defaultOptions, { label: 'Exercise', type: 'option' }]; - }, []); - - return ( - - - - ); -} - -export function WithDisabled() { - const options = useMemo>(() => { - return [ - ...defaultOptions, - { label: 'Exercise', type: 'option', disabled: true }, - ]; - }, []); - - return ( - - - - ); -} - -export function WithDivider() { - const options = useMemo>(() => { - return [ - ...defaultOptions, - { type: 'divider' }, - { label: 'Exercise', type: 'option' }, - ]; - }, []); - - return ( - - - - ); -} - -export function Complex() { - const options = useMemo>(() => { - return [ - { label: 'Back', type: 'option', icon: }, - { - label: 'Forward', - type: 'option', - disabled: true, - icon: , - }, - { label: 'Refresh', type: 'option', icon: }, - { type: 'divider' }, - { label: 'Save as', type: 'option', icon: }, - { label: 'Print', type: 'option', icon: }, - { label: 'Cast media to device', type: 'option', icon: }, - { type: 'divider' }, - { - label: 'Send page to your devices', - type: 'option', - icon: , - }, - { - label: 'Create QR Code for this page', - type: 'option', - icon: , - }, - ]; - }, []); - - return ( + const content = ( - + + + + + + + ); -} -export function TableWithHeaderDropDownMenu() { - const options = useMemo>(() => { - return [ - { label: 'Back', type: 'option', icon: }, - { - label: 'Forward', - type: 'option', - disabled: true, - icon: , - }, - { label: 'Refresh', type: 'option', icon: }, - { type: 'divider' }, - { label: 'Save as', type: 'option', icon: }, - { label: 'Print', type: 'option', icon: }, - { label: 'Cast media to device', type: 'option', icon: }, - { type: 'divider' }, - { - label: 'Send page to your devices', - type: 'option', - icon: , - }, - { - label: 'Create QR Code for this page', - type: 'option', - icon: , - }, - ]; - }, []); - - return ( - - - - - - - - - - - - - - {data - .slice(0, 2) - .map(({ id, name, rn, mw, em, isExpensive, color }) => ( - - - - - - - - - - ))} - -
- ); -} - -function ColumnWithDropdownMenu({ - value, - options, -}: { - value: string; - options: MenuOptions; -}) { - return ( - - -
{value}
-
- - ); -} - -const TableWithContext = styled.table` - border: 0.55px solid gray; - th, - td { - border: 0.55px solid gray; - padding: 0.4em; - } -`; - -export function TableWithContextMenu() { - const headerOptions = useMemo>(() => { - return [ - { label: 'Copy', type: 'option', icon: }, - { - label: 'Past', - type: 'option', - disabled: true, - icon: , - }, - ]; - }, []); - const options = useMemo>(() => { - return [ - { label: 'Back', type: 'option', icon: }, - { - label: 'Forward', - type: 'option', - disabled: true, - icon: , - }, - { label: 'Refresh', type: 'option', icon: }, - { type: 'divider' }, - { label: 'Save as', type: 'option', icon: }, - { label: 'Print', type: 'option', icon: }, - { label: 'Cast media to device', type: 'option', icon: }, - { type: 'divider' }, - { - label: 'Send page to your devices', - type: 'option', - icon: , - }, - { - label: 'Create QR Code for this page', - type: 'option', - icon: , - }, - ]; - }, []); return ( - - - - id - name - rn - mw - em - isExpensive - - - - {data.slice(0, 2).map(({ id, name, rn, mw, em, isExpensive }) => ( - - {id} - {name} - {rn} - {mw} - {em} - {isExpensive} - - ))} - - + +