ItsJonQ / g2

✨ An experimental reimagining of WordPress components
http://g2-components.com/
MIT License
105 stars 12 forks source link

Global Styles Sidebar (Design/Code V2) #293

Open ItsJonQ opened 3 years ago

ItsJonQ commented 3 years ago

Prototype Links


A revisited attempt to constructing the Global Styles sidebar experience: https://github.com/WordPress/gutenberg/issues/27473

Created this draft PR to have auto deployed previews 🎉

cc'ing @mtias @pablohoneyhoney @sarayourfriend


Screenshot of Mockups

110799320-54c95100-827b-11eb-9db0-e768a6d31a12

vercel[bot] commented 3 years ago

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/itsjonq/g2/FPBTZNhJqJNkPeK4fANsTaZ8iEfi
✅ Preview: https://g2-git-try-global-styles-sidebar-v2-itsjonq.vercel.app

ItsJonQ commented 3 years ago

The prototype code is located here: packages/components/src/__fixtures__/GlobalStylesSidebarV2/

🧭 Routes

The idea behind the project structure is to construct it like a typical router-driven React application. This is powered by the Navigator component, which is based largely on react-router.

📱 Screens

Continuing with the router paradigm, individual "views" are labelled as "screens" - borrowing a concept from mobile development. These screens are individual components that are coordinated through the main Route / Switch.

Not prototyped... In the eventual implementation, these screens may have individual loading/fetching states. In case they need to retrieve data from the global WP data store.

♻️ Get/Set

When rendering values from the store (e.g. paragraph color), I think it's profoundly important that values can be elegantly retrieved and updated as they are bound to controls.

We need to figure out a code pattern to support this.

Otherwise, it will get exponentially messy as we attempt to verbosely deconstruct/stitch together getter/setter values. Not unlike how attributes and setAttributes works now within blocks.

📦 Components

Part of this effort is to create/consolidate Components that's required to construct this new experience. List comes to mind, which is currently being built in Gutenberg as ItemGroup (name is TBD).

Other component refinements that come to mind would be:

ItsJonQ commented 3 years ago

Just added a URL sync feature so that changes in the Sidebar screens (routes) is bookmarkable/sharable.

ItsJonQ commented 3 years ago

Potential (Navigator) Router conflicts

At the moment, any instance of Navigator will share the same context - the "brains" to coordinate the routing/navigation mechanics of the component.

I suspect we may run into issues if there are multiple nested Navigator components together. For context, Navigator is basically the framework for creating generic (route-powered) navigation. It doesn't aesthetically resemble a sidebar, a carousel, etc... It's the framework that can be used to create any of those things.

In the case of Global Styles, we may have a carousel-like component rendering in the Sidebar. And maybe this carousel component uses Navigator. In this scenario, we may have conflicts.

Solution: createNavigator?

Looking at the world of React Native, @react-navigation has a really nice (and fundamental) solution for this. They have a createStackNavigator function, which generates the (pre-bound) contexts and components: https://reactnavigation.org/docs/hello-react-navigation#creating-a-stack-navigator

In other words, we may need a (factory?) function to createNavigator()

Something like this:

const GlobalStylesNavigator = createNavigator()
const { Navigator, NavigatorScreens, NavigatorScreen, useNavigator, ...theRest } = GlobalStylesNavigator

With the above implementation, we can have multiple Navigator-powered components living in harmony.

ItsJonQ commented 3 years ago

cc'ing @griffbrad (Who may be interested in progress 🎉 )

ItsJonQ commented 3 years ago

Screencast of latest(ish) updates: https://www.loom.com/share/2cda11bb95174420b20e82ca96cf0892

I also recently added the bones for the Typography section:

Screen Shot 2021-03-24 at 2 55 18 PM
ItsJonQ commented 3 years ago

Control <-> Options

Screen Capture on 2021-03-25 at 16-00-25 (1)

Part of this new design introduces a new interaction to the sidebar controls. This dropdown consolidates both a "reset" mechanic and show/hide into a single interaction.

If we are to adopt this pattern to other style attributes, we have to come up with a solid (code) design pattern for how to handle this. From my experiments, the most consistent ways I've found to do this resolves around a handful of code patterns (Note: There may be better ones out there).

  1. Rely heavily on abstracted get/set/toggle callbacks to pass in values to the control
  2. Render the control is a wrapper that can automatically handle show/hide.
  3. Using a null value for "show, but not set", and undefined for "hide"

Assuming we have something like this as the styles shape for a typography element:

{
  fontSize: '13px'
}

We'll walk through how we may connect that value with the UI with the design concepts above. Note: The follow code examples will be high level pseudo code.

get, set

The UI component may look like this:

<FormGroup label="Font size">
  <UnitInput value={fontSize} onChange={handleOnChangeFontSize} />
</FormGroup>

The challenge is getting (or creating) the fontSize value and handleOnChangeFontSize callback.

Instead of manually destructuring (nested) objects and manually creating callbacks per style value, maybe we can have something like this:

const [fontSize, setFontSize] = useStyleValue('typography.elements[0]')

<FormGroup label="Font size">
  <UnitInput value={fontSize} onChange={setFontSize} />
</FormGroup>

Borrowing from react-use-gesture, maybe it could be simplified to just this:

const bind = useStyleValue('typography.elements[0]')

<FormGroup label="Font size">
  <UnitInput {...bind()} />
</FormGroup>

Note: This would only work with the most straight-forward use-cases.

Render

With out value and onChange bound to the UI control, we now need to conditionally render the <FormGroup /> based on whether or not the style attribute is either enabled or set.

Instead of wrapping each UI chunk with something like:

{fontSize !== undefined && (
  <FormGroup>
    ...
  </FormGroup>
)}

Maybe we can do something like this:

<RenderControl prop="fontSize">
  <FormGroup>
    ...
  </FormGroup>
</RenderControl>

And the RenderControl renders the content depending on whether the prop is enabled / set.

Dropdown / Options

Screen Shot 2021-03-25 at 4 52 51 PM

The dropdown of display options synchronize with the controls presented below. These items will always show. The only thing that visually changes is the ✅ that indicates whether the controls is available or not.

With the current implementation of <DropdownMenuItem />, we just need to pass in a boolean value for isSelected:

<DropdownMenuItem isSelected={isSelected} />

If we're going to be using this as a common design pattern, we'll need to generalize it enough so that we can do something like this:

<Dropdown>
  <DropdownTrigger
    icon={<FiMoreHorizontal />}
    isControl
    isSubtle
    size="small"
  />
  <DropdownMenu>
    {options.map((option) => (
      <DropdownMenuItem {...option}>
        {option.label}
      </DropdownMenuItem>
    ))}
  </DropdownMenu>
</Dropdown>

Or better yet, something like this:

<DisplayOptions options={[...]} />

toggle

This happens when clicking an item from the display options control.

If we assume:

We can basically toggle the value like:

disable = undefined
enable = defaultValue || null

For the DropdownMenuItem, we should be using some sort of abstracted function as the callback. Something like this:

<DropdownMenuItem onClick={toggleAttribute('fontSize')} {...options}>
  Font size
</DropdownMenuItem>

defaultValue

Establishing a defaultValue of some kind is important. This is not the same as initial value for a control. The defaultValue (as I'm describing it) is the value that would be toggled from disabled to enabled. For example, from undefined (disabled) to 13px (defaultValue).

Ideally, style attributes would have some sort of defaultValue. If not, we can use null.


I know all of this is very abstract.

I think it would help by poking at the prototype: https://g2-git-try-global-styles-sidebar-v2-itsjonq.vercel.app/iframe.html?id=examples-wip-globalstylessidebarv2--default&viewMode=story&gssb=%252Ftypography%252Felements%252Fheadings

And checking out the rough code implementation: https://github.com/ItsJonQ/g2/blob/try/global-styles-sidebar-v2/packages/components/src/__fixtures__/GlobalStylesSidebarV2/screens/TypographyElementScreen/TypographyElementScreen.js

Hope this helps!!