Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
d7883ac
feat(org-lens): scaffold project detail page, route & demo data
danoqualls Jun 8, 2026
1dacc10
feat(org-lens): add project detail breadcrumb and hero
danoqualls Jun 8, 2026
c34092e
feat(org-lens): add our influence technical and ecosystem cards
danoqualls Jun 8, 2026
60e67e4
feat(org-lens): add influence trend chart
danoqualls Jun 8, 2026
d70ba2f
feat(org-lens): add leaderboards tab with toggles and pagination
danoqualls Jun 8, 2026
9fdd523
test(org-lens): add project detail e2e coverage
danoqualls Jun 8, 2026
5698a9f
refactor(org-lens): align project detail with design + lfx patterns
danoqualls Jun 8, 2026
25b1089
refactor(org-lens): two side-by-side leaderboards per prototype
danoqualls Jun 8, 2026
7f7c910
refactor(org-lens): unify influence cards with trendlines per prototype
danoqualls Jun 8, 2026
7ea46d5
refactor(org-lens): align project hero with My Org design
danoqualls Jun 8, 2026
1d3d083
refactor(org-lens): drop 'Project' suffix from ecosystem scope label
danoqualls Jun 8, 2026
d11cc7b
feat(org-lens): full influence card set (5 technical, 9 ecosystem)
danoqualls Jun 9, 2026
ddfac97
fix(org-lens): show org name context and load without selected account
danoqualls Jun 23, 2026
c76b798
feat(org-lens): polish influence cards and sparkline tooltips
danoqualls Jun 23, 2026
2198414
feat(org-lens): external HTML tooltip for sparkline cards
danoqualls Jun 23, 2026
c3bb528
feat(org-lens): hero cleanup, chip alignment, and card carousel arrows
danoqualls Jun 23, 2026
3fac23a
fix(org-lens): floating tooltip and hero identity layout
danoqualls Jun 23, 2026
678194d
fix(org-lens): move carousel arrows to section header row
danoqualls Jun 23, 2026
0fc4f6f
feat(org-lens): match card style to app reference and smart arrow vis…
danoqualls Jun 23, 2026
26f9671
feat(org-lens): vary chart types per metric and fix sparkline options…
danoqualls Jun 23, 2026
6aff8bb
fix(org-lens): fix member ordering and nested ternary lint errors
danoqualls Jun 23, 2026
87d7229
style(org-lens): match carousel arrow style to app.lfx.dev production
danoqualls Jun 24, 2026
bebe965
fix(org-lens): fix sparkline tooltip HTML and remove hover ring markers
danoqualls Jun 24, 2026
4a213a5
style(org-lens): pill tabs and section header divider lines on projec…
danoqualls Jun 24, 2026
aa067c7
feat(org-lens): card detail drawer with trend chart and monthly break…
danoqualls Jun 24, 2026
47a21a8
feat(org-lens): replace drawer chart with card-specific detail tables…
danoqualls Jun 24, 2026
60f0dd9
fix(org-lens): move p-drawer header template outside @if block
danoqualls Jun 24, 2026
ee6725c
fix(org-lens): add COCOMO tooltip to software value info icon
danoqualls Jun 24, 2026
98c9353
feat(org-lens): add paginator to influence leaderboard tables
danoqualls Jun 24, 2026
59ecbf4
feat(org-lens): show org logos in influence leaderboard tables
danoqualls Jun 24, 2026
fdeab1a
feat(org-lens): stacked area trend chart with time range selector on …
danoqualls Jun 24, 2026
ada3eab
fix(org-lens): use fill: stack for stacked area trend chart in Chart.…
danoqualls Jun 24, 2026
7dec43a
feat(org-lens): add tab bar component and restyle metric toggle
danoqualls Jun 24, 2026
b3b61bc
fix(org-lens): rework tab bar to match app.lfx.dev meetings page pattern
danoqualls Jun 24, 2026
8db7012
feat(org-lens): move metric toggle into tab bar, normalize trend to 100%
danoqualls Jun 24, 2026
9fbb1d9
fix(org-lens): tab bar layout, metric toggle placement, and chart pol…
danoqualls Jun 24, 2026
d6ada95
Potential fix for pull request finding
danoqualls Jun 25, 2026
5c325d9
Potential fix for pull request finding
danoqualls Jun 25, 2026
a8a2329
Potential fix for pull request finding
danoqualls Jun 25, 2026
4480a22
Potential fix for pull request finding
danoqualls Jun 25, 2026
91bd0e7
Potential fix for pull request finding
danoqualls Jun 25, 2026
1e047c4
Apply remaining changes
Copilot Jun 25, 2026
4fdd6a0
fix(org-lens): wire project row handlers and fix broken /org/projects…
Copilot Jun 25, 2026
8decc3c
fix(org-lens): address review comments — server route, handler wiring…
Copilot Jun 25, 2026
5b6bbcc
fix(review): address PR #1028 review feedback
danoqualls Jun 25, 2026
5e4f39b
Potential fix for pull request finding
danoqualls Jun 25, 2026
6ff84af
Potential fix for pull request finding
danoqualls Jun 25, 2026
6dec5a9
Potential fix for pull request finding
danoqualls Jun 25, 2026
4b56539
fix(review): address PR #1028 review feedback
danoqualls Jun 25, 2026
efc3d5e
Potential fix for pull request finding
danoqualls Jun 25, 2026
918b5c4
Potential fix for pull request finding
danoqualls Jun 25, 2026
903f756
Potential fix for pull request finding
danoqualls Jun 25, 2026
e48e9b9
Potential fix for pull request finding
danoqualls Jun 25, 2026
73998f6
Potential fix for pull request finding
danoqualls Jun 25, 2026
7d2c38e
Potential fix for pull request finding
danoqualls Jun 25, 2026
d53e638
fix(review): address PR #1028 review feedback (continued)
danoqualls Jun 25, 2026
49e0690
style(org-lens): fix prettier formatting on project detail files
danoqualls Jun 29, 2026
2c1cadd
fix(org-lens): remove unused OrgLensProjectHealth import
danoqualls Jun 29, 2026
74f99fe
fix(org-lens): address review 4593147828 findings
Copilot Jun 29, 2026
8b0803c
fix(org-lens): address PR #1028 review feedback (round 2)
danoqualls Jun 29, 2026
8d834c2
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jun 29, 2026
dc2a616
Potential fix for pull request finding
danoqualls Jun 29, 2026
8e6641e
Potential fix for pull request finding
danoqualls Jun 30, 2026
a978308
Potential fix for pull request finding
danoqualls Jun 30, 2026
b406405
Potential fix for pull request finding
danoqualls Jun 30, 2026
b2b39fc
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jun 30, 2026
200039d
fix(review): address PR #1028 review feedback (round 3)
danoqualls Jun 30, 2026
30ea9cf
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jun 30, 2026
3300654
fix(review): address PR #1028 review feedback (round 4)
danoqualls Jun 30, 2026
8aef932
style(org-lens): apply prettier formatting to project detail componen…
danoqualls Jun 30, 2026
895a694
fix(review): address PR #1028 review feedback (round 5)
danoqualls Jun 30, 2026
0eae6dd
fix(review): address PR #1028 review feedback (round 6)
danoqualls Jul 1, 2026
752f6c0
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jul 2, 2026
efbed98
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jul 2, 2026
f675b40
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jul 2, 2026
1c3ad78
fix(review): guard SEEDS lookup against prototype pollution
danoqualls Jul 2, 2026
ab80b8a
Merge branch 'main' into feat/LFXV2-1885
danoqualls Jul 2, 2026
2229321
Merge remote-tracking branch 'origin/feat/LFXV2-1885' into feat/LFXV2…
danoqualls Jul 2, 2026
a22a241
fix(review): point project detail breadcrumb/CTA to Projects list
danoqualls Jul 2, 2026
2a44667
fix(review): scope constants, drop dead hero/trend fields, fix stale doc
danoqualls Jul 2, 2026
4b0dc69
fix(review): fall back to demo org uid on project detail page
danoqualls Jul 2, 2026
83c6b79
fix(review): revert demo-org fallback and add row aria-label
danoqualls Jul 2, 2026
15b4d5d
fix(review): address PR #1028 review feedback from audigregorie
danoqualls Jul 3, 2026
0c0a69f
Merge remote-tracking branch 'origin/main' into feat/LFXV2-1885
danoqualls Jul 3, 2026
232083b
style(org-lens): fix prettier formatting in project detail component
danoqualls Jul 3, 2026
3597e1a
fix(review): address PR #1028 Copilot review feedback
danoqualls Jul 3, 2026
010c669
fix(review): address PR #1028 Copilot review feedback
danoqualls Jul 3, 2026
7ad8323
fix(review): address PR #1028 Copilot review feedback
danoqualls Jul 3, 2026
419b06c
Merge branch 'main' into feat/LFXV2-1885
MRashad26 Jul 6, 2026
1cc1b88
fix(review): address PR #1028 review feedback
danoqualls Jul 6, 2026
7021c43
Merge branch 'main' into feat/LFXV2-1885
MRashad26 Jul 6, 2026
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
131 changes: 131 additions & 0 deletions apps/lfx-one/e2e/org-project-detail.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

/**
* Org Lens · Project Detail Page E2E Tests (LFXV2-1885)
*
* Covers the acceptance criteria for the per-project Org Lens view:
* - data-testid resolution smoke test (breadcrumb, hero, tabs, card groups, trend)
* - tab strip switching (click + keyboard) with ?tab= URL persistence
* - leaderboard ranking, score/metric toggles with ?metric= persistence,
* Activity Count hiding the band tags, and Show more pagination
* - not-found (404) panel for an unknown slug
*
* Prerequisites:
* - Dev server running on the Playwright baseURL
* - User authenticated with the `org-lens-enabled` flag on and an organization selected
*
* Demo semantics (v1): the page is served from server-side demo fixtures
* (server/services/org-lens-project-detail.demo-data.ts). `kubernetes` is a rich seeded project; an
* unknown slug returns null → the not-found panel.
*/

import { expect, test } from '@playwright/test';

const DETAIL_URL = '/org/projects/kubernetes';
const DETAIL_URL_BOGUS = '/org/projects/totally-bogus-project';
const DATA_LOAD_TIMEOUT = 30_000;

test.setTimeout(90_000);

test.describe('Org Project Detail — testid resolution', () => {
test.beforeEach(async ({ page }) => {
await page.goto(DETAIL_URL, { waitUntil: 'domcontentloaded' });
await expect(page).not.toHaveURL(/auth0\.com/);
await expect(page.getByTestId('project-detail-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
});

test('renders breadcrumb, hero and tab strip', async ({ page }) => {
await expect(page.getByTestId('project-detail-breadcrumb')).toBeVisible();
await expect(page.getByTestId('project-detail-hero')).toBeVisible();
await expect(page.getByTestId('project-detail-name')).toHaveText('Kubernetes');
await expect(page.getByTestId('project-detail-first-commit')).toBeVisible();
await expect(page.getByTestId('project-detail-software-value')).toBeVisible();
await expect(page.getByTestId('project-detail-health-badge')).toBeVisible();
await expect(page.getByTestId('project-detail-foundation-pill')).toBeVisible();
await expect(page.getByTestId('project-detail-tab-pd-influence')).toBeVisible();
await expect(page.getByTestId('project-detail-tab-pd-leaderboards')).toBeVisible();
});

test('renders the Technical and Ecosystem card groups', async ({ page }) => {
for (const key of ['maintainers', 'contributors', 'commits', 'pull-requests']) {
await expect(page.getByTestId(`project-detail-technical-card-${key}`)).toBeVisible();
}
for (const key of ['collaboration', 'meeting-attendance', 'board-members', 'committee-members']) {
await expect(page.getByTestId(`project-detail-ecosystem-card-${key}`)).toBeVisible();
}

await page.getByTestId('project-detail-tab-pd-leaderboards').click();
await expect(page.getByTestId('project-detail-trend-group')).toBeVisible();
});
Comment thread
Copilot marked this conversation as resolved.
});

test.describe('Org Project Detail — tab strip', () => {
test.beforeEach(async ({ page }) => {
await page.goto(DETAIL_URL, { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('project-detail-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
});

test('defaults to Our Influence and switches to Leaderboards via click + URL', async ({ page }) => {
await expect(page.getByTestId('project-detail-technical-group')).toBeVisible();
await page.getByTestId('project-detail-tab-pd-leaderboards').click();
await expect(page).toHaveURL(/tab=pd-leaderboards/);
await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible();
});

test('deep-links to the Leaderboards tab via ?tab=', async ({ page }) => {
await page.goto(`${DETAIL_URL}?tab=pd-leaderboards`, { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
});
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.

test('arrow keys move between tabs', async ({ page }) => {
await page.getByTestId('project-detail-tab-pd-influence').focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByTestId('project-detail-tab-pd-leaderboards')).toBeFocused();
});
});

test.describe('Org Project Detail — leaderboards', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${DETAIL_URL}?tab=pd-leaderboards`, { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
});

test('renders both side-by-side boards with the viewing-org row pinned', async ({ page }) => {
await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible();
await expect(page.getByTestId('project-detail-leaderboard-ecosystem')).toBeVisible();
await expect(page.getByTestId('project-detail-leaderboard-technical-viewing-row')).toBeVisible();
await expect(page.getByTestId('project-detail-leaderboard-ecosystem-viewing-row')).toBeVisible();
await expect(page.getByTestId('project-detail-trend-group')).toBeVisible();
});

test('metric toggle persists in the URL and switches the score column', async ({ page }) => {
await expect(page.getByRole('columnheader', { name: 'Influence Score' }).first()).toBeVisible();
await page.getByTestId('project-detail-metric-activity').click();
await expect(page).toHaveURL(/metric=activity/);
await expect(page.getByRole('columnheader', { name: 'Activity (12mo)' }).first()).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Influence Score' })).toHaveCount(0);
});
Comment thread
danoqualls marked this conversation as resolved.

test('time-range toggle persists in the URL and updates the activity column label', async ({ page }) => {
await page.getByTestId('project-detail-metric-activity').click();
await page.getByTestId('project-detail-time-range-2y').click();
await expect(page).toHaveURL(/range=2y/);
await expect(page.getByRole('columnheader', { name: 'Activity (24mo)' }).first()).toBeVisible();
});

test('search filters a board to matching organizations', async ({ page }) => {
await page.locator('[data-test="project-detail-search-technical"]').fill('Google');
const rows = page.locator('[data-testid="project-detail-leaderboard-technical"] tbody tr');
await expect(rows).toHaveCount(1);
await expect(rows.first()).toContainText('Google');
});
});

test.describe('Org Project Detail — not found', () => {
test('renders the 404 panel for an unknown slug', async ({ page }) => {
await page.goto(DETAIL_URL_BOGUS, { waitUntil: 'domcontentloaded' });
await expect(page).not.toHaveURL(/auth0\.com/);
await expect(page.getByTestId('project-detail-not-found')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
});
});
10 changes: 10 additions & 0 deletions apps/lfx-one/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ export const routes: Routes = [
data: { lens: 'org', title: 'Projects', description: 'Projects your organization participates in.', icon: 'fa-light fa-folder' },
loadComponent: () => import('./modules/dashboards/org/org-projects/org-projects.component').then((m) => m.OrgProjectsComponent),
},
{
path: 'projects/:projectSlug',
data: {
lens: 'org',
title: 'Project Detail',
description: "Your organization's involvement and competitive standing on a project.",
icon: 'fa-light fa-folder',
},
loadComponent: () => import('./modules/dashboards/org/org-project-detail/org-project-detail.component').then((m) => m.OrgProjectDetailComponent),
},
Comment thread
danoqualls marked this conversation as resolved.
{
// INFO: Future Epic implementation — the ROI page is hidden; deep links fall
// back to the org overview until the org ROI feature is built.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,14 @@ <h2 class="flex items-center gap-2 text-base font-medium text-gray-900" data-tes
</thead>
<tbody>
@for (project of row.projects; track project.projectId) {
<!-- INFO: Future Epic implementation — rows are non-interactive while the
/org/projects drilldown is hidden. Restore the role/tabindex/cursor +
(click)/(keydown) handlers when the drilldown page is built. -->
<tr
class="border-b border-gray-100"
[class.cursor-pointer]="project.isLfProject"
[attr.role]="project.isLfProject ? 'button' : null"
[attr.tabindex]="project.isLfProject ? 0 : null"
[attr.aria-label]="project.isLfProject ? 'View project details for ' + project.projectName : null"
(click)="onProjectRowClick(project)"
(keydown)="onProjectRowKeydown($event, project)"
[attr.data-testid]="
'org-overview-foundations-and-projects-row-' +
(row.foundationSlug || row.foundationId) +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { DecimalPipe } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { Router, RouterLink } from '@angular/router';
import { SkeletonModule } from 'primeng/skeleton';
import { AccountContextService } from '@services/account-context.service';
import { OrgLensFoundationsService } from '@services/org-lens-foundations.service';
Expand All @@ -28,6 +28,7 @@ export class OrgOverviewFoundationsAndProjectsComponent {
private readonly accountContextService = inject(AccountContextService);
private readonly foundationsService = inject(OrgLensFoundationsService);
private readonly plausibleService = inject(PlausibleService);
private readonly router = inject(Router);

private readonly retryTrigger = signal(0);
private readonly expansionState = signal<Record<string, boolean>>({});
Expand Down Expand Up @@ -115,9 +116,27 @@ export class OrgOverviewFoundationsAndProjectsComponent {
});
}

// INFO: Future Epic implementation — project-row click/keydown handlers and their
// `mfp_project_row_click` telemetry were removed with the hidden /org/projects drilldown.
// Restore them alongside the row's interactive affordances when the drilldown is built.
protected onProjectClick(payload: { projectId: string; projectName: string }): void {
const orgId = this.accountContextService.selectedAccount().accountId;
this.plausibleService.trackEvent('mfp_project_row_click', {
orgId,
projectId: payload.projectId,
projectName: payload.projectName,
});
}

protected onProjectRowClick(project: OrgLensFoundationRow['projects'][number]): void {
if (!project.isLfProject) return;
this.onProjectClick({ projectId: project.projectId, projectName: project.projectName });
void this.router.navigate(['/org/projects', project.projectSlug || project.projectId]);
}

protected onProjectRowKeydown(event: KeyboardEvent, project: OrgLensFoundationRow['projects'][number]): void {
if (event.key !== 'Enter' && event.key !== ' ') return;
if (!project.isLfProject) return;
event.preventDefault();
this.onProjectRowClick(project);
}
Comment thread
danoqualls marked this conversation as resolved.

private emitOverviewViewOnce(orgUid: string): void {
if (this.viewedOrgs.has(orgUid)) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<!-- Left: tab pills -->
<div class="inline-flex items-center gap-1 rounded-full bg-gray-100 p-1" role="tablist" aria-label="Project detail sections" (keydown)="onTabKeydown($event)">
@for (tab of tabs(); track tab.id) {
<button
#tabBtn
type="button"
role="tab"
[id]="'project-detail-tab-trigger-' + tab.id"
[attr.aria-selected]="activeTab() === tab.id"
[attr.aria-controls]="'project-detail-tab-panel-' + tab.id"
[attr.tabindex]="activeTab() === tab.id ? 0 : -1"
class="flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors"
[class.bg-blue-600]="activeTab() === tab.id"
[class.text-white]="activeTab() === tab.id"
[class.text-gray-600]="activeTab() !== tab.id"
[attr.data-testid]="'project-detail-tab-' + tab.id"
(click)="tabChange.emit(tab.id)">
{{ tab.label }}
</button>
}
</div>

<!-- Right: optional metric toggle + time range dropdown -->
<div class="flex items-center gap-3">
@if (metricOptions(); as opts) {
<div class="flex items-center gap-1.5" role="group" aria-label="Metric" data-testid="project-detail-metric-toggle">
@for (opt of opts; track opt.id) {
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
[class.bg-blue-600]="metric() === opt.id"
[class.border-blue-600]="metric() === opt.id"
[class.text-white]="metric() === opt.id"
[class.bg-white]="metric() !== opt.id"
[class.border-gray-300]="metric() !== opt.id"
[class.text-gray-700]="metric() !== opt.id"
[attr.aria-pressed]="metric() === opt.id"
[attr.data-testid]="'project-detail-metric-' + opt.id"
(click)="metricChange.emit(opt.id)">
<i [class]="opt.icon" aria-hidden="true"></i>
{{ opt.label }}
</button>
}
</div>
}
<div class="flex items-center gap-1 rounded-full bg-gray-100 p-1" role="group" aria-label="Time range" data-testid="project-detail-time-range">
@for (opt of timeRangeOptions(); track opt.id) {
<button
type="button"
class="rounded-full px-3 py-1 text-sm font-medium transition-colors"
[class.bg-white]="timeRange() === opt.id"
[class.text-gray-900]="timeRange() === opt.id"
[class.shadow-sm]="timeRange() === opt.id"
[class.text-gray-600]="timeRange() !== opt.id"
[attr.aria-pressed]="timeRange() === opt.id"
[attr.data-testid]="'project-detail-time-range-' + opt.id"
(click)="timeRangeChange.emit(opt.id)">
{{ opt.label }}
</button>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { isPlatformBrowser } from '@angular/common';
import { Component, ElementRef, inject, input, output, PLATFORM_ID, viewChildren } from '@angular/core';
import type { OrgLensLeaderboardMetric, OrgLensLeaderboardTimeRange, OrgLensProjectDetailTab } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-org-project-detail-tab-bar',
host: {
class: 'flex w-full items-center justify-between gap-4 rounded-xl border border-gray-200 bg-white px-4 py-2.5 shadow-sm',
'data-testid': 'project-detail-controls-bar',
},
templateUrl: './org-project-detail-tab-bar.component.html',
})
export class OrgProjectDetailTabBarComponent {
private readonly tabBtns = viewChildren<ElementRef<HTMLButtonElement>>('tabBtn');

private readonly platformId = inject(PLATFORM_ID);

public readonly tabs = input.required<{ id: OrgLensProjectDetailTab; label: string }[]>();
public readonly activeTab = input.required<OrgLensProjectDetailTab>();
public readonly timeRange = input.required<OrgLensLeaderboardTimeRange>();
public readonly timeRangeOptions = input.required<{ id: OrgLensLeaderboardTimeRange; label: string }[]>();
/** When provided, renders the metric toggle between the tab pills and the time dropdown. */
public readonly metric = input<OrgLensLeaderboardMetric | null>(null);
public readonly metricOptions = input<{ id: OrgLensLeaderboardMetric; label: string; icon: string }[] | null>(null);
public readonly tabChange = output<OrgLensProjectDetailTab>();
public readonly timeRangeChange = output<OrgLensLeaderboardTimeRange>();
public readonly metricChange = output<OrgLensLeaderboardMetric>();

protected onTabKeydown(event: KeyboardEvent): void {
const ids = this.tabs().map((t) => t.id);
const idx = ids.indexOf(this.activeTab());
let next: number | null = null;
if (event.key === 'ArrowRight') next = (idx + 1) % ids.length;
else if (event.key === 'ArrowLeft') next = (idx - 1 + ids.length) % ids.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = ids.length - 1;
if (next !== null) {
event.preventDefault();
this.tabChange.emit(ids[next]);
if (isPlatformBrowser(this.platformId)) {
const n = next;
setTimeout(() => this.tabBtns()[n]?.nativeElement.focus());
}
}
}
}
Loading
Loading