Design fixes (#665)

This commit is contained in:
Charles Bochet
2023-07-14 18:43:16 -07:00
committed by GitHub
parent 0a319bcf86
commit b971464fe5
22 changed files with 464 additions and 138 deletions

View File

@ -1,33 +1,118 @@
import React from 'react';
import styled from '@emotion/styled';
const StyledIconButton = styled.button`
export type IconButtonVariant = 'transparent' | 'border' | 'shadow' | 'white';
export type IconButtonSize = 'large' | 'medium' | 'small';
export type ButtonProps = {
icon?: React.ReactNode;
variant?: IconButtonVariant;
size?: IconButtonSize;
} & React.ComponentProps<'button'>;
const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
align-items: center;
background: ${({ theme }) => theme.color.blue};
border: none;
background: ${({ theme, variant, disabled }) => {
switch (variant) {
case 'shadow':
case 'white':
return theme.background.transparent.lighter;
case 'transparent':
case 'border':
default:
return 'transparent';
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'border':
return theme.border.color.medium;
case 'shadow':
case 'white':
case 'transparent':
default:
return 'none';
}
}};
transition: background 0.1s ease;
border-radius: ${({ theme }) => {
return theme.border.radius.sm;
}};
border-width: ${({ variant }) => {
switch (variant) {
case 'border':
return '1px';
case 'shadow':
case 'white':
case 'transparent':
default:
return 0;
}
}};
color: ${({ theme, disabled }) => {
if (disabled) {
return theme.font.color.extraLight;
}
border-radius: 50%;
color: ${({ theme }) => theme.font.color.inverted};
cursor: pointer;
return theme.font.color.tertiary;
}};
border-style: solid;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
height: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
display: flex;
height: 20px;
justify-content: center;
padding: 0;
transition: color 0.1s ease-in-out, background 0.1s ease-in-out;
width: 20px;
&:disabled {
background: ${({ theme }) => theme.background.quaternary};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: default;
width: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
flex-shrink: 0;
&:hover {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.light;
}};
}
user-select: none;
&:active {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.medium;
}};
`;
export function IconButton({
icon,
title,
variant = 'transparent',
size = 'medium',
disabled = false,
...props
}: { icon: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <StyledIconButton {...props}>{icon}</StyledIconButton>;
}: ButtonProps) {
return (
<StyledIconButton
variant={variant}
size={size}
disabled={disabled}
{...props}
>
{icon}
</StyledIconButton>
);
}

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
const StyledIconButton = styled.button`
align-items: center;
background: ${({ theme }) => theme.color.blue};
border: none;
border-radius: 50%;
color: ${({ theme }) => theme.font.color.inverted};
cursor: pointer;
display: flex;
height: 20px;
justify-content: center;
padding: 0;
transition: color 0.1s ease-in-out, background 0.1s ease-in-out;
width: 20px;
&:disabled {
background: ${({ theme }) => theme.background.quaternary};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: default;
}
`;
export function RoundedIconButton({
icon,
...props
}: { icon: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <StyledIconButton {...props}>{icon}</StyledIconButton>;
}

View File

@ -1,33 +1,153 @@
import React from 'react';
import styled from '@emotion/styled';
import { withKnobs } from '@storybook/addon-knobs';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons';
import { IconUser } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { IconButton } from '../IconButton';
type IconButtonProps = React.ComponentProps<typeof IconButton>;
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
`;
const StyledIconButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
width: 50px;
`;
const meta: Meta<typeof IconButton> = {
title: 'UI/Buttons/IconButton',
component: IconButton,
decorators: [withKnobs],
};
export default meta;
type Story = StoryObj<typeof IconButton>;
const variants: IconButtonProps['variant'][] = [
'transparent',
'border',
'shadow',
'white',
];
const clickJestFn = jest.fn();
export const Default: Story = {
const states = {
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
},
};
function IconButtonRow({ variant, size, ...props }: IconButtonProps) {
const iconSize = size === 'small' ? 14 : 16;
return (
<>
{Object.entries(states).map(([state, { description, extraProps }]) => (
<StyledIconButtonContainer key={`${variant}-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<IconButton
{...props}
{...extraProps(variant ?? '')}
variant={variant}
size={size}
icon={<IconUser size={iconSize} />}
/>
</StyledIconButtonContainer>
))}
</>
);
}
const generateStory = (
size: IconButtonProps['size'],
LineComponent: React.ComponentType<IconButtonProps>,
): Story => ({
render: getRenderWrapperForComponent(
<IconButton onClick={clickJestFn} icon={<IconArrowRight size={15} />} />,
<StyledContainer>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
<LineComponent size={size} variant={variant} />
</StyledLine>
</div>
))}
</StyledContainer>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
const button = canvas.getByTestId(`transparent-button-default`);
expect(clickJestFn).toHaveBeenCalledTimes(1);
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
},
};
});
export const LargeSize = generateStory('large', IconButtonRow);
export const MediumSize = generateStory('medium', IconButtonRow);
export const SmallSize = generateStory('small', IconButtonRow);

View File

@ -0,0 +1,36 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { RoundedIconButton } from '../RoundedIconButton';
const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Buttons/RoundedIconButton',
component: RoundedIconButton,
};
export default meta;
type Story = StoryObj<typeof RoundedIconButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<RoundedIconButton
onClick={clickJestFn}
icon={<IconArrowRight size={15} />}
/>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};

View File

@ -4,7 +4,7 @@ import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/components/buttons/IconButton';
import { RoundedIconButton } from '@/ui/components/buttons/RoundedIconButton';
import { IconArrowRight } from '@/ui/icons/index';
const MAX_ROWS = 5;
@ -47,7 +47,7 @@ const StyledTextArea = styled(TextareaAutosize)`
`;
// TODO: this messes with the layout, fix it
const StyledBottomRightIconButton = styled.div`
const StyledBottomRightRoundedIconButton = styled.div`
height: 0;
position: relative;
right: 26px;
@ -129,13 +129,13 @@ export function AutosizeTextInput({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<StyledBottomRightIconButton>
<IconButton
<StyledBottomRightRoundedIconButton>
<RoundedIconButton
onClick={handleOnClickSendButton}
icon={<IconArrowRight size={15} />}
disabled={isSendButtonDisabled}
/>
</StyledBottomRightIconButton>
</StyledBottomRightRoundedIconButton>
</StyledContainer>
</>
);

View File

@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { TableColumn } from '@/people/table/components/peopleColumns';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState';
@ -11,13 +12,15 @@ import { EntityTableRow } from './EntityTableRow';
export function EntityTableBody({ columns }: { columns: Array<TableColumn> }) {
const rowIds = useRecoilValue(tableRowIdsState);
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
const isFetchingEntityTableData = useRecoilValue(
isFetchingEntityTableDataState,
);
return (
<tbody>
{!isFetchingEntityTableData
{!isFetchingEntityTableData && !isNavbarSwitchingSize
? rowIds.map((rowId, index) => (
<RecoilScope SpecificContext={RowContext} key={rowId}>
<EntityTableRow columns={columns} rowId={rowId} index={index} />