Skip to content

v1.0.0

Choose a tag to compare

@stamat stamat released this 31 Jul 19:28
· 31 commits to main since this release

After a detailed polish releasing the first stable version

Fixed

  • A sample's own <script> stopped running after the first edit. A js demo keeps its
    script inside the sample, because the element takes one fence — and an edit was applied
    to the loaded frame with innerHTML, which never executes a script it inserts. The
    first paint went through srcdoc and worked, so the demo only died from the first
    keystroke on: still rendered, still correctly marked up, nothing in the console. Only
    js urls and the reload attribute forced the rebuild; the sample's own script was
    not looked at.

    A sample containing a <script> now rebuilds the frame rather than patching it, on the
    same longer debounce a js url already used. Nothing to change in a page: samples with
    no script in them still patch, and keep their scroll position and stylesheets as before.

  • A sample's own elements never came alive when js pointed at a custom element
    bundle — which is most of what this element is for. The scripts went into <head>
    undeferred, so customElements.define ran before the body was parsed and the parser
    then upgraded each element the instant it opened its tag, with none of its light-DOM
    children there yet. Every element that reads its own children on connect found nothing
    and bailed.

    The failure was silent and total: the sample rendered, the markup was right, the
    stylesheet applied, nothing appeared in the console — and not one element was wired.
    Native behaviour inside the sample (a <details> toggling) still worked, which is
    exactly what made it read as "the preview is a bit unresponsive" rather than as a bug.
    It also made the options panel look broken from the outside: its knobs were writing
    correctly the whole time, into elements that were not listening.

    js scripts now carry defer, which is what these libraries already document as their
    requirement, and which keeps execution order across several urls. An inline <script>
    in the sample is untouched — that one is the author's, and it is in body where they
    wrote it.

  • The editable code block was a keyboard trap. Tab indents in there, which left a
    keyboard user who tabbed in with nothing to press — WCAG 2.1.2 Level
    A
    , and the one
    failure here with no workaround from the outside. Esc now hands Tab back: the next
    Tab moves focus, and leaving the block re-arms it, so tab-to-indent is unchanged for
    anyone who does not need to leave by keyboard.

  • The editor says what it is. CodeJar leaves a block that is editable and nothing
    else, so the element now adds role="textbox", aria-multiline="true",
    aria-keyshortcuts="Escape" and an aria-label naming the language. An aria-label
    or aria-labelledby already on the block is left alone.

  • The focus ring follows focus. It hung off code-preview:focus-within, so clicking
    a width button or a tab lit the code block up instead. It is now on the block.

  • Switching tab no longer drops focus on the floor. Setting tab from a script or
    from markup while the reader was inside the pane being hidden left focus on an element
    that was about to disappear, which the browser answers by moving it to the body — the
    next Tab starts again at the top of the page, with the whole document between a screen
    reader and the widget it was just in. Focus now moves to the tab being switched to,
    which is where clicking or arrowing to that tab had already left it. Focus outside the
    pane is not touched, so the frame's own load — which calls the same code — cannot yank a
    reader into the tab strip.

  • A keystroke that changed nothing reloaded the preview. CodeJar reports an update on
    every keyup, not only the ones that edited the text — the arrows, Tab, every modifier,
    and now the Esc this element asks people to press — and each one rebuilt the frame a
    quarter-second later for a sample that had not moved. That reload throws away everything
    live inside the preview: a script's state, and the focus a keyboard user had put on a
    control in there. So an accessible component could not be demonstrated in its own
    preview — Tab into the frame, focus a control, and it vanished under you a moment later.
    The frame is now rendered only when the source it would render has actually changed.

    Editing the sample still rebuilds, and still costs whatever was live in there. That one
    is the sample changing, which is the point.

  • An Escape pressed outside the editor released its Tab. The listener sits on the
    element, so an Escape in an options-panel field or on a width button also flipped the
    editor's tab-to-indent off and rewrote the keyboard hint — about an editor the reader
    was not in. Leaving the editor re-armed it, so no trap could result, but the hint could
    claim a state that was no longer true. Only an Escape from inside the editor counts now.

  • A failed manifest fetch was cached for the life of the page. One transient network
    error cost every preview sharing that url its options panel until a reload. A rejected
    fetch is now evicted from the cache, so a preview mounting later tries again.

  • An attribute <select> now says its default. Its empty option reads
    default (quiet) when the manifest documents one, the same way a custom property's
    already did — it used to say only default, with the manifest's answer dropped.

  • A duplicate width in viewport-widths no longer renders a duplicate button.

  • The color swatch treats an alpha it cannot parse as unknown — the swatch stays
    where it was, like every other value it cannot be sure about, rather than showing the
    color as opaque.

  • An attribute name containing a . is matched literally when the options panel
    reads or rewrites the sample, rather than as a regex wildcard.

  • Publishing runs the tests. CI runs on branches and pull requests, not on tags, so
    the publish workflow ran none at all — a tag cut from a broken commit would have
    published untested code. npm test now runs before npm publish.

Added

  • Every color and font the stylesheet reads now has a --code-preview- name.
    --code-preview-bg, --code-preview-fg, --code-preview-fg-muted,
    --code-preview-border, --code-preview-accent, --code-preview-danger,
    --code-preview-radius and --code-preview-font-mono join the four
    --code-preview-* sizing properties that were already there, so nothing about the
    element's look is reachable except through its own namespace.

    Nothing to change in a page. Each one falls back to the unprefixed name it used to
    read before its default — var(--code-preview-bg, var(--bg, #fff)) — so a host page
    themed through --border, --bg, --accent, --fg, --fg-muted, --danger,
    --radius or --font-mono looks exactly as it did. The prefixed name is only the
    first lookup, which is what makes it possible to move this element alone without
    moving the page around it:

    code-preview {
      --code-preview-bg: #161b22;
    }

    dist/code-preview-hljs.css reads --code-preview-fg-muted the same way, so the
    optional syntax theme moves with the element rather than with the page.

  • The options panel lists what the sample fires. A third group, Events, built from
    the manifest's events[] — every documented event is listed whether or not it has fired,
    with a count and the last detail beside it once it has. An element whose whole API is a
    CustomEvent was otherwise a preview that appears to do nothing when you click it.

    The rows are <div class="code-preview-event"> with a
    <span class="code-preview-event-value"> readout, and the <fieldset> around them
    carries aria-live="polite". Nothing here is a control, so nothing writes to the sample
    or to the frame's stylesheet.

    The listeners go on the frame's document, in the capture phase: capture is what hears
    an event that does not bubble — most of them, dispatched on the element itself — and the
    document is what survives the innerHTML patch a keystroke does. A rebuilt frame is a
    new document with a new sample in it, so its counts start again from .

    They are also attached whichever tab is open, which is a behaviour change inside the
    panel: the controls used to be re-read only when the Options tab was activated, and an
    event fired while the reader is looking at the code still has to be counted.

  • The event readout is highlighted, and says when it changed. A detail is now written
    as spans carrying highlight.js's own token classes — hljs-attr for a key, hljs-string,
    hljs-number, hljs-literal, hljs-tag for a node — so a docs page that already ships a
    syntax theme colors it with no extra css. dist/code-preview-hljs.css scopes its rules to
    :is(pre code, .code-preview-event-value) for the same reason; a theme of your own that
    targeted the pre code form still wins on any real code block. The readout's text is
    unchanged, so anything reading textContent reads what it read before.

    A detail is one line and stays one line: a string over 42 characters is clipped, a
    function is ƒ, anything nested is {…} and an array is its length. The sample's own
    console is where a full payload is read.

    The readout is now two cells — <span class="code-preview-event-count"> and
    <span class="code-preview-event-detail"> inside the same
    .code-preview-event-value — and the row no longer borrows the knobs' column grid. A
    knob's second column is a field wide, which put a two-character count an inch from the
    name it belongs to; the name takes what it needs and the count follows it, with the
    details lined up in a column of their own.

  • An event says so over the preview. The name of a documented event appears in a
    <div class="code-preview-toast"> inside .code-preview-viewport for about a second and a
    half whenever the sample fires one — that is where the reader is looking when they click
    the thing that fires it. One box per element, reused, and opacity only, so there is
    nothing in it for prefers-reduced-motion to object to. no-toast on the element
    turns it off, for a sample that fires on every pointermove; the panel still counts.

    .code-preview-viewport is now position: relative — it is the toast's containing block,
    so the notice lands on the sample rather than on the toolbar. A sample tall enough to
    scroll (past max-height: 70vh) scrolls its toast with it, until anchor positioning is
    available everywhere.

    The row also flashes when its count goes up.

  • A keyboard hint, <p class="code-preview-hint">, appended to the element for every
    editable sample: Press Esc, then Tab, to leave the editor, becoming Tab now leaves the editor once Esc has been pressed. It is the aria-describedby of the editor and a
    role="status" live region, so the same sentence reaches a screen reader and the
    screen. The stylesheet keeps it invisible until the block has focus and positions it
    absolutely, so it costs no layout — which is why code-preview is now
    position: relative.

    It shows for a keyboard and not for a pointer, because it is advice about a key and
    someone who clicked in can click back out. The element sets .is-key-focus on
    itself when focus arrives on a keypress rather than a click, so the rule is
    code-preview.is-key-focus:has(pre:focus-within); it is dropped again on focusout,
    and added late if someone who clicked in starts typing, since from that keystroke on
    they are in the same trap. Not :focus-visible — a contenteditable matches that on a
    mouse click too, because a browser assumes anything taking text input wants its focus
    ring — so intent is tracked with a keydown/pointerdown pair per document, added once
    however many editors a page has. A screen reader is unaffected either way:
    aria-describedby is read on arrival however focus got there.

    An editable block gets padding-block-end: var(--code-preview-hint-space, 2.25rem) to
    hold the room the hint sits in. Reserved from upgrade rather than added on focus:
    growing the block at the moment someone clicks into it would shift the page under their
    cursor. Set --code-preview-hint-space to the block's normal padding to turn the
    reservation off.

    It is a child of the element rather than of the code block, because a copy-button script
    that reads the block's innerText would otherwise put the sentence on the clipboard —
    so the tab strip hides it itself, with display: none on any tab but code. Left
    showing it would be a live region describing an editor the reader has switched away
    from.

Changed

  • The options panel's three groups collapse. Attributes, Custom properties and Events
    each open on arrival and can be closed, so a panel documenting all three is no longer
    taller than the sample above it.

    Each group is a <details class="code-preview-group" open> with a <summary>, where
    the panel that shipped in 0.2.0 used a <fieldset> with a <legend>. A stylesheet of
    your own targeting .code-preview-group > legend wants > summary instead;
    .code-preview-group and .code-preview-knobs are unchanged. Nothing is lost naming
    the set — <details> maps to role="group" and its summary is that group's accessible
    name, exactly as the legend was — and the disclosure is the browser's, so there is no
    new ARIA and no new key handling. The Events group is still the aria-live region;
    closed, it announces nothing, which is the bargain the hidden pane already made — the
    toast is what says an event fired.

  • The tabs, the width buttons and the group summaries have a hover state, which none
    of them had: a wash tinted from --fg-muted plus the full --fg text color, behind
    @media (hover: hover) so a tap does not leave it stuck on. A wash rather than the
    color alone, because the color is already how a tab says it is the selected one.

  • Spelling is en-US throughoutcolor, not colour, in the docs, the comments and
    this file. No identifier, class or attribute changed: the API was already --color-*
    and <input type="color">.

  • The color swatch is the color. <input type="color"> draws the value as a square
    inset inside its own padding and border, which at 1.75rem is more chrome than color, and
    the chrome was already drawn around it by this stylesheet. The value now fills the button
    (::-webkit-color-swatch-wrapper, ::-webkit-color-swatch, ::-moz-color-swatch, one
    rule each — a selector list containing a pseudo-element the engine does not know is a
    list it drops whole).

    That only pays if the color is true, so the swatch now follows the field: the value is
    resolved by setting it on the swatch and reading the computed color back, which is what
    turns a named color, hsl(…) or a color-mix(…) into channels. A value nothing can
    resolve leaves the swatch where it was, rather than claiming a color the sample does not
    have.

  • transparent is drawn as a crossed-out square, a thin red cross over black, the way
    a mac shows no color. There is no transparent in a color picker: <input type="color">
    holds an opaque #rrggbb and nothing else, and the newer alpha attribute only buys
    #rrggbbaa — still not the keyword, which is a real default in a themeable library. The
    text field remains the control; the swatch stops lying about it. The class is
    .code-preview-swatch.is-transparent, and the cross takes --danger.

  • The <select> caret is drawn rather than left to the platform, which put it hard
    against the field's right edge with 0.375rem of padding on the other side. It now sits at
    the same 0.375rem, and takes currentColor — two gradients making one triangle, so there
    is no data uri to recolor per theme and no extra element.

  • Group titles are uppercasedAttributes, Custom properties, Events. Only the
    legends: the names below them are verbatim attribute and custom-property names, where
    case is meaning.

Full Changelog: v0.2.0...v1.0.0