Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multi-column layouts #37

Merged
merged 25 commits into from
Sep 22, 2023
Merged

Add support for multi-column layouts #37

merged 25 commits into from
Sep 22, 2023

Conversation

chrisvxd
Copy link
Member

@chrisvxd chrisvxd commented Sep 6, 2023

Description

This PR adds:

  • support for multi-column layouts via the new <DropZone /> component.
  • support for infinitely nestable DropZones
  • a reducer to handle data mutations
  • improved UI to improve differentiation for hover, active and drag states
  • a redesigned action bar that sits outside the component, rather than inside
  • a redesigned outline area to support drop zones

Unfortunately this results in a rather large PR that was hard to envision before starting the work. Further work should be smaller following this significant effort.

Closes #43

Example

import { DropZone } from "@measured/puck";

const config = {
  MyComponent: {
    render: () => {
      return (
        <div>
          <DropZone dropzone="content" />
        </div>
      );
    },
  },
};

Implementation

This PR adds a new <DropZone> component that replaces the core react-beautiful-dnd droppable functionality that was previously kept in the <Puck> component, and exposes this to the user. It's essentially a wrapper around react-beautiful-dnd, with additional logic to support infinitely nested DropZones, application-level context and a robust UX.

Specifically, the <DropZone> component:

  • replaces much of the functionality previously kept in the Puck component
  • exposes react-beautiful-dnd Droppable component to users
  • handles hover states and drag states
  • introduces mouse-over collision detection, disabling other DropZones
  • restricts dragging to those that share a parent component (or live at the root), with the exception of first drags (see The Rules of DropZones below)
  • exposes an app-wide context to understand which DropZones exist, which component the user is hovered over and which component owns that DropZone

The Rules of DropZones

  1. You can drag from the component list on the LHS into any DropZone
  2. You can drag components between DropZones, so long as those DropZones share a parent (also known as area)
  3. You can't drag between DropZones that don't share a parent (or area)
  4. Your mouse must be directly over a DropZone for a collision to be detected

Data model

To support this, we add the dropzones key to the Data model. This is an additive, non-breaking change.

  • dropzones (object)
    • [dropzoneCompoundId] (object[]) : Same shape as the content field on Data
      • type (string): Component name
      • props (object):
        • [prop] (string): User defined data from component fields

dropzoneCompoundId is a string that takes the shape area:dropzone. area is the id of the parent component that contains this dropzone. dropzone is the ID provided by the user when using the DropZone.

I did alternatively consider using a nested structure here. This may be clearer, but comes with trade-offs for the implementation. Example alternative:

  • dropzones (object)
    • [areaId] (object) :
      • [dropzoneId] (object[]): Same shape as the content field on Data
        • type (string): Component name
        • props (object):
          • [prop] (string): User defined data from component fields

Rendering multiple columns

Example data model for a page containing a Columns component at the root, which renders 2 columns. The first column contains a Heading component.

This config:

import { DropZone } from "@measured/puck";

const config = {
  Columns: {
    render: () => {
      return (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr'}}>
          <DropZone dropzone="column-1" />
          <DropZone dropzone="column-2" />
        </div>
      );
    },
  },
  Heading: {
    fields: {
      title: {
        type: "text",
      },
    },
    render: ({text}) => {
      return <h2>{text}</h2>
    }
  }
};

Produces this data:

{
  "root": {},
  "content": [
    {
      "type": "Columns",
      "props": {
        "id": "Columns-0d9077e00e0ad66c34c62ab6986967e1ce04f9e4",
      },
    }
  ],
  "dropzones": {
    "Columns-2d650a8ceb081a2c04f3a2d17a7703ca6efb0d06:column-1": [
      {
        "type": "Heading",
        "props": {
          "title": "Hello, world",
          "id": "Card-0d9077e00e0ad66c34c62ab6986967e1ce04f9e4",
        },
      },
    ],
  }
}

Retaining backwards compatibility with the root DropZone

In order to retain backwards compatibility and avoid a breaking change, I made the decision to keep the existing content key on the root of Data when the user is not using the DropZone component.

Under the hood, the default drop zone (identified as puck-drop-zone) is no different to any other drop zone. It just stores its data under content, and there is some additional logic (mostly in the newly introduced reducer) to handle the relevant data operations.

Alternatively, I considered a breaking change, which would move the content underneath the dropzones key. Reusing our example above:

{
 "root": {},
 "dropzones": {
   "root:puck-drop-zone": [
     {
       "type": "Columns",
       "props": {
         "id": "Columns-0d9077e00e0ad66c34c62ab6986967e1ce04f9e4",
       },
     }
   ],
   "Columns-2d650a8ceb081a2c04f3a2d17a7703ca6efb0d06:column-1": [
     {
       "type": "Heading",
       "props": {
         "title": "Hello, world",
         "id": "Card-0d9077e00e0ad66c34c62ab6986967e1ce04f9e4",
       },
     },
   ],
 }
}

Data model changes might become more viable after #41, which proposes a mechanism for data migration that will automatically migrate any breaking data-model changes, making them non-breaking.

Rendering multiple root DropZones

If you render children in your root component, you'll render the default puck drop zone (puck-drop-zone). However, you can also choose to create multiple root drop zones.

import { DropZone } from "@measured/puck";

const config = {
  root: {
    render: () => {
      return (
        <div>
          <div>
            <DropZone dropzone="root-dropzone-1" />
          </div>
          <div>
            <DropZone dropzone="root-dropzone-2" />
          </div>
        </div>
      );
    },
  },
};

Results in the data

{
  "root": {},
  "content": [],
  "dropzones": {
    "root:root-dropzone-1": [],
    "root:root-dropzone-2": [],
  }
}

Note that, in this example, content is empty since we're not rendering the default dropzone via children.

As above, the content for the default DropZone will never appear in dropzones using this implementation, even if children are rendered. This can mean we have two ways of handling the root dropzone data - via content for the default root dropzone, and also via dropzones for any other DropZone defined at the root.

Questions

  1. Do we like the DropZone component name?*
  2. Should we use id, dropzone or dropZone for the DropZone identity prop?*
  3. Should we keep the proposed API, keeping the existing root and content fields in Data for the default dropzone, or merge the default DropZone behaviour into the dropzones key?*
  4. Should we rename dropzones to areas?*
  5. Should we change the way we key dropzones from {"areaId:dropzoneId": []} to nested objects like {"areaId": {"dropzoneId: []}}""? This may be cleaner from a data model point of view, but will require some rearchitecture.*
  6. Should we expose this API publicly, or keep it private (can be accessed via dist) for the time being?
  7. Should we release this as a canary build, or just regular build?
  8. Should we rename puck-drop-zone to default?

*Breaking changes

Tasks

  • Rename dropzone prop to dropZone or id

Future tasks

  • Add light documentation to README (before release)
  • Add detailed documentation (too verbose for README)
  • Support dragging between areas

Further examples

Dynamic columns

import { DropZone } from "@measured/puck";

const config = {
  Columns: {
    fields: {
      columns: {
        type: "array",
      },
    },
    defaultProps: {
      columns: [],
    },
    render: ({ columns }) => {
      return (
        <div>
          {columns.map((_, idx) => (
            <div>
              <DropZone dropzone={`column-${idx}`} />
            </div>
          ))}
        </div>
      );
    },
  },
};

@vercel
Copy link

vercel bot commented Sep 6, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
puck-demo ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 21, 2023 3:49pm

@ahmedrowaihi-official
Copy link

Can't wait for this to be merged
Anything to help here? This is exciting

@chrisvxd
Copy link
Member Author

Me neither @ahmedrowaihi-official! It's going to be a bit of a game-changer.

Regarding help - any feedback on the API would be great, if you have any?

Otherwise I just need some time to push it over the line. I hope by the end of this coming week!

@ahmedrowaihi-official
Copy link

ahmedrowaihi-official commented Sep 10, 2023

On this particular PR no, but I might do some refactoring as for imports/exports consistency as well as fixing packages resolutions for the whole puck packages

@chrisvxd
Copy link
Member Author

@ahmedrowaihi-official Sure! I'd consider waiting until this PR lands as it will touch a lot of that stuff.

@Danm72
Copy link

Danm72 commented Sep 10, 2023

Really nice UX

Copy link
Member

@monospaced monospaced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI QA and comments

  • Demo outputs register zone messages to console, intentional?
  • Expected the chevron here to indicate the item could be show/hide toggled. Wasn't immediately clear to me that ButtonGroup was a child of Flex (in fact, maybe it isn't?)
    Screenshot 2023-09-21 at 11 44 37
  • 🦊 Firefox
    • No parent > child highlighting, expected due to lack of :has support 👍🏻
    • 0.75 zoom effect on the preview doesn't work, expected since zoom is non-standard & not supported in FF
    • These two in combination do mean the FF UI is noticeably not as nice as in Chrome/Safari, especially on my small screen. But :has is coming 🙏🏻
  • 🧱 Demo components
    • Be good to have a way of making these Cards full height Screenshot 2023-09-21 at 12 12 30
    • Column spanning also doesn't do anything unless distribution="Manual" selected, which confused me for a minute.
    • I can get the Columns span selector in a weird state. In general I avoid type="number" cause of stuff like this Screenshot 2023-09-21 at 12 14 30
    • Also Flex minimum item width
    Screenshot 2023-09-21 at 12 26 10

zoom: 1.33;
position: relative;
height: 100%;
outline-offset: -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a px

@chrisvxd
Copy link
Member Author

chrisvxd commented Sep 21, 2023

  • Demo outputs register zone messages to console, intentional? Fixed
  • Expected the chevron here to indicate the item could be show/hide toggled. Wasn't immediately clear to me that ButtonGroup was a child of Flex (in fact, maybe it isn't?) This is another :has Firefox issue, but I've managed to address with JS.
  • 🦊 Firefox
    • No parent > child highlighting, expected due to lack of :has support 👍🏻 Agree, but think it's still usable. I believe we can get 100% FF coverage with same fix as above in follow-on
    • 0.75 zoom effect on the preview doesn't work, expected since zoom is non-standard & not supported in FF already tracked Firefox doesn't respect zoom #15
    • These two in combination do mean the FF UI is noticeably not as nice as in Chrome/Safari, especially on my small screen. But :has is coming 🙏🏻 Yes
  • 🧱 Demo components
    • Be good to have a way of making these Cards full height Non trivial, suggest we pick it up in separate issue
    • Column spanning also doesn't do anything unless distribution="Manual" selected, which confused me for a minute. Maybe we should call it "Manual span"?
    • I can get the Columns span selector in a weird state. In general I avoid type="number" cause of stuff like this We need min / max support for numbers. Suggest follow on.
    • Also Flex minimum item width as above

One more thing from me

  • Safari is constantly showing DropZone outlines on initial render, even if the DropZone has children Addressed
image

@monospaced
Copy link
Member

  • Be good to have a way of making these Cards full height Non trivial, suggest we pick it up in separate issue

  • Column spanning also doesn't do anything unless distribution="Manual" selected, which confused me for a minute. Maybe we should call it "Manual span"?

  • I can get the Columns span selector in a weird state. In general I avoid type="number" cause of stuff like this We need min / max support for numbers. Suggest follow on.

  • Also Flex minimum item width as above

@chrisvxd happy for all these to be spun out, certainly not blocking on this PR 👍🏻

@chrisvxd chrisvxd mentioned this pull request Sep 21, 2023
@chrisvxd
Copy link
Member Author

@monospaced Tracking all remaining tasks in #100.

@monospaced
Copy link
Member

@monospaced Tracking all remaining tasks in #100.

💯

Copy link
Member

@monospaced monospaced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎤 drop

@ahmedrowaihi
Copy link
Contributor

ahmedrowaihi commented Sep 21, 2023

[Updated Reply]:
nvm, I was confused by myself
looks good

@chrisvxd chrisvxd merged commit 85b14d2 into main Sep 22, 2023
2 checks passed
@chrisvxd chrisvxd deleted the nested-dropzones branch September 22, 2023 09:21
@larowlan
Copy link

Coming in late here, user of rbd - wondering how you managed to get the onMouseOver/onMouseOut events to fire whilst a drag was active.
In my use of rbd/testing - the events stop firing when I'm dragging.
But in the puck example, they keep firing.

@larowlan
Copy link

found it, pointer-events: all on the elements with the events

@chrisvxd
Copy link
Member Author

Ah nice one @larowlan - was meaning to reply and say pointer-events, but I couldn't remember which pointer-events definition!

@larowlan
Copy link

Thanks for following up ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for multi-column layouts
6 participants