diff --git a/README.md b/README.md index 68fdc7d2..ebb747d9 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Apply style reset, wrap your app with ThemeProvider with theme of your choice... import React from 'react'; import { createGlobalStyle, ThemeProvider } from 'styled-components'; -import { Divider, List, ListItem, styleReset } from 'react95'; +import { MenuList, MenuListItem, Separator, styleReset } from 'react95'; // pick a theme of your choice import original from 'react95/dist/themes/original'; // original Windows95 font (optionally) @@ -72,12 +72,12 @@ const App = () => (
- - 🎤 Sing - 💃🏻 Dance - - 😴 Sleep - + + 🎤 Sing + 💃🏻 Dance + + 😴 Sleep +
); diff --git a/docs/Getting-Started.stories.mdx b/docs/Getting-Started.stories.mdx index 71947108..56cd9d8b 100644 --- a/docs/Getting-Started.stories.mdx +++ b/docs/Getting-Started.stories.mdx @@ -29,7 +29,7 @@ choice... and you are ready to go! 🚀 ```jsx import React from 'react'; -import { Divider, List, ListItem, styleReset } from 'react95'; +import { MenuList, MenuListItem, Separator, styleReset } from 'react95'; import { createGlobalStyle, ThemeProvider } from 'styled-components'; /* Pick a theme of your choice */ @@ -62,12 +62,12 @@ const App = () => (
- - 🎤 Sing - 💃🏻 Dance - - 😴 Sleep - + + 🎤 Sing + 💃🏻 Dance + + 😴 Sleep +
); diff --git a/src/Anchor/Anchor.mdx b/src/Anchor/Anchor.mdx deleted file mode 100644 index 7c0c3409..00000000 --- a/src/Anchor/Anchor.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Anchor -menu: Components ---- - -import Anchor from './Anchor'; - -# Anchor - -## Usage - - - - Expensive Toys - - - -## API - -### Props - - - -### Import - -``` -import { Anchor } from 'react95' -``` diff --git a/src/Anchor/Anchor.stories.tsx b/src/Anchor/Anchor.stories.tsx index f2fb21c7..7bb6c7c2 100644 --- a/src/Anchor/Anchor.stories.tsx +++ b/src/Anchor/Anchor.stories.tsx @@ -1,16 +1,15 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; +import { Anchor } from 'react95'; import styled from 'styled-components'; -import { Anchor } from './Anchor'; - const Wrapper = styled.div` padding: 5rem; background: ${({ theme }) => theme.material}; `; export default { - title: 'Anchor', + title: 'Typography/Anchor', component: Anchor, decorators: [story => {story()}] } as ComponentMeta; @@ -18,9 +17,8 @@ export default { export function Default() { return (

- Everybody likes + Everybody likes{' '} - {' '} https://expensive.toys

diff --git a/src/Anchor/Anchor.tsx b/src/Anchor/Anchor.tsx index 5e27e141..20170fea 100644 --- a/src/Anchor/Anchor.tsx +++ b/src/Anchor/Anchor.tsx @@ -3,7 +3,12 @@ import styled from 'styled-components'; import { CommonStyledProps } from '../types'; -const StyledAnchor = styled.a` +type AnchorProps = { + children: React.ReactNode; +} & React.AnchorHTMLAttributes & + CommonStyledProps; + +const StyledAnchor = styled.a<{ underline: boolean }>` color: ${({ theme }) => theme.anchor}; font-size: inherit; text-decoration: underline; @@ -12,20 +17,16 @@ const StyledAnchor = styled.a` } `; -type AnchorProps = { - children: React.ReactNode; -} & React.AnchorHTMLAttributes & - CommonStyledProps; +const Anchor = forwardRef( + ({ children, ...otherProps }: AnchorProps, ref) => { + return ( + + {children} + + ); + } +); -const Anchor = forwardRef(function Anchor( - { children, ...otherProps }: AnchorProps, - ref -) { - return ( - - {children} - - ); -}); +Anchor.displayName = 'Anchor'; export { Anchor, AnchorProps }; diff --git a/src/AppBar/AppBar.mdx b/src/AppBar/AppBar.mdx deleted file mode 100644 index 85a2325e..00000000 --- a/src/AppBar/AppBar.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: AppBar -menu: Components ---- - -import { AppBar } from './AppBar'; -import { Toolbar } from '../Toolbar/Toolbar'; -import { Button } from '../Button/Button'; -import { TextField } from '../TextField/TextField'; -import List from '../List/List'; -import ListItem from '../ListItem/ListItem'; -import Divider from '../Divider/Divider'; - -# AppBar - -## Usage - -#### Default - - - {() => { - const [open, setOpen] = React.useState(false); - function handleClick() { - setOpen(!open); - } - function handleClose() { - setOpen(false); - } - const renderMenu = () => { - return ( -
- {open && ( - - - - 👨‍💻 - - Profile - - - - 📁 - - My account - - - - - 🔙 - - Logout - - - )} - -
- ); - }; - return ( - - - {renderMenu()} - - - - ); - }} -
- -## API - -### Import - -``` -import { AppBar } from 'react95' -``` - -### Props - - diff --git a/src/AppBar/AppBar.stories.tsx b/src/AppBar/AppBar.stories.tsx index bb12aa22..c8fc80e9 100644 --- a/src/AppBar/AppBar.stories.tsx +++ b/src/AppBar/AppBar.stories.tsx @@ -3,10 +3,10 @@ import React, { useState } from 'react'; import { AppBar, Button, - Divider, - List, - ListItem, - TextField, + MenuList, + MenuListItem, + Separator, + TextInput, Toolbar } from 'react95'; import styled from 'styled-components'; @@ -18,7 +18,7 @@ const Wrapper = styled.div` `; export default { - title: 'AppBar', + title: 'Environment/AppBar', component: AppBar, decorators: [story => {story()}] } as ComponentMeta; @@ -43,7 +43,7 @@ export function Default() { Start {open && ( - setOpen(false)} > - + 👨‍💻 Profile - - + + 📁 My account - - - + + + 🔙 Logout - - + + )} - + ); diff --git a/src/AppBar/AppBar.tsx b/src/AppBar/AppBar.tsx index 8fa4cca9..416ae5a2 100644 --- a/src/AppBar/AppBar.tsx +++ b/src/AppBar/AppBar.tsx @@ -1,7 +1,8 @@ import React, { forwardRef } from 'react'; import styled from 'styled-components'; -import { CommonStyledProps } from '../types'; + import { createBorderStyles, createBoxStyles } from '../common'; +import { CommonStyledProps } from '../types'; type AppBarProps = { children: React.ReactNode; @@ -22,15 +23,16 @@ const StyledAppBar = styled.header` width: 100%; `; -const AppBar = forwardRef(function AppBar( - { children, fixed = true, ...otherProps }, - ref -) { - return ( - - {children} - - ); -}); +const AppBar = forwardRef( + ({ children, fixed = true, ...otherProps }, ref) => { + return ( + + {children} + + ); + } +); + +AppBar.displayName = 'AppBar'; export { AppBar, AppBarProps }; diff --git a/src/Avatar/Avatar.mdx b/src/Avatar/Avatar.mdx deleted file mode 100644 index b2788b41..00000000 --- a/src/Avatar/Avatar.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: Avatar -menu: Components ---- - -import { Avatar } from './Avatar'; - -# Avatar - -## Usage - -#### Default - - - - - -#### No border - - - - - -#### Lettered - - - AK - - -#### Squared - - - - - 🚀 - - - - -## API - -### Import - -``` -import { Avatar } from 'react95' -``` - -### Props - - diff --git a/src/Avatar/Avatar.stories.tsx b/src/Avatar/Avatar.stories.tsx index f1cdd4e3..55bb1f00 100644 --- a/src/Avatar/Avatar.stories.tsx +++ b/src/Avatar/Avatar.stories.tsx @@ -12,7 +12,7 @@ const Wrapper = styled.div` `; export default { - title: 'Avatar', + title: 'Other/Avatar', component: Avatar, decorators: [story => {story()}] } as ComponentMeta; diff --git a/src/Avatar/Avatar.tsx b/src/Avatar/Avatar.tsx index 62f3b791..89990fd4 100644 --- a/src/Avatar/Avatar.tsx +++ b/src/Avatar/Avatar.tsx @@ -53,30 +53,34 @@ const StyledAvatarImg = styled.img` height: 100%; `; -const Avatar = forwardRef(function Avatar( - { - alt = '', - children, - noBorder = false, - size = 35, - square = false, - src, - ...otherProps - }, - ref -) { - return ( - - {src ? : children} - - ); -}); +const Avatar = forwardRef( + ( + { + alt = '', + children, + noBorder = false, + size = 35, + square = false, + src, + ...otherProps + }, + ref + ) => { + return ( + + {src ? : children} + + ); + } +); + +Avatar.displayName = 'Avatar'; export { Avatar, AvatarProps }; diff --git a/src/Bar/Bar.mdx b/src/Bar/Bar.mdx deleted file mode 100644 index d0f2f026..00000000 --- a/src/Bar/Bar.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bar -menu: Components ---- - -import { Bar } from '../Bar/Bar'; -import { AppBar } from '../AppBar/AppBar.js'; -import { Toolbar } from '../Toolbar/Toolbar.js'; -import { Button } from '../Button/Button.js'; - -# Bar - -## Usage - - - - - - - - - - - - -## API - -### Import - -### Props - - diff --git a/src/Button/Button.mdx b/src/Button/Button.mdx deleted file mode 100644 index ba6c19a2..00000000 --- a/src/Button/Button.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: Button -menu: Components ---- - -import { Button } from './Button'; -import Window from '../Window/Window'; -import WindowContent from '../WindowContent/WindowContent'; -import { Cutout } from '../Cutout/Cutout'; -import { Toolbar } from '../Toolbar/Toolbar'; - -# Button - -## Usage - -#### Default - - - - - -#### Disabled - - - - - -#### Full Width - - - - - -#### Square - - - - - -#### Active - - - - - -#### Different sizes - - -
- - - -
-
- -#### Flat - - - - - -

- When you want to use Buttons on a light background (like scrollable - content), just use the flat variant: -

-
- - - - - -
-
-
-
-
- -## API - -### Import - -``` -import { Button } from 'react95' -``` - -### Props - - diff --git a/src/Button/Button.spec.tsx b/src/Button/Button.spec.tsx index a0f496f3..a980b3c1 100644 --- a/src/Button/Button.spec.tsx +++ b/src/Button/Button.spec.tsx @@ -55,6 +55,10 @@ describe(' + + + + + + + + ); +} + +Flat.story = { + name: 'flat' +}; + const imageSrc = 'https://image.freepik.com/foto-gratuito/la-frutta-fresca-del-kiwi-tagliata-a-meta-con-la-decorazione-completa-del-pezzo-e-bella-sulla-tavola-di-legno_47436-1.jpg'; -export function Menu() { +export function Thin() { const [open, setOpen] = useState(false); return ( @@ -77,10 +113,10 @@ export function Menu() { Kiwi.app - -
{open && ( - setOpen(false)} > - Copy link - - Facebook - Twitter - Instagram - - + Copy link + + Facebook + Twitter + Instagram + + MySpace - - + + )}
- + kiwi - + ); } -Menu.story = { - name: 'menu' -}; - -export function Flat() { - return ( - - - -

- When you want to use Buttons on a light background (like scrollable - content), just use the flat variant: -

-
- - - - - -
-
-
-
- ); -} - -Flat.story = { - name: 'flat' +Thin.story = { + name: 'thin' }; diff --git a/src/Button/Button.tsx b/src/Button/Button.tsx index e74a2547..6cbf6aa0 100644 --- a/src/Button/Button.tsx +++ b/src/Button/Button.tsx @@ -6,7 +6,6 @@ import { createDisabledTextStyles, createFlatBoxStyles, createHatchedBackground, - createWellBorderStyles, focusOutline } from '../common'; import { blockSizes } from '../common/system'; @@ -24,11 +23,19 @@ type ButtonProps = { size?: Sizes; square?: boolean; type?: string; - variant?: 'default' | 'menu' | 'flat'; -} & Omit< - React.ButtonHTMLAttributes, - 'disabled' | 'onClick' | 'onTouchStart' | 'type' -> & +} & ( + | { + variant?: 'default' | 'flat' | 'thin'; + } + | { + /** @deprecated Use `thin` */ + variant?: 'menu'; + } +) & + Omit< + React.ButtonHTMLAttributes, + 'disabled' | 'onClick' | 'onTouchStart' | 'type' + > & CommonStyledProps; type StyledButtonProps = Pick< @@ -93,18 +100,20 @@ export const StyledButton = styled.button` outline-offset: -4px; } ` - : variant === 'menu' + : variant === 'menu' || variant === 'thin' ? css` ${createBoxStyles()}; 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` @@ -156,41 +165,46 @@ export const StyledButton = styled.button` ${commonButtonStyles} `; -const Button = forwardRef(function Button( - { - onClick, - disabled = false, - children, - type = 'button', - fullWidth = false, - size = 'md', - square = false, - active = false, - onTouchStart = noOp, - primary = false, - variant = 'default', - ...otherProps - }, - ref -) { - return ( - - {children} - - ); -}); +const Button = forwardRef( + ( + { + onClick, + disabled = false, + children, + type = 'button', + fullWidth = false, + size = 'md', + square = false, + active = false, + onTouchStart = noOp, + primary = false, + variant = 'default', + ...otherProps + }, + ref + ) => { + return ( + + {children} + + ); + } +); + +Button.displayName = 'Button'; export { Button, ButtonProps }; diff --git a/src/Checkbox/Checkbox.mdx b/src/Checkbox/Checkbox.mdx deleted file mode 100644 index 7c84413f..00000000 --- a/src/Checkbox/Checkbox.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -name: Checkbox -menu: Components ---- - -import { Checkbox } from './Checkbox'; -import Fieldset from '../Fieldset/Fieldset'; -import { Button } from '../Button/Button'; -import { Cutout } from '../Cutout/Cutout'; -import List from '../List/List'; -import ListItem from '../ListItem/ListItem'; -import Divider from '../Divider/Divider'; - -# Checkbox - -## Usage - -#### Controlled group - - - {() => { - const [steak, setSteak] = React.useState(true); - const [tortilla, setTortilla] = React.useState(false); - const [pizza, setPizza] = React.useState(false); - const handleChange = event => { - const { - target: { value } - } = event; - if (value === 'steak') { - setSteak(!steak); - return; - } - if (value === 'tortilla') { - setTortilla(!tortilla); - return; - } - if (value === 'pizza') { - setPizza(!pizza); - return; - } - }; - const reset = () => { - setSteak(false); - setTortilla(false); - setPizza(false); - }; - return ( -
-
- -
- -
- -
- -
- ); - }} -
- -#### Uncontrolled - - - <> - -
- - -
- -#### Flat - - - -

- When you want to add input field on a light background (like scrollable - content), just use the flat variant: -

-
- - - -
-
-
- -#### Menu - - - - - - - - - - - - - - - - -## API - -### Import - -``` -import { Checkbox } from 'react95' -``` - -### Props - - diff --git a/src/Checkbox/Checkbox.stories.tsx b/src/Checkbox/Checkbox.stories.tsx index cfee5b07..b03326dc 100644 --- a/src/Checkbox/Checkbox.stories.tsx +++ b/src/Checkbox/Checkbox.stories.tsx @@ -1,8 +1,15 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import { Checkbox, Fieldset, Cutout, List, ListItem, Divider } from 'react95'; import { ComponentMeta } from '@storybook/react'; +import { + Checkbox, + GroupBox, + MenuList, + MenuListItem, + ScrollView, + Separator +} from 'react95'; const Wrapper = styled.div` background: ${({ theme }) => theme.material}; @@ -19,7 +26,7 @@ const Wrapper = styled.div` `; export default { - title: 'Checkbox', + title: 'Controls/Checkbox', component: Checkbox, decorators: [story => {story()}] } as ComponentMeta; @@ -71,7 +78,7 @@ export function Default() { return (
-
+
- + +
-
+
- + - +
); } @@ -233,8 +240,8 @@ Flat.story = { export function Menu() { return ( - - + + - - + + - - - + + + - - + + ); } diff --git a/src/Checkbox/Checkbox.tsx b/src/Checkbox/Checkbox.tsx index 777d10d0..1b245ebd 100644 --- a/src/Checkbox/Checkbox.tsx +++ b/src/Checkbox/Checkbox.tsx @@ -3,15 +3,15 @@ import styled, { css } from 'styled-components'; import { createHatchedBackground } from '../common'; import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled'; -import { noOp } from '../common/utils'; -import { StyledCutout } from '../Cutout/Cutout'; -import { StyledListItem } from '../ListItem/ListItem'; import { LabelText, size, StyledInput, StyledLabel -} from '../SwitchBase/SwitchBase'; +} from '../common/SwitchBase'; +import { noOp } from '../common/utils'; +import { StyledMenuListItem } from '../MenuList/MenuList'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonThemeProps } from '../types'; type CheckboxProps = { @@ -52,7 +52,7 @@ const sharedCheckboxStyles = css` justify-content: space-around; margin-right: 0.5rem; `; -const StyledCheckbox = styled(StyledCutout)` +const StyledCheckbox = styled(StyledScrollView)` ${sharedCheckboxStyles} width: ${size}px; height: ${size}px; @@ -129,7 +129,7 @@ const CheckmarkIcon = styled.span.attrs(() => ({ ? theme.checkmarkDisabled : theme.checkmark}; `} - ${StyledListItem}:hover & { + ${StyledMenuListItem}:hover & { ${({ $disabled, theme, variant }) => !$disabled && variant === 'menu' && @@ -170,7 +170,7 @@ const IndeterminateIcon = styled.span.attrs(() => ({ ${({ $disabled, theme, variant }) => variant === 'menu' && css` - ${StyledListItem}:hover & { + ${StyledMenuListItem}:hover & { ${createHatchedBackground({ mainColor: theme.materialTextInvert })} @@ -189,64 +189,68 @@ const CheckboxComponents = { menu: StyledMenuCheckbox }; -const Checkbox = forwardRef(function Checkbox( - { - checked, - className = '', - defaultChecked = false, - disabled = false, - indeterminate = false, - label = '', - onChange = noOp, - style = {}, - value, - variant = 'default', - ...otherProps - }, - ref -) { - const [state, setState] = useControlledOrUncontrolled({ - value: checked, - defaultValue: defaultChecked - }); - - const handleChange = useCallback( - (e: React.ChangeEvent) => { - const newState = e.target.checked; - setState(newState); - onChange(e); +const Checkbox = forwardRef( + ( + { + checked, + className = '', + defaultChecked = false, + disabled = false, + indeterminate = false, + label = '', + onChange = noOp, + style = {}, + value, + variant = 'default', + ...otherProps }, - [onChange, setState] - ); + ref + ) => { + const [state, setState] = useControlledOrUncontrolled({ + value: checked, + defaultValue: defaultChecked + }); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const newState = e.target.checked; + setState(newState); + onChange(e); + }, + [onChange, setState] + ); + + const CheckboxComponent = CheckboxComponents[variant]; - const CheckboxComponent = CheckboxComponents[variant]; + let Icon = null; + if (indeterminate) { + Icon = IndeterminateIcon; + } else if (state) { + Icon = CheckmarkIcon; + } - let Icon = null; - if (indeterminate) { - Icon = IndeterminateIcon; - } else if (state) { - Icon = CheckmarkIcon; + return ( + + + + {Icon && } + + {label && {label}} + + ); } +); - return ( - - - - {Icon && } - - {label && {label}} - - ); -}); +Checkbox.displayName = 'Checkbox'; export { Checkbox, CheckboxProps }; diff --git a/src/ColorInput/ColorInput.stories.tsx b/src/ColorInput/ColorInput.stories.tsx index 93ce49dd..5ec6a981 100644 --- a/src/ColorInput/ColorInput.stories.tsx +++ b/src/ColorInput/ColorInput.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; import styled from 'styled-components'; -import { ColorInput, Cutout } from 'react95'; +import { ColorInput, ScrollView } from 'react95'; const Wrapper = styled.div` background: ${({ theme }) => theme.material}; @@ -28,7 +28,7 @@ const Wrapper = styled.div` `; export default { - title: 'ColorInput', + title: 'Controls/ColorInput', component: ColorInput, decorators: [story => {story()}] } as ComponentMeta; @@ -50,7 +50,7 @@ Default.story = { export function Flat() { return ( - +
enabled: @@ -61,7 +61,7 @@ export function Flat() {
-
+ ); } diff --git a/src/ColorInput/ColorInput.tsx b/src/ColorInput/ColorInput.tsx index bd98b407..70adaeeb 100644 --- a/src/ColorInput/ColorInput.tsx +++ b/src/ColorInput/ColorInput.tsx @@ -4,7 +4,7 @@ import { StyledButton } from '../Button/Button'; import { focusOutline } from '../common'; import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled'; import { noOp } from '../common/utils'; -import { Divider } from '../Divider/Divider'; +import { Separator } from '../Separator/Separator'; import { CommonStyledProps } from '../types'; type ColorInputProps = { @@ -23,7 +23,7 @@ const Trigger = styled(StyledButton)` padding-left: 8px; `; -const StyledDivider = styled(Divider)` +const StyledSeparator = styled(Separator)` height: 21px; position: relative; top: 0; @@ -116,7 +116,7 @@ const ChevronIcon = styled.span< // TODO make sure all aria and role attributes are in place const ColorInput = forwardRef( - function ColorInput( + ( { value, defaultValue, @@ -126,7 +126,7 @@ const ColorInput = forwardRef( ...otherProps }, ref - ) { + ) => { const [valueDerived, setValueState] = useControlledOrUncontrolled({ value, defaultValue @@ -156,11 +156,13 @@ const ColorInput = forwardRef( color={valueDerived ?? '#008080'} role='presentation' /> - {variant === 'default' && } + {variant === 'default' && } ); } ); +ColorInput.displayName = 'ColorInput'; + export { ColorInput, ColorInputProps }; diff --git a/src/Counter/Counter.mdx b/src/Counter/Counter.mdx deleted file mode 100644 index 0d8428b2..00000000 --- a/src/Counter/Counter.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Bar -menu: Components ---- - -import { Counter } from '../Counter/Counter'; - -# Counter - -## Usage - - - - - -## API - -### Import - -### Props - - diff --git a/src/Counter/Counter.stories.tsx b/src/Counter/Counter.stories.tsx index 63ed01dc..a7553247 100644 --- a/src/Counter/Counter.stories.tsx +++ b/src/Counter/Counter.stories.tsx @@ -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` @@ -20,7 +20,7 @@ const Wrapper = styled.div` `; export default { - title: 'Counter', + title: 'Other/Counter', component: Counter, decorators: [story => {story()}] } as ComponentMeta; @@ -29,14 +29,14 @@ export function Default() { const [count, setCount] = useState(13); const handleClick = () => setCount(count + 1); return ( - +
-
+ ); } diff --git a/src/Counter/Counter.tsx b/src/Counter/Counter.tsx index 9e468b2f..7c2b5c2f 100644 --- a/src/Counter/Counter.tsx +++ b/src/Counter/Counter.tsx @@ -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; `; @@ -25,21 +25,22 @@ const pixelSizes = { xl: 4 }; -const Counter = forwardRef(function Counter( - { value = 0, minLength = 3, size = 'md', ...otherProps }, - ref -) { - const digits = useMemo( - () => value.toString().padStart(minLength, '0').split(''), - [minLength, value] - ); - return ( - - {digits.map((digit, i) => ( - - ))} - - ); -}); +const Counter = forwardRef( + ({ value = 0, minLength = 3, size = 'md', ...otherProps }, ref) => { + const digits = useMemo( + () => value.toString().padStart(minLength, '0').split(''), + [minLength, value] + ); + return ( + + {digits.map((digit, i) => ( + + ))} + + ); + } +); + +Counter.displayName = 'Counter'; export { Counter, CounterProps }; diff --git a/src/Cutout/Cutout.mdx b/src/Cutout/Cutout.mdx deleted file mode 100644 index a29422ee..00000000 --- a/src/Cutout/Cutout.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Cutout -menu: Components ---- - -import { Cutout } from './Cutout'; -import Window from '../Window/Window'; -import WindowContent from '../WindowContent/WindowContent'; - -# Cutout - -## Usage - - - - - -

- React95 -

-
-
-
-
- -## API - -### Import - -``` -import { Cutout } from 'react95' -``` - -### Props - - diff --git a/src/Cutout/Cutout.spec.tsx b/src/Cutout/Cutout.spec.tsx deleted file mode 100644 index 08681f7e..00000000 --- a/src/Cutout/Cutout.spec.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; - -import { Cutout } from './Cutout'; - -describe('', () => { - it('should render cutout', () => { - const { container } = render(); - const cutout = container.firstElementChild; - - expect(cutout).toBeInTheDocument(); - }); - - it('should render custom styles', () => { - const { container } = render( - - ); - const cutout = container.firstElementChild; - - expect(cutout).toHaveAttribute('style', 'background-color: papayawhip;'); - }); - - it('should render children', async () => { - const { findByText } = render( - - Cool cutout - - ); - const content = await findByText(/cool cutout/i); - - expect(content).toBeInTheDocument(); - }); - - it('should render custom props', () => { - const customProps = { title: 'cutout' }; - const { container } = render(); - const cutout = container.firstElementChild; - - expect(cutout).toHaveAttribute('title', 'cutout'); - }); -}); diff --git a/src/DatePicker/DatePicker.mdx b/src/DatePicker/DatePicker.mdx deleted file mode 100644 index 259727d5..00000000 --- a/src/DatePicker/DatePicker.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: DatePicker -menu: Components ---- - -import { DatePicker } from './DatePicker'; - -# DatePicker - -## Usage - - - console.log(date)} /> - - -## API - -### Import - -``` -import { DatePicker } from 'react95' -``` - -### Props - - diff --git a/src/DatePicker/DatePicker.tsx b/src/DatePicker/DatePicker.tsx index 4b4521c2..a8aaaf10 100644 --- a/src/DatePicker/DatePicker.tsx +++ b/src/DatePicker/DatePicker.tsx @@ -2,14 +2,12 @@ import React, { forwardRef, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { Button } from '../Button/Button'; -import { Cutout } from '../Cutout/Cutout'; -import { NumberField } from '../NumberField/NumberField'; +import { NumberInput } from '../NumberInput/NumberInput'; +import { ScrollView } from '../ScrollView/ScrollView'; import { Select } from '../Select/Select'; import { SelectChangeEvent } from '../Select/Select.types'; import { Toolbar } from '../Toolbar/Toolbar'; -import { Window } from '../Window/Window'; -import { WindowContent } from '../WindowContent/WindowContent'; -import { WindowHeader } from '../WindowHeader/WindowHeader'; +import { Window, WindowContent, WindowHeader } from '../Window/Window'; type DatePickerProps = { className?: string; @@ -19,7 +17,7 @@ type DatePickerProps = { shadow?: boolean; }; -const Calendar = styled(Cutout)` +const Calendar = styled(ScrollView)` width: 234px; margin: 1rem 0; background: ${({ theme }) => theme.canvas}; @@ -175,7 +173,7 @@ const DatePicker = forwardRef( width={128} menuMaxHeight={200} /> - + @@ -207,5 +205,7 @@ const DatePicker = forwardRef( } ); +DatePicker.displayName = 'DatePicker'; + // eslint-disable-next-line camelcase export { DatePicker as DatePicker__UNSTABLE, DatePickerProps }; diff --git a/src/Desktop/Desktop.mdx b/src/Desktop/Desktop.mdx deleted file mode 100644 index a34d2aad..00000000 --- a/src/Desktop/Desktop.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Desktop -menu: Components ---- - -import { Desktop } from '../Desktop/Desktop'; - -# Desktop - -## Usage - - - - - -## API - -### Import - -### Props - - diff --git a/src/Divider/Divider.mdx b/src/Divider/Divider.mdx deleted file mode 100644 index d3c29adb..00000000 --- a/src/Divider/Divider.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- - -name: Divider -menu: Components -import { Divider } from './Divider' -import List from '../List/List' -import ListItem from '../ListItem/ListItem' - -# Divider - -## Usage - -#### Default - - - - Item 1 - - Item 2 - - Item 3 - - - -#### vertical - - - - Item 1 - - Item 2 - - Item 3 - - - -## API - -### Import - -``` -import { Divider } from 'react95' -``` - -### Props - - diff --git a/src/Divider/Divider.spec.tsx b/src/Divider/Divider.spec.tsx deleted file mode 100644 index 7054c79e..00000000 --- a/src/Divider/Divider.spec.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import { renderWithTheme } from '../../test/utils'; - -import { Divider } from './Divider'; - -describe('', () => { - it('should render Divider', () => { - const { container } = renderWithTheme(); - const divider = container.firstElementChild; - - expect(divider).toBeInTheDocument(); - }); - - describe('prop: size', () => { - it('defaults to 100%', () => { - const { container } = renderWithTheme(); - const divider = container.firstElementChild; - expect(divider).toHaveStyleRule('width', '100%'); - }); - it('sets size passed correctly', () => { - const size = '53px'; - const { container } = renderWithTheme(); - const divider = container.firstElementChild; - - expect(divider).toHaveStyleRule('width', size); - }); - }); - - describe('prop: orientation', () => { - it('renders horizontal line by default', () => { - const size = '53px'; - const { container } = renderWithTheme(); - const divider = container.firstElementChild; - - expect(divider).toHaveStyleRule('width', size); - }); - - it('renders vertical line when orientation="vertical"', () => { - const size = '53px'; - const { container } = renderWithTheme( - - ); - const divider = container.firstElementChild; - - expect(divider).toHaveStyleRule('height', size); - }); - }); - describe('prop: size', () => { - it('should set proper size', () => { - const { container } = renderWithTheme(); - const avatarEl = container.firstElementChild; - - expect(avatarEl).toHaveStyleRule('width', '85%'); - }); - - it('when passed a number, sets size in px', () => { - const { container } = renderWithTheme(); - const avatarEl = container.firstElementChild; - - expect(avatarEl).toHaveStyleRule('width', '25px'); - }); - - it('should set height when vertical', () => { - const { container } = renderWithTheme( - - ); - const avatarEl = container.firstElementChild; - - expect(avatarEl).toHaveStyleRule('height', '25px'); - }); - }); -}); diff --git a/src/Divider/Divider.stories.tsx b/src/Divider/Divider.stories.tsx deleted file mode 100644 index 4f697e29..00000000 --- a/src/Divider/Divider.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ComponentMeta } from '@storybook/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { Divider, List, ListItem } from 'react95'; - -const Wrapper = styled.div` - padding: 5rem; - background: ${({ theme }) => theme.desktopBackground}; -`; - -export default { - title: 'Divider', - component: Divider, - decorators: [story => {story()}] -} as ComponentMeta; - -export function Default() { - return ( - <> - - Item 1 - - Item 2 - - Item 3 - - - Item 1 - - Item 2 - - Item 3 - - - ); -} - -Default.story = { - name: 'default' -}; diff --git a/src/Fieldset/Fieldset.mdx b/src/Fieldset/Fieldset.mdx deleted file mode 100644 index 2880426c..00000000 --- a/src/Fieldset/Fieldset.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: Fieldset -menu: Components ---- - -import Fieldset from './Fieldset' -import Window from '../Window/Window' -import WindowContent from '../WindowContent/WindowContent' -import Checkbox from '../Checkbox/Checkbox' -import { Cutout } from '../Cutout/Cutout'; - -# Fieldset - -## Usage - -#### Default - - - - -
- Some content here - - 😍 - -
-
-
-
- -#### With label - - - - -
- Some content here - - 😍 - -
-
-
-
- -#### Flat - - - {() => { - const [enabled, setEnabled] = React.useState(false) - return ( - - - - <> -

- When you want to use Fieldset on a light background (like scrollable - content), just use the flat variant: -

-
-
setEnabled(!enabled)} - /> - } - disabled={!enabled} - > - <> - Some content here - - 😍 - - -
-
- -
-
-
- ); - }} -
- -## API - -### Import - -``` -import { Fieldset } from 'react95' -``` - -### Props - - diff --git a/src/Fieldset/Fieldset.spec.tsx b/src/Fieldset/Fieldset.spec.tsx deleted file mode 100644 index da7a64e7..00000000 --- a/src/Fieldset/Fieldset.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; - -import { renderWithTheme, theme } from '../../test/utils'; - -import { Fieldset } from './Fieldset'; - -describe('
', () => { - it('renders Fieldset', () => { - const { container } = renderWithTheme(
); - const fieldset = container.firstChild as HTMLFieldSetElement; - - expect(fieldset).toBeInTheDocument(); - }); - it('renders children', () => { - const textContent = 'Hi there!'; - const { getByText } = renderWithTheme( -
- {textContent} -
- ); - expect(getByText(textContent)).toBeInTheDocument(); - }); - - describe('prop: label', () => { - it('renders Label', () => { - const labelText = 'Name:'; - const { container } = renderWithTheme(
); - const fieldset = container.firstChild as HTMLFieldSetElement; - const legend = fieldset.querySelector('legend'); - expect(legend?.textContent).toBe(labelText); - }); - it('when not provided, element is not rendered', () => { - const { container } = renderWithTheme(
); - const fieldset = container.firstChild as HTMLFieldSetElement; - const legend = fieldset.querySelector('legend'); - expect(legend).not.toBeInTheDocument(); - }); - }); - describe('prop: disabled', () => { - it('renders with disabled text content', () => { - const { container } = renderWithTheme(
); - const fieldset = container.firstChild as HTMLFieldSetElement; - - expect(fieldset).toHaveAttribute('aria-disabled', 'true'); - - expect(fieldset).toHaveStyleRule('color', theme.materialTextDisabled); - expect(fieldset).toHaveStyleRule( - 'text-shadow', - `1px 1px ${theme.materialTextDisabledShadow}` - ); - }); - }); -}); diff --git a/src/Frame/Frame.spec.tsx b/src/Frame/Frame.spec.tsx new file mode 100644 index 00000000..2cabe125 --- /dev/null +++ b/src/Frame/Frame.spec.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { Frame } from './Frame'; + +describe('', () => { + it('should render frame', () => { + const { container } = render(); + const frame = container.firstElementChild; + + expect(frame).toBeInTheDocument(); + }); + + it('should render custom styles', () => { + const { container } = render( + + ); + const frame = container.firstElementChild; + + expect(frame).toHaveAttribute('style', 'background-color: papayawhip;'); + }); + + it('should render children', async () => { + const { findByText } = render( + + Cool frame + + ); + const content = await findByText(/cool frame/i); + + expect(content).toBeInTheDocument(); + }); + + it('should render custom props', () => { + const customProps = { title: 'frame' }; + const { container } = render(); + const frame = container.firstElementChild; + + expect(frame).toHaveAttribute('title', 'frame'); + }); +}); diff --git a/src/Panel/Panel.stories.tsx b/src/Frame/Frame.stories.tsx similarity index 54% rename from src/Panel/Panel.stories.tsx rename to src/Frame/Frame.stories.tsx index 930fb96c..dd19c232 100644 --- a/src/Panel/Panel.stories.tsx +++ b/src/Frame/Frame.stories.tsx @@ -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 => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { return ( -

- Notice the subtle difference in borders. The lightest border is not on - the edge of this panel. + This is a frame of the 'window' variant, the default. Notice + the subtle difference in borders. The lightest border is not on the edge + of this frame.

- - This panel on the other hand has the lightest border on the edge. Use - this panel inside 'outside' panels. + + This frame of the 'button' variant on the other hand has the + lightest border on the edge. Use this frame inside 'window' + frames.
- - Put some content here - -
- + + - The 'well' variant of a panel is often used as a window - footer. - -
+ The 'status' variant of a frame is often used as a status bar + at the end of the window. + + ); } diff --git a/src/Frame/Frame.tsx b/src/Frame/Frame.tsx new file mode 100644 index 00000000..440eddd9 --- /dev/null +++ b/src/Frame/Frame.tsx @@ -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 & + 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>>` + position: relative; + font-size: 1rem; + ${({ variant }) => createFrameStyles(variant)} + ${({ variant }) => + createBoxStyles( + variant === 'field' + ? { background: 'canvas', color: 'canvasText' } + : undefined + )} +`; + +const Frame = forwardRef( + ({ children, shadow = false, variant = 'window', ...otherProps }, ref) => { + return ( + + {children} + + ); + } +); + +Frame.displayName = 'Frame'; + +export { Frame, FrameProps }; diff --git a/src/GroupBox/GroupBox.spec.tsx b/src/GroupBox/GroupBox.spec.tsx new file mode 100644 index 00000000..168c58ad --- /dev/null +++ b/src/GroupBox/GroupBox.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { renderWithTheme, theme } from '../../test/utils'; + +import { GroupBox } from './GroupBox'; + +describe('', () => { + it('renders GroupBox', () => { + const { container } = renderWithTheme(); + const groupBox = container.firstChild as HTMLFieldSetElement; + + expect(groupBox).toBeInTheDocument(); + }); + it('renders children', () => { + const textContent = 'Hi there!'; + const { getByText } = renderWithTheme( + + {textContent} + + ); + expect(getByText(textContent)).toBeInTheDocument(); + }); + + describe('prop: label', () => { + it('renders Label', () => { + const labelText = 'Name:'; + const { container } = renderWithTheme(); + const groupBox = container.firstChild as HTMLFieldSetElement; + const legend = groupBox.querySelector('legend'); + expect(legend?.textContent).toBe(labelText); + }); + it('when not provided, element is not rendered', () => { + const { container } = renderWithTheme(); + const groupBox = container.firstChild as HTMLFieldSetElement; + const legend = groupBox.querySelector('legend'); + expect(legend).not.toBeInTheDocument(); + }); + }); + describe('prop: disabled', () => { + it('renders with disabled text content', () => { + const { container } = renderWithTheme(); + const groupBox = container.firstChild as HTMLFieldSetElement; + + expect(groupBox).toHaveAttribute('aria-disabled', 'true'); + + expect(groupBox).toHaveStyleRule('color', theme.materialTextDisabled); + expect(groupBox).toHaveStyleRule( + 'text-shadow', + `1px 1px ${theme.materialTextDisabledShadow}` + ); + }); + }); +}); diff --git a/src/Fieldset/Fieldset.stories.tsx b/src/GroupBox/GroupBox.stories.tsx similarity index 76% rename from src/Fieldset/Fieldset.stories.tsx rename to src/GroupBox/GroupBox.stories.tsx index 819fcafa..0cba694b 100644 --- a/src/Fieldset/Fieldset.stories.tsx +++ b/src/GroupBox/GroupBox.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React, { useState } from 'react'; -import { Checkbox, Cutout, Fieldset, Window, WindowContent } from 'react95'; +import { Checkbox, GroupBox, ScrollView, Window, WindowContent } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -9,28 +9,28 @@ const Wrapper = styled.div` `; export default { - title: 'Fieldset', - component: Fieldset, + title: 'Controls/GroupBox', + component: GroupBox, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { return ( -
+ Some content here 😍 -
+

-
+ Some content here 😍 -
+
); @@ -44,23 +44,23 @@ export function Flat() { return ( - -
+ Some content here 😍 -
+

-
+ Some content here 😍 -
- +
+ ); @@ -75,7 +75,7 @@ export function ToggleExample() { return ( -
😍 -
+
); diff --git a/src/Fieldset/Fieldset.tsx b/src/GroupBox/GroupBox.tsx similarity index 84% rename from src/Fieldset/Fieldset.tsx rename to src/GroupBox/GroupBox.tsx index 05ff72b4..a2d381c5 100644 --- a/src/Fieldset/Fieldset.tsx +++ b/src/GroupBox/GroupBox.tsx @@ -3,7 +3,7 @@ import styled, { css } from 'styled-components'; import { createDisabledTextStyles } from '../common'; import { CommonStyledProps } from '../types'; -type FieldsetProps = { +type GroupBoxProps = { label?: React.ReactNode; children?: React.ReactNode; disabled?: boolean; @@ -12,7 +12,7 @@ type FieldsetProps = { CommonStyledProps; const StyledFieldset = styled.fieldset< - Pick & { $disabled: boolean } + Pick & { $disabled: boolean } >` position: relative; border: 2px solid @@ -31,7 +31,7 @@ const StyledFieldset = styled.fieldset< ${props => props.$disabled && createDisabledTextStyles()} `; -const StyledLegend = styled.legend>` +const StyledLegend = styled.legend>` display: flex; position: absolute; top: 0; @@ -44,11 +44,11 @@ const StyledLegend = styled.legend>` variant === 'flat' ? theme.canvas : theme.material}; `; -const Fieldset = forwardRef( - function Fieldset( +const GroupBox = forwardRef( + ( { label, disabled = false, variant = 'default', children, ...otherProps }, ref - ) { + ) => { return ( ( } ); -export { Fieldset, FieldsetProps }; +GroupBox.displayName = 'GroupBox'; + +export { GroupBox, GroupBoxProps }; diff --git a/src/Bar/Bar.spec.tsx b/src/Handle/Handle.spec.tsx similarity index 71% rename from src/Bar/Bar.spec.tsx rename to src/Handle/Handle.spec.tsx index de79c49c..4455413b 100644 --- a/src/Bar/Bar.spec.tsx +++ b/src/Handle/Handle.spec.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { renderWithTheme } from '../../test/utils'; -import { Bar } from './Bar'; +import { Handle } from './Handle'; -describe('', () => { +describe('', () => { it('should render bar', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const barEl = container.firstChild; expect(barEl).toBeInTheDocument(); @@ -14,7 +14,7 @@ describe('', () => { it('should handle custom style', () => { const { container } = renderWithTheme( - + ); const barEl = container.firstChild; @@ -23,7 +23,7 @@ describe('', () => { it('should handle custom props', () => { const customProps = { title: 'potatoe' }; - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const barEl = container.firstChild; expect(barEl).toHaveAttribute('title', 'potatoe'); @@ -31,14 +31,14 @@ describe('', () => { describe('prop: size', () => { it('should set proper size', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const barEl = container.firstChild; expect(barEl).toHaveStyleRule('height', '85%'); }); it('when passed a number, sets size in px', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const barEl = container.firstChild; expect(barEl).toHaveStyleRule('height', '25px'); diff --git a/src/Bar/Bar.stories.tsx b/src/Handle/Handle.stories.tsx similarity index 73% rename from src/Bar/Bar.stories.tsx rename to src/Handle/Handle.stories.tsx index 2e018b0b..a6a9fe04 100644 --- a/src/Bar/Bar.stories.tsx +++ b/src/Handle/Handle.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { AppBar, Bar, Button, Toolbar } from 'react95'; +import { AppBar, Button, Handle, Toolbar } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -9,21 +9,21 @@ const Wrapper = styled.div` `; export default { - title: 'Bar', - component: Bar, + title: 'Controls/Handle', + component: Handle, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { return ( - + - + ); diff --git a/src/Bar/Bar.tsx b/src/Handle/Handle.tsx similarity index 85% rename from src/Bar/Bar.tsx rename to src/Handle/Handle.tsx index 1adb0c32..fc8fd851 100644 --- a/src/Bar/Bar.tsx +++ b/src/Handle/Handle.tsx @@ -3,14 +3,14 @@ import styled from 'styled-components'; import { CommonStyledProps } from '../types'; import { getSize } from '../common/utils'; -type BarProps = { +type HandleProps = { size?: string | number; } & React.HTMLAttributes & CommonStyledProps; // TODO: add horizontal variant // TODO: allow user to specify number of bars (like 3 horizontal bars for drag handle) -const Bar = styled.div` +const Handle = styled.div` ${({ theme, size = '100%' }) => ` display: inline-block; box-sizing: border-box; @@ -24,4 +24,6 @@ const Bar = styled.div` `} `; -export { Bar, BarProps }; +Handle.displayName = 'Handle'; + +export { Handle, HandleProps }; diff --git a/src/Hourglass/Hourglass.mdx b/src/Hourglass/Hourglass.mdx deleted file mode 100644 index e4ca81be..00000000 --- a/src/Hourglass/Hourglass.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Hourglass -menu: Components ---- - -import { Hourglass } from './Hourglass'; - -# Hourglass - -## Usage - - - - - -## API - -### Import - -``` -import { Hourglass } from 'react95' -``` - -### Props - - diff --git a/src/Hourglass/Hourglass.stories.tsx b/src/Hourglass/Hourglass.stories.tsx index 8657f775..14c73c31 100644 --- a/src/Hourglass/Hourglass.stories.tsx +++ b/src/Hourglass/Hourglass.stories.tsx @@ -9,7 +9,7 @@ const Wrapper = styled.div` `; export default { - title: 'Hourglass', + title: 'Other/Hourglass', component: Hourglass, decorators: [story => {story()}] } as ComponentMeta; diff --git a/src/Hourglass/Hourglass.tsx b/src/Hourglass/Hourglass.tsx index d20ae4f7..807bc28d 100644 --- a/src/Hourglass/Hourglass.tsx +++ b/src/Hourglass/Hourglass.tsx @@ -22,7 +22,7 @@ const StyledHourglass = styled.span` `; const Hourglass = forwardRef( - function Hourglass({ size = 30, ...otherProps }, ref) { + ({ size = 30, ...otherProps }, ref) => { return ( @@ -31,4 +31,6 @@ const Hourglass = forwardRef( } ); +Hourglass.displayName = 'Hourglass'; + export { Hourglass, HourglassProps }; diff --git a/src/List/List.mdx b/src/List/List.mdx deleted file mode 100644 index b3d8546a..00000000 --- a/src/List/List.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: List -menu: Components ---- - -import { List } from './List'; -import { ListItem } from '../ListItem/ListItem'; - -# List - -## Usage - -#### Default - - - - Photos - Videos - Other - - - -#### Inline - - - - - - 🌿 - - - - Tackle - Growl - Razor Leaf - - - -#### No shadow - - - - Photos - Videos - Other - - - -#### Full width - - - - Photos - Videos - Other - - - -## API - -### Import - -``` -import { List } from 'react95' -``` - -### Props - - diff --git a/src/List/List.spec.tsx b/src/List/List.spec.tsx deleted file mode 100644 index 48943d95..00000000 --- a/src/List/List.spec.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import { renderWithTheme } from '../../test/utils'; - -import { List } from './List'; - -describe('', () => { - it('renders List', () => { - const { container } = renderWithTheme(); - const list = container.firstElementChild; - - expect(list).toBeInTheDocument(); - }); - it('is an ul', () => { - const { container } = renderWithTheme(); - const list = container.firstElementChild; - - expect(list?.tagName).toBe('UL'); - }); - it('renders children', () => { - const textContent = 'Hi there!'; - const { getByText } = renderWithTheme( - - {textContent} - - ); - expect(getByText(textContent)).toBeInTheDocument(); - }); - - describe('prop: inline', () => { - it('renders inline', () => { - const { container } = renderWithTheme(); - const list = container.firstElementChild; - - expect(list).toHaveStyleRule('display', 'inline-flex'); - expect(list).toHaveStyleRule('align-items', 'center'); - }); - }); - describe('prop: fullWidth', () => { - it('has 100% width', () => { - const { container } = renderWithTheme(); - const list = container.firstElementChild; - - expect(list).toHaveStyleRule('width', '100%'); - }); - }); -}); diff --git a/src/List/List.stories.tsx b/src/List/List.stories.tsx deleted file mode 100644 index b365fcb6..00000000 --- a/src/List/List.stories.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ComponentMeta } from '@storybook/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { Bar, Divider, List, ListItem } from 'react95'; - -const Wrapper = styled.div` - padding: 5rem; - background: ${({ theme }) => theme.desktopBackground}; - display: flex; - align-items: center; - & > * { - margin-right: 1rem; - } -`; - -export default { - title: 'List', - component: List, - subcomponents: { ListItem }, - decorators: [story => {story()}] -} as ComponentMeta; - -export function Default() { - return ( - <> - - Photos - - Link - - Other - - - - - 🌿 - - - - Tackle - Growl - Razor Leaf - - - - View - - - Paste - Paste Shortcut - Undo Copy - - Properties - - - - - 😎 - - - - - 🤖 - - - - - 🎁 - - - - - ); -} - -Default.story = { - name: 'default' -}; diff --git a/src/ListItem/ListItem.mdx b/src/ListItem/ListItem.mdx deleted file mode 100644 index 7f398f56..00000000 --- a/src/ListItem/ListItem.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: ListItem -menu: Components ---- - -import ListItem from './ListItem'; -import List from '../List/List'; -import Divider from '../Divider/Divider'; - -# ListItem - -## Usage - -#### Default - - - - Item 1 - Item 2 - Item 3 - - - -#### Disabled - - - - disabled - disabled - disabled - - - -#### Square - - - - - - 😎 - - - - - 🤖 - - - - - 🎁 - - - - - -#### Small size - - - - View - - Paste - Paste Shortcut - Undo Copy - - Properties - - - -#### Render as link - - - - Normal item - - - 🔗 - - Link! - - - - -#### Primary - - - - Item 1 - Item 2 - Item 3 - - - -## API - -### Import - -``` -import { ListItem } from 'react95' -``` - -### Props - - diff --git a/src/ListItem/ListItem.spec.tsx b/src/ListItem/ListItem.spec.tsx deleted file mode 100644 index 6adb7734..00000000 --- a/src/ListItem/ListItem.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; - -import { renderWithTheme, theme } from '../../test/utils'; -import { blockSizes } from '../common/system'; -import { ListItem } from './ListItem'; - -const defaultSize = 'lg'; -describe('', () => { - it('renders ListItem', () => { - const { getByRole } = renderWithTheme(); - const listItem = getByRole('menuitem'); - expect(listItem).toBeInTheDocument(); - expect(listItem).not.toHaveAttribute('aria-disabled'); - }); - it('renders children', () => { - const textContent = 'Hi there!'; - const { getByText } = renderWithTheme( - - {textContent} - - ); - expect(getByText(textContent)).toBeInTheDocument(); - }); - it('should have a default role of menuitem', () => { - const { getByRole } = renderWithTheme(); - const listItem = getByRole('menuitem'); - expect(listItem).toHaveAttribute('role', 'menuitem'); - }); - - it('should render with custom role', () => { - const { getByRole } = renderWithTheme(); - const listItem = getByRole('option'); - expect(listItem).toHaveAttribute('role', 'option'); - }); - - // it('should have a tabIndex of -1 by default', () => { - // const { getByRole } = renderWithTheme(); - // const listItem = getByRole('menuitem'); - // expect(listItem).toHaveAttribute('tabIndex', '-1'); - // }); - describe('prop: disabled', () => { - it('should not trigger onClick callback', () => { - const clickHandler = jest.fn(); - const { getByRole } = renderWithTheme( - - ); - const listItem = getByRole('menuitem') as HTMLElement; - listItem.click(); - expect(clickHandler).not.toBeCalled(); - expect(listItem).toHaveAttribute('aria-disabled', 'true'); - }); - it('renders with disabled styles ', () => { - const { getByRole } = renderWithTheme(); - const listItem = getByRole('menuitem'); - expect(listItem).toHaveStyleRule('pointer-events', 'none'); - expect(listItem).toHaveStyleRule('color', theme.materialTextDisabled); - expect(listItem).toHaveStyleRule( - 'text-shadow', - `1px 1px ${theme.materialTextDisabledShadow}` - ); - }); - }); - describe('prop: onClick', () => { - it('should be called when clicked', () => { - const clickHandler = jest.fn(); - const { getByRole } = renderWithTheme( - - ); - const listItem = getByRole('menuitem') as HTMLElement; - listItem.click(); - expect(clickHandler).toHaveBeenCalledTimes(1); - }); - }); - describe('prop: square', () => { - it('should render square ListItem', () => { - const { getByRole } = renderWithTheme(); - const listItem = getByRole('menuitem'); - - expect(listItem).toHaveStyleRule('width', blockSizes[defaultSize]); - expect(listItem).toHaveStyleRule('height', blockSizes[defaultSize]); - }); - }); - describe('prop: size', () => { - it('should define ListItem height', () => { - const size = 'sm'; - const { getByRole } = renderWithTheme(); - const listItem = getByRole('menuitem'); - - expect(listItem).toHaveStyleRule('height', blockSizes[size]); - }); - }); -}); diff --git a/src/LoadingIndicator/LoadingIndicator.spec.tsx b/src/LoadingIndicator/LoadingIndicator.spec.tsx deleted file mode 100644 index 929ec4d4..00000000 --- a/src/LoadingIndicator/LoadingIndicator.spec.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { renderWithTheme } from '../../test/utils'; -import { LoadingIndicator } from './LoadingIndicator'; - -describe('', () => { - it('renders LoadingIndicator', () => { - const { getByRole } = renderWithTheme(); - - const progress = getByRole('progressbar'); - - expect(progress).toBeInTheDocument(); - }); - - describe('prop: isLoading', () => { - it('when set to false, does not display progress bars', () => { - const { rerender, queryByTestId } = renderWithTheme(); - - expect(queryByTestId('loading-wrapper')).not.toBeNull(); - - rerender(); - - expect(queryByTestId('loading-wrapper')).toBeNull(); - }); - }); -}); diff --git a/src/LoadingIndicator/LoadingIndicator.stories.tsx b/src/LoadingIndicator/LoadingIndicator.stories.tsx deleted file mode 100644 index f200813a..00000000 --- a/src/LoadingIndicator/LoadingIndicator.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ComponentMeta } from '@storybook/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { LoadingIndicator } from 'react95'; - -const Wrapper = styled.div` - background: ${({ theme }) => theme.material}; - padding: 5rem; -`; - -export default { - title: 'LoadingIndicator', - component: LoadingIndicator, - decorators: [story => {story()}] -} as ComponentMeta; - -export function Default() { - return ( - <> -

Loading...

- - - ); -} - -Default.story = { - name: 'default' -}; diff --git a/src/LoadingIndicator/LoadingIndicator.tsx b/src/LoadingIndicator/LoadingIndicator.tsx deleted file mode 100644 index 78457d71..00000000 --- a/src/LoadingIndicator/LoadingIndicator.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { forwardRef } from 'react'; -import styled, { css, keyframes } from 'styled-components'; -import { StyledCutout } from '../Cutout/Cutout'; -import { CommonStyledProps } from '../types'; - -type LoadingIndicatorProps = { - isLoading?: boolean; - shadow?: boolean; -} & React.HTMLAttributes & - CommonStyledProps; - -const Wrapper = styled.div` - display: inline-block; - height: 15px; - width: 100%; -`; -const ProgressCutout = styled(StyledCutout)` - width: 100%; - height: 100%; - width: 100%; - position: relative; - padding: 0; - overflow: hidden; -`; - -// animations taken from https://material.io/develop/web/ Linear Progress -const primaryTranslate = keyframes` - 0% { - transform: translateX(0); - } - 20% { - animation-timing-function: cubic-bezier(0.5, 0, 0.701732, 0.495819); - transform: translateX(0); - } - 59.15% { - animation-timing-function: cubic-bezier(0.302435, 0.381352, 0.55, 0.956352); - transform: translateX(83.67142%); - } - 100% { - transform: translateX(200.611057%); - } -`; -const primaryScale = keyframes` -0% { - transform: scaleX(0.08); - } - 36.65% { - animation-timing-function: cubic-bezier(0.334731, 0.12482, 0.785844, 1); - transform: scaleX(0.08); - } - 69.15% { - animation-timing-function: cubic-bezier(0.06, 0.11, 0.6, 1); - transform: scaleX(0.661479); - } - 100% { - transform: scaleX(0.08); - } -`; -const secondaryTranslate = keyframes` - 0% { - animation-timing-function: cubic-bezier(0.15, 0, 0.515058, 0.409685); - transform: translateX(0); - } - 25% { - animation-timing-function: cubic-bezier(0.31033, 0.284058, 0.8, 0.733712); - transform: translateX(37.651913%); - } - 48.35% { - animation-timing-function: cubic-bezier(0.4, 0.627035, 0.6, 0.902026); - transform: translateX(84.386165%); - } - 100% { - transform: translateX(160.277782%); - } -`; -const secondaryScale = keyframes` - 0% { - animation-timing-function: cubic-bezier(0.205028, 0.057051, 0.57661, 0.453971); - transform: scaleX(0.08); - } - 19.15% { - animation-timing-function: cubic-bezier(0.152313, 0.196432, 0.648374, 1.004315); - transform: scaleX(0.457104); - } - 44.15% { - animation-timing-function: cubic-bezier(0.257759, -0.003163, 0.211762, 1.38179); - transform: scaleX(0.72796); - } - 100% { - transform: scaleX(0.08); - } -`; -const sharedIndeterminateStyles = css` - height: 100%; - width: 100%; - position: absolute; - transform-origin: top left; - transform: scaleX(0); -`; -const sharedIndeterminateInnerStyles = css` - height: 100%; - width: 100%; - display: inline-block; - background: ${({ theme }) => theme.progress}; - position: absolute; -`; -const IndeterminateWrapper = styled.div` - position: relative; - top: 2px; - left: 2px; - width: calc(100% - 4px); - height: calc(100% - 4px); - overflow: hidden; -`; -const IndeterminatePrimary = styled.div` - ${sharedIndeterminateStyles} - left: -145.166611%; - animation: ${primaryTranslate} 2s infinite linear; -`; -const IndeterminatePrimaryInner = styled.span` - ${sharedIndeterminateInnerStyles} - animation: ${primaryScale} 2s infinite linear; -`; -const IndeterminateSecondary = styled.div` - ${sharedIndeterminateStyles} - left: -54.888891%; - animation: ${secondaryTranslate} 2s infinite linear; -`; -const IndeterminateSecondaryInner = styled.span` - ${sharedIndeterminateInnerStyles} - animation: ${secondaryScale} 2s infinite linear; -`; - -const LoadingIndicator = forwardRef( - function LoadingIndicator( - { isLoading = true, shadow = false, ...otherProps }, - ref - ) { - return ( - - - {isLoading && ( - - - - - - - - - )} - - - ); - } -); - -export { LoadingIndicator, LoadingIndicatorProps }; diff --git a/src/MenuList/MenuList.spec.tsx b/src/MenuList/MenuList.spec.tsx new file mode 100644 index 00000000..4bbae381 --- /dev/null +++ b/src/MenuList/MenuList.spec.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import { renderWithTheme } from '../../test/utils'; + +import { MenuList } from './MenuList'; + +describe('', () => { + it('renders MenuList', () => { + const { container } = renderWithTheme(); + const menuList = container.firstElementChild; + + expect(menuList).toBeInTheDocument(); + }); + it('is an ul', () => { + const { container } = renderWithTheme(); + const menuList = container.firstElementChild; + + expect(menuList?.tagName).toBe('UL'); + }); + it('renders children', () => { + const textContent = 'Hi there!'; + const { getByText } = renderWithTheme( + + {textContent} + + ); + expect(getByText(textContent)).toBeInTheDocument(); + }); + + describe('prop: inline', () => { + it('renders inline', () => { + const { container } = renderWithTheme(); + const menuList = container.firstElementChild; + + expect(menuList).toHaveStyleRule('display', 'inline-flex'); + expect(menuList).toHaveStyleRule('align-items', 'center'); + }); + }); + describe('prop: fullWidth', () => { + it('has 100% width', () => { + const { container } = renderWithTheme(); + const menuList = container.firstElementChild; + + expect(menuList).toHaveStyleRule('width', '100%'); + }); + }); +}); diff --git a/src/MenuList/MenuList.stories.tsx b/src/MenuList/MenuList.stories.tsx new file mode 100644 index 00000000..05633a91 --- /dev/null +++ b/src/MenuList/MenuList.stories.tsx @@ -0,0 +1,85 @@ +import { ComponentMeta } from '@storybook/react'; +import React from 'react'; +import { Handle, MenuList, MenuListItem, Separator } from 'react95'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + padding: 5rem; + background: ${({ theme }) => theme.desktopBackground}; + display: flex; + align-items: center; + & > * { + margin-right: 1rem; + } +`; + +export default { + title: 'Controls/MenuList', + component: MenuList, + subcomponents: { MenuListItem }, + decorators: [story => {story()}] +} as ComponentMeta; + +export function Default() { + return ( + <> + + Photos + + Link + + Other + + + + + 🌿 + + + + Tackle + Growl + Razor Leaf + + + + View + + + Paste + Paste Shortcut + Undo Copy + + Properties + + + + + 😎 + + + + + 🤖 + + + + + 🎁 + + + + + ); +} + +Default.story = { + name: 'default' +}; diff --git a/src/List/List.tsx b/src/MenuList/MenuList.tsx similarity index 67% rename from src/List/List.tsx rename to src/MenuList/MenuList.tsx index 2a175c3f..57b57848 100644 --- a/src/List/List.tsx +++ b/src/MenuList/MenuList.tsx @@ -4,20 +4,20 @@ import styled from 'styled-components'; import { createBorderStyles, createBoxStyles } from '../common'; import { CommonStyledProps } from '../types'; -type ListProps = React.HTMLAttributes & { +type MenuListProps = React.HTMLAttributes & { fullWidth?: boolean; shadow?: boolean; inline?: boolean; } & CommonStyledProps; // TODO keyboard controls -const List = styled.ul.attrs(() => ({ +const MenuList = styled.ul.attrs(() => ({ role: 'menu' -}))` +}))` box-sizing: border-box; width: ${props => (props.fullWidth ? '100%' : 'auto')}; padding: 4px; - ${createBorderStyles({ windowBorders: true })} + ${createBorderStyles({ style: 'window' })} ${createBoxStyles()} ${props => props.inline && @@ -29,4 +29,8 @@ const List = styled.ul.attrs(() => ({ position: relative; `; -export { List, ListProps }; +MenuList.displayName = 'MenuList'; + +export * from './MenuListItem'; + +export { MenuList, MenuListProps }; diff --git a/src/MenuList/MenuListItem.spec.tsx b/src/MenuList/MenuListItem.spec.tsx new file mode 100644 index 00000000..734d31d2 --- /dev/null +++ b/src/MenuList/MenuListItem.spec.tsx @@ -0,0 +1,92 @@ +import React from 'react'; + +import { renderWithTheme, theme } from '../../test/utils'; +import { blockSizes } from '../common/system'; +import { MenuListItem } from './MenuListItem'; + +const defaultSize = 'lg'; +describe('', () => { + it('renders MenuListItem', () => { + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('menuitem'); + expect(menuListItem).toBeInTheDocument(); + expect(menuListItem).not.toHaveAttribute('aria-disabled'); + }); + it('renders children', () => { + const textContent = 'Hi there!'; + const { getByText } = renderWithTheme( + + {textContent} + + ); + expect(getByText(textContent)).toBeInTheDocument(); + }); + it('should have a default role of menuitem', () => { + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('menuitem'); + expect(menuListItem).toHaveAttribute('role', 'menuitem'); + }); + + it('should render with custom role', () => { + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('option'); + expect(menuListItem).toHaveAttribute('role', 'option'); + }); + + // it('should have a tabIndex of -1 by default', () => { + // const { getByRole } = renderWithTheme(); + // const menuListItem = getByRole('menuitem'); + // expect(menuListItem).toHaveAttribute('tabIndex', '-1'); + // }); + describe('prop: disabled', () => { + it('should not trigger onClick callback', () => { + const clickHandler = jest.fn(); + const { getByRole } = renderWithTheme( + + ); + const menuListItem = getByRole('menuitem') as HTMLElement; + menuListItem.click(); + expect(clickHandler).not.toBeCalled(); + expect(menuListItem).toHaveAttribute('aria-disabled', 'true'); + }); + it('renders with disabled styles ', () => { + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('menuitem'); + expect(menuListItem).toHaveStyleRule('pointer-events', 'none'); + expect(menuListItem).toHaveStyleRule('color', theme.materialTextDisabled); + expect(menuListItem).toHaveStyleRule( + 'text-shadow', + `1px 1px ${theme.materialTextDisabledShadow}` + ); + }); + }); + describe('prop: onClick', () => { + it('should be called when clicked', () => { + const clickHandler = jest.fn(); + const { getByRole } = renderWithTheme( + + ); + const menuListItem = getByRole('menuitem') as HTMLElement; + menuListItem.click(); + expect(clickHandler).toHaveBeenCalledTimes(1); + }); + }); + describe('prop: square', () => { + it('should render square MenuListItem', () => { + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('menuitem'); + + expect(menuListItem).toHaveStyleRule('width', blockSizes[defaultSize]); + expect(menuListItem).toHaveStyleRule('height', blockSizes[defaultSize]); + }); + }); + describe('prop: size', () => { + it('should define MenuListItem height', () => { + const size = 'sm'; + const { getByRole } = renderWithTheme(); + const menuListItem = getByRole('menuitem'); + + expect(menuListItem).toHaveStyleRule('height', blockSizes[size]); + }); + }); +}); diff --git a/src/ListItem/ListItem.tsx b/src/MenuList/MenuListItem.tsx similarity index 59% rename from src/ListItem/ListItem.tsx rename to src/MenuList/MenuListItem.tsx index 31c47890..45c7e1f2 100644 --- a/src/ListItem/ListItem.tsx +++ b/src/MenuList/MenuListItem.tsx @@ -5,7 +5,7 @@ import { createDisabledTextStyles } from '../common'; import { blockSizes } from '../common/system'; import { CommonStyledProps, Sizes } from '../types'; -type ListItemProps = { +type MenuListItemProps = { disabled?: boolean; square?: boolean; primary?: boolean; @@ -13,7 +13,7 @@ type ListItemProps = { } & React.HTMLAttributes & CommonStyledProps; -export const StyledListItem = styled.li<{ +export const StyledMenuListItem = styled.li<{ $disabled?: boolean; square?: boolean; primary?: boolean; @@ -49,40 +49,44 @@ export const StyledListItem = styled.li<{ ${props => props.$disabled && createDisabledTextStyles()} `; -const ListItem = forwardRef(function ListItem( - { - size = 'lg', - disabled, - // tabIndex: tabIndexProp, - square, - children, - onClick, - primary, - ...otherProps - }, - ref -) { - // let tabIndex; - // if (!disabled) { - // tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1; - // } +const MenuListItem = forwardRef( + ( + { + size = 'lg', + disabled, + // tabIndex: tabIndexProp, + square, + children, + onClick, + primary, + ...otherProps + }, + ref + ) => { + // let tabIndex; + // if (!disabled) { + // tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1; + // } - return ( - - {children} - - ); -}); + return ( + + {children} + + ); + } +); + +MenuListItem.displayName = 'MenuListItem'; -export { ListItem, ListItemProps }; +export { MenuListItem, MenuListItemProps }; diff --git a/src/Desktop/Desktop.spec.tsx b/src/Monitor/Monitor.spec.tsx similarity index 61% rename from src/Desktop/Desktop.spec.tsx rename to src/Monitor/Monitor.spec.tsx index c7e239e0..43f01b08 100644 --- a/src/Desktop/Desktop.spec.tsx +++ b/src/Monitor/Monitor.spec.tsx @@ -2,30 +2,30 @@ import React from 'react'; import { renderWithTheme } from '../../test/utils'; -import { Desktop } from './Desktop'; +import { Monitor } from './Monitor'; -describe('', () => { +describe('', () => { it('should render', () => { - const { container } = renderWithTheme(); - const desktopElement = container.firstElementChild; + const { container } = renderWithTheme(); + const monitorElement = container.firstElementChild; - expect(desktopElement).toBeInTheDocument(); + expect(monitorElement).toBeInTheDocument(); }); it('should handle custom props', () => { const customProps: React.HTMLAttributes = { title: 'potatoe' }; - const { container } = renderWithTheme(); - const desktopElement = container.firstElementChild; + const { container } = renderWithTheme(); + const monitorElement = container.firstElementChild; - expect(desktopElement).toHaveAttribute('title', 'potatoe'); + expect(monitorElement).toHaveAttribute('title', 'potatoe'); }); describe('prop: backgroundStyles', () => { it('should forward styles to background element', () => { const { getByTestId } = renderWithTheme( - + ); const backgroundElement = getByTestId('background'); @@ -38,7 +38,7 @@ describe('', () => { describe('prop: children', () => { it('children should be rendered in background element', () => { - const { getByTestId } = renderWithTheme(Hi!); + const { getByTestId } = renderWithTheme(Hi!); const backgroundElement = getByTestId('background'); expect(backgroundElement.innerHTML).toBe('Hi!'); diff --git a/src/Desktop/Desktop.stories.tsx b/src/Monitor/Monitor.stories.tsx similarity index 66% rename from src/Desktop/Desktop.stories.tsx rename to src/Monitor/Monitor.stories.tsx index dfaa324f..2e3394fd 100644 --- a/src/Desktop/Desktop.stories.tsx +++ b/src/Monitor/Monitor.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { Desktop } from 'react95'; +import { Monitor } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -9,13 +9,13 @@ const Wrapper = styled.div` `; export default { - title: 'Desktop', - component: Desktop, + title: 'Other/Monitor', + component: Monitor, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { - return ; + return ; } Default.story = { diff --git a/src/Desktop/Desktop.tsx b/src/Monitor/Monitor.tsx similarity index 81% rename from src/Desktop/Desktop.tsx rename to src/Monitor/Monitor.tsx index 22541bb0..8abfa45e 100644 --- a/src/Desktop/Desktop.tsx +++ b/src/Monitor/Monitor.tsx @@ -1,9 +1,9 @@ import React, { forwardRef } from 'react'; import styled from 'styled-components'; -import { StyledCutout } from '../Cutout/Cutout'; +import { StyledScrollView } from '../ScrollView/ScrollView'; -type DesktopProps = { +type MonitorProps = { backgroundStyles?: React.CSSProperties; children?: React.ReactNode; }; @@ -18,7 +18,7 @@ const Inner = styled.div` position: relative; `; -const Monitor = styled.div` +const MonitorBody = styled.div` position: relative; z-index: 1; box-sizing: border-box; @@ -56,7 +56,7 @@ const Monitor = styled.div` } `; -const Background = styled(StyledCutout).attrs(() => ({ +const Background = styled(StyledScrollView).attrs(() => ({ 'data-testid': 'background' }))` width: 100%; @@ -106,20 +106,21 @@ const Stand = styled.div` } `; -const Desktop = forwardRef(function Desktop( - { backgroundStyles, children, ...otherProps }, - ref -) { - return ( - - - - {children} - - - - - ); -}); +const Monitor = forwardRef( + ({ backgroundStyles, children, ...otherProps }, ref) => { + return ( + + + + {children} + + + + + ); + } +); + +Monitor.displayName = 'Monitor'; -export { Desktop, DesktopProps }; +export { Monitor, MonitorProps }; diff --git a/src/NumberField/NumberField.mdx b/src/NumberField/NumberField.mdx deleted file mode 100644 index eeba5ce1..00000000 --- a/src/NumberField/NumberField.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: NumberField -menu: Components ---- - -import { NumberField } from './NumberField'; -import { Cutout } from '../Cutout/Cutout'; - -# NumberField - -## Usage - -#### Default - - - console.log(value)} /> - - -#### Fixed width - - - console.log(value)} /> - - -#### Disabled - - - console.log(value)} /> - - -#### Disabled keyboard input - - - console.log(value)} - /> - - -#### No shadow - - - console.log(value)} - /> - - -#### Flat - - - -

- When you want to use NumberField on a light background (like scrollable - content), just use the flat variant: -

- console.log(value)} - /> -
-
- -## API - -### Import - -``` -import { NumberField } from 'react95' -``` - -### Props - - diff --git a/src/NumberField/NumberField.spec.tsx b/src/NumberInput/NumberInput.spec.tsx similarity index 86% rename from src/NumberField/NumberField.spec.tsx rename to src/NumberInput/NumberInput.spec.tsx index ee3ee61a..486d193f 100644 --- a/src/NumberField/NumberField.spec.tsx +++ b/src/NumberInput/NumberInput.spec.tsx @@ -2,15 +2,15 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import { renderWithTheme } from '../../test/utils'; -import { NumberField } from './NumberField'; +import { NumberInput } from './NumberInput'; // TODO: should we pass number or string to callbacks? -describe('', () => { +describe('', () => { it('should call onChange on spin buttons click', () => { const handleChange = jest.fn(); const { getByTestId } = renderWithTheme( - + ); const spinButton = getByTestId('increment'); spinButton.click(); @@ -21,7 +21,7 @@ describe('', () => { it('should call onChange on blur after keyboard input', () => { const handleChange = jest.fn(); const { container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; input.focus(); @@ -39,7 +39,7 @@ describe('', () => { const handleChange = jest.fn(); const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -55,7 +55,7 @@ describe('', () => { it('should give correct result after user changes input value and then clicks increment button', () => { const handleChange = jest.fn(); const { container, getByTestId } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -68,7 +68,7 @@ describe('', () => { it('should reach max value', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -79,7 +79,7 @@ describe('', () => { it('should reach min value', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const decrementButton = getByTestId('decrement'); @@ -91,7 +91,7 @@ describe('', () => { describe('prop: step', () => { it('should be 1 by default', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -102,7 +102,7 @@ describe('', () => { it('should change value by specified step', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const decrementButton = getByTestId('decrement'); @@ -113,7 +113,7 @@ describe('', () => { it('should handle decimal step', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const decrementButton = getByTestId('decrement'); @@ -126,7 +126,7 @@ describe('', () => { describe('prop: disabled', () => { it('should render disabled', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -139,7 +139,7 @@ describe('', () => { it('should not react to button clicks', () => { const { getByTestId, container } = renderWithTheme( - + ); const input = container.querySelector('input') as HTMLInputElement; const incrementButton = getByTestId('increment'); @@ -156,7 +156,7 @@ describe('', () => { describe('prop: width', () => { it('should render component of specified width', () => { const { container } = renderWithTheme( - + ); expect( getComputedStyle(container.firstElementChild as HTMLInputElement).width @@ -165,7 +165,7 @@ describe('', () => { it('should handle %', () => { const { container } = renderWithTheme( - + ); expect( getComputedStyle(container.firstElementChild as HTMLInputElement).width diff --git a/src/NumberField/NumberField.stories.tsx b/src/NumberInput/NumberInput.stories.tsx similarity index 61% rename from src/NumberField/NumberField.stories.tsx rename to src/NumberInput/NumberInput.stories.tsx index 84f5b93e..d9f089c6 100644 --- a/src/NumberField/NumberField.stories.tsx +++ b/src/NumberInput/NumberInput.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { Cutout, NumberField } from 'react95'; +import { ScrollView, NumberInput } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -21,19 +21,19 @@ const Wrapper = styled.div` `; export default { - title: 'NumberField', - component: NumberField, + title: 'Controls/NumberInput', + component: NumberInput, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { return ( <> - +
- +
- + ); } @@ -44,12 +44,12 @@ Default.story = { export function Flat() { return ( - +

- When you want to use NumberField on a light background (like scrollable + When you want to use NumberInput on a light background (like scrollable content), just use the flat variant:

-
- +
- -
+ + ); } diff --git a/src/NumberField/NumberField.tsx b/src/NumberInput/NumberInput.tsx similarity index 90% rename from src/NumberField/NumberField.tsx rename to src/NumberInput/NumberInput.tsx index 76931564..7ef5d3f4 100644 --- a/src/NumberField/NumberField.tsx +++ b/src/NumberInput/NumberInput.tsx @@ -5,10 +5,10 @@ import { Button } from '../Button/Button'; import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled'; import { blockSizes } from '../common/system'; import { clamp, getSize } from '../common/utils'; -import { TextField } from '../TextField/TextField'; +import { TextInput } from '../TextInput/TextInput'; import { CommonStyledProps } from '../types'; -type NumberFieldProps = { +type NumberInputProps = { className?: string; defaultValue?: number; disabled?: boolean; @@ -22,12 +22,12 @@ type NumberFieldProps = { width?: string | number; } & CommonStyledProps; -const StyledNumberFieldWrapper = styled.div` +const StyledNumberInputWrapper = styled.div` display: inline-flex; align-items: center; `; -const StyledButton = styled(Button)>` +const StyledButton = styled(Button)>` width: 30px; padding: 0; flex-shrink: 0; @@ -49,7 +49,7 @@ const StyledButton = styled(Button)>` `} `; -const StyledButtonWrapper = styled.div>` +const StyledButtonWrapper = styled.div>` display: flex; flex-direction: column; flex-wrap: nowrap; @@ -96,9 +96,8 @@ const StyledButtonIcon = styled.span<{ invert?: boolean }>` `} } `; - -const NumberField = forwardRef( - function NumberField( +const NumberInput = forwardRef( + ( { className, defaultValue, @@ -113,7 +112,7 @@ const NumberField = forwardRef( width }, ref - ) { + ) => { const [valueDerived, setValueState] = useControlledOrUncontrolled({ value, defaultValue @@ -157,14 +156,14 @@ const NumberField = forwardRef( }, [handleClick, step]); return ( - - ( - + ); } ); -export { NumberField, NumberFieldProps }; +NumberInput.displayName = 'NumberInput'; + +export { NumberInput, NumberInputProps }; diff --git a/src/Panel/Panel.spec.tsx b/src/Panel/Panel.spec.tsx deleted file mode 100644 index 90d7e4a9..00000000 --- a/src/Panel/Panel.spec.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; - -import { Panel } from './Panel'; - -describe('', () => { - it('should render panel', () => { - const { container } = render(); - const panel = container.firstElementChild; - - expect(panel).toBeInTheDocument(); - }); - - it('should render custom styles', () => { - const { container } = render( - - ); - const panel = container.firstElementChild; - - expect(panel).toHaveAttribute('style', 'background-color: papayawhip;'); - }); - - it('should render children', async () => { - const { findByText } = render( - - Cool panel - - ); - const content = await findByText(/cool panel/i); - - expect(content).toBeInTheDocument(); - }); - - it('should render custom props', () => { - const customProps = { title: 'panel' }; - const { container } = render(); - const panel = container.firstElementChild; - - expect(panel).toHaveAttribute('title', 'panel'); - }); -}); diff --git a/src/Panel/Panel.tsx b/src/Panel/Panel.tsx deleted file mode 100644 index 58af24de..00000000 --- a/src/Panel/Panel.tsx +++ /dev/null @@ -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 & - 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>>` - position: relative; - font-size: 1rem; - ${({ variant }) => createPanelStyles(variant)} - ${createBoxStyles()} -`; - -const Panel = forwardRef(function Panel( - { children, shadow = false, variant = 'outside', ...otherProps }, - ref -) { - return ( - - {children} - - ); -}); - -export { Panel, PanelProps }; diff --git a/src/Progress/Progress.mdx b/src/Progress/Progress.mdx deleted file mode 100644 index 8965b107..00000000 --- a/src/Progress/Progress.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Progress -menu: Components ---- - -import { Progress } from './Progress'; - -# Progress - -## Usage - -#### Default - - - {() => { - { - /* - @toDo - for some reason the value isn't being updated, check this later - */ - } - const [percent, setPercent] = React.useState(0); - React.useEffect(() => { - const timer = setInterval(() => { - setPercent(previousPercent => { - if (previousPercent === 100) { - return 0; - } - const diff = Math.random() * 10; - return Math.min(previousPercent + diff, 100); - }); - }, 1000); - return () => clearInterval(timer); - }, []); - return ; - }} - - -## API - -### Import - -``` -import { Progress } from 'react95' -``` - -### Props - - diff --git a/src/Progress/Progress.tsx b/src/Progress/Progress.tsx deleted file mode 100644 index 79ea5cd7..00000000 --- a/src/Progress/Progress.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { - forwardRef, - useCallback, - useEffect, - useRef, - useState -} from 'react'; -import styled, { css } from 'styled-components'; - -import { blockSizes } from '../common/system'; -import { StyledCutout } from '../Cutout/Cutout'; -import { CommonStyledProps } from '../types'; - -type ProgressProps = { - hideValue?: boolean; - shadow?: boolean; - value?: number; - variant?: 'default' | 'tile'; -} & React.HTMLAttributes & - CommonStyledProps; - -const Wrapper = styled.div` - display: inline-block; - height: ${blockSizes.md}; - width: 100%; -`; -const ProgressCutout = styled(StyledCutout)` - width: 100%; - height: 100%; - width: 100%; - position: relative; - text-align: center; - padding: 0; - overflow: hidden; - &:before { - z-index: 1; - } -`; -const commonBarStyles = css` - width: calc(100% - 4px); - height: calc(100% - 4px); - - display: flex; - align-items: center; - justify-content: space-around; -`; -const WhiteBar = styled.div` - position: relative; - top: 4px; - ${commonBarStyles} - background: ${({ theme }) => theme.canvas}; - color: #000; - margin-left: 2px; - margin-top: -2px; - color: ${({ theme }) => theme.materialText}; -`; - -const BlueBar = styled.div>>` - position: absolute; - top: 2px; - left: 2px; - ${commonBarStyles} - color: ${({ theme }) => theme.materialTextInvert}; - background: ${({ theme }) => theme.progress}; - clip-path: polygon( - 0 0, - ${({ value }) => value}% 0, - ${({ value }) => value}% 100%, - 0 100% - ); - transition: 0.4s linear clip-path; -`; - -const TilesWrapper = styled.div` - width: calc(100% - 6px); - height: calc(100% - 8px); - position: absolute; - left: 3px; - top: 4px; - box-sizing: border-box; - display: inline-flex; -`; -const tileWidth = 17; -const Tile = styled.span` - display: inline-block; - width: ${tileWidth}px; - box-sizing: border-box; - height: 100%; - background: ${({ theme }) => theme.progress}; - border-color: ${({ theme }) => theme.material}; - border-width: 0px 1px; - border-style: solid; -`; - -const Progress = forwardRef(function Progress( - { - hideValue = false, - shadow = true, - value = 0, - variant = 'default', - ...otherProps - }, - ref -) { - const displayValue = hideValue ? null : `${value}%`; - - const tilesWrapperRef = useRef(null); - const [tiles, setTiles] = useState([]); - - // TODO debounce this function - const updateTilesNumber = useCallback(() => { - if (!tilesWrapperRef.current) { - return; - } - const progressWidth = tilesWrapperRef.current.getBoundingClientRect().width; - const newTilesNumber = Math.round( - ((value / 100) * progressWidth) / tileWidth - ); - setTiles(Array.from({ length: newTilesNumber })); - }, [value]); - - useEffect(() => { - updateTilesNumber(); - - window.addEventListener('resize', updateTilesNumber); - return () => window.removeEventListener('resize', updateTilesNumber); - }, [updateTilesNumber]); - - return ( - - - {variant === 'default' ? ( - <> - {displayValue} - - {displayValue} - - - ) : ( - - {tiles.map((_, index) => ( - - ))} - - )} - - - ); -}); - -export { Progress, ProgressProps }; diff --git a/src/Progress/Progress.spec.tsx b/src/ProgressBar/ProgressBar.spec.tsx similarity index 78% rename from src/Progress/Progress.spec.tsx rename to src/ProgressBar/ProgressBar.spec.tsx index 3288e7a0..fb2c053d 100644 --- a/src/Progress/Progress.spec.tsx +++ b/src/ProgressBar/ProgressBar.spec.tsx @@ -1,24 +1,26 @@ import React from 'react'; import { renderWithTheme } from '../../test/utils'; -import { Progress } from './Progress'; +import { ProgressBar } from './ProgressBar'; -describe('', () => { - it('renders Progress', () => { +describe('', () => { + it('renders ProgressBar', () => { const value = 32; - const { getByRole } = renderWithTheme(); + const { getByRole } = renderWithTheme(); - const progress = getByRole('progressbar'); + const progressBar = getByRole('progressbar'); - expect(progress).toBeInTheDocument(); - expect(progress).toHaveAttribute('aria-valuenow', value.toString()); + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveAttribute('aria-valuenow', value.toString()); }); describe('prop: variant', () => { describe('variant: "default"', () => { it('displays current percentage value', () => { const value = 32; - const { queryByTestId } = renderWithTheme(); + const { queryByTestId } = renderWithTheme( + + ); expect(queryByTestId('defaultProgress1')?.textContent).toBe( `${value}%` @@ -38,7 +40,9 @@ describe('', () => { describe('variant: "tile"', () => { it('Renders "tile" progress', () => { - const { queryByTestId } = renderWithTheme(); + const { queryByTestId } = renderWithTheme( + + ); expect(queryByTestId('defaultProgress1')).not.toBeInTheDocument(); expect(queryByTestId('defaultProgress2')).not.toBeInTheDocument(); expect(queryByTestId('tileProgress')).toBeInTheDocument(); @@ -66,7 +70,7 @@ describe('', () => { it('renders progress bars, but does not show value', () => { const value = 32; const { queryByTestId } = renderWithTheme( - + ); expect(queryByTestId('defaultProgress1')).toBeInTheDocument(); expect(queryByTestId('defaultProgress2')).toBeInTheDocument(); diff --git a/src/Progress/Progress.stories.tsx b/src/ProgressBar/ProgressBar.stories.tsx similarity index 83% rename from src/Progress/Progress.stories.tsx rename to src/ProgressBar/ProgressBar.stories.tsx index 87e6a4b9..458cae26 100644 --- a/src/Progress/Progress.stories.tsx +++ b/src/ProgressBar/ProgressBar.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React, { useEffect, useState } from 'react'; -import { Progress } from 'react95'; +import { ProgressBar } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -9,10 +9,10 @@ const Wrapper = styled.div` `; export default { - title: 'Progress', - component: Progress, + title: 'Controls/ProgressBar', + component: ProgressBar, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { const [percent, setPercent] = useState(0); @@ -32,7 +32,7 @@ export function Default() { }; }, []); - return ; + return ; } Default.story = { @@ -57,7 +57,7 @@ export function Tile() { }; }, []); - return ; + return ; } Tile.story = { @@ -82,7 +82,7 @@ export function HideValue() { }; }, []); - return ; + return ; } HideValue.story = { diff --git a/src/ProgressBar/ProgressBar.tsx b/src/ProgressBar/ProgressBar.tsx new file mode 100644 index 00000000..349f8079 --- /dev/null +++ b/src/ProgressBar/ProgressBar.tsx @@ -0,0 +1,164 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useRef, + useState +} from 'react'; +import styled, { css } from 'styled-components'; + +import { blockSizes } from '../common/system'; +import { StyledScrollView } from '../ScrollView/ScrollView'; +import { CommonStyledProps } from '../types'; + +type ProgressBarProps = { + hideValue?: boolean; + shadow?: boolean; + value?: number; + variant?: 'default' | 'tile'; +} & React.HTMLAttributes & + CommonStyledProps; + +const Wrapper = styled.div>>` + display: inline-block; + height: ${blockSizes.md}; + width: 100%; +`; + +const ProgressCutout = styled(StyledScrollView)< + Required> +>` + width: 100%; + height: 100%; + position: relative; + text-align: center; + padding: 0; + overflow: hidden; + &:before { + z-index: 1; + } +`; +const commonBarStyles = css` + width: calc(100% - 4px); + height: calc(100% - 4px); + + display: flex; + align-items: center; + justify-content: space-around; +`; +const WhiteBar = styled.div` + position: relative; + top: 4px; + ${commonBarStyles} + background: ${({ theme }) => theme.canvas}; + color: #000; + margin-left: 2px; + margin-top: -2px; + color: ${({ theme }) => theme.materialText}; +`; + +const BlueBar = styled.div>` + position: absolute; + top: 2px; + left: 2px; + ${commonBarStyles} + color: ${({ theme }) => theme.materialTextInvert}; + background: ${({ theme }) => theme.progress}; + clip-path: polygon( + 0 0, + ${({ value = 0 }) => value}% 0, + ${({ value = 0 }) => value}% 100%, + 0 100% + ); + transition: 0.4s linear clip-path; +`; + +const TilesWrapper = styled.div` + width: calc(100% - 6px); + height: calc(100% - 8px); + position: absolute; + left: 3px; + top: 4px; + box-sizing: border-box; + display: inline-flex; +`; +const tileWidth = 17; +const Tile = styled.span` + display: inline-block; + width: ${tileWidth}px; + box-sizing: border-box; + height: 100%; + background: ${({ theme }) => theme.progress}; + border-color: ${({ theme }) => theme.material}; + border-width: 0px 1px; + border-style: solid; +`; + +const ProgressBar = forwardRef( + ( + { + hideValue = false, + shadow = true, + value, + variant = 'default', + ...otherProps + }, + ref + ) => { + const displayValue = hideValue ? null : `${value}%`; + + const tilesWrapperRef = useRef(null); + const [tiles, setTiles] = useState([]); + + // TODO debounce this function + const updateTilesNumber = useCallback(() => { + if (!tilesWrapperRef.current || value === undefined) { + return; + } + const progressWidth = + tilesWrapperRef.current.getBoundingClientRect().width; + const newTilesNumber = Math.round( + ((value / 100) * progressWidth) / tileWidth + ); + setTiles(Array.from({ length: newTilesNumber })); + }, [value]); + + useEffect(() => { + updateTilesNumber(); + + window.addEventListener('resize', updateTilesNumber); + return () => window.removeEventListener('resize', updateTilesNumber); + }, [updateTilesNumber]); + + return ( + + + {variant === 'default' ? ( + <> + {displayValue} + + {displayValue} + + + ) : ( + + {tiles.map((_, index) => ( + + ))} + + )} + + + ); + } +); + +ProgressBar.displayName = 'ProgressBar'; + +export { ProgressBar, ProgressBarProps }; diff --git a/src/Radio/Radio.mdx b/src/Radio/Radio.mdx deleted file mode 100644 index a576344b..00000000 --- a/src/Radio/Radio.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Radio -menu: Components ---- - -import { Radio } from './Radio'; -import Fieldset from '../Fieldset/Fieldset'; -import Window from '../Window/Window'; -import WindowContent from '../WindowContent/WindowContent'; - -# Radio - -## Usage - - - - - - - - - -## API - -### Import - -``` -import { Radio } from 'react95' -``` - -### Props - - diff --git a/src/Radio/Radio.stories.tsx b/src/Radio/Radio.stories.tsx index 0abd89ae..a36b1543 100644 --- a/src/Radio/Radio.stories.tsx +++ b/src/Radio/Radio.stories.tsx @@ -1,12 +1,12 @@ import { ComponentMeta } from '@storybook/react'; import React, { useState } from 'react'; import { - Cutout, - Divider, - Fieldset, - List, - ListItem, + GroupBox, + MenuList, + MenuListItem, Radio, + ScrollView, + Separator, Window, WindowContent } from 'react95'; @@ -26,7 +26,7 @@ const Wrapper = styled.div` } `; export default { - title: 'Radio', + title: 'Controls/Radio', component: Radio, decorators: [story => {story()}] } as ComponentMeta; @@ -39,7 +39,7 @@ export function Default() { return ( -
+ -
+
); @@ -90,13 +90,13 @@ export function Flat() { return ( - +

When you want to use radio buttons on a light background (like scrollable content), just use the flat variant:

-
+ -
-
+ +
); @@ -157,8 +157,8 @@ export function Menu() { const { tool, color } = state; return ( - - + + - - +
+ -
- - + + + - - + + - -
+ + ); } Menu.story = { diff --git a/src/Radio/Radio.tsx b/src/Radio/Radio.tsx index b1bee405..f49b51ca 100644 --- a/src/Radio/Radio.tsx +++ b/src/Radio/Radio.tsx @@ -2,14 +2,14 @@ import React, { forwardRef } from 'react'; import styled, { css, CSSProperties } from 'styled-components'; import { createFlatBoxStyles } from '../common'; -import { StyledCutout } from '../Cutout/Cutout'; -import { StyledListItem } from '../ListItem/ListItem'; import { LabelText, size, StyledInput, StyledLabel -} from '../SwitchBase/SwitchBase'; +} from '../common/SwitchBase'; +import { StyledMenuListItem } from '../MenuList/MenuList'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonStyledProps } from '../types'; type RadioVariant = 'default' | 'flat' | 'menu'; @@ -44,7 +44,7 @@ type StyledCheckboxProps = { $disabled: boolean; }; -const StyledCheckbox = styled(StyledCutout)` +const StyledCheckbox = styled(StyledScrollView)` ${sharedCheckboxStyles} background: ${({ $disabled, theme }) => $disabled ? theme.material : theme.canvas}; @@ -120,7 +120,7 @@ const Icon = styled.span.attrs(() => ({ : css` background: ${$disabled ? theme.checkmarkDisabled : theme.checkmark}; `} - ${StyledListItem}:hover & { + ${StyledMenuListItem}:hover & { ${({ $disabled, theme, variant }) => !$disabled && variant === 'menu' && @@ -136,38 +136,42 @@ const CheckboxComponents = { menu: StyledMenuCheckbox }; -const Radio = forwardRef(function Radio( - { - checked, - className = '', - disabled = false, - label = '', - onChange, - style = {}, - variant = 'default', - ...otherProps - }, - ref -) { - const CheckboxComponent = CheckboxComponents[variant]; +const Radio = forwardRef( + ( + { + checked, + className = '', + disabled = false, + label = '', + onChange, + style = {}, + variant = 'default', + ...otherProps + }, + ref + ) => { + const CheckboxComponent = CheckboxComponents[variant]; - return ( - - - {checked && } - - - {label && {label}} - - ); -}); + return ( + + + {checked && } + + + {label && {label}} + + ); + } +); + +Radio.displayName = 'Radio'; export { Radio, RadioProps }; diff --git a/src/ScrollView/ScrollView.spec.tsx b/src/ScrollView/ScrollView.spec.tsx new file mode 100644 index 00000000..71593cde --- /dev/null +++ b/src/ScrollView/ScrollView.spec.tsx @@ -0,0 +1,44 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ScrollView } from './ScrollView'; + +describe('', () => { + it('should render scrollview', () => { + const { container } = render(); + const scrollView = container.firstElementChild; + + expect(scrollView).toBeInTheDocument(); + }); + + it('should render custom styles', () => { + const { container } = render( + + ); + const scrollView = container.firstElementChild; + + expect(scrollView).toHaveAttribute( + 'style', + 'background-color: papayawhip;' + ); + }); + + it('should render children', async () => { + const { findByText } = render( + + Cool ScrollView + + ); + const content = await findByText(/cool scrollview/i); + + expect(content).toBeInTheDocument(); + }); + + it('should render custom props', () => { + const customProps = { title: 'scrollview' }; + const { container } = render(); + const scrollView = container.firstElementChild; + + expect(scrollView).toHaveAttribute('title', 'scrollview'); + }); +}); diff --git a/src/Cutout/Cutout.stories.tsx b/src/ScrollView/ScrollView.stories.tsx similarity index 82% rename from src/Cutout/Cutout.stories.tsx rename to src/ScrollView/ScrollView.stories.tsx index 5dc2b909..4d7964b5 100644 --- a/src/Cutout/Cutout.stories.tsx +++ b/src/ScrollView/ScrollView.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { Cutout, Window, WindowContent } from 'react95'; +import { ScrollView, Window, WindowContent } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -9,16 +9,16 @@ const Wrapper = styled.div` `; export default { - title: 'Cutout', - component: Cutout, + title: 'Layout/ScrollView', + component: ScrollView, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { return ( - +

React95 is the best UI library ever created @@ -32,7 +32,7 @@ export function Default() {

React95 is the best UI library ever created

React95 is the best UI library ever created

-
+
); diff --git a/src/Cutout/Cutout.tsx b/src/ScrollView/ScrollView.tsx similarity index 74% rename from src/Cutout/Cutout.tsx rename to src/ScrollView/ScrollView.tsx index 94c0ab7c..632ae1e3 100644 --- a/src/Cutout/Cutout.tsx +++ b/src/ScrollView/ScrollView.tsx @@ -3,13 +3,13 @@ import styled from 'styled-components'; import { insetShadow, createScrollbars } from '../common'; import { CommonStyledProps } from '../types'; -type CutoutProps = { +type ScrollViewProps = { children?: React.ReactNode; shadow?: boolean; } & React.HTMLAttributes & CommonStyledProps; -export const StyledCutout = styled.div>` +export const StyledScrollView = styled.div>` position: relative; box-sizing: border-box; padding: 2px; @@ -50,15 +50,16 @@ const Content = styled.div` ${createScrollbars()} `; -const Cutout = forwardRef(function Cutout( - { children, shadow = true, ...otherProps }, - ref -) { - return ( - - {children} - - ); -}); +const ScrollView = forwardRef( + ({ children, shadow = true, ...otherProps }, ref) => { + return ( + + {children} + + ); + } +); + +ScrollView.displayName = 'ScrollView'; -export { Cutout, CutoutProps }; +export { ScrollView, ScrollViewProps }; diff --git a/src/Select/Select.mdx b/src/Select/Select.mdx deleted file mode 100644 index 41086103..00000000 --- a/src/Select/Select.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: Select -menu: Components ---- - -import { Select } from './Select'; -import Window from '../Window/Window'; -import WindowContent from '../WindowContent/WindowContent'; -import { Cutout } from '../Cutout/Cutout'; - -# Select - -## Usage - -#### Fixed Width - - - {() => { - const items = [ - { value: 1, label: '⚡ Pikachu' }, - { value: 2, label: '🌿 Bulbasaur' }, - { value: 3, label: '💦 Squirtle' }, - { value: 4, label: '🔥 Charizard' }, - { value: 5, label: '🎤 Jigglypuff' }, - { value: 6, label: '🛌🏻 Snorlax' }, - { value: 7, label: '⛰ Geodude' } - ]; - return ( - console.log(value)} - height={100} - width={150} - /> - ); - }} - - -#### No shadow - - - {() => { - const items = [ - { value: 1, label: '⚡ Pikachu' }, - { value: 2, label: '🌿 Bulbasaur' }, - { value: 3, label: '💦 Squirtle' }, - { value: 4, label: '🔥 Charizard' }, - { value: 5, label: '🎤 Jigglypuff' }, - { value: 6, label: '🛌🏻 Snorlax' }, - { value: 7, label: '⛰ Geodude' } - ]; - return ( - console.log(value)} - height={100} - width={150} - /> - - - - - ); - }} - - -## API - -### Import - -``` -import { Select } from 'react95' -``` - -### Props - - diff --git a/src/Select/Select.stories.tsx b/src/Select/Select.stories.tsx index 3184100a..a0621276 100644 --- a/src/Select/Select.stories.tsx +++ b/src/Select/Select.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { Cutout, Fieldset, Select, Window, WindowContent } from 'react95'; +import { GroupBox, ScrollView, Select, Window, WindowContent } from 'react95'; import styled from 'styled-components'; import { SelectChangeEvent, SelectOption } from './Select.types'; @@ -43,7 +43,7 @@ const onChange = (event: SelectChangeEvent, option: SelectOption) => console.log(event, option); export default { - title: 'Select', + title: 'Controls/Select', component: Select, decorators: [story => {story()}] } as ComponentMeta; @@ -51,7 +51,7 @@ export default { export function Default() { return (
-
+ -
+
); } @@ -106,12 +106,12 @@ export function Flat() { return ( - +

When you want to use Select on a light background (like scrollable content), just use the flat variant:

-
+ -
-
+ +
); diff --git a/src/Select/Select.styles.tsx b/src/Select/Select.styles.tsx index 6855df2f..c3e679ea 100644 --- a/src/Select/Select.styles.tsx +++ b/src/Select/Select.styles.tsx @@ -8,7 +8,7 @@ import { shadow as commonShadow } from '../common'; import { blockSizes } from '../common/system'; -import { StyledCutout } from '../Cutout/Cutout'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonThemeProps } from '../types'; import { SelectVariants } from './Select.types'; @@ -66,7 +66,9 @@ const sharedWrapperStyles = css` cursor: ${({ $disabled }) => ($disabled ? 'default' : 'pointer')}; `; -export const StyledSelectWrapper = styled(StyledCutout)` +export const StyledSelectWrapper = styled( + StyledScrollView +)` ${sharedWrapperStyles} background: ${({ $disabled = false, theme }) => $disabled ? theme.material : theme.canvas}; diff --git a/src/Separator/Separator.spec.tsx b/src/Separator/Separator.spec.tsx new file mode 100644 index 00000000..2e8c8e86 --- /dev/null +++ b/src/Separator/Separator.spec.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +import { renderWithTheme } from '../../test/utils'; + +import { Separator } from './Separator'; + +describe('', () => { + it('should render Separator', () => { + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + + expect(separator).toBeInTheDocument(); + }); + + describe('prop: size', () => { + it('defaults to 100%', () => { + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + expect(separator).toHaveStyleRule('width', '100%'); + }); + it('sets size passed correctly', () => { + const size = '53px'; + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('width', size); + }); + }); + + describe('prop: orientation', () => { + it('renders horizontal line by default', () => { + const size = '53px'; + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('width', size); + }); + + it('renders vertical line when orientation="vertical"', () => { + const size = '53px'; + const { container } = renderWithTheme( + + ); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('height', size); + }); + }); + + describe('prop: size', () => { + it('should set proper size', () => { + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('width', '85%'); + }); + + it('when passed a number, sets size in px', () => { + const { container } = renderWithTheme(); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('width', '25px'); + }); + + it('should set height when vertical', () => { + const { container } = renderWithTheme( + + ); + const separator = container.firstElementChild; + + expect(separator).toHaveStyleRule('height', '25px'); + }); + }); +}); diff --git a/src/Separator/Separator.stories.tsx b/src/Separator/Separator.stories.tsx new file mode 100644 index 00000000..030e95f9 --- /dev/null +++ b/src/Separator/Separator.stories.tsx @@ -0,0 +1,41 @@ +import { ComponentMeta } from '@storybook/react'; +import React from 'react'; +import styled from 'styled-components'; + +import { MenuList, MenuListItem, Separator } from 'react95'; + +const Wrapper = styled.div` + padding: 5rem; + background: ${({ theme }) => theme.desktopBackground}; +`; + +export default { + title: 'Layout/Separator', + component: Separator, + decorators: [story => {story()}] +} as ComponentMeta; + +export function Default() { + return ( + <> + + Item 1 + + Item 2 + + Item 3 + + + Item 1 + + Item 2 + + Item 3 + + + ); +} + +Default.story = { + name: 'default' +}; diff --git a/src/Divider/Divider.tsx b/src/Separator/Separator.tsx similarity index 80% rename from src/Divider/Divider.tsx rename to src/Separator/Separator.tsx index 788a8213..1956a098 100644 --- a/src/Divider/Divider.tsx +++ b/src/Separator/Separator.tsx @@ -2,11 +2,12 @@ import styled from 'styled-components'; import { getSize } from '../common/utils'; import { Orientation } from '../types'; -type DividerProps = { +type SeparatorProps = { size?: string | number; orientation?: Orientation; }; -const Divider = styled.div` + +const Separator = styled.div` ${({ orientation, theme, size = '100%' }) => orientation === 'vertical' ? ` @@ -23,4 +24,6 @@ const Divider = styled.div` `} `; -export { Divider, DividerProps }; +Separator.displayName = 'Separator'; + +export { Separator, SeparatorProps }; diff --git a/src/Slider/Slider.stories.tsx b/src/Slider/Slider.stories.tsx index 6f1f453d..b7b96466 100644 --- a/src/Slider/Slider.stories.tsx +++ b/src/Slider/Slider.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; -import { Cutout, Slider } from 'react95'; +import { ScrollView, Slider } from 'react95'; import styled from 'styled-components'; const Wrapper = styled.div` @@ -35,7 +35,7 @@ const Wrapper = styled.div` `; export default { - title: 'Slider', + title: 'Controls/Slider', component: Slider, decorators: [story => {story()}] } as ComponentMeta; @@ -116,7 +116,7 @@ Default.story = { export function Flat() { return ( - +

When you want to add input field on a light background (like scrollable content), just use the flat variant: @@ -146,7 +146,7 @@ export function Flat() { { value: 6, label: '6°C' } ]} /> - + ); } diff --git a/src/Slider/Slider.tsx b/src/Slider/Slider.tsx index 1f37168c..574229ad 100644 --- a/src/Slider/Slider.tsx +++ b/src/Slider/Slider.tsx @@ -20,7 +20,7 @@ import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontro import useForkRef from '../common/hooks/useForkRef'; import { useIsFocusVisible } from '../common/hooks/useIsFocusVisible'; import { clamp, getSize, roundValueToStep } from '../common/utils'; -import { StyledCutout } from '../Cutout/Cutout'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonStyledProps } from '../types'; type SliderProps = { @@ -187,10 +187,10 @@ const sharedGrooveStyles = () => css` width: 100%; `} `; -const StyledGroove = styled(StyledCutout)` +const StyledGroove = styled(StyledScrollView)` ${sharedGrooveStyles()} `; -const StyledFlatGroove = styled(StyledCutout)` +const StyledFlatGroove = styled(StyledScrollView)` ${sharedGrooveStyles()} border-left-color: ${({ theme }) => theme.flatLight}; @@ -290,352 +290,356 @@ const Mark = styled.div` `} `; -const Slider = forwardRef(function Slider( - { - defaultValue, - disabled = false, - marks: marksProp = false, - max = 100, - min = 0, - name, - onChange, - onChangeCommitted, - onMouseDown, - orientation = 'horizontal', - size = '100%', - step = 1, - value, - variant = 'default', - ...otherProps - }, - ref -) { - const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove; - const vertical = orientation === 'vertical'; - const [valueDerived, setValueState] = useControlledOrUncontrolled({ - value, - defaultValue: defaultValue ?? min - }); - - const { - isFocusVisible, - onBlurVisible, - ref: focusVisibleRef - } = useIsFocusVisible(); - const [focusVisible, setFocusVisible] = useState(false); - const sliderRef = useRef(); - const thumbRef = useRef(null); - const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); - const handleRef = useForkRef(ref, handleFocusRef); - - const handleFocus = useCallback( - (event: React.FocusEvent) => { - if (isFocusVisible(event)) { - setFocusVisible(true); - } - }, - [isFocusVisible] - ); - - const handleBlur = useCallback(() => { - if (focusVisible !== false) { - setFocusVisible(false); - onBlurVisible(); - } - }, [focusVisible, onBlurVisible]); - - const touchId = useRef(); - - const marks = useMemo( - () => - marksProp === true && Number.isFinite(step) - ? [...Array(Math.round((max - min) / (step as number)) + 1)].map( - (_, index) => ({ - label: undefined, - value: min + (step as number) * index - }) - ) - : Array.isArray(marksProp) - ? marksProp - : [], - [marksProp, max, min, step] - ); - - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - const tenPercents = (max - min) / 10; - const marksValues = marks.map(mark => mark.value); - const marksIndex = marksValues.indexOf(valueDerived); - let newValue = 0; - - switch (event.key) { - case 'Home': - newValue = min; - break; - case 'End': - newValue = max; - break; - case 'PageUp': - if (step) { - newValue = valueDerived + tenPercents; - } - break; - case 'PageDown': - if (step) { - newValue = valueDerived - tenPercents; - } - break; - case 'ArrowRight': - case 'ArrowUp': - if (step) { - newValue = valueDerived + step; - } else { - newValue = - marksValues[marksIndex + 1] || - marksValues[marksValues.length - 1]; - } - break; - case 'ArrowLeft': - case 'ArrowDown': - if (step) { - newValue = valueDerived - step; - } else { - newValue = marksValues[marksIndex - 1] || marksValues[0]; - } - break; - default: - return; - } - - // Prevent scroll of the page - event.preventDefault(); - if (step) { - newValue = roundValueToStep(newValue, step, min); - } - - newValue = clamp(newValue, min, max); - - setValueState(newValue); - setFocusVisible(true); - - onChange?.(event, newValue); - onChangeCommitted?.(event, newValue); - }, - [ - marks, - max, - min, +const Slider = forwardRef( + ( + { + defaultValue, + disabled = false, + marks: marksProp = false, + max = 100, + min = 0, + name, onChange, onChangeCommitted, - setValueState, - step, - valueDerived - ] - ); - - const getNewValue = useCallback( - (finger: { x: number; y: number }) => { - if (!sliderRef.current) { - return 0; - } - const rect = sliderRef.current.getBoundingClientRect(); - - let percent; - if (vertical) { - percent = (rect.bottom - finger.y) / rect.height; - } else { - percent = (finger.x - rect.left) / rect.width; - } - let newValue; - - newValue = percentToValue(percent, min, max); - if (step) { - newValue = roundValueToStep(newValue, step, min); - } else { - const marksValues = marks.map(mark => mark.value); - const closestIndex = findClosest(marksValues, newValue); - newValue = marksValues[closestIndex]; - } - newValue = clamp(newValue, min, max); - return newValue; + onMouseDown, + orientation = 'horizontal', + size = '100%', + step = 1, + value, + variant = 'default', + ...otherProps }, - [marks, max, min, step, vertical] - ); - - const handleTouchMove = useCallback( - (event: MouseEvent | TouchEvent) => { - const finger = trackFinger(event, touchId.current); - - if (!finger) { - return; + ref + ) => { + const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove; + const vertical = orientation === 'vertical'; + const [valueDerived, setValueState] = useControlledOrUncontrolled({ + value, + defaultValue: defaultValue ?? min + }); + + const { + isFocusVisible, + onBlurVisible, + ref: focusVisibleRef + } = useIsFocusVisible(); + const [focusVisible, setFocusVisible] = useState(false); + const sliderRef = useRef(); + const thumbRef = useRef(null); + const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); + const handleRef = useForkRef(ref, handleFocusRef); + + const handleFocus = useCallback( + (event: React.FocusEvent) => { + if (isFocusVisible(event)) { + setFocusVisible(true); + } + }, + [isFocusVisible] + ); + + const handleBlur = useCallback(() => { + if (focusVisible !== false) { + setFocusVisible(false); + onBlurVisible(); } - const newValue = getNewValue(finger); - - thumbRef.current?.focus(); - setValueState(newValue); - setFocusVisible(true); + }, [focusVisible, onBlurVisible]); + + const touchId = useRef(); + + const marks = useMemo( + () => + marksProp === true && Number.isFinite(step) + ? [...Array(Math.round((max - min) / (step as number)) + 1)].map( + (_, index) => ({ + label: undefined, + value: min + (step as number) * index + }) + ) + : Array.isArray(marksProp) + ? marksProp + : [], + [marksProp, max, min, step] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + const tenPercents = (max - min) / 10; + const marksValues = marks.map(mark => mark.value); + const marksIndex = marksValues.indexOf(valueDerived); + let newValue = 0; + + switch (event.key) { + case 'Home': + newValue = min; + break; + case 'End': + newValue = max; + break; + case 'PageUp': + if (step) { + newValue = valueDerived + tenPercents; + } + break; + case 'PageDown': + if (step) { + newValue = valueDerived - tenPercents; + } + break; + case 'ArrowRight': + case 'ArrowUp': + if (step) { + newValue = valueDerived + step; + } else { + newValue = + marksValues[marksIndex + 1] || + marksValues[marksValues.length - 1]; + } + break; + case 'ArrowLeft': + case 'ArrowDown': + if (step) { + newValue = valueDerived - step; + } else { + newValue = marksValues[marksIndex - 1] || marksValues[0]; + } + break; + default: + return; + } + + // Prevent scroll of the page + event.preventDefault(); + if (step) { + newValue = roundValueToStep(newValue, step, min); + } + + newValue = clamp(newValue, min, max); - onChange?.(event, newValue); - }, - [getNewValue, onChange, setValueState] - ); + setValueState(newValue); + setFocusVisible(true); - const handleTouchEnd = useCallback( - (event: MouseEvent | TouchEvent) => { - const finger = trackFinger(event, touchId.current); + onChange?.(event, newValue); + onChangeCommitted?.(event, newValue); + }, + [ + marks, + max, + min, + onChange, + onChangeCommitted, + setValueState, + step, + valueDerived + ] + ); + + const getNewValue = useCallback( + (finger: { x: number; y: number }) => { + if (!sliderRef.current) { + return 0; + } + const rect = sliderRef.current.getBoundingClientRect(); + + let percent; + if (vertical) { + percent = (rect.bottom - finger.y) / rect.height; + } else { + percent = (finger.x - rect.left) / rect.width; + } + let newValue; + + newValue = percentToValue(percent, min, max); + if (step) { + newValue = roundValueToStep(newValue, step, min); + } else { + const marksValues = marks.map(mark => mark.value); + const closestIndex = findClosest(marksValues, newValue); + newValue = marksValues[closestIndex]; + } + newValue = clamp(newValue, min, max); + return newValue; + }, + [marks, max, min, step, vertical] + ); + + const handleTouchMove = useCallback( + (event: MouseEvent | TouchEvent) => { + const finger = trackFinger(event, touchId.current); + + if (!finger) { + return; + } + const newValue = getNewValue(finger); - if (!finger) { - return; - } + thumbRef.current?.focus(); + setValueState(newValue); + setFocusVisible(true); - const newValue = getNewValue(finger); + onChange?.(event, newValue); + }, + [getNewValue, onChange, setValueState] + ); - onChangeCommitted?.(event, newValue); + const handleTouchEnd = useCallback( + (event: MouseEvent | TouchEvent) => { + const finger = trackFinger(event, touchId.current); - touchId.current = undefined; + if (!finger) { + return; + } - const doc = ownerDocument(sliderRef.current); - doc.removeEventListener('mousemove', handleTouchMove); - doc.removeEventListener('mouseup', handleTouchEnd); - doc.removeEventListener('touchmove', handleTouchMove); - doc.removeEventListener('touchend', handleTouchEnd); - }, - [getNewValue, handleTouchMove, onChangeCommitted] - ); + const newValue = getNewValue(finger); - const handleMouseDown = useCallback( - (event: React.MouseEvent) => { - // TODO should we also pass event together with new value to callbacks? (same thing with other input components) - onMouseDown?.(event); + onChangeCommitted?.(event, newValue); - event.preventDefault(); - thumbRef.current?.focus(); - setFocusVisible(true); + touchId.current = undefined; - const finger = trackFinger(event, touchId.current); - if (finger) { - const newValue = getNewValue(finger); - setValueState(newValue); - onChange?.(event, newValue); - } + const doc = ownerDocument(sliderRef.current); + doc.removeEventListener('mousemove', handleTouchMove); + doc.removeEventListener('mouseup', handleTouchEnd); + doc.removeEventListener('touchmove', handleTouchMove); + doc.removeEventListener('touchend', handleTouchEnd); + }, + [getNewValue, handleTouchMove, onChangeCommitted] + ); - const doc = ownerDocument(sliderRef.current); - doc.addEventListener('mousemove', handleTouchMove); - doc.addEventListener('mouseup', handleTouchEnd); - }, - [ - getNewValue, - handleTouchEnd, - handleTouchMove, - onChange, - onMouseDown, - setValueState - ] - ); - - const handleTouchStart = useCallback( - (event: TouchEvent) => { - // Workaround as Safari has partial support for touchAction: 'none'. - event.preventDefault(); - const touch = event.changedTouches[0]; - if (touch != null) { - // A number that uniquely identifies the current finger in the touch session. - touchId.current = touch.identifier; - } + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + // TODO should we also pass event together with new value to callbacks? (same thing with other input components) + onMouseDown?.(event); - thumbRef.current?.focus(); - setFocusVisible(true); + event.preventDefault(); + thumbRef.current?.focus(); + setFocusVisible(true); - const finger = trackFinger(event, touchId.current); - if (finger) { - const newValue = getNewValue(finger); - setValueState(newValue); - onChange?.(event, newValue); - } + const finger = trackFinger(event, touchId.current); + if (finger) { + const newValue = getNewValue(finger); + setValueState(newValue); + onChange?.(event, newValue); + } + + const doc = ownerDocument(sliderRef.current); + doc.addEventListener('mousemove', handleTouchMove); + doc.addEventListener('mouseup', handleTouchEnd); + }, + [ + getNewValue, + handleTouchEnd, + handleTouchMove, + onChange, + onMouseDown, + setValueState + ] + ); + + const handleTouchStart = useCallback( + (event: TouchEvent) => { + // Workaround as Safari has partial support for touchAction: 'none'. + event.preventDefault(); + const touch = event.changedTouches[0]; + if (touch != null) { + // A number that uniquely identifies the current finger in the touch session. + touchId.current = touch.identifier; + } + + thumbRef.current?.focus(); + setFocusVisible(true); - const doc = ownerDocument(sliderRef.current); - doc.addEventListener('touchmove', handleTouchMove); - doc.addEventListener('touchend', handleTouchEnd); - }, - [getNewValue, handleTouchEnd, handleTouchMove, onChange, setValueState] - ); - - useEffect(() => { - const { current: slider } = sliderRef; - slider?.addEventListener('touchstart', handleTouchStart); - const doc = ownerDocument(slider); - - return () => { - slider?.removeEventListener('touchstart', handleTouchStart); - doc.removeEventListener('mousemove', handleTouchMove); - doc.removeEventListener('mouseup', handleTouchEnd); - doc.removeEventListener('touchmove', handleTouchMove); - doc.removeEventListener('touchend', handleTouchEnd); - }; - }, [handleTouchEnd, handleTouchMove, handleTouchStart]); - - return ( - - {/* should we keep the hidden input ? */} - - {marks && - marks.map(m => ( - - {m.label && ( - - {m.label} - - )} - - ))} - - { + const { current: slider } = sliderRef; + slider?.addEventListener('touchstart', handleTouchStart); + const doc = ownerDocument(slider); + + return () => { + slider?.removeEventListener('touchstart', handleTouchStart); + doc.removeEventListener('mousemove', handleTouchMove); + doc.removeEventListener('mouseup', handleTouchEnd); + doc.removeEventListener('touchmove', handleTouchMove); + doc.removeEventListener('touchend', handleTouchEnd); + }; + }, [handleTouchEnd, handleTouchMove, handleTouchStart]); + + return ( + - - ); -}); + ref={handleRef} + size={getSize(size)} + {...otherProps} + > + {/* should we keep the hidden input ? */} + + {marks && + marks.map(m => ( + + {m.label && ( + + {m.label} + + )} + + ))} + + + + ); + } +); + +Slider.displayName = 'Slider'; export { Slider, SliderProps }; diff --git a/src/Table/Table.mdx b/src/Table/Table.mdx deleted file mode 100644 index 66276c24..00000000 --- a/src/Table/Table.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: Table -menu: Components ---- - -import { Table } from './Table'; -import { TableBody } from '../TableBody/TableBody'; -import { TableHead } from '../TableHead/TableHead'; -import { TableRow } from '../TableRow/TableRow'; -import { TableHeadCell } from '../TableHeadCell/TableHeadCell'; -import { TableDataCell } from '../TableDataCell/TableDataCell'; -import Window from '../Window/Window'; -import WindowHeader from '../WindowHeader/WindowHeader'; -import WindowContent from '../WindowContent/WindowContent'; - -# Table - -## Usage - - - - Pokedex.exe - - - - - Type - Name - Level - - - - - - - 🌿 - - - Bulbasaur - 64 - - - - - 🔥 - - - Charizard - 209 - - - - - ⚡ - - - Pikachu - 82 - - -
-
-
-
- -## API - -### Import - -``` -import { Table } from 'react95' -``` - -### Props - - diff --git a/src/Table/Table.stories.tsx b/src/Table/Table.stories.tsx index 98713dfc..0b524b50 100644 --- a/src/Table/Table.stories.tsx +++ b/src/Table/Table.stories.tsx @@ -19,7 +19,7 @@ const Wrapper = styled.div` `; export default { - title: 'Table', + title: 'Controls/Table', component: Table, subcomponents: { Table, diff --git a/src/Table/Table.tsx b/src/Table/Table.tsx index 304cb0e2..9f82c79b 100644 --- a/src/Table/Table.tsx +++ b/src/Table/Table.tsx @@ -1,6 +1,6 @@ import React, { forwardRef } from 'react'; import styled from 'styled-components'; -import { StyledCutout } from '../Cutout/Cutout'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonStyledProps } from '../types'; type TableProps = { @@ -16,23 +16,30 @@ const StyledTable = styled.table` font-size: 1rem; `; -const Wrapper = styled(StyledCutout)` +const Wrapper = styled(StyledScrollView)` &:before { box-shadow: none; } `; -const Table = forwardRef(function Table( - { children, ...otherProps }, - ref -) { - return ( - - - {children} - - - ); -}); +const Table = forwardRef( + ({ children, ...otherProps }, ref) => { + return ( + + + {children} + + + ); + } +); + +Table.displayName = 'Table'; + +export * from './TableBody'; +export * from './TableDataCell'; +export * from './TableHead'; +export * from './TableHeadCell'; +export * from './TableRow'; export { Table, TableProps }; diff --git a/src/TableBody/TableBody.spec.tsx b/src/Table/TableBody.spec.tsx similarity index 100% rename from src/TableBody/TableBody.spec.tsx rename to src/Table/TableBody.spec.tsx diff --git a/src/TableBody/TableBody.tsx b/src/Table/TableBody.tsx similarity index 95% rename from src/TableBody/TableBody.tsx rename to src/Table/TableBody.tsx index 0c5ef687..542de3a0 100644 --- a/src/TableBody/TableBody.tsx +++ b/src/Table/TableBody.tsx @@ -25,4 +25,6 @@ const TableBody = forwardRef( } ); +TableBody.displayName = 'TableBody'; + export { TableBody, TableBodyProps }; diff --git a/src/TableDataCell/TableDataCell.spec.tsx b/src/Table/TableDataCell.spec.tsx similarity index 100% rename from src/TableDataCell/TableDataCell.spec.tsx rename to src/Table/TableDataCell.spec.tsx diff --git a/src/TableDataCell/TableDataCell.tsx b/src/Table/TableDataCell.tsx similarity index 92% rename from src/TableDataCell/TableDataCell.tsx rename to src/Table/TableDataCell.tsx index 764445fe..f97f8b76 100644 --- a/src/TableDataCell/TableDataCell.tsx +++ b/src/Table/TableDataCell.tsx @@ -21,4 +21,6 @@ const TableDataCell = forwardRef( } ); +TableDataCell.displayName = 'TableDataCell'; + export { TableDataCell, TableDataCellProps }; diff --git a/src/TableHead/TableHead.spec.tsx b/src/Table/TableHead.spec.tsx similarity index 100% rename from src/TableHead/TableHead.spec.tsx rename to src/Table/TableHead.spec.tsx diff --git a/src/TableHead/TableHead.tsx b/src/Table/TableHead.tsx similarity index 94% rename from src/TableHead/TableHead.tsx rename to src/Table/TableHead.tsx index 14afd6e3..368c4da5 100644 --- a/src/TableHead/TableHead.tsx +++ b/src/Table/TableHead.tsx @@ -20,4 +20,6 @@ const TableHead = forwardRef( } ); +TableHead.displayName = 'TableHead'; + export { TableHead, TableHeadProps }; diff --git a/src/TableHeadCell/TableHeadCell.spec.tsx b/src/Table/TableHeadCell.spec.tsx similarity index 100% rename from src/TableHeadCell/TableHeadCell.spec.tsx rename to src/Table/TableHeadCell.spec.tsx diff --git a/src/TableHeadCell/TableHeadCell.tsx b/src/Table/TableHeadCell.tsx similarity index 95% rename from src/TableHeadCell/TableHeadCell.tsx rename to src/Table/TableHeadCell.tsx index dd4c359d..e653d6b6 100644 --- a/src/TableHeadCell/TableHeadCell.tsx +++ b/src/Table/TableHeadCell.tsx @@ -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; @@ -88,4 +88,6 @@ const TableHeadCell = forwardRef( } ); +TableHeadCell.displayName = 'TableHeadCell'; + export { TableHeadCell, TableHeadCellProps }; diff --git a/src/TableRow/TableRow.spec.tsx b/src/Table/TableRow.spec.tsx similarity index 100% rename from src/TableRow/TableRow.spec.tsx rename to src/Table/TableRow.spec.tsx diff --git a/src/TableRow/TableRow.tsx b/src/Table/TableRow.tsx similarity index 96% rename from src/TableRow/TableRow.tsx rename to src/Table/TableRow.tsx index 3c30d8ca..b601c093 100644 --- a/src/TableRow/TableRow.tsx +++ b/src/Table/TableRow.tsx @@ -31,4 +31,6 @@ const TableRow = forwardRef( } ); +TableRow.displayName = 'TableRow'; + export { TableRow, TableRowProps }; diff --git a/src/Tab/Tab.spec.tsx b/src/Tabs/Tab.spec.tsx similarity index 100% rename from src/Tab/Tab.spec.tsx rename to src/Tabs/Tab.spec.tsx diff --git a/src/Tab/Tab.tsx b/src/Tabs/Tab.tsx similarity index 79% rename from src/Tab/Tab.tsx rename to src/Tabs/Tab.tsx index 935d7232..17f0666f 100644 --- a/src/Tab/Tab.tsx +++ b/src/Tabs/Tab.tsx @@ -69,22 +69,25 @@ const StyledTab = styled.button` } `; -const Tab = forwardRef(function Tab( - { value, onClick, selected = false, children, ...otherProps }, - ref -) { - return ( - ) => onClick?.(e, value)} - ref={ref} - role='tab' - {...otherProps} - > - {children} - - ); -}); +const Tab = forwardRef( + ({ value, onClick, selected = false, children, ...otherProps }, ref) => { + return ( + ) => + onClick?.(e, value) + } + ref={ref} + role='tab' + {...otherProps} + > + {children} + + ); + } +); + +Tab.displayName = 'Tab'; export { Tab, TabProps }; diff --git a/src/TabBody/TabBody.spec.tsx b/src/Tabs/TabBody.spec.tsx similarity index 100% rename from src/TabBody/TabBody.spec.tsx rename to src/Tabs/TabBody.spec.tsx diff --git a/src/TabBody/TabBody.tsx b/src/Tabs/TabBody.tsx similarity index 67% rename from src/TabBody/TabBody.tsx rename to src/Tabs/TabBody.tsx index c27da639..1afb4f54 100644 --- a/src/TabBody/TabBody.tsx +++ b/src/Tabs/TabBody.tsx @@ -18,15 +18,16 @@ const StyledTabBody = styled.div` padding: 16px; font-size: 1rem; `; -const TabBody = forwardRef(function TabBody( - { children, ...otherProps }, - ref -) { - return ( - - {children} - - ); -}); +const TabBody = forwardRef( + ({ children, ...otherProps }, ref) => { + return ( + + {children} + + ); + } +); + +TabBody.displayName = 'TabBody'; export { TabBody, TabBodyProps }; diff --git a/src/Tabs/Tabs.mdx b/src/Tabs/Tabs.mdx deleted file mode 100644 index c39489aa..00000000 --- a/src/Tabs/Tabs.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -name: Tabs -menu: Components ---- - -import { Tabs } from './Tabs'; -import { Tab } from '../Tab/Tab'; -import { TabBody } from '../TabBody/TabBody'; -import Fieldset from '../Fieldset/Fieldset'; -import Window from '../Window/Window'; -import WindowHeader from '../WindowHeader/WindowHeader'; -import { NumberField } from '../NumberField/NumberField'; -import Checkbox from '../Checkbox/Checkbox'; -import WindowContent from '../WindowContent/WindowContent'; - -# Tabs - -## Usage - - - {() => { - const [activeTab, setActiveTab] = React.useState(0); - const handleChange = (event, value) => { - setActiveTab(value); - }; - const a11yProps = index => ({ - id: `tab-${index}`, - 'aria-controls': `tabpanel-${index}` - }); - const TabPanel = ({ children, value, activeTab, ...other }) => { - return ( -

- ); - }; - return ( - - - - 👗 - - store.exe - - - - - Shoes - - - Accesories - - - Clothing - - - - -
-
Amount:
- null} - /> - null} - defaultChecked - /> -
-
- -
Accesories stuff here
-
- -
Clothing stuff here
-
-
-
-
- ); - }} - - -## API - -### Import - -``` -import { Tabs } from 'react95' -``` - -### Props - - diff --git a/src/Tabs/Tabs.stories.tsx b/src/Tabs/Tabs.stories.tsx index 5ba196d3..26a47afb 100644 --- a/src/Tabs/Tabs.stories.tsx +++ b/src/Tabs/Tabs.stories.tsx @@ -3,8 +3,8 @@ import React, { useState } from 'react'; import { Anchor, Checkbox, - Fieldset, - NumberField, + GroupBox, + NumberInput, Tab, TabBody, Tabs, @@ -20,7 +20,7 @@ const Wrapper = styled.div` `; export default { - title: 'Tabs', + title: 'Controls/Tabs', component: Tabs, subcomponents: { Tab, TabBody }, decorators: [story => {story()}] @@ -49,9 +49,9 @@ export function Default() { {activeTab === 0 && (
-
+
Amount:
- +
null} defaultChecked /> -
+
)} {activeTab === 1 && ( diff --git a/src/Tabs/Tabs.tsx b/src/Tabs/Tabs.tsx index bc69f111..b0c95582 100644 --- a/src/Tabs/Tabs.tsx +++ b/src/Tabs/Tabs.tsx @@ -2,8 +2,8 @@ import React, { forwardRef, useMemo } from 'react'; import styled from 'styled-components'; import { noOp } from '../common/utils'; -import { TabProps } from '../Tab/Tab'; import { CommonStyledProps } from '../types'; +import { TabProps } from './Tab'; type TabsProps = { value?: TabProps['value']; @@ -56,45 +56,55 @@ function splitToChunks(array: T[], parts: number) { return result; } -const Tabs = forwardRef(function Tabs( - { value, onChange = noOp, children, rows = 1, ...otherProps }, - ref -) { - // split tabs into equal rows and assign key to each row - const tabRowsToRender = useMemo(() => { - const childrenWithProps = - React.Children.map(children, child => { - if (!React.isValidElement(child)) { - return null; - } - const tabProps = { - selected: child.props.value === value, - onClick: onChange - }; - return React.cloneElement(child, tabProps); - }) ?? []; +const Tabs = forwardRef( + ({ value, onChange = noOp, children, rows = 1, ...otherProps }, ref) => { + // split tabs into equal rows and assign key to each row + const tabRowsToRender = useMemo(() => { + const childrenWithProps = + React.Children.map(children, child => { + if (!React.isValidElement(child)) { + return null; + } + const tabProps = { + selected: child.props.value === value, + onClick: onChange + }; + return React.cloneElement(child, tabProps); + }) ?? []; - const tabRows = splitToChunks(childrenWithProps, rows).map((tabs, i) => ({ - key: i, - tabs - })); + const tabRows = splitToChunks(childrenWithProps, rows).map((tabs, i) => ({ + key: i, + tabs + })); - // move row containing currently selected tab to the bottom - const currentlySelectedRowIndex = tabRows.findIndex(tabRow => - tabRow.tabs.some(tab => tab.props.selected) + // move row containing currently selected tab to the bottom + const currentlySelectedRowIndex = tabRows.findIndex(tabRow => + tabRow.tabs.some(tab => tab.props.selected) + ); + tabRows.push(tabRows.splice(currentlySelectedRowIndex, 1)[0]); + + return tabRows; + }, [children, onChange, rows, value]); + + return ( + 1} + role='tablist' + ref={ref} + > + {tabRowsToRender.map(row => ( + {row.tabs} + ))} + ); - tabRows.push(tabRows.splice(currentlySelectedRowIndex, 1)[0]); + } +); + +Tabs.displayName = 'Tabs'; - return tabRows; - }, [children, onChange, rows, value]); +export * from './Tab'; - return ( - 1} role='tablist' ref={ref}> - {tabRowsToRender.map(row => ( - {row.tabs} - ))} - - ); -}); +export * from './TabBody'; export { Tabs, TabsProps }; diff --git a/src/TextField/TextField.mdx b/src/TextField/TextField.mdx deleted file mode 100644 index 0ea168dd..00000000 --- a/src/TextField/TextField.mdx +++ /dev/null @@ -1,112 +0,0 @@ ---- -name: TextField -menu: Components ---- - -import { TextField } from './TextField'; -import { Cutout } from '../Cutout/Cutout'; - -# TextField - -## Usage - -#### Default - - - console.log(e.target.value)} /> - - -#### No shadow - - - console.log(e.target.value)} - /> - - -#### Disabled - - - console.log(e.target.value)} - disabled - /> - - -#### Custom width - - - console.log(e.target.value)} - /> - - -#### Flat - - - -

- When you want to add input field on a light background (like scrollable - content), just use the flat variant: -

-
- - console.log(e.target.value)} - /> -
-
-
- -#### Flat disabled - - - -

- When you want to add input field on a light background (like scrollable - content), just use the flat variant: -

-
- - console.log(e.target.value)} - disabled - /> -
-
-
- -## API - -### Import - -``` -import { TextField } from 'react95' -``` - -### Props - - diff --git a/src/TextField/TextField.stories.tsx b/src/TextInput/TextInout.stories.tsx similarity index 86% rename from src/TextField/TextField.stories.tsx rename to src/TextInput/TextInout.stories.tsx index f9134231..c7638eb1 100644 --- a/src/TextField/TextField.stories.tsx +++ b/src/TextInput/TextInout.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta } from '@storybook/react'; import React, { useState } from 'react'; -import { Button, Cutout, TextField } from 'react95'; +import { Button, ScrollView, TextInput } from 'react95'; import styled from 'styled-components'; const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sollicitudin, ante vel porttitor posuere, tellus nisi interdum ipsum, non bibendum ante risus ut purus. Curabitur vel posuere odio. Vivamus rutrum, nunc et ullamcorper sagittis, tellus ligula maximus quam, id dapibus sapien metus lobortis diam. Proin luctus, dolor in finibus feugiat, lacus enim gravida sem, quis aliquet tellus leo nec enim. Morbi varius bibendum augue quis venenatis. Curabitur ut elit augue. Pellentesque posuere enim a mattis interdum. Donec sodales convallis turpis, a vulputate elit. Suspendisse potenti.`; @@ -19,10 +19,10 @@ const Wrapper = styled.div` `; export default { - title: 'TextField', - component: TextField, + title: 'Controls/TextInput', + component: TextInput, decorators: [story => {story()}] -} as ComponentMeta; +} as ComponentMeta; export function Default() { const [state, setState] = useState({ @@ -36,7 +36,7 @@ export function Default() { return (
-

- +
- +
- +

When you want to add input field on a light background (like scrollable content), just use the flat variant:


-
-
-
- - +
); } diff --git a/src/TextField/TextField.spec.tsx b/src/TextInput/TextInput.spec.tsx similarity index 81% rename from src/TextField/TextField.spec.tsx rename to src/TextInput/TextInput.spec.tsx index be6cf9ab..ad647f81 100644 --- a/src/TextField/TextField.spec.tsx +++ b/src/TextInput/TextInput.spec.tsx @@ -4,11 +4,11 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { renderWithTheme } from '../../test/utils'; -import { TextField } from './TextField'; +import { TextInput } from './TextInput'; -describe('', () => { +describe('', () => { it('should render an inside the div', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const input = container.querySelector('input'); expect(input).toHaveAttribute('type', 'text'); expect(input).not.toHaveAttribute('required'); @@ -21,7 +21,7 @@ describe('', () => { const handleKeyUp = jest.fn(); const handleKeyDown = jest.fn(); const { getByRole } = renderWithTheme( - ', () => { }); it('should considered [] as controlled', () => { - const { getByRole } = renderWithTheme(); + const { getByRole } = renderWithTheme(); const input = getByRole('textbox'); expect(input).toHaveProperty('value', ''); @@ -60,20 +60,20 @@ describe('', () => { it('should forwardRef to native input', () => { const inputRef = React.createRef(); - const { getByRole } = renderWithTheme(); + const { getByRole } = renderWithTheme(); const input = getByRole('textbox'); expect(inputRef.current).toBe(input); }); describe('multiline', () => { it('should render textarea when passed the multiline prop', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const textarea = container.querySelector('textarea'); expect(textarea).not.toBe(null); }); it('should forward rows prop', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const textarea = container.querySelector('textarea'); expect(textarea).toHaveAttribute('rows', '3'); }); @@ -81,13 +81,13 @@ describe('', () => { describe('prop: disabled', () => { it('should render a disabled ', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); const input = container.querySelector('input'); expect(input).toHaveAttribute('disabled'); }); it('should be overridden by props', () => { - const { getByRole, rerender } = renderWithTheme(); - rerender(); + const { getByRole, rerender } = renderWithTheme(); + rerender(); const input = getByRole('textbox'); expect(input).not.toHaveAttribute('disabled'); }); @@ -95,18 +95,18 @@ describe('', () => { describe('prop: variant', () => { it('should be "default" by default', () => { - const { getByTestId } = renderWithTheme(); + const { getByTestId } = renderWithTheme(); expect(getByTestId('variant-default')).toBeInTheDocument(); }); it('should handle "flat" variant', () => { - const { getByTestId } = renderWithTheme(); + const { getByTestId } = renderWithTheme(); expect(getByTestId('variant-flat')).toBeInTheDocument(); }); }); describe('prop: fullWidth', () => { it('should make component take 100% width', () => { - const { container } = renderWithTheme(); + const { container } = renderWithTheme(); expect( window.getComputedStyle(container.firstChild as HTMLInputElement).width ).toBe('100%'); diff --git a/src/TextField/TextField.tsx b/src/TextInput/TextInput.tsx similarity index 57% rename from src/TextField/TextField.tsx rename to src/TextInput/TextInput.tsx index 8014c57f..11f92999 100644 --- a/src/TextField/TextField.tsx +++ b/src/TextInput/TextInput.tsx @@ -7,10 +7,10 @@ import { } from '../common'; import { blockSizes } from '../common/system'; import { noOp } from '../common/utils'; -import { StyledCutout } from '../Cutout/Cutout'; +import { StyledScrollView } from '../ScrollView/ScrollView'; import { CommonStyledProps, CommonThemeProps } from '../types'; -type TextFieldInputProps = { +type TextInputInputProps = { multiline?: false | undefined; onChange?: React.ChangeEventHandler; /** @default text */ @@ -20,7 +20,7 @@ type TextFieldInputProps = { 'className' | 'disabled' | 'style' | 'type' >; -type TextFieldTextAreaProps = { +type TextInputTextAreaProps = { multiline: true; onChange?: React.ChangeEventHandler; } & Omit< @@ -28,7 +28,7 @@ type TextFieldTextAreaProps = { 'className' | 'disabled' | 'style' | 'type' >; -type TextFieldProps = { +type TextInputProps = { className?: string; disabled?: boolean; fullWidth?: boolean; @@ -36,10 +36,10 @@ type TextFieldProps = { shadow?: boolean; style?: React.CSSProperties; variant?: 'default' | 'flat'; -} & (TextFieldInputProps | TextFieldTextAreaProps) & +} & (TextInputInputProps | TextInputTextAreaProps) & CommonStyledProps; -type WrapperProps = Pick & +type WrapperProps = Pick & CommonThemeProps; const sharedWrapperStyles = css` @@ -49,7 +49,7 @@ const sharedWrapperStyles = css` min-height: ${blockSizes.md}; `; -const Wrapper = styled(StyledCutout).attrs({ +const Wrapper = styled(StyledScrollView).attrs({ 'data-testid': 'variant-default' })` ${sharedWrapperStyles} @@ -65,7 +65,7 @@ const FlatWrapper = styled.div.attrs({ position: relative; `; -type InputProps = Pick; +type InputProps = Pick; const sharedInputStyles = css` display: block; @@ -95,60 +95,64 @@ const StyledTextArea = styled.textarea` ${({ variant }) => createScrollbars(variant)} `; -const TextField = forwardRef< +const TextInput = forwardRef< HTMLInputElement | HTMLTextAreaElement, - TextFieldProps ->(function TextField( - { - className, - disabled = false, - fullWidth, - onChange = noOp, - shadow = true, - style, - variant = 'default', - ...otherProps - }, - ref -) { - const WrapperComponent = variant === 'flat' ? FlatWrapper : Wrapper; - - const field = useMemo( - () => - otherProps.multiline ? ( - - ) : ( - - ), - [disabled, onChange, otherProps, ref, variant] - ); - - return ( - - {field} - - ); -}); - -export { TextField, TextFieldProps }; + TextInputProps +>( + ( + { + className, + disabled = false, + fullWidth, + onChange = noOp, + shadow = true, + style, + variant = 'default', + ...otherProps + }, + ref + ) => { + const WrapperComponent = variant === 'flat' ? FlatWrapper : Wrapper; + + const field = useMemo( + () => + otherProps.multiline ? ( + + ) : ( + + ), + [disabled, onChange, otherProps, ref, variant] + ); + + return ( + + {field} + + ); + } +); + +TextInput.displayName = 'TextInput'; + +export { TextInput, TextInputProps }; diff --git a/src/Tooltip/Tooltip.mdx b/src/Tooltip/Tooltip.mdx deleted file mode 100644 index 2f18c020..00000000 --- a/src/Tooltip/Tooltip.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Tooltip -menu: Components ---- - -import { Tooltip } from './Tooltip'; -import { Button } from '../Button/Button'; - -# Tooltip - -## Usage - - - - - - - -## API - -### Import - -``` -import { Tooltip } from 'react95' -``` - -### Props - - diff --git a/src/Tooltip/Tooltip.stories.tsx b/src/Tooltip/Tooltip.stories.tsx index 2ad6f141..186af133 100644 --- a/src/Tooltip/Tooltip.stories.tsx +++ b/src/Tooltip/Tooltip.stories.tsx @@ -9,7 +9,7 @@ const Wrapper = styled.div` `; export default { - title: 'Tooltip', + title: 'Controls/Tooltip', component: Tooltip, decorators: [story => {story()}] } as ComponentMeta; diff --git a/src/Tooltip/Tooltip.tsx b/src/Tooltip/Tooltip.tsx index a71d894d..60cf5558 100644 --- a/src/Tooltip/Tooltip.tsx +++ b/src/Tooltip/Tooltip.tsx @@ -68,126 +68,130 @@ const Wrapper = styled.div` white-space: nowrap; `; -const Tooltip = forwardRef(function Tooltip( - { - className, - children, - disableFocusListener = false, - disableMouseListener = false, - enterDelay = 1000, - leaveDelay = 0, - onBlur, - onClose, - onFocus, - onMouseEnter, - onMouseLeave, - onOpen, - style, - text, - position = 'top', - ...otherProps - }, - ref -) { - const [show, setShow] = useState(false); - const [openTimer, setOpenTimer] = useState(); - const [closeTimer, setCloseTimer] = useState(); - - const isUsingFocus = !disableFocusListener; - const isUsingMouse = !disableMouseListener; - - const handleOpen = ( - event: React.FocusEvent | React.MouseEvent - ) => { - window.clearTimeout(openTimer); - window.clearTimeout(closeTimer); - - const timer = window.setTimeout(() => { - setShow(true); - - onOpen?.(event); - }, enterDelay); - - setOpenTimer(timer); - }; - - const handleEnter = ( - event: React.FocusEvent | React.MouseEvent - ) => { - event.persist(); - - if (isReactFocusEvent(event)) { - onFocus?.(event); - } else if (isReactMouseEvent(event)) { - onMouseEnter?.(event); - } - - handleOpen(event); - }; - - const handleClose = ( - event: React.FocusEvent | React.MouseEvent - ) => { - window.clearTimeout(openTimer); - window.clearTimeout(closeTimer); - - const timer = window.setTimeout(() => { - setShow(false); - - onClose?.(event); - }, leaveDelay); - - setCloseTimer(timer); - }; - - const handleLeave = ( - event: React.FocusEvent | React.MouseEvent +const Tooltip = forwardRef( + ( + { + className, + children, + disableFocusListener = false, + disableMouseListener = false, + enterDelay = 1000, + leaveDelay = 0, + onBlur, + onClose, + onFocus, + onMouseEnter, + onMouseLeave, + onOpen, + style, + text, + position = 'top', + ...otherProps + }, + ref ) => { - event.persist(); - - if (isReactFocusEvent(event)) { - onBlur?.(event); - } else if (isReactMouseEvent(event)) { - onMouseLeave?.(event); - } - - handleClose(event); - }; - - // set callbacks for onBlur and onFocus, unless disableFocusListener is true - const blurCb = isUsingFocus ? handleLeave : undefined; - const focusCb = isUsingFocus ? handleEnter : undefined; - - // set callbacks for onMouseEnter and onMouseLeave, unless disableMouseListener is true - const mouseEnterCb = isUsingMouse ? handleEnter : undefined; - const mouseLeaveCb = isUsingMouse ? handleLeave : undefined; - - // set the wrapper's tabIndex for focus events, unless disableFocusListener is true - const tabIndex = isUsingFocus ? 0 : undefined; - - return ( - - (); + const [closeTimer, setCloseTimer] = useState(); + + const isUsingFocus = !disableFocusListener; + const isUsingMouse = !disableMouseListener; + + const handleOpen = ( + event: React.FocusEvent | React.MouseEvent + ) => { + window.clearTimeout(openTimer); + window.clearTimeout(closeTimer); + + const timer = window.setTimeout(() => { + setShow(true); + + onOpen?.(event); + }, enterDelay); + + setOpenTimer(timer); + }; + + const handleEnter = ( + event: React.FocusEvent | React.MouseEvent + ) => { + event.persist(); + + if (isReactFocusEvent(event)) { + onFocus?.(event); + } else if (isReactMouseEvent(event)) { + onMouseEnter?.(event); + } + + handleOpen(event); + }; + + const handleClose = ( + event: React.FocusEvent | React.MouseEvent + ) => { + window.clearTimeout(openTimer); + window.clearTimeout(closeTimer); + + const timer = window.setTimeout(() => { + setShow(false); + + onClose?.(event); + }, leaveDelay); + + setCloseTimer(timer); + }; + + const handleLeave = ( + event: React.FocusEvent | React.MouseEvent + ) => { + event.persist(); + + if (isReactFocusEvent(event)) { + onBlur?.(event); + } else if (isReactMouseEvent(event)) { + onMouseLeave?.(event); + } + + handleClose(event); + }; + + // set callbacks for onBlur and onFocus, unless disableFocusListener is true + const blurCb = isUsingFocus ? handleLeave : undefined; + const focusCb = isUsingFocus ? handleEnter : undefined; + + // set callbacks for onMouseEnter and onMouseLeave, unless disableMouseListener is true + const mouseEnterCb = isUsingMouse ? handleEnter : undefined; + const mouseLeaveCb = isUsingMouse ? handleLeave : undefined; + + // set the wrapper's tabIndex for focus events, unless disableFocusListener is true + const tabIndex = isUsingFocus ? 0 : undefined; + + return ( + - {text} - - {children} - - ); -}); + + {text} + + {children} + + ); + } +); + +Tooltip.displayName = 'Tooltip'; export { Tooltip, TooltipProps }; diff --git a/src/Tree/Tree.spec.tsx b/src/TreeView/TreeView.spec.tsx similarity index 91% rename from src/Tree/Tree.spec.tsx rename to src/TreeView/TreeView.spec.tsx index 17ea298b..97600c11 100644 --- a/src/Tree/Tree.spec.tsx +++ b/src/TreeView/TreeView.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { renderWithTheme } from '../../test/utils'; -import { Tree } from './Tree'; +import { TreeView } from './TreeView'; const categories = [ { @@ -59,13 +59,13 @@ const categories = [ } ]; -describe('', () => { +describe('', () => { describe('prop: onNodeSelect', () => { it('should call onNodeSelect when uncontrolled', () => { const onNodeSelect = jest.fn((_, id) => id); const { getByText } = renderWithTheme( - + ); getByText('Beverages').click(); @@ -78,7 +78,11 @@ describe('', () => { const onNodeSelect = jest.fn((_, id) => id); const { getByText } = renderWithTheme( - + ); getByText('Beverages').click(); @@ -93,7 +97,7 @@ describe('', () => { const onNodeToggle = jest.fn((_, ids) => ids); const { getByText } = renderWithTheme( - + ); getByText('Beverages').click(); @@ -106,7 +110,7 @@ describe('', () => { const onNodeToggle = jest.fn((_, ids) => ids); const { getByText } = renderWithTheme( - ', () => { const onNodeToggle = jest.fn((_, ids) => ids); const { getByText } = renderWithTheme( - ', () => { ); const { getByText } = renderWithTheme( - {story()}] -} as ComponentMeta; +} as ComponentMeta; const categories = [ { @@ -97,9 +97,9 @@ categories.forEach(getIds); export function Basic() { return (
-
- -
+ + +
); } @@ -124,15 +124,15 @@ export function Controlled() { -
- + setSelected(id)} onNodeToggle={(_, ids) => setExpanded(ids)} expanded={expanded} selected={selected} /> -
+
); } @@ -144,9 +144,9 @@ Controlled.story = { export function Disabled() { return (
-
- -
+ + +
); } @@ -167,9 +167,9 @@ export function DisabledTreeItems() { return (
-
- -
+ + +
); } diff --git a/src/Tree/Tree.tsx b/src/TreeView/TreeView.tsx similarity index 94% rename from src/Tree/Tree.tsx rename to src/TreeView/TreeView.tsx index 2b321c5b..60d3f43d 100644 --- a/src/Tree/Tree.tsx +++ b/src/TreeView/TreeView.tsx @@ -1,8 +1,8 @@ import React, { forwardRef, useCallback } from 'react'; import styled, { css } from 'styled-components'; -import { StyledLabel, LabelText } from '../SwitchBase/SwitchBase'; import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled'; +import { LabelText, StyledLabel } from '../common/SwitchBase'; import { CommonStyledProps } from '../types'; type TreeLeaf = { @@ -13,7 +13,7 @@ type TreeLeaf = { label?: string; }; -type TreeProps = { +type TreeViewProps = { className?: string; defaultExpanded?: T[]; defaultSelected?: T; @@ -66,7 +66,7 @@ const focusedElementStyles = css<{ $disabled: boolean }>` : `cursor: default;`} `; -const TreeView = styled.ul<{ isRootLevel: boolean }>` +const TreeWrapper = styled.ul<{ isRootLevel: boolean }>` position: relative; isolation: isolate; @@ -293,7 +293,7 @@ function TreeBranch({ ); return ( - ({ isRootLevel={isRootLevel} > {tree.map(renderLeaf)} - + ); } @@ -317,7 +317,7 @@ function TreeInner( selected, style, tree = [] - }: TreeProps, + }: TreeViewProps, ref: React.ForwardedRef ) { const [expandedInternal, setExpandedInternal] = useControlledOrUncontrolled({ @@ -381,9 +381,13 @@ function TreeInner( ); } -const Tree = forwardRef(TreeInner) as ( +const TreeView = forwardRef(TreeInner) as ( // eslint-disable-next-line no-use-before-define - props: TreeProps & { ref?: React.ForwardedRef } + props: TreeViewProps & { ref?: React.ForwardedRef } ) => ReturnType; -export { Tree, TreeLeaf, TreeProps }; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +TreeView.displayProps = 'TreeView'; + +export { TreeView, TreeViewProps, TreeLeaf }; diff --git a/src/Window/Window.mdx b/src/Window/Window.mdx deleted file mode 100644 index 70c88c0c..00000000 --- a/src/Window/Window.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: Window -menu: Components ---- - -import Window from './Window'; -import WindowContent from '../WindowContent/WindowContent'; -import WindowHeader from '../WindowHeader/WindowHeader'; -import { Button } from '../Button/Button'; -import { Toolbar } from '../Toolbar/Toolbar'; - -# Window - -## Usage - -#### Default - - - - - react95.exe - - - - - - - - -
    -
  • something here
  • -
  • something here
  • -
  • something here
  • -
  • something here
  • -
-
-
-
- -#### Resizable - - - - react95.exe - - Resizable Window displays resize handle in bottom right corner - - - - -#### Not active - - - - react95.exe - I am not active - - - -## API - -### Import - -``` -import { Window } from 'react95' -``` - -### Props - - diff --git a/src/Window/Window.stories.tsx b/src/Window/Window.stories.tsx index 050a044e..06c88960 100644 --- a/src/Window/Window.stories.tsx +++ b/src/Window/Window.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta } from '@storybook/react'; import React from 'react'; import { Button, - Panel, + Frame, Toolbar, Window, WindowContent, @@ -13,7 +13,7 @@ import styled from 'styled-components'; const Wrapper = styled.div` padding: 5rem; background: ${({ theme }) => theme.desktopBackground}; - .window-header { + .window-title { display: flex; align-items: center; justify-content: space-between; @@ -63,7 +63,7 @@ const Wrapper = styled.div` `; export default { - title: 'Window', + title: 'Environment/Window', component: Window, subcomponents: { WindowHeader, WindowContent }, decorators: [story => {story()}] @@ -73,7 +73,7 @@ export function Default() { return ( <> - + react95.exe