Skip to content

Prevent layout shift as the editor mounts, for editors sized by their content - #1188

Open
sedubois wants to merge 12 commits into
basecamp:mainfrom
sedubois:prerender-content-element
Open

Prevent layout shift as the editor mounts, for editors sized by their content#1188
sedubois wants to merge 12 commits into
basecamp:mainfrom
sedubois:prerender-content-element

Conversation

@sedubois

@sedubois sedubois commented Jul 9, 2026

Copy link
Copy Markdown

Problem

Loading a page whose editor is sized by its content shows a layout shift as the editor mounts: the field paints at ~0px and jumps to its content height, shoving everything below it down the page.

This is the same symptom #1201 fixed for the default editor, in the one configuration that fix cannot reach.

Why #1201 doesn't cover this

#1201 reserves the mounted height on the host before upgrade:

:where(lexxy-editor:not(:defined)) {
  min-block-size: calc(var(--lexxy-toolbar-height) + var(--lexxy-editor-rows) + 2 * var(--lexxy-editor-padding));
}

That works whenever --lexxy-editor-rows is a length — including a per-instance override like the sandbox's 30lh. It cannot work for an editor that is sized by its content, for two independent reasons:

  1. The reservation evaluates to nothing. A content-sized field sets --lexxy-editor-rows: auto, and calc(30px + auto + 0px) is not a valid length, so the whole declaration is dropped and min-block-size stays 0. Verified in Chrome 150: with --lexxy-editor-rows: auto the computed min-block-size is 0px, where 8lh gives 246px.

  2. A row count is the wrong height anyway. These editors stand in for published copy in the page around them, so the height to reserve is this record's rendered height — 106px for one body, 35px for another — not N rows. Reserving a fixed number of rows would replace a shift down with a shift up.

Setting --lexxy-editor-rows: 0 (as an earlier revision of this PR suggested) does not rescue it either: the reservation then collapses to the toolbar height, which for these editors is out of flow.

Measured on a real app, with the custom-element registration deferred so the pre-upgrade window is observable:

Pre-upgrade host height Mounted height
Default editor (--lexxy-editor-rows: 8lh) reserved by #1201, matches matches
Content-sized editor (--lexxy-editor-rows: auto) 0px 35px / 106px, per record

Fix

Opt-in prerendering, so the reserved height is the record's own:

<%= form.rich_text_area :body, prerender: true %>

Rails renders the editor's current value as a static content element inside <lexxy-editor>:

<lexxy-editor value="&lt;p&gt;...&lt;/p&gt;">
  <div class="lexxy-editor__content"><p>...</p></div>
</lexxy-editor>

It also ships the toolbar itself, empty, ahead of that content element, and fills it in on connect rather than building a second one — so the reservation covers the whole mounted height, not just the body. That has to be a real <lexxy-toolbar> rather than a neutral spacer: whether a toolbar occupies space at all is the host's CSS to decide, and an inline editor floats it out of flow, where a spacer would reserve height nothing fills and shift the page up. No toolbar is emitted when the editor will not prepend one (rich text off, or a toolbar naming an element by id).

When Lexxy connects it adopts that existing .lexxy-editor__content instead of appending an empty one. Lexical still parses value as the source of truth and reconciles its state into the adopted node; the prerendered child only gives the browser the right layout before JavaScript finishes.

Off by default. Without prerender: true, and when callers pass an explicit block, the markup path is unchanged. It composes with #1201 rather than competing: #1201 reserves a height for editors that have one to reserve, this reserves this field's height for editors that do not.

Where the empty-editor window shows up

Cold load and reload, back/forward without a Turbo cache hit, server re-rendered forms, Turbo stream refreshes that morph the editor back toward server HTML, and any slow connection or heavy page — where the window lasts longest and the shift is most visible.

Reproduction

A browser fixture in this branch demonstrates the case:

VITE_PORT=5173 npx vite --config test/browser/vite.config.js

Then open http://localhost:5173/prerender.html?delay=1500. Two editors with identical content and content below them: the left has no prerendered child and the content below it jumps when delayed initialization runs; the right is prerendered and the content below it starts and stays put. The browser test pins this by measuring the following content before and after Lexxy connects.

prerender-layout-shift.mp4

Design notes

  • value remains the source of truth. Adoption reuses the DOM node; it does not trust the child markup as state.
  • The prerendered HTML is sanitized server-side with Action Text's display sanitizer, because it enters the DOM at first paint.
  • The server-rendered element is static markup only — no contenteditable, role or ARIA — until Lexxy adopts it, so the page does not advertise an editor that cannot yet accept input.
  • Both Rails integration paths are covered: the Rails 8.2 editor adapter and the older tag-helper fallback, sharing Lexxy::Prerender.

Known limitations

  • Attachments/images still use the editable representation before mount, so image-heavy documents may retain some shift until a follow-up prerenders display-height previews.
  • Empty-editor placeholder text is not shown before mount, though a blank value still prerenders the baseline editor body.
  • The content ships twice, in value and as a child element — only when opted in.

Status

Not blocking us. We ship the same idea app-side: the published copy is rendered as a static placeholder stacked in the same CSS grid cell as the editor, and CSS drops it once Lexical's mounted root appears. That works and is tested, so this PR is offered as the cleaner in-library version — adoption instead of a stacked placeholder, no :has() handoff rule for consumers to get right — not as something we need merged.

One gotcha worth recording either way, in case it informs the design: the handoff must key on Lexical's mounted root ([data-lexical-editor]), not on Lexxy's own connected attribute. connected is set at the end of connectedCallback but the root mounts a frame later, so handing off on connected collapses the field for a frame — the same layout shift, smaller. Adoption avoids the question entirely, which is one of the reasons this belongs in the library.

Testing

Rebased onto main at 41f776e (0.9.28 + #1201), conflict-free, and re-verified there:

  • yarn lint — clean.
  • yarn test — 128 unit tests, 19 files, all passing.
  • Full Playwright chromium suite — 625 collected, 622 passed, 2 flaky on retry and 1 conditional skip; the flaky ones vary run to run (paste/plain_text_paths, tables/toolbar, editor/leak) and are pre-existing and unrelated.
  • test/browser/tests/editor/prerender_adoption.test.js — 5 passing: the reproduction, stability with the toolbar floated and in normal flow, and that the prerendered toolbar is filled rather than duplicated.
  • test/helpers/prerender_test.rb — 6 runs, 19 assertions: rich_textarea_tag output, sanitization, blank values, and block precedence.

Worth noting for review: the reproduction still reproduces on top of #1201. The fixture's control editor is asserted to move by more than 60px as Lexxy mounts, and that assertion passes against current main — because with --lexxy-editor-rows: 0 the new reservation collapses to the toolbar height, which in this layout is out of flow. So the two changes do not overlap in practice.

Copilot AI review requested due to automatic review settings July 9, 2026 06:58

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in server prerendering path for Lexxy inline editors so the initial paint matches the editor’s eventual content height (reducing layout shift), and teaches the custom element to adopt the prerendered content node on connect.

Changes:

  • Add prerender: true option to Rails integrations (editor adapter + tag-helper fallback) to emit a sanitized static .lexxy-editor__content child inside <lexxy-editor>.
  • Update LexicalEditorElement to adopt an existing direct child .lexxy-editor__content element instead of always creating a new one.
  • Add Ruby + Playwright coverage and a dedicated browser fixture demonstrating height-hugging inline-editor behavior.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/elements/editor.js Adopts a server-rendered .lexxy-editor__content node (if present) and applies the interactive attributes on connect.
lib/lexxy/rich_text_area_tag.rb Adds prerender: option to the tag-helper fallback and emits prerendered content when opted in (and no explicit block is given).
lib/action_text/editor/lexxy_editor.rb Adds prerender: support to the Rails editor-adapter path by supplying a block that renders the prerendered content node.
lib/lexxy/prerender.rb Introduces shared server-side prerender helper that sanitizes and builds the static content element.
lib/lexxy.rb Requires the new prerender helper.
test/helpers/prerender_test.rb Verifies prerender output, sanitization, blank handling, and block precedence at the helper layer.
test/browser/fixtures/prerender.html Adds a browser fixture demonstrating “height-hugging” inline editor layout with/without prerender.
test/browser/tests/editor/prerender_adoption.test.js Adds Playwright coverage to ensure following content remains stable and prerendered content is adopted (not duplicated).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/lexxy/prerender.rb Outdated
Comment thread test/browser/tests/editor/prerender_adoption.test.js Outdated
Copilot AI review requested due to automatic review settings July 9, 2026 07:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread test/browser/tests/editor/prerender_adoption.test.js
Copilot AI review requested due to automatic review settings July 9, 2026 07:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread test/browser/tests/editor/prerender_adoption.test.js
Comment thread test/helpers/prerender_test.rb Outdated
Copilot AI review requested due to automatic review settings July 9, 2026 07:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@sedubois sedubois changed the title Prerender editor content to stabilize height-hugging inline editors Prevent layout shift as the editor mounts, for editors sized by their content Aug 2, 2026
@sedubois

sedubois commented Aug 2, 2026

Copy link
Copy Markdown
Author

Refreshed the description now that #1201 has landed, since that changes what this PR is asking for.

#1201 fixes the general case; this is the one configuration it structurally cannot reach. An editor sized by its content sets --lexxy-editor-rows: auto, which makes calc(… + auto + …) invalid — so the reservation is dropped entirely and min-block-size stays 0. Verified in Chrome 150: auto computes to 0px where 8lh gives 246px. And even a valid row count would be the wrong height for these fields, which stand in for published copy and must reserve that record's height (106px for one body, 35px for another), not N rows.

Also retitled to match #1201's vocabulary — the original title said "stabilize height-hugging inline editors" and never used the term layout shift, which made it read as a different concern than it is.

To be clear about priority: we are not blocked. We ship the same idea app-side (published copy as a static placeholder stacked in the same grid cell, dropped by CSS once Lexical's root mounts) and it works. This is offered as the cleaner in-library version — node adoption instead of a stacked placeholder, and no :has() handoff rule for consumers to get right. Happy to close it if you'd rather not carry the opt-in surface; happy to rebase onto 0.9.28 if it's worth a look.

sedubois added 10 commits August 3, 2026 06:13
`<lexxy-editor>` renders empty and builds its contenteditable a frame after
load, so an inline field paints at ~0 height and then jumps to full content
height, shifting everything below it. That reflow shows on every cold load,
reload, and server re-render.

Opt in with `rich_text_area(..., prerender: true)`: the helper renders the value
into a `.lexxy-editor__content` element inside `<lexxy-editor>`, and on connect
LexicalEditorElement adopts that element instead of creating an empty one.
Lexical reconciles its parsed state into the adopted element, so the field has
its final height from first paint and the live editor lands at the same height —
no shift. Absent the prerendered element the empty-editor path is unchanged, so
this is backwards compatible and off by default.

Verified against the shipped bundle: without adoption the editor builds a second
`.lexxy-editor__content` (the server one is orphaned and the field double-renders);
with it there is exactly one element and `setRootElement` reconciles into the
server node (it gains `data-lexical-editor`).

Follow-up: the general, visible-toolbar case still reflows by the toolbar's
height when it mounts; reserving that space server-side is left for a later change.
The prerender option only lived in the TagHelper fallback, which Rails 8.2+
never loads — it registers ActionText::Editor::LexxyEditor instead, so
`rich_text_area(..., prerender: true)` silently emitted the option as an HTML
attribute and the editor still rendered empty. Emit the prerendered content
element from the adapter Tag too, via the block Editor::Tag#render_in already
supports; an explicit caller block still wins.
… attributes

Two hardenings of the prerendered content element, extracted into a shared
Lexxy::Prerender used by both the editor-adapter Tag and the TagHelper fallback:

- Sanitize the value with Action Text's display sanitizer (whose allow-list this
  engine already extends). The attribute path is HTML-escaped and DOMPurify
  cleans it client-side before it reaches the live DOM, but the prerendered
  element enters the DOM straight from storage — unsanitized, a stored <script>
  or onerror handler would execute at first paint.

- Emit static markup only: no contenteditable, role or aria attributes until
  Lexical mounts. Before adoption they would advertise an editability that isn't
  there yet — keystrokes into the static element are discarded when the editor
  loads its state from the value attribute — and adoption already dresses the
  node with those attributes. The browser test now pins that.
Covers both integrations — the ActionText::Editor adapter and the TagHelper
fallback — since rich_textarea_tag dispatches to whichever is active and CI
runs both legs: off by default, the content element and its content, no
interactive attributes until adoption, sanitization of the injected HTML, the
blank-value paragraph, and an explicit block winning over the option.
Copilot AI review requested due to automatic review settings August 3, 2026 04:18
@sedubois
sedubois force-pushed the prerender-content-element branch from 285d517 to 2773949 Compare August 3, 2026 04:18
@sedubois

sedubois commented Aug 3, 2026

Copy link
Copy Markdown
Author

Rebased onto main at 41f776e (0.9.28, with #1201), 100 commits of drift, conflict-free. Re-verified there:

  • yarn lint clean, yarn test 128/128.
  • Full Playwright chromium suite: 622 passed, 2 flaky on retry (paste/plain_text_paths, tables/toolbar) — pre-existing and unrelated.
  • prerender_adoption.test.js 3/3, test/helpers/prerender_test.rb 6 runs / 19 assertions.

The thing I most wanted to check after #1201: the reproduction still reproduces. The fixture's control editor is asserted to move by more than 60px as Lexxy mounts, and that assertion passes against current main — with --lexxy-editor-rows: 0 the new reservation collapses to the toolbar height, which in that layout is out of flow. The two changes address disjoint configurations rather than overlapping.

One gap I'd flag honestly: the fixture exercises --lexxy-editor-rows: 0, while the sharper argument in the description is about auto — where calc(… + auto + …) is invalid so the reservation is dropped entirely. I verified that separately in Chrome 150 (auto0px, 8lh246px) but it isn't pinned by a test here. Happy to add that case to the fixture if you'd like it covered in-repo.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

test/browser/tests/editor/prerender_adoption.test.js:28

  • This assertion only rejects downward movement; a large upward layout shift produces a negative delta and still passes. Since the behavior under test is stability in either direction, assert the absolute displacement.
    expect(withAfter - withBefore).toBeLessThan(1)

lib/lexxy/prerender.rb:22

  • For a content-sized editor with the default toolbar, this still does not reserve the mounted height. When --lexxy-editor-rows: auto, the pre-upgrade host reservation is invalid, this child contributes only the body height, and #createDefaultToolbar() later prepends an in-flow toolbar, shifting following content by the toolbar height. The fixture masks this case by making its toolbar position: absolute. Either support/reserve the in-flow toolbar case or scope and document this option as requiring an out-of-flow toolbar, with a matching test.
      view.content_tag "div", html, class: "lexxy-editor__content"

Copilot AI review requested due to automatic review settings August 3, 2026 06:20
@sedubois

sedubois commented Aug 3, 2026

Copy link
Copy Markdown
Author

Both suppressed comments were worth acting on — the second one especially. Addressed in f8a19a1.

1. Signed vs absolute displacement (prerender_adoption.test.js:28) — correct, and a real hole: expect(withAfter - withBefore).toBeLessThan(1) passes comfortably for any upward shift. Now compares Math.abs(...).

2. In-flow default toolbar (prerender.rb:22) — correct, and I'd missed it. Verified rather than taken on faith: putting the fixture's toolbar back into normal flow and measuring the following content gives

before=441.875  after=482.875  shift=41  toolbarHeight=41

Exactly the toolbar's height. #createDefaultToolbar() does this.prepend(toolbar) during connectedCallback, so prerendering the content child reserves the body but not that element — and the fixture's position: absolute is indeed what hid it.

I took the "scope and document, with a matching test" option:

  • Lexxy::Prerender now states what it does and does not reserve, and why the distinction exists — the content height varies per record and so cannot be reserved by a CSS rule the way a fixed row count can, which is the whole reason this option exists; the toolbar is a fixed height and a different problem.
  • A new test puts the toolbar back in flow and pins the residual shift to exactly the toolbar's height, so the boundary is asserted rather than implied by the fixture's CSS.
  • The PR description now leads its limitations with this.

The reasoning for scoping rather than reserving: the editors this option is for float the toolbar out of flow, because an inline field standing in for published copy has nowhere to put one — our own app included. But that is a judgement about the use case, not a technical obstacle. If you'd rather prerender: true covered the in-flow toolbar too, say so and I'll add the reservation — a static spacer sized to --lexxy-toolbar-height, removed as the real toolbar is prepended, in the same task so nothing paints in between.

Full chromium suite after the change: 625 collected, 622 passed, 2 flaky on retry (varying, pre-existing), 1 conditional skip. yarn lint clean, test/helpers/prerender_test.rb 6 runs / 19 assertions.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated no new comments.

Two review points from Copilot on the rebased branch.

The stability assertion compared a signed delta against 1, so a large upward
shift would have passed as a comfortably negative number. Compare absolute
displacement.

The second is substantive and correct. Prerendering reserves the content
element's height, but #createDefaultToolbar prepends the default toolbar in
normal flow during connectedCallback, so an editor using it still gains that
height on mount — measured at 41px, exactly the toolbar's height, once the
fixture's out-of-flow positioning is removed. The fixture floats its toolbar, so
nothing exercised this.

Scope it explicitly rather than leave it implied: document what the option does
and does not reserve, and add a test that puts the toolbar back in flow and pins
the residual shift to the toolbar's height. The editors this option is for float
the toolbar out of flow — an inline field standing in for published copy has
nowhere to put one — but a reader should not have to infer that from the
fixture's CSS.
…itor

Prerendering reserved the content element's height but not the toolbar's, so an
editor using the default in-flow toolbar still moved the page by exactly that
element's height on mount — 41px, measured with the fixture's out-of-flow
positioning removed. The option is meant to mean "this editor will not shift",
so make it true rather than document the gap.

Ship the toolbar itself, empty, ahead of the content element, and fill it in on
connect instead of building a second one. It has to be a real <lexxy-toolbar>
rather than a neutral spacer: whether a toolbar takes space at all is the host's
CSS to decide, and an inline editor floats it out of flow. A spacer sized from
--lexxy-toolbar-height would reserve height nothing fills there and shift the
page *up* — which is exactly what the first attempt did, caught by the existing
stability test.

Two details worth keeping:

- The reservation is a border-box one. --lexxy-toolbar-height already counts the
  toolbar's padding and border, and the toolbar is content-box, so applying it
  as a plain min-block-size added both a second time and left 5px to give back.
- Only reserve a toolbar the editor is actually going to prepend: rich text off,
  or a `toolbar` naming an element by id, means none arrives, and reserving then
  is the same defect upwards. The client drops a prerendered toolbar it turns out
  not to want, in the same task as connect, so nothing paints in between.

Tests: the fixture keeps its floated toolbar, so the existing stability test now
covers "reserves nothing when the toolbar takes no space"; a new one puts the
toolbar back in normal flow and requires the same stability; a third pins that
the prerendered toolbar is filled rather than duplicated. Ruby-side, the emitted
toolbar and each of the three opt-out conditions.
@sedubois
sedubois force-pushed the prerender-content-element branch from f8a19a1 to 99e221e Compare August 3, 2026 06:43
Copilot AI review requested due to automatic review settings August 3, 2026 06:43
@sedubois

sedubois commented Aug 3, 2026

Copy link
Copy Markdown
Author

Implemented the in-flow toolbar reservation in 99e221e, so prerender: true now means the editor does not shift, unconditionally — no scoping caveat.

The editor ships its toolbar too, empty, ahead of the content element, and fills it in on connect instead of building a second one. It has to be a real <lexxy-toolbar> rather than a neutral spacer: whether a toolbar takes space at all is the host's CSS to decide, and an inline editor floats it out of flow — a spacer sized from --lexxy-toolbar-height reserves height nothing fills there and shifts the page up. My first attempt did exactly that, and the existing stability test caught it.

Two details that took measuring:

  • The reservation has to be border-box. --lexxy-toolbar-height already counts the toolbar's padding and border, and the toolbar is content-box, so a plain min-block-size added both a second time — 46px reserved against 41px filled, giving 5px back on connect.
  • Only reserve a toolbar that will actually arrive. Rich text off, or a toolbar naming an element by id, means none is prepended; reserving then is the same defect upwards. The client also drops a prerendered toolbar it turns out not to want, in the same task as connect.

Tests: the fixture keeps its floated toolbar, so the original stability test now also covers "reserves nothing when the toolbar takes no space"; a new one puts the toolbar back in normal flow and requires the same stability; a third pins that the prerendered toolbar is filled rather than duplicated. Ruby-side, the emitted toolbar and each opt-out condition.

Full chromium suite 622 passed / 2 flaky (pre-existing, varying), yarn lint and rubocop clean, Ruby prerender tests 8 runs / 24 assertions.

Separately — apologies for the noise in the previous push: it briefly added a package-lock.json and rewrote yarn.lock because I ran npm install in a yarn repo. Both are gone; the branch is back to the eight files the change is actually about.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

lib/lexxy/prerender.rb:56

  • lexxy_rich_textarea_tag symbolically normalizes its options, so the valid "rich-text": false option arrives here as :"rich-text". This lookup misses that key, prerenders a toolbar, and the client then removes it because rich text is disabled, reintroducing an upward layout shift. The current test uses rich_text: false, which does not produce the rich-text attribute the client reads; please cover the hyphenated option too.
      rich_text = options.fetch(:rich_text) { options.fetch("rich-text") { options["rich_text"] } }

app/assets/stylesheets/lexxy-editor.css:428

  • The reservation is intended only for the newly generated placeholder, but this selector applies it to every unupgraded toolbar. A pre-existing custom child toolbar can therefore be forced to the default toolbar height before upgrade and shrink when :defined starts matching, introducing CLS for editors that did not opt into prerendering. Scope the rule to the data-prerendered marker emitted by toolbar_placeholder_tag.
:where(lexxy-toolbar:not(:defined)) {

lib/lexxy/prerender.rb:62

  • This decision cannot actually mirror the client when toolbar/rich-text behavior comes from the documented Lexxy.configure defaults or a named preset. For example, an editor using a preset with richText: false has no corresponding helper option here, so the server reserves a toolbar and #discardUnusedPrerenderedToolbar removes it on connect, causing the shift this option is meant to prevent. The API needs a reliable server-side signal for whether to reserve the toolbar (or this limitation must be avoided by not reserving it implicitly), with a browser test for configured presets/defaults.
      return false if [ false, "false" ].include?(rich_text)
      return false if [ false, "false" ].include?(toolbar)
      return false if toolbar.is_a?(String)

      true

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.

2 participants