mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 11:54:10 +00:00 
			
		
		
		
	ML: Add FolderCard component
This commit is contained in:
		
							parent
							
								
									c494431a20
								
							
						
					
					
						commit
						609fb1336d
					
				@ -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 (
 | 
			
		||||
    <FolderCardContext.Provider value={{ id: generatedId }}>
 | 
			
		||||
      <Box position="relative" {...props}>
 | 
			
		||||
        <FauxClickWrapper
 | 
			
		||||
          type="button"
 | 
			
		||||
          onClick={event => event.preventDefault()}
 | 
			
		||||
          onDoubleClick={onDoubleClick}
 | 
			
		||||
          zIndex={1}
 | 
			
		||||
          tabIndex={-1}
 | 
			
		||||
          aria-label={ariaLabel}
 | 
			
		||||
          aria-hidden
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Stack
 | 
			
		||||
          hasRadius
 | 
			
		||||
          background="neutral0"
 | 
			
		||||
          shadow="tableShadow"
 | 
			
		||||
          paddingBottom={3}
 | 
			
		||||
          paddingLeft={4}
 | 
			
		||||
          paddingRight={4}
 | 
			
		||||
          paddingTop={3}
 | 
			
		||||
          spacing={3}
 | 
			
		||||
          horizontal
 | 
			
		||||
          cursor="pointer"
 | 
			
		||||
        >
 | 
			
		||||
          {startAction}
 | 
			
		||||
 | 
			
		||||
          <Box
 | 
			
		||||
            hasRadius
 | 
			
		||||
            background="secondary100"
 | 
			
		||||
            color="secondary500"
 | 
			
		||||
            paddingBottom={2}
 | 
			
		||||
            paddingLeft={3}
 | 
			
		||||
            paddingRight={3}
 | 
			
		||||
            paddingTop={2}
 | 
			
		||||
          >
 | 
			
		||||
            <StyledFolder width={pxToRem(20)} height={pxToRem(18)} />
 | 
			
		||||
          </Box>
 | 
			
		||||
 | 
			
		||||
          {children}
 | 
			
		||||
        </Stack>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </FolderCardContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
};
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <StyledBox
 | 
			
		||||
      {...props}
 | 
			
		||||
      id={`${id}-title`}
 | 
			
		||||
      alignItems="flex-start"
 | 
			
		||||
      direction="column"
 | 
			
		||||
      position="relative"
 | 
			
		||||
      zIndex={3}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <Box position="relative" zIndex={2}>
 | 
			
		||||
      <BaseCheckbox aria-labelledby={`${id}-title`} {...props} />
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import { createContext, useContext } from 'react';
 | 
			
		||||
 | 
			
		||||
export const FolderCardContext = createContext();
 | 
			
		||||
 | 
			
		||||
export function useFolderCard() {
 | 
			
		||||
  return useContext(FolderCardContext);
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
 | 
			
		||||
import { BaseLink } from '@strapi/design-system/BaseLink';
 | 
			
		||||
 | 
			
		||||
export const FolderCardLink = styled(BaseLink)`
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
`;
 | 
			
		||||
@ -0,0 +1,4 @@
 | 
			
		||||
export { FolderCard } from './FolderCard';
 | 
			
		||||
export { FolderCardBody } from './FolderCardBody';
 | 
			
		||||
export { FolderCardCheckbox } from './FolderCardCheckbox';
 | 
			
		||||
export { FolderCardLink } from './FolderCardLink';
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <ThemeProvider theme={lightTheme}>
 | 
			
		||||
      <FolderCard
 | 
			
		||||
        id={ID_FIXTURE}
 | 
			
		||||
        ariaLabel="Folder 1"
 | 
			
		||||
        href="/"
 | 
			
		||||
        startAction={<></>}
 | 
			
		||||
        onDoubleClick={() => {}}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children || ''}
 | 
			
		||||
      </FolderCard>
 | 
			
		||||
    </ThemeProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('FolderCard', () => {
 | 
			
		||||
  it('renders and matches the snapshot', () => {
 | 
			
		||||
    const { container } = render(<ComponentFixture />);
 | 
			
		||||
    expect(container).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('properly calls the onDoubleClick callback', () => {
 | 
			
		||||
    const callback = jest.fn();
 | 
			
		||||
    const { container } = render(<ComponentFixture onDoubleClick={callback} />);
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
      <ComponentFixture startAction={<FolderCardCheckbox value={false} />}>
 | 
			
		||||
        <FolderCardBody as="h2">
 | 
			
		||||
          <BaseLink href="https://strapi.io" textDecoration="none">
 | 
			
		||||
            <Flex direction="column" alignItems="flex-start">
 | 
			
		||||
              <Typography variant="omega" fontWeight="semiBold">
 | 
			
		||||
                Pictures
 | 
			
		||||
              </Typography>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </BaseLink>
 | 
			
		||||
        </FolderCardBody>
 | 
			
		||||
      </ComponentFixture>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(container.querySelector(`[id="${ID_FIXTURE}-title"]`)).toBeInTheDocument();
 | 
			
		||||
    expect(container.querySelector(`[aria-labelledby="${ID_FIXTURE}-title"]`)).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
  <div
 | 
			
		||||
    class="c0"
 | 
			
		||||
  >
 | 
			
		||||
    <a
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
      aria-label="Folder 1"
 | 
			
		||||
      class="c1 c2"
 | 
			
		||||
      href="/"
 | 
			
		||||
      tabindex="-1"
 | 
			
		||||
      target="_self"
 | 
			
		||||
      text-decoration="none"
 | 
			
		||||
    />
 | 
			
		||||
    <div
 | 
			
		||||
      class="c3 c4 c5"
 | 
			
		||||
      cursor="pointer"
 | 
			
		||||
      spacing="3"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        class="c6"
 | 
			
		||||
      >
 | 
			
		||||
        <svg
 | 
			
		||||
          class="c7"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          height="1em"
 | 
			
		||||
          viewBox="0 0 24 24"
 | 
			
		||||
          width="1em"
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        >
 | 
			
		||||
          <path
 | 
			
		||||
            d="M12.414 5H21a1 1 0 011 1v14a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1h7.414l2 2z"
 | 
			
		||||
            fill="#212134"
 | 
			
		||||
          />
 | 
			
		||||
        </svg>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div
 | 
			
		||||
    class="c8"
 | 
			
		||||
  >
 | 
			
		||||
    <p
 | 
			
		||||
      aria-live="polite"
 | 
			
		||||
      aria-relevant="all"
 | 
			
		||||
      id="live-region-log"
 | 
			
		||||
      role="log"
 | 
			
		||||
    />
 | 
			
		||||
    <p
 | 
			
		||||
      aria-live="polite"
 | 
			
		||||
      aria-relevant="all"
 | 
			
		||||
      id="live-region-status"
 | 
			
		||||
      role="status"
 | 
			
		||||
    />
 | 
			
		||||
    <p
 | 
			
		||||
      aria-live="assertive"
 | 
			
		||||
      aria-relevant="all"
 | 
			
		||||
      id="live-region-alert"
 | 
			
		||||
      role="alert"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
@ -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(<TestComponent />);
 | 
			
		||||
 | 
			
		||||
  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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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;
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user