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

Global Styles Sidebar (Design/Code V2) #293

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

ItsJonQ
Copy link
Owner

@ItsJonQ ItsJonQ commented Mar 23, 2021

Prototype Links


A revisited attempt to constructing the Global Styles sidebar experience:
WordPress/gutenberg#27473

Created this draft PR to have auto deployed previews 🎉

cc'ing @mtias @pablohoneyhoney @sarayourfriend


Screenshot of Mockups

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

@ItsJonQ ItsJonQ self-assigned this Mar 23, 2021
@vercel
Copy link

vercel bot commented Mar 23, 2021

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
Copy link
Owner Author

ItsJonQ commented Mar 23, 2021

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:

  • Navigator
  • ListGroup
  • ColorPicker
  • SegmentedControl

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Mar 23, 2021

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

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Mar 24, 2021

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
Copy link
Owner Author

ItsJonQ commented Mar 24, 2021

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

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Mar 24, 2021

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

* Add Typography tools

* Add support for Typography options
@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Mar 25, 2021

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:

  • null = Show control, but currently has no value (or enabled)
  • undefined = Hide control (or disabled)

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!!

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

Successfully merging this pull request may close these issues.

1 participant