Skip to content

v2.0.0

Choose a tag to compare

@stamat stamat released this 03 Aug 11:01
· 23 commits to main since this release

Added

  • A sample can be several fences — markup, its css, its js — and each becomes a tab.
    Until now the element took one <pre><code>, so a demo that needed a stylesheet or a
    script had to bury both inside the html as <style> and <script>: unreadable as a
    sample, uneditable as css, and impossible to copy the interesting half out of. Write
    them as the separate blocks they are and the element pairs them up:

    <code-preview css="dist/lib.css" js="dist/lib.js">
      <pre><code class="language-html">&lt;aside class="drawer"&gt;…&lt;/aside&gt;</code></pre>
      <pre><code class="language-css">.drawer { transition: transform 0.2s; }</code></pre>
      <pre><code class="language-js">document.querySelector(".drawer");</code></pre>
    </code-preview>

    The languages are read off the language-* class a site generator already writes, so
    there is no new markup vocabulary — three fences in the markdown, three tabs on the page.
    Anything the frame cannot run (a scss block beside the css it compiles to) still gets a
    tab, read-only. So does any fence beside a sample that is a whole document — it owns its
    head and body, so there is nowhere in it to write the pane — and a second fence in a
    language that already has one, under a numbered tab (CSS2), since the frame is built
    from the first.

    The js pane is inlined as <script type="module">, and that is not about scoping. A
    classic inline script runs while the parser is still going, before the deferred bundles
    in js have defined anything — so a sample that writes a property on a custom element
    gets one that has not upgraded, and the write installs an own property that shadows the
    accessor the class is about to bring. It fails silently and for good. A module is
    deferred, and deferred scripts run in document order, so the pane runs after every url in
    js.

    A css edit no longer reloads the frame. The pane is one <style> in a head this
    element built, so an edit is a write to its text: nothing reparses, and the sample keeps
    the state a rebuild would cost it — a script's variables, an open menu, the control the
    reader had focused. Markup edits patch or reload exactly as before, and a js pane always
    reloads, for the reason js urls always have.

    DOM it produces: each pane's box gets role="tabpanel", data-pane="<name>" and
    hidden="until-found" while it is not showing; the strip is a role="tablist" of
    .code-preview-tab buttons in the existing .code-preview-bar. The markup pane is named
    code, not html — so tab="code" still means the sample, and every page already using
    this element is untouched. One fence still produces exactly what it did: no strip, no
    role, no hidden, byte for byte.

    CSS an author may be targeting: the rule that collapsed the hidden pane was
    code-preview.is-tabbed[tab="options"] > :is(pre, .code-wrap) and is now keyed off
    [data-pane][hidden], which is one rule for two panes or for five. The editor's keyboard
    hint is hidden by code-preview.is-tabbed:not(.is-code-pane) rather than by naming the
    options tab.

  • no-edit can lock some panes and not others. It was all-or-nothing, which for a
    three-fence sample meant choosing between an editable stylesheet you did not want touched
    and no editing at all. Two ways to say it, and they add up:

    <code-preview no-edit="css js"></code-preview>
    <pre no-edit><code class="language-css">.drawer { transition: transform 0.2s; }</code></pre>

    Panes are named by what their tab says (html, css, js) or by the pane's own name
    (code for the markup one). In markdown the per-fence form needs no new vocabulary if
    your generator turns a bare word in the info string into a class on the block — ```css no-edit — since that class is what the element reads. Bare no-edit is unchanged:
    the whole sample stays read-only.

    CSS an author may be targeting: code-preview.is-code-pane now means the pane
    showing has an editor in it, not merely code — a pane locked by either form no longer
    gets the class, so the buttons and the keyboard hint are not left on a block nobody can
    type into.
    Unchanged for a sample that locks nothing.

  • Editing is opt-in: a block takes no keystrokes until you open it. A block that can be
    edited is not editable at rest — no contenteditable, nothing announced as a text field.
    An Edit button in its bottom-left corner opens it, Enter on the focused
    block does the same, and Esc or a second press on the button closes it again.

    The reason is Tab. Tab has to indent inside a code editor, so it cannot also be the way
    out — which makes an always-editable block a keyboard trap sitting in a docs page, hit by
    every reader tabbing past a sample they never meant to type into, with the way out being
    a key they are told about only once they are already stuck. Opting in removes the trap
    rather than signposting it, and the Esc advice is then owed only to someone who
    asked to be there. The block keeps a tab stop at rest so that Enter has
    somewhere to be pressed: a keyboard user is offered the editor where they already are.

    Closing the editor is also a second way to apply a js edit, alongside Run.

    DOM the element produces: a div.code-preview-actions (role="group") as a child of
    the host — not of the strip, and not of the code block — holding
    button.code-preview-action: .code-preview-edit (with aria-pressed) and
    .code-preview-run. Each holds an aria-hidden glyph and a <span> with the word for it
    Edit, Run — and that word is the accessible name, so neither carries an
    aria-label or a title. The <pre> of an editable pane now carries tabindex="0" and an
    aria-describedby pointing at p.code-preview-hint; role="textbox",
    aria-multiline, aria-keyshortcuts and contenteditable are written on the <code>
    only while the editor is open, and removed when it closes.

    Both buttons are on by default and neither is built on a sample with no editor in it.
    no-actions takes them away, spelled the way no-edit is: bare for both, or naming the
    one to drop (no-actions="run"). Dropping Run from a js sample leaves
    Ctrl/Cmd + Enter and closing the editor as the ways to
    apply an edit — both are keyed to what the sample is, not to whether the button exists.

    The Esc hint is not drawn under (hover: none) and (pointer: coarse): it names
    a key a touch device does not have, and the trap it warns about is a keyboard trap. Its
    aria-describedby is unaffected.

    CSS an author may be targeting: code-preview.is-editing is new and says the editor
    is open. .is-editable still says the element has one to open. The --code-preview-hint-space
    bottom padding on an editable block is now the room the buttons sit in as well as the hint.

  • No copy button of the element's own, and a docs theme's is left alone. Copying a code
    block is something a docs theme already does, on every block on the page rather than only
    on the samples. The element's own copy button is gone and so is the rule that hid the
    theme's.

    CSS an author may be targeting: code-preview :is(pre, .code-wrap) > button { display: none }
    no longer ships. A theme that was relying on the element to hide its button gets it back;
    the display: revert override some pages added for exactly that is now a no-op and can go.
    .code-preview-copy, .code-preview-note, .code-preview-icon-copy and
    .code-preview-icon-check no longer exist, and no-actions="copy" names nothing.

  • The sample's console, under the preview. console.log, info, warn, error and
    debug from inside the frame land in a strip directly under the preview — on screen
    while the reader types the js that causes them, which a tab of its own could not be.
    The strip appears with the first line and costs nothing before it: no box, no reserved
    height. It holds the last hundred lines, follows the tail unless the reader has
    scrolled up to read, and starts over when the frame rebuilds — a new document is a new
    run, the same bargain the event counts make. A patched frame keeps its document and so
    keeps its log. Lines still reach the browser's own console.

    The capture is an inline script written first into the frame's head, ahead of every
    deferred js url and of the sample's own module — so a top-level console.log on the
    first run is caught, which wrapping the console from the host on the frame's load event
    would miss: load fires after the sample has already said the interesting thing. Each
    call is forwarded to the host as a code-preview-log CustomEvent on the iframe.
    Values are formatted without instanceof — the frame is another realm, where its
    Element and Error are different classes — so an element prints as <tag>, an
    error as name: message, the rest as JSON where JSON can say it.

    no-console on the element turns it off, hook and all, for a sample that logs on
    every frame. A whole-document sample owns its head and gets no hook. Uncaught errors
    stay the error banner's job.

    DOM the element produces: div.code-preview-console[role="log"] between the
    viewport and whatever sits below, once something has logged; p.code-preview-console-line
    per line, with .is-warn/.is-error by level. --code-preview-console-height
    (default 10rem) caps the strip.

    Contents of the preview iframe: one inline <script> first in head, rewiring the
    console. A sample asserting on its document's first script will see this one.

Changed

  • The text that is code waits for a Run button; everything else applies as you type.
    The 600ms reload debounce is gone — it was never the right tool. Markup and css are
    inert and keep the 250ms live path. Two edits are not: the js pane's own text, and
    markup carrying an inline <script> — a single-fence js demo is exactly that. Those
    apply on Run or on Ctrl/Cmd + Enter and not
    before, because a srcdoc frame is same-origin and shares the page's event loop:
    half-typed js, while (true with the closing paren still to come, hangs the whole tab
    and not just the preview. A longer debounce only decides how long the reader gets first.

    A sample that runs js it is not typing — a js asset, the reload attribute — still
    follows the typing: the rebuild re-runs that js from its own file, complete and valid,
    never mid-statement. What the rebuild costs is the sample's live state, which is the
    price of a preview that moves while the reader types markup; where nothing ran at all,
    markup edits patch and cost nothing, as before.

    Run is always live and has no edited state. It re-runs the sample from whatever the
    blocks say when it is pressed, so pressing it on a sample nobody has touched still starts
    the demo over — the counter back to zero, the animation from the top. A button that greys
    itself out between edits is one whose job the reader has to keep track of. And it appears
    only where edits wait on it: on the js tab, or on a lone fence carrying its own
    <script>. Everywhere else it is not shown — edits there apply themselves, and a button
    with nothing to do is one the reader has to wonder about. It is also a plain button now,
    the same size and color as Edit: appearing at all is its statement, and the accent fill
    it had on the js tab said that twice. Edit's pressed fill stays — that one is a toggle.

    A whole-document sample is rebuilt rather than patched, as it always was, but that is a
    different question: with no script in it there is nothing to execute, so it keeps the
    live typing. Turning an options panel knob is exempt and applies immediately, as it did.

    DOM the element produces: button.code-preview-run only on samples with an editor
    whose text is code — a js pane, or markup with an inline <script> — and it carries no
    disabled state of any kind. A sample that merely reloads (js asset, reload) builds
    no Run button at all; Ctrl/Cmd + Enter still restarts
    it from the editor.

    CSS an author may be targeting: editable blocks gained
    aria-keyshortcuts="Escape Control+Enter Meta+Enter", where it was Escape alone.
    .is-js-pane now means "the pane showing is one whose edits wait on Run" — the js tab,
    or a single fence carrying its own <script> — and is what shows the Run button;
    without it the button is display: none. The accent rules on
    .is-js-pane .code-preview-run are gone.

  • The tab strip moved above the code, and the toolbar split in two. The tabs and the
    new actions belong to the code — a tab strip has to sit against the thing it labels to
    read as its label — so they are now in their own strip between the preview and the panes.
    The width switcher stays above the preview it re-renders. Both are still
    .code-preview-bar; which one is which is decided by the side of the viewport they are
    on, and the stylesheet already had the rules for both.

    DOM the element produces: two .code-preview-bar strips where there was one, each
    built only when it has something to hold — the code's when the tabs are coming, the
    preview's with viewport-widths. A single-fence sample has neither. toolbar still means
    the preview's strip; the new codeBar is the code's.

    CSS an author may be targeting: the height reserved before upgrade counts each strip
    the markup can be seen to be asking for, through a new internal --code-preview-bars.
    --code-preview-bar-height is unchanged and still the knob. .code-preview-widths no
    longer carries margin-inline-start: auto — it is alone in its strip now.

  • The tab strip moved from the options bundle into the element. It was built by
    code-preview-options.js, which could only ever know about two panes. addPane() is the
    contract now, and the options panel is one caller of it — so the roving tabindex, the
    APG arrow keys, the beforematch handling and the focus rescue are written once and are
    the same for two panes or five. No markup changes; the options panel behaves as it did.

  • The edit mode follows the reader across tabs. Switching panes used to close the
    editor outright; now the reader is editing the sample, not one block, so moving from the
    markup tab to the css tab closes the hidden pane's editor and opens the new pane's own
    in the same gesture. Focus stays where the switch put it — on the tab a click pressed,
    or mid-flight along the strip on arrow keys — rather than being pulled into the block.
    A pane with no editor (the options panel, a read-only fence) still closes the mode, and
    Esc and the Edit toggle still end it from anywhere.

  • Read-only panes no longer reserve the button strip. The bottom padding that makes
    room for Edit, Run and the keyboard hint (--code-preview-hint-space) is only held on
    blocks that have an editor behind the button — a no-edit sample, a scss fence or a
    numbered duplicate shows none of that furniture, and reserving a strip of nothing at
    the bottom of it was dead space.

    CSS an author may be targeting: the padding rule is now keyed off
    .is-editable:not(.is-tabbed) for the lone block and .is-code-pane in a strip,
    instead of .is-editable alone.

Fixed

  • A script error is announced, not just drawn. The error banner was CSS generated
    content (::after reading data-error), which changes silently — a screen-reader user
    who typed the edit that threw heard nothing. It is a real element with role="alert"
    now, so the moment it appears is a moment assistive tech reports; the message is also
    selectable at last, and an error message is the one string worth copying into a search.

    DOM the element produces: p.code-preview-error[role="alert"], appended on the
    first error and kept (hidden) between errors. data-error on the host stays, and the
    corner-radius rules still key off it — but a stylesheet targeting
    code-preview[data-error]::after now styles nothing.

  • The frame's document declares its language and a title. buildSrcdoc copies the
    host page's <html lang> into the frame — a screen reader picks its voice per
    document, and a frame that does not say is read in the user's default — and writes a
    <title>Preview</title> alongside the charset. A sample that brings its own document
    is passed through untouched, as before.

    Contents of the preview iframe: <html lang="…"> whenever the host page has one,
    and a <title> in head.

  • Focus and state survive Windows High Contrast. The focus ring on the code block is
    a box-shadow, which forced-colors mode strips — and the rule that kept host themes'
    outlines off the block used outline: none, leaving a focused block with no indicator
    at all. It is outline: 2px solid transparent now: invisible in normal rendering,
    repainted in a system color under forced colors. Selected tabs, pressed width buttons
    and the open Edit toggle likewise restate themselves in SelectedItem/SelectedItemText
    under forced-colors: active, where the border and fill they speak through flatten away.

  • The keyboard stop on a sample names itself. The focusable pre was a generic
    element with no role and no name — a screen reader landing on it had nothing to
    announce. Where it is not already the tabpanel (the single-fence page, or a pre
    inside a copy-button script's wrapper) it is role="group" with the sample's label
    now. The label the code block used to carry permanently moved with it: code is an
    ARIA role that prohibits naming, so the block is labelled only while it is a
    role="textbox" — the one moment naming it is allowed. A markup-supplied aria-label
    on either element still wins.

    DOM the element produces: role="group" and aria-label on the editable block's
    pre outside tab strips; aria-label on the code block only while editing.

  • Every preview frame has its own name. All iframes were title="Rendered preview",
    so a screen reader's frame list on a page of twenty samples distinguished none of them.
    The title is derived per sample now — Rendered html sample, or the block's own
    aria-label when the markup gave one.

  • Knob descriptions reach the keyboard. A manifest entry's description sat only on
    the row as a title tooltip, which never follows focus; it is on the control itself
    too now, where a screen reader reads it as the field's description.

Full Changelog: v1.0.0...v2.0.0