Skip to content

Commit

Permalink
feat: implement auto reflow for MessageBar layouts (#29328)
Browse files Browse the repository at this point in the history
* feat: Initial implementation

Adds an initial implementation of MessageBar that includes design for
different intents and multiline handling.

* remove TODOs

* fix multiline alignment

* fix slot type misalign

* fix tests

* rename to secondaryActions

* fix warnings

* break out into components

* remove stories

* revert tsconfig changes

* remove outdated keyborg cypress test

* add keyborg test again

* add tsconfig paths to cypress

* feat: Implement auto reflow for MessageBar layouts

* update api

* pr comments

* cleanup
  • Loading branch information
ling1726 authored Sep 29, 2023
1 parent 03466e4 commit f1e96a1
Show file tree
Hide file tree
Showing 18 changed files with 221 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const MessageBarActions: ForwardRefComponent<MessageBarActionsProps>;
export const messageBarActionsClassNames: SlotClassNames<MessageBarActionsSlots>;

// @public
export type MessageBarActionsProps = ComponentProps<MessageBarActionsSlots> & {};
export type MessageBarActionsProps = ComponentProps<MessageBarActionsSlots>;

// @public (undocumented)
export type MessageBarActionsSlots = {
Expand All @@ -41,7 +41,7 @@ export const MessageBarBody: ForwardRefComponent<MessageBarBodyProps>;
export const messageBarBodyClassNames: SlotClassNames<MessageBarBodySlots>;

// @public
export type MessageBarBodyProps = ComponentProps<MessageBarBodySlots> & {};
export type MessageBarBodyProps = ComponentProps<MessageBarBodySlots>;

// @public (undocumented)
export type MessageBarBodySlots = {
Expand All @@ -59,12 +59,11 @@ export const MessageBarContextProvider: React_2.Provider<MessageBarContextValue

// @public (undocumented)
export type MessageBarContextValue = {
layout?: 'multiline' | 'singleline';
layout?: 'multiline' | 'singleline' | 'auto';
};

// @public
export type MessageBarProps = ComponentProps<MessageBarSlots> & Pick<MessageBarContextValue, 'layout'> & {
multiline?: boolean;
intent?: 'info' | 'success' | 'warning' | 'error';
};

Expand All @@ -84,7 +83,7 @@ export const MessageBarTitle: ForwardRefComponent<MessageBarTitleProps>;
export const messageBarTitleClassNames: SlotClassNames<MessageBarTitleSlots>;

// @public
export type MessageBarTitleProps = ComponentProps<MessageBarTitleSlots> & {};
export type MessageBarTitleProps = ComponentProps<MessageBarTitleSlots>;

// @public (undocumented)
export type MessageBarTitleSlots = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@fluentui/react-button": "^9.3.43",
"@fluentui/react-icons": "^2.0.217",
"@fluentui/react-jsx-runtime": "^9.0.12",
"@fluentui/react-shared-contexts": "^9.9.1",
"@fluentui/react-theme": "^9.1.14",
"@fluentui/react-utilities": "^9.13.5",
"@griffel/react": "^1.5.14",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { isConformant } from '../../testing/isConformant';
import { MessageBar } from './MessageBar';

describe('MessageBar', () => {
beforeAll(() => {
// https://github.com/jsdom/jsdom/issues/3368
global.ResizeObserver = class ResizeObserver {
public observe() {
// do nothing
}
public unobserve() {
// do nothing
}
public disconnect() {
// do nothing
}
};
});

isConformant({
Component: MessageBar,
displayName: 'MessageBar',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export type MessageBarContextValues = {
*/
export type MessageBarProps = ComponentProps<MessageBarSlots> &
Pick<MessageBarContextValue, 'layout'> & {
multiline?: boolean;
intent?: 'info' | 'success' | 'warning' | 'error';
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import { getNativeElementProps, slot } from '@fluentui/react-utilities';
import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities';
import type { MessageBarProps, MessageBarState } from './MessageBar.types';
import { getIntentIcon } from './getIntentIcon';
import { useMessageBarReflow } from './useMessageBarReflow';

/**
* Create the state required to render MessageBar.
Expand All @@ -13,7 +14,12 @@ import { getIntentIcon } from './getIntentIcon';
* @param ref - reference to root HTMLElement of MessageBar
*/
export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref<HTMLElement>): MessageBarState => {
const { layout = 'singleline', intent = 'info' } = props;
const { layout = 'auto', intent = 'info' } = props;

const autoReflow = layout === 'auto';
const { ref: reflowRef, reflowing } = useMessageBarReflow(autoReflow);

const computedLayout = autoReflow ? (reflowing ? 'multiline' : 'singleline') : layout;

return {
components: {
Expand All @@ -22,7 +28,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref<HT
},
root: slot.always(
getNativeElementProps('div', {
ref,
ref: useMergedRefs(ref, reflowRef),
...props,
}),
{ elementType: 'div' },
Expand All @@ -33,7 +39,7 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref<HT
elementType: 'div',
defaultProps: { children: getIntentIcon(intent) },
}),
layout,
layout: computedLayout,
intent,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { isHTMLElement } from '@fluentui/react-utilities';

export function useMessageBarReflow(enabled: boolean = false) {
const { targetDocument } = useFluent();
const forceUpdate = React.useReducer(() => ({}), {})[1];
const reflowingRef = React.useRef(false);
const resizeObserverRef = React.useRef<ResizeObserver | null>(null);
const prevInlineSizeRef = React.useRef(-1);

const handleResize: ResizeObserverCallback = React.useCallback(
entries => {
// Resize observer is only owned by this component - one resize observer entry expected
// No need to support mutliple fragments - one border box entry expected
if (process.env.NODE_ENV !== 'production' && entries.length > 1) {
// eslint-disable-next-line no-console
console.error(
[
'useMessageBarReflow: Resize observer should only have one entry. ',
'If multiple entries are observed, the first entry will be used.',
'This is a bug, please report it to the Fluent UI team.',
].join(' '),
);
}

const entry = entries[0];
const borderBoxSize = entry?.borderBoxSize[0];
if (!borderBoxSize || !entry) {
return;
}

const { inlineSize } = borderBoxSize;
const { target } = entry;

if (!isHTMLElement(target)) {
return;
}

let nextReflowing: boolean | undefined;

// No easy way to really determine when the single line layout will fit
// Just keep try to set single line layout as long as the size is growing
// Will cause flickering when size is being adjusted gradually (i.e. drag) - but this should not be a common case
if (reflowingRef.current) {
if (prevInlineSizeRef.current < inlineSize) {
nextReflowing = false;
}
} else {
const scrollWidth = target.scrollWidth;
if (inlineSize < scrollWidth) {
nextReflowing = true;
}
}

prevInlineSizeRef.current = inlineSize;
if (typeof nextReflowing !== 'undefined' && reflowingRef.current !== nextReflowing) {
reflowingRef.current = nextReflowing;
forceUpdate();
}
},
[forceUpdate],
);

const ref = React.useCallback(
(el: HTMLElement | null) => {
if (!enabled || !el || !targetDocument?.defaultView) {
return;
}

resizeObserverRef.current?.disconnect();

const win = targetDocument.defaultView;
const resizeObserver = new win.ResizeObserver(handleResize);
resizeObserverRef.current = resizeObserver;
resizeObserver.observe(el, { box: 'border-box' });
},
[targetDocument, handleResize, enabled],
);

React.useEffect(() => {
return () => {
resizeObserverRef.current?.disconnect();
};
}, []);

return { ref, reflowing: reflowingRef.current };
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export const messageBarClassNames: SlotClassNames<MessageBarSlots> = {
};

const useRootBaseStyles = makeResetStyles({
whiteSpace: 'nowrap',
display: 'grid',
gridTemplateColumns: 'auto 1fr auto auto',
gridTemplateRows: '1fr',
gridTemplateAreas: '"icon body secondaryActions actions"',
...shorthands.padding('0', tokens.spacingHorizontalM),
...shorthands.border(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke1),
Expand All @@ -30,6 +32,8 @@ const useIconBaseStyles = makeResetStyles({

const useMultilineStyles = makeStyles({
rootMultiline: {
whiteSpace: 'normal',
alignItems: 'start',
paddingTop: tokens.spacingVerticalMNudge,
gridTemplateColumns: 'auto 1fr auto',
gridTemplateAreas: `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useMessageBarActionsStyles_unstable } from './useMessageBarActionsStyle
import type { MessageBarActionsProps } from './MessageBarActions.types';

/**
* MessageBarActions component - TODO: add more docs
* MessageBarActions component
*/
export const MessageBarActions: ForwardRefComponent<MessageBarActionsProps> = React.forwardRef((props, ref) => {
const state = useMessageBarActions_unstable(props, ref);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type MessageBarActionsSlots = {
/**
* MessageBarActions Props
*/
export type MessageBarActionsProps = ComponentProps<MessageBarActionsSlots> & {};
export type MessageBarActionsProps = ComponentProps<MessageBarActionsSlots>;

/**
* State used in rendering MessageBarActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const messageBarActionsClassNames: SlotClassNames<MessageBarActionsSlots>
const useRootBaseStyles = makeResetStyles({
...shorthands.gridArea('secondaryActions'),
display: 'flex',
alignItems: 'center',
columnGap: tokens.spacingHorizontalM,
marginRight: tokens.spacingHorizontalM,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type MessageBarBodySlots = {
/**
* MessageBarBody Props
*/
export type MessageBarBodyProps = ComponentProps<MessageBarBodySlots> & {};
export type MessageBarBodyProps = ComponentProps<MessageBarBodySlots>;

/**
* State used in rendering MessageBarBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type MessageBarTitleSlots = {
/**
* MessageBarTitle Props
*/
export type MessageBarTitleProps = ComponentProps<MessageBarTitleSlots> & {};
export type MessageBarTitleProps = ComponentProps<MessageBarTitleSlots>;

/**
* State used in rendering MessageBarTitle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,5 @@ import type { MessageBarTitleState, MessageBarTitleSlots } from './MessageBarTit
export const renderMessageBarTitle_unstable = (state: MessageBarTitleState) => {
assertSlots<MessageBarTitleSlots>(state);

// TODO Add additional slots in the appropriate place
return (
<>
<state.root />
</>
);
return <state.root />;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';

export type MessageBarContextValue = {
layout?: 'multiline' | 'singleline';
layout?: 'multiline' | 'singleline' | 'auto';
};
const messageBarContext = React.createContext<MessageBarContextValue | undefined>(undefined);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { Button, Link, Switch } from '@fluentui/react-components';
import { DismissRegular } from '@fluentui/react-icons';
import { MessageBar, MessageBarActions, MessageBarBody, MessageBarTitle } from '@fluentui/react-message-bar-preview';

const intents = ['info', 'warning', 'error', 'success'] as const;
export const ManualLayout = () => {
const [single, setSingle] = React.useState(true);
return (
<>
<Switch
label={single ? 'Single line layout' : 'Multi line layout'}
checked={single}
onChange={(_, { checked }) => setSingle(checked)}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{intents.map(intent => (
<MessageBar key={intent} layout={single ? 'singleline' : 'multiline'} intent={intent}>
<MessageBarBody>
<MessageBarTitle>Descriptive title</MessageBarTitle>
Message providing information to the user with actionable insights. <Link>Link</Link>
</MessageBarBody>
<MessageBarActions containerAction={<Button appearance="transparent" icon={<DismissRegular />} />}>
<Button>Action</Button>
<Button>Action</Button>
</MessageBarActions>
</MessageBar>
))}
</div>
</>
);
};

This file was deleted.

Loading

0 comments on commit f1e96a1

Please sign in to comment.