From 1e635b9d2f9a7cae0195d7bf852f2f23d00fe82a Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 19 Apr 2023 12:58:08 +0200 Subject: [PATCH] Add dropdown on Sort button on table --- README.md | 1 + .../form/__stories__/Checkbox.stories.tsx | 6 +- .../form/__tests__/Checkbox.test.tsx | 2 +- front/src/components/table/Table.tsx | 2 +- .../table/table-header/DropdownButton.tsx | 106 ++++++++++++++++++ .../table/{ => table-header}/TableHeader.tsx | 18 +-- .../__stories__/TableHeader.stories.tsx | 17 +++ .../__tests__/TableHeader.test.tsx | 9 ++ .../__tests__/useOutsideAlerter.test.tsx | 33 ++++++ front/src/hooks/useOutsideAlerter.ts | 23 ++++ front/src/layout/styles/themes.ts | 14 +++ 11 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 front/src/components/table/table-header/DropdownButton.tsx rename front/src/components/table/{ => table-header}/TableHeader.tsx (73%) create mode 100644 front/src/components/table/table-header/__stories__/TableHeader.stories.tsx create mode 100644 front/src/components/table/table-header/__tests__/TableHeader.test.tsx create mode 100644 front/src/hooks/__tests__/useOutsideAlerter.test.tsx create mode 100644 front/src/hooks/useOutsideAlerter.ts diff --git a/README.md b/README.md index 89dd1ab151..4db3360d44 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ cd infra/dev ``` make build +make up ``` Once this is completed you should have: diff --git a/front/src/components/form/__stories__/Checkbox.stories.tsx b/front/src/components/form/__stories__/Checkbox.stories.tsx index 903095f623..4d2bdc3767 100644 --- a/front/src/components/form/__stories__/Checkbox.stories.tsx +++ b/front/src/components/form/__stories__/Checkbox.stories.tsx @@ -1,5 +1,3 @@ -import { MemoryRouter } from 'react-router-dom'; - import Checkbox from '../Checkbox'; import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../layout/styles/themes'; @@ -12,9 +10,7 @@ export default { export const RegularCheckbox = () => { return ( - - - + ); }; diff --git a/front/src/components/form/__tests__/Checkbox.test.tsx b/front/src/components/form/__tests__/Checkbox.test.tsx index 3e0180ad35..b4b2c402e7 100644 --- a/front/src/components/form/__tests__/Checkbox.test.tsx +++ b/front/src/components/form/__tests__/Checkbox.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react'; import { RegularCheckbox } from '../__stories__/Checkbox.stories'; -it('Checks the NavItem renders', () => { +it('Checks the Checkbox renders', () => { const { getByTestId } = render(); expect(getByTestId('input-checkbox')).toHaveAttribute( diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index 844e21ad8d..2e7624f0a8 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -6,7 +6,7 @@ import { getCoreRowModel, useReactTable, } from '@tanstack/react-table'; -import TableHeader from './TableHeader'; +import TableHeader from './table-header/TableHeader'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import styled from '@emotion/styled'; diff --git a/front/src/components/table/table-header/DropdownButton.tsx b/front/src/components/table/table-header/DropdownButton.tsx new file mode 100644 index 0000000000..6aac670462 --- /dev/null +++ b/front/src/components/table/table-header/DropdownButton.tsx @@ -0,0 +1,106 @@ +import styled from '@emotion/styled'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useState, useRef } from 'react'; +import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter'; +import { modalBackground } from '../../../layout/styles/themes'; + +type OwnProps = { + label: string; + options: Array<{ label: string; icon: IconProp }>; +}; + +const StyledDropdownButtonContainer = styled.div` + display: flex; + flex-direction: column; + position: relative; +`; + +type StyledDropdownButtonProps = { + isUnfolded: boolean; +}; + +const StyledDropdownButton = styled.div` + display: flex; + margin-left: ${(props) => props.theme.spacing(3)}; + cursor: pointer; + background: ${(props) => props.theme.primaryBackground}; + padding: ${(props) => props.theme.spacing(1)}; + border-radius: 4px; + filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')}; + + &:hover { + filter: brightness(0.95); + } +`; + +const StyledDropdown = styled.ul` + display: flex; + position: absolute; + top: 14px; + right: 0; + border: 1px solid ${(props) => props.theme.primaryBorder}; + box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09); + border-radius: 8px; + padding: 0px; + min-width: 160px; + ${modalBackground} +`; + +const StyledDropdownItem = styled.li` + display: flex; + padding: ${(props) => props.theme.spacing(2)} + calc(${(props) => props.theme.spacing(2)} - 2px); + margin: 2px; + background: ${(props) => props.theme.primaryBackground}; + cursor: pointer; + width: 100%; + border-radius: 4px; + color: ${(props) => props.theme.text60}; + + &:hover { + filter: brightness(0.95); + } +`; + +const StyledIcon = styled.div` + display: flex; + margin-right: ${(props) => props.theme.spacing(1)}; +`; + +function DropdownButton({ label, options }: OwnProps) { + const [isUnfolded, setIsUnfolded] = useState(false); + + const onButtonClick = () => { + setIsUnfolded(!isUnfolded); + }; + + const onOutsideClick = () => { + setIsUnfolded(false); + }; + + const dropdownRef = useRef(null); + useOutsideAlerter(dropdownRef, onOutsideClick); + + return ( + + + {label} + + {isUnfolded && options.length > 0 && ( + + {options.map((option, index) => ( + + + + + {option.label} + + ))} + + )} + + ); +} + +export default DropdownButton; diff --git a/front/src/components/table/TableHeader.tsx b/front/src/components/table/table-header/TableHeader.tsx similarity index 73% rename from front/src/components/table/TableHeader.tsx rename to front/src/components/table/table-header/TableHeader.tsx index 7d26e62964..ac1f3c5f67 100644 --- a/front/src/components/table/TableHeader.tsx +++ b/front/src/components/table/table-header/TableHeader.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import DropdownButton from './DropdownButton'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faCalendar } from '@fortawesome/pro-regular-svg-icons'; type OwnProps = { viewName: string; @@ -30,12 +32,7 @@ const StyledViewSection = styled.div` const StyledFilters = styled.div` display: flex; font-weight: 400; - margin-right: ${(props) => props.theme.spacing(1)}; -`; - -const StyledFilterButton = styled.div` - display: flex; - margin-left: ${(props) => props.theme.spacing(4)}; + margin-right: ${(props) => props.theme.spacing(2)}; `; function TableHeader({ viewName, viewIcon }: OwnProps) { @@ -48,9 +45,12 @@ function TableHeader({ viewName, viewIcon }: OwnProps) { {viewName} - Filter - Sort - Settings + + + ); diff --git a/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx new file mode 100644 index 0000000000..dc804ba317 --- /dev/null +++ b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx @@ -0,0 +1,17 @@ +import TableHeader from '../TableHeader'; +import { ThemeProvider } from '@emotion/react'; +import { lightTheme } from '../../../../layout/styles/themes'; +import { faBuilding } from '@fortawesome/pro-regular-svg-icons'; + +export default { + title: 'TableHeader', + component: TableHeader, +}; + +export const RegularTableHeader = () => { + return ( + + + + ); +}; diff --git a/front/src/components/table/table-header/__tests__/TableHeader.test.tsx b/front/src/components/table/table-header/__tests__/TableHeader.test.tsx new file mode 100644 index 0000000000..d3d7fe83b3 --- /dev/null +++ b/front/src/components/table/table-header/__tests__/TableHeader.test.tsx @@ -0,0 +1,9 @@ +import { render } from '@testing-library/react'; + +import { RegularTableHeader } from '../__stories__/TableHeader.stories'; + +it('Checks the TableHeader renders', () => { + const { getByText } = render(); + + expect(getByText('Test')).toBeDefined(); +}); diff --git a/front/src/hooks/__tests__/useOutsideAlerter.test.tsx b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx new file mode 100644 index 0000000000..e9aa336440 --- /dev/null +++ b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx @@ -0,0 +1,33 @@ +const onOutsideClick = jest.fn(); +import { useRef } from 'react'; +import TableHeader from '../../components/table/table-header/TableHeader'; +import { render, fireEvent } from '@testing-library/react'; +import { useOutsideAlerter } from '../useOutsideAlerter'; +import { act } from 'react-dom/test-utils'; + +function TestComponent() { + const buttonRef = useRef(null); + useOutsideAlerter(buttonRef, onOutsideClick); + + return ( +
+ Outside + +
+ ); +} + +export default TableHeader; + +test('clicking the button toggles an answer on/off', async () => { + const { getByText } = render(); + const inside = getByText('Inside'); + const outside = getByText('Outside'); + await act(() => Promise.resolve()); + + fireEvent.mouseDown(inside); + expect(onOutsideClick).toHaveBeenCalledTimes(0); + + fireEvent.mouseDown(outside); + expect(onOutsideClick).toHaveBeenCalledTimes(1); +}); diff --git a/front/src/hooks/useOutsideAlerter.ts b/front/src/hooks/useOutsideAlerter.ts new file mode 100644 index 0000000000..746c44afba --- /dev/null +++ b/front/src/hooks/useOutsideAlerter.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; + +declare type CallbackType = () => void; + +export function useOutsideAlerter( + ref: React.RefObject, + callback: CallbackType, +) { + useEffect(() => { + function handleClickOutside(event: Event) { + console.log('test3'); + + const target = event.target as HTMLButtonElement; + if (ref.current && !ref.current.contains(target)) { + callback(); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref]); +} diff --git a/front/src/layout/styles/themes.ts b/front/src/layout/styles/themes.ts index 160f14d63e..74eee0bda3 100644 --- a/front/src/layout/styles/themes.ts +++ b/front/src/layout/styles/themes.ts @@ -1,3 +1,5 @@ +import { css } from '@emotion/react'; + const commonTheme = { fontSizeSmall: '0.92rem', fontSizeMedium: '1rem', @@ -21,6 +23,8 @@ const lightThemeSpecific = { purpleBackground: '#e0e0ff', yellowBackground: '#fff2e7', + secondaryBackgroundSmallTransparency: 'rgba(252, 252, 252, 0.8)', + primaryBorder: 'rgba(0, 0, 0, 0.08)', text100: '#000', @@ -49,6 +53,10 @@ const darkThemeSpecific = { purpleBackground: '#1111b7', yellowBackground: '#cc660a', + secondaryBackgroundSmallTransparency: 'rgba(23, 23, 23, 0.8)', + + primaryBorder: 'rgba(255, 255, 255, 0.08)', + text100: '#ffffff', text80: '#ccc', text60: '#999', @@ -64,6 +72,12 @@ const darkThemeSpecific = { yellow: '#fff2e7', }; +export const modalBackground = (props: any) => + css` + backdrop-filter: blur(20px); + background: ${props.theme.secondaryBackgroundSmallTransparency}; + `; + export const lightTheme = { ...commonTheme, ...lightThemeSpecific }; export const darkTheme = { ...commonTheme, ...darkThemeSpecific };