From b7083bdafce29dd82069f437fe25a3245718d182 Mon Sep 17 00:00:00 2001 From: Robert Niznik Date: Mon, 25 Nov 2024 16:41:17 -0500 Subject: [PATCH] feat(components): add `DropZone` (#1489) * feat(components): add `DropZone` * chore: add changeset * fix: add gap --- .changeset/moody-bobcats-hear.md | 5 ++ .../components/__tests__/DropZone.spec.tsx | 11 +++ packages/components/src/DropZone.tsx | 24 +++++++ packages/components/src/index.ts | 2 + .../components/src/styles/DropZone.module.css | 28 ++++++++ .../components/stories/DropZone.stories.tsx | 71 +++++++++++++++++++ 6 files changed, 141 insertions(+) create mode 100644 .changeset/moody-bobcats-hear.md create mode 100644 packages/components/__tests__/DropZone.spec.tsx create mode 100644 packages/components/src/DropZone.tsx create mode 100644 packages/components/src/styles/DropZone.module.css create mode 100644 packages/components/stories/DropZone.stories.tsx diff --git a/.changeset/moody-bobcats-hear.md b/.changeset/moody-bobcats-hear.md new file mode 100644 index 000000000..a325b1f01 --- /dev/null +++ b/.changeset/moody-bobcats-hear.md @@ -0,0 +1,5 @@ +--- +"@launchpad-ui/components": patch +--- + +Add `DropZone` diff --git a/packages/components/__tests__/DropZone.spec.tsx b/packages/components/__tests__/DropZone.spec.tsx new file mode 100644 index 000000000..8284deec1 --- /dev/null +++ b/packages/components/__tests__/DropZone.spec.tsx @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from '../../../test/utils'; +import { DropZone } from '../src'; + +describe('DropZone', () => { + it('renders', () => { + render(); + expect(screen.getByRole('button')).toBeVisible(); + }); +}); diff --git a/packages/components/src/DropZone.tsx b/packages/components/src/DropZone.tsx new file mode 100644 index 000000000..42dfbdf72 --- /dev/null +++ b/packages/components/src/DropZone.tsx @@ -0,0 +1,24 @@ +import type { ForwardedRef } from 'react'; +import type { DropZoneProps } from 'react-aria-components'; + +import { cva } from 'class-variance-authority'; +import { forwardRef } from 'react'; +import { DropZone as AriaDropZone } from 'react-aria-components'; + +import styles from './styles/DropZone.module.css'; + +const zone = cva(styles.zone); + +const _DropZone = ({ className, ...props }: DropZoneProps, ref: ForwardedRef) => { + return ; +}; + +/** + * A drop zone is an area into which one or multiple objects can be dragged and dropped. + * + * https://react-spectrum.adobe.com/react-aria/DropZone.html + */ +const DropZone = forwardRef(_DropZone); + +export { DropZone }; +export type { DropZoneProps }; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 19649de03..18f2d89fe 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -24,6 +24,7 @@ export type { DatePickerProps, DateRangePickerProps } from './DatePicker'; export type { DialogProps, DialogTriggerProps } from './Dialog'; export type { DisclosureProps, DisclosurePanelProps } from './Disclosure'; export type { DropIndicatorProps } from './DropIndicator'; +export type { DropZoneProps } from './DropZone'; export type { FieldErrorProps } from './FieldError'; export type { FieldGroupProps } from './FieldGroup'; export type { FileTriggerProps } from './FileTrigger'; @@ -99,6 +100,7 @@ export { DatePicker, DateRangePicker } from './DatePicker'; export { Dialog, DialogTrigger } from './Dialog'; export { Disclosure, DisclosurePanel } from './Disclosure'; export { DropIndicator } from './DropIndicator'; +export { DropZone } from './DropZone'; export { FieldError } from './FieldError'; export { FieldGroup } from './FieldGroup'; export { FileTrigger } from './FileTrigger'; diff --git a/packages/components/src/styles/DropZone.module.css b/packages/components/src/styles/DropZone.module.css new file mode 100644 index 000000000..c8c2d2134 --- /dev/null +++ b/packages/components/src/styles/DropZone.module.css @@ -0,0 +1,28 @@ +.zone { + background-color: var(--lp-color-bg-ui-secondary); + border: 1px solid var(--lp-color-border-ui-primary); + border-radius: var(--lp-border-radius-medium); + align-items: center; + justify-content: center; + margin: 0; + outline: none; + padding: var(--lp-spacing-500) var(--lp-spacing-700); + display: inline-flex; + flex-direction: column; + gap: var(--lp-spacing-400); + + &[data-focus-visible], + &[data-drop-target] { + outline: 2px solid var(--lp-color-shadow-interactive-focus); + outline-offset: -2px; + z-index: 2; + } + + &[data-drop-target] { + background-color: var(--lp-color-bg-interactive-selected); + } + + &[data-disabled] { + opacity: 0.64; + } +} diff --git a/packages/components/stories/DropZone.stories.tsx b/packages/components/stories/DropZone.stories.tsx new file mode 100644 index 000000000..bce848b40 --- /dev/null +++ b/packages/components/stories/DropZone.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import type { FileDropItem } from 'react-aria'; + +import { useState } from 'react'; + +import { Button, DropZone, FileTrigger, Text } from '../src'; + +const meta: Meta = { + component: DropZone, + title: 'Components/Drag and drop/DropZone', + parameters: { + status: { + type: import.meta.env.STORYBOOK_PACKAGE_STATUS__COMPONENTS, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Example: Story = { + render: (args) => { + const [dropped, setDropped] = useState(false); + + return ( + { + setDropped(true); + }} + {...args} + > + {dropped ? 'You dropped something' : 'Drop object here'} + + ); + }, +}; + +export const WithFileTrigger: Story = { + render: (args) => { + const [files, setFiles] = useState(null); + + return ( + { + const files = e.items.filter((file) => file.kind === 'file') as FileDropItem[]; + const filenames = files.map((file) => file.name); + setFiles(filenames.join(', ')); + }} + {...args} + > + { + if (e) { + const files = Array.from(e); + const filenames = files.map((file) => file.name); + setFiles(filenames.join(', ')); + } + }} + > + + + + {files || 'Drop files here'} + + + ); + }, + name: 'FileTrigger', +};