From 03735acac068fd5a74a9236a40d4ff973b5559ad Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Thu, 14 May 2026 15:44:01 -0500
Subject: [PATCH 01/23] add breakpoint column
---
packages/ok-components/src/ts/all-ts.ts | 1 +
.../breakpoint/cell-view/index.ts | 227 +++++++
.../breakpoint/cell-view/styles.ts | 65 ++
.../breakpoint/cell-view/template.ts | 61 ++
.../src/ts/table-column/breakpoint/index.ts | 80 +++
.../src/ts/table-column/breakpoint/styles.ts | 12 +
.../ts/table-column/breakpoint/template.ts | 9 +
.../tests/ts-table-column-breakpoint.spec.ts | 561 ++++++++++++++++++
.../src/ts/table-column/breakpoint/types.ts | 20 +
...-table-column-breakpoint-matrix.stories.ts | 68 +++
.../ts-table-column-breakpoint.mdx | 54 ++
.../ts-table-column-breakpoint.stories.ts | 142 +++++
12 files changed, 1300 insertions(+)
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/index.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/styles.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/template.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
create mode 100644 packages/ok-components/src/ts/table-column/breakpoint/types.ts
create mode 100644 packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
create mode 100644 packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
create mode 100644 packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
diff --git a/packages/ok-components/src/ts/all-ts.ts b/packages/ok-components/src/ts/all-ts.ts
index 815f43d4ba..0f446e829b 100644
--- a/packages/ok-components/src/ts/all-ts.ts
+++ b/packages/ok-components/src/ts/all-ts.ts
@@ -1 +1,2 @@
import './icon-dynamic';
+import './table-column/breakpoint';
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
new file mode 100644
index 0000000000..65754a0fb7
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -0,0 +1,227 @@
+import { DesignSystem } from '@ni/fast-foundation';
+import { TableCellView } from '@ni/nimble-components/dist/esm/table-column/base/cell-view';
+import type { MenuButton } from '@ni/nimble-components/dist/esm/menu-button';
+import type { Table } from '@ni/nimble-components/dist/esm/table';
+import { template } from './template';
+import { styles } from './styles';
+import { BreakpointState, type BreakpointToggleEventDetail } from '../types';
+import type { TsTableColumnBreakpointCellRecord, TsTableColumnBreakpointColumnConfig } from '../index';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ok-ts-table-column-breakpoint-cell-view': TsTableColumnBreakpointCellView;
+ }
+}
+
+/**
+ * Cell view for the breakpoint column that renders a clickable breakpoint indicator.
+ */
+export class TsTableColumnBreakpointCellView extends TableCellView<
+ TsTableColumnBreakpointCellRecord,
+ TsTableColumnBreakpointColumnConfig
+> {
+ private static readonly contextMenuKey = 'ContextMenu';
+
+ private static readonly legacyContextMenuKey = 'Apps';
+
+ private static readonly menuKeyAlias = 'Menu';
+
+ /** @internal */
+ public contextMenuButton?: MenuButton;
+
+ /** @internal */
+ public get currentState(): BreakpointState {
+ const value = this.cellRecord?.value;
+ if (value && Object.values(BreakpointState).includes(value as BreakpointState)) {
+ return value as BreakpointState;
+ }
+ return BreakpointState.off;
+ }
+
+ /** @internal */
+ public get tooltipText(): string {
+ if (this.currentState === BreakpointState.off) {
+ return 'Add breakpoint';
+ }
+ return 'Remove breakpoint';
+ }
+
+ /** @internal */
+ public get ariaLabelText(): string {
+ switch (this.currentState) {
+ case BreakpointState.enabled:
+ return 'Breakpoint enabled';
+ case BreakpointState.disabled:
+ return 'Breakpoint disabled';
+ case BreakpointState.hit:
+ return 'Breakpoint hit';
+ default:
+ return 'Add breakpoint';
+ }
+ }
+
+ public override get tabbableChildren(): HTMLElement[] {
+ const button = this.shadowRoot?.querySelector('.breakpoint-button') as HTMLElement | null;
+ if (button) {
+ return [button];
+ }
+ return [];
+ }
+
+ /** @internal */
+ public onButtonClick(event: Event): void {
+ event.stopPropagation();
+ const oldState = this.currentState;
+ const newState = oldState === BreakpointState.off
+ ? BreakpointState.enabled
+ : BreakpointState.off;
+ this.emitToggle(oldState, newState);
+ }
+
+ /** @internal */
+ public onContextMenu(event: Event): void {
+ event.preventDefault();
+ event.stopPropagation();
+ this.openContextMenu();
+ }
+
+ /** @internal */
+ public onKeyDown(event: KeyboardEvent): void {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.tryFocusSiblingBreakpoint(event.key === 'ArrowUp');
+ return;
+ }
+
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.onButtonClick(event);
+ return;
+ }
+
+ if ((event.key === 'F10' && event.shiftKey)
+ || event.key === TsTableColumnBreakpointCellView.contextMenuKey
+ || event.key === TsTableColumnBreakpointCellView.legacyContextMenuKey
+ || event.key === TsTableColumnBreakpointCellView.menuKeyAlias) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.openContextMenu();
+ return;
+ }
+
+ if (event.key === 'F9' || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'b')) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.onButtonClick(event);
+ }
+ }
+
+ /** @internal */
+ public onDisableMenuItemSelected(): void {
+ this.emitToggle(this.currentState, BreakpointState.disabled);
+ }
+
+ /** @internal */
+ public onAddMenuItemSelected(): void {
+ this.emitToggle(this.currentState, BreakpointState.enabled);
+ }
+
+ /** @internal */
+ public onEnableMenuItemSelected(): void {
+ this.emitToggle(this.currentState, BreakpointState.enabled);
+ }
+
+ /** @internal */
+ public onRemoveMenuItemSelected(): void {
+ this.emitToggle(this.currentState, BreakpointState.off);
+ }
+
+ private emitToggle(
+ oldState: BreakpointState,
+ newState: BreakpointState
+ ): void {
+ const detail: BreakpointToggleEventDetail = {
+ recordId: this.recordId ?? '',
+ newState,
+ oldState
+ };
+ this.$emit('breakpoint-column-toggle', detail);
+ }
+
+ private openContextMenu(): void {
+ if (this.contextMenuButton && !this.contextMenuButton.open) {
+ this.contextMenuButton.open = true;
+ }
+ }
+
+ private tryFocusSiblingBreakpoint(backward: boolean): boolean {
+ const currentCell = this.getContainingHost(this) as {
+ getRootNode: () => Node;
+ } | undefined;
+ if (!currentCell) {
+ return false;
+ }
+
+ const currentRow = this.getContainingHost(currentCell) as {
+ getFocusableElements: () => { cells: Array<{ cell: { cellView: TableCellView } }> };
+ getRootNode: () => Node;
+ } | undefined;
+ if (!currentRow) {
+ return false;
+ }
+
+ const table = this.getContainingHost(currentRow) as Table | undefined;
+ if (!table) {
+ return false;
+ }
+
+ const rowElements = table.rowElements;
+ const rowIndex = rowElements.findIndex(row => row === currentRow);
+ if (rowIndex < 0) {
+ return false;
+ }
+
+ const currentRowCells = currentRow.getFocusableElements().cells;
+ const columnIndex = currentRowCells.findIndex(cellInfo => cellInfo.cell === currentCell as unknown as typeof cellInfo.cell);
+ if (columnIndex < 0) {
+ return false;
+ }
+
+ const delta = backward ? -1 : 1;
+ for (let i = rowIndex + delta; i >= 0 && i < rowElements.length; i += delta) {
+ const row = rowElements[i] as {
+ getFocusableElements?: () => { cells: Array<{ cell: { cellView: TableCellView } }> };
+ };
+ if (!row.getFocusableElements) {
+ continue;
+ }
+
+ const cellInfo = row.getFocusableElements().cells[columnIndex];
+ const target = cellInfo?.cell.cellView.tabbableChildren[0];
+ if (target) {
+ target.focus();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private getContainingHost(element: { getRootNode: () => Node }): HTMLElement | undefined {
+ const root = element.getRootNode();
+ if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
+ return root.host;
+ }
+ return undefined;
+ }
+}
+
+const tsTableColumnBreakpointCellView = TsTableColumnBreakpointCellView.compose({
+ baseName: 'ts-table-column-breakpoint-cell-view',
+ template,
+ styles
+});
+DesignSystem.getOrCreate().withPrefix('ok').register(tsTableColumnBreakpointCellView());
+export const tsTableColumnBreakpointCellViewTag = 'ok-ts-table-column-breakpoint-cell-view';
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
new file mode 100644
index 0000000000..d610eec58e
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
@@ -0,0 +1,65 @@
+import { css } from '@ni/fast-element';
+
+export const styles = css`
+ :host {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
+
+ .breakpoint-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ margin: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: 50%;
+ outline-offset: -2px;
+ }
+
+ .breakpoint-button:focus-visible {
+ outline: 2px solid Highlight;
+ }
+
+ .breakpoint-button svg {
+ width: 12px;
+ height: 12px;
+ }
+
+ .breakpoint-button.state-off svg {
+ opacity: 0;
+ }
+
+ .breakpoint-button.state-off:hover svg,
+ .breakpoint-button.state-off:focus-visible svg {
+ opacity: 1;
+ }
+
+ .context-menu-button {
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ pointer-events: none;
+ }
+
+ .context-menu-button::part(button) {
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ .context-menu-button[open] {
+ pointer-events: auto;
+ }
+
+ .context-menu-button::part(menu) {
+ z-index: 1;
+ }
+`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
new file mode 100644
index 0000000000..c32ce4cf72
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -0,0 +1,61 @@
+import { html, ref, when } from '@ni/fast-element';
+import type { TsTableColumnBreakpointCellView } from './index';
+import { BreakpointState } from '../types';
+import { menuButtonTag } from '@ni/nimble-components/dist/esm/menu-button';
+import { menuTag } from '@ni/nimble-components/dist/esm/menu';
+import { menuItemTag } from '@ni/nimble-components/dist/esm/menu-item';
+
+// Placeholder SVGs for breakpoint states - to be replaced with proper icons later
+const offSvg = html``;
+const enabledSvg = html``;
+const disabledSvg = html``;
+const hitSvg = html``;
+
+export const template = html`
+
+ <${menuButtonTag}
+ ${ref('contextMenuButton')}
+ class="context-menu-button"
+ content-hidden
+ position="above"
+ tabindex="-1"
+ @click="${(_, c) => c.event.stopPropagation()}"
+ >
+ <${menuTag} slot="menu">
+ ${when(x => x.currentState === BreakpointState.off, html`
+ <${menuItemTag} @change="${x => x.onAddMenuItemSelected()}">
+ Add breakpoint
+ ${menuItemTag}>
+ `)}
+ ${when(x => x.currentState === BreakpointState.enabled || x.currentState === BreakpointState.hit, html`
+ <${menuItemTag} @change="${x => x.onDisableMenuItemSelected()}">
+ Disable breakpoint
+ ${menuItemTag}>
+ <${menuItemTag} @change="${x => x.onRemoveMenuItemSelected()}">
+ Remove breakpoint
+ ${menuItemTag}>
+ `)}
+ ${when(x => x.currentState === BreakpointState.disabled, html`
+ <${menuItemTag} @change="${x => x.onEnableMenuItemSelected()}">
+ Enable breakpoint
+ ${menuItemTag}>
+ <${menuItemTag} @change="${x => x.onRemoveMenuItemSelected()}">
+ Remove breakpoint
+ ${menuItemTag}>
+ `)}
+ ${menuTag}>
+ ${menuButtonTag}>
+`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
new file mode 100644
index 0000000000..e30be40c45
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -0,0 +1,80 @@
+import { DesignSystem } from '@ni/fast-foundation';
+import { attr } from '@ni/fast-element';
+import { template } from './template';
+import { styles } from './styles';
+import type { TableStringField } from '@ni/nimble-components/dist/esm/table/types';
+import { tsTableColumnBreakpointCellViewTag } from './cell-view';
+import type { ColumnInternalsOptions } from '@ni/nimble-components/dist/esm/table-column/base/models/column-internals';
+import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
+import { ColumnValidator } from '@ni/nimble-components/dist/esm/table-column/base/models/column-validator';
+import { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
+import { type BreakpointToggleEventDetail } from './types';
+import type { DelegatedEventEventDetails } from '@ni/nimble-components/dist/esm/table-column/base/types';
+
+export type TsTableColumnBreakpointCellRecord = TableStringField<'value'>;
+
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
+export interface TsTableColumnBreakpointColumnConfig {}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ok-ts-table-column-breakpoint': TsTableColumnBreakpoint;
+ }
+}
+
+/**
+ * A table column that displays a breakpoint indicator with toggle functionality.
+ */
+export class TsTableColumnBreakpoint extends TableColumn {
+ @attr({ attribute: 'field-name' })
+ public fieldName?: string;
+
+ public constructor() {
+ super();
+ // Breakpoint columns are icon-only and should remain fixed-size and non-resizable.
+ this.columnInternals.resizingDisabled = true;
+ this.columnInternals.pixelWidth = singleIconColumnWidth;
+ this.columnInternals.minPixelWidth = singleIconColumnWidth;
+ }
+
+ /** @internal */
+ public onDelegatedEvent(e: Event): void {
+ e.stopImmediatePropagation();
+
+ const event = e as CustomEvent;
+ const originalEvent = event.detail.originalEvent as CustomEvent;
+
+ if (originalEvent.type === 'breakpoint-column-toggle') {
+ const detail: BreakpointToggleEventDetail = {
+ ...originalEvent.detail,
+ recordId: event.detail.recordId
+ };
+ this.$emit('breakpoint-column-toggle', detail);
+ }
+ }
+
+ protected override getColumnInternalsOptions(): ColumnInternalsOptions {
+ return {
+ cellRecordFieldNames: ['value'],
+ cellViewTag: tsTableColumnBreakpointCellViewTag,
+ delegatedEvents: ['breakpoint-column-toggle'],
+ validator: new ColumnValidator<[]>([])
+ };
+ }
+
+ protected fieldNameChanged(): void {
+ this.columnInternals.dataRecordFieldNames = [this.fieldName];
+ this.columnInternals.operandDataRecordFieldName = this.fieldName;
+ }
+}
+
+const tsTableColumnBreakpoint = TsTableColumnBreakpoint.compose({
+ baseName: 'ts-table-column-breakpoint',
+ template,
+ styles
+});
+
+DesignSystem.getOrCreate()
+ .withPrefix('ok')
+ .register(tsTableColumnBreakpoint());
+export const tsTableColumnBreakpointTag = 'ok-ts-table-column-breakpoint';
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/styles.ts
new file mode 100644
index 0000000000..a567158eb9
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/styles.ts
@@ -0,0 +1,12 @@
+import { css } from '@ni/fast-element';
+import { display } from '../../../utilities/style/display';
+
+export const styles = css`
+ ${display('contents')}
+
+ .header-content {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/template.ts
new file mode 100644
index 0000000000..b6b37cbd6f
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/template.ts
@@ -0,0 +1,9 @@
+import { html } from '@ni/fast-element';
+import type { TsTableColumnBreakpoint } from '.';
+import { template as baseTemplate } from '@ni/nimble-components/dist/esm/table-column/base/template';
+
+export const template = html`
+ x.onDelegatedEvent(c.event)}"
+ >${baseTemplate}
+`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
new file mode 100644
index 0000000000..f8fca68b0c
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -0,0 +1,561 @@
+import { html, ref } from '@ni/fast-element';
+import { tableTag, type Table } from '@ni/nimble-components/dist/esm/table';
+import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';
+import { TablePageObject } from '@ni/nimble-components/dist/esm/table/testing/table.pageobject';
+import type { TableRecord } from '@ni/nimble-components/dist/esm/table/types';
+import { fixture, type Fixture } from '../../../../utilities/tests/fixture';
+import { TsTableColumnBreakpoint, tsTableColumnBreakpointTag } from '..';
+import { TsTableColumnBreakpointCellView } from '../cell-view';
+import { BreakpointState, type BreakpointToggleEventDetail } from '../types';
+import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
+
+interface SimpleTableRecord extends TableRecord {
+ id?: string;
+ breakpointState?: string | null;
+}
+
+class ElementReferences {
+ public table!: Table;
+ public column!: TsTableColumnBreakpoint;
+}
+
+describe('TsTableColumnBreakpoint', () => {
+ let table: Table;
+ let connect: () => Promise;
+ let disconnect: () => Promise;
+ let elementReferences: ElementReferences;
+ let tablePageObject: TablePageObject;
+
+ async function setup(
+ source: ElementReferences
+ ): Promise>> {
+ return await fixture>(
+ html`<${tableTag} ${ref('table')} id-field-name="id" style="width: 700px">
+ <${tsTableColumnBreakpointTag} ${ref('column')} field-name="breakpointState">
+ ${tsTableColumnBreakpointTag}>
+ ${tableTag}>`,
+ { source }
+ );
+ }
+
+ beforeEach(async () => {
+ elementReferences = new ElementReferences();
+ ({ connect, disconnect } = await setup(elementReferences));
+ table = elementReferences.table;
+ tablePageObject = new TablePageObject(table);
+ await connect();
+ await waitForUpdatesAsync();
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ it('can construct an element instance', () => {
+ expect(
+ document.createElement(tsTableColumnBreakpointTag)
+ ).toBeInstanceOf(TsTableColumnBreakpoint);
+ });
+
+ it('reports column configuration valid', () => {
+ expect(elementReferences.column.checkValidity()).toBeTrue();
+ });
+
+ it('uses fixed icon width and disables resizing', () => {
+ expect(elementReferences.column.columnInternals.resizingDisabled).toBeTrue();
+ expect(elementReferences.column.columnInternals.pixelWidth).toBe(singleIconColumnWidth);
+ expect(elementReferences.column.columnInternals.minPixelWidth).toBe(singleIconColumnWidth);
+ });
+
+ describe('rendering breakpoint states', () => {
+ it('renders off state when field value is "off"', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.off);
+ });
+
+ it('renders enabled state when field value is "enabled"', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.enabled);
+ });
+
+ it('renders disabled state when field value is "disabled"', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.disabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.disabled);
+ });
+
+ it('renders hit state when field value is "hit"', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.hit }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.hit);
+ });
+
+ it('renders off state when field value is null', async () => {
+ await table.setData([{ id: '1', breakpointState: null }]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.off);
+ });
+
+ it('renders off state when field value is undefined', async () => {
+ await table.setData([{ id: '1', breakpointState: undefined }]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.off);
+ });
+
+ it('renders off state when field value is invalid', async () => {
+ await table.setData([
+ { id: '1', breakpointState: 'invalid-state' }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.off);
+ });
+ });
+
+ describe('click-to-toggle', () => {
+ it('emits toggle event from off to enabled on click', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ button.click();
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.oldState).toBe(BreakpointState.off);
+ expect(eventDetail.newState).toBe(BreakpointState.enabled);
+ expect(eventDetail.recordId).toBe('1');
+ });
+
+ it('emits toggle event from enabled to off on click', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ button.click();
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.oldState).toBe(BreakpointState.enabled);
+ expect(eventDetail.newState).toBe(BreakpointState.off);
+ expect(eventDetail.recordId).toBe('1');
+ });
+
+ it('emits toggle event from hit to off on click', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.hit }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ button.click();
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.oldState).toBe(BreakpointState.hit);
+ expect(eventDetail.newState).toBe(BreakpointState.off);
+ });
+ });
+
+ describe('tooltip text', () => {
+ it('shows "Add breakpoint" when state is off', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.tooltipText).toBe('Add breakpoint');
+ });
+
+ it('shows "Remove breakpoint" when state is enabled', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.tooltipText).toBe('Remove breakpoint');
+ });
+ });
+
+ describe('tabbable children', () => {
+ it('cell view has one tabbable child (the button)', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.tabbableChildren.length).toBe(1);
+ });
+ });
+
+ describe('context menu', () => {
+ it('opens internal nimble context menu when state is off', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ button.dispatchEvent(
+ new MouseEvent('contextmenu', { bubbles: true })
+ );
+ await waitForUpdatesAsync();
+
+ expect(cellView.contextMenuButton?.open).toBeTrue();
+ });
+
+ it('opens internal nimble context menu when state is enabled', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ button.dispatchEvent(
+ new MouseEvent('contextmenu', { bubbles: true })
+ );
+ await waitForUpdatesAsync();
+
+ expect(cellView.contextMenuButton?.open).toBeTrue();
+ });
+
+ it('opens internal nimble context menu on Shift+F10', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ button.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'F10',
+ shiftKey: true,
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(cellView.contextMenuButton?.open).toBeTrue();
+ });
+
+ it('opens internal nimble context menu on Menu key', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ button.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'ContextMenu',
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(cellView.contextMenuButton?.open).toBeTrue();
+ });
+ });
+
+ describe('keyboard shortcuts', () => {
+ it('toggles breakpoint on F9 when focused in breakpoint cell', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ button.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'F9',
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.oldState).toBe(BreakpointState.off);
+ expect(eventDetail.newState).toBe(BreakpointState.enabled);
+ });
+
+ it('toggles breakpoint on Ctrl+B when focused in breakpoint cell', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ button.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'b',
+ ctrlKey: true,
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.oldState).toBe(BreakpointState.enabled);
+ expect(eventDetail.newState).toBe(BreakpointState.off);
+ });
+
+ it('moves focus to next row breakpoint on ArrowDown', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled },
+ { id: '2', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const firstCellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const secondCellView = tablePageObject.getRenderedCellView(
+ 1,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const firstButton = firstCellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ const secondButton = secondCellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ firstButton.focus();
+ firstButton.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'ArrowDown',
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(secondButton.matches(':focus')).toBeTrue();
+ });
+
+ it('moves focus to previous row breakpoint on ArrowUp', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.off },
+ { id: '2', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const firstCellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const secondCellView = tablePageObject.getRenderedCellView(
+ 1,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const firstButton = firstCellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+ const secondButton = secondCellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ ) as HTMLButtonElement;
+
+ secondButton.focus();
+ secondButton.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'ArrowUp',
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ expect(firstButton.matches(':focus')).toBeTrue();
+ });
+
+ });
+
+ describe('field-name attribute', () => {
+ it('updating fieldName updates cell rendering', async () => {
+ await table.setData([
+ {
+ id: '1',
+ breakpointState: BreakpointState.enabled
+ }
+ ]);
+ await waitForUpdatesAsync();
+
+ let cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.enabled);
+
+ elementReferences.column.fieldName = undefined;
+ await waitForUpdatesAsync();
+
+ cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ expect(cellView.currentState).toBe(BreakpointState.off);
+ });
+ });
+});
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/types.ts b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
new file mode 100644
index 0000000000..0304690018
--- /dev/null
+++ b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
@@ -0,0 +1,20 @@
+/**
+ * The possible states of a breakpoint indicator.
+ */
+export const BreakpointState = {
+ off: 'off',
+ enabled: 'enabled',
+ disabled: 'disabled',
+ hit: 'hit'
+} as const;
+export type BreakpointState =
+ (typeof BreakpointState)[keyof typeof BreakpointState];
+
+/**
+ * The event detail for the `breakpoint-column-toggle` event.
+ */
+export interface BreakpointToggleEventDetail {
+ recordId: string;
+ newState: BreakpointState;
+ oldState: BreakpointState;
+}
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
new file mode 100644
index 0000000000..620eadfe6a
--- /dev/null
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
@@ -0,0 +1,68 @@
+import type { StoryFn, Meta } from '@storybook/html-vite';
+import { html, ViewTemplate } from '@ni/fast-element';
+import { tableTag } from '@ni/nimble-components/dist/esm/table';
+import { tsTableColumnBreakpointTag } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint';
+import { BreakpointState } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint/types';
+import {
+ createMatrixThemeStory,
+ createMatrix,
+ sharedMatrixParameters
+} from '../../../utilities/matrix';
+
+const data = [
+ {
+ id: '0',
+ breakpointState: BreakpointState.off
+ },
+ {
+ id: '1',
+ breakpointState: BreakpointState.enabled
+ },
+ {
+ id: '2',
+ breakpointState: BreakpointState.disabled
+ },
+ {
+ id: '3',
+ breakpointState: BreakpointState.hit
+ },
+ {
+ id: '4',
+ breakpointState: null
+ },
+ {
+ id: '5',
+ breakpointState: undefined
+ }
+] as const;
+
+const metadata: Meta = {
+ title: 'Tests Ok/Ts Table Column: Breakpoint',
+ parameters: {
+ ...sharedMatrixParameters()
+ }
+};
+
+export default metadata;
+
+const component = (): ViewTemplate => html`
+ <${tableTag} id-field-name="id" style="height: 320px">
+ <${tsTableColumnBreakpointTag}
+ field-name="breakpointState"
+ >
+ BP
+ ${tsTableColumnBreakpointTag}>
+ ${tableTag}>
+`;
+
+export const themeMatrix: StoryFn = createMatrixThemeStory(
+ createMatrix(component)
+);
+
+themeMatrix.play = async (): Promise => {
+ await Promise.all(
+ Array.from(document.querySelectorAll(tableTag)).map(async table => {
+ await table.setData(data);
+ })
+ );
+};
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
new file mode 100644
index 0000000000..9d2b50d464
--- /dev/null
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
@@ -0,0 +1,54 @@
+import { Controls, Canvas, Meta, Title } from '@storybook/addon-docs/blocks';
+import * as breakpointColumnStories from './ts-table-column-breakpoint.stories';
+import ComponentApisLink from '../../../docs/component-apis-link.mdx';
+import { tsTableColumnBreakpointTag } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint';
+import { Tag } from '../../../utilities/story-layout';
+
+
+
+
+The renders a breakpoint indicator in each row that
+can be toggled between different states (Off, Enabled, Disabled, Hit) via click or context menu.
+
+The column is intended for code-centric table displays where debugging breakpoints need to be
+visually indicated and interactively managed.
+
+The column is neither sortable nor groupable. It has a fixed width of 32 pixels and is not resizable.
+
+
+
+## API
+
+
+
+
+## Usage
+
+### State Transitions
+
+- **Off → Enabled:** Click on the breakpoint indicator
+- **Enabled → Off:** Click on the active breakpoint indicator
+- **Enabled → Disabled:** Right-click and select "Disable breakpoint"
+- **Disabled → Enabled:** Right-click and select "Enable breakpoint"
+- **Disabled → Off:** Right-click and select "Remove breakpoint"
+- **Hit → Off:** Click on the hit breakpoint indicator
+
+### Breakpoint States
+
+| State | Description |
+|-------|-------------|
+| `off` | No breakpoint set (empty outline shown on hover) |
+| `enabled` | Active breakpoint (filled red circle) |
+| `disabled` | Inactive breakpoint (outlined red circle with slash) |
+| `hit` | Breakpoint currently being hit during debugging (red circle with gold border) |
+
+### Event Handling
+
+Listen to the `breakpoint-column-toggle` event on the column to handle state changes:
+
+```ts
+column.addEventListener('breakpoint-column-toggle', (event) => {
+ const { recordId, oldState, newState } = event.detail;
+ // Update your data source with the new breakpoint state
+});
+```
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
new file mode 100644
index 0000000000..72767b451f
--- /dev/null
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
@@ -0,0 +1,142 @@
+import { html, ref } from '@ni/fast-element';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite';
+import { withActions } from 'storybook/actions/decorator';
+import { tableTag, type Table } from '@ni/nimble-components/dist/esm/table';
+import type { TableRecord } from '@ni/nimble-components/dist/esm/table/types';
+import { tableColumnTextTag } from '@ni/nimble-components/dist/esm/table-column/text';
+import { tsTableColumnBreakpointTag } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint';
+import { BreakpointState, type BreakpointToggleEventDetail } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint/types';
+import {
+ type SharedTableArgs,
+ sharedTableActions,
+ sharedTableArgTypes,
+ sharedTableArgs
+} from '../../../nimble/table-column/base/table-column-stories-utils';
+import {
+ apiCategory,
+ createUserSelectedThemeStory,
+ disableStorybookZoomTransform
+} from '../../../utilities/storybook';
+
+interface CodeRecord extends TableRecord {
+ id: string;
+ lineNumber: number;
+ code: string;
+ breakpointState: string;
+}
+
+const simpleData: CodeRecord[] = [
+ {
+ id: '1',
+ lineNumber: 1,
+ code: 'function hello() {',
+ breakpointState: BreakpointState.off
+ },
+ {
+ id: '2',
+ lineNumber: 2,
+ code: ' console.log("Hello");',
+ breakpointState: BreakpointState.enabled
+ },
+ {
+ id: '3',
+ lineNumber: 3,
+ code: ' const x = 42;',
+ breakpointState: BreakpointState.disabled
+ },
+ {
+ id: '4',
+ lineNumber: 4,
+ code: ' return x;',
+ breakpointState: BreakpointState.hit
+ },
+ {
+ id: '5',
+ lineNumber: 5,
+ code: '}',
+ breakpointState: BreakpointState.off
+ }
+];
+
+const metadata: Meta = {
+ title: 'Ok/Ts Table Column: Breakpoint',
+ decorators: [withActions],
+ parameters: {
+ actions: {
+ handles: [...sharedTableActions, 'breakpoint-column-toggle']
+ }
+ },
+ argTypes: {
+ ...sharedTableArgTypes,
+ selectionMode: {
+ table: {
+ disable: true
+ }
+ }
+ },
+ args: {
+ ...sharedTableArgs(simpleData)
+ }
+};
+
+export default metadata;
+
+interface BreakpointColumnTableArgs extends SharedTableArgs {
+ fieldName: string;
+ toggleEvent: never;
+ currentData: CodeRecord[];
+}
+
+export const breakpointColumn: StoryObj = {
+ parameters: {},
+ render: createUserSelectedThemeStory(html`
+ ${disableStorybookZoomTransform}
+ <${tableTag}
+ ${ref('tableRef')}
+ data-unused="${x => x.updateData(x)}"
+ id-field-name="id"
+ style="height: 320px"
+ >
+ <${tsTableColumnBreakpointTag}
+ field-name="${x => x.fieldName}"
+ @breakpoint-column-toggle="${(x, c) => {
+ const event = c.event as CustomEvent;
+ const detail = event.detail;
+ x.currentData = x.currentData.map(record => (record.id === detail.recordId
+ ? { ...record, breakpointState: detail.newState }
+ : record));
+ void x.tableRef.setData(x.currentData);
+ }}"
+ >
+ ${tsTableColumnBreakpointTag}>
+ <${tableColumnTextTag} field-name="code">
+ Code
+ ${tableColumnTextTag}>
+ ${tableTag}>
+ `),
+ argTypes: {
+ fieldName: {
+ name: 'field-name',
+ description:
+ 'Set this attribute to identify which field in the data record contains the breakpoint state value for each row.',
+ control: false,
+ table: { category: apiCategory.attributes }
+ },
+ toggleEvent: {
+ name: 'breakpoint-column-toggle',
+ description:
+ 'Emitted when a breakpoint is toggled via click, keyboard, or context menu. The event detail includes `recordId`, `oldState`, and `newState`.',
+ control: false,
+ table: { category: apiCategory.events }
+ },
+ currentData: {
+ table: {
+ disable: true
+ }
+ }
+ },
+ args: {
+ fieldName: 'breakpointState',
+ currentData: [...simpleData]
+ }
+};
From c4482c8183c8fcb6cdde1d0992746c8fec0bd692 Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Wed, 20 May 2026 19:28:13 -0500
Subject: [PATCH 02/23] add events for context menu, cleanup accessibility
---
.../CodeAnalysisDictionary.xml | 1 +
.../OkBlazor/Source/Patterns/EventHandlers.cs | 2 +
.../BreakpointEventArgs.cs | 33 ++++
.../OkTsTableColumnBreakpoint.razor | 10 +
.../OkTsTableColumnBreakpoint.razor.cs | 54 +++++
.../OkBlazor/wwwroot/OkBlazor.lib.module.js | 17 +-
.../breakpoint/cell-view/index.ts | 184 ++++++++++++++----
.../breakpoint/cell-view/styles.ts | 23 +--
.../breakpoint/cell-view/template.ts | 61 +++---
.../src/ts/table-column/breakpoint/index.ts | 16 +-
.../tests/ts-table-column-breakpoint.spec.ts | 52 ++++-
.../src/ts/table-column/breakpoint/types.ts | 12 +-
12 files changed, 356 insertions(+), 109 deletions(-)
create mode 100644 packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
create mode 100644 packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor
create mode 100644 packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor.cs
diff --git a/packages/blazor-workspace/CodeAnalysisDictionary.xml b/packages/blazor-workspace/CodeAnalysisDictionary.xml
index 76b55dd8e0..b2c2aafaf6 100644
--- a/packages/blazor-workspace/CodeAnalysisDictionary.xml
+++ b/packages/blazor-workspace/CodeAnalysisDictionary.xml
@@ -5,6 +5,7 @@
args
blazor
bool
+ breakpoint
clearable
combobox
dropdown
diff --git a/packages/blazor-workspace/OkBlazor/Source/Patterns/EventHandlers.cs b/packages/blazor-workspace/OkBlazor/Source/Patterns/EventHandlers.cs
index 3e0ca5e06a..aaf17b7436 100644
--- a/packages/blazor-workspace/OkBlazor/Source/Patterns/EventHandlers.cs
+++ b/packages/blazor-workspace/OkBlazor/Source/Patterns/EventHandlers.cs
@@ -2,6 +2,8 @@
namespace OkBlazor;
+[EventHandler("onokbreakpointcolumntoggle", typeof(BreakpointColumnToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
+[EventHandler("onokbreakpointcolumncontextmenu", typeof(BreakpointColumnContextMenuEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
public static class EventHandlers
{
}
diff --git a/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
new file mode 100644
index 0000000000..1bcc12bdb3
--- /dev/null
+++ b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
@@ -0,0 +1,33 @@
+namespace OkBlazor;
+
+///
+/// The possible states of a breakpoint indicator.
+///
+public static class BreakpointState
+{
+ public const string Off = "off";
+ public const string Enabled = "enabled";
+ public const string Disabled = "disabled";
+ public const string Hit = "hit";
+ public const string Conditional = "conditional";
+ public const string HitDisabled = "hit-disabled";
+}
+
+///
+/// Event args for the breakpoint-column-toggle event.
+///
+public class BreakpointColumnToggleEventArgs : EventArgs
+{
+ public string RecordId { get; set; } = string.Empty;
+ public string NewState { get; set; } = string.Empty;
+ public string OldState { get; set; } = string.Empty;
+}
+
+///
+/// Event args for the breakpoint-column-context-menu event.
+///
+public class BreakpointColumnContextMenuEventArgs : EventArgs
+{
+ public string RecordId { get; set; } = string.Empty;
+ public string CurrentState { get; set; } = string.Empty;
+}
diff --git a/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor
new file mode 100644
index 0000000000..0031c4385f
--- /dev/null
+++ b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor
@@ -0,0 +1,10 @@
+@namespace OkBlazor
+
+
+
diff --git a/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor.cs b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor.cs
new file mode 100644
index 0000000000..98cd975b93
--- /dev/null
+++ b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/OkTsTableColumnBreakpoint.razor.cs
@@ -0,0 +1,54 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+
+namespace OkBlazor;
+
+public partial class OkTsTableColumnBreakpoint : ComponentBase
+{
+ ///
+ /// The ID of the column.
+ ///
+ [Parameter]
+ public string? ColumnId { get; set; }
+
+ ///
+ /// Gets or sets the field in the data record that contains the breakpoint state value.
+ ///
+ [Parameter]
+ [DisallowNull]
+ public string FieldName { get; set; } = null!;
+
+ ///
+ /// Whether or not the column should be hidden.
+ ///
+ [Parameter]
+ public bool? ColumnHidden { get; set; }
+
+ ///
+ /// Gets or sets a callback invoked when a breakpoint is toggled (clicked).
+ ///
+ [Parameter]
+ public EventCallback BreakpointToggle { get; set; }
+
+ ///
+ /// Gets or sets a callback invoked when a context menu is requested on a breakpoint.
+ ///
+ [Parameter]
+ public EventCallback BreakpointContextMenu { get; set; }
+
+ ///
+ /// Any additional attributes that did not match known properties.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary? AdditionalAttributes { get; set; }
+
+ protected async void HandleBreakpointToggle(BreakpointColumnToggleEventArgs eventArgs)
+ {
+ await BreakpointToggle.InvokeAsync(eventArgs);
+ }
+
+ protected async void HandleBreakpointContextMenu(BreakpointColumnContextMenuEventArgs eventArgs)
+ {
+ await BreakpointContextMenu.InvokeAsync(eventArgs);
+ }
+}
diff --git a/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js b/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
index 77525ed3e3..010ed5b69e 100644
--- a/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
+++ b/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
@@ -23,17 +23,26 @@ function registerEvents(Blazor) {
hasRegisteredEvents = true;
- /* Register any custom events here
- Blazor.registerCustomEventType('okeventname', {
- browserEventName: 'foo',
+ Blazor.registerCustomEventType('okbreakpointcolumntoggle', {
+ browserEventName: 'breakpoint-column-toggle',
createEventArgs: event => {
return {
+ recordId: event.detail.recordId,
newState: event.detail.newState,
oldState: event.detail.oldState
};
}
});
- */
+
+ Blazor.registerCustomEventType('okbreakpointcolumncontextmenu', {
+ browserEventName: 'breakpoint-column-context-menu',
+ createEventArgs: event => {
+ return {
+ recordId: event.detail.recordId,
+ currentState: event.detail.currentState
+ };
+ }
+ });
}
function handleRuntimeStarted() {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
index 65754a0fb7..d1dc4e7e58 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -1,10 +1,13 @@
import { DesignSystem } from '@ni/fast-foundation';
+import { observable } from '@ni/fast-element';
+import { eventChange, keyEscape } from '@ni/fast-web-utilities';
import { TableCellView } from '@ni/nimble-components/dist/esm/table-column/base/cell-view';
-import type { MenuButton } from '@ni/nimble-components/dist/esm/menu-button';
import type { Table } from '@ni/nimble-components/dist/esm/table';
+import type { AnchoredRegion } from '@ni/nimble-components/dist/esm/anchored-region';
+import type { CellViewSlotRequestEventDetail } from '@ni/nimble-components/dist/esm/table/types';
import { template } from './template';
import { styles } from './styles';
-import { BreakpointState, type BreakpointToggleEventDetail } from '../types';
+import { BreakpointState, type BreakpointToggleEventDetail, type BreakpointContextMenuEventDetail } from '../types';
import type { TsTableColumnBreakpointCellRecord, TsTableColumnBreakpointColumnConfig } from '../index';
declare global {
@@ -20,15 +23,27 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
TsTableColumnBreakpointCellRecord,
TsTableColumnBreakpointColumnConfig
> {
+ /** @internal */
+ @observable
+ public open = false;
+
+ /** @internal */
+ public button?: HTMLButtonElement;
+
+ /** @internal */
+ @observable
+ public region?: AnchoredRegion;
+
+ /** @internal */
+ @observable
+ public slottedMenus?: HTMLElement[];
+
private static readonly contextMenuKey = 'ContextMenu';
private static readonly legacyContextMenuKey = 'Apps';
private static readonly menuKeyAlias = 'Menu';
- /** @internal */
- public contextMenuButton?: MenuButton;
-
/** @internal */
public get currentState(): BreakpointState {
const value = this.cellRecord?.value;
@@ -55,19 +70,77 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
return 'Breakpoint disabled';
case BreakpointState.hit:
return 'Breakpoint hit';
+ case BreakpointState.conditional:
+ return 'Conditional breakpoint';
+ case BreakpointState.hitDisabled:
+ return 'Breakpoint hit (disabled)';
default:
return 'Add breakpoint';
}
}
public override get tabbableChildren(): HTMLElement[] {
- const button = this.shadowRoot?.querySelector('.breakpoint-button') as HTMLElement | null;
- if (button) {
- return [button];
+ if (this.button) {
+ return [this.button];
}
return [];
}
+ public regionChanged(
+ prev: AnchoredRegion | undefined,
+ _next: AnchoredRegion | undefined
+ ): void {
+ if (prev) {
+ prev.removeEventListener(eventChange, this.menuChangeHandler);
+ }
+
+ if (this.region && this.button) {
+ this.region.anchorElement = this.button;
+ this.region.addEventListener(eventChange, this.menuChangeHandler, {
+ capture: true
+ });
+ }
+ }
+
+ public override disconnectedCallback(): void {
+ super.disconnectedCallback();
+ if (this.region) {
+ this.region.removeEventListener(eventChange, this.menuChangeHandler);
+ }
+ }
+
+ /** @internal */
+ public onContextMenuKeyDown(e: KeyboardEvent): boolean {
+ switch (e.key) {
+ case keyEscape:
+ this.setContextMenuOpen(false);
+ this.button?.focus();
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /** @internal */
+ public regionLoadedHandler(): void {
+ this.focusMenu();
+ }
+
+ /** @internal */
+ public onContextMenuFocusOut(e: FocusEvent): boolean {
+ if (!this.open) {
+ return true;
+ }
+
+ const focusTarget = e.relatedTarget as HTMLElement;
+ if (!this.contains(focusTarget) && !this.getMenu()?.contains(focusTarget)) {
+ this.setContextMenuOpen(false);
+ return false;
+ }
+
+ return true;
+ }
+
/** @internal */
public onButtonClick(event: Event): void {
event.stopPropagation();
@@ -82,7 +155,7 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
public onContextMenu(event: Event): void {
event.preventDefault();
event.stopPropagation();
- this.openContextMenu();
+ this.emitContextMenu();
}
/** @internal */
@@ -107,7 +180,7 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
|| event.key === TsTableColumnBreakpointCellView.menuKeyAlias) {
event.preventDefault();
event.stopPropagation();
- this.openContextMenu();
+ this.emitContextMenu();
return;
}
@@ -118,26 +191,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
}
}
- /** @internal */
- public onDisableMenuItemSelected(): void {
- this.emitToggle(this.currentState, BreakpointState.disabled);
- }
-
- /** @internal */
- public onAddMenuItemSelected(): void {
- this.emitToggle(this.currentState, BreakpointState.enabled);
- }
-
- /** @internal */
- public onEnableMenuItemSelected(): void {
- this.emitToggle(this.currentState, BreakpointState.enabled);
- }
-
- /** @internal */
- public onRemoveMenuItemSelected(): void {
- this.emitToggle(this.currentState, BreakpointState.off);
- }
-
private emitToggle(
oldState: BreakpointState,
newState: BreakpointState
@@ -150,10 +203,75 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
this.$emit('breakpoint-column-toggle', detail);
}
- private openContextMenu(): void {
- if (this.contextMenuButton && !this.contextMenuButton.open) {
- this.contextMenuButton.open = true;
+ private emitContextMenu(): void {
+ const slotRequestDetail: CellViewSlotRequestEventDetail = {
+ slots: [{ name: 'menu', slot: 'menu' }]
+ };
+ this.$emit('cell-view-slots-request', slotRequestDetail);
+ this.setContextMenuOpen(true);
+ }
+
+ private setContextMenuOpen(newValue: boolean): void {
+ if (this.open === newValue) {
+ return;
+ }
+
+ const detail: BreakpointContextMenuEventDetail = {
+ recordId: this.recordId ?? '',
+ currentState: this.currentState
+ };
+
+ if (newValue) {
+ // Emit beforetoggle when opening
+ this.$emit('breakpoint-column-beforetoggle', detail);
}
+
+ this.open = newValue;
+
+ if (newValue) {
+ // Emit context-menu event only when opening.
+ this.$emit('breakpoint-column-context-menu', detail);
+ }
+ }
+
+ private getMenu(): HTMLElement | undefined {
+ // Resolve nested slot forwarding (table -> row -> cell-view) to find the actual menu.
+ if (!this.slottedMenus || this.slottedMenus.length === 0) {
+ return undefined;
+ }
+
+ let currentItem: HTMLElement | undefined = this.slottedMenus[0];
+ while (currentItem) {
+ if (currentItem.getAttribute('role') === 'menu') {
+ return currentItem;
+ }
+
+ if (this.isSlotElement(currentItem)) {
+ const firstNode = currentItem.assignedNodes()[0];
+ if (firstNode instanceof HTMLElement) {
+ currentItem = firstNode;
+ } else {
+ currentItem = undefined;
+ }
+ } else {
+ return undefined;
+ }
+ }
+
+ return undefined;
+ }
+
+ private isSlotElement(element: HTMLElement | undefined): element is HTMLSlotElement {
+ return element?.nodeName === 'SLOT';
+ }
+
+ private readonly menuChangeHandler = (): void => {
+ this.setContextMenuOpen(false);
+ this.button?.focus();
+ };
+
+ private focusMenu(): void {
+ this.getMenu()?.focus();
}
private tryFocusSiblingBreakpoint(backward: boolean): boolean {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
index d610eec58e..14c34d73e8 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
@@ -21,8 +21,7 @@ export const styles = css`
border: none;
background: transparent;
cursor: pointer;
- border-radius: 50%;
- outline-offset: -2px;
+ outline-offset: -1px;
}
.breakpoint-button:focus-visible {
@@ -42,24 +41,4 @@ export const styles = css`
.breakpoint-button.state-off:focus-visible svg {
opacity: 1;
}
-
- .context-menu-button {
- position: absolute;
- width: 24px;
- height: 24px;
- pointer-events: none;
- }
-
- .context-menu-button::part(button) {
- opacity: 0;
- pointer-events: none;
- }
-
- .context-menu-button[open] {
- pointer-events: auto;
- }
-
- .context-menu-button::part(menu) {
- z-index: 1;
- }
`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
index c32ce4cf72..0df0e0c08e 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -1,18 +1,20 @@
-import { html, ref, when } from '@ni/fast-element';
+import { html, ref, slotted, when } from '@ni/fast-element';
import type { TsTableColumnBreakpointCellView } from './index';
import { BreakpointState } from '../types';
-import { menuButtonTag } from '@ni/nimble-components/dist/esm/menu-button';
-import { menuTag } from '@ni/nimble-components/dist/esm/menu';
-import { menuItemTag } from '@ni/nimble-components/dist/esm/menu-item';
+import { anchoredRegionTag } from '@ni/nimble-components/dist/esm/anchored-region';
// Placeholder SVGs for breakpoint states - to be replaced with proper icons later
const offSvg = html``;
const enabledSvg = html``;
const disabledSvg = html``;
const hitSvg = html``;
+const conditionalSvg = html``;
+const hitDisabledSvg = html``;
export const template = html`
- <${menuButtonTag}
- ${ref('contextMenuButton')}
- class="context-menu-button"
- content-hidden
- position="above"
- tabindex="-1"
- @click="${(_, c) => c.event.stopPropagation()}"
- >
- <${menuTag} slot="menu">
- ${when(x => x.currentState === BreakpointState.off, html`
- <${menuItemTag} @change="${x => x.onAddMenuItemSelected()}">
- Add breakpoint
- ${menuItemTag}>
- `)}
- ${when(x => x.currentState === BreakpointState.enabled || x.currentState === BreakpointState.hit, html`
- <${menuItemTag} @change="${x => x.onDisableMenuItemSelected()}">
- Disable breakpoint
- ${menuItemTag}>
- <${menuItemTag} @change="${x => x.onRemoveMenuItemSelected()}">
- Remove breakpoint
- ${menuItemTag}>
- `)}
- ${when(x => x.currentState === BreakpointState.disabled, html`
- <${menuItemTag} @change="${x => x.onEnableMenuItemSelected()}">
- Enable breakpoint
- ${menuItemTag}>
- <${menuItemTag} @change="${x => x.onRemoveMenuItemSelected()}">
- Remove breakpoint
- ${menuItemTag}>
- `)}
- ${menuTag}>
- ${menuButtonTag}>
-`;
+ ${when(x => x.open, html`
+ <${anchoredRegionTag}
+ ${ref('region')}
+ part="menu"
+ fixed-placement="true"
+ auto-update-mode="auto"
+ horizontal-inset="true"
+ horizontal-positioning-mode="dynamic"
+ vertical-positioning-mode="dynamic"
+ @loaded="${x => x.regionLoadedHandler()}"
+ @focusout="${(x, c) => x.onContextMenuFocusOut(c.event as FocusEvent)}"
+ @keydown="${(x, c) => x.onContextMenuKeyDown(c.event as KeyboardEvent)}"
+ >
+
+ ${anchoredRegionTag}>
+ `)}`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
index e30be40c45..2f6a39d9b4 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -8,7 +8,7 @@ import type { ColumnInternalsOptions } from '@ni/nimble-components/dist/esm/tabl
import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
import { ColumnValidator } from '@ni/nimble-components/dist/esm/table-column/base/models/column-validator';
import { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
-import { type BreakpointToggleEventDetail } from './types';
+import { type BreakpointToggleEventDetail, type BreakpointContextMenuEventDetail } from './types';
import type { DelegatedEventEventDetails } from '@ni/nimble-components/dist/esm/table-column/base/types';
export type TsTableColumnBreakpointCellRecord = TableStringField<'value'>;
@@ -42,14 +42,21 @@ export class TsTableColumnBreakpoint extends TableColumn;
- const originalEvent = event.detail.originalEvent as CustomEvent;
- if (originalEvent.type === 'breakpoint-column-toggle') {
+ if (event.detail.originalEvent.type === 'breakpoint-column-toggle') {
+ const originalEvent = event.detail.originalEvent as CustomEvent;
const detail: BreakpointToggleEventDetail = {
...originalEvent.detail,
recordId: event.detail.recordId
};
this.$emit('breakpoint-column-toggle', detail);
+ } else if (event.detail.originalEvent.type === 'breakpoint-column-context-menu') {
+ const originalEvent = event.detail.originalEvent as CustomEvent;
+ const detail: BreakpointContextMenuEventDetail = {
+ ...originalEvent.detail,
+ recordId: event.detail.recordId
+ };
+ this.$emit('breakpoint-column-context-menu', detail);
}
}
@@ -57,7 +64,8 @@ export class TsTableColumnBreakpoint extends TableColumn([])
};
}
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index f8fca68b0c..3d2f4200b6 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -294,12 +294,18 @@ describe('TsTableColumnBreakpoint', () => {
});
describe('context menu', () => {
- it('opens internal nimble context menu when state is off', async () => {
+ it('emits breakpoint-column-context-menu on right-click when state is off', async () => {
await table.setData([
{ id: '1', breakpointState: BreakpointState.off }
]);
await waitForUpdatesAsync();
+ const contextMenuSpy = jasmine.createSpy('contextmenu');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-context-menu',
+ contextMenuSpy
+ );
+
const cellView = tablePageObject.getRenderedCellView(
0,
0
@@ -312,15 +318,24 @@ describe('TsTableColumnBreakpoint', () => {
);
await waitForUpdatesAsync();
- expect(cellView.contextMenuButton?.open).toBeTrue();
+ expect(contextMenuSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ expect(eventDetail.recordId).toBe('1');
+ expect(eventDetail.currentState).toBe(BreakpointState.off);
});
- it('opens internal nimble context menu when state is enabled', async () => {
+ it('emits breakpoint-column-context-menu on right-click when state is enabled', async () => {
await table.setData([
{ id: '1', breakpointState: BreakpointState.enabled }
]);
await waitForUpdatesAsync();
+ const contextMenuSpy = jasmine.createSpy('contextmenu');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-context-menu',
+ contextMenuSpy
+ );
+
const cellView = tablePageObject.getRenderedCellView(
0,
0
@@ -333,15 +348,24 @@ describe('TsTableColumnBreakpoint', () => {
);
await waitForUpdatesAsync();
- expect(cellView.contextMenuButton?.open).toBeTrue();
+ expect(contextMenuSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ expect(eventDetail.recordId).toBe('1');
+ expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
- it('opens internal nimble context menu on Shift+F10', async () => {
+ it('emits breakpoint-column-context-menu on Shift+F10', async () => {
await table.setData([
{ id: '1', breakpointState: BreakpointState.enabled }
]);
await waitForUpdatesAsync();
+ const contextMenuSpy = jasmine.createSpy('contextmenu');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-context-menu',
+ contextMenuSpy
+ );
+
const cellView = tablePageObject.getRenderedCellView(
0,
0
@@ -359,15 +383,24 @@ describe('TsTableColumnBreakpoint', () => {
);
await waitForUpdatesAsync();
- expect(cellView.contextMenuButton?.open).toBeTrue();
+ expect(contextMenuSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ expect(eventDetail.recordId).toBe('1');
+ expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
- it('opens internal nimble context menu on Menu key', async () => {
+ it('emits breakpoint-column-context-menu on ContextMenu key', async () => {
await table.setData([
{ id: '1', breakpointState: BreakpointState.enabled }
]);
await waitForUpdatesAsync();
+ const contextMenuSpy = jasmine.createSpy('contextmenu');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-context-menu',
+ contextMenuSpy
+ );
+
const cellView = tablePageObject.getRenderedCellView(
0,
0
@@ -384,7 +417,10 @@ describe('TsTableColumnBreakpoint', () => {
);
await waitForUpdatesAsync();
- expect(cellView.contextMenuButton?.open).toBeTrue();
+ expect(contextMenuSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ expect(eventDetail.recordId).toBe('1');
+ expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
});
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/types.ts b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
index 0304690018..0569c070b7 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/types.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
@@ -5,7 +5,9 @@ export const BreakpointState = {
off: 'off',
enabled: 'enabled',
disabled: 'disabled',
- hit: 'hit'
+ hit: 'hit',
+ conditional: 'conditional',
+ hitDisabled: 'hit-disabled'
} as const;
export type BreakpointState =
(typeof BreakpointState)[keyof typeof BreakpointState];
@@ -18,3 +20,11 @@ export interface BreakpointToggleEventDetail {
newState: BreakpointState;
oldState: BreakpointState;
}
+
+/**
+ * The event detail for the `breakpoint-column-context-menu` event.
+ */
+export interface BreakpointContextMenuEventDetail {
+ recordId: string;
+ currentState: BreakpointState;
+}
From 5e9df873467c50248dbeb4c324e25f109d0bf33d Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Wed, 20 May 2026 19:59:32 -0500
Subject: [PATCH 03/23] lint errors
---
.../breakpoint/cell-view/index.ts | 22 ++---
.../breakpoint/cell-view/template.ts | 4 +-
.../src/ts/table-column/breakpoint/index.ts | 8 +-
.../ts/table-column/breakpoint/template.ts | 2 +-
.../tests/ts-table-column-breakpoint.spec.ts | 89 +++++++++----------
.../src/ts/table-column/breakpoint/types.ts | 3 +-
.../ts-table-column-breakpoint.stories.ts | 16 ++--
7 files changed, 70 insertions(+), 74 deletions(-)
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
index d1dc4e7e58..2da033cd40 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -8,7 +8,7 @@ import type { CellViewSlotRequestEventDetail } from '@ni/nimble-components/dist/
import { template } from './template';
import { styles } from './styles';
import { BreakpointState, type BreakpointToggleEventDetail, type BreakpointContextMenuEventDetail } from '../types';
-import type { TsTableColumnBreakpointCellRecord, TsTableColumnBreakpointColumnConfig } from '../index';
+import type { TsTableColumnBreakpointCellRecord, TsTableColumnBreakpointColumnConfig } from '..';
declare global {
interface HTMLElementTagNameMap {
@@ -23,6 +23,12 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
TsTableColumnBreakpointCellRecord,
TsTableColumnBreakpointColumnConfig
> {
+ private static readonly contextMenuKey = 'ContextMenu';
+
+ private static readonly legacyContextMenuKey = 'Apps';
+
+ private static readonly menuKeyAlias = 'Menu';
+
/** @internal */
@observable
public open = false;
@@ -38,12 +44,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
@observable
public slottedMenus?: HTMLElement[];
- private static readonly contextMenuKey = 'ContextMenu';
-
- private static readonly legacyContextMenuKey = 'Apps';
-
- private static readonly menuKeyAlias = 'Menu';
-
/** @internal */
public get currentState(): BreakpointState {
const value = this.cellRecord?.value;
@@ -276,15 +276,15 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
private tryFocusSiblingBreakpoint(backward: boolean): boolean {
const currentCell = this.getContainingHost(this) as {
- getRootNode: () => Node;
+ getRootNode: () => Node
} | undefined;
if (!currentCell) {
return false;
}
const currentRow = this.getContainingHost(currentCell) as {
- getFocusableElements: () => { cells: Array<{ cell: { cellView: TableCellView } }> };
- getRootNode: () => Node;
+ getFocusableElements: () => { cells: { cell: { cellView: TableCellView } }[] },
+ getRootNode: () => Node
} | undefined;
if (!currentRow) {
return false;
@@ -310,7 +310,7 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
const delta = backward ? -1 : 1;
for (let i = rowIndex + delta; i >= 0 && i < rowElements.length; i += delta) {
const row = rowElements[i] as {
- getFocusableElements?: () => { cells: Array<{ cell: { cellView: TableCellView } }> };
+ getFocusableElements?: () => { cells: { cell: { cellView: TableCellView } }[] }
};
if (!row.getFocusableElements) {
continue;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
index 0df0e0c08e..c9deb28ed1 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -1,7 +1,7 @@
import { html, ref, slotted, when } from '@ni/fast-element';
-import type { TsTableColumnBreakpointCellView } from './index';
-import { BreakpointState } from '../types';
import { anchoredRegionTag } from '@ni/nimble-components/dist/esm/anchored-region';
+import type { TsTableColumnBreakpointCellView } from '.';
+import { BreakpointState } from '../types';
// Placeholder SVGs for breakpoint states - to be replaced with proper icons later
const offSvg = html``;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
index 2f6a39d9b4..1394ff46bd 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -1,15 +1,15 @@
import { DesignSystem } from '@ni/fast-foundation';
import { attr } from '@ni/fast-element';
-import { template } from './template';
-import { styles } from './styles';
import type { TableStringField } from '@ni/nimble-components/dist/esm/table/types';
-import { tsTableColumnBreakpointCellViewTag } from './cell-view';
import type { ColumnInternalsOptions } from '@ni/nimble-components/dist/esm/table-column/base/models/column-internals';
import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
import { ColumnValidator } from '@ni/nimble-components/dist/esm/table-column/base/models/column-validator';
import { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
-import { type BreakpointToggleEventDetail, type BreakpointContextMenuEventDetail } from './types';
import type { DelegatedEventEventDetails } from '@ni/nimble-components/dist/esm/table-column/base/types';
+import type { BreakpointToggleEventDetail, BreakpointContextMenuEventDetail } from './types';
+import { tsTableColumnBreakpointCellViewTag } from './cell-view';
+import { styles } from './styles';
+import { template } from './template';
export type TsTableColumnBreakpointCellRecord = TableStringField<'value'>;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/template.ts
index b6b37cbd6f..02bb11910d 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/template.ts
@@ -1,6 +1,6 @@
import { html } from '@ni/fast-element';
-import type { TsTableColumnBreakpoint } from '.';
import { template as baseTemplate } from '@ni/nimble-components/dist/esm/table-column/base/template';
+import type { TsTableColumnBreakpoint } from '.';
export const template = html`
x.onDelegatedEvent(c.event)}"
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index 3d2f4200b6..36d7526737 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -3,11 +3,15 @@ import { tableTag, type Table } from '@ni/nimble-components/dist/esm/table';
import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';
import { TablePageObject } from '@ni/nimble-components/dist/esm/table/testing/table.pageobject';
import type { TableRecord } from '@ni/nimble-components/dist/esm/table/types';
+import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
import { fixture, type Fixture } from '../../../../utilities/tests/fixture';
import { TsTableColumnBreakpoint, tsTableColumnBreakpointTag } from '..';
import { TsTableColumnBreakpointCellView } from '../cell-view';
-import { BreakpointState, type BreakpointToggleEventDetail } from '../types';
-import { singleIconColumnWidth } from '@ni/nimble-components/dist/esm/table-column/base/types';
+import {
+ BreakpointState,
+ type BreakpointToggleEventDetail,
+ type BreakpointContextMenuEventDetail
+} from '../types';
interface SimpleTableRecord extends TableRecord {
id?: string;
@@ -38,6 +42,26 @@ describe('TsTableColumnBreakpoint', () => {
);
}
+ function getBreakpointButton(
+ cellView: TsTableColumnBreakpointCellView
+ ): HTMLButtonElement {
+ const button = cellView.shadowRoot!.querySelector(
+ '.breakpoint-button'
+ );
+ if (!button) {
+ throw new Error('Expected breakpoint button');
+ }
+ return button;
+ }
+
+ function getContextMenuEventDetail(
+ contextMenuSpy: jasmine.Spy
+ ): BreakpointContextMenuEventDetail {
+ return (
+ contextMenuSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ }
+
beforeEach(async () => {
elementReferences = new ElementReferences();
({ connect, disconnect } = await setup(elementReferences));
@@ -173,9 +197,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.click();
await waitForUpdatesAsync();
@@ -204,9 +226,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.click();
await waitForUpdatesAsync();
@@ -235,9 +255,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.click();
await waitForUpdatesAsync();
@@ -310,16 +328,14 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new MouseEvent('contextmenu', { bubbles: true })
);
await waitForUpdatesAsync();
expect(contextMenuSpy).toHaveBeenCalledTimes(1);
- const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ const eventDetail = getContextMenuEventDetail(contextMenuSpy);
expect(eventDetail.recordId).toBe('1');
expect(eventDetail.currentState).toBe(BreakpointState.off);
});
@@ -340,16 +356,14 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new MouseEvent('contextmenu', { bubbles: true })
);
await waitForUpdatesAsync();
expect(contextMenuSpy).toHaveBeenCalledTimes(1);
- const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ const eventDetail = getContextMenuEventDetail(contextMenuSpy);
expect(eventDetail.recordId).toBe('1');
expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
@@ -370,9 +384,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new KeyboardEvent('keydown', {
@@ -384,7 +396,7 @@ describe('TsTableColumnBreakpoint', () => {
await waitForUpdatesAsync();
expect(contextMenuSpy).toHaveBeenCalledTimes(1);
- const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ const eventDetail = getContextMenuEventDetail(contextMenuSpy);
expect(eventDetail.recordId).toBe('1');
expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
@@ -405,9 +417,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new KeyboardEvent('keydown', {
@@ -418,7 +428,7 @@ describe('TsTableColumnBreakpoint', () => {
await waitForUpdatesAsync();
expect(contextMenuSpy).toHaveBeenCalledTimes(1);
- const eventDetail = contextMenuSpy.calls.first().args[0].detail;
+ const eventDetail = getContextMenuEventDetail(contextMenuSpy);
expect(eventDetail.recordId).toBe('1');
expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
@@ -441,9 +451,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new KeyboardEvent('keydown', {
@@ -477,9 +485,7 @@ describe('TsTableColumnBreakpoint', () => {
0,
0
) as TsTableColumnBreakpointCellView;
- const button = cellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const button = getBreakpointButton(cellView);
button.dispatchEvent(
new KeyboardEvent('keydown', {
@@ -513,12 +519,8 @@ describe('TsTableColumnBreakpoint', () => {
1,
0
) as TsTableColumnBreakpointCellView;
- const firstButton = firstCellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
- const secondButton = secondCellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const firstButton = getBreakpointButton(firstCellView);
+ const secondButton = getBreakpointButton(secondCellView);
firstButton.focus();
firstButton.dispatchEvent(
@@ -547,12 +549,8 @@ describe('TsTableColumnBreakpoint', () => {
1,
0
) as TsTableColumnBreakpointCellView;
- const firstButton = firstCellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
- const secondButton = secondCellView.shadowRoot!.querySelector(
- '.breakpoint-button'
- ) as HTMLButtonElement;
+ const firstButton = getBreakpointButton(firstCellView);
+ const secondButton = getBreakpointButton(secondCellView);
secondButton.focus();
secondButton.dispatchEvent(
@@ -565,7 +563,6 @@ describe('TsTableColumnBreakpoint', () => {
expect(firstButton.matches(':focus')).toBeTrue();
});
-
});
describe('field-name attribute', () => {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/types.ts b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
index 0569c070b7..61afeb9612 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/types.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
@@ -9,8 +9,7 @@ export const BreakpointState = {
conditional: 'conditional',
hitDisabled: 'hit-disabled'
} as const;
-export type BreakpointState =
- (typeof BreakpointState)[keyof typeof BreakpointState];
+export type BreakpointState = (typeof BreakpointState)[keyof typeof BreakpointState];
/**
* The event detail for the `breakpoint-column-toggle` event.
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
index 72767b451f..5294ff0cab 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
@@ -1,7 +1,7 @@
import { html, ref } from '@ni/fast-element';
import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite';
import { withActions } from 'storybook/actions/decorator';
-import { tableTag, type Table } from '@ni/nimble-components/dist/esm/table';
+import { tableTag } from '@ni/nimble-components/dist/esm/table';
import type { TableRecord } from '@ni/nimble-components/dist/esm/table/types';
import { tableColumnTextTag } from '@ni/nimble-components/dist/esm/table-column/text';
import { tsTableColumnBreakpointTag } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint';
@@ -100,13 +100,13 @@ export const breakpointColumn: StoryObj = {
<${tsTableColumnBreakpointTag}
field-name="${x => x.fieldName}"
@breakpoint-column-toggle="${(x, c) => {
- const event = c.event as CustomEvent;
- const detail = event.detail;
- x.currentData = x.currentData.map(record => (record.id === detail.recordId
- ? { ...record, breakpointState: detail.newState }
- : record));
- void x.tableRef.setData(x.currentData);
- }}"
+ const event = c.event as CustomEvent;
+ const detail = event.detail;
+ x.currentData = x.currentData.map(record => (record.id === detail.recordId
+ ? { ...record, breakpointState: detail.newState }
+ : record));
+ void x.tableRef.setData(x.currentData);
+ }}"
>
${tsTableColumnBreakpointTag}>
<${tableColumnTextTag} field-name="code">
From 6a899174d2fc05384fa780cd2fd61818ef722c22 Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Wed, 20 May 2026 20:00:18 -0500
Subject: [PATCH 04/23] Change files
---
...@ni-ok-blazor-39bf770b-9e85-4d46-ba51-c915ecd4c543.json | 7 +++++++
...ok-components-744c8788-8d43-48b6-9bff-7f72757113b5.json | 7 +++++++
2 files changed, 14 insertions(+)
create mode 100644 change/@ni-ok-blazor-39bf770b-9e85-4d46-ba51-c915ecd4c543.json
create mode 100644 change/@ni-ok-components-744c8788-8d43-48b6-9bff-7f72757113b5.json
diff --git a/change/@ni-ok-blazor-39bf770b-9e85-4d46-ba51-c915ecd4c543.json b/change/@ni-ok-blazor-39bf770b-9e85-4d46-ba51-c915ecd4c543.json
new file mode 100644
index 0000000000..bd8503ae99
--- /dev/null
+++ b/change/@ni-ok-blazor-39bf770b-9e85-4d46-ba51-c915ecd4c543.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "add breakpoint column",
+ "packageName": "@ni/ok-blazor",
+ "email": "5265744+hellovolcano@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@ni-ok-components-744c8788-8d43-48b6-9bff-7f72757113b5.json b/change/@ni-ok-components-744c8788-8d43-48b6-9bff-7f72757113b5.json
new file mode 100644
index 0000000000..6e948dd9a7
--- /dev/null
+++ b/change/@ni-ok-components-744c8788-8d43-48b6-9bff-7f72757113b5.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "add breakpoint column",
+ "packageName": "@ni/ok-components",
+ "email": "5265744+hellovolcano@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
From 0dfed811649247fd9c402b004fd0d80838a048ed Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Thu, 21 May 2026 16:45:32 -0500
Subject: [PATCH 05/23] fix context menu event
---
.../breakpoint/cell-view/index.ts | 12 ++++---
.../breakpoint/cell-view/styles.ts | 11 ++++---
.../tests/ts-table-column-breakpoint.spec.ts | 31 +++++++++++++++++++
3 files changed, 45 insertions(+), 9 deletions(-)
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
index 2da033cd40..d6ec909a3a 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -208,7 +208,14 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
slots: [{ name: 'menu', slot: 'menu' }]
};
this.$emit('cell-view-slots-request', slotRequestDetail);
+
+ const detail: BreakpointContextMenuEventDetail = {
+ recordId: this.recordId ?? '',
+ currentState: this.currentState
+ };
+
this.setContextMenuOpen(true);
+ this.$emit('breakpoint-column-context-menu', detail);
}
private setContextMenuOpen(newValue: boolean): void {
@@ -227,11 +234,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
}
this.open = newValue;
-
- if (newValue) {
- // Emit context-menu event only when opening.
- this.$emit('breakpoint-column-context-menu', detail);
- }
}
private getMenu(): HTMLElement | undefined {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
index 14c34d73e8..9920c7a267 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
@@ -1,4 +1,7 @@
import { css } from '@ni/fast-element';
+import {
+ iconSize,
+} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens';
export const styles = css`
:host {
@@ -14,8 +17,8 @@ export const styles = css`
display: flex;
align-items: center;
justify-content: center;
- width: 24px;
- height: 24px;
+ width: 100%;
+ height: 100%;
padding: 0;
margin: 0;
border: none;
@@ -29,8 +32,8 @@ export const styles = css`
}
.breakpoint-button svg {
- width: 12px;
- height: 12px;
+ width: ${iconSize};
+ height: ${iconSize};
}
.breakpoint-button.state-off svg {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index 36d7526737..644e660b37 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -432,6 +432,37 @@ describe('TsTableColumnBreakpoint', () => {
expect(eventDetail.recordId).toBe('1');
expect(eventDetail.currentState).toBe(BreakpointState.enabled);
});
+
+ it('emits breakpoint-column-context-menu when right-clicking while menu is already open', async () => {
+ await table.setData([
+ { id: '1', breakpointState: BreakpointState.enabled }
+ ]);
+ await waitForUpdatesAsync();
+
+ const contextMenuSpy = jasmine.createSpy('contextmenu');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-context-menu',
+ contextMenuSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = getBreakpointButton(cellView);
+
+ button.dispatchEvent(
+ new MouseEvent('contextmenu', { bubbles: true })
+ );
+ await waitForUpdatesAsync();
+
+ button.dispatchEvent(
+ new MouseEvent('contextmenu', { bubbles: true })
+ );
+ await waitForUpdatesAsync();
+
+ expect(contextMenuSpy).toHaveBeenCalledTimes(2);
+ });
});
describe('keyboard shortcuts', () => {
From c70a4a4d40ac0e67b81c1ae6f1be8881c2df3aa5 Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Thu, 21 May 2026 17:04:46 -0500
Subject: [PATCH 06/23] add blazor example
---
.../Demo.Shared/Pages/ComponentsDemo.razor | 1 +
.../Ts/TsBreakpointTableSection.razor | 38 +++++++
.../Ts/TsBreakpointTableSection.razor.cs | 106 ++++++++++++++++++
.../Pages/Sections/Ts/TsSection.razor | 3 +
.../ts-table-column-breakpoint.stories.ts | 16 ++-
5 files changed, 162 insertions(+), 2 deletions(-)
create mode 100644 packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
create mode 100644 packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
create mode 100644 packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsSection.razor
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/ComponentsDemo.razor b/packages/blazor-workspace/Examples/Demo.Shared/Pages/ComponentsDemo.razor
index fc01301b41..12ac3706c7 100644
--- a/packages/blazor-workspace/Examples/Demo.Shared/Pages/ComponentsDemo.razor
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/ComponentsDemo.razor
@@ -45,5 +45,6 @@
+
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
new file mode 100644
index 0000000000..40197086bf
--- /dev/null
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
@@ -0,0 +1,38 @@
+@namespace Demo.Shared.Pages.Sections
+
+
+
+
+
+
+
+ Name
+ Line
+
+ Last event: @_lastEvent
+
+
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
new file mode 100644
index 0000000000..a93b398035
--- /dev/null
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
@@ -0,0 +1,106 @@
+using NimbleBlazor;
+using OkBlazor;
+
+namespace Demo.Shared.Pages.Sections;
+
+public partial class TsBreakpointTableSection
+{
+ private NimbleTable? _table;
+ private string? _contextMenuRecordId;
+ private string _contextMenuRecordState = BreakpointState.Off;
+ private string _lastEvent = "(none)";
+
+ private List _tableData = new()
+ {
+ new("1", "Main.cs", 12, BreakpointState.Enabled),
+ new("2", "Helper.cs", 45, BreakpointState.Off),
+ new("3", "Service.cs", 78, BreakpointState.Disabled),
+ new("4", "Controller.cs", 23, BreakpointState.Hit),
+ new("5", "Model.cs", 91, BreakpointState.Conditional),
+ new("6", "Startup.cs", 5, BreakpointState.HitDisabled),
+ new("7", "Program.cs", 1, BreakpointState.Off),
+ };
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(_tableData);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ private void OnBreakpointToggle(BreakpointColumnToggleEventArgs e)
+ {
+ var record = _tableData.FirstOrDefault(r => r.Id == e.RecordId);
+ if (record != null)
+ {
+ record.BreakpointState = e.NewState;
+ }
+ _lastEvent = $"Toggle: record={e.RecordId}, {e.OldState} -> {e.NewState}";
+ StateHasChanged();
+ }
+
+ private void OnBreakpointContextMenu(BreakpointColumnContextMenuEventArgs e)
+ {
+ _contextMenuRecordId = e.RecordId;
+ _contextMenuRecordState = e.CurrentState;
+ _lastEvent = $"ContextMenu: record={e.RecordId}, state={e.CurrentState}";
+ StateHasChanged();
+ }
+
+ private void OnAddBreakpoint()
+ {
+ CloseContextMenuAndSetState(BreakpointState.Enabled);
+ }
+
+ private void OnAddConditionalBreakpoint()
+ {
+ CloseContextMenuAndSetState(BreakpointState.Conditional);
+ }
+
+ private void OnRemoveBreakpoint()
+ {
+ CloseContextMenuAndSetState(BreakpointState.Off);
+ }
+
+ private void OnDisableBreakpoint()
+ {
+ CloseContextMenuAndSetState(BreakpointState.Disabled);
+ }
+
+ private void OnEnableBreakpoint()
+ {
+ CloseContextMenuAndSetState(BreakpointState.Enabled);
+ }
+
+ private void CloseContextMenuAndSetState(string newState)
+ {
+ // Close the context menu by triggering Escape key on the element
+ // This will be handled by the cell-view's anchored-region
+ SetRecordState(newState);
+ }
+
+ private void SetRecordState(string newState)
+ {
+ var record = _tableData.FirstOrDefault(r => r.Id == _contextMenuRecordId);
+ if (record != null)
+ {
+ record.BreakpointState = newState;
+ }
+ StateHasChanged();
+ }
+}
+
+public class BreakpointTableRecord
+{
+ public BreakpointTableRecord(string id, string name, int lineNumber, string breakpointState)
+ {
+ Id = id;
+ Name = name;
+ LineNumber = lineNumber;
+ BreakpointState = breakpointState;
+ }
+
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public int LineNumber { get; set; }
+ public string BreakpointState { get; set; }
+}
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsSection.razor b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsSection.razor
new file mode 100644
index 0000000000..e16222eed9
--- /dev/null
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsSection.razor
@@ -0,0 +1,3 @@
+@namespace Demo.Shared.Pages.Sections
+
+
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
index 5294ff0cab..7ae9cc8ac5 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
@@ -63,7 +63,11 @@ const metadata: Meta = {
decorators: [withActions],
parameters: {
actions: {
- handles: [...sharedTableActions, 'breakpoint-column-toggle']
+ handles: [
+ ...sharedTableActions,
+ 'breakpoint-column-toggle',
+ 'breakpoint-column-context-menu'
+ ]
}
},
argTypes: {
@@ -84,6 +88,7 @@ export default metadata;
interface BreakpointColumnTableArgs extends SharedTableArgs {
fieldName: string;
toggleEvent: never;
+ contextMenuEvent: never;
currentData: CodeRecord[];
}
@@ -125,7 +130,14 @@ export const breakpointColumn: StoryObj = {
toggleEvent: {
name: 'breakpoint-column-toggle',
description:
- 'Emitted when a breakpoint is toggled via click, keyboard, or context menu. The event detail includes `recordId`, `oldState`, and `newState`.',
+ 'Emitted when a breakpoint is toggled via click or keyboard. The event detail includes `recordId`, `oldState`, and `newState`.',
+ control: false,
+ table: { category: apiCategory.events }
+ },
+ contextMenuEvent: {
+ name: 'breakpoint-column-context-menu',
+ description:
+ 'Emitted when the breakpoint context menu is requested. The event detail includes `recordId` and `currentState`.',
control: false,
table: { category: apiCategory.events }
},
From 380d34de9c2ba33bc27c7d3c60bd1549a8d4bb7b Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Fri, 22 May 2026 11:22:53 -0500
Subject: [PATCH 07/23] update storybook docs
---
.../breakpoint/cell-view/index.ts | 79 ---------------
.../src/ts/table-column/breakpoint/index.ts | 2 +-
.../tests/ts-table-column-breakpoint.spec.ts | 99 ++++++++-----------
...-table-column-breakpoint-matrix.stories.ts | 10 +-
.../ts-table-column-breakpoint.mdx | 24 +----
.../ts-table-column-breakpoint.stories.ts | 18 ++++
6 files changed, 71 insertions(+), 161 deletions(-)
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
index d6ec909a3a..50e9a8531b 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -2,7 +2,6 @@ import { DesignSystem } from '@ni/fast-foundation';
import { observable } from '@ni/fast-element';
import { eventChange, keyEscape } from '@ni/fast-web-utilities';
import { TableCellView } from '@ni/nimble-components/dist/esm/table-column/base/cell-view';
-import type { Table } from '@ni/nimble-components/dist/esm/table';
import type { AnchoredRegion } from '@ni/nimble-components/dist/esm/anchored-region';
import type { CellViewSlotRequestEventDetail } from '@ni/nimble-components/dist/esm/table/types';
import { template } from './template';
@@ -160,13 +159,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
/** @internal */
public onKeyDown(event: KeyboardEvent): void {
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
- event.preventDefault();
- event.stopPropagation();
- this.tryFocusSiblingBreakpoint(event.key === 'ArrowUp');
- return;
- }
-
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
@@ -223,16 +215,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
return;
}
- const detail: BreakpointContextMenuEventDetail = {
- recordId: this.recordId ?? '',
- currentState: this.currentState
- };
-
- if (newValue) {
- // Emit beforetoggle when opening
- this.$emit('breakpoint-column-beforetoggle', detail);
- }
-
this.open = newValue;
}
@@ -275,67 +257,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
private focusMenu(): void {
this.getMenu()?.focus();
}
-
- private tryFocusSiblingBreakpoint(backward: boolean): boolean {
- const currentCell = this.getContainingHost(this) as {
- getRootNode: () => Node
- } | undefined;
- if (!currentCell) {
- return false;
- }
-
- const currentRow = this.getContainingHost(currentCell) as {
- getFocusableElements: () => { cells: { cell: { cellView: TableCellView } }[] },
- getRootNode: () => Node
- } | undefined;
- if (!currentRow) {
- return false;
- }
-
- const table = this.getContainingHost(currentRow) as Table | undefined;
- if (!table) {
- return false;
- }
-
- const rowElements = table.rowElements;
- const rowIndex = rowElements.findIndex(row => row === currentRow);
- if (rowIndex < 0) {
- return false;
- }
-
- const currentRowCells = currentRow.getFocusableElements().cells;
- const columnIndex = currentRowCells.findIndex(cellInfo => cellInfo.cell === currentCell as unknown as typeof cellInfo.cell);
- if (columnIndex < 0) {
- return false;
- }
-
- const delta = backward ? -1 : 1;
- for (let i = rowIndex + delta; i >= 0 && i < rowElements.length; i += delta) {
- const row = rowElements[i] as {
- getFocusableElements?: () => { cells: { cell: { cellView: TableCellView } }[] }
- };
- if (!row.getFocusableElements) {
- continue;
- }
-
- const cellInfo = row.getFocusableElements().cells[columnIndex];
- const target = cellInfo?.cell.cellView.tabbableChildren[0];
- if (target) {
- target.focus();
- return true;
- }
- }
-
- return false;
- }
-
- private getContainingHost(element: { getRootNode: () => Node }): HTMLElement | undefined {
- const root = element.getRootNode();
- if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
- return root.host;
- }
- return undefined;
- }
}
const tsTableColumnBreakpointCellView = TsTableColumnBreakpointCellView.compose({
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
index 1394ff46bd..4f0b990e92 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -64,7 +64,7 @@ export class TsTableColumnBreakpoint extends TableColumn([])
};
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index 644e660b37..b3cac05b5a 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -268,6 +268,46 @@ describe('TsTableColumnBreakpoint', () => {
});
});
+ describe('table selection does not change', () => {
+ let button: HTMLButtonElement;
+
+ beforeEach(async () => {
+ table.selectionMode = 'multiple';
+ await waitForUpdatesAsync();
+
+ await table.setData([{ id: '1', breakpointState: BreakpointState.off }]);
+ await waitForUpdatesAsync();
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 0,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ button = getBreakpointButton(cellView);
+ button.focus();
+ });
+
+ it('when clicking a breakpoint button', async () => {
+ button.click();
+ await waitForUpdatesAsync();
+
+ const selection = await table.getSelectedRecordIds();
+ expect(selection.length).toBe(0);
+ });
+
+ it('when toggling a breakpoint button by pressing Enter', async () => {
+ button.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Enter',
+ bubbles: true
+ })
+ );
+ await waitForUpdatesAsync();
+
+ const selection = await table.getSelectedRecordIds();
+ expect(selection.length).toBe(0);
+ });
+ });
+
describe('tooltip text', () => {
it('shows "Add breakpoint" when state is off', async () => {
await table.setData([
@@ -535,65 +575,6 @@ describe('TsTableColumnBreakpoint', () => {
expect(eventDetail.newState).toBe(BreakpointState.off);
});
- it('moves focus to next row breakpoint on ArrowDown', async () => {
- await table.setData([
- { id: '1', breakpointState: BreakpointState.enabled },
- { id: '2', breakpointState: BreakpointState.off }
- ]);
- await waitForUpdatesAsync();
-
- const firstCellView = tablePageObject.getRenderedCellView(
- 0,
- 0
- ) as TsTableColumnBreakpointCellView;
- const secondCellView = tablePageObject.getRenderedCellView(
- 1,
- 0
- ) as TsTableColumnBreakpointCellView;
- const firstButton = getBreakpointButton(firstCellView);
- const secondButton = getBreakpointButton(secondCellView);
-
- firstButton.focus();
- firstButton.dispatchEvent(
- new KeyboardEvent('keydown', {
- key: 'ArrowDown',
- bubbles: true
- })
- );
- await waitForUpdatesAsync();
-
- expect(secondButton.matches(':focus')).toBeTrue();
- });
-
- it('moves focus to previous row breakpoint on ArrowUp', async () => {
- await table.setData([
- { id: '1', breakpointState: BreakpointState.off },
- { id: '2', breakpointState: BreakpointState.enabled }
- ]);
- await waitForUpdatesAsync();
-
- const firstCellView = tablePageObject.getRenderedCellView(
- 0,
- 0
- ) as TsTableColumnBreakpointCellView;
- const secondCellView = tablePageObject.getRenderedCellView(
- 1,
- 0
- ) as TsTableColumnBreakpointCellView;
- const firstButton = getBreakpointButton(firstCellView);
- const secondButton = getBreakpointButton(secondCellView);
-
- secondButton.focus();
- secondButton.dispatchEvent(
- new KeyboardEvent('keydown', {
- key: 'ArrowUp',
- bubbles: true
- })
- );
- await waitForUpdatesAsync();
-
- expect(firstButton.matches(':focus')).toBeTrue();
- });
});
describe('field-name attribute', () => {
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
index 620eadfe6a..7d0bf794e9 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint-matrix.stories.ts
@@ -28,10 +28,18 @@ const data = [
},
{
id: '4',
- breakpointState: null
+ breakpointState: BreakpointState.conditional
},
{
id: '5',
+ breakpointState: BreakpointState.hitDisabled
+ },
+ {
+ id: '6',
+ breakpointState: null
+ },
+ {
+ id: '7',
breakpointState: undefined
}
] as const;
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
index 9d2b50d464..3f3a9dd794 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
@@ -8,7 +8,7 @@ import { Tag } from '../../../utilities/story-layout';
The renders a breakpoint indicator in each row that
-can be toggled between different states (Off, Enabled, Disabled, Hit) via click or context menu.
+can be toggled between breakpoint states by click or keyboard interaction.
The column is intended for code-centric table displays where debugging breakpoints need to be
visually indicated and interactively managed.
@@ -24,15 +24,6 @@ The column is neither sortable nor groupable. It has a fixed width of 32 pixels
## Usage
-### State Transitions
-
-- **Off → Enabled:** Click on the breakpoint indicator
-- **Enabled → Off:** Click on the active breakpoint indicator
-- **Enabled → Disabled:** Right-click and select "Disable breakpoint"
-- **Disabled → Enabled:** Right-click and select "Enable breakpoint"
-- **Disabled → Off:** Right-click and select "Remove breakpoint"
-- **Hit → Off:** Click on the hit breakpoint indicator
-
### Breakpoint States
| State | Description |
@@ -41,14 +32,5 @@ The column is neither sortable nor groupable. It has a fixed width of 32 pixels
| `enabled` | Active breakpoint (filled red circle) |
| `disabled` | Inactive breakpoint (outlined red circle with slash) |
| `hit` | Breakpoint currently being hit during debugging (red circle with gold border) |
-
-### Event Handling
-
-Listen to the `breakpoint-column-toggle` event on the column to handle state changes:
-
-```ts
-column.addEventListener('breakpoint-column-toggle', (event) => {
- const { recordId, oldState, newState } = event.detail;
- // Update your data source with the new breakpoint state
-});
-```
+| `conditional` | Conditional breakpoint (diamond indicator) |
+| `hit-disabled` | Hit breakpoint that is disabled |
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
index 7ae9cc8ac5..8840521642 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
@@ -53,6 +53,24 @@ const simpleData: CodeRecord[] = [
{
id: '5',
lineNumber: 5,
+ code: ' if (x > 0) {',
+ breakpointState: BreakpointState.conditional
+ },
+ {
+ id: '6',
+ lineNumber: 6,
+ code: ' throw new Error("hit-disabled");',
+ breakpointState: BreakpointState.hitDisabled
+ },
+ {
+ id: '7',
+ lineNumber: 7,
+ code: ' }',
+ breakpointState: BreakpointState.off
+ },
+ {
+ id: '8',
+ lineNumber: 8,
code: '}',
breakpointState: BreakpointState.off
}
From 172e23af2d64dca3762024ac55997d3532aeeedf Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Fri, 22 May 2026 11:25:19 -0500
Subject: [PATCH 08/23] update docs
---
.../ts-table-column-breakpoint.mdx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
index 3f3a9dd794..fca00aebc6 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.mdx
@@ -28,9 +28,9 @@ The column is neither sortable nor groupable. It has a fixed width of 32 pixels
| State | Description |
|-------|-------------|
-| `off` | No breakpoint set (empty outline shown on hover) |
-| `enabled` | Active breakpoint (filled red circle) |
-| `disabled` | Inactive breakpoint (outlined red circle with slash) |
-| `hit` | Breakpoint currently being hit during debugging (red circle with gold border) |
-| `conditional` | Conditional breakpoint (diamond indicator) |
+| `off` | No breakpoint set |
+| `enabled` | Active breakpoint |
+| `disabled` | Inactive breakpoint |
+| `hit` | Breakpoint currently being hit during debugging |
+| `conditional` | Conditional breakpoint |
| `hit-disabled` | Hit breakpoint that is disabled |
From ebdfba151abf00235431487acccffe861ba21278 Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Fri, 22 May 2026 13:02:23 -0500
Subject: [PATCH 09/23] lint
---
.../breakpoint/tests/ts-table-column-breakpoint.spec.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index b3cac05b5a..d6527bbe2b 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -574,7 +574,6 @@ describe('TsTableColumnBreakpoint', () => {
expect(eventDetail.oldState).toBe(BreakpointState.enabled);
expect(eventDetail.newState).toBe(BreakpointState.off);
});
-
});
describe('field-name attribute', () => {
From 04e871d4e2b09c550cdf2f9099aec175655a8d5f Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Wed, 27 May 2026 17:12:21 -0500
Subject: [PATCH 10/23] fix context menu
---
.../Ts/TsBreakpointTableSection.razor | 15 +-
.../Ts/TsBreakpointTableSection.razor.cs | 37 +++--
.../BreakpointEventArgs.cs | 2 +
.../OkBlazor/wwwroot/OkBlazor.lib.module.js | 4 +-
.../breakpoint/cell-view/index.ts | 140 ++----------------
.../breakpoint/cell-view/styles.ts | 2 +-
.../breakpoint/cell-view/template.ts | 20 +--
.../src/ts/table-column/breakpoint/index.ts | 1 -
.../tests/ts-table-column-breakpoint.spec.ts | 32 ++++
.../src/ts/table-column/breakpoint/types.ts | 2 +
10 files changed, 91 insertions(+), 164 deletions(-)
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
index 40197086bf..d9719bd51e 100644
--- a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor
@@ -2,13 +2,20 @@
-
+
+ Name
-
+
+ @if (_contextMenuOpen)
+ {
+
+
@if (_contextMenuRecordState == OkBlazor.BreakpointState.Off)
{
OnAddBreakpoint())">Add breakpoint
@@ -30,9 +37,7 @@
}
}
- Name
- Line
-
+ }
Last event: @_lastEvent
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
index a93b398035..855f4f506b 100644
--- a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
@@ -6,19 +6,24 @@ namespace Demo.Shared.Pages.Sections;
public partial class TsBreakpointTableSection
{
private NimbleTable? _table;
+ private bool _contextMenuOpen;
+ private double _menuX;
+ private double _menuY;
private string? _contextMenuRecordId;
private string _contextMenuRecordState = BreakpointState.Off;
private string _lastEvent = "(none)";
+ private string ContextMenuStyle => $"position: fixed; left: {_menuX}px; top: {_menuY}px; z-index: 2147483647;";
+
private List _tableData = new()
{
- new("1", "Main.cs", 12, BreakpointState.Enabled),
- new("2", "Helper.cs", 45, BreakpointState.Off),
- new("3", "Service.cs", 78, BreakpointState.Disabled),
- new("4", "Controller.cs", 23, BreakpointState.Hit),
- new("5", "Model.cs", 91, BreakpointState.Conditional),
- new("6", "Startup.cs", 5, BreakpointState.HitDisabled),
- new("7", "Program.cs", 1, BreakpointState.Off),
+ new("1", null, "Main.cs", 12, BreakpointState.Enabled),
+ new("2", "1", "Helper.cs", 45, BreakpointState.Off),
+ new("3", "1", "Service.cs", 78, BreakpointState.Disabled),
+ new("4", null, "Controller.cs", 23, BreakpointState.Hit),
+ new("5", "4", "Model.cs", 91, BreakpointState.Conditional),
+ new("6", null, "Startup.cs", 5, BreakpointState.HitDisabled),
+ new("7", "6", "Program.cs", 1, BreakpointState.Off),
};
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -42,7 +47,10 @@ private void OnBreakpointContextMenu(BreakpointColumnContextMenuEventArgs e)
{
_contextMenuRecordId = e.RecordId;
_contextMenuRecordState = e.CurrentState;
- _lastEvent = $"ContextMenu: record={e.RecordId}, state={e.CurrentState}";
+ _menuX = e.AnchorX;
+ _menuY = e.AnchorY;
+ _contextMenuOpen = true;
+ _lastEvent = $"ContextMenu: record={e.RecordId}, state={e.CurrentState}, x={e.AnchorX}, y={e.AnchorY}";
StateHasChanged();
}
@@ -71,10 +79,15 @@ private void OnEnableBreakpoint()
CloseContextMenuAndSetState(BreakpointState.Enabled);
}
+ private void CloseContextMenu()
+ {
+ _contextMenuOpen = false;
+ StateHasChanged();
+ }
+
private void CloseContextMenuAndSetState(string newState)
{
- // Close the context menu by triggering Escape key on the element
- // This will be handled by the cell-view's anchored-region
+ _contextMenuOpen = false;
SetRecordState(newState);
}
@@ -91,15 +104,17 @@ private void SetRecordState(string newState)
public class BreakpointTableRecord
{
- public BreakpointTableRecord(string id, string name, int lineNumber, string breakpointState)
+ public BreakpointTableRecord(string id, string? parentId, string name, int lineNumber, string breakpointState)
{
Id = id;
+ ParentId = parentId;
Name = name;
LineNumber = lineNumber;
BreakpointState = breakpointState;
}
public string Id { get; set; }
+ public string? ParentId { get; set; }
public string Name { get; set; }
public int LineNumber { get; set; }
public string BreakpointState { get; set; }
diff --git a/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
index 1bcc12bdb3..9d860d2f86 100644
--- a/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
+++ b/packages/blazor-workspace/OkBlazor/Source/Ts/TableColumnBreakpoint/BreakpointEventArgs.cs
@@ -30,4 +30,6 @@ public class BreakpointColumnContextMenuEventArgs : EventArgs
{
public string RecordId { get; set; } = string.Empty;
public string CurrentState { get; set; } = string.Empty;
+ public double AnchorX { get; set; }
+ public double AnchorY { get; set; }
}
diff --git a/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js b/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
index 010ed5b69e..8a08697be2 100644
--- a/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
+++ b/packages/blazor-workspace/OkBlazor/wwwroot/OkBlazor.lib.module.js
@@ -39,7 +39,9 @@ function registerEvents(Blazor) {
createEventArgs: event => {
return {
recordId: event.detail.recordId,
- currentState: event.detail.currentState
+ currentState: event.detail.currentState,
+ anchorX: event.detail.anchorX,
+ anchorY: event.detail.anchorY
};
}
});
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
index 50e9a8531b..de5433933f 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/index.ts
@@ -1,9 +1,5 @@
import { DesignSystem } from '@ni/fast-foundation';
-import { observable } from '@ni/fast-element';
-import { eventChange, keyEscape } from '@ni/fast-web-utilities';
import { TableCellView } from '@ni/nimble-components/dist/esm/table-column/base/cell-view';
-import type { AnchoredRegion } from '@ni/nimble-components/dist/esm/anchored-region';
-import type { CellViewSlotRequestEventDetail } from '@ni/nimble-components/dist/esm/table/types';
import { template } from './template';
import { styles } from './styles';
import { BreakpointState, type BreakpointToggleEventDetail, type BreakpointContextMenuEventDetail } from '../types';
@@ -28,21 +24,9 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
private static readonly menuKeyAlias = 'Menu';
- /** @internal */
- @observable
- public open = false;
-
/** @internal */
public button?: HTMLButtonElement;
- /** @internal */
- @observable
- public region?: AnchoredRegion;
-
- /** @internal */
- @observable
- public slottedMenus?: HTMLElement[];
-
/** @internal */
public get currentState(): BreakpointState {
const value = this.cellRecord?.value;
@@ -85,61 +69,6 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
return [];
}
- public regionChanged(
- prev: AnchoredRegion | undefined,
- _next: AnchoredRegion | undefined
- ): void {
- if (prev) {
- prev.removeEventListener(eventChange, this.menuChangeHandler);
- }
-
- if (this.region && this.button) {
- this.region.anchorElement = this.button;
- this.region.addEventListener(eventChange, this.menuChangeHandler, {
- capture: true
- });
- }
- }
-
- public override disconnectedCallback(): void {
- super.disconnectedCallback();
- if (this.region) {
- this.region.removeEventListener(eventChange, this.menuChangeHandler);
- }
- }
-
- /** @internal */
- public onContextMenuKeyDown(e: KeyboardEvent): boolean {
- switch (e.key) {
- case keyEscape:
- this.setContextMenuOpen(false);
- this.button?.focus();
- return false;
- default:
- return true;
- }
- }
-
- /** @internal */
- public regionLoadedHandler(): void {
- this.focusMenu();
- }
-
- /** @internal */
- public onContextMenuFocusOut(e: FocusEvent): boolean {
- if (!this.open) {
- return true;
- }
-
- const focusTarget = e.relatedTarget as HTMLElement;
- if (!this.contains(focusTarget) && !this.getMenu()?.contains(focusTarget)) {
- this.setContextMenuOpen(false);
- return false;
- }
-
- return true;
- }
-
/** @internal */
public onButtonClick(event: Event): void {
event.stopPropagation();
@@ -154,7 +83,8 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
public onContextMenu(event: Event): void {
event.preventDefault();
event.stopPropagation();
- this.emitContextMenu();
+ const mouseEvent = event as MouseEvent;
+ this.emitContextMenu(mouseEvent.clientX, mouseEvent.clientY);
}
/** @internal */
@@ -172,7 +102,7 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
|| event.key === TsTableColumnBreakpointCellView.menuKeyAlias) {
event.preventDefault();
event.stopPropagation();
- this.emitContextMenu();
+ this.emitContextMenuFromButton();
return;
}
@@ -195,67 +125,23 @@ export class TsTableColumnBreakpointCellView extends TableCellView<
this.$emit('breakpoint-column-toggle', detail);
}
- private emitContextMenu(): void {
- const slotRequestDetail: CellViewSlotRequestEventDetail = {
- slots: [{ name: 'menu', slot: 'menu' }]
- };
- this.$emit('cell-view-slots-request', slotRequestDetail);
-
+ private emitContextMenu(anchorX: number, anchorY: number): void {
const detail: BreakpointContextMenuEventDetail = {
recordId: this.recordId ?? '',
- currentState: this.currentState
+ currentState: this.currentState,
+ anchorX,
+ anchorY
};
-
- this.setContextMenuOpen(true);
this.$emit('breakpoint-column-context-menu', detail);
}
- private setContextMenuOpen(newValue: boolean): void {
- if (this.open === newValue) {
- return;
+ private emitContextMenuFromButton(): void {
+ const rect = this.button?.getBoundingClientRect();
+ if (rect) {
+ this.emitContextMenu(rect.left, rect.bottom);
+ } else {
+ this.emitContextMenu(0, 0);
}
-
- this.open = newValue;
- }
-
- private getMenu(): HTMLElement | undefined {
- // Resolve nested slot forwarding (table -> row -> cell-view) to find the actual menu.
- if (!this.slottedMenus || this.slottedMenus.length === 0) {
- return undefined;
- }
-
- let currentItem: HTMLElement | undefined = this.slottedMenus[0];
- while (currentItem) {
- if (currentItem.getAttribute('role') === 'menu') {
- return currentItem;
- }
-
- if (this.isSlotElement(currentItem)) {
- const firstNode = currentItem.assignedNodes()[0];
- if (firstNode instanceof HTMLElement) {
- currentItem = firstNode;
- } else {
- currentItem = undefined;
- }
- } else {
- return undefined;
- }
- }
-
- return undefined;
- }
-
- private isSlotElement(element: HTMLElement | undefined): element is HTMLSlotElement {
- return element?.nodeName === 'SLOT';
- }
-
- private readonly menuChangeHandler = (): void => {
- this.setContextMenuOpen(false);
- this.button?.focus();
- };
-
- private focusMenu(): void {
- this.getMenu()?.focus();
}
}
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
index 9920c7a267..51554ca2ee 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
@@ -10,7 +10,6 @@ export const styles = css`
justify-content: center;
width: 100%;
height: 100%;
- position: relative;
}
.breakpoint-button {
@@ -44,4 +43,5 @@ export const styles = css`
.breakpoint-button.state-off:focus-visible svg {
opacity: 1;
}
+
`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
index c9deb28ed1..876bc17d33 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -1,5 +1,4 @@
-import { html, ref, slotted, when } from '@ni/fast-element';
-import { anchoredRegionTag } from '@ni/nimble-components/dist/esm/anchored-region';
+import { html, ref, when } from '@ni/fast-element';
import type { TsTableColumnBreakpointCellView } from '.';
import { BreakpointState } from '../types';
@@ -30,19 +29,4 @@ export const template = html`
${when(x => x.currentState === BreakpointState.conditional, conditionalSvg)}
${when(x => x.currentState === BreakpointState.hitDisabled, hitDisabledSvg)}
- ${when(x => x.open, html`
- <${anchoredRegionTag}
- ${ref('region')}
- part="menu"
- fixed-placement="true"
- auto-update-mode="auto"
- horizontal-inset="true"
- horizontal-positioning-mode="dynamic"
- vertical-positioning-mode="dynamic"
- @loaded="${x => x.regionLoadedHandler()}"
- @focusout="${(x, c) => x.onContextMenuFocusOut(c.event as FocusEvent)}"
- @keydown="${(x, c) => x.onContextMenuKeyDown(c.event as KeyboardEvent)}"
- >
-
- ${anchoredRegionTag}>
- `)}`;
+`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
index 4f0b990e92..69b3dd14f4 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -65,7 +65,6 @@ export class TsTableColumnBreakpoint extends TableColumn([])
};
}
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
index d6527bbe2b..d85166215b 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/tests/ts-table-column-breakpoint.spec.ts
@@ -15,6 +15,7 @@ import {
interface SimpleTableRecord extends TableRecord {
id?: string;
+ parentId?: string;
breakpointState?: string | null;
}
@@ -266,6 +267,37 @@ describe('TsTableColumnBreakpoint', () => {
expect(eventDetail.oldState).toBe(BreakpointState.hit);
expect(eventDetail.newState).toBe(BreakpointState.off);
});
+
+ it('emits toggle event for child rows in hierarchical data', async () => {
+ table.parentIdFieldName = 'parentId';
+ await table.setData([
+ { id: 'parent', breakpointState: BreakpointState.off },
+ { id: 'child', parentId: 'parent', breakpointState: BreakpointState.off }
+ ]);
+ await waitForUpdatesAsync();
+
+ const toggleSpy = jasmine.createSpy('toggle');
+ elementReferences.column.addEventListener(
+ 'breakpoint-column-toggle',
+ toggleSpy
+ );
+
+ const cellView = tablePageObject.getRenderedCellView(
+ 1,
+ 0
+ ) as TsTableColumnBreakpointCellView;
+ const button = getBreakpointButton(cellView);
+ button.click();
+ await waitForUpdatesAsync();
+
+ expect(toggleSpy).toHaveBeenCalledTimes(1);
+ const eventDetail = (
+ toggleSpy.calls.first().args[0] as CustomEvent
+ ).detail;
+ expect(eventDetail.recordId).toBe('child');
+ expect(eventDetail.oldState).toBe(BreakpointState.off);
+ expect(eventDetail.newState).toBe(BreakpointState.enabled);
+ });
});
describe('table selection does not change', () => {
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/types.ts b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
index 61afeb9612..f8f86e399c 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/types.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
@@ -26,4 +26,6 @@ export interface BreakpointToggleEventDetail {
export interface BreakpointContextMenuEventDetail {
recordId: string;
currentState: BreakpointState;
+ anchorX: number;
+ anchorY: number;
}
From 5e64d97ffb370d38f5a1dc7a9cb999e5bfbf107a Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Thu, 28 May 2026 14:00:56 -0500
Subject: [PATCH 11/23] use new spright icons
---
.../CodeAnalysisDictionary.xml | 1 -
.../breakpoint/cell-view/styles.ts | 17 ++++++------
.../breakpoint/cell-view/template.ts | 26 +++++++++----------
3 files changed, 21 insertions(+), 23 deletions(-)
diff --git a/packages/blazor-workspace/CodeAnalysisDictionary.xml b/packages/blazor-workspace/CodeAnalysisDictionary.xml
index 3b37eb365a..b2c2aafaf6 100644
--- a/packages/blazor-workspace/CodeAnalysisDictionary.xml
+++ b/packages/blazor-workspace/CodeAnalysisDictionary.xml
@@ -4,7 +4,6 @@
args
blazor
- breakpoint
bool
breakpoint
clearable
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
index 51554ca2ee..a2eadad7d5 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/styles.ts
@@ -1,23 +1,24 @@
import { css } from '@ni/fast-element';
+import { display } from '@ni/nimble-components/dist/esm/utilities/style/display';
import {
iconSize,
} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens';
export const styles = css`
+ ${display('inline-flex')}
+
:host {
- display: flex;
align-items: center;
justify-content: center;
- width: 100%;
- height: 100%;
}
.breakpoint-button {
display: flex;
align-items: center;
justify-content: center;
- width: 100%;
- height: 100%;
+ width: ${iconSize};
+ height: ${iconSize};
+ flex-shrink: 0;
padding: 0;
margin: 0;
border: none;
@@ -35,12 +36,12 @@ export const styles = css`
height: ${iconSize};
}
- .breakpoint-button.state-off svg {
+ .breakpoint-button.state-off > * {
opacity: 0;
}
- .breakpoint-button.state-off:hover svg,
- .breakpoint-button.state-off:focus-visible svg {
+ .breakpoint-button.state-off:hover > *,
+ .breakpoint-button.state-off:focus-visible > * {
opacity: 1;
}
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
index 876bc17d33..1b1b35385c 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -1,14 +1,12 @@
import { html, ref, when } from '@ni/fast-element';
import type { TsTableColumnBreakpointCellView } from '.';
import { BreakpointState } from '../types';
-
-// Placeholder SVGs for breakpoint states - to be replaced with proper icons later
-const offSvg = html``;
-const enabledSvg = html``;
-const disabledSvg = html``;
-const hitSvg = html``;
-const conditionalSvg = html``;
-const hitDisabledSvg = html``;
+import { iconBreakpointConditionalTag } from "@ni/spright-components/dist/esm/icons/breakpoint-conditional";
+import { iconBreakpointDisabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-disabled";
+import { iconBreakpointHitTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hit";
+import { iconBreakpointHitDisabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hit-disabled";
+import { iconBreakpointEnabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-enabled";
+import { iconBreakpointHoverTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hover";
export const template = html`
`;
From 9d84a53cd9abc4f6af63ec0fdedd9ca969799cda Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Thu, 28 May 2026 14:16:03 -0500
Subject: [PATCH 12/23] fix imports
---
.../table-column/breakpoint/cell-view/template.ts | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
index 1b1b35385c..2fc89c8487 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/cell-view/template.ts
@@ -1,12 +1,12 @@
import { html, ref, when } from '@ni/fast-element';
-import type { TsTableColumnBreakpointCellView } from '.';
+import { iconBreakpointConditionalTag } from '@ni/spright-components/dist/esm/icons/breakpoint-conditional';
+import { iconBreakpointDisabledTag } from '@ni/spright-components/dist/esm/icons/breakpoint-disabled';
+import { iconBreakpointHitTag } from '@ni/spright-components/dist/esm/icons/breakpoint-hit';
+import { iconBreakpointHitDisabledTag } from '@ni/spright-components/dist/esm/icons/breakpoint-hit-disabled';
+import { iconBreakpointEnabledTag } from '@ni/spright-components/dist/esm/icons/breakpoint-enabled';
+import { iconBreakpointHoverTag } from '@ni/spright-components/dist/esm/icons/breakpoint-hover';
import { BreakpointState } from '../types';
-import { iconBreakpointConditionalTag } from "@ni/spright-components/dist/esm/icons/breakpoint-conditional";
-import { iconBreakpointDisabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-disabled";
-import { iconBreakpointHitTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hit";
-import { iconBreakpointHitDisabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hit-disabled";
-import { iconBreakpointEnabledTag } from "@ni/spright-components/dist/esm/icons/breakpoint-enabled";
-import { iconBreakpointHoverTag } from "@ni/spright-components/dist/esm/icons/breakpoint-hover";
+import type { TsTableColumnBreakpointCellView } from '.';
export const template = html`
`;
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/index.ts b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
index 494c0acf53..db9565225f 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/index.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/index.ts
@@ -7,14 +7,18 @@ import { ColumnValidator } from '@ni/nimble-components/dist/esm/table-column/bas
import { TableColumn } from '@ni/nimble-components/dist/esm/table-column/base';
import { styles } from '@ni/nimble-components/dist/esm/table-column/base/styles';
import type { DelegatedEventEventDetails } from '@ni/nimble-components/dist/esm/table-column/base/types';
+import { MenuButtonPosition, type MenuButtonPosition as BreakpointMenuPosition } from '@ni/nimble-components/dist/esm/menu-button/types';
import type { BreakpointToggleEventDetail, BreakpointContextMenuEventDetail } from './types';
+import { breakpointCellViewMenuSlotName } from './types';
import { tsTableColumnBreakpointCellViewTag } from './cell-view';
import { template } from './template';
export type TsTableColumnBreakpointCellRecord = TableStringField<'value'>;
-// eslint-disable-next-line @typescript-eslint/no-empty-object-type
-export interface TsTableColumnBreakpointColumnConfig {}
+export interface TsTableColumnBreakpointColumnConfig {
+ menuSlot?: string;
+ position?: BreakpointMenuPosition;
+}
declare global {
interface HTMLElementTagNameMap {
@@ -29,6 +33,12 @@ export class TsTableColumnBreakpoint extends TableColumn([])
};
}
@@ -73,6 +84,21 @@ export class TsTableColumnBreakpoint extends TableColumn {
@@ -38,8 +43,18 @@ describe('TsTableColumnBreakpoint', () => {
): Promise>> {
return await fixture>(
html`<${tableTag} ${ref('table')} id-field-name="id" style="width: 700px">
- <${tsTableColumnBreakpointTag} ${ref('column')} field-name="breakpointState">
+ <${tsTableColumnBreakpointTag}
+ ${ref('column')}
+ field-name="breakpointState"
+ menu-slot="breakpoint-menu"
+ >
${tsTableColumnBreakpointTag}>
+
+ <${menuTag} slot="breakpoint-menu">
+ <${menuItemTag} ${ref('firstMenuItem')}>Toggle${menuItemTag}>
+ <${menuItemTag} ${ref('secondMenuItem')}>Disable${menuItemTag}>
+ <${menuItemTag} ${ref('lastMenuItem')}>Edit${menuItemTag}>
+ ${menuTag}>
${tableTag}>`,
{ source }
);
diff --git a/packages/ok-components/src/ts/table-column/breakpoint/types.ts b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
index f8f86e399c..e3ffea578f 100644
--- a/packages/ok-components/src/ts/table-column/breakpoint/types.ts
+++ b/packages/ok-components/src/ts/table-column/breakpoint/types.ts
@@ -26,6 +26,7 @@ export interface BreakpointToggleEventDetail {
export interface BreakpointContextMenuEventDetail {
recordId: string;
currentState: BreakpointState;
- anchorX: number;
- anchorY: number;
}
+
+/** @internal */
+export const breakpointCellViewMenuSlotName = 'menu';
diff --git a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
index 4487e8a0e4..576ee713ae 100644
--- a/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
+++ b/packages/storybook/src/ok/ts/table-column-breakpoint/ts-table-column-breakpoint.stories.ts
@@ -4,6 +4,8 @@ import { withActions } from 'storybook/actions/decorator';
import { tableTag } from '@ni/nimble-components/dist/esm/table';
import type { TableRecord } from '@ni/nimble-components/dist/esm/table/types';
import { tableColumnTextTag } from '@ni/nimble-components/dist/esm/table-column/text';
+import { menuTag } from '@ni/nimble-components/dist/esm/menu';
+import { menuItemTag } from '@ni/nimble-components/dist/esm/menu-item';
import { tsTableColumnBreakpointTag } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint';
import { BreakpointState, type BreakpointToggleEventDetail } from '@ni/ok-components/dist/esm/ts/table-column/breakpoint/types';
import {
@@ -106,6 +108,7 @@ export default metadata;
interface BreakpointColumnTableArgs extends SharedTableArgs {
fieldName: string;
+ menuSlot: string;
toggleEvent: never;
contextMenuEvent: never;
currentData: CodeRecord[];
@@ -127,6 +130,7 @@ export const breakpointColumn: StoryObj = {
>
<${tsTableColumnBreakpointTag}
field-name="${x => x.fieldName}"
+ menu-slot="${x => x.menuSlot}"
@breakpoint-column-toggle="${(x, c) => {
const event = c.event as CustomEvent;
const detail = event.detail;
@@ -143,6 +147,11 @@ export const breakpointColumn: StoryObj = {
>
Code
${tableColumnTextTag}>
+ <${menuTag} slot="${x => x.menuSlot}">
+ <${menuItemTag}>Enable breakpoint${menuItemTag}>
+ <${menuItemTag}>Disable breakpoint${menuItemTag}>
+ <${menuItemTag}>Remove breakpoint${menuItemTag}>
+ ${menuTag}>
${tableTag}>
`),
argTypes: {
@@ -153,6 +162,13 @@ export const breakpointColumn: StoryObj = {
control: false,
table: { category: apiCategory.attributes }
},
+ menuSlot: {
+ name: 'menu-slot',
+ description:
+ 'The name of the slot within the table where context menu content is provided. When configured, context menu requests render this slotted content inside an anchored region in the active breakpoint cell.',
+ control: false,
+ table: { category: apiCategory.attributes }
+ },
toggleEvent: {
name: 'breakpoint-column-toggle',
description:
@@ -175,6 +191,7 @@ export const breakpointColumn: StoryObj = {
},
args: {
fieldName: 'breakpointState',
+ menuSlot: 'breakpoint-menu',
currentData: [...simpleData]
}
};
From 431a7fa0daa3589c935390d386d3c695179effff Mon Sep 17 00:00:00 2001
From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com>
Date: Tue, 2 Jun 2026 18:09:57 -0500
Subject: [PATCH 23/23] fix blazor example
---
.../Pages/Sections/Ts/TsBreakpointTableSection.razor.cs | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
index c03ac4594f..d2ee3351d1 100644
--- a/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
+++ b/packages/blazor-workspace/Examples/Demo.Shared/Pages/Sections/Ts/TsBreakpointTableSection.razor.cs
@@ -22,11 +22,7 @@ public partial class TsBreakpointTableSection
protected override async Task OnAfterRenderAsync(bool firstRender)
{
- if (firstRender)
- {
- await _table!.SetDataAsync(_tableData);
- }
-
+ await _table!.SetDataAsync(_tableData);
await base.OnAfterRenderAsync(firstRender);
}