Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Overflow): Support margin and gaps to space overflow items #33929

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -9,6 +9,8 @@ export function createOverflowManager(): OverflowManager;

Copy link
Collaborator

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual regressions to review in the fluentuiv9 Visual Regression Report

Drawer 1 screenshots
Image Name Diff(in Pixels) Image Type
Drawer.overlay drawer full - High Contrast.chromium.png 2231 Changed

// @public (undocumented)
export interface ObserveOptions {
boxModel?: 'border' | 'inline-margin';
measureGap?: boolean;
minimumVisible?: number;
onUpdateItemVisibility: OnUpdateItemVisibility;
onUpdateOverflow: OnUpdateOverflow;
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import type {
ObserveOptions,
OverflowDividerEntry,
} from './types';
import { parseCSSLength } from './utils';

/**
* @internal
@@ -18,6 +19,7 @@ export function createOverflowManager(): OverflowManager {
// calls to `offsetWidth or offsetHeight` can happen multiple times in an update
// Use a cache to avoid causing too many recalcs and avoid scripting time to meausure sizes
const sizeCache = new Map<HTMLElement, number>();
const liveStylesCache = new Map<HTMLElement, CSSStyleDeclaration>();
let container: HTMLElement | undefined;
let overflowMenu: HTMLElement | undefined;
// Set as true when resize observer is observing
@@ -32,6 +34,8 @@ export function createOverflowManager(): OverflowManager {
minimumVisible: 0,
onUpdateItemVisibility: () => undefined,
onUpdateOverflow: () => undefined,
boxModel: 'border',
measureGap: false,
};

const overflowItems: Record<string, OverflowItemEntry> = {};
@@ -76,10 +80,19 @@ export function createOverflowManager(): OverflowManager {
vertical: 'clientHeight' | 'offsetHeight',
el: HTMLElement,
): number {
if (!sizeCache.has(el)) {
sizeCache.set(el, options.overflowAxis === 'horizontal' ? el[horizontal] : el[vertical]);
if (sizeCache.has(el)) {
return sizeCache.get(el)!;
}

const requested = options.overflowAxis === 'horizontal' ? horizontal : vertical;
let measurement = el[requested];

if (options.boxModel === 'inline-margin' && requested === 'offsetWidth' && liveStylesCache.has(el)) {
const liveStyles = liveStylesCache.get(el)!;
measurement += parseCSSLength(liveStyles.marginInlineStart) + parseCSSLength(liveStyles.marginInlineEnd);
}

sizeCache.set(el, measurement);
return sizeCache.get(el)!;
}

@@ -144,14 +157,31 @@ export function createOverflowManager(): OverflowManager {
options.onUpdateOverflow({ visibleItems, invisibleItems, groupVisibility: groupManager.groupVisibility() });
};

const getAvailableSize = () => {
if (!container) {
return 0;
}

let totalGapSize = 0;

if (options.measureGap && liveStylesCache.has(container)) {
const cssColumnGap = liveStylesCache.get(container)!.columnGap;
const columnGap = parseCSSLength(cssColumnGap);
const overflowMenuMod = overflowMenu ? 1 : 0;
if (columnGap) {
totalGapSize = columnGap * (visibleItemQueue.size() - 1 + overflowMenuMod);
}
}

return getClientSize(container) - options.padding - totalGapSize;
};

const processOverflowItems = (): boolean => {
if (!container) {
return false;
}
sizeCache.clear();

const availableSize = getClientSize(container) - options.padding;

// Snapshot of the visible/invisible state to compare for updates
const visibleTop = visibleItemQueue.peek();
const invisibleTop = invisibleItemQueue.peek();
@@ -165,14 +195,14 @@ export function createOverflowManager(): OverflowManager {
for (let i = 0; i < 2; i++) {
// Add items until available width is filled - can result in overflow
while (
(occupiedSize() < availableSize && invisibleItemQueue.size() > 0) ||
(occupiedSize() < getAvailableSize() && invisibleItemQueue.size() > 0) ||
invisibleItemQueue.size() === 1 // attempt to show the last invisible item hoping it's size does not exceed overflow menu size
) {
showItem();
}

// Remove items until there's no more overflow
while (occupiedSize() > availableSize && visibleItemQueue.size() > options.minimumVisible) {
while (occupiedSize() > getAvailableSize() && visibleItemQueue.size() > options.minimumVisible) {
hideItem();
}
}
@@ -196,6 +226,11 @@ export function createOverflowManager(): OverflowManager {
Object.values(overflowItems).forEach(item => visibleItemQueue.enqueue(item.id));

container = observedContainer;
if (container.ownerDocument.defaultView) {
const liveStyles = container.ownerDocument.defaultView.getComputedStyle(container);
liveStylesCache.set(container, liveStyles);
}

disposeResizeObserver = observeResize(container, entries => {
if (!entries[0] || !container) {
return;
@@ -211,6 +246,10 @@ export function createOverflowManager(): OverflowManager {
}

overflowItems[item.id] = item;
if (item.element.ownerDocument.defaultView) {
const liveStyles = item.element.ownerDocument.defaultView.getComputedStyle(item.element);
liveStylesCache.set(item.element, liveStyles);
}

// some options can affect priority which are only set on `observe`
if (observing) {
@@ -231,6 +270,10 @@ export function createOverflowManager(): OverflowManager {

const addOverflowMenu: OverflowManager['addOverflowMenu'] = el => {
overflowMenu = el;
if (overflowMenu.ownerDocument.defaultView) {
const liveStyles = overflowMenu.ownerDocument.defaultView.getComputedStyle(overflowMenu);
liveStylesCache.set(overflowMenu, liveStyles);
}
};

const addDivider: OverflowManager['addDivider'] = divider => {
@@ -240,9 +283,16 @@ export function createOverflowManager(): OverflowManager {

divider.element.setAttribute(DATA_OVERFLOW_GROUP, divider.groupId);
overflowDividers[divider.groupId] = divider;
if (divider.element.ownerDocument.defaultView) {
const liveStyles = divider.element.ownerDocument.defaultView.getComputedStyle(divider.element);
liveStylesCache.set(divider.element, liveStyles);
}
};

const removeOverflowMenu: OverflowManager['removeOverflowMenu'] = () => {
if (overflowMenu) {
liveStylesCache.delete(overflowMenu);
}
overflowMenu = undefined;
};

@@ -251,6 +301,8 @@ export function createOverflowManager(): OverflowManager {
return;
}
const divider = overflowDividers[groupId];
sizeCache.delete(divider.element);
liveStylesCache.delete(divider.element);
if (divider.groupId) {
delete overflowDividers[groupId];
divider.element.removeAttribute(DATA_OVERFLOW_GROUP);
@@ -278,6 +330,7 @@ export function createOverflowManager(): OverflowManager {
}

sizeCache.delete(item.element);
liveStylesCache.delete(item.element);
delete overflowItems[itemId];
update();
};
@@ -295,6 +348,7 @@ export function createOverflowManager(): OverflowManager {
Object.keys(overflowDividers).forEach(dividerId => removeDivider(dividerId));
removeOverflowMenu();
sizeCache.clear();
liveStylesCache.clear();
};

return {
15 changes: 15 additions & 0 deletions packages/react-components/priority-overflow/src/types.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,21 @@ export interface ObserveOptions {
* Callback when item visibility is updated
*/
onUpdateOverflow: OnUpdateOverflow;

/**
* This library provides some support for different box models
* - border - the default box model, width and height are measured with offsetWidth and offsetHeight
* - inline-margin - inline margins are added to the border box
*
* @default border
*/
boxModel?: 'border' | 'inline-margin';

/**
* The library will read the CSS gap on the the container element and take that into account
* when calculating available space
*/
measureGap?: boolean;
}

/**
8 changes: 8 additions & 0 deletions packages/react-components/priority-overflow/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const parseCSSLength = (value: string): number => {
const parseResult = parseFloat(value);
if (isNaN(parseResult)) {
return 0;
}

return parseResult;
};
Original file line number Diff line number Diff line change
@@ -653,6 +653,7 @@ import { OptionSlots } from '@fluentui/react-combobox';
import { OptionState } from '@fluentui/react-combobox';
import { Overflow } from '@fluentui/react-overflow';
import { OverflowDivider } from '@fluentui/react-overflow';
import { OverflowImperativeRef } from '@fluentui/react-overflow';
import { OverflowItem } from '@fluentui/react-overflow';
import { OverflowItemProps } from '@fluentui/react-overflow';
import { OverflowProps } from '@fluentui/react-overflow';
@@ -3128,6 +3129,8 @@ export { Overflow }

export { OverflowDivider }

export { OverflowImperativeRef }

export { OverflowItem }

export { OverflowItemProps }
2 changes: 1 addition & 1 deletion packages/react-components/react-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -925,7 +925,7 @@ export {
useOverflowVisibility,
} from '@fluentui/react-overflow';

export type { OverflowProps, OverflowItemProps } from '@fluentui/react-overflow';
export type { OverflowProps, OverflowItemProps, OverflowImperativeRef } from '@fluentui/react-overflow';

export {
Toolbar,
Original file line number Diff line number Diff line change
@@ -29,14 +29,20 @@ export interface OnOverflowChangeData extends OverflowState {
}

// @public
export const Overflow: React_2.ForwardRefExoticComponent<Partial<Pick<ObserveOptions, "padding" | "overflowDirection" | "overflowAxis" | "minimumVisible">> & {
export const Overflow: React_2.ForwardRefExoticComponent<Partial<Pick<ObserveOptions, "padding" | "overflowDirection" | "overflowAxis" | "minimumVisible" | "boxModel" | "measureGap">> & {
children: React_2.ReactElement;
onOverflowChange?: ((ev: null, data: OverflowState) => void) | undefined;
imperativeRef?: React_2.Ref<OverflowImperativeRef | null | undefined> | undefined;
} & React_2.RefAttributes<unknown>>;

// @public
export const OverflowDivider: React_2.ForwardRefExoticComponent<OverflowDividerProps & React_2.RefAttributes<unknown>>;

// @public (undocumented)
export interface OverflowImperativeRef {
updateOverflow: () => void;
}

// @public
export const OverflowItem: React_2.ForwardRefExoticComponent<OverflowItemProps & React_2.RefAttributes<unknown>>;

@@ -49,9 +55,10 @@ export type OverflowItemProps = {
};

// @public
export type OverflowProps = Partial<Pick<ObserveOptions, 'overflowAxis' | 'overflowDirection' | 'padding' | 'minimumVisible'>> & {
export type OverflowProps = Partial<Pick<ObserveOptions, 'overflowAxis' | 'overflowDirection' | 'padding' | 'minimumVisible' | 'boxModel' | 'measureGap'>> & {
children: React_2.ReactElement;
onOverflowChange?: (ev: null, data: OverflowState) => void;
imperativeRef?: React_2.Ref<OverflowImperativeRef | null | undefined>;
};

// @public (undocumented)
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.