v2.0.0
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"><aside class="drawer">…</aside></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 (ascssblock 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
injshave 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 reasonjsurls 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 arole="tablist"of
.code-preview-tabbuttons in the existing.code-preview-bar. The markup pane is named
code, nothtml— sotab="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, nohidden, 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 bycode-preview.is-tabbed:not(.is-code-pane)rather than by naming the
options tab. -
no-editcan 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
(codefor 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. Bareno-editis unchanged:
the whole sample stays read-only.CSS an author may be targeting:
code-preview.is-code-panenow 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 — nocontenteditable, 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(witharia-pressed) and
.code-preview-run. Each holds anaria-hiddenglyph and a<span>with the word for it
— Edit, Run — and that word is the accessible name, so neither carries an
aria-labelor atitle. The<pre>of an editable pane now carriestabindex="0"and an
aria-describedbypointing atp.code-preview-hint;role="textbox",
aria-multiline,aria-keyshortcutsandcontenteditableare 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-actionstakes them away, spelled the wayno-editis: 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-describedbyis unaffected.CSS an author may be targeting:
code-preview.is-editingis new and says the editor
is open..is-editablestill 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;
thedisplay: revertoverride some pages added for exactly that is now a no-op and can go.
.code-preview-copy,.code-preview-note,.code-preview-icon-copyand
.code-preview-icon-checkno longer exist, andno-actions="copy"names nothing. -
The sample's console, under the preview.
console.log,info,warn,errorand
debugfrom 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
deferredjsurl and of the sample's own module — so a top-levelconsole.logon 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 acode-preview-logCustomEvent on the iframe.
Values are formatted withoutinstanceof— the frame is another realm, where its
ElementandErrorare different classes — so an element prints as<tag>, an
error asname: message, the rest as JSON where JSON can say it.no-consoleon 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-errorby level.--code-preview-console-height
(default10rem) 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 asrcdocframe is same-origin and shares the page's event loop:
half-typed js,while (truewith 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
jsasset, thereloadattribute — 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-runonly 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 (jsasset,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 wasEscapealone.
.is-js-panenow 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 isdisplay: none. The accent rules on
.is-js-pane .code-preview-runare 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-barstrips where there was one, each
built only when it has something to hold — the code's when the tabs are coming, the
preview's withviewport-widths. A single-fence sample has neither.toolbarstill means
the preview's strip; the newcodeBaris 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-heightis unchanged and still the knob..code-preview-widthsno
longer carriesmargin-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, thebeforematchhandling 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 — ano-editsample, ascssfence 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-panein a strip,
instead of.is-editablealone.
Fixed
-
A script error is announced, not just drawn. The error banner was CSS generated
content (::afterreadingdata-error), which changes silently — a screen-reader user
who typed the edit that threw heard nothing. It is a real element withrole="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-erroron the host stays, and the
corner-radius rules still key off it — but a stylesheet targeting
code-preview[data-error]::afternow styles nothing. -
The frame's document declares its language and a title.
buildSrcdoccopies 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 usedoutline: none, leaving a focused block with no indicator
at all. It isoutline: 2px solid transparentnow: 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 inSelectedItem/SelectedItemText
underforced-colors: active, where the border and fill they speak through flatten away. -
The keyboard stop on a sample names itself. The focusable
prewas 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 apre
inside a copy-button script's wrapper) it isrole="group"with the sample's label
now. The label thecodeblock used to carry permanently moved with it:codeis 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-suppliedaria-label
on either element still wins.DOM the element produces:
role="group"andaria-labelon the editable block's
preoutside tab strips;aria-labelon thecodeblock 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-labelwhen the markup gave one. -
Knob descriptions reach the keyboard. A manifest entry's
descriptionsat only on
the row as atitletooltip, 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