diff --git a/package.json b/package.json index ea70234ff..a79702218 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "@emotion/react": "11.10.0", "@emotion/styled": "11.10.0", "@jest/environment": "28.1.3", + "@mui/material": "^5.10.2", "@release-it/conventional-changelog": "5.1.0", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "22.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9bc79122..15ee475f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,7 @@ specifiers: '@emotion/react': 11.10.0 '@emotion/styled': 11.10.0 '@jest/environment': 28.1.3 + '@mui/material': ^5.10.2 '@release-it/conventional-changelog': 5.1.0 '@rollup/plugin-babel': 5.3.1 '@rollup/plugin-commonjs': 22.0.2 @@ -163,6 +164,7 @@ devDependencies: '@emotion/react': 11.10.0_msmmgljd7hl2w2irtggflhmema '@emotion/styled': 11.10.0_5sec57kzpgkzooe4crua5kfcly '@jest/environment': 28.1.3 + '@mui/material': 5.10.2_sqzxty2p7kxc2tmue4tecplwku '@release-it/conventional-changelog': 5.1.0_release-it@15.3.0 '@rollup/plugin-babel': 5.3.1_4ce4roknt3navmu3q3hwcigmqq '@rollup/plugin-commonjs': 22.0.2_rollup@2.78.1 @@ -2686,6 +2688,164 @@ packages: glob-to-regexp: 0.3.0 dev: true + /@mui/base/5.0.0-alpha.94_zxljzmqdrxwnuenbkrz77w74uy: + resolution: {integrity: sha512-IJXmgTF07H1Iv5zjDV7zJZGUmb9cN8ERzd2dgA1akh6NWZgwyIGyQx+Au9+QSDoM5vN3FqZvU/0YCU6inUwgeQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.18.9 + '@emotion/is-prop-valid': 1.2.0 + '@mui/types': 7.1.5_@types+react@18.0.17 + '@mui/utils': 5.9.3_react@18.2.0 + '@popperjs/core': 2.11.6 + '@types/react': 18.0.17 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 18.2.0 + dev: true + + /@mui/core-downloads-tracker/5.10.2: + resolution: {integrity: sha512-1guoGvL3QZ7VjA3y9zye9Rpm+jz18rVZIo3AauTGyW5ntDMxr/cR0M18nuc/NH2KqpMt+coh4NwPEO1uPuKM5w==} + dev: true + + /@mui/material/5.10.2_sqzxty2p7kxc2tmue4tecplwku: + resolution: {integrity: sha512-ay43fuQLXROXkxFd6tqbj394Hu8BlbmpCdEDFtAisijulla2ZLfQa24pjhdX+56HrHReB3cZsf/sRq+DSfIgiA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.18.9 + '@emotion/react': 11.10.0_msmmgljd7hl2w2irtggflhmema + '@emotion/styled': 11.10.0_5sec57kzpgkzooe4crua5kfcly + '@mui/base': 5.0.0-alpha.94_zxljzmqdrxwnuenbkrz77w74uy + '@mui/core-downloads-tracker': 5.10.2 + '@mui/system': 5.10.2_4thu2zqr4v2ubfoxjiyrxa5urm + '@mui/types': 7.1.5_@types+react@18.0.17 + '@mui/utils': 5.9.3_react@18.2.0 + '@types/react': 18.0.17 + '@types/react-transition-group': 4.4.5 + clsx: 1.2.1 + csstype: 3.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 18.2.0 + react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y + dev: true + + /@mui/private-theming/5.9.3_ug65io7jkbhmo4fihdmbrh3ina: + resolution: {integrity: sha512-Ys3WO39WqoGciGX9k5AIi/k2zJhlydv4FzlEEwtw9OqdMaV0ydK/TdZekKzjP9sTI/JcdAP3H5DWtUaPLQJjWg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.18.9 + '@mui/utils': 5.9.3_react@18.2.0 + '@types/react': 18.0.17 + prop-types: 15.8.1 + react: 18.2.0 + dev: true + + /@mui/styled-engine/5.10.2_rq3l25qc4qpq3j4w6o4x7hatzy: + resolution: {integrity: sha512-YqnptNQ2E0cWwOTmLCEvrddiiR/neUfn2AD/4TDUXZu8B2n7NfDb9d3bAUfWZV+KmulQdAedoaZDqyXBFGLdbQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.18.9 + '@emotion/cache': 11.10.1 + '@emotion/react': 11.10.0_msmmgljd7hl2w2irtggflhmema + '@emotion/styled': 11.10.0_5sec57kzpgkzooe4crua5kfcly + csstype: 3.1.0 + prop-types: 15.8.1 + react: 18.2.0 + dev: true + + /@mui/system/5.10.2_4thu2zqr4v2ubfoxjiyrxa5urm: + resolution: {integrity: sha512-YudwJhLcEoQiwCAmzeMr9P3ISiVGNsxBIIPzFxaGwJ8+mMrx3qoPVOV2sfm0ZuNiQuABshEw4KqHa5ftNC+pOQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.18.9 + '@emotion/react': 11.10.0_msmmgljd7hl2w2irtggflhmema + '@emotion/styled': 11.10.0_5sec57kzpgkzooe4crua5kfcly + '@mui/private-theming': 5.9.3_ug65io7jkbhmo4fihdmbrh3ina + '@mui/styled-engine': 5.10.2_rq3l25qc4qpq3j4w6o4x7hatzy + '@mui/types': 7.1.5_@types+react@18.0.17 + '@mui/utils': 5.9.3_react@18.2.0 + '@types/react': 18.0.17 + clsx: 1.2.1 + csstype: 3.1.0 + prop-types: 15.8.1 + react: 18.2.0 + dev: true + + /@mui/types/7.1.5_@types+react@18.0.17: + resolution: {integrity: sha512-HnRXrxgHJYJcT8ZDdDCQIlqk0s0skOKD7eWs9mJgBUu70hyW4iA6Kiv3yspJR474RFH8hysKR65VVSzUSzkuwA==} + peerDependencies: + '@types/react': '*' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.0.17 + dev: true + + /@mui/utils/5.9.3_react@18.2.0: + resolution: {integrity: sha512-l0N5bcrenE9hnwZ/jPecpIRqsDFHkPXoFUcmkgysaJwVZzJ3yQkGXB47eqmXX5yyGrSc6HksbbqXEaUya+siew==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.18.9 + '@types/prop-types': 15.7.5 + '@types/react-is': 17.0.3 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: true + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2919,6 +3079,10 @@ packages: config-chain: 1.1.13 dev: true + /@popperjs/core/2.11.6: + resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} + dev: true + /@release-it/conventional-changelog/5.1.0_release-it@15.3.0: resolution: {integrity: sha512-o55D822tVIoldUDj1Fp1KvenVREcEEjYOyuVNwRVnTcExFN6nWUPrH05q7Y8opT23N5snuCwPJ5bzLPEcpBvRg==} engines: {node: '>=14'} @@ -4774,6 +4938,12 @@ packages: dependencies: '@types/react': 18.0.17 + /@types/react-is/17.0.3: + resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} + dependencies: + '@types/react': 18.0.17 + dev: true + /@types/react-redux/7.1.24: resolution: {integrity: sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==} dependencies: @@ -4783,6 +4953,12 @@ packages: redux: 4.2.0 dev: true + /@types/react-transition-group/4.4.5: + resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} + dependencies: + '@types/react': 18.0.17 + dev: true + /@types/react-virtualized/9.21.21: resolution: {integrity: sha512-Exx6I7p4Qn+BBA1SRyj/UwQlZ0I0Pq7g7uhAp0QQ4JWzZunqEqNBGTmCmMmS/3N9wFgAGWuBD16ap7k8Y14VPA==} dependencies: @@ -14418,6 +14594,20 @@ packages: refractor: 3.6.0 dev: true + /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.18.9 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: true + /react-virtualized/9.22.3_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==} peerDependencies: diff --git a/stories/examples/13-mui.stories.tsx b/stories/examples/13-mui.stories.tsx new file mode 100644 index 000000000..984db8695 --- /dev/null +++ b/stories/examples/13-mui.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { getQuotes } from '../src/data'; +import QuoteAppMUI from '../src/vertical/quote-app-mui'; + +const generateData = { + small: () => getQuotes(), + medium: () => getQuotes(40), + large: () => getQuotes(500), +}; + +storiesOf('Examples/MUI', module) + .add('cards', () => ( + + )) + .add('boxes', () => ( + + )); diff --git a/stories/src/primatives/quote-item-mui.tsx b/stories/src/primatives/quote-item-mui.tsx new file mode 100644 index 000000000..a19a48031 --- /dev/null +++ b/stories/src/primatives/quote-item-mui.tsx @@ -0,0 +1,188 @@ +import React, { CSSProperties } from 'react'; +import styled from '@emotion/styled'; +import { colors } from '@atlaskit/theme'; +import type { DraggableProvided } from '@hello-pangea/dnd'; +import { Box, Card, Chip, Grid, Typography } from '@mui/material'; +import { grid } from '../constants'; +import type { Quote, AuthorColors } from '../types'; + +interface Props { + quote: Quote; + isDragging: boolean; + provided: DraggableProvided; + isClone?: boolean; + isGroupedOver?: boolean; + style?: CSSProperties; + index?: number; + variant: 'card' | 'box'; +} + +const getBackgroundColor = ( + isDragging: boolean, + isGroupedOver: boolean, + authorColors: AuthorColors, +) => { + if (isDragging) { + return authorColors.soft; + } + + if (isGroupedOver) { + return colors.N30; + } + + return colors.N0; +}; + +const getBorderColor = (isDragging: boolean, authorColors: AuthorColors) => + isDragging ? authorColors.hard : 'transparent'; + +const imageSize = 40; + +const CloneBadge = styled.div` + background: ${colors.G100}; + bottom: ${grid / 2}px; + border: 2px solid ${colors.G200}; + border-radius: 50%; + box-sizing: border-box; + font-size: 10px; + position: absolute; + right: -${imageSize / 3}px; + top: -${imageSize / 3}px; + transform: rotate(40deg); + + height: ${imageSize}px; + width: ${imageSize}px; + + display: flex; + justify-content: center; + align-items: center; +`; + +const Avatar = styled.img` + width: ${imageSize}px; + height: ${imageSize}px; + border-radius: 50%; + margin-right: ${grid}px; + flex-shrink: 0; + flex-grow: 0; +`; + +function getStyle(provided: DraggableProvided, style?: CSSProperties | null) { + if (!style) { + return provided.draggableProps.style; + } + + return { + ...provided.draggableProps.style, + ...style, + }; +} + +interface ContainerProps extends Props { + children: React.ReactNode; + variant: 'card' | 'box'; +} + +function Container({ children, variant, ...props }: ContainerProps) { + if (variant === 'card') { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +// Previously this extended React.Component +// That was a good thing, because using React.PureComponent can hide +// issues with the selectors. However, moving it over does can considerable +// performance improvements when reordering big lists (400ms => 200ms) +// Need to be super sure we are not relying on PureComponent here for +// things we should be doing in the selector as we do not know if consumers +// will be using PureComponent +function QuoteItemMUI(props: Props) { + const { quote, isClone } = props; + + return ( + + + + + {isClone ? Clone : null} + + + {quote.content} + + + id:{quote.id} + + + + + ); +} + +export default React.memo(QuoteItemMUI); diff --git a/stories/src/primatives/quote-list-mui.tsx b/stories/src/primatives/quote-list-mui.tsx new file mode 100644 index 000000000..0093c02bc --- /dev/null +++ b/stories/src/primatives/quote-list-mui.tsx @@ -0,0 +1,211 @@ +import React, { CSSProperties, ReactElement } from 'react'; +import styled from '@emotion/styled'; +import { colors } from '@atlaskit/theme'; +import { Droppable, Draggable } from '@hello-pangea/dnd'; +import type { + DroppableProvided, + DroppableStateSnapshot, + DraggableProvided, + DraggableStateSnapshot, +} from '@hello-pangea/dnd'; +import { grid } from '../constants'; +import Title from './title'; +import type { Quote } from '../types'; +import QuoteItemMUI from './quote-item-mui'; + +export const getBackgroundColor = ( + isDraggingOver: boolean, + isDraggingFrom: boolean, +): string => { + if (isDraggingOver) { + return colors.R50; + } + if (isDraggingFrom) { + return colors.T50; + } + return colors.N30; +}; + +interface WrapperProps { + isDraggingOver: boolean; + isDraggingFrom: boolean; + isDropDisabled: boolean; +} + +const Wrapper = styled.div` + background-color: ${(props) => + getBackgroundColor(props.isDraggingOver, props.isDraggingFrom)}; + display: flex; + flex-direction: column; + opacity: ${({ isDropDisabled }) => (isDropDisabled ? 0.5 : 'inherit')}; + padding: ${grid}px; + border: ${grid}px; + padding-bottom: 0; + transition: background-color 0.2s ease, opacity 0.1s ease; + user-select: none; + width: 250px; +`; + +const scrollContainerHeight = 250; + +const DropZone = styled.div` + /* stop the list collapsing when empty */ + min-height: ${scrollContainerHeight}px; + + /* + not relying on the items for a margin-bottom + as it will collapse when the list is empty + */ + padding-bottom: ${grid}px; +`; + +const ScrollContainer = styled.div` + overflow-x: hidden; + overflow-y: auto; + max-height: ${scrollContainerHeight}px; +`; + +/* stylelint-disable block-no-empty */ +const Container = styled.div``; + +/* stylelint-enable */ + +interface Props { + listId?: string; + listType?: string; + quotes: Quote[]; + title?: string; + internalScroll?: boolean; + scrollContainerStyle?: CSSProperties; + isDropDisabled?: boolean; + isCombineEnabled?: boolean; + style?: CSSProperties; + // may not be provided - and might be null + ignoreContainerClipping?: boolean; + useClone?: boolean; + variant: 'card' | 'box'; +} + +interface QuoteListProps { + quotes: Quote[]; + variant: 'card' | 'box'; +} + +function InnerQuoteList(props: QuoteListProps): ReactElement { + return ( + <> + {props.quotes.map((quote: Quote, index: number) => ( + + {( + dragProvided: DraggableProvided, + dragSnapshot: DraggableStateSnapshot, + ) => ( + + )} + + ))} + + ); +} + +const InnerQuoteListMemo = React.memo(InnerQuoteList); + +interface InnerListProps { + dropProvided: DroppableProvided; + quotes: Quote[]; + title: string | undefined | null; + variant: 'card' | 'box'; +} + +function InnerList(props: InnerListProps) { + const { quotes, dropProvided } = props; + const title = props.title ? {props.title} : null; + + return ( + + {title} + + + {dropProvided.placeholder} + + + ); +} + +export default function QuoteListMUI(props: Props): ReactElement { + const { + ignoreContainerClipping, + internalScroll, + scrollContainerStyle, + isDropDisabled, + isCombineEnabled, + listId = 'LIST', + listType, + style, + quotes, + title, + useClone, + variant, + } = props; + + return ( + ( + + ) + : undefined + } + > + {( + dropProvided: DroppableProvided, + dropSnapshot: DroppableStateSnapshot, + ) => ( + + {internalScroll ? ( + + + + ) : ( + + )} + + )} + + ); +} diff --git a/stories/src/vertical/quote-app-mui.tsx b/stories/src/vertical/quote-app-mui.tsx new file mode 100644 index 000000000..04f01be4a --- /dev/null +++ b/stories/src/vertical/quote-app-mui.tsx @@ -0,0 +1,86 @@ +import React, { CSSProperties, ReactElement, useState } from 'react'; +import styled from '@emotion/styled'; +import type { DropResult } from '@hello-pangea/dnd'; +import { DragDropContext } from '@hello-pangea/dnd'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import type { Quote } from '../types'; +import reorder from '../reorder'; +import { grid } from '../constants'; +import QuoteListMUI from '../primatives/quote-list-mui'; + +const Root = styled.div` + /* flexbox */ + padding-top: ${grid * 2}px; + display: flex; + justify-content: center; + align-items: flex-start; +`; + +const theme = createTheme({ + palette: { + mode: 'light', + }, +}); + +interface Props { + initial: Quote[]; + isCombineEnabled?: boolean; + listStyle?: CSSProperties; + variant: 'card' | 'box'; +} + +export default function QuoteAppMUI(props: Props): ReactElement { + const [quotes, setQuotes] = useState(() => props.initial); + + function onDragStart() { + // Add a little vibration if the browser supports it. + // Add's a nice little physical feedback + if (window.navigator.vibrate) { + window.navigator.vibrate(100); + } + } + + function onDragEnd(result: DropResult) { + // combining item + if (result.combine) { + // super simple: just removing the dragging item + const newQuotes: Quote[] = [...quotes]; + newQuotes.splice(result.source.index, 1); + setQuotes(newQuotes); + return; + } + + // dropped outside the list + if (!result.destination) { + return; + } + + if (result.destination.index === result.source.index) { + return; + } + + const newQuotes = reorder( + quotes, + result.source.index, + result.destination.index, + ); + + setQuotes(newQuotes); + } + + return ( + + + + + + + + ); +}