#56 named 44px and #195 took the page header there. Settings is the other half of #186 and much the larger one: swept on the reference device (TLP301, 424x439) with all eleven config-sections expanded, **120 controls** were under the floor -- not the 93 the issue's table implies, and config-field is eight of them. The bulk is behind the disclosures, which is why nobody had counted it: 36 .column-arrow-btn 16x14 <- smallest in the app 29 .column-toggle 16x16 26 shortcut-capture button 80x25 8 download format checkbox 16x16 7 config-field select 335x30 6 wa-input / wa-button 204x20, 185x21 **The density argument, measured rather than guessed, and it is smaller than it looks.** The rows were already near the floor -- .column-item is 335x36 and .shortcut-row 335x37; it is the controls *inside* them that were 14-25px. So a control grows into the row it already occupies and the row goes 36 to 44. Measured after: the two column lists went 373->447 and 690->850, +234px over the whole page. Half a screen of extra scroll on a page that already scrolls, against 36 targets of 16x14. **Settings is cheaper than the header was, and for a stated reason.** There is no overflow fit on this page, so the header's "only width is contested" rule does not bind at all and nothing here needs padding with a negative margin. Height is a min-size, and the two square controls can simply be square. Three shapes, because one rule does not fit three kinds of control: **A native checkbox is targeted through its label.** It cannot grow its hit area without growing its paint, and a 44px checkbox is not what anyone wants -- so .column-label is a real <label for> now and the column's *name* is the target, 70x44 rather than 16x16. That is the argument config-field already makes one file over ("a real label association also makes the label text a click target, which is behaviour, not annotation"), and here it is the whole fix. The download formats already had the label; they only needed the height. **The arrows take padding, which is invisible.** They carry background: none and a transparent border, so 16x14 -> 44x44 changes nothing anyone can see until hover -- #186's Direction exactly. **Web Awesome's controls come from the library's own API.** Their height is decided inside somebody else's shadow root, and --wa-form-control-height is the variable that decides it. A custom property inherits through a shadow boundary, so a :host declaration reaches them; styles/wa-touch-floor.css.ts is that, once, adopted rather than written at :root in index.css -- a :root rule would be invisible to the component tier, which renders a component and no page stylesheet. **Two controls no sweep can see are fixed by name**, and they are the trap this issue keeps setting. config-field's toggle has an <input> that is opacity: 0; width: 0; height: 0, so a walk of every input skips it as a zero-sized node -- what a finger hits is the <label>, which measured **34x19**, smaller than anything in either of #186's tables and absent from both. It is 44x44 with the pill still painted at 2.5em x 1.4em and negative inline margins keeping it flush with the inputs above. And shortcut-capture's reset button renders only for a shortcut somebody has rebound, so a sweep of a fresh install never meets it. Verified on the device, same method as the sweep that filed it: 120 controls under the floor before, 42 after. All 42 are accounted for -- 37 are checkboxes whose labels measure 70x44 and 57x44, four are wa-input's inner input at 204x**42**, which is the control measured *inside* its own 1px border (part=base is 238x44), and one is the skip link, which #186 already ruled out as keyboard-only. The e2e suite passes, top-bar-fit and header-action-overflow included -- but that is **chromium**, which is half an answer, and saying so is the whole of what #195's second commit was about. What can be argued rather than run: library-filter is the only thing here in a container that measures itself, and its width did not change. The fit measures inline size. Two page-header screenshots are refreshed because they are this issue's own debris -- #195's taller sort control, merged last session, with its references never re-recorded. app-sidebar's and now-playing's are deliberately left: they are unrelated drift, and blessing an unrelated screenshot is how the sidebar reference came to still list a destination #27 retired. That is #196.
204 lines
6.4 KiB
TypeScript
204 lines
6.4 KiB
TypeScript
import { LitElement, html, css } from 'lit';
|
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
import { buildKeyString } from '../../services/keyboard-shortcut-service';
|
|
|
|
@customElement('shortcut-capture')
|
|
export class ShortcutCapture extends LitElement {
|
|
@property() action = '';
|
|
@property() currentKey = '';
|
|
@property() defaultKey = '';
|
|
|
|
/**
|
|
* What the binding does, for the name.
|
|
*
|
|
* The button's text is the *key* — so the shortcuts list rendered
|
|
* three buttons called "S", two called "Down" and one called "?",
|
|
* beside a visible label that named none of them (it is a sibling,
|
|
* in another shadow root, and nothing associated the two). The
|
|
* label is what a control is for; the key is its value.
|
|
*/
|
|
@property() label = '';
|
|
|
|
@state() private recording = false;
|
|
|
|
static override styles = css`
|
|
:host {
|
|
display: inline-block;
|
|
}
|
|
/* 80x25, twenty-six of them -- the most numerous control on
|
|
the Settings page after the column lists (#186). The floor
|
|
is a height here and nothing else: the width was already
|
|
past it, and the type stays where it is so a shortcut still
|
|
reads as a key rather than as a button. */
|
|
button {
|
|
font-family: inherit;
|
|
font-size: var(--yj-text-sm, 13px);
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--yj-border, #555);
|
|
background: var(--yj-bg-input, #333);
|
|
color: var(--yj-text-primary, #eee);
|
|
cursor: pointer;
|
|
min-width: 80px;
|
|
min-height: 44px;
|
|
text-align: center;
|
|
transition:
|
|
border-color 0.15s,
|
|
background 0.15s;
|
|
}
|
|
button:hover {
|
|
border-color: var(--yj-accent, #ffd43b);
|
|
}
|
|
button.recording {
|
|
border-color: var(--yj-accent, #ffd43b);
|
|
background: var(--yj-bg-active, #444);
|
|
animation: pulse 1.2s ease-in-out infinite;
|
|
}
|
|
button.not-set {
|
|
color: var(--yj-text-tertiary, #888);
|
|
font-style: italic;
|
|
}
|
|
@keyframes pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
}
|
|
}
|
|
/* Reset renders only for a rebound shortcut, so a sweep of a
|
|
freshly-installed app never sees it -- it is not in #186's
|
|
tables for that reason, and it is a touch target the moment
|
|
anybody uses the feature. It also has no background, so the
|
|
padding out to 44px is invisible. */
|
|
.reset-btn {
|
|
font-size: var(--yj-text-xs, 11px);
|
|
padding: 2px 6px;
|
|
margin-left: 4px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--yj-text-tertiary, #888);
|
|
cursor: pointer;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
:host(:hover) .reset-btn {
|
|
opacity: 1;
|
|
}
|
|
.reset-btn:hover {
|
|
color: var(--yj-accent-text, #ffd43b);
|
|
}
|
|
/*
|
|
* Reset is the only way to put a rebound shortcut back, so where
|
|
* the device has no hover it is always visible rather than an
|
|
* invisible button holding its hit area. The inverse of #68's
|
|
* rule, which applies where the hover control is redundant.
|
|
*/
|
|
@media not all and (hover: hover) {
|
|
.reset-btn {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
`;
|
|
|
|
private handleClick = () => {
|
|
this.recording = true;
|
|
// Focus self so keydown events arrive
|
|
this.shadowRoot?.querySelector('button')?.focus();
|
|
};
|
|
|
|
private handleKeydown = (e: KeyboardEvent) => {
|
|
if (!this.recording) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const keyStr = buildKeyString(e);
|
|
if (!keyStr) return; // bare modifier press — keep recording
|
|
|
|
if (keyStr === 'Escape') {
|
|
this.recording = false;
|
|
return;
|
|
}
|
|
|
|
this.recording = false;
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent('shortcut-change', {
|
|
detail: { action: this.action, key: keyStr },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
};
|
|
|
|
private handleBlur = () => {
|
|
// Cancel recording if focus leaves
|
|
if (this.recording) {
|
|
this.recording = false;
|
|
}
|
|
};
|
|
|
|
private handleReset = (e: Event) => {
|
|
e.stopPropagation();
|
|
if (this.defaultKey && this.currentKey !== this.defaultKey) {
|
|
this.dispatchEvent(
|
|
new CustomEvent('shortcut-change', {
|
|
detail: {
|
|
action: this.action,
|
|
key: this.defaultKey,
|
|
},
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
override render() {
|
|
const showReset =
|
|
this.defaultKey && this.currentKey !== this.defaultKey;
|
|
return html`
|
|
<button
|
|
class=${this.recording
|
|
? 'recording'
|
|
: this.currentKey
|
|
? ''
|
|
: 'not-set'}
|
|
aria-label=${this.label
|
|
? `${this.label} shortcut: ${this.currentKey || 'not set'}`
|
|
: ''}
|
|
@click=${this.handleClick}
|
|
@keydown=${this.handleKeydown}
|
|
@blur=${this.handleBlur}
|
|
>
|
|
${this.recording
|
|
? 'Press a key combo\u2026'
|
|
: this.currentKey || 'Not set'}
|
|
</button>
|
|
${showReset
|
|
? html`
|
|
<button
|
|
class="reset-btn"
|
|
@click=${this.handleReset}
|
|
title="Reset to default (${this.defaultKey})"
|
|
aria-label="Reset ${this.label ||
|
|
this.action} to ${this.defaultKey}"
|
|
>
|
|
\u21BA
|
|
</button>
|
|
`
|
|
: ''}
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'shortcut-capture': ShortcutCapture;
|
|
}
|
|
}
|