refactor(frame): categorize under Layout, rename Panel to Frame

This also adds the field variant and refactors createBorderStyles to implement all border styles.
This commit is contained in:
Wes Souza
2022-07-31 14:02:25 -04:00
committed by Artur Bień
parent 8256a47090
commit bde588c1f3
15 changed files with 286 additions and 182 deletions

View File

@@ -6,7 +6,6 @@ import {
createDisabledTextStyles,
createFlatBoxStyles,
createHatchedBackground,
createWellBorderStyles,
focusOutline
} from '../common';
import { blockSizes } from '../common/system';
@@ -107,12 +106,14 @@ export const StyledButton = styled.button<StyledButtonProps>`
border: 2px solid transparent;
&:hover,
&:focus {
${!disabled && !active && createWellBorderStyles(false)}
${!disabled &&
!active &&
createBorderStyles({ style: 'buttonThin' })}
}
&:active {
${!disabled && createWellBorderStyles(true)}
${!disabled && createBorderStyles({ style: 'buttonThinPressed' })}
}
${active && createWellBorderStyles(true)}
${active && createBorderStyles({ style: 'buttonThinPressed' })}
${disabled && createDisabledTextStyles()}
`
: css`

View File

@@ -1,6 +1,6 @@
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import { Button, Counter, Panel } from 'react95';
import { Button, Counter, Frame } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
@@ -29,14 +29,14 @@ export function Default() {
const [count, setCount] = useState(13);
const handleClick = () => setCount(count + 1);
return (
<Panel className='wrapper'>
<Frame className='wrapper'>
<Counter value={123456789} minLength={11} size='lg' />
<div className='counter-wrapper'>
<Counter value={count} minLength={3} />
<Button onClick={handleClick}>Click!</Button>
</div>
</Panel>
</Frame>
);
}

View File

@@ -1,7 +1,7 @@
import React, { forwardRef, useMemo } from 'react';
import styled from 'styled-components';
import { createWellBorderStyles } from '../common';
import { createBorderStyles } from '../common';
import { CommonStyledProps, Sizes } from '../types';
import { Digit } from './Digit';
@@ -13,7 +13,7 @@ type CounterProps = {
CommonStyledProps;
const CounterWrapper = styled.div`
${createWellBorderStyles(true)}
${createBorderStyles({ style: 'status' })}
display: inline-flex;
background: #000000;
`;

41
src/Frame/Frame.spec.tsx Normal file
View File

@@ -0,0 +1,41 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Frame } from './Frame';
describe('<Frame />', () => {
it('should render frame', () => {
const { container } = render(<Frame />);
const frame = container.firstElementChild;
expect(frame).toBeInTheDocument();
});
it('should render custom styles', () => {
const { container } = render(
<Frame style={{ backgroundColor: 'papayawhip' }} />
);
const frame = container.firstElementChild;
expect(frame).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should render children', async () => {
const { findByText } = render(
<Frame>
<span>Cool frame</span>
</Frame>
);
const content = await findByText(/cool frame/i);
expect(content).toBeInTheDocument();
});
it('should render custom props', () => {
const customProps = { title: 'frame' };
const { container } = render(<Frame {...customProps} />);
const frame = container.firstElementChild;
expect(frame).toHaveAttribute('title', 'frame');
});
});

View File

@@ -1,6 +1,6 @@
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Panel } from 'react95';
import { Frame } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
@@ -19,28 +19,30 @@ const Wrapper = styled.div`
`;
export default {
title: 'Panel',
component: Panel,
title: 'Layout/Frame',
component: Frame,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Panel>;
} as ComponentMeta<typeof Frame>;
export function Default() {
return (
<Panel
<Frame
variant='outside'
shadow
style={{ padding: '0.5rem', lineHeight: '1.5', width: 600 }}
>
<p style={{ padding: '0.5rem' }}>
Notice the subtle difference in borders. The lightest border is not on
the edge of this panel.
This is a frame of the &apos;window&apos; variant, the default. Notice
the subtle difference in borders. The lightest border is not on the edge
of this frame.
</p>
<Panel variant='inside' style={{ margin: '1rem', padding: '1rem' }}>
This panel on the other hand has the lightest border on the edge. Use
this panel inside &apos;outside&apos; panels.
<Frame variant='inside' style={{ margin: '1rem', padding: '1rem' }}>
This frame of the &apos;button&apos; variant on the other hand has the
lightest border on the edge. Use this frame inside &apos;window&apos;
frames.
<br />
<Panel
variant='well'
<Frame
variant='field'
style={{
marginTop: '1rem',
padding: '1rem',
@@ -48,17 +50,17 @@ export function Default() {
width: 100
}}
>
Put some content here
</Panel>
</Panel>
<Panel
A field frame variant is used to display content.
</Frame>
</Frame>
<Frame
variant='well'
style={{ marginTop: '1rem', padding: '0.1rem 0.25rem', width: '100%' }}
>
The &apos;well&apos; variant of a panel is often used as a window
footer.
</Panel>
</Panel>
The &apos;status&apos; variant of a frame is often used as a status bar
at the end of the window.
</Frame>
</Frame>
);
}

58
src/Frame/Frame.tsx Normal file
View File

@@ -0,0 +1,58 @@
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import { createBorderStyles, createBoxStyles } from '../common';
import { CommonStyledProps } from '../types';
type FrameProps = {
children?: React.ReactNode;
shadow?: boolean;
variant?: 'outside' | 'field' | 'inside' | 'well';
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const createFrameStyles = (variant: FrameProps['variant']) => {
switch (variant) {
case 'well':
return css`
${createBorderStyles({ style: 'status' })}
`;
case 'outside':
return css`
${createBorderStyles({ style: 'window' })}
`;
case 'field':
return css`
${createBorderStyles({ style: 'field' })}
`;
default:
return css`
${createBorderStyles()}
`;
}
};
const StyledFrame = styled.div<Required<Pick<FrameProps, 'variant'>>>`
position: relative;
font-size: 1rem;
${({ variant }) => createFrameStyles(variant)}
${({ variant }) =>
createBoxStyles(
variant === 'field'
? { background: 'canvas', color: 'canvasText' }
: undefined
)}
`;
const Frame = forwardRef<HTMLDivElement, FrameProps>(
({ children, shadow = false, variant = 'window', ...otherProps }, ref) => {
return (
<StyledFrame ref={ref} shadow={shadow} variant={variant} {...otherProps}>
{children}
</StyledFrame>
);
}
);
Frame.displayName = 'Frame';
export { Frame, FrameProps };

View File

@@ -17,7 +17,7 @@ const MenuList = styled.ul.attrs(() => ({
box-sizing: border-box;
width: ${props => (props.fullWidth ? '100%' : 'auto')};
padding: 4px;
${createBorderStyles({ windowBorders: true })}
${createBorderStyles({ style: 'window' })}
${createBoxStyles()}
${props =>
props.inline &&

View File

@@ -1,41 +0,0 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Panel } from './Panel';
describe('<Panel />', () => {
it('should render panel', () => {
const { container } = render(<Panel />);
const panel = container.firstElementChild;
expect(panel).toBeInTheDocument();
});
it('should render custom styles', () => {
const { container } = render(
<Panel style={{ backgroundColor: 'papayawhip' }} />
);
const panel = container.firstElementChild;
expect(panel).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should render children', async () => {
const { findByText } = render(
<Panel>
<span>Cool panel</span>
</Panel>
);
const content = await findByText(/cool panel/i);
expect(content).toBeInTheDocument();
});
it('should render custom props', () => {
const customProps = { title: 'panel' };
const { container } = render(<Panel {...customProps} />);
const panel = container.firstElementChild;
expect(panel).toHaveAttribute('title', 'panel');
});
});

View File

@@ -1,52 +0,0 @@
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import {
createBorderStyles,
createBoxStyles,
createWellBorderStyles
} from '../common';
import { CommonStyledProps } from '../types';
type PanelProps = {
children?: React.ReactNode;
shadow?: boolean;
variant?: 'outside' | 'inside' | 'well';
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const createPanelStyles = (variant: PanelProps['variant']) => {
switch (variant) {
case 'well':
return css`
${createWellBorderStyles(true)}
`;
case 'outside':
return css`
${createBorderStyles({ windowBorders: true })}
`;
default:
return css`
${createBorderStyles()}
`;
}
};
const StyledPanel = styled.div<Required<Pick<PanelProps, 'variant'>>>`
position: relative;
font-size: 1rem;
${({ variant }) => createPanelStyles(variant)}
${createBoxStyles()}
`;
const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
{ children, shadow = false, variant = 'outside', ...otherProps },
ref
) {
return (
<StyledPanel ref={ref} shadow={shadow} variant={variant} {...otherProps}>
{children}
</StyledPanel>
);
});
export { Panel, PanelProps };

View File

@@ -37,7 +37,7 @@ const StyledHeadCell = styled.th<{ $disabled: boolean }>`
css`
&:active {
&:before {
${createBorderStyles({ invert: true, windowBorders: true })}
${createBorderStyles({ invert: true, style: 'window' })}
border-left: none;
border-top: none;
padding-top: 2px;

View File

@@ -2,7 +2,7 @@ import { ComponentMeta } from '@storybook/react';
import React from 'react';
import {
Button,
Panel,
Frame,
Toolbar,
Window,
WindowContent,
@@ -97,9 +97,9 @@ export function Default() {
you tho!)
</p>
</WindowContent>
<Panel variant='well' className='footer'>
<Frame variant='well' className='footer'>
Put some useful information here
</Panel>
</Frame>
</Window>
<Window className='window'>

View File

@@ -15,7 +15,7 @@ const StyledWindow = styled.div`
position: relative;
padding: 4px;
font-size: 1rem;
${createBorderStyles({ windowBorders: true })}
${createBorderStyles({ style: 'window' })}
${createBoxStyles()}
`;

View File

@@ -1,5 +1,5 @@
import { css } from 'styled-components';
import { Color, CommonThemeProps } from '../types';
import { Color, CommonThemeProps, Theme } from '../types';
export const shadow = '4px 4px 10px 0 rgba(0, 0, 0, 0.35)';
export const insetShadow = 'inset 2px 2px 3px rgba(0,0,0,0.2)';
@@ -11,11 +11,17 @@ export const createDisabledTextStyles = () => css`
/* filter: grayscale(100%); */
`;
export const createBoxStyles = () => css`
export const createBoxStyles = ({
background = 'material',
color = 'materialText'
}: {
background?: keyof Theme;
color?: keyof Theme;
} = {}) => css`
box-sizing: border-box;
display: inline-block;
background: ${({ theme }) => theme.material};
color: ${({ theme }) => theme.materialText};
background: ${({ theme }) => theme[background]};
color: ${({ theme }) => theme[color]};
`;
// TODO for flat box styles add checkered background when disabled (not solid color)
@@ -57,56 +63,137 @@ export const createFlatBoxStyles = () => css<CommonThemeProps>`
outline-offset: -4px;
`;
export type BorderStyles =
| 'button'
| 'buttonPressed'
| 'buttonThin'
| 'buttonThinPressed'
| 'field'
| 'grouping'
| 'status'
| 'window';
type BorderStyle = {
topLeftOuter: keyof Theme;
topLeftInner: keyof Theme | null;
bottomRightInner: keyof Theme | null;
bottomRightOuter: keyof Theme;
};
const borderStyles: Record<BorderStyles, BorderStyle> = {
button: {
topLeftOuter: 'borderLightest',
topLeftInner: 'borderLight',
bottomRightInner: 'borderDark',
bottomRightOuter: 'borderDarkest'
},
buttonPressed: {
topLeftOuter: 'borderDarkest',
topLeftInner: 'borderDark',
bottomRightInner: 'borderLight',
bottomRightOuter: 'borderLightest'
},
buttonThin: {
topLeftOuter: 'borderLightest',
topLeftInner: null,
bottomRightInner: null,
bottomRightOuter: 'borderDark'
},
buttonThinPressed: {
topLeftOuter: 'borderDark',
topLeftInner: null,
bottomRightInner: null,
bottomRightOuter: 'borderLightest'
},
field: {
topLeftOuter: 'borderDark',
topLeftInner: 'borderDarkest',
bottomRightInner: 'borderLight',
bottomRightOuter: 'borderLightest'
},
grouping: {
topLeftOuter: 'borderDark',
topLeftInner: 'borderLightest',
bottomRightInner: 'borderDark',
bottomRightOuter: 'borderLightest'
},
status: {
topLeftOuter: 'borderDark',
topLeftInner: null,
bottomRightInner: null,
bottomRightOuter: 'borderLightest'
},
window: {
topLeftOuter: 'borderLight',
topLeftInner: 'borderLightest',
bottomRightInner: 'borderDark',
bottomRightOuter: 'borderDarkest'
}
};
export const createInnerBorderWithShadow = ({
theme,
topLeftInner,
bottomRightInner,
hasShadow = false,
hasInsetShadow = false
}: {
theme: Theme;
topLeftInner: keyof Theme | null;
bottomRightInner: keyof Theme | null;
hasShadow?: boolean;
hasInsetShadow?: boolean;
}) =>
[
hasShadow ? shadow : false,
hasInsetShadow ? insetShadow : false,
topLeftInner !== null
? `inset 1px 1px 0px 1px ${theme[topLeftInner]}`
: false,
bottomRightInner !== null
? `inset -1px -1px 0 1px ${theme[bottomRightInner]}`
: false
]
.filter(Boolean)
.join(', ');
export const createBorderStyles = ({
invert = false,
windowBorders = false
} = {}) =>
invert
? css<CommonThemeProps>`
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) => theme.borderDarkest};
border-top-color: ${({ theme }) => theme.borderDarkest};
border-right-color: ${({ theme }) => theme.borderLightest};
border-bottom-color: ${({ theme }) => theme.borderLightest};
box-shadow: ${props => props.shadow && `${shadow}, `} inset 1px 1px 0px
1px ${({ theme }) => theme.borderDark},
inset -1px -1px 0 1px ${({ theme }) => theme.borderLight};
`
: css<CommonThemeProps>`
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) =>
windowBorders ? theme.borderLight : theme.borderLightest};
border-top-color: ${({ theme }) =>
windowBorders ? theme.borderLight : theme.borderLightest};
border-right-color: ${({ theme }) => theme.borderDarkest};
border-bottom-color: ${({ theme }) => theme.borderDarkest};
box-shadow: ${props => props.shadow && `${shadow}, `} inset 1px 1px 0px
1px
${({ theme }) =>
windowBorders ? theme.borderLightest : theme.borderLight},
inset -1px -1px 0 1px ${({ theme }) => theme.borderDark};
`;
style = 'button'
}: { invert?: boolean; style?: BorderStyles } = {}) => {
const borders = {
topLeftOuter: invert ? 'bottomRightOuter' : 'topLeftOuter',
topLeftInner: invert ? 'bottomRightInner' : 'topLeftInner',
bottomRightInner: invert ? 'topLeftInner' : 'bottomRightInner',
bottomRightOuter: invert ? 'topLeftOuter' : 'bottomRightOuter'
} as const;
return css<CommonThemeProps>`
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) =>
theme[borderStyles[style][borders.topLeftOuter]]};
border-top-color: ${({ theme }) =>
theme[borderStyles[style][borders.topLeftOuter]]};
border-right-color: ${({ theme }) =>
theme[borderStyles[style][borders.bottomRightOuter]]};
border-bottom-color: ${({ theme }) =>
theme[borderStyles[style][borders.bottomRightOuter]]};
box-shadow: ${({ theme, shadow: hasShadow }) =>
createInnerBorderWithShadow({
theme,
topLeftInner: borderStyles[style][borders.topLeftInner],
bottomRightInner: borderStyles[style][borders.bottomRightInner],
hasShadow
})};
`;
};
/** @deprecated Use `createBorderStyles` instead */
export const createWellBorderStyles = (invert = false) =>
invert
? css`
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) => theme.borderDark};
border-top-color: ${({ theme }) => theme.borderDark};
border-right-color: ${({ theme }) => theme.borderLightest};
border-bottom-color: ${({ theme }) => theme.borderLightest};
`
: css`
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) => theme.borderLightest};
border-top-color: ${({ theme }) => theme.borderLightest};
border-right-color: ${({ theme }) => theme.borderDark};
border-bottom-color: ${({ theme }) => theme.borderDark};
`;
createBorderStyles({
invert: !invert,
style: 'status'
});
export const focusOutline = () => css`
outline: 2px dotted ${({ theme }) => theme.materialText};
@@ -141,7 +228,7 @@ export const createScrollbars = (variant = 'default') => css`
${createBoxStyles()}
${variant === 'flat'
? createFlatBoxStyles()
: createBorderStyles({ windowBorders: true })}
: createBorderStyles({ style: 'window' })}
outline-offset: -2px;
}
@@ -152,7 +239,7 @@ export const createScrollbars = (variant = 'default') => css`
${createBoxStyles()}
${variant === 'flat'
? createFlatBoxStyles()
: createBorderStyles({ windowBorders: true })}
: createBorderStyles({ style: 'window' })}
display: block;
outline-offset: -2px;
height: 26px;
@@ -165,7 +252,7 @@ export const createScrollbars = (variant = 'default') => css`
::-webkit-scrollbar-button:active {
background-position: 0 1px;
${variant === 'default'
? createBorderStyles({ windowBorders: true, invert: true })
? createBorderStyles({ style: 'window', invert: true })
: ''}
}

View File

@@ -11,13 +11,13 @@ export * from './Checkbox/Checkbox';
export * from './ColorInput/ColorInput';
export * from './Counter/Counter';
export * from './DatePicker/DatePicker';
export * from './Frame/Frame';
export * from './GroupBox/GroupBox';
export * from './Handle/Handle';
export * from './Hourglass/Hourglass';
export * from './MenuList/MenuList';
export * from './Monitor/Monitor';
export * from './NumberInput/NumberInput';
export * from './Panel/Panel';
export * from './ProgressBar/ProgressBar';
export * from './Radio/Radio';
export * from './ScrollView/ScrollView';
@@ -48,4 +48,5 @@ export * from './legacy/Fieldset';
export * from './legacy/List';
export * from './legacy/ListItem';
export * from './legacy/NumberField';
export * from './legacy/Panel';
export * from './legacy/Progress';

7
src/legacy/Panel.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { Frame, FrameProps } from '../Frame/Frame';
/** @deprecated Use `FrameProps` */
export type PanelProps = FrameProps;
/** @deprecated Use `Frame` */
export const Panel = Frame;