@fusion.dev/helix-headless 0.219.0 → 0.221.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { HelixElement, type TerminalResizeDetail } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../button/index.js';
4
+ import '../../find/index.js';
4
5
  import '../../icon-button/index.js';
5
6
  import '../../tooltip/index.js';
6
7
  import '../../toolbar/index.js';
@@ -164,6 +165,7 @@ export interface TerminalSink {
164
165
  * @slot restart-icon - Replaces the glyph inside the Restart button.
165
166
  *
166
167
  * @event {HelixCopyEvent} helix-copy - The copy control or its key put the selection on the clipboard.
168
+ * @event {HelixFoundEvent} helix-found - The find bar searched or moved among the matches, with `count` and the current match's `index`, counting from 0, or -1 for none.
167
169
  * @event {HelixTerminalBellEvent} helix-terminal-bell - The program rang the bell. The frame flashes once.
168
170
  * @event {HelixTerminalInputEvent} helix-terminal-input - The reader typed or pasted, with `data` encoded for the program. Write it to the process. Not sent while `readonly`, or once `exit-code` is set.
169
171
  * @event {HelixTerminalLinkEvent} helix-terminal-link - A link in the output was activated, with its `href`. Cancelable: cancel it to open the link yourself. Left alone, a web address opens in a new tab.
@@ -185,23 +187,23 @@ export interface TerminalSink {
185
187
  * @csspart copy-spinner - The ring the copy button turns while it is pending.
186
188
  * @csspart copy-text - The box the copy button's icon sits in.
187
189
  * @csspart exit-actions - The `exit-actions` slot, beside Restart in the status line.
188
- * @csspart find - The button that opens and closes the find bar. Expanded while it is open.
189
- * @csspart find-actions - The `find-actions` slot, first in the find bar's toolbar.
190
- * @csspart find-bar - The bar under the header where the reader searches the output.
191
- * @csspart find-button - The find button's inner button.
190
+ * @csspart find - The find bar under the header, where the reader searches the output: a Find of its own.
191
+ * @csspart find-actions - The `find-actions` slot, in the find bar's toolbar.
192
+ * @csspart find-bar - The find bar's strip: the field, the count, and the toolbar.
192
193
  * @csspart find-close - The button that closes the find bar.
193
194
  * @csspart find-close-button - The find bar's close button's inner button.
194
195
  * @csspart find-close-live - The find bar's close button's live region, which speaks while the button is pending.
195
196
  * @csspart find-close-spinner - The ring the find bar's close button turns while it is pending.
196
197
  * @csspart find-close-text - The box the find bar's close button's icon sits in.
197
- * @csspart find-controls - The toolbar at the end of the find bar: your find actions, then the buttons that move between matches and close the bar.
198
+ * @csspart find-controls - The toolbar at the end of the find bar: the options, your find actions, and the buttons that move between matches and close the bar.
198
199
  * @csspart find-controls-toolbar - The box inside the find bar's toolbar that lays its buttons out.
199
200
  * @csspart find-count - Which match is current and how many there are, such as "3 of 12 matches".
200
201
  * @csspart find-field - The search field in the find bar.
201
- * @csspart find-live - The find button's live region, which speaks while the button is pending.
202
+ * @csspart find-live - The find bar's live region, which says the count as it changes.
202
203
  * @csspart find-match-case - The button that matches case in the find bar. Pressed while it does.
203
204
  * @csspart find-match-case-button - The match case button's inner button.
204
205
  * @csspart find-match-case-live - The match case button's live region, which speaks while the button is pending.
206
+ * @csspart find-match-case-pressed - The match case button while case has to match, beside `find-match-case`.
205
207
  * @csspart find-match-case-spinner - The ring the match case button turns while it is pending.
206
208
  * @csspart find-match-case-text - The box the match case button's icon sits in.
207
209
  * @csspart find-next - The button that moves to the next match.
@@ -217,13 +219,34 @@ export interface TerminalSink {
217
219
  * @csspart find-regex - The button that reads the find field as a regular expression. Pressed while it does.
218
220
  * @csspart find-regex-button - The regular expression button's inner button.
219
221
  * @csspart find-regex-live - The regular expression button's live region, which speaks while the button is pending.
222
+ * @csspart find-regex-pressed - The regular expression button while the field reads as one, beside `find-regex`.
220
223
  * @csspart find-regex-spinner - The ring the regular expression button turns while it is pending.
221
224
  * @csspart find-regex-text - The box the regular expression button's icon sits in.
222
- * @csspart find-spinner - The ring the find button turns while it is pending.
223
- * @csspart find-text - The box the find button's icon sits in.
225
+ * @csspart find-replace - The find bar's Replace button, which a terminal does not draw, since its output cannot be changed.
226
+ * @csspart find-replace-all - The find bar's Replace all button, which a terminal does not draw.
227
+ * @csspart find-replace-all-button - The Replace all button's inner button.
228
+ * @csspart find-replace-all-live - The Replace all button's live region, which speaks while the button is pending.
229
+ * @csspart find-replace-all-spinner - The ring the Replace all button turns while it is pending.
230
+ * @csspart find-replace-all-text - The box the Replace all button's words sits in.
231
+ * @csspart find-replace-button - The Replace button's inner button.
232
+ * @csspart find-replace-field - The find bar's replace field, which a terminal does not draw.
233
+ * @csspart find-replace-live - The Replace button's live region, which speaks while the button is pending.
234
+ * @csspart find-replace-spinner - The ring the Replace button turns while it is pending.
235
+ * @csspart find-replace-text - The box the Replace button's words sits in.
236
+ * @csspart find-replace-toggle - The button that shows the find bar's replace row, which a terminal does not draw.
237
+ * @csspart find-replace-toggle-button - The replace toggle's inner button.
238
+ * @csspart find-replace-toggle-live - The replace toggle's live region, which speaks while the button is pending.
239
+ * @csspart find-replace-toggle-spinner - The ring the replace toggle turns while it is pending.
240
+ * @csspart find-replace-toggle-text - The box the replace toggle's icon sits in.
241
+ * @csspart find-toggle - The button in the header that opens and closes the find bar. Expanded while it is open.
242
+ * @csspart find-toggle-button - The find button's inner button.
243
+ * @csspart find-toggle-live - The find button's live region, which speaks while the button is pending.
244
+ * @csspart find-toggle-spinner - The ring the find button turns while it is pending.
245
+ * @csspart find-toggle-text - The box the find button's icon sits in.
224
246
  * @csspart find-whole-word - The button that matches whole words only in the find bar. Pressed while it does.
225
247
  * @csspart find-whole-word-button - The whole word button's inner button.
226
248
  * @csspart find-whole-word-live - The whole word button's live region, which speaks while the button is pending.
249
+ * @csspart find-whole-word-pressed - The whole word button while only whole words match, beside `find-whole-word`.
227
250
  * @csspart find-whole-word-spinner - The ring the whole word button turns while it is pending.
228
251
  * @csspart find-whole-word-text - The box the whole word button's icon sits in.
229
252
  * @csspart fullscreen - The button that makes the terminal fill the screen. Pressed while it does.
@@ -385,16 +408,12 @@ export declare class HelixHeadlessTerminal extends HelixElement {
385
408
  tabMovesFocusLabel: string;
386
409
  /** What screen readers hear when Tab goes back to typing in the terminal. */
387
410
  tabTypesLabel: string;
388
- /** Whether the find bar is open. */
411
+ /** Whether the find bar is open, as it last said. */
389
412
  private findOpen;
390
- /** What the find field holds. */
391
- private query;
392
- /** The current match, counting from `0`, or `-1` for none. */
393
- private matchIndex;
394
- /** How many matches the search found, or `undefined` before one ran. */
395
- private matchCount;
396
- /** Whether the regular expression in the field cannot be read, which the count says instead. */
397
- private invalid;
413
+ /** The find bar drawn now, and how the renderer reports a search to it. */
414
+ private find;
415
+ /** The options the renderer last searched with, which a search with others ends first. */
416
+ private searchedWith;
398
417
  /** Whether Tab moves focus, after Control+M. */
399
418
  private tabMovesFocus;
400
419
  /** Whether any text is selected in the grid. */
@@ -468,8 +487,6 @@ export declare class HelixHeadlessTerminal extends HelixElement {
468
487
  private say;
469
488
  /** Whether `controls`, or the resting controls, names a control the renderer can answer. */
470
489
  private draws;
471
- /** The find bar's count, as the reader reads it. */
472
- private get countText();
473
490
  /** The chord that switches Tab, as a reader says it. */
474
491
  private get tabKey();
475
492
  /**
@@ -490,16 +507,15 @@ export declare class HelixHeadlessTerminal extends HelixElement {
490
507
  * a key the frame takes never reaches the program.
491
508
  */
492
509
  private readonly handleKeys;
493
- /** Opens the find bar and puts focus in its field, searching again for what it holds. */
510
+ /** Opens the find bar and puts focus in its field. */
494
511
  private openFind;
495
- /** Closes the find bar, takes its highlights off, and returns focus to the terminal. */
496
- private closeFind;
497
- /** Searches for what the field holds, toward the bottom or with `backward` toward the top. */
498
- private search;
499
- /** Turns one of the find bar's options on or off, and searches again with it, staying on a match that still matches. */
500
- private toggleOption;
501
- private readonly handleFindInput;
502
- private readonly handleFindKeys;
512
+ /**
513
+ * The renderer, as the find bar searches through it. The renderer reports
514
+ * the matches through `found`, once it has counted them.
515
+ */
516
+ private readonly finder;
517
+ /** The find bar opened or closed, which the find button shows. */
518
+ private readonly handleFindToggle;
503
519
  /** Copies the selection, and says so. */
504
520
  private copySelection;
505
521
  private readonly handleActions;
@@ -512,6 +528,8 @@ export declare class HelixHeadlessTerminal extends HelixElement {
512
528
  */
513
529
  protected willUpdate(changed: PropertyValues<this>): void;
514
530
  protected updated(changed: PropertyValues<this>): void;
531
+ /** Hands the find bar drawn now the renderer to search through, once per bar. */
532
+ private attachFind;
515
533
  connectedCallback(): void;
516
534
  disconnectedCallback(): void;
517
535
  /** Whether any built-in control is drawn. */
@@ -526,10 +544,9 @@ export declare class HelixHeadlessTerminal extends HelixElement {
526
544
  */
527
545
  protected renderControls(): TemplateResult;
528
546
  /**
529
- * The find bar, kept in the tree while closed so its field keeps what was
530
- * typed. The field stands before the toolbar and outside it, because a
531
- * toolbar leaves the arrows to a text field, and the buttons in one with it
532
- * would be out of their reach.
547
+ * The find bar, a Find of the frame's own that searches through the
548
+ * renderer, kept in the tree while closed so its field keeps what was
549
+ * typed. The frame takes the find key itself, so the bar takes none.
533
550
  */
534
551
  protected renderFind(): TemplateResult | typeof nothing;
535
552
  /**
@@ -4,22 +4,13 @@ import { arrowClockwise, asterisk, broom, caretDown, caretUp, copy, cornersOut,
4
4
  import { nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
6
  import { keyed } from 'lit/directives/keyed.js';
7
- import { live } from 'lit/directives/live.js';
8
7
  import { html } from 'lit/static-html.js';
9
8
  import '../../button/index.js';
9
+ import '../../find/index.js';
10
10
  import '../../icon-button/index.js';
11
11
  import '../../tooltip/index.js';
12
12
  import '../../toolbar/index.js';
13
13
  import { isTab, shortcutOf, spokenChord } from '../keys.js';
14
- const readable = (source) => {
15
- try {
16
- new RegExp(source);
17
- return true;
18
- }
19
- catch {
20
- return false;
21
- }
22
- };
23
14
  const ANSI = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
24
15
  const camel = (name) => name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
25
16
  const unset = (color) => color === '' || color === 'transparent' || /^rgba\(\s*0,\s*0,\s*0,\s*0\s*\)$/.test(color);
@@ -58,10 +49,6 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
58
49
  this.tabMovesFocusLabel = 'Tab moves focus. Press {key} to type Tab in the terminal.';
59
50
  this.tabTypesLabel = 'Tab types in the terminal.';
60
51
  this.findOpen = false;
61
- this.query = '';
62
- this.matchIndex = -1;
63
- this.matchCount = undefined;
64
- this.invalid = false;
65
52
  this.tabMovesFocus = false;
66
53
  this.anySelected = false;
67
54
  this.hasActions = false;
@@ -118,19 +105,33 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
118
105
  event.stopPropagation();
119
106
  }
120
107
  };
121
- this.handleFindInput = (event) => {
122
- this.query = event.target.value;
123
- this.search(false, true);
108
+ this.finder = {
109
+ end: () => {
110
+ this.searchedWith = undefined;
111
+ this.renderer?.findEnd?.();
112
+ },
113
+ find: (query, direction) => {
114
+ const options = {
115
+ backward: direction === 'previous',
116
+ caseSensitive: query.caseSensitive,
117
+ incremental: direction === 'first',
118
+ regex: query.regex,
119
+ wholeWord: query.wholeWord,
120
+ };
121
+ const searching = `${options.caseSensitive} ${options.regex} ${options.wholeWord}`;
122
+ if (this.searchedWith !== undefined && this.searchedWith !== searching) {
123
+ this.renderer?.findEnd?.();
124
+ }
125
+ this.searchedWith = searching;
126
+ this.findMatchCase = query.caseSensitive;
127
+ this.findRegex = query.regex;
128
+ this.findWholeWord = query.wholeWord;
129
+ this.renderer?.find?.(query.search, options);
130
+ return undefined;
131
+ },
124
132
  };
125
- this.handleFindKeys = (event) => {
126
- if (event.key === 'Escape') {
127
- event.preventDefault();
128
- this.closeFind();
129
- }
130
- else if (event.key === 'Enter') {
131
- event.preventDefault();
132
- this.search(event.shiftKey);
133
- }
133
+ this.handleFindToggle = (event) => {
134
+ this.findOpen = event.newState === 'open';
134
135
  };
135
136
  this.handleActions = (event) => {
136
137
  this.hasActions = event.target.assignedNodes().length > 0;
@@ -221,9 +222,7 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
221
222
  },
222
223
  found: (index, count) => {
223
224
  if (current()) {
224
- this.matchIndex = index;
225
- this.matchCount = count;
226
- this.say(this.countText);
225
+ this.find?.sink.found({ count, index });
227
226
  }
228
227
  },
229
228
  input: (data, binary = false) => {
@@ -300,17 +299,6 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
300
299
  ? renderer.selection !== undefined
301
300
  : renderer.find !== undefined;
302
301
  }
303
- get countText() {
304
- if (this.invalid) {
305
- return this.findInvalidLabel;
306
- }
307
- return this.matchCount === undefined
308
- ? ''
309
- : formatWords(this, 'matchesLabel', {
310
- count: this.matchCount,
311
- index: Math.max(this.matchIndex, 0) + 1,
312
- });
313
- }
314
302
  get tabKey() {
315
303
  return spokenChord('tab-focus', onApple());
316
304
  }
@@ -355,8 +343,9 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
355
343
  }
356
344
  release() {
357
345
  this.restoreInput();
358
- this.findOpen = false;
359
- this.matchCount = undefined;
346
+ if (this.find) {
347
+ this.find.element.open = false;
348
+ }
360
349
  this.anySelected = false;
361
350
  }
362
351
  takes(shortcut) {
@@ -373,45 +362,7 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
373
362
  return true;
374
363
  }
375
364
  async openFind() {
376
- this.findOpen = true;
377
- await this.updateComplete;
378
- const field = this.partNamed('find-field');
379
- field?.focus();
380
- field?.select();
381
- if (this.query !== '') {
382
- this.search(false, true);
383
- }
384
- }
385
- closeFind() {
386
- this.findOpen = false;
387
- this.matchCount = undefined;
388
- this.matchIndex = -1;
389
- this.renderer?.findEnd?.();
390
- this.focus();
391
- }
392
- search(backward, incremental = false) {
393
- if (this.query === '' || (this.findRegex && !readable(this.query))) {
394
- this.invalid = this.query !== '';
395
- this.matchCount = undefined;
396
- this.renderer?.findEnd?.();
397
- if (this.invalid) {
398
- this.say(this.findInvalidLabel);
399
- }
400
- return;
401
- }
402
- this.invalid = false;
403
- this.renderer?.find?.(this.query, {
404
- backward,
405
- caseSensitive: this.findMatchCase,
406
- incremental,
407
- regex: this.findRegex,
408
- wholeWord: this.findWholeWord,
409
- });
410
- }
411
- toggleOption(option) {
412
- this[option] = !this[option];
413
- this.renderer?.findEnd?.();
414
- this.search(false, true);
365
+ await this.find?.element.show();
415
366
  }
416
367
  async copySelection() {
417
368
  const text = this.renderer?.selection?.() ?? '';
@@ -442,6 +393,15 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
442
393
  if (this.renderer && (changed.has('inputLabel') || changed.has('tabHintLabel'))) {
443
394
  this.nameInput();
444
395
  }
396
+ this.attachFind();
397
+ }
398
+ attachFind() {
399
+ const element = this.partNamed('find') ?? undefined;
400
+ if (element === this.find?.element) {
401
+ return;
402
+ }
403
+ this.find?.sink.detach();
404
+ this.find = element ? { element, sink: element.attach(this.finder) } : undefined;
445
405
  }
446
406
  connectedCallback() {
447
407
  super.connectedCallback();
@@ -463,12 +423,12 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
463
423
  <slot name="actions" part="actions" @slotchange=${this.handleActions}></slot>
464
424
  ${this.draws('find')
465
425
  ? html `<${button}
466
- part="find"
426
+ part="find-toggle"
467
427
  id="find"
468
- exportparts="button: find-button, live: find-live, spinner: find-spinner, text: find-text"
428
+ exportparts="button: find-toggle-button, live: find-toggle-live, spinner: find-toggle-spinner, text: find-toggle-text"
469
429
  .expanded=${this.findOpen}
470
430
  label=${this.findLabel}
471
- @click=${() => (this.findOpen ? this.closeFind() : void this.openFind())}
431
+ @click=${() => (this.findOpen ? this.find?.element.hide() : void this.openFind())}
472
432
  @helix-press=${stopPress}
473
433
  >
474
434
  <slot name="find-icon">${drawIcon(magnifyingGlass)}</slot>
@@ -518,91 +478,34 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
518
478
  if (!this.draws('find')) {
519
479
  return nothing;
520
480
  }
521
- const toolbar = this.tag('toolbar');
522
- const button = this.tag('icon-button');
523
- const tip = this.tag('tooltip');
524
- return html `<div part="find-bar" role="search" aria-label=${this.findLabel} ?hidden=${!this.findOpen}>
525
- <input
526
- part="find-field"
527
- aria-label=${this.findLabel}
528
- autocomplete="off"
529
- placeholder=${this.findLabel}
530
- spellcheck="false"
531
- type="search"
532
- .value=${live(this.query)}
533
- @input=${this.handleFindInput}
534
- @keydown=${this.handleFindKeys}
535
- />
536
- <span part="find-count" aria-hidden="true">${this.countText}</span>
537
- <${toolbar} part="find-controls" exportparts="toolbar: find-controls-toolbar" label=${this.findControlsLabel}>
538
- <${button}
539
- part="find-match-case"
540
- id="find-match-case"
541
- exportparts="button: find-match-case-button, live: find-match-case-live, spinner: find-match-case-spinner, text: find-match-case-text"
542
- aria-pressed=${this.findMatchCase ? 'true' : 'false'}
543
- label=${this.findMatchCaseLabel}
544
- @click=${() => this.toggleOption('findMatchCase')}
545
- @helix-press=${stopPress}
546
- >
547
- <slot name="find-match-case-icon">${drawIcon(textAa)}</slot>
548
- </${button}><${tip} for="find-match-case" content=${this.findMatchCaseLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
549
- <${button}
550
- part="find-whole-word"
551
- id="find-whole-word"
552
- exportparts="button: find-whole-word-button, live: find-whole-word-live, spinner: find-whole-word-spinner, text: find-whole-word-text"
553
- aria-pressed=${this.findWholeWord ? 'true' : 'false'}
554
- label=${this.findWholeWordLabel}
555
- @click=${() => this.toggleOption('findWholeWord')}
556
- @helix-press=${stopPress}
557
- >
558
- <slot name="find-whole-word-icon">${drawIcon(textAUnderline)}</slot>
559
- </${button}><${tip} for="find-whole-word" content=${this.findWholeWordLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
560
- <${button}
561
- part="find-regex"
562
- id="find-regex"
563
- exportparts="button: find-regex-button, live: find-regex-live, spinner: find-regex-spinner, text: find-regex-text"
564
- aria-pressed=${this.findRegex ? 'true' : 'false'}
565
- label=${this.findRegexLabel}
566
- @click=${() => this.toggleOption('findRegex')}
567
- @helix-press=${stopPress}
568
- >
569
- <slot name="find-regex-icon">${drawIcon(asterisk)}</slot>
570
- </${button}><${tip} for="find-regex" content=${this.findRegexLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
571
- <slot name="find-actions" part="find-actions"></slot>
572
- <${button}
573
- part="find-previous"
574
- id="find-previous"
575
- exportparts="button: find-previous-button, live: find-previous-live, spinner: find-previous-spinner, text: find-previous-text"
576
- ?disabled=${!this.matchCount}
577
- label=${this.findPreviousLabel}
578
- @click=${() => this.search(true)}
579
- @helix-press=${stopPress}
580
- >
581
- <slot name="find-previous-icon">${drawIcon(caretUp)}</slot>
582
- </${button}><${tip} for="find-previous" content=${this.findPreviousLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
583
- <${button}
584
- part="find-next"
585
- id="find-next"
586
- exportparts="button: find-next-button, live: find-next-live, spinner: find-next-spinner, text: find-next-text"
587
- ?disabled=${!this.matchCount}
588
- label=${this.findNextLabel}
589
- @click=${() => this.search(false)}
590
- @helix-press=${stopPress}
591
- >
592
- <slot name="find-next-icon">${drawIcon(caretDown)}</slot>
593
- </${button}><${tip} for="find-next" content=${this.findNextLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
594
- <${button}
595
- part="find-close"
596
- id="find-close"
597
- exportparts="button: find-close-button, live: find-close-live, spinner: find-close-spinner, text: find-close-text"
598
- label=${this.findCloseLabel}
599
- @click=${() => this.closeFind()}
600
- @helix-press=${stopPress}
601
- >
602
- <slot name="find-close-icon">${drawIcon(x)}</slot>
603
- </${button}><${tip} for="find-close" content=${this.findCloseLabel} exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom, arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right, arrow-top: tooltip-arrow-top, surface: tooltip-surface, surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left, surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"></${tip}>
604
- </${toolbar}>
605
- </div>`;
481
+ const find = this.tag('find');
482
+ return html `<${find}
483
+ part="find"
484
+ exportparts="actions: find-actions, bar: find-bar, close: find-close, close-button: find-close-button, close-live: find-close-live, close-spinner: find-close-spinner, close-text: find-close-text, controls: find-controls, controls-toolbar: find-controls-toolbar, count: find-count, field: find-field, live: find-live, match-case: find-match-case, match-case-button: find-match-case-button, match-case-live: find-match-case-live, match-case-pressed: find-match-case-pressed, match-case-spinner: find-match-case-spinner, match-case-text: find-match-case-text, next: find-next, next-button: find-next-button, next-live: find-next-live, next-spinner: find-next-spinner, next-text: find-next-text, previous: find-previous, previous-button: find-previous-button, previous-live: find-previous-live, previous-spinner: find-previous-spinner, previous-text: find-previous-text, regex: find-regex, regex-button: find-regex-button, regex-live: find-regex-live, regex-pressed: find-regex-pressed, regex-spinner: find-regex-spinner, regex-text: find-regex-text, replace-all-button: find-replace-all-button, replace-all-live: find-replace-all-live, replace-all-spinner: find-replace-all-spinner, replace-all-text: find-replace-all-text, replace-all: find-replace-all, replace-button: find-replace-button, replace-field: find-replace-field, replace-live: find-replace-live, replace-spinner: find-replace-spinner, replace-text: find-replace-text, replace-toggle-button: find-replace-toggle-button, replace-toggle-live: find-replace-toggle-live, replace-toggle-spinner: find-replace-toggle-spinner, replace-toggle-text: find-replace-toggle-text, replace-toggle: find-replace-toggle, replace: find-replace, whole-word: find-whole-word, whole-word-button: find-whole-word-button, whole-word-live: find-whole-word-live, whole-word-pressed: find-whole-word-pressed, whole-word-spinner: find-whole-word-spinner, whole-word-text: find-whole-word-text"
485
+ close-label=${this.findCloseLabel}
486
+ controls-label=${this.findControlsLabel}
487
+ invalid-label=${this.findInvalidLabel}
488
+ label=${this.findLabel}
489
+ ?match-case=${this.findMatchCase}
490
+ match-case-label=${this.findMatchCaseLabel}
491
+ matches-label=${this.matchesLabel}
492
+ next-label=${this.findNextLabel}
493
+ previous-label=${this.findPreviousLabel}
494
+ ?regex=${this.findRegex}
495
+ regex-label=${this.findRegexLabel}
496
+ shortcut=""
497
+ ?whole-word=${this.findWholeWord}
498
+ whole-word-label=${this.findWholeWordLabel}
499
+ @toggle=${this.handleFindToggle}
500
+ >
501
+ <slot name="find-actions" slot="actions"></slot>
502
+ <slot name="find-match-case-icon" slot="match-case-icon">${drawIcon(textAa)}</slot>
503
+ <slot name="find-whole-word-icon" slot="whole-word-icon">${drawIcon(textAUnderline)}</slot>
504
+ <slot name="find-regex-icon" slot="regex-icon">${drawIcon(asterisk)}</slot>
505
+ <slot name="find-previous-icon" slot="previous-icon">${drawIcon(caretUp)}</slot>
506
+ <slot name="find-next-icon" slot="next-icon">${drawIcon(caretDown)}</slot>
507
+ <slot name="find-close-icon" slot="close-icon">${drawIcon(x)}</slot>
508
+ </${find}>`;
606
509
  }
607
510
  renderLook() {
608
511
  return html `<span data-look hidden aria-hidden="true" style="color: transparent">
@@ -778,18 +681,6 @@ __decorate([
778
681
  __decorate([
779
682
  state()
780
683
  ], HelixHeadlessTerminal.prototype, "findOpen", void 0);
781
- __decorate([
782
- state()
783
- ], HelixHeadlessTerminal.prototype, "query", void 0);
784
- __decorate([
785
- state()
786
- ], HelixHeadlessTerminal.prototype, "matchIndex", void 0);
787
- __decorate([
788
- state()
789
- ], HelixHeadlessTerminal.prototype, "matchCount", void 0);
790
- __decorate([
791
- state()
792
- ], HelixHeadlessTerminal.prototype, "invalid", void 0);
793
684
  __decorate([
794
685
  state()
795
686
  ], HelixHeadlessTerminal.prototype, "tabMovesFocus", void 0);
@@ -29,6 +29,10 @@ declare const HelixHeadlessTreeItem_base: (new (...args: any[]) => import("@fusi
29
29
  * A `disabled` item is reached by the arrow keys and announced as unavailable,
30
30
  * and a press, Enter or Space on it does nothing.
31
31
  *
32
+ * A closed branch's children are hidden until found: the browser's find and
33
+ * Find search them, and the branch opens on the one that holds the match,
34
+ * unless `helix-before-open` is canceled.
35
+ *
32
36
  * A `lazy` item is a branch whose children you load when it opens: opening it
33
37
  * sends `helix-load`, the row says it is busy, and the children you add appear
34
38
  * in it. Remove `lazy` once they are in.
@@ -264,6 +268,13 @@ export declare class HelixHeadlessTreeItem extends HelixHeadlessTreeItem_base {
264
268
  * its children.
265
269
  */
266
270
  private follow;
271
+ /**
272
+ * A search found a match among the closed branch's children, and shows it
273
+ * next: the browser's find or Find. The branch opens, and where the page
274
+ * refuses, its children are hidden again once the search has shown them, as
275
+ * Accordion's section is.
276
+ */
277
+ private readonly handleBeforeMatch;
267
278
  /** A press on the check mark checks the item and leaves a branch as it is. */
268
279
  private readonly handleCheckClick;
269
280
  private readonly handleToggleClick;
@@ -21,6 +21,19 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
21
21
  this.focusable = false;
22
22
  this.value = '';
23
23
  this.branch = false;
24
+ this.handleBeforeMatch = (event) => {
25
+ if (this.open || this.requestOpen(true)) {
26
+ return;
27
+ }
28
+ const group = event.currentTarget;
29
+ const observer = new MutationObserver(() => {
30
+ observer.disconnect();
31
+ if (!this.open && !group.hasAttribute('hidden')) {
32
+ group.setAttribute('hidden', 'until-found');
33
+ }
34
+ });
35
+ observer.observe(group, { attributeFilter: ['hidden'] });
36
+ };
24
37
  this.handleCheckClick = (event) => {
25
38
  event.preventDefault();
26
39
  event.stopPropagation();
@@ -227,7 +240,12 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
227
240
  branch reports children it has no relationship to. The pattern is
228
241
  \`treeitem\` containing \`group\`, which is also what an \`<li>\`
229
242
  holding a \`<ul>\` is. -->
230
- <div part="group" role="group" ?hidden=${!this.open}>
243
+ <div
244
+ part="group"
245
+ role=${this.open ? 'group' : nothing}
246
+ hidden=${this.open ? nothing : 'until-found'}
247
+ @beforematch=${this.handleBeforeMatch}
248
+ >
231
249
  <slot name="children"></slot>
232
250
  </div>
233
251
  </div>`;