Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

Copy link
Copy Markdown

@github-actions github-actions Bot May 27, 2026

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Charts-DonutChart 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 5581 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 5570 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - Dark Mode.default.chromium.png 7530 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 404 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 510 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 959 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 31 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 93 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 93 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

```ts

// @internal (undocumented)
export function createOverflowManager(): OverflowManager;
// @internal
export function createOverflowManager(initialOptions?: Partial<ObserveOptions>): OverflowManager;

// @public (undocumented)
// @public
export interface ObserveOptions {
hasHiddenItems?: boolean;
minimumVisible?: number;
Expand All @@ -18,70 +18,67 @@ export interface ObserveOptions {
padding?: number;
}

// @public (undocumented)
// @public
export type OnUpdateItemVisibility = (data: OnUpdateItemVisibilityPayload) => void;

// @public (undocumented)
// @public
export interface OnUpdateItemVisibilityPayload {
// (undocumented)
item: OverflowItemEntry;
// (undocumented)
visible: boolean;
}

// @public
export type OnUpdateOverflow = (data: OverflowEventPayload) => void;

// @public (undocumented)
// @public
export type OverflowAxis = 'horizontal' | 'vertical';

// @public (undocumented)
// @public
export type OverflowDirection = 'start' | 'end';

// @public (undocumented)
// @public
export interface OverflowDividerEntry {
element: HTMLElement;
// (undocumented)
groupId: string;
}

// @public
export interface OverflowEventPayload {
// (undocumented)
groupVisibility: Record<string, OverflowGroupState>;
// (undocumented)
invisibleItems: OverflowItemEntry[];
// (undocumented)
visibleItems: OverflowItemEntry[];
}

// @public (undocumented)
// @public
export type OverflowGroupState = 'visible' | 'hidden' | 'overflow';

// @public (undocumented)
// @public
export interface OverflowItemEntry {
element: HTMLElement;
// (undocumented)
groupId?: string;
id: string;
pinned?: boolean;
priority: number;
}

// @internal (undocumented)
// @internal
export interface OverflowManager {
addDivider: (divider: OverflowDividerEntry) => void;
addItem: (items: OverflowItemEntry) => void;
addItem: (item: OverflowItemEntry) => void;
addOverflowMenu: (element: HTMLElement) => void;
disconnect: () => void;
forceUpdate: () => void;
observe: (container: HTMLElement, options: ObserveOptions) => void;
getSnapshot: () => OverflowEventPayload;
observe: (container: HTMLElement) => () => void;
removeDivider: (groupId: string) => void;
removeItem: (itemId: string) => void;
removeOverflowMenu: () => void;
setOptions: (options: Partial<OverflowManagerOptions>) => void;
subscribe: (listener: () => void) => () => void;
update: () => void;
}

// (No @packageDocumentation comment for this package)
// @public
export type OverflowManagerOptions = ObserveOptions;

```
6 changes: 6 additions & 0 deletions packages/react-components/priority-overflow/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/**
* Utilities for measuring container overflow and managing priority-based item visibility.
*
* @packageDocumentation
*/
export { createOverflowManager } from './overflowManager';
export type {
ObserveOptions,
OverflowManagerOptions,
OnUpdateItemVisibility,
OnUpdateItemVisibilityPayload,
OnUpdateOverflow,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { createOverflowManager } from './overflowManager';
import type { ObserveOptions } from './types';

describe('overflowManager', () => {
beforeAll(() => {
global.ResizeObserver = class ResizeObserver {
public observe() {
// do nothing
}

public unobserve() {
// do nothing
}

public disconnect() {
// do nothing
}
} as unknown as typeof ResizeObserver;
});

const createElementWithSize = (tagName: string, width: number) => {
const element = document.createElement(tagName);
Object.defineProperty(element, 'offsetWidth', { configurable: true, value: width });
Object.defineProperty(element, 'offsetHeight', { configurable: true, value: width });

return element;
};

const createContainer = (width: number) => {
const container = document.createElement('div');
Object.defineProperty(container, 'clientWidth', { configurable: true, value: width });
Object.defineProperty(container, 'clientHeight', { configurable: true, value: width });

return container;
};

const createObserveOptions = (options: Partial<ObserveOptions> = {}): ObserveOptions => ({
overflowAxis: 'horizontal',
overflowDirection: 'end',
padding: 10,
minimumVisible: 0,
hasHiddenItems: false,
onUpdateItemVisibility: jest.fn(),
onUpdateOverflow: jest.fn(),
...options,
});

const getVisibleIds = (manager: ReturnType<typeof createOverflowManager>) =>
manager
.getSnapshot()
.visibleItems.map(item => item.id)
.sort();

const getInvisibleIds = (manager: ReturnType<typeof createOverflowManager>) =>
manager
.getSnapshot()
.invisibleItems.map(item => item.id)
.sort();

it('should expose a stable snapshot after forceUpdate', () => {
const manager = createOverflowManager();
const container = createContainer(100);
const itemA = createElementWithSize('button', 40);
const itemB = createElementWithSize('button', 40);
const menu = createElementWithSize('button', 20);

manager.setOptions(createObserveOptions());
manager.addItem({ element: itemA, id: 'a', priority: 1 });
manager.addItem({ element: itemB, id: 'b', priority: 0 });
manager.addOverflowMenu(menu);
manager.observe(container);
manager.forceUpdate();

expect(getVisibleIds(manager)).toEqual(['a', 'b']);
expect(getInvisibleIds(manager)).toEqual([]);
expect(manager.getSnapshot().groupVisibility).toEqual({});
});

it('should update snapshot and notify subscribers when options change', () => {
const manager = createOverflowManager();
const container = createContainer(100);
const itemA = createElementWithSize('button', 40);
const itemB = createElementWithSize('button', 40);
const menu = createElementWithSize('button', 20);
const listener = jest.fn();

manager.setOptions(createObserveOptions());
manager.addItem({ element: itemA, id: 'a', priority: 1 });
manager.addItem({ element: itemB, id: 'b', priority: 0 });
manager.addOverflowMenu(menu);
manager.observe(container);
manager.forceUpdate();
const unsubscribe = manager.subscribe(listener);

manager.setOptions({ padding: 30 });

expect(listener).toHaveBeenCalled();
expect(getVisibleIds(manager)).toEqual(['a']);
expect(getInvisibleIds(manager)).toEqual(['b']);
expect(manager.getSnapshot().groupVisibility).toEqual({});

unsubscribe();
});

it('should reset snapshot state when observation cleanup runs', () => {
const manager = createOverflowManager();
const container = createContainer(100);
const item = createElementWithSize('button', 40);

manager.setOptions(createObserveOptions());
manager.addItem({ element: item, id: 'a', priority: 1 });
const cleanup = manager.observe(container);
manager.forceUpdate();

expect(getVisibleIds(manager)).toEqual(['a']);

cleanup();

expect(manager.getSnapshot()).toEqual({
visibleItems: [],
invisibleItems: [],
groupVisibility: {},
});
});

it('should remove items through removeItem', () => {
const manager = createOverflowManager();
const container = createContainer(100);
const item = createElementWithSize('button', 40);

manager.setOptions(createObserveOptions());
manager.addItem({ element: item, id: 'a', priority: 1 });
manager.observe(container);
manager.forceUpdate();

expect(getVisibleIds(manager)).toEqual(['a']);

manager.removeItem('a');
manager.forceUpdate();

expect(manager.getSnapshot()).toEqual({
visibleItems: [],
invisibleItems: [],
groupVisibility: {},
});
});
});
Loading
Loading