-
Notifications
You must be signed in to change notification settings - Fork 4
fix(code-block)!: errors following a full review of the component (#DS-5482) #1988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,9 +2,9 @@ import { DOCUMENT } from '@angular/common'; | ||||||||||||||||
| import { | |||||||||||||||||
| booleanAttribute, | |||||||||||||||||
| Directive, | |||||||||||||||||
| effect, | |||||||||||||||||
| inject, | |||||||||||||||||
| InjectionToken, | |||||||||||||||||
| Input, | |||||||||||||||||
| input, | |||||||||||||||||
| isDevMode, | |||||||||||||||||
| numberAttribute, | |||||||||||||||||
|
|
@@ -95,6 +95,7 @@ export class KbqCodeBlockHighlight { | ||||||||||||||||
| private readonly window = inject(KBQ_WINDOW); | |||||||||||||||||
| private readonly config = inject(KBQ_CODE_BLOCK_HIGHLIGHT_JS_CONFIG, { optional: true }); | |||||||||||||||||
| private hljs: HLJSApi | null = null; | |||||||||||||||||
| private hljsLoading: Promise<void> | null = null; | |||||||||||||||||
| private readonly _pending = signal(false); | |||||||||||||||||
|
|
|||||||||||||||||
| /** | |||||||||||||||||
|
|
@@ -105,17 +106,32 @@ export class KbqCodeBlockHighlight { | ||||||||||||||||
| readonly pending = this._pending.asReadonly(); | |||||||||||||||||
|
|
|||||||||||||||||
| /** The code file. */ | |||||||||||||||||
| // TODO: Skipped for migration because: | |||||||||||||||||
| // Accessor inputs cannot be migrated as they are too complex. | |||||||||||||||||
| @Input({ required: true }) | |||||||||||||||||
| set file(file: KbqCodeBlockFile) { | |||||||||||||||||
| if (!this.window) return; | |||||||||||||||||
| readonly file = input.required<KbqCodeBlockFile>(); | |||||||||||||||||
|
|
|||||||||||||||||
| if (!this.hljs) { | |||||||||||||||||
| this.load(this.config ?? {}).then(() => this.highlight(file)); | |||||||||||||||||
| } else { | |||||||||||||||||
| this.highlight(file); | |||||||||||||||||
| } | |||||||||||||||||
| constructor() { | |||||||||||||||||
| effect((onCleanup) => { | |||||||||||||||||
| const file = this.file(); | |||||||||||||||||
|
|
|||||||||||||||||
| if (!this.window) return; | |||||||||||||||||
|
|
|||||||||||||||||
| if (this.hljs) { | |||||||||||||||||
| this.highlight(file); | |||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The effect's dependency set is history-dependent:
Verified by counting
Once live, each gutter-only step also costs a full
|
|||||||||||||||||
|
|
|||||||||||||||||
| return; | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| // A `file` that arrives while highlight.js is still loading must not start a second load, | |||||||||||||||||
| // and must not highlight through a directive that has gone away in the meantime. | |||||||||||||||||
| let cancelled = false; | |||||||||||||||||
|
|
|||||||||||||||||
| onCleanup(() => (cancelled = true)); | |||||||||||||||||
|
|
|||||||||||||||||
| this.loadOnce().then(() => { | |||||||||||||||||
| if (!cancelled) { | |||||||||||||||||
| this.highlight(file); | |||||||||||||||||
| } | |||||||||||||||||
| }); | |||||||||||||||||
| }); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| /** The starting line number. */ | |||||||||||||||||
|
|
@@ -124,6 +140,18 @@ export class KbqCodeBlockHighlight { | ||||||||||||||||
| /** Whether to display line numbers for single line code block. */ | |||||||||||||||||
| readonly singleLine = input<boolean, unknown>(false, { transform: booleanAttribute }); | |||||||||||||||||
|
|
|||||||||||||||||
| /** Loads highlight.js at most once, so overlapping `file` changes share a single import. */ | |||||||||||||||||
| private loadOnce(): Promise<void> { | |||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Verified: a host with three
The intra-instance race this fixes is real and the fix works (3 rapid |
|||||||||||||||||
| this.hljsLoading ??= this.load(this.config ?? {}).finally(() => { | |||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A failed load un-latches
Verified with a rejecting Consequences in the host:
Triggers: a 404 on the lazy The comment on the next line says the failure is not latched — the load is not, private async load({ core, languages }: KbqCodeBlockHighlightJsConfig): Promise<void> {
this._pending.set(true);
try {
// …
} catch (error) {
this._pending.set(false);
throw error;
}
}…plus a rejection handler on the |
|||||||||||||||||
| if (!this.hljs) { | |||||||||||||||||
| // The load failed; let a later file change try again rather than latching the failure. | |||||||||||||||||
| this.hljsLoading = null; | |||||||||||||||||
| } | |||||||||||||||||
| }); | |||||||||||||||||
|
|
|||||||||||||||||
| return this.hljsLoading; | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| private async load({ core, languages }: KbqCodeBlockHighlightJsConfig): Promise<void> { | |||||||||||||||||
| this._pending.set(true); | |||||||||||||||||
|
|
|||||||||||||||||
|
|
|||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -939,4 +939,81 @@ describe(KbqCodeBlock.name, () => { | |
| expect(scrollSpy).toHaveBeenCalledWith({ top: 100 }); | ||
| })); | ||
| }); | ||
| it('should report undefined for an unbound maxHeight', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A genuinely unbound Rename to say the input is bound to |
||
| const fixture = createComponent(BaseCodeBlock); | ||
| const codeBlock = geCodeBlockDebugElement(fixture.debugElement).componentInstance as KbqCodeBlock; | ||
|
|
||
| fixture.detectChanges(); | ||
|
|
||
| expect(codeBlock.maxHeight()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should drop the calculated max height once viewAll is on', () => { | ||
| const fixture = createComponent(BaseCodeBlock); | ||
| const { componentInstance } = fixture; | ||
| const codeBlock = geCodeBlockDebugElement(fixture.debugElement).componentInstance as KbqCodeBlock; | ||
|
|
||
| componentInstance.maxHeight = 200; | ||
| fixture.detectChanges(); | ||
|
|
||
| const main = fixture.nativeElement.querySelector('.kbq-code-block__main') as HTMLElement; | ||
|
|
||
| expect(codeBlock.maxHeight()).toBe(200); | ||
| expect(main.style.maxHeight).toBe('200px'); | ||
|
|
||
| codeBlock.toggleViewAll(); | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(main.style.maxHeight).toBe(''); | ||
| }); | ||
|
|
||
| it('should expose the highlighted file on the highlight directive', () => { | ||
| const fixture = createComponent(BaseCodeBlock); | ||
|
|
||
| fixture.detectChanges(); | ||
|
|
||
| const highlight = fixture.debugElement | ||
| .query(By.directive(KbqCodeBlockHighlight)) | ||
| .injector.get(KbqCodeBlockHighlight); | ||
|
|
||
| expect(highlight.file().filename).toBe('index.html'); | ||
|
|
||
| fixture.componentInstance.activeFileIndex = 1; | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(highlight.file().filename).toBe('main.ts'); | ||
| }); | ||
| it('should follow a maxHeight that changes after init', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This duplicates the first half of the test above it and pins nothing the PR changed. Lines 987–995 are statement for statement lines 952–962: same It is also the test that runs entirely inside the dead zone of Suggest folding the |
||
| const fixture = createComponent(BaseCodeBlock); | ||
| const { componentInstance } = fixture; | ||
|
|
||
| componentInstance.maxHeight = 200; | ||
| fixture.detectChanges(); | ||
|
|
||
| const main = fixture.nativeElement.querySelector('.kbq-code-block__main') as HTMLElement; | ||
|
|
||
| expect(main.style.maxHeight).toBe('200px'); | ||
|
|
||
| componentInstance.maxHeight = 400; | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(main.style.maxHeight).toBe('400px'); | ||
| }); | ||
|
|
||
| it('should reset the active file when it falls outside a shorter file list', () => { | ||
| const fixture = createComponent(BaseCodeBlock); | ||
| const { componentInstance } = fixture; | ||
| const codeBlock = geCodeBlockDebugElement(fixture.debugElement).componentInstance as KbqCodeBlock; | ||
|
|
||
| componentInstance.activeFileIndex = 2; | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(codeBlock.activeFileIndex).toBe(2); | ||
|
|
||
| // Two files leave 0 and 1 valid, so the index has to come back into range. | ||
| componentInstance.files = componentInstance.files.slice(0, 2); | ||
| fixture.detectChanges(); | ||
|
|
||
| expect(codeBlock.activeFileIndex).toBe(0); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence contradicts the subsections it points at, and
mainhas already replaced the whole paragraph.§18 contains exactly four
####subsections:Code block(1052),Search expandable(1078),Split button(1100),Title(1122). The sentence lists search-expandable, split-button and title as first wave and then says "the second is the one each subsection below belongs to" — so a reader concludes those three were reviewed twice in 20.3.0.migration.ru.md:1042repeats it verbatim.Meanwhile
origin/mainalready retitled the section to### 18. Component review (21.0.0)and rewrote the intro to:which solves the same problem properly — so this hunk is both a merge conflict and a regression on top of it. Rebasing onto main's rewrite drops the wave framing entirely.
One more inaccuracy in the same hunk: it says
softWrap,viewAll,canDownload,activeFileIndexandfiles"stay accessor inputs". At the merge base four of the five were plain public fields (@Input({ transform: booleanAttribute }) softWrap: boolean = false;) — they became accessors here, which moves them off the instance (Object.keys, spread,JSON.stringify) and makes a subclass'soverride softWrap = trueshadow the accessor under a consumer's defaultuseDefineForClassFields: true.