Add dropdown on Sort button on table
This commit is contained in:
@ -36,6 +36,7 @@ cd infra/dev
|
|||||||
|
|
||||||
```
|
```
|
||||||
make build
|
make build
|
||||||
|
make up
|
||||||
```
|
```
|
||||||
|
|
||||||
Once this is completed you should have:
|
Once this is completed you should have:
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Checkbox from '../Checkbox';
|
import Checkbox from '../Checkbox';
|
||||||
import { ThemeProvider } from '@emotion/react';
|
import { ThemeProvider } from '@emotion/react';
|
||||||
import { lightTheme } from '../../../layout/styles/themes';
|
import { lightTheme } from '../../../layout/styles/themes';
|
||||||
@ -12,9 +10,7 @@ export default {
|
|||||||
export const RegularCheckbox = () => {
|
export const RegularCheckbox = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<MemoryRouter initialEntries={['/companies']}>
|
<Checkbox name="selected-company-1" id="selected-company--1" />
|
||||||
<Checkbox name="selected-company-1" id="selected-company--1" />
|
|
||||||
</MemoryRouter>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { render } from '@testing-library/react';
|
|||||||
|
|
||||||
import { RegularCheckbox } from '../__stories__/Checkbox.stories';
|
import { RegularCheckbox } from '../__stories__/Checkbox.stories';
|
||||||
|
|
||||||
it('Checks the NavItem renders', () => {
|
it('Checks the Checkbox renders', () => {
|
||||||
const { getByTestId } = render(<RegularCheckbox />);
|
const { getByTestId } = render(<RegularCheckbox />);
|
||||||
|
|
||||||
expect(getByTestId('input-checkbox')).toHaveAttribute(
|
expect(getByTestId('input-checkbox')).toHaveAttribute(
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import TableHeader from './TableHeader';
|
import TableHeader from './table-header/TableHeader';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
|||||||
106
front/src/components/table/table-header/DropdownButton.tsx
Normal file
106
front/src/components/table/table-header/DropdownButton.tsx
Normal file
@ -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<StyledDropdownButtonProps>`
|
||||||
|
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 (
|
||||||
|
<StyledDropdownButtonContainer>
|
||||||
|
<StyledDropdownButton isUnfolded={isUnfolded} onClick={onButtonClick}>
|
||||||
|
{label}
|
||||||
|
</StyledDropdownButton>
|
||||||
|
{isUnfolded && options.length > 0 && (
|
||||||
|
<StyledDropdown ref={dropdownRef}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<StyledDropdownItem key={index}>
|
||||||
|
<StyledIcon>
|
||||||
|
<FontAwesomeIcon icon={option.icon} />
|
||||||
|
</StyledIcon>
|
||||||
|
{option.label}
|
||||||
|
</StyledDropdownItem>
|
||||||
|
))}
|
||||||
|
</StyledDropdown>
|
||||||
|
)}
|
||||||
|
</StyledDropdownButtonContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropdownButton;
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import DropdownButton from './DropdownButton';
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import { faCalendar } from '@fortawesome/pro-regular-svg-icons';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
viewName: string;
|
viewName: string;
|
||||||
@ -30,12 +32,7 @@ const StyledViewSection = styled.div`
|
|||||||
const StyledFilters = styled.div`
|
const StyledFilters = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-right: ${(props) => props.theme.spacing(1)};
|
margin-right: ${(props) => props.theme.spacing(2)};
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFilterButton = styled.div`
|
|
||||||
display: flex;
|
|
||||||
margin-left: ${(props) => props.theme.spacing(4)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function TableHeader({ viewName, viewIcon }: OwnProps) {
|
function TableHeader({ viewName, viewIcon }: OwnProps) {
|
||||||
@ -48,9 +45,12 @@ function TableHeader({ viewName, viewIcon }: OwnProps) {
|
|||||||
{viewName}
|
{viewName}
|
||||||
</StyledViewSection>
|
</StyledViewSection>
|
||||||
<StyledFilters>
|
<StyledFilters>
|
||||||
<StyledFilterButton>Filter</StyledFilterButton>
|
<DropdownButton label="Filter" options={[]} />
|
||||||
<StyledFilterButton>Sort</StyledFilterButton>
|
<DropdownButton
|
||||||
<StyledFilterButton>Settings</StyledFilterButton>
|
label="Sort"
|
||||||
|
options={[{ label: 'Created at', icon: faCalendar }]}
|
||||||
|
/>
|
||||||
|
<DropdownButton label="Settings" options={[]} />
|
||||||
</StyledFilters>
|
</StyledFilters>
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
);
|
);
|
||||||
@ -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 (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<TableHeader viewName="Test" viewIcon={faBuilding} />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { RegularTableHeader } from '../__stories__/TableHeader.stories';
|
||||||
|
|
||||||
|
it('Checks the TableHeader renders', () => {
|
||||||
|
const { getByText } = render(<RegularTableHeader />);
|
||||||
|
|
||||||
|
expect(getByText('Test')).toBeDefined();
|
||||||
|
});
|
||||||
33
front/src/hooks/__tests__/useOutsideAlerter.test.tsx
Normal file
33
front/src/hooks/__tests__/useOutsideAlerter.test.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<span>Outside</span>
|
||||||
|
<button ref={buttonRef}>Inside</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableHeader;
|
||||||
|
|
||||||
|
test('clicking the button toggles an answer on/off', async () => {
|
||||||
|
const { getByText } = render(<TestComponent />);
|
||||||
|
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);
|
||||||
|
});
|
||||||
23
front/src/hooks/useOutsideAlerter.ts
Normal file
23
front/src/hooks/useOutsideAlerter.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
declare type CallbackType = () => void;
|
||||||
|
|
||||||
|
export function useOutsideAlerter(
|
||||||
|
ref: React.RefObject<HTMLInputElement>,
|
||||||
|
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]);
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
const commonTheme = {
|
const commonTheme = {
|
||||||
fontSizeSmall: '0.92rem',
|
fontSizeSmall: '0.92rem',
|
||||||
fontSizeMedium: '1rem',
|
fontSizeMedium: '1rem',
|
||||||
@ -21,6 +23,8 @@ const lightThemeSpecific = {
|
|||||||
purpleBackground: '#e0e0ff',
|
purpleBackground: '#e0e0ff',
|
||||||
yellowBackground: '#fff2e7',
|
yellowBackground: '#fff2e7',
|
||||||
|
|
||||||
|
secondaryBackgroundSmallTransparency: 'rgba(252, 252, 252, 0.8)',
|
||||||
|
|
||||||
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
||||||
|
|
||||||
text100: '#000',
|
text100: '#000',
|
||||||
@ -49,6 +53,10 @@ const darkThemeSpecific = {
|
|||||||
purpleBackground: '#1111b7',
|
purpleBackground: '#1111b7',
|
||||||
yellowBackground: '#cc660a',
|
yellowBackground: '#cc660a',
|
||||||
|
|
||||||
|
secondaryBackgroundSmallTransparency: 'rgba(23, 23, 23, 0.8)',
|
||||||
|
|
||||||
|
primaryBorder: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
|
||||||
text100: '#ffffff',
|
text100: '#ffffff',
|
||||||
text80: '#ccc',
|
text80: '#ccc',
|
||||||
text60: '#999',
|
text60: '#999',
|
||||||
@ -64,6 +72,12 @@ const darkThemeSpecific = {
|
|||||||
yellow: '#fff2e7',
|
yellow: '#fff2e7',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const modalBackground = (props: any) =>
|
||||||
|
css`
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
background: ${props.theme.secondaryBackgroundSmallTransparency};
|
||||||
|
`;
|
||||||
|
|
||||||
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
||||||
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
|
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user