From 30f505bf3966ca0e9bc8108ef04e64b0a3d11dc0 Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Tue, 21 Sep 2021 19:43:51 -0300 Subject: [PATCH] Create dropdown component --- .../components/OrdersTableWidget/index.tsx | 68 ++++++++++- .../common/Dropdown/Drodown.stories.tsx | 48 ++++++++ .../components/common/Dropdown/index.tsx | 111 ++++++++++++++++++ .../components/common/Dropdown/styled.tsx | 11 ++ .../common/Dropdown/useOnClickOutside.tsx | 29 +++++ 5 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 src/apps/explorer/components/common/Dropdown/Drodown.stories.tsx create mode 100644 src/apps/explorer/components/common/Dropdown/index.tsx create mode 100644 src/apps/explorer/components/common/Dropdown/styled.tsx create mode 100644 src/apps/explorer/components/common/Dropdown/useOnClickOutside.tsx diff --git a/src/apps/explorer/components/OrdersTableWidget/index.tsx b/src/apps/explorer/components/OrdersTableWidget/index.tsx index 36aad2df1..9ac2d7283 100644 --- a/src/apps/explorer/components/OrdersTableWidget/index.tsx +++ b/src/apps/explorer/components/OrdersTableWidget/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import styled from 'styled-components' import { faSpinner } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -8,6 +8,7 @@ import { OrdersTableWithData } from './OrdersTableWithData' import { OrdersTableContext } from './context/OrdersTableContext' import { useGetOrders } from './useGetOrders' import { TabItemInterface } from 'components/common/Tabs/Tabs' +import { Dropdown } from '../../components/common/Dropdown' const StyledTabLoader = styled.span` padding-left: 4px; @@ -28,6 +29,69 @@ const tabItems = (isLoadingOrders: boolean): TabItemInterface[] => { ] } +const PaginationDropdownButton = styled.div` + font-size: 13px; + font-weight: normal; + white-space: nowrap; + cursor: pointer; +` +const PaginationWrapper = styled.div` + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 50px; + padding: 0 15px; +` + +const PaginationText = styled.p` + cursor: pointer; + white-space: nowrap; + font-size: ${({ theme }): string => theme.fontSizeDefault}; +` + +const DropdownPagination = styled(Dropdown)` + .dropdownItems { + min-width: 70px; + } +` +const PaginationItem = styled.option` + align-items: center; + cursor: pointer; +` + +const Pagination: React.FC = () => { + const [pageSize, setPageSize] = useState(20) + + const onSetPageSize = (pageOption: number): void => setPageSize(pageOption) + + return ( + + Rows per page: + {pageSize} ▼} + items={[10, 20, 30, 50].map((pageOption) => ( + onSetPageSize(pageOption)}> + {pageOption} + + ))} + /> + + ) +} + +const WrapperExtraComponents = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + height: 100%; +` + +const ExtraComponentNode: React.ReactNode = ( + + + +) interface Props { ownerAddress: string } @@ -43,7 +107,7 @@ const OrdersTableWidget: React.FC = ({ ownerAddress }) => { return ( - + ) } diff --git a/src/apps/explorer/components/common/Dropdown/Drodown.stories.tsx b/src/apps/explorer/components/common/Dropdown/Drodown.stories.tsx new file mode 100644 index 000000000..d8a7510a0 --- /dev/null +++ b/src/apps/explorer/components/common/Dropdown/Drodown.stories.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import styled, { css } from 'styled-components' + +import { Meta, Story } from '@storybook/react' +import { GlobalStyles, ThemeToggler } from 'storybook/decorators' + +import { Dropdown, DropdownProps, DropdownOption } from './index' + +export default { + title: 'ExplorerApp/Dropdown', + component: Dropdown, + decorators: [GlobalStyles, ThemeToggler], +} as Meta + +const itemsPerPage = [15, 30, 50, 100] +let pageSize = itemsPerPage[0] + +const onSetPageSize = (pageOption: number): void => { + pageSize = pageOption + console.info(`Selected option ${pageSize}`) +} + +const PaginationTextCSS = css` + color: ${({ theme }): string => theme.textPrimary1}; + font-size: 13px; + font-weight: normal; + white-space: nowrap; +` + +const PaginationDropdownButton = styled.div` + ${PaginationTextCSS} + cursor: pointer; + white-space: nowrap; +` + +const dropdownOptions = itemsPerPage.map((pageOption) => ( + onSetPageSize(pageOption)}> + {pageOption} + +)) + +const Template: Story = (args) => + +export const DefaultTabs = Template.bind({}) +DefaultTabs.args = { + items: dropdownOptions, + dropdownButtonContent: {pageSize} ▼, +} diff --git a/src/apps/explorer/components/common/Dropdown/index.tsx b/src/apps/explorer/components/common/Dropdown/index.tsx new file mode 100644 index 000000000..fcb6d7094 --- /dev/null +++ b/src/apps/explorer/components/common/Dropdown/index.tsx @@ -0,0 +1,111 @@ +import React, { DOMAttributes, useState, useCallback, createRef, ReactElement } from 'react' +import styled from 'styled-components' + +import { BaseCard } from './styled' +import useOnClickOutside from './useOnClickOutside' + +const Wrapper = styled.div<{ isOpen: boolean; disabled: boolean }>` + outline: none; + pointer-events: ${(props): string => (props.disabled ? 'none' : 'initial')}; + position: relative; + z-index: ${(props): string => (props.isOpen ? '100' : '50')}; + + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + } +` +const ButtonContainer = styled.div` + background-color: transparent; + border: none; + display: block; + outline: none; + padding: 0; + user-select: none; + width: 100%; +` +export interface DropdownProps extends DOMAttributes { + activeItemHighlight?: boolean | undefined + className?: string + closeOnClick?: boolean + currentItem?: number | undefined + disabled?: boolean + items: Array + triggerClose?: boolean + dropdownButtonContent?: React.ReactNode | string +} + +const Items = styled(BaseCard)<{ + fullWidth?: boolean + isOpen: boolean +}>` + border-radius: 5px; + border: ${({ theme }): string => `1px solid ${theme.borderPrimary}`}; + display: ${(props): string => (props.isOpen ? 'block' : 'none')}; + min-width: 160px; + position: absolute; + white-space: nowrap; +` + +export const DropdownOption = styled.li` + align-items: center; + cursor: pointer; +` + +export const Dropdown: React.FC = (props) => { + const { + activeItemHighlight = true, + closeOnClick = true, + className = '', + currentItem = 0, + disabled = false, + items, + dropdownButtonContent = '▼', + } = props + const [isOpen, setIsOpen] = useState(false) + const dropdownContainerRef = createRef() + useOnClickOutside(dropdownContainerRef, () => setIsOpen(!isOpen)) + + const onButtonClick = useCallback( + (e) => { + e.stopPropagation() + if (disabled) return + setIsOpen(!isOpen) + }, + [disabled, isOpen], + ) + + return ( + + {dropdownButtonContent} + + {items.map((item: ReactElement, index: number) => { + const isActive = activeItemHighlight && index === currentItem + + return React.cloneElement(item, { + className: `dropdown-item ${isActive && 'active'}`, + key: item.key ? item.key : index, + onClick: (e: Event) => { + e.stopPropagation() + + if (closeOnClick) { + setIsOpen(false) + } + + if (!item.props.onClick) { + return + } + + item.props.onClick() + }, + }) + })} + + + ) +} diff --git a/src/apps/explorer/components/common/Dropdown/styled.tsx b/src/apps/explorer/components/common/Dropdown/styled.tsx new file mode 100644 index 000000000..d9a850378 --- /dev/null +++ b/src/apps/explorer/components/common/Dropdown/styled.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components' + +export const BaseCard = styled.div<{ noPadding?: boolean }>` + display: flex; + flex-direction: column; + position: relative; +` + +BaseCard.defaultProps = { + noPadding: false, +} diff --git a/src/apps/explorer/components/common/Dropdown/useOnClickOutside.tsx b/src/apps/explorer/components/common/Dropdown/useOnClickOutside.tsx new file mode 100644 index 000000000..83396ad06 --- /dev/null +++ b/src/apps/explorer/components/common/Dropdown/useOnClickOutside.tsx @@ -0,0 +1,29 @@ +import { useEffect, RefObject } from 'react' + +type Event = MouseEvent | TouchEvent + +const useOnClickOutside = ( + ref: RefObject, + handler: (event: Event) => void, +): void => { + useEffect(() => { + const listener = (event: Event): void => { + const el = ref?.current + if (!el || el.contains((event?.target as Node) || null)) { + return + } + + handler(event) // Call the handler only if the click is outside of the element passed. + } + + document.addEventListener('mousedown', listener) + document.addEventListener('touchstart', listener) + + return (): void => { + document.removeEventListener('mousedown', listener) + document.removeEventListener('touchstart', listener) + } + }, [ref, handler]) // Reload only if ref or handler changes +} + +export default useOnClickOutside