Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,6 @@
min-height: 0;
}

.panel-toggle {
display: none;
align-items: center;
gap: 7px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
color: var(--foreground);
font: 500 12px var(--sans);
padding: 8px 12px;
cursor: pointer;
}

.panel-toggle:hover {
background: var(--accent);
}

.panel-toggle:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}

.toggle-badge {
display: inline-flex;
min-width: 16px;
height: 16px;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--primary);
color: var(--primary-foreground);
font: 700 10px var(--mono);
padding: 0 4px;
}

.panel-backdrop {
display: none;
}
Expand Down Expand Up @@ -75,7 +40,7 @@
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 32px;
margin-top: 20px;
}

.panel-section:first-of-type {
Expand Down Expand Up @@ -495,10 +460,6 @@ app-array-picker {
height: auto;
}

.panel-toggle {
display: inline-flex;
}

.panel-backdrop {
display: block;
position: fixed;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
<button
#toggleButton
type="button"
class="panel-toggle"
data-tour="analysis-toggle"
aria-controls="app-analysis-panel"
[attr.aria-expanded]="open()"
(click)="toggle()"
>
Analysis
@if (result(); as diff) {
<span class="toggle-badge">{{ diff.summary.totalChanges }}</span>
}
</button>

@if (open()) {
<!-- Click-outside-to-dismiss only; Escape is already handled at the document level (see onEscape). -->
<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
<div class="panel-backdrop" (click)="close()"></div>
}

<aside id="app-analysis-panel" class="analysis-panel" [class.is-open]="open()" aria-label="Analysis">
<button type="button" class="drawer-close" (click)="close(true)" aria-label="Close analysis">×</button>
<button type="button" class="drawer-close" (click)="closeAndRestoreFocus()" aria-label="Close analysis">×</button>

@if (result(); as diff) {
<section class="panel-section">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
afterNextRender,
computed,
effect,
input,
output,
signal,
viewChild
} from '@angular/core';
import { ChangeDetectionStrategy, Component, afterNextRender, computed, effect, input, model, output, signal } from '@angular/core';
import { ArrayMatchAnalysis, CandidateStats, DiffResult, ScoreBreakdownTerm } from 'json-semantic-diff';
import { DIFF_EXAMPLES, DiffExample } from '../../examples';
import { decimalPercent, percent } from '../../shared/format';
Expand All @@ -28,8 +17,10 @@ import { pickRandomExample } from './analysis-panel-example';
* mutates comparison options, it just always shows the currently selected one.
*
* Above the drawer breakpoint it is a flex column with its own scroll. Below
* it, the panel becomes an off-canvas drawer behind a toggle button - the same
* signal-driven pattern used by `SidebarComponent`.
* it, the panel becomes an off-canvas drawer, driven by the `open` model() -
* the same pattern used by `SidebarComponent`. Its trigger button lives in
* HomeComponent's shared mobile panel bar rather than here; see that
* component's doc comment and `SidebarComponent`'s for why.
*/
@Component({
selector: 'app-analysis-panel',
Expand All @@ -43,8 +34,6 @@ import { pickRandomExample } from './analysis-panel-example';
}
})
export class AnalysisPanelComponent {
private readonly toggleButton = viewChild.required<ElementRef<HTMLButtonElement>>('toggleButton');

readonly result = input<DiffResult | null>(null);
/** The array currently explained; the parent defaults this to `primaryAnalysis`. */
readonly analysis = input<ArrayMatchAnalysis | null>(null);
Expand All @@ -60,8 +49,20 @@ export class AnalysisPanelComponent {
/** Fired when the user clicks the empty-state "Try [Example] example" link. */
readonly exampleRequested = output<DiffExample>();

/** Drawer state; only meaningful below the breakpoint, where the toggle shows. */
readonly open = signal(false);
/**
* Drawer state; only meaningful below the breakpoint, where the trigger
* button shows. A model() rather than a plain signal so the mobile trigger
* button - which lives in HomeComponent's shared panel bar, not here, see
* that component's doc comment - can drive it via `[(open)]`.
*/
readonly open = model(false);

/**
* Escape and the drawer's own "×" close the drawer AND should return focus
* to the external trigger button; HomeComponent listens for this to do
* that, since it owns that button and this component has no reference to it.
*/
readonly restoreFocusRequested = output<void>();

/**
* Always starts with DIFF_EXAMPLES[0] so server-prerendered HTML and the
Expand Down Expand Up @@ -118,13 +119,17 @@ export class AnalysisPanelComponent {
onEscape(event: Event): void {
const target = event.target as Element | null;
if (target?.closest?.('.cdk-overlay-container')) return;
this.close(true);
this.closeAndRestoreFocus();
}

close(restoreFocus = false): void {
if (!this.open()) return;
close(): void {
this.open.set(false);
if (restoreFocus) this.toggleButton().nativeElement.focus();
}

closeAndRestoreFocus(): void {
if (!this.open()) return;
this.close();
this.restoreFocusRequested.emit();
}

/** Heading for the selected-key section, branching on provenance not mechanism. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ app-array-picker {

.toggle-row {
display: flex;
align-items: flex-start;
align-items: center;
gap: 9px;
border-radius: var(--radius);
padding: 6px 8px;
Expand All @@ -36,7 +36,7 @@ app-array-picker {
}

.toggle-row input {
margin: 1px 0 0;
margin: 0;
accent-color: var(--primary);
cursor: pointer;
}
Expand All @@ -47,6 +47,7 @@ app-array-picker {
}

.toggle-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
Expand All @@ -58,9 +59,29 @@ app-array-picker {
font: 500 12px var(--sans);
}

.toggle-hint {
color: var(--muted-foreground);
font: 11px/1.4 var(--sans);
.hint-icon {
flex: none;
width: 16px;
height: 16px;
display: grid;
place-items: center;
border: 1px solid var(--border);
border-radius: 999px;
background: transparent;
color: var(--subtle-foreground);
font: 600 10px var(--sans);
line-height: 1;
cursor: help;
}

.hint-icon:hover {
color: var(--foreground);
border-color: var(--border-strong);
}

.hint-icon:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}

.field-list {
Expand Down Expand Up @@ -171,7 +192,8 @@ app-array-picker {
}

.matching-fieldset:disabled .toggle-row,
.matching-fieldset:disabled .toggle-row input {
.matching-fieldset:disabled .toggle-row input,
.matching-fieldset:disabled .hint-icon {
cursor: not-allowed;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,45 @@
<input type="radio" name="matching-strategy-disabled" checked disabled />
<span class="toggle-text">
<span class="toggle-name">Auto</span>
<span class="toggle-hint">JSON Semantic Diff chooses the best identity.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="JSON Semantic Diff chooses the best identity."
aria-label="What does Auto matching do?"
>
?
</button>
</label>

<label class="toggle-row">
<input type="radio" name="matching-strategy-disabled" disabled />
<span class="toggle-text">
<span class="toggle-name">Match by fields</span>
<span class="toggle-hint">Pick one or more fields that identify a record.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="Pick one or more fields that identify a record."
aria-label="What does Match by fields do?"
>
?
</button>
</label>

<label class="toggle-row">
<input type="radio" name="matching-strategy-disabled" disabled />
<span class="toggle-text">
<span class="toggle-name">Position</span>
<span class="toggle-hint">Compare elements by array position.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="Compare elements by array position."
aria-label="What does Position matching do?"
>
?
</button>
</label>

<div class="matching-actions">
Expand All @@ -38,16 +59,30 @@
<input type="radio" name="matching-strategy" [checked]="strategy() === 'auto'" (change)="strategy.set('auto')" />
<span class="toggle-text">
<span class="toggle-name">Auto</span>
<span class="toggle-hint">JSON Semantic Diff chooses the best identity.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="JSON Semantic Diff chooses the best identity."
aria-label="What does Auto matching do?"
>
?
</button>
</label>

<label class="toggle-row" [class.is-active]="strategy() === 'key'">
<input type="radio" name="matching-strategy" [checked]="strategy() === 'key'" (change)="strategy.set('key')" />
<span class="toggle-text">
<span class="toggle-name">Match by fields</span>
<span class="toggle-hint">Pick one or more fields that identify a record.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="Pick one or more fields that identify a record."
aria-label="What does Match by fields do?"
>
?
</button>
</label>

@if (strategy() === 'key') {
Expand Down Expand Up @@ -84,8 +119,15 @@
<input type="radio" name="matching-strategy" [checked]="strategy() === 'position'" (change)="strategy.set('position')" />
<span class="toggle-text">
<span class="toggle-name">Position</span>
<span class="toggle-hint">Compare elements by array position.</span>
</span>
<button
type="button"
class="hint-icon"
appTooltip="Compare elements by array position."
aria-label="What does Position matching do?"
>
?
</button>
</label>

@if (context.analysis.duplicateKeyCount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from 'json-semantic-diff';
import { percent } from '../../shared/format';
import { MatchingOverrideChange, overridePatternFor } from '../../shared/node-actions';
import { TooltipDirective } from '../../shared/tooltip/tooltip.directive';
import { ArrayPickerComponent } from '../array-picker/array-picker.component';

/** One configurable array: its core analysis plus the node holding the rows. */
Expand All @@ -30,7 +31,7 @@ type Strategy = 'auto' | 'key' | 'position';
@Component({
selector: 'app-array-matching',
standalone: true,
imports: [ArrayPickerComponent],
imports: [ArrayPickerComponent, TooltipDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './array-matching.component.html',
styleUrl: './array-matching.component.css'
Expand Down
Loading
Loading