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
Expand Up @@ -521,6 +521,22 @@ describe('TableEditForm focus return', () => {
expect(within(row).getByRole('rowheader')).toHaveFocus();
});

it('names the row header from the reset cell before focus returns', async () => {
await mount();
const handler = await openEditor();
const row = harness.row('1');
harness.table.editing.forms['1'].resource = buildWorkPackage({ id: '1', subject: 'Renamed' });
let nameAtFocus:string|null = null;
row.addEventListener('focusin', () => {
nameAtFocus = within(row).getByRole('rowheader').getAttribute('aria-label');
});

handler.handleUserCancel();

expect(nameAtFocus).toBe('Renamed');
expect(within(row).getByRole('rowheader')).toHaveAccessibleName('Renamed');
});
Comment thread
myabc marked this conversation as resolved.
Resolved

it('returns to the origin after a user-ended owned overlay closes', async () => {
await mount();
const handler = await openEditor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export class TableEditForm extends EditForm<WorkPackageResource> {
td.style.maxWidth = '';
this.cellBuilder.refresh(cell, this.resource, fieldName);
td.classList.remove(editModeClassName);
this.table.painter.paint(this.table.painter.keysOf(this.resource.id!));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API seems a bit odd. We ask the painter what rows it would have to repaint, then immediately tell it to paint those rows. That sounds like a missing method of the painter this.table.painter.paintWp(this.resource.id!) 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. This breaks the "Tell, Don't Ask" principle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix belongs in #26507's mutate API,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now implemented in #26507: the table provides a mutateWorkPackages(ids, change) method and looks up rows itself.


if (focus || userEnded) {
this.returnFocusFrom(cell, fieldName, focus, closing);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,6 @@ export class CellBuilder {
if (attribute === 'subject' || attribute === 'id') {
const name = displayElement.textContent ?? '';
cell.dataset.rowHeaderName = name;
if (cell.matches('th[scope="row"]')) {
cell.removeAttribute('aria-labelledby');
cell.setAttribute('aria-label', name);
}
}

if (hasBaseline) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public readonly injector:Injector,
const { occurrences } = this.tablePass.draft;
void this.wpTableOrder.positionsFor(query).then((positions:QueryOrder) => {
if (this.table.destroyed || this.table.querySpace.query.value !== query) return;
occurrences.forEach(({ workPackage, renderType, element }) => {
const replaced = new Set<string>();
occurrences.forEach(({ key, workPackage, renderType, element }) => {
// We only care for rows that are natural work packages and are not relation sub-rows
if (!workPackage || renderType !== 'primary') {
return;
Expand All @@ -75,10 +76,11 @@ public readonly injector:Injector,
const handle = this.dragDropHandleBuilder.build(latestWorkPackage, positions[workPackage.id!]);

if (handle && element) {
handle.setAttribute('role', 'gridcell');
element.replaceChild(handle, element.firstElementChild!);
replaced.add(key);
}
});
if (replaced.size > 0) this.table.painter.paint(replaced);
}).catch((error:unknown) => notification.handleRawError(error));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class GroupedRenderPass extends PlainRenderPass {
if (nextGroup && groupsChanged) {
const groupClass = groupClassNameFor(nextGroup);
const rowElement = this.headerBuilder.buildGroupRow(nextGroup, this.colspan);
this.appendNonWorkPackageRow(groupOccurrenceKey(nextGroup.index, 'header'), rowElement, groupClass, [groupRowClass], false, nextGroup.identifier);
this.appendNonWorkPackageRow(groupOccurrenceKey(nextGroup.index, 'header'), 'groupHeader', rowElement, groupClass, [groupRowClass], false, nextGroup.identifier);
currentGroup = nextGroup;
}

Expand Down Expand Up @@ -184,7 +184,7 @@ export class GroupedRenderPass extends PlainRenderPass {
const { additionalClasses, hidden } = this.groupRowState(group);

rowElement.classList.add(...additionalClasses);
this.appendNonWorkPackageRow(groupOccurrenceKey(group.index, 'sums'), rowElement, groupClass, additionalClasses, hidden, group.identifier, 2);
this.appendNonWorkPackageRow(groupOccurrenceKey(group.index, 'sums'), 'sums', rowElement, groupClass, additionalClasses, hidden, group.identifier, 2);
}

private groupRowState(group:GroupObject):{ additionalClasses:string[], hidden:boolean } {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {

return {
key: this.occurrenceKey(workPackage, isAncestor),
kind: 'workPackage',
classIdentifier: isAncestor ? ancestorClassIdentifier(workPackage.id!) : this.rowBuilder.classIdentifier(workPackage),
additionalClasses: isAncestor ? [additionalHierarchyRowClassName].concat(ancestorClasses) : ancestorClasses,
workPackage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {

subjectCell.classList.add('-with-hierarchy');
subjectCell.prepend(hierarchyElement);
if (this.parentsWithVisibleChildren[workPackage.id!]) row.dataset.expandable = '';

// Assure that the content is still visible when the hierarchy indentation is very large
subjectCell.style.minWidth = `${125 + (hierarchyIndentation * hierarchyLevel)}px`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public readonly injector:Injector,
if (this.draft.occurrences.length === 0 && this.workPackageTable.renderPlaceholderRow) {
this.registerAppended(this.rowBuilder.placeholderRow, {
key: placeholderOccurrenceKey(),
kind: 'placeholder',
classIdentifier: 'wp--placeholder-row',
additionalClasses: [],
workPackage: null,
Expand Down Expand Up @@ -226,6 +227,7 @@ workPackage:WorkPackageResource,
) {
this.registerAppended(row, {
key: wpOccurrenceKey(workPackage.id!),
kind: 'workPackage',
classIdentifier: this.rowBuilder.classIdentifier(workPackage),
additionalClasses,
workPackage,
Expand All @@ -244,6 +246,7 @@ workPackage:WorkPackageResource,
*/
protected appendNonWorkPackageRow(
key:OccurrenceKey,
kind:'groupHeader'|'sums',
row:HTMLTableRowElement,
classIdentifier:string,
additionalClasses:string[] = [],
Expand All @@ -254,6 +257,7 @@ workPackage:WorkPackageResource,
row.classList.add(classIdentifier);
this.registerAppended(row, {
key,
kind,
classIdentifier,
additionalClasses,
workPackage: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class RelationsRenderPass {
`.${this.relationRowBuilder.classIdentifier(from)},.${relationGroupClass(from.id!)}`,
{
key: relationOccurrenceKey(type, from.id!, to.id!),
kind: 'workPackage',
classIdentifier: this.relationRowBuilder.relationClassIdentifier(from, to),
additionalClasses: row.additionalClasses.concat(['wp-table--relations-additional-row']),
workPackage: to,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { locateTableRowByIdentifier } from 'core-app/features/work-packages/comp
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { WorkPackageViewColumnsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-columns.service';
import { TableActionRenderer } from 'core-app/features/work-packages/components/wp-fast-table/builders/table-action-renderer';
import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service';
import {
internalBaselineColumn,
internalContextMenuColumn,
Expand All @@ -41,8 +40,6 @@ import {
} from 'core-app/features/work-packages/components/wp-fast-table/builders/internal-sort-columns';
import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator';
import { debugLog } from 'core-app/shared/helpers/debug_output';
import { paintRowSelection } from './row-selection-painter';
import { WorkPackageViewFocusService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service';
import { RelationCellbuilder } from '../relation-cell-builder';
import {
CellBuilder,
Expand All @@ -65,10 +62,6 @@ export const commonRowClassName = 'wp--row';

export class SingleRowBuilder {
// Injections
@LazyInject() wpTableSelection:WorkPackageViewSelectionService;

@LazyInject() wpTableFocus:WorkPackageViewFocusService;

@LazyInject() wpTableColumns:WorkPackageViewColumnsService;

@LazyInject() wpTableBaseline:WorkPackageViewBaselineService;
Expand Down Expand Up @@ -296,11 +289,6 @@ export class SingleRowBuilder {
}
});

paintRowSelection(row, {
selected: this.wpTableSelection.isSelected(workPackage.id!),
pressed: this.wpTableFocus.isFocused(workPackage.id!),
});

return [row, false];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class DragAndDropTransformer {
recoverIfCurrent = () => {
if (!isCurrent() || table.lastRenderPass !== renderPass || source.parentNode !== parentNode) return;
parentNode?.insertBefore(source, nextSibling?.parentNode === parentNode ? nextSibling : null);
table.rowPositions.paint();
table.painter.paint(new Set());
};
if (!stillEligible()) {
complete(false);
Expand Down Expand Up @@ -238,7 +238,7 @@ export class DragAndDropTransformer {
// Only preparation observes the staged neighbors. Persistence captures no row.
if (target) table.tbody.insertBefore(source, edge === 'top' ? target : target.nextSibling);
else table.tbody.appendChild(source);
table.rowPositions.paint();
table.painter.paint(new Set());
const preparedAction = await actionService.prepareDrop(workPackage, source);
if (!stillEligible()) {
recoverIfCurrent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
// See COPYRIGHT and LICENSE files for more details.
//++

export type RowKind = 'header'|'workPackage'|'groupHeader'|'sums'|'totals';
import type { RowRecordKind } from '../../../semantics/row-classifier';

export const ROW_KINDS = ['header', 'workPackage', 'groupHeader', 'sums', 'totals'] as const satisfies readonly RowRecordKind[];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lwassermann this should help prevent drift: renaming a RowRecordKind in
the classifier and not updating here will fail the typecheck.


export type RowKind = (typeof ROW_KINDS)[number];

export interface NavigableRow {
key:string;
Expand Down
Comment thread
myabc marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -25,62 +25,38 @@
// See COPYRIGHT and LICENSE files for more details.
//++

import { WorkPackageViewHierarchiesService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-hierarchy.service';
import type { RenderedOccurrence } from '../../../rendered-occurrence-ledger';
import type { WorkPackageTable } from '../../../wp-fast-table';
import { NavigableRow, RowKind } from './focus-position';
import type { RowRecord, RowRecordKind } from '../../../semantics/row-classifier';
import { NavigableRow, ROW_KINDS, RowKind } from './focus-position';
import { navigableCells } from './roving-tabindex';
import { expansionStateOf, rowKindOf } from '../../../helpers/row-expansion';
import { headerRowOf, totalsRowOf } from '../../../helpers/structural-rows';

export const headerRowKey = 'header';
export const totalsRowKey = 'totals';

export { headerRowOf, totalsRowOf };

function occurrenceRow(
table:WorkPackageTable,
hierarchies:WorkPackageViewHierarchiesService,
occurrence:RenderedOccurrence,
):NavigableRow|null {
const kind = rowKindOf(occurrence);
if (kind === null || !occurrence.element) {
return null;
}
const navigableKinds:ReadonlySet<RowRecordKind> = new Set(ROW_KINDS);

function isNavigableKind(kind:RowRecordKind):kind is RowKind {
return navigableKinds.has(kind);
}

function navigableRow({ element, occurrence, state }:RowRecord, key:string, kind:RowKind):NavigableRow {
const row:NavigableRow = {
key: occurrence.key,
key,
kind,
hidden: occurrence.hidden,
cellCount: navigableCells(occurrence.element).length,
expanded: expansionStateOf(table, hierarchies, occurrence, kind),
hidden: state.hidden,
cellCount: navigableCells(element).length,
expanded: state.expanded,
};

return occurrence.groupIdentifier === undefined ? row : { ...row, groupIdentifier: occurrence.groupIdentifier };
}

function edgeRow(key:string, kind:RowKind, element:HTMLTableRowElement|null):NavigableRow[] {
if (!element) {
return [];
}

const cellCount = navigableCells(element).length;
return [{
key, kind, hidden: kind === 'totals' && cellCount === 0, cellCount, expanded: null,
}];
return occurrence?.groupIdentifier === undefined ? row : { ...row, groupIdentifier: occurrence.groupIdentifier };
}

/** The rows keyboard navigation walks, in visual order: header, rendered occurrences, totals. */
export function buildNavigableRows(table:WorkPackageTable):NavigableRow[] {
const { ledger } = table;
const hierarchies = table.injector.get(WorkPackageViewHierarchiesService);
const body = Array.from({ length: ledger.size }, (_, index) => ledger.byKey(ledger.keyAt(index)!)!)
.map((occurrence) => occurrenceRow(table, hierarchies, occurrence))
.filter((row):row is NavigableRow => row !== null);

return [
...edgeRow(headerRowKey, 'header', headerRowOf(table)),
...body,
...edgeRow(totalsRowKey, 'totals', totalsRowOf(table)),
];
return table.rows.rows().flatMap((record) => {
const { key, state: { kind } } = record;
return key !== null && isNavigableKind(kind) ? [navigableRow(record, key, kind)] : [];
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('SelectionTransformer', () => {
destroyRef: lifetime.get(DestroyRef),
tableAndTimelineContainer: root,
renderedRows,
semantics: { paintSelection: vi.fn() },
painter: { paint: vi.fn() },
} as unknown as WorkPackageTable;
new SelectionTransformer(injector, table);

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('SelectionTransformer', () => {
const lifetime = Injector.create({ providers: [], parent: injector });
const root = document.createElement('div');
document.body.append(root);
new SelectionTransformer(injector, { injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], semantics: { paintSelection: vi.fn() } } as unknown as WorkPackageTable);
new SelectionTransformer(injector, { injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], painter: { paint: vi.fn() } } as unknown as WorkPackageTable);
return { injector, lifetime, root, reset };
};
const first = build();
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('SelectionTransformer', () => {
const root = document.createElement('div');
root.innerHTML = '<div class="wp-table--row" tabindex="0" data-work-package-id="2" data-class-identifier="wp-row-2">Row</div>';
document.body.append(root);
new SelectionTransformer(injector, { injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], semantics: { paintSelection: vi.fn() } } as unknown as WorkPackageTable);
new SelectionTransformer(injector, { injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], painter: { paint: vi.fn() } } as unknown as WorkPackageTable);
selection.initializeSelection(['2']);

const allowed = fireEvent.keyDown(root.firstElementChild!, {
Expand Down Expand Up @@ -191,7 +191,7 @@ describe('SelectionTransformer', () => {
const root = document.createElement('div');
document.body.append(root);
new SelectionTransformer(injector, {
injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], ledger, semantics: { paintSelection: vi.fn() },
injector, destroyRef: lifetime.get(DestroyRef), tableAndTimelineContainer: root, renderedRows: [], ledger, painter: { paint: vi.fn() },
} as unknown as WorkPackageTable);

const stale = [{ workPackageId: '2', classIdentifier: 'wp-card-2', hidden: false }];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import { onDestroySafely } from 'core-app/shared/helpers/angular/owned-ui-cleanu
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service';
import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator';
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { paintRowSelection } from '../../builders/rows/row-selection-painter';
import { WorkPackageTable } from '../../wp-fast-table';
import { selectionGesture, TableSelectionFeedback } from './selection-feedback';
import { registerWorkPackageDeselectAll } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/wp-selection-keyboard';
Expand Down Expand Up @@ -83,15 +81,6 @@ export class SelectionTransformer {
}

private paintRows():void {
this.table.tableAndTimelineContainer
.querySelectorAll<HTMLElement>(`.${tableRowClassName}[data-work-package-id]`)
.forEach((row) => {
const workPackageId = row.dataset.workPackageId!;
paintRowSelection(row, {
selected: this.wpTableSelection.isSelected(workPackageId),
pressed: this.wpTableFocus.isFocused(workPackageId),
});
});
this.table.semantics.paintSelection();
this.table.painter.paint();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { occurrenceRowKind } from './structural-rows';
/** The row's navigation category, or null when the occurrence is excluded from keyboard navigation. */
export function rowKindOf(occurrence:RenderedOccurrence):RowKind|null {
const kind = occurrenceRowKind(occurrence);
return kind === 'placeholder' || kind === 'foreign' ? null : kind;
return kind === 'placeholder' ? null : kind;
}

export function expansionStateOf(
Expand All @@ -47,7 +47,7 @@ export function expansionStateOf(
return !table.workPackageViewCollapsedGroupsService.config.state[occurrence.groupIdentifier ?? ''];
}

if (kind === 'workPackage' && occurrence.element?.querySelector('.wp-table--hierarchy-indicator')) {
if (kind === 'workPackage' && occurrence.element?.dataset.expandable !== undefined) {
return !hierarchies.collapsed(occurrence.workPackageId!);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('structural rows', () => {
expect(kind(harness.row('1'))).toBe('workPackage');
expect(kind(harness.table.ledger.byKey(groupOccurrenceKey(0, 'sums'))!.element!)).toBe('sums');
expect(kind(harness.totalsRow())).toBe('totals');
expect(kind(createRow())).toBe('create');
expect(kind(createRow())).toBe('inlineCreate');
});

it('names placeholder, empty, unexposed and unknown rows', async () => {
Expand All @@ -110,15 +110,15 @@ describe('structural rows', () => {
harness.tbody.before(empty);
const unknown = harness.tbody.closest('table')!.createTBody().insertRow();
expect(kind(harness.table.ledger.byKey(placeholderOccurrenceKey())!.element!)).toBe('placeholder');
expect(kind(emptyRow)).toBe('empty');
expect(kind(emptyRow)).toBe('emptyResults');
expect(kind(unknown)).toBe('foreign');
unknown.hidden = true;
expect(kind(unknown)).toBe('unexposed');
});

it('treats an occurrence it cannot name as foreign', () => {
const occurrence = { key: 'banner', workPackageId: null } as RenderedOccurrence;
expect(occurrenceRowKind(occurrence)).toBe('foreign');
it('reads the kind the render pass registered rather than parsing the key', () => {
const occurrence = { key: 'group:0:header', kind: 'workPackage', workPackageId: null } as RenderedOccurrence;
expect(occurrenceRowKind(occurrence)).toBe('workPackage');
});
});
});
Loading
Loading