Open ItsJonQ opened 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
The prototype code is located here:
packages/components/src/__fixtures__/GlobalStylesSidebarV2/
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
.
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.
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.
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:
Just added a URL sync feature so that changes in the Sidebar screens (routes) is bookmarkable/sharable.
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.
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.
cc'ing @griffbrad (Who may be interested in progress 🎉 )
Screencast of latest(ish) updates: https://www.loom.com/share/2cda11bb95174420b20e82ca96cf0892
I also recently added the bones for the Typography section:
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).
get
/set
/toggle
callbacks to pass in values to the controlshow
/hide
.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.
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.
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.
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={[...]} />
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>
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!!
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