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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: New overflow items all always dispatch updates to subscriber",
"packageName": "@fluentui/priority-overflow",
"email": "lingfangao@hotmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Export useOverflowContext as @internal for testing purposes",
"packageName": "@fluentui/react-overflow",
"email": "lingfangao@hotmail.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type { OverflowGroupState, OverflowItemEntry, OverflowManager, ObserveOpt
export function createOverflowManager(): OverflowManager {
let container: HTMLElement | undefined;
let overflowMenu: HTMLElement | undefined;
// Set as true when resize observer is observing
let observing = false;
// If true, next update will dispatch to onUpdateOverflow even if queue top states don't change
let forceDispatch = false;
const options: Required<ObserveOptions> = {
padding: 10,
overflowAxis: 'horizontal',
Expand Down Expand Up @@ -118,11 +121,12 @@ export function createOverflowManager(): OverflowManager {
options.onUpdateOverflow({ visibleItems, invisibleItems, groupVisibility });
};

const processOverflowItems = (availableSize: number) => {
const processOverflowItems = (): boolean => {
if (!container) {
return;
return false;
}

const availableSize = getOffsetSize(container) - options.padding;
const overflowMenuOffset = overflowMenu ? getOffsetSize(overflowMenu) : 0;

// Snapshot of the visible/invisible state to compare for updates
Expand Down Expand Up @@ -159,17 +163,17 @@ export function createOverflowManager(): OverflowManager {

// only update when the state of visible/invisible items has changed
if (visibleItemQueue.peek() !== visibleTop || invisibleItemQueue.peek() !== invisibleTop) {
dispatchOverflowUpdate();
return true;
}

return false;
};

const forceUpdate: OverflowManager['forceUpdate'] = () => {
if (!container) {
return;
if (processOverflowItems() || forceDispatch) {
forceDispatch = false;
dispatchOverflowUpdate();
}

const availableSize = getOffsetSize(container) - options.padding;
processOverflowItems(availableSize);
};

const update: OverflowManager['update'] = debounce(forceUpdate);
Expand Down Expand Up @@ -197,6 +201,10 @@ export function createOverflowManager(): OverflowManager {

// some options can affect priority which are only set on `observe`
if (observing) {
// Updates to elements might not change the queue tops
// i.e. new element is enqueued but the top of the queue stays the same
// force a dispatch on the next batched update
forceDispatch = true;
visibleItemQueue.enqueue(item.id);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

```ts

import { ContextSelector } from '@fluentui/react-context-selector';
import type { ObserveOptions } from '@fluentui/priority-overflow';
import type { OnUpdateOverflow } from '@fluentui/priority-overflow';
import { OverflowGroupState } from '@fluentui/priority-overflow';
Expand Down Expand Up @@ -54,6 +55,9 @@ export interface UseOverflowContainerReturn<TElement extends HTMLElement> extend
containerRef: React_2.RefObject<TElement>;
}

// @internal (undocumented)
export const useOverflowContext: <SelectedValue>(selector: ContextSelector<OverflowContextValue, SelectedValue>) => SelectedValue;

// @public (undocumented)
export const useOverflowCount: () => number;

Expand Down
68 changes: 65 additions & 3 deletions packages/react-components/react-overflow/src/Overflow.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
OverflowProps,
useIsOverflowGroupVisible,
useOverflowMenu,
useOverflowContext,
} from '@fluentui/react-overflow';
import { Portal } from '@fluentui/react-portal';

const selectors = {
container: 'data-test-container',
Expand Down Expand Up @@ -72,6 +74,7 @@ const Item: React.FC<{ children?: React.ReactNode; width?: number } & Omit<Overf

const Menu: React.FC<{ width?: number }> = ({ width }) => {
const { isOverflowing, ref, overflowCount } = useOverflowMenu<HTMLButtonElement>();
const itemVisibility = useOverflowContext(ctx => ctx.itemVisibility);
const selector = {
[selectors.menu]: '',
};
Expand All @@ -82,9 +85,21 @@ const Menu: React.FC<{ width?: number }> = ({ width }) => {

// No need to actually render a menu, we're testing state
return (
<button {...selector} ref={ref} style={{ width: width ?? 50, height: 20 }}>
+{overflowCount}
</button>
<>
<button {...selector} ref={ref} style={{ width: width ?? 50, height: 20 }}>
+{overflowCount}
</button>
<Portal>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: 200 }}>
{Object.entries(itemVisibility).map(([id, visible]) => (
<>
<div>{id}</div>
<div id={`${id}-visibility`}>{visible ? 'visible' : 'invisible'}</div>
</>
))}
</div>
</Portal>
</>
);
};

Expand Down Expand Up @@ -452,4 +467,51 @@ describe('Overflow', () => {
cy.get(`[${selectors.menu}]`).should('not.be.visible');
cy.get(`[${selectors.item}="4"]`).should('not.be.visible');
});

// Container will fit 4 items with width 100 and the overflow menu
// Once the priority of the foo item is updated it should have more
// priority than Item 5 (width 100 item), so the foo item should be visible
// once this foo item is visible, make sure that the subscriber is in sync.
it('should dispatch update when updating priority of an item', () => {
const Example = () => {
const [priority, setPriority] = React.useState<number | undefined>();

return (
<>
<Container>
<Item id="1" width={100}>
Item 1
</Item>
<Item id="2" width={100}>
Item 2
</Item>
<Item id="3" width={100}>
Item 3
</Item>
<Item id="4" width={100}>
Item 4
</Item>
<Item id="5" width={100}>
Item 5
</Item>
<Item id="foo" width={50} priority={priority}>
Foo
</Item>
<Item id="7" width={50}>
Item
</Item>
<Item id="8" width={50}>
Item
</Item>
<Menu width={32} />
</Container>
<button onClick={() => setPriority(2)}>Update priority</button>
</>
);
};
mount(<Example />);

setContainerSize(500);
cy.contains('Update priority').click().get('#foo-visibility').should('have.text', 'visible');
});
});
2 changes: 2 additions & 0 deletions packages/react-components/react-overflow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ export { useOverflowCount } from './useOverflowCount';
export { useOverflowItem } from './useOverflowItem';
export { useOverflowMenu } from './useOverflowMenu';

export { useOverflowContext } from './overflowContext';

export type { OverflowItemProps } from './components/OverflowItem/OverflowItem.types';
export { OverflowItem } from './components/OverflowItem/OverflowItem';
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { OverflowGroupState, OverflowItemEntry } from '@fluentui/priority-overflow';
import { ContextSelector, createContext, useContextSelector, Context } from '@fluentui/react-context-selector';

/**
* @internal
*/
export interface OverflowContextValue {
itemVisibility: Record<string, boolean>;
groupVisibility: Record<string, OverflowGroupState>;
Expand All @@ -23,5 +26,8 @@ const overflowContextDefaultValue: OverflowContextValue = {
registerOverflowMenu: () => () => null,
};

/**
* @internal
*/
export const useOverflowContext = <SelectedValue>(selector: ContextSelector<OverflowContextValue, SelectedValue>) =>
useContextSelector(OverflowContext, (ctx = overflowContextDefaultValue) => selector(ctx));