From f5aa79a74d1f37b84d2e21b970dec91787e9c5e8 Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Fri, 17 Jul 2026 13:27:18 +0200 Subject: [PATCH 1/3] docs: add code examples for Scheduler, Select, Sidebar Menu --- .../code-example/code-example.component.html | 46 +-- .../code-example/code-example.component.scss | 34 +++ .../code-example.component.spec.ts | 79 +++++ .../code-example/code-example.component.ts | 5 +- .../scheduler-page.component.html | 7 +- .../scheduler-page.component.scss | 5 - .../scheduler-page.component.ts | 9 +- .../scheduler-page/scheduler-page.examples.ts | 29 ++ .../select-page/select-page.component.html | 282 +++++++++++------- .../select-page/select-page.component.scss | 6 - .../select-page/select-page.component.ts | 6 +- .../pages/select-page/select-page.examples.ts | 220 ++++++++++++++ .../sidebar-menu-page.component.html | 18 +- .../sidebar-menu-page.component.scss | 10 +- .../sidebar-menu-page.component.ts | 6 +- .../sidebar-menu-page.examples.ts | 60 ++++ 16 files changed, 669 insertions(+), 153 deletions(-) create mode 100644 projects/composition/src/app/pages/scheduler-page/scheduler-page.examples.ts create mode 100644 projects/composition/src/app/pages/select-page/select-page.examples.ts create mode 100644 projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.examples.ts diff --git a/projects/composition/src/app/components/code-example/code-example.component.html b/projects/composition/src/app/components/code-example/code-example.component.html index e2417ce31..e3070dc2c 100644 --- a/projects/composition/src/app/components/code-example/code-example.component.html +++ b/projects/composition/src/app/components/code-example/code-example.component.html @@ -1,4 +1,6 @@ -
+
@if (label()) {

{{ label() }} @@ -11,18 +13,20 @@

[attr.aria-labelledby]="label() ? instanceId + '-label' : null" [attr.aria-label]="label() ? null : 'Code example'" (keydown)="navigateTabs($event)"> - + @if (!previewOutside()) { + + } @if (htmlCode()) {

+ [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' + ">
diff --git a/projects/composition/src/app/components/code-example/code-example.component.scss b/projects/composition/src/app/components/code-example/code-example.component.scss index b3787eb52..e69024fb8 100644 --- a/projects/composition/src/app/components/code-example/code-example.component.scss +++ b/projects/composition/src/app/components/code-example/code-example.component.scss @@ -97,9 +97,43 @@ top: 0.625rem; right: 0.75rem; z-index: 1; + + ::ng-deep button { + background-color: var(--cps-surface-elevated) !important; + } } &__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; + } + + .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; + } + } } diff --git a/projects/composition/src/app/components/code-example/code-example.component.spec.ts b/projects/composition/src/app/components/code-example/code-example.component.spec.ts index df8b77430..8c7dc4e56 100644 --- a/projects/composition/src/app/components/code-example/code-example.component.spec.ts +++ b/projects/composition/src/app/components/code-example/code-example.component.spec.ts @@ -250,6 +250,85 @@ describe('CodeExampleComponent', () => { }); }); + describe('previewOutside', () => { + it('does not render a Preview tab', () => { + fixture = TestBed.createComponent(CodeExampleComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('htmlCode', '
'); + 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', '
'); + 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('.code-example__preview') + ); + expect(previewPanel).not.toBeNull(); + expect( + previewPanel.nativeElement.classList.contains( + 'code-example__panel--hidden' + ) + ).toBe(false); + expect(previewPanel.nativeElement.getAttribute('aria-hidden')).toBe(null); + }); + + it('does not render tab-related ARIA attributes on the preview panel', () => { + fixture = TestBed.createComponent(CodeExampleComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('htmlCode', '
'); + fixture.componentRef.setInput('previewOutside', true); + fixture.detectChanges(); + + const previewPanel = fixture.debugElement.query( + 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 + ); + }); + + it('renders the host with the preview-outside modifier class', () => { + fixture = TestBed.createComponent(CodeExampleComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('htmlCode', '
'); + 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', '
'); + 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); diff --git a/projects/composition/src/app/components/code-example/code-example.component.ts b/projects/composition/src/app/components/code-example/code-example.component.ts index 23f81568d..83ba51787 100644 --- a/projects/composition/src/app/components/code-example/code-example.component.ts +++ b/projects/composition/src/app/components/code-example/code-example.component.ts @@ -32,6 +32,7 @@ export class CodeExampleComponent { tsCode = input(); label = input(''); isPreviewNonInteractive = input(false); + previewOutside = input(false); private sanitizer = inject(DomSanitizer); @@ -55,7 +56,7 @@ export class CodeExampleComponent { ); } - const availableTabs: TabId[] = ['preview']; + const availableTabs: TabId[] = this.previewOutside() ? [] : ['preview']; if (htmlCode) { const htmlResult = hljs.highlight(htmlCode.trim(), { language: 'xml' }); @@ -78,7 +79,7 @@ export class CodeExampleComponent { this.tabs.set(availableTabs); if (!this.tabs().includes(this.activeTab())) { - this.activeTab.set('preview'); + this.activeTab.set(availableTabs[0] ?? 'preview'); } }); } diff --git a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.html b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.html index 3d3b1ae7b..fc245b876 100644 --- a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.html +++ b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.html @@ -2,7 +2,10 @@ [componentData]="componentData" [services]="serviceData"> -
+ -
+ diff --git a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.scss b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.scss index 49eecc429..e69de29bb 100644 --- a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.scss +++ b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.scss @@ -1,5 +0,0 @@ -.scheduler-wrapper { - margin-left: 0.5rem; - margin-right: 0.5rem; - margin-bottom: 0.5rem; -} diff --git a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.ts b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.ts index 21e95a51d..7f2ee0c3c 100644 --- a/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.ts +++ b/projects/composition/src/app/pages/scheduler-page/scheduler-page.component.ts @@ -3,10 +3,16 @@ import { CpsSchedulerComponent } from 'cps-ui-kit'; import ComponentData from '../../api-data/cps-scheduler.json'; import ServiceData from '../../api-data/cps-cron-validation.json'; import { ComponentDocsViewerComponent } from '../../components/component-docs-viewer/component-docs-viewer.component'; +import { CodeExampleComponent } from '../../components/code-example/code-example.component'; +import { schedulerExamples } from './scheduler-page.examples'; @Component({ selector: 'app-scheduler-page', - imports: [CpsSchedulerComponent, ComponentDocsViewerComponent], + imports: [ + CpsSchedulerComponent, + ComponentDocsViewerComponent, + CodeExampleComponent + ], templateUrl: './scheduler-page.component.html', styleUrl: './scheduler-page.component.scss', host: { class: 'composition-page' } @@ -14,6 +20,7 @@ import { ComponentDocsViewerComponent } from '../../components/component-docs-vi export class SchedulerPageComponent { componentData = ComponentData; serviceData = [ServiceData]; + readonly examples = schedulerExamples; cronExpression = '30 9 ? 7/4 WED#5 *'; timeZone = 'UTC'; diff --git a/projects/composition/src/app/pages/scheduler-page/scheduler-page.examples.ts b/projects/composition/src/app/pages/scheduler-page/scheduler-page.examples.ts new file mode 100644 index 000000000..fabbad700 --- /dev/null +++ b/projects/composition/src/app/pages/scheduler-page/scheduler-page.examples.ts @@ -0,0 +1,29 @@ +export const schedulerExamples: Record = + { + basicUsage: { + html: ` +`, + ts: ` +cronExpression = '30 9 ? 7/4 WED#5 *'; +timeZone = 'UTC'; + +onCronExpressionChanged(cron: string) { + console.log('CRON expression', cron); +} + +onTimezoneChanged(tz: string) { + console.log('Time zone', tz); +}` + } + }; diff --git a/projects/composition/src/app/pages/select-page/select-page.component.html b/projects/composition/src/app/pages/select-page/select-page.component.html index 88dd53253..59167351d 100644 --- a/projects/composition/src/app/pages/select-page/select-page.component.html +++ b/projects/composition/src/app/pages/select-page/select-page.component.html @@ -1,7 +1,10 @@ -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- - - - - - - - - - -
- - {{ syncVal }} -
- - - - + [(ngModel)]="syncVal" + [ngModelOptions]="{ standalone: true }"> + {{ syncVal }}
- +
+ + + + + + + + + + diff --git a/projects/composition/src/app/pages/select-page/select-page.component.scss b/projects/composition/src/app/pages/select-page/select-page.component.scss index 18606114a..fe6f52680 100644 --- a/projects/composition/src/app/pages/select-page/select-page.component.scss +++ b/projects/composition/src/app/pages/select-page/select-page.component.scss @@ -1,9 +1,3 @@ -.selects-group { - gap: 1.5rem; - display: flex; - flex-direction: column; -} - .sync-val-example { display: flex; align-items: center; diff --git a/projects/composition/src/app/pages/select-page/select-page.component.ts b/projects/composition/src/app/pages/select-page/select-page.component.ts index f6d3bb533..92cc7f581 100644 --- a/projects/composition/src/app/pages/select-page/select-page.component.ts +++ b/projects/composition/src/app/pages/select-page/select-page.component.ts @@ -10,13 +10,16 @@ import { CpsSelectComponent } from 'cps-ui-kit'; import ComponentData from '../../api-data/cps-select.json'; import { ComponentDocsViewerComponent } from '../../components/component-docs-viewer/component-docs-viewer.component'; +import { CodeExampleComponent } from '../../components/code-example/code-example.component'; +import { selectExamples } from './select-page.examples'; @Component({ imports: [ CpsSelectComponent, FormsModule, ReactiveFormsModule, - ComponentDocsViewerComponent + ComponentDocsViewerComponent, + CodeExampleComponent ], selector: 'app-select-page', templateUrl: './select-page.component.html', @@ -24,6 +27,7 @@ import { ComponentDocsViewerComponent } from '../../components/component-docs-vi host: { class: 'composition-page' } }) export class SelectPageComponent implements OnInit { + readonly examples = selectExamples; options = [ { name: 'New York', data: { code: 'NY' } }, { name: 'Prague', data: { code: 'PRG' }, info: 'Prague info' }, diff --git a/projects/composition/src/app/pages/select-page/select-page.examples.ts b/projects/composition/src/app/pages/select-page/select-page.examples.ts new file mode 100644 index 000000000..14aa67894 --- /dev/null +++ b/projects/composition/src/app/pages/select-page/select-page.examples.ts @@ -0,0 +1,220 @@ +const citiesOptionsTs = ` +options = [ + { name: 'New York', data: { code: 'NY' } }, + { name: 'Prague', data: { code: 'PRG' }, info: 'Prague info' }, + { name: 'Capetown', data: { code: 'CPT' }, info: 'Capetown info' }, + { name: 'Rome', data: { code: 'RM' } }, + { name: 'London', data: { code: 'LDN' }, info: 'London info' }, + { name: 'Istanbul', data: { code: 'IST' } }, + { name: 'Paris', data: { code: 'PRS' } }, + { name: 'Tokyo', data: { code: 'TOK' } }, + { name: 'Oslo', data: { code: 'OSL' }, info: 'Oslo info' }, + { name: 'Berlin', data: { code: 'BER' } } +];`; + +export const selectExamples: Record = { + requiredSelect: { + html: ` +
+ + +
`, + ts: ` +private readonly _formBuilder = inject(UntypedFormBuilder); + +${citiesOptionsTs.trim()} + +form!: UntypedFormGroup; + +ngOnInit(): void { + this.form = this._formBuilder.group({ + requiredSelect: [this.options[1], [Validators.required]] + }); +}` + }, + + loadingSelect: { + html: ` + +`, + ts: citiesOptionsTs + }, + + disabledSelect: { + html: ` + +` + }, + + multipleSelect: { + html: ` +`, + ts: citiesOptionsTs + }, + + virtualScrollSelect: { + html: ` +`, + ts: citiesOptionsTs + }, + + nonClosableChipsSelect: { + html: ` +`, + ts: citiesOptionsTs + }, + + prefixIconSelect: { + html: ` + +`, + ts: citiesOptionsTs + }, + + iconsSelect: { + html: ` + +`, + ts: ` +statusOptions = [ + { name: 'Success', icon: 'circle', color: 'success' }, + { name: 'Pending', icon: 'circle', color: 'info' }, + { name: 'Warning', icon: 'circle', color: 'warn' }, + { name: 'Failed', icon: 'circle', color: 'error' } +];` + }, + + twoWayBindingSelect: { + html: ` +
+ + {{ syncVal }} +
`, + ts: ` +syncOptions = [ + { title: 'Amazon', val: 'AMZN', ticker: 'AMZN' }, + { title: 'Apple', val: 'AAPL', ticker: 'AAPL' }, + { title: 'Google', val: 'GOOGL', ticker: 'GOOGL' }, + { title: 'Meta', val: 'META', ticker: 'META' }, + { title: 'Microsoft', val: 'MSFT', ticker: 'MSFT' }, + { title: 'Netflix', val: 'NFLX', ticker: 'NFLX' }, + { title: 'Tesla', val: 'TSLA', ticker: 'TSLA' } +]; +syncVal: any = [];` + }, + + underlinedSelect: { + html: ` + +`, + ts: citiesOptionsTs + }, + + borderlessSelect: { + html: ` + +`, + ts: citiesOptionsTs + } +}; diff --git a/projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.component.html b/projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.component.html index 2573c62aa..0ade5de03 100644 --- a/projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.component.html +++ b/projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.component.html @@ -5,11 +5,19 @@ [exactRoutes]="true" ariaLabel="Sidebar menu example"> -