Skip to content
Merged
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
7 changes: 7 additions & 0 deletions change/@fluentui-priority-overflow-pr1-strict-mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: createOverflowManager accepts initialOptions, new setOptions method, observe now returns its cleanup, and the OverflowManagerOptions type is exported.",
"packageName": "@fluentui/priority-overflow",
"email": "bsunderhus@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: export BadgeSlots, ComboboxSlots, useMenuListContextValues to complete consumer-side composition surface",
"packageName": "@fluentui/react-headless-components-preview",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 7 additions & 0 deletions change/@fluentui-react-overflow-pr1-strict-mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: make Overflow container strict-mode safe",
"packageName": "@fluentui/react-overflow",
"email": "bsunderhus@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

```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,67 +18,61 @@ 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;
addOverflowMenu: (element: HTMLElement) => void;
disconnect: () => void;
forceUpdate: () => void;
observe: (container: HTMLElement, options: ObserveOptions) => void;
observe: (container: HTMLElement, options?: ObserveOptions) => void;
removeDivider: (groupId: string) => void;
removeItem: (itemId: string) => void;
removeOverflowMenu: () => void;
setOptions: (options: Partial<ObserveOptions>) => void;
update: () => void;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { createOverflowManager } from './overflowManager';
import type { ObserveOptions, OverflowEventPayload } 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 lastDispatch = (onUpdateOverflow: jest.Mock): OverflowEventPayload =>
onUpdateOverflow.mock.calls[onUpdateOverflow.mock.calls.length - 1][0];

it('should dispatch overflow update after forceUpdate', () => {
const onUpdateOverflow = jest.fn();
const options = createObserveOptions({ onUpdateOverflow });
const manager = createOverflowManager(options);
const container = createContainer(100);
const itemA = createElementWithSize('button', 40);
const itemB = createElementWithSize('button', 40);
const menu = createElementWithSize('button', 20);

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 dispatch = lastDispatch(onUpdateOverflow);
expect(dispatch.visibleItems.map(item => item.id).sort()).toEqual(['a', 'b']);
expect(dispatch.invisibleItems).toEqual([]);
expect(dispatch.groupVisibility).toEqual({});
});

it('should re-dispatch when setOptions changes a relevant option', () => {
const onUpdateOverflow = jest.fn();
const options = createObserveOptions({ onUpdateOverflow });
const manager = createOverflowManager(options);
const container = createContainer(100);
const itemA = createElementWithSize('button', 40);
const itemB = createElementWithSize('button', 40);
const menu = createElementWithSize('button', 20);

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

onUpdateOverflow.mockClear();
manager.setOptions({ padding: 30 });

expect(onUpdateOverflow).toHaveBeenCalled();
const dispatch = lastDispatch(onUpdateOverflow);
expect(dispatch.visibleItems.map(item => item.id)).toEqual(['a']);
expect(dispatch.invisibleItems.map(item => item.id)).toEqual(['b']);
});

it('should not re-dispatch when setOptions is called with a partial that does not change anything', () => {
const onUpdateOverflow = jest.fn();
const options = createObserveOptions({ onUpdateOverflow });
const manager = createOverflowManager(options);
const container = createContainer(100);
const itemA = createElementWithSize('button', 40);

manager.addItem({ element: itemA, id: 'a', priority: 1 });
manager.observe(container, options);
manager.forceUpdate();

onUpdateOverflow.mockClear();
manager.setOptions({ padding: 10 }); // padding is already 10; no real change

expect(onUpdateOverflow).not.toHaveBeenCalled();
});

it('disconnect stops observation and re-observe restarts dispatching', () => {
const onUpdateOverflow = jest.fn();
const options = createObserveOptions({ onUpdateOverflow });
const manager = createOverflowManager(options);
const container = createContainer(100);
const item = createElementWithSize('button', 40);

manager.addItem({ element: item, id: 'a', priority: 1 });
manager.observe(container);
manager.forceUpdate();
expect(lastDispatch(onUpdateOverflow).visibleItems.map(i => i.id)).toEqual(['a']);

manager.disconnect();
onUpdateOverflow.mockClear();

manager.addItem({ element: item, id: 'a', priority: 1 });
manager.observe(container);
manager.forceUpdate();
expect(onUpdateOverflow).toHaveBeenCalled();
expect(lastDispatch(onUpdateOverflow).visibleItems.map(i => i.id)).toEqual(['a']);
});

it('should remove items through removeItem', () => {
const onUpdateOverflow = jest.fn();
const options = createObserveOptions({ onUpdateOverflow });
const manager = createOverflowManager(options);
const container = createContainer(100);
const item = createElementWithSize('button', 40);

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

expect(lastDispatch(onUpdateOverflow).visibleItems.map(i => i.id)).toEqual(['a']);

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

const dispatch = lastDispatch(onUpdateOverflow);
expect(dispatch.visibleItems).toEqual([]);
expect(dispatch.invisibleItems).toEqual([]);
});
});
Loading
Loading