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
Expand Up @@ -4,9 +4,13 @@

```ts

/// <reference types="react" />

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { ForwardRefComponent } from '@fluentui/react-utilities';
import type { PopoverProps } from '@fluentui/react-popover';
import type { PopoverSurface } from '@fluentui/react-popover';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';
Expand All @@ -18,11 +22,13 @@ export const InfoButton: ForwardRefComponent<InfoButtonProps>;
export const infoButtonClassNames: SlotClassNames<InfoButtonSlots>;

// @public
export type InfoButtonProps = ComponentProps<InfoButtonSlots> & {};
export type InfoButtonProps = ComponentProps<Partial<InfoButtonSlots>>;

// @public (undocumented)
export type InfoButtonSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'button'>>;
popover: NonNullable<Slot<PopoverProps>>;
content: NonNullable<Slot<typeof PopoverSurface>>;
};

// @public
Expand Down
3 changes: 3 additions & 0 deletions packages/react-components/react-infobutton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"@fluentui/scripts": "^1.0.0"
},
"dependencies": {
"@fluentui/react-icons": "^2.0.175",
"@fluentui/react-popover": "^9.3.0",
"@fluentui/react-tabster": "^9.2.1",
"@fluentui/react-theme": "^9.1.1",
"@fluentui/react-utilities": "^9.1.2",
"@griffel/react": "^1.4.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Info12Regular, Info12Filled, bundleIcon } from '@fluentui/react-icons';

export const DefaultInfoButtonIcon = bundleIcon(Info12Filled, Info12Regular);
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { InfoButton } from './InfoButton';
import { isConformant } from '../../common/isConformant';
import { infoButtonClassNames } from './useInfoButtonStyles';
import type { RenderResult } from '@testing-library/react';

// testing-library's queryByRole function doesn't look inside portals
function queryByRoleDialog(result: RenderResult) {
const dialogs = result.baseElement.querySelectorAll('[role="dialog"]');
if (!dialogs?.length) {
return null;
} else {
expect(dialogs.length).toBe(1);
return dialogs.item(0) as HTMLElement;
}
}

const getPopoverSurfaceElement = (result: RenderResult) => {
// button needs to be clicked otherwise content won't be rendered.
result.getByRole('button').click();
const dialog = queryByRoleDialog(result);
expect(dialog).not.toBeNull();
return dialog!;
};

describe('InfoButton', () => {
isConformant({
Component: InfoButton,
displayName: 'InfoButton',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<InfoButton>Default InfoButton</InfoButton>);
expect(result.container).toMatchSnapshot();
requiredProps: {
content: 'Popover content',
},
testOptions: {
'has-static-classnames': [
{
props: {
content: 'Popover content',
},
expectedClassNames: {
root: infoButtonClassNames.root,
content: infoButtonClassNames.content,
},
getPortalElement: getPopoverSurfaceElement,
},
],
},
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as React from 'react';
import { useInfoButton_unstable } from './useInfoButton';
import { ForwardRefComponent } from '@fluentui/react-utilities';
import { renderInfoButton_unstable } from './renderInfoButton';
import { useInfoButton_unstable } from './useInfoButton';
import { useInfoButtonStyles_unstable } from './useInfoButtonStyles';
import type { InfoButtonProps } from './InfoButton.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';

/**
* InfoButton component - TODO: add more docs
* InfoButtons provide a way to display additional information about a form field or an area in the UI.
*/
export const InfoButton: ForwardRefComponent<InfoButtonProps> = React.forwardRef((props, ref) => {
const state = useInfoButton_unstable(props, ref);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { PopoverProps, PopoverSurface } from '@fluentui/react-popover';

export type InfoButtonSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'button'>>;

/**
* The Popover element that wraps the content and root. Use this slot to pass props to the Popover.
*/
popover: NonNullable<Slot<PopoverProps>>;

/**
* The content to be displayed in the PopoverSurface when the button is pressed.
*/
content: NonNullable<Slot<typeof PopoverSurface>>;
};

/**
* InfoButton Props
*/
export type InfoButtonProps = ComponentProps<InfoButtonSlots> & {};
export type InfoButtonProps = ComponentProps<Partial<InfoButtonSlots>>;

/**
* State used in rendering InfoButton
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';
import { getSlots } from '@fluentui/react-utilities';
import { PopoverTrigger } from '@fluentui/react-popover';
import type { PopoverProps } from '@fluentui/react-popover';
import type { InfoButtonState, InfoButtonSlots } from './InfoButton.types';

/**
Expand All @@ -8,6 +10,12 @@ import type { InfoButtonState, InfoButtonSlots } from './InfoButton.types';
export const renderInfoButton_unstable = (state: InfoButtonState) => {
const { slots, slotProps } = getSlots<InfoButtonSlots>(state);

// TODO Add additional slots in the appropriate place
return <slots.root {...slotProps.root} />;
return (
<slots.popover {...(slotProps.popover as PopoverProps)}>
<PopoverTrigger>
<slots.root {...slotProps.root} />
</PopoverTrigger>
<slots.content {...slotProps.content} />
</slots.popover>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import { DefaultInfoButtonIcon } from './DefaultInfoButtonIcon';
import { getNativeElementProps, mergeCallbacks, resolveShorthand } from '@fluentui/react-utilities';
import { Popover, PopoverSurface } from '@fluentui/react-popover';
import { useControllableState } from '@fluentui/react-utilities';
import type { InfoButtonProps, InfoButtonState } from './InfoButton.types';

/**
* Create the state required to render InfoButton.
*
* The returned state can be modified with hooks such as useInfoButtonStyles_unstable,
* before being passed to renderInfoButton_unstable.
*
* @param props - props from this instance of InfoButton
* @param ref - reference to root HTMLElement of InfoButton
*/
export const useInfoButton_unstable = (props: InfoButtonProps, ref: React.Ref<HTMLElement>): InfoButtonState => {
const state: InfoButtonState = {
components: {
root: 'button',
popover: Popover,
content: PopoverSurface,
},

root: getNativeElementProps('button', {
children: <DefaultInfoButtonIcon />,
type: 'button',
...props,
ref,
}),
popover: resolveShorthand(props.popover, {
required: true,
defaultProps: {
children: <></>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason this needs to be set? I would expect if there are no children for children to be undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, children cannot be undefined, so this is needed.

positioning: 'above-start',
size: 'small',
withArrow: true,
},
}),
content: resolveShorthand(props.content, {
required: true,
defaultProps: {
role: 'dialog',
},
}),
};

const [popoverOpen, setPopoverOpen] = useControllableState({
state: state.popover.open,
defaultState: state.popover.defaultOpen,
initialState: false,
});

state.popover.open = popoverOpen;
state.popover.onOpenChange = mergeCallbacks(state.popover.onOpenChange, (e, data) => setPopoverOpen(data.open));

return state;
};
Original file line number Diff line number Diff line change
@@ -1,33 +1,107 @@
import { makeStyles, mergeClasses } from '@griffel/react';
import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster';
import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons';
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
import type { InfoButtonSlots, InfoButtonState } from './InfoButton.types';
import type { SlotClassNames } from '@fluentui/react-utilities';

export const infoButtonClassNames: SlotClassNames<InfoButtonSlots> = {
root: 'fui-InfoButton',
// TODO: add class names for all slots on InfoButtonSlots.
// Should be of the form `<slotName>: 'fui-InfoButton__<slotName>`
// this className won't be used, but it's needed to satisfy the type checker
popover: 'fui-InfoButton__popover',
content: 'fui-InfoButton__content',
};

/**
* Styles for the root slot
*/
const useStyles = makeStyles({
root: {
// TODO Add default styles for the root element
const useButtonStyles = makeStyles({
base: {
alignItems: 'center',
boxSizing: 'border-box',
display: 'inline-flex',
justifyContent: 'center',
textDecorationLine: 'none',
verticalAlign: 'middle',

backgroundColor: tokens.colorTransparentBackground,
color: tokens.colorNeutralForeground2,
fontFamily: tokens.fontFamilyBase,

...shorthands.overflow('hidden'),
...shorthands.border(tokens.strokeWidthThin, 'solid', tokens.colorTransparentStroke),
...shorthands.padding(tokens.spacingVerticalXS, tokens.spacingHorizontalXS),
...shorthands.margin(0),

[`& .${iconFilledClassName}`]: {
display: 'none',
},
[`& .${iconRegularClassName}`]: {
display: 'inline-flex',
},

':enabled:hover': {
backgroundColor: tokens.colorTransparentBackgroundHover,
color: tokens.colorNeutralForeground2BrandHover,

[`& .${iconFilledClassName}`]: {
display: 'inline-flex',
},
[`& .${iconRegularClassName}`]: {
display: 'none',
},
},
':enabled:hover:active': {
backgroundColor: tokens.colorTransparentBackgroundPressed,
color: tokens.colorNeutralForeground2BrandPressed,
},
':disabled': {
cursor: 'not-allowed',
color: tokens.colorNeutralForegroundDisabled,
},
},

// TODO add additional classes for different states and/or slots
focusIndicator: createCustomFocusIndicatorStyle({
...shorthands.borderRadius(tokens.borderRadiusSmall),
...shorthands.borderColor(tokens.colorTransparentStroke),
outlineColor: tokens.colorTransparentStroke,
outlineWidth: tokens.strokeWidthThick,
outlineStyle: 'solid',
boxShadow: `
${tokens.shadow4},
0 0 0 ${tokens.borderRadiusSmall} ${tokens.colorStrokeFocus2}
`,
zIndex: 1,
}),

selected: {
backgroundColor: tokens.colorTransparentBackgroundSelected,
color: tokens.colorNeutralForeground2BrandSelected,

[`& .${iconFilledClassName}`]: {
display: 'inline-flex',
},
[`& .${iconRegularClassName}`]: {
display: 'none',
},
},
});

/**
* Apply styling to the InfoButton slots based on the state
*/
export const useInfoButtonStyles_unstable = (state: InfoButtonState): InfoButtonState => {
const styles = useStyles();
state.root.className = mergeClasses(infoButtonClassNames.root, styles.root, state.root.className);
const { open } = state.popover;
const buttonStyles = useButtonStyles();

// TODO Add class names to slots, for example:
// state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className);
state.content.className = mergeClasses(infoButtonClassNames.content, state.content.className);
state.root.className = mergeClasses(
infoButtonClassNames.root,
buttonStyles.base,
buttonStyles.focusIndicator,
open && buttonStyles.selected,
state.root.className,
);

return state;
};
Loading