Skip to content

Commit

Permalink
fix(BoardControls): use Popover in smaller screens for responsiveness
Browse files Browse the repository at this point in the history
  • Loading branch information
remarkablemark committed Jul 3, 2023
1 parent ab4e836 commit fa5e474
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 53 deletions.
100 changes: 64 additions & 36 deletions src/components/BoardControls/BoardControls.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';

import { renderWithProviders, updateStore } from '../../utils/test';
import BoardControls from './BoardControls';
Expand All @@ -16,44 +16,72 @@ it('does not throw error when board is invalid', () => {
}).not.toThrow();
});

it('renders "Add column" button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(
screen.getByRole('button', { name: 'Add column' })
).toBeInTheDocument();
});
describe('desktop', () => {
let board: ReturnType<typeof updateStore.withBoard>;

it('renders "Timer" input and "Start" button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Timer in minutes')).toBeInTheDocument();
expect(screen.getByLabelText('Start timer')).toBeInTheDocument();
});
beforeEach(() => {
board = updateStore.withBoard();
updateStore.withUser();
});

it('renders "Hide Likes" switch', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Hide Likes')).toBeInTheDocument();
});
it('renders "Add column" button', () => {
renderWithProviders(<BoardControls boardId={board.id} />);
expect(
screen.getByRole('button', { name: 'Add column' })
).toBeInTheDocument();
});

it('renders "Timer" input and "Start" button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Timer in minutes')).toBeInTheDocument();
expect(screen.getByLabelText('Start timer')).toBeInTheDocument();
});

it('renders "Sort by likes" button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(
screen.getByRole('button', { name: 'Sort by likes' })
).toBeInTheDocument();
it('renders "Hide Likes" switch', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Hide Likes')).toBeInTheDocument();
});

it('renders "Sort by likes" button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(
screen.getByRole('button', { name: 'Sort by likes' })
).toBeInTheDocument();
});

it('renders copy board button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Copy board as Markdown')).toBe(
screen.getByRole('button', { name: 'Copy board as Markdown' })
);
});
});

it('renders copy board icon button', () => {
const board = updateStore.withBoard();
updateStore.withUser();
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getByLabelText('Copy board as Markdown')).toBe(
screen.getByRole('button', { name: 'Copy board as Markdown' })
);
describe('mobile', () => {
let board: ReturnType<typeof updateStore.withBoard>;

beforeEach(() => {
board = updateStore.withBoard();
updateStore.withUser();
});

it('toggles board controls', async () => {
renderWithProviders(<BoardControls boardId={board.id} />);
expect(screen.getAllByLabelText('Hide Likes')).toHaveLength(1);
fireEvent.click(screen.getByLabelText('Toggle board controls'));
expect(screen.getAllByLabelText('Hide Likes')).toHaveLength(2);
// eslint-disable-next-line testing-library/no-node-access
fireEvent.click(screen.getByRole('presentation').firstChild!);
await waitFor(() => {
expect(screen.getAllByLabelText('Hide Likes')).toHaveLength(1);
});
});
});
79 changes: 74 additions & 5 deletions src/components/BoardControls/BoardControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import MenuIcon from '@mui/icons-material/Menu';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Popover from '@mui/material/Popover';
import Stack from '@mui/material/Stack';
import { type MouseEvent, useCallback, useState } from 'react';

import type { Id } from '../../types';
import AddColumn from './AddColumn';
Expand All @@ -13,17 +18,81 @@ interface Props {
}

export default function BoardControls(props: Props) {
const [anchorElement, setAnchorElement] = useState<HTMLButtonElement | null>(
null
);

const handleClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) =>
setAnchorElement(event.currentTarget),
[setAnchorElement]
);

const handleClose = useCallback(
() => setAnchorElement(null),
[setAnchorElement]
);

const isPopoverOpen = Boolean(anchorElement);
const popoverId = isPopoverOpen ? 'board-controls-popover' : undefined;

return (
<Box display="flex" marginBottom={4}>
<Box flexGrow={1}>
<AddColumn boardId={props.boardId} />
</Box>

<Timer boardId={props.boardId} />
<MaxLikes boardId={props.boardId} />
<HideLikes />
<Sort boardId={props.boardId} />
<Export />
{/* desktop */}
<Stack direction="row" sx={{ display: { xs: 'none', md: 'flex' } }}>
<Timer sx={{ marginRight: 3 }} boardId={props.boardId} />
<MaxLikes sx={{ marginRight: 2 }} boardId={props.boardId} />
<HideLikes />
<Sort sx={{ marginRight: 0.5 }} boardId={props.boardId} />
<Export />
</Stack>

{/* mobile */}
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
<IconButton
aria-describedby={popoverId}
aria-label="Toggle board controls"
onClick={handleClick}
title="Toggle board controls"
>
<MenuIcon />
</IconButton>

<Popover
anchorEl={anchorElement}
anchorOrigin={{
horizontal: 'right',
vertical: 'bottom',
}}
id={popoverId}
onClose={handleClose}
open={isPopoverOpen}
transformOrigin={{
horizontal: 'right',
vertical: 'top',
}}
>
<Stack>
<Timer sx={{ margin: 2 }} boardId={props.boardId} />
<MaxLikes sx={{ margin: 2 }} boardId={props.boardId} />
<HideLikes sx={{ marginLeft: 1, marginRight: 1 }} />
<Sort sx={{ margin: 2 }} boardId={props.boardId} />
<Box
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: 1,
}}
>
<Export />
</Box>
</Stack>
</Popover>
</Box>
</Box>
);
}
15 changes: 8 additions & 7 deletions src/components/BoardControls/Export.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import type { SxProps } from '@mui/system';
import { createSelector } from '@reduxjs/toolkit';

import { logEvent } from '../../firebase';
Expand All @@ -13,7 +14,11 @@ const selectColumns = createSelector(
(columns) => columns
);

export default function Export() {
interface Props {
sx?: SxProps;
}

export default function Export(props: Props) {
const columns = useSelector(selectColumns);
const items = useSelector((state) => state.items);

Expand All @@ -24,12 +29,8 @@ export default function Export() {
}

return (
<Tooltip arrow title="Copy board as Markdown">
<IconButton
color="info"
onClick={copyBoardMarkdownToClipboard}
sx={{ marginLeft: 0.5 }}
>
<Tooltip arrow sx={props.sx} title="Copy board as Markdown">
<IconButton color="info" onClick={copyBoardMarkdownToClipboard}>
<FileDownloadIcon />
</IconButton>
</Tooltip>
Expand Down
9 changes: 7 additions & 2 deletions src/components/BoardControls/HideLikes.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import type { SxProps } from '@mui/system';

import { logEvent } from '../../firebase';
import { useDispatch, useSelector } from '../../hooks';
import { actions } from '../../store';

export default function HideLikes() {
interface Props {
sx?: SxProps;
}

export default function HideLikes(props: Props) {
const dispatch = useDispatch();
const hideLikes = useSelector((state) => state.user.hideLikes);

Expand All @@ -20,7 +25,7 @@ export default function HideLikes() {
<Switch checked={hideLikes} color="primary" onChange={handleChange} />
}
label="Hide Likes"
sx={{ marginRight: 0 }}
sx={props.sx}
/>
);
}
4 changes: 3 additions & 1 deletion src/components/BoardControls/MaxLikes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import TextField from '@mui/material/TextField';
import type { SxProps } from '@mui/system';
import type { ChangeEvent } from 'react';

import { logEvent } from '../../firebase';
Expand All @@ -8,6 +9,7 @@ import type { Id } from '../../types';

interface Props {
boardId: Id;
sx?: SxProps;
}

export default function MaxLikes(props: Props) {
Expand Down Expand Up @@ -46,7 +48,7 @@ export default function MaxLikes(props: Props) {
label="Max Likes"
onChange={handleChange}
size="small"
sx={{ marginRight: 2 }}
sx={props.sx}
type="number"
value={maxLikes}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/components/BoardControls/Sort.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Button from '@mui/material/Button';
import type { SxProps } from '@mui/system';

import { logEvent } from '../../firebase';
import { useDispatch, useIsAdmin, useSelector } from '../../hooks';
Expand All @@ -8,6 +9,7 @@ import { sortByLikes } from './utils';

interface Props {
boardId: Id;
sx?: SxProps;
}

export default function Sort(props: Props) {
Expand Down Expand Up @@ -37,8 +39,8 @@ export default function Sort(props: Props) {
<Button
color="primary"
onClick={sortItems}
sx={props.sx}
variant="outlined"
sx={{ marginLeft: 2 }}
>
Sort by likes
</Button>
Expand Down
4 changes: 3 additions & 1 deletion src/components/BoardControls/Timer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import type { SxProps } from '@mui/system';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';

Expand All @@ -20,6 +21,7 @@ const initialState = {

interface Props {
boardId: Id;
sx?: SxProps;
}

export default function Timer(props: Props) {
Expand Down Expand Up @@ -105,7 +107,7 @@ export default function Timer(props: Props) {
}

return (
<Box display="flex" alignItems="flex-end" marginRight={3}>
<Box display="flex" alignItems="flex-end" sx={props.sx}>
<Snackbar message="⏰ Time's up!" lastOpened={state.lastOpened} />
<TextField
disabled={Boolean(timerEnd)}
Expand Down

0 comments on commit fa5e474

Please sign in to comment.