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

Component-based toolbar customisation API #3067

Merged
merged 9 commits into from
Mar 12, 2024
Merged

Component-based toolbar customisation API #3067

merged 9 commits into from
Mar 12, 2024

Conversation

SomeHats
Copy link
Contributor

@SomeHats SomeHats commented Mar 4, 2024

When we went from overrides-based to component based UI customisation APIs, we didn't do the toolbar because it had some significant extra complexity around overflowing the contents of the menu into the dropdown. This is really hard to do at render-time with react - you can't introspect what a component will return to move some of it into an overflow.

Instead, this diff runs that logic in a useLayoutEffect - we render all the items into both the main toolbar and the overflow menu, then in the effect (or if the rendered components change) we use CSS to remove the items we don't need, check which was last active, etc. Originally, I wasn't really into this approach - but i've actually found it to work super well and be very reliable.

Change Type

  • major — Breaking change
  • dependencies — Changes to package dependencies1
  • documentation — Changes to the documentation only2
  • tests — Changes to any test code only2
  • internal — Any other changes that don't affect the published package2
  • I don't know

Test Plan

  1. Test the toolbar at many different sizes with many different 'active tools'

Footnotes

  1. publishes a patch release, for devDependencies use internal

  2. will not publish a new version 2 3

@huppy-bot huppy-bot bot added the major Increment the major version when merged label Mar 4, 2024
Copy link

vercel bot commented Mar 4, 2024

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

Name Status Preview Updated (UTC)
examples ✅ Ready (Inspect) Visit Preview Mar 12, 2024 4:20pm
1 Ignored Deployment
Name Status Preview Updated (UTC)
tldraw-docs ⬜️ Ignored (Inspect) Visit Preview Mar 12, 2024 4:20pm

@steveruizok
Copy link
Collaborator

First thought before jumping in, I thought we could use two groups here rather than one:

image

My hunch was that we could remove a lot of the complexity while still keeping the behavior that we want and keeping the toolbar pretty open to customization. This would remove a lot of the complexity here (including the CSS manipulation, which makes me a little nervous).

function DefaultToolbar() {
  return (
    <div>
      <MainToolbarButtons><ToolbarContent/></MainToolbarButtons>
      <OverflowToolbarButtons><ToolbarContent/></OverflowToolbarButtons>
    </div>
}

I also would like us to use the same menu concepts as elsewhere, and I think we could more easily if those two things were managed independently.

@SomeHats
Copy link
Contributor Author

SomeHats commented Mar 5, 2024

Happy to talk through what you're unsure about with the CSS - afaict, there's not a way of avoiding doing something DOM-based like this if we want to drive the API through react components rather than e.g. an items array (which would be another option)

For me, the main thing I want to avoid is making consumers have to manage the overflow logic themselves if they want to do custom tools. I don't understand how the two separate groups thing avoids that - the overflow group needs to know how many items are visible in the main group, right? I'm probably missing something from yr description tho

@steveruizok
Copy link
Collaborator

I don't understand how the two separate groups thing avoids that - the overflow group needs to know how many items are visible in the main group, right? I'm probably missing something from yr description tho

I think no, it would just use the opposite logic as relates to the breakpoint! i.e. the toolbar might have "show only the first five at breakpoint 4" and the overflow menu would have "show all but the first five at breakpoint 4".

@steveruizok
Copy link
Collaborator

I have a branch that works for what I expected it to work for, but the "last selected shape" is beyond me. We can review together to see if you have any ideas, otherwise I think your approach is the best option.


export function OverflowingToolbar({ children }: { children: React.ReactNode }) {
const editor = useEditor()
const id = useId().replace(/:/g, '_')
Copy link
Member

Choose a reason for hiding this comment

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

why the replacement here? i see we do it in ArrowShapeUtil - should there be a util function safeId or something like that? hard to know why this is necessary on first read

</div>
{totalItems > overflowIndex && (
<IsInOverflowContext.Provider value={true}>
<TldrawUiDropdownMenuRoot id="toolbar overflow" modal={false}>
Copy link
Member

Choose a reason for hiding this comment

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

i know we have this in a couple places in the codebase but it feels odd to have a space in the id - i know you're not doing this but if there was ever a css selector rule targeting this you wouldn't be able to do it. maybe a bit of pedanticism on my part ¯_(ツ)_/¯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeahhh, this is just copied from what was here before. confusingly, this isn't actually a DOM ID at all - it's used to identify this menu in our weird isMenuOpen system

if (lastActiveOverflowItem) {
showInMainSelectors.push(`[data-value="${lastActiveOverflowItem}"]`)
} else {
showInMainSelectors.push(`:nth-child(${overflowIndex + 1})`)
Copy link
Member

Choose a reason for hiding this comment

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

i'm curious: is there a way to do this without the dynamic css?
like, can there be a demarcated element with a className that looks at a boundary or some other kind grouping?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i don't think so? steve's branch was trying that - we can sort of make it work for truncating the list, but not really for the active item which is pretty dynamic. open to suggestions tho!

if (!mainToolsRef.current) return

const mutationObserver = new MutationObserver(onDomUpdate)
mutationObserver.observe(mainToolsRef.current, {
Copy link
Member

Choose a reason for hiding this comment

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

oo fancy!

Copy link
Member

Choose a reason for hiding this comment

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

i'm obligated to put this here:
mutation_observer

@SomeHats SomeHats enabled auto-merge March 12, 2024 16:11
@SomeHats SomeHats added this pull request to the merge queue Mar 12, 2024
Merged via the queue into main with commit adebb68 Mar 12, 2024
9 of 10 checks passed
@SomeHats SomeHats deleted the alex/toolbar-api branch March 12, 2024 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
major Increment the major version when merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants