Skip to content
Open
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
@@ -1,4 +1,6 @@
<div class="code-example">
<div
class="code-example"
[class.code-example--preview-outside]="previewOutside()">
@if (label()) {
<h2 class="code-example__label" [id]="instanceId + '-label'">
{{ label() }}
Expand All @@ -11,18 +13,20 @@ <h2 class="code-example__label" [id]="instanceId + '-label'">
[attr.aria-labelledby]="label() ? instanceId + '-label' : null"
[attr.aria-label]="label() ? null : 'Code example'"
(keydown)="navigateTabs($event)">
<button
type="button"
role="tab"
class="code-example__tab"
[id]="instanceId + '-tab-preview'"
[attr.aria-selected]="activeTab() === 'preview'"
[attr.aria-controls]="instanceId + '-panel-preview'"
[tabindex]="activeTab() === 'preview' ? 0 : -1"
[class.code-example__tab--active]="activeTab() === 'preview'"
(click)="activeTab.set('preview')">
Preview
</button>
@if (!previewOutside()) {
<button
type="button"
role="tab"
class="code-example__tab"
[id]="instanceId + '-tab-preview'"
[attr.aria-selected]="activeTab() === 'preview'"
[attr.aria-controls]="instanceId + '-panel-preview'"
[tabindex]="activeTab() === 'preview' ? 0 : -1"
[class.code-example__tab--active]="activeTab() === 'preview'"
(click)="activeTab.set('preview')">
Preview
</button>
}
@if (htmlCode()) {
<button
type="button"
Expand Down Expand Up @@ -54,13 +58,18 @@ <h2 class="code-example__label" [id]="instanceId + '-label'">
</div>

<div
role="tabpanel"
#previewPanel
[attr.role]="previewOutside() ? null : 'tabpanel'"
class="code-example__preview"
[id]="instanceId + '-panel-preview'"
[attr.aria-labelledby]="instanceId + '-tab-preview'"
[attr.aria-hidden]="activeTab() !== 'preview'"
[tabindex]="previewTabIndex()"
[class.code-example__panel--hidden]="activeTab() !== 'preview'">
[attr.id]="previewOutside() ? null : instanceId + '-panel-preview'"
[attr.aria-labelledby]="
previewOutside() ? null : instanceId + '-tab-preview'
"
[attr.aria-hidden]="previewOutside() ? null : activeTab() !== 'preview'"
[attr.tabindex]="previewOutside() ? null : previewTabIndex()"
[class.code-example__panel--hidden]="
!previewOutside() && activeTab() !== 'preview'
">
<ng-content></ng-content>
</div>

Expand Down Expand Up @@ -89,7 +98,10 @@ <h2 class="code-example__label" [id]="instanceId + '-label'">
: ''
}}</span>
<pre
#htmlPre
class="hljs"
role="group"
[attr.tabindex]="htmlCodeTabIndex()"
[attr.aria-label]="
(label() || 'Code') + ' HTML snippet'
"><code [innerHTML]="highlightedCode()"></code></pre>
Expand Down Expand Up @@ -121,7 +133,10 @@ <h2 class="code-example__label" [id]="instanceId + '-label'">
: ''
}}</span>
<pre
#tsPre
class="hljs"
role="group"
[attr.tabindex]="tsCodeTabIndex()"
[attr.aria-label]="
(label() || 'Code') + ' TypeScript snippet'
"><code [innerHTML]="highlightedTsCode()"></code></pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,113 @@
overflow-x: auto;
font-size: 0.8125rem;
line-height: 1.6;
border-radius: inherit;

&:focus-visible {
outline: none;
box-shadow:
inset 0 0 0 0.0625rem var(--cps-color-calm),
inset 0 0 0 0.125rem var(--cps-background-color),
inset 0 0 0 0.1875rem var(--cps-color-calm-highlighten);
}
}

code {
font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
'Liberation Mono', monospace;
}

// The atom-one-light/dark hljs themes (loaded globally at runtime) fail
// WCAG AA contrast for several syntax classes against our background.
// Darkened/lightened variants of the same hues restore compliance.
::ng-deep {
.hljs-comment,
.hljs-quote {
color: #71727a;
}

.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #da3020;
}

.hljs-literal {
color: #017bae;
}

.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string {
color: #40803f;
}

.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #2d6af1;
}

.hljs-built_in,
.hljs-title.class_,
.hljs-class .hljs-title {
color: #9b6a01;
}
}
}

&__copy {
position: absolute;
top: 0.625rem;
right: 0.75rem;
z-index: 1;
background-color: var(--cps-surface-elevated);
border-radius: 0.25rem;
}

&__panel--hidden {
display: none;
}

&--preview-outside {
display: flex;
flex-direction: column;

.code-example__card {
display: contents;
}

.code-example__preview {
order: 1;
padding: 0;
margin-bottom: 1.5rem;
background-color: transparent;
border-radius: 0;

&:empty {
margin-bottom: 0;
}
}

.code-example__tabbar {
order: 2;
border: 0.0625rem solid var(--cps-color-line);
border-radius: 0.4375rem 0.4375rem 0 0;
}

.code-example__code {
order: 3;
border: 0.0625rem solid var(--cps-color-line);
border-top: none;
border-radius: 0 0 0.4375rem 0.4375rem;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,47 +250,108 @@ describe('CodeExampleComponent', () => {
});
});

describe('tabindex', () => {
it('preview tabpanel has tabindex -1 by default (interactive content)', () => {
describe('previewOutside', () => {
it('does not render a Preview tab', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('tsCode', 'const x = 1;');
fixture.componentRef.setInput('previewOutside', true);
fixture.detectChanges();

const tabs = fixture.debugElement.queryAll(By.css('[role="tab"]'));
expect(tabs.length).toBe(2);
expect(tabs[0].nativeElement.textContent.trim()).toBe('HTML');
expect(tabs[1].nativeElement.textContent.trim()).toBe('TS');
});

it('never hides the preview panel, regardless of the active tab', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('tsCode', 'const x = 1;');
fixture.componentRef.setInput('previewOutside', true);
fixture.detectChanges();
component.activeTab.set('ts');
fixture.detectChanges();

const previewPanel = fixture.debugElement.query(
By.css('[id*="-panel-preview"]')
By.css('.code-example__preview')
);
expect(previewPanel.nativeElement.tabIndex).toBe(-1);
expect(previewPanel).not.toBeNull();
expect(
previewPanel.nativeElement.classList.contains(
'code-example__panel--hidden'
)
).toBe(false);
expect(previewPanel.nativeElement.getAttribute('aria-hidden')).toBe(null);
});

it('preview tabpanel has tabindex 0 when isPreviewNonInteractive is true', () => {
it('does not render tab-related ARIA attributes on the preview panel', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('isPreviewNonInteractive', true);
fixture.componentRef.setInput('previewOutside', true);
fixture.detectChanges();

const previewPanel = fixture.debugElement.query(
By.css('[id*="-panel-preview"]')
By.css('.code-example__preview')
);
expect(previewPanel.nativeElement.getAttribute('role')).toBe(null);
expect(previewPanel.nativeElement.getAttribute('id')).toBe(null);
expect(previewPanel.nativeElement.getAttribute('aria-labelledby')).toBe(
null
);
expect(previewPanel.nativeElement.tabIndex).toBe(0);
});

it('preview tabpanel tabindex updates reactively when input changes', () => {
it('renders the host with the preview-outside modifier class', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('previewOutside', true);
fixture.detectChanges();

const host = fixture.debugElement.query(By.css('.code-example'));
expect(
host.nativeElement.classList.contains('code-example--preview-outside')
).toBe(true);
});

it('defaults active tab to the first available tab instead of preview', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('tsCode', 'const x = 1;');
fixture.componentRef.setInput('previewOutside', true);
fixture.detectChanges();

expect(component.activeTab()).toBe('html');
});
});

describe('tabindex', () => {
it('preview tabpanel has tabindex -1 by default (interactive content)', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('isPreviewNonInteractive', false);
fixture.detectChanges();

const previewPanel = fixture.debugElement.query(
By.css('[id*="-panel-preview"]')
);
expect(previewPanel.nativeElement.tabIndex).toBe(-1);
});

fixture.componentRef.setInput('isPreviewNonInteractive', true);
it('code tabpanel pre elements default to tabindex -1 when not scrollable', () => {
fixture = TestBed.createComponent(CodeExampleComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('htmlCode', '<div></div>');
fixture.componentRef.setInput('tsCode', 'const x = 1;');
fixture.detectChanges();
expect(previewPanel.nativeElement.tabIndex).toBe(0);

const pres = fixture.debugElement.queryAll(By.css('pre.hljs'));
expect(pres[0].nativeElement.tabIndex).toBe(-1);
expect(pres[1].nativeElement.tabIndex).toBe(-1);
});
});
});
Loading
Loading