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 (
+
+
+
+
+
+
+
+ );
+}