diff --git a/packages/core/upload/admin/src/components/FolderCard/FolderCard.js b/packages/core/upload/admin/src/components/FolderCard/FolderCard.js
new file mode 100644
index 0000000000..a297ffa2ba
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/FolderCard.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+
+import { pxToRem } from '@strapi/helper-plugin';
+import { Box } from '@strapi/design-system/Box';
+import { Stack } from '@strapi/design-system/Stack';
+import Folder from '@strapi/icons/Folder';
+
+import { FolderCardContext } from './FolderCardContext';
+import useId from './utils/useId';
+
+const FauxClickWrapper = styled.button`
+ height: 100%;
+ left: 0;
+ position: absolute;
+ opacity: 0;
+ top: 0;
+ width: 100%;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+`;
+
+const StyledFolder = styled(Folder)`
+ path {
+ fill: currentColor;
+ }
+`;
+
+export const FolderCard = ({ children, id, startAction, ariaLabel, onDoubleClick, ...props }) => {
+ const generatedId = useId(id);
+
+ return (
+
+
+ event.preventDefault()}
+ onDoubleClick={onDoubleClick}
+ zIndex={1}
+ tabIndex={-1}
+ aria-label={ariaLabel}
+ aria-hidden
+ />
+
+
+ {startAction}
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+FolderCard.defaultProps = {
+ id: undefined,
+};
+
+FolderCard.propTypes = {
+ ariaLabel: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ id: PropTypes.string,
+ onDoubleClick: PropTypes.func.isRequired,
+ startAction: PropTypes.element.isRequired,
+};
diff --git a/packages/core/upload/admin/src/components/FolderCard/FolderCardBody.js b/packages/core/upload/admin/src/components/FolderCard/FolderCardBody.js
new file mode 100644
index 0000000000..bd7ecb5cb5
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/FolderCardBody.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { Flex } from '@strapi/design-system/Flex';
+
+import { useFolderCard } from './FolderCardContext';
+
+const StyledBox = styled(Flex)`
+ user-select: none;
+`;
+
+export const FolderCardBody = props => {
+ const { id } = useFolderCard();
+
+ return (
+
+ );
+};
diff --git a/packages/core/upload/admin/src/components/FolderCard/FolderCardCheckbox.js b/packages/core/upload/admin/src/components/FolderCard/FolderCardCheckbox.js
new file mode 100644
index 0000000000..ced28a17cc
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/FolderCardCheckbox.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Box } from '@strapi/design-system/Box';
+import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
+import { useFolderCard } from './FolderCardContext';
+
+export const FolderCardCheckbox = props => {
+ const { id } = useFolderCard();
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/upload/admin/src/components/FolderCard/FolderCardContext.js b/packages/core/upload/admin/src/components/FolderCard/FolderCardContext.js
new file mode 100644
index 0000000000..985ae836d9
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/FolderCardContext.js
@@ -0,0 +1,7 @@
+import { createContext, useContext } from 'react';
+
+export const FolderCardContext = createContext();
+
+export function useFolderCard() {
+ return useContext(FolderCardContext);
+}
diff --git a/packages/core/upload/admin/src/components/FolderCard/FolderCardLink.js b/packages/core/upload/admin/src/components/FolderCard/FolderCardLink.js
new file mode 100644
index 0000000000..f5cf80ceb5
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/FolderCardLink.js
@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+import { BaseLink } from '@strapi/design-system/BaseLink';
+
+export const FolderCardLink = styled(BaseLink)`
+ text-decoration: none;
+`;
diff --git a/packages/core/upload/admin/src/components/FolderCard/index.js b/packages/core/upload/admin/src/components/FolderCard/index.js
new file mode 100644
index 0000000000..4da01d2615
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/index.js
@@ -0,0 +1,4 @@
+export { FolderCard } from './FolderCard';
+export { FolderCardBody } from './FolderCardBody';
+export { FolderCardCheckbox } from './FolderCardCheckbox';
+export { FolderCardLink } from './FolderCardLink';
diff --git a/packages/core/upload/admin/src/components/FolderCard/tests/FolderCard.test.js b/packages/core/upload/admin/src/components/FolderCard/tests/FolderCard.test.js
new file mode 100644
index 0000000000..e91e4408d5
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/tests/FolderCard.test.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import { BaseLink } from '@strapi/design-system/BaseLink';
+import { Flex } from '@strapi/design-system/Flex';
+import { ThemeProvider, lightTheme } from '@strapi/design-system';
+import { Typography } from '@strapi/design-system/Typography';
+import { render, fireEvent } from '@testing-library/react';
+
+import { FolderCard } from '../FolderCard';
+import { FolderCardBody } from '../FolderCardBody';
+import { FolderCardCheckbox } from '../FolderCardCheckbox';
+
+const ID_FIXTURE = 'folder-1';
+
+// eslint-disable-next-line react/prop-types
+const ComponentFixture = ({ children, ...props }) => {
+ return (
+
+ >}
+ onDoubleClick={() => {}}
+ {...props}
+ >
+ {children || ''}
+
+
+ );
+};
+
+describe('FolderCard', () => {
+ it('renders and matches the snapshot', () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('properly calls the onDoubleClick callback', () => {
+ const callback = jest.fn();
+ const { container } = render();
+
+ fireEvent(container.querySelector('a'), new MouseEvent('dblclick', { bubbles: true }));
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it('has all required ids set when rendering a start action', () => {
+ const { container } = render(
+ }>
+
+
+
+
+ Pictures
+
+
+
+
+
+ );
+
+ expect(container.querySelector(`[id="${ID_FIXTURE}-title"]`)).toBeInTheDocument();
+ expect(container.querySelector(`[aria-labelledby="${ID_FIXTURE}-title"]`)).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/upload/admin/src/components/FolderCard/tests/__snapshots__/FolderCard.test.js.snap b/packages/core/upload/admin/src/components/FolderCard/tests/__snapshots__/FolderCard.test.js.snap
new file mode 100644
index 0000000000..2851e7ab2a
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/tests/__snapshots__/FolderCard.test.js.snap
@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FolderCard renders and matches the snapshot 1`] = `
+.c1 {
+ cursor: pointer;
+}
+
+.c8 {
+ border: 0;
+ -webkit-clip: rect(0 0 0 0);
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
+.c0 {
+ position: relative;
+}
+
+.c6 {
+ background: #eaf5ff;
+ color: #66b7f1;
+ padding-top: 8px;
+ padding-right: 12px;
+ padding-bottom: 8px;
+ padding-left: 12px;
+ border-radius: 4px;
+}
+
+.c3 {
+ background: #ffffff;
+ padding-top: 12px;
+ padding-right: 16px;
+ padding-bottom: 12px;
+ padding-left: 16px;
+ border-radius: 4px;
+ box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
+ cursor: pointer;
+ cursor: pointer;
+}
+
+.c4 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+}
+
+.c5 > * {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.c5 > * + * {
+ margin-left: 12px;
+}
+
+.c2 {
+ height: 100%;
+ left: 0;
+ position: absolute;
+ opacity: 0;
+ top: 0;
+ width: 100%;
+}
+
+.c2:hover,
+.c2:focus {
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.c7 path {
+ fill: currentColor;
+}
+
+
+`;
diff --git a/packages/core/upload/admin/src/components/FolderCard/utils/tests/useId.test.js b/packages/core/upload/admin/src/components/FolderCard/utils/tests/useId.test.js
new file mode 100644
index 0000000000..2012b24645
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/utils/tests/useId.test.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render, act } from '@testing-library/react';
+
+import useId from '../useId';
+
+function setup(...args) {
+ let returnVal;
+
+ function TestComponent() {
+ returnVal = useId(...args);
+
+ return null;
+ }
+
+ render();
+
+ return returnVal;
+}
+
+describe('useId', () => {
+ let id;
+
+ test('increments', () => {
+ id = setup('one');
+
+ expect(id).toBe('one-1');
+
+ act(() => {
+ id = setup('one');
+ });
+
+ expect(id).toBe('one-2');
+ });
+
+ test('works with namespaces', () => {
+ act(() => {
+ id = setup('two');
+ });
+
+ expect(id).toBe('two-3');
+ });
+});
diff --git a/packages/core/upload/admin/src/components/FolderCard/utils/useId.js b/packages/core/upload/admin/src/components/FolderCard/utils/useId.js
new file mode 100644
index 0000000000..91b8b43377
--- /dev/null
+++ b/packages/core/upload/admin/src/components/FolderCard/utils/useId.js
@@ -0,0 +1,13 @@
+import { useRef } from 'react';
+
+let id = 0;
+
+const genId = () => ++id;
+
+const useId = (prefix, initialId) => {
+ const idRef = useRef(initialId || `${prefix}-${genId()}`);
+
+ return idRef.current;
+};
+
+export default useId;