diff --git a/packages/react-components/react-infobutton/etc/react-infobutton.api.md b/packages/react-components/react-infobutton/etc/react-infobutton.api.md
index 3d74b54b50698b..080037287f1be2 100644
--- a/packages/react-components/react-infobutton/etc/react-infobutton.api.md
+++ b/packages/react-components/react-infobutton/etc/react-infobutton.api.md
@@ -4,9 +4,13 @@
```ts
+///
+
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';
@@ -18,11 +22,13 @@ export const InfoButton: ForwardRefComponent;
export const infoButtonClassNames: SlotClassNames;
// @public
-export type InfoButtonProps = ComponentProps & {};
+export type InfoButtonProps = ComponentProps>;
// @public (undocumented)
export type InfoButtonSlots = {
- root: Slot<'div'>;
+ root: NonNullable>;
+ popover: NonNullable>;
+ content: NonNullable>;
};
// @public
diff --git a/packages/react-components/react-infobutton/package.json b/packages/react-components/react-infobutton/package.json
index ef46a6847f730f..94bab915c948af 100644
--- a/packages/react-components/react-infobutton/package.json
+++ b/packages/react-components/react-infobutton/package.json
@@ -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",
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/DefaultInfoButtonIcon.tsx b/packages/react-components/react-infobutton/src/components/InfoButton/DefaultInfoButtonIcon.tsx
new file mode 100644
index 00000000000000..336ae52755c871
--- /dev/null
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/DefaultInfoButtonIcon.tsx
@@ -0,0 +1,3 @@
+import { Info12Regular, Info12Filled, bundleIcon } from '@fluentui/react-icons';
+
+export const DefaultInfoButtonIcon = bundleIcon(Info12Filled, Info12Regular);
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.test.tsx b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.test.tsx
index 3ef2254124d51d..a22e9cc47c03b6 100644
--- a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.test.tsx
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.test.tsx
@@ -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(Default 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,
+ },
+ ],
+ },
});
});
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.tsx b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.tsx
index 2284305794c455..867bc036864b26 100644
--- a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.tsx
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.tsx
@@ -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 = React.forwardRef((props, ref) => {
const state = useInfoButton_unstable(props, ref);
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.types.ts b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.types.ts
index ff95c26471146e..97083e38847291 100644
--- a/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.types.ts
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/InfoButton.types.ts
@@ -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>;
+
+ /**
+ * The Popover element that wraps the content and root. Use this slot to pass props to the Popover.
+ */
+ popover: NonNullable>;
+
+ /**
+ * The content to be displayed in the PopoverSurface when the button is pressed.
+ */
+ content: NonNullable>;
};
/**
* InfoButton Props
*/
-export type InfoButtonProps = ComponentProps & {};
+export type InfoButtonProps = ComponentProps>;
/**
* State used in rendering InfoButton
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/__snapshots__/InfoButton.test.tsx.snap b/packages/react-components/react-infobutton/src/components/InfoButton/__snapshots__/InfoButton.test.tsx.snap
deleted file mode 100644
index 0b1dbff1b7af5f..00000000000000
--- a/packages/react-components/react-infobutton/src/components/InfoButton/__snapshots__/InfoButton.test.tsx.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`InfoButton renders a default state 1`] = `
-
-
- Default InfoButton
-
-
-`;
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/renderInfoButton.tsx b/packages/react-components/react-infobutton/src/components/InfoButton/renderInfoButton.tsx
index 0082ea998e1f43..2b2884a7965eb3 100644
--- a/packages/react-components/react-infobutton/src/components/InfoButton/renderInfoButton.tsx
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/renderInfoButton.tsx
@@ -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';
/**
@@ -8,6 +10,12 @@ import type { InfoButtonState, InfoButtonSlots } from './InfoButton.types';
export const renderInfoButton_unstable = (state: InfoButtonState) => {
const { slots, slotProps } = getSlots(state);
- // TODO Add additional slots in the appropriate place
- return ;
+ return (
+
+
+
+
+
+
+ );
};
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.ts b/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.ts
deleted file mode 100644
index 56c30895b815ae..00000000000000
--- a/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as React from 'react';
-import { getNativeElementProps } 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): InfoButtonState => {
- return {
- // TODO add appropriate props/defaults
- components: {
- // TODO add each slot's element type or component
- root: 'div',
- },
- // TODO add appropriate slots, for example:
- // mySlot: resolveShorthand(props.mySlot),
- root: getNativeElementProps('div', {
- ref,
- ...props,
- }),
- };
-};
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.tsx b/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.tsx
new file mode 100644
index 00000000000000..a998066b66d83a
--- /dev/null
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButton.tsx
@@ -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): InfoButtonState => {
+ const state: InfoButtonState = {
+ components: {
+ root: 'button',
+ popover: Popover,
+ content: PopoverSurface,
+ },
+
+ root: getNativeElementProps('button', {
+ children: ,
+ type: 'button',
+ ...props,
+ ref,
+ }),
+ popover: resolveShorthand(props.popover, {
+ required: true,
+ defaultProps: {
+ children: <>>,
+ 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;
+};
diff --git a/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButtonStyles.ts b/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButtonStyles.ts
index c1f5c927ea9a3e..4d3ed6908f9cb1 100644
--- a/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButtonStyles.ts
+++ b/packages/react-components/react-infobutton/src/components/InfoButton/useInfoButtonStyles.ts
@@ -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 = {
root: 'fui-InfoButton',
- // TODO: add class names for all slots on InfoButtonSlots.
- // Should be of the form `: 'fui-InfoButton__`
+ // 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;
};
diff --git a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonBestPractices.md b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonBestPractices.md
index 08ff8ddeeb5f86..865548d4e1bcad 100644
--- a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonBestPractices.md
+++ b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonBestPractices.md
@@ -1,5 +1,10 @@
-## Best practices
-
-### Do
+
+
+Best Practices
+
### Don't
+
+- Because the Popover isn't always visible, don't include information that people must know in order to complete the field.
+
+
diff --git a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDefault.stories.tsx b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDefault.stories.tsx
index 95c4860b164699..63dd8419d45426 100644
--- a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDefault.stories.tsx
+++ b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDefault.stories.tsx
@@ -1,4 +1,14 @@
import * as React from 'react';
import { InfoButton, InfoButtonProps } from '@fluentui/react-infobutton';
+import { Link } from '@fluentui/react-components';
-export const Default = (props: Partial) => ;
+export const Default = (props: Partial) => (
+
+ This is example content for an InfoButton. Learn more
+ >
+ }
+ />
+);
diff --git a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDescription.md b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDescription.md
index e69de29bb2d1d6..71e51a19a3d6ad 100644
--- a/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDescription.md
+++ b/packages/react-components/react-infobutton/src/stories/Infobutton/InfoButtonDescription.md
@@ -0,0 +1 @@
+InfoButtons provide a way to display additional information about a form field or an area in the UI.