Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
39 changes: 39 additions & 0 deletions apps/vr-tests-react-components/src/stories/Field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Checkbox } from '@fluentui/react-checkbox';
import { Combobox, Dropdown } from '@fluentui/react-combobox';

@fabricteam fabricteam Mar 2, 2023

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🕵 fluentuiv9 Open the Visual Regressions report to inspect the 7 screenshots

✅ There was 7 screenshots added, 0 screenshots removed, 1868 screenshots unchanged, 0 screenshots with different dimensions and 0 screenshots with visible difference.

unknown 7 screenshots
Image Name Diff(in Pixels) Image Type
Field.infoButton+horizontal.default.chromium.png 0 Added
Field.infoButton+longLabel.default.chromium.png 0 Added
Field.infoButton+noLabel.default.chromium.png 0 Added
Field.infoButton+required.default.chromium.png 0 Added
Field.infoButton+size-large.default.chromium.png 0 Added
Field.infoButton+size-small.default.chromium.png 0 Added
Field.infoButton.default.chromium.png 0 Added

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like size small + infobutton has some alignment issues. The label is position on the top and the infobutton looks to be lower than the label. Is this expected? Same for large, but not as noticeable.

import { Field } from '@fluentui/react-field';
import { Dismiss12Filled } from '@fluentui/react-icons';
import { InfoButton } from '@fluentui/react-infobutton';
import { Input } from '@fluentui/react-input';
import { ProgressBar } from '@fluentui/react-progress';
import { Radio, RadioGroup } from '@fluentui/react-radio';
Expand Down Expand Up @@ -109,6 +110,44 @@ storiesOf('Field', module)
<Checkbox label="Checkbox in a horizontal field" />
</Field>
))
.addStory('infoButton', () => (
<Field label="With info" infoButton={<InfoButton content="Example" />}>
<Input />
</Field>
))
.addStory('infoButton+required', () => (
<Field label="Required with info" required infoButton={<InfoButton content="Example" />}>
<Input />
</Field>
))
.addStory('infoButton+longLabel', () => (
<Field
label="With info button and a very long label that should wrap and the info button to appear on the last line"
infoButton={<InfoButton content="Example" />}
>
<Input />
</Field>
))
.addStory('infoButton+size:small', () => (
<Field label="Small with info" infoButton={<InfoButton content="Example" />} size="small">
<Input size="small" />
</Field>
))
.addStory('infoButton+size:large', () => (
<Field label="Large with info" infoButton={<InfoButton content="Example" />} size="large">
<Input size="large" />
</Field>
))
.addStory('infoButton+noLabel', () => (
<Field infoButton={<InfoButton content="Example" />}>
<Input />
</Field>
))
.addStory('infoButton+horizontal', () => (
<Field orientation="horizontal" label="With info" infoButton={<InfoButton content="Example" />}>
<Input />
</Field>
))
.addStory('Checkbox:error', () => (
<Field validationMessage="Error message">
<Checkbox label="Checkbox in a Field with an error" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Add infoButton slot to Field",
"packageName": "@fluentui/react-field",
"email": "behowell@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Add InfoButtonContext to allow other components to set default prop values",
"packageName": "@fluentui/react-infobutton",
"email": "behowell@microsoft.com",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/react-components/react-field/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@fluentui/react-context-selector": "^9.1.10",
"@fluentui/react-icons": "^2.0.175",
"@fluentui/react-infobutton": "9.0.0-beta.17",
"@fluentui/react-label": "^9.0.22",
"@fluentui/react-theme": "^9.1.5",
"@fluentui/react-utilities": "^9.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { InfoButton } from '@fluentui/react-infobutton';
import { render } from '@testing-library/react';
import { isConformant } from '../../testing/isConformant';
import { Field } from './index';
Expand All @@ -12,9 +13,9 @@ describe('Field', () => {
{
props: {
label: 'Test label',
infoButton: <InfoButton content="Test info button" />,
hint: 'Test hint',
validationMessage: 'Test validation message',
validationState: 'error',
},
},
],
Expand Down Expand Up @@ -189,4 +190,18 @@ describe('Field', () => {
'aria-required': true,
});
});

it('passes context to InfoButton to allow it to be labelledby the Field label', () => {
const result = render(
<Field label="Test label" infoButton={<InfoButton content="test" />}>
<input />
</Field>,
);

const label = result.getByText('Test label');
const infoButton = result.getByRole('button');

expect(label.id).toBeTruthy();
expect(infoButton.getAttribute('aria-labelledby')).toBe(`${label.id} ${infoButton.id}`);
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as React from 'react';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useFieldContextValues_unstable } from '../../contexts/useFieldContextValues';
import type { FieldProps } from './Field.types';
import { renderField_unstable } from './renderField';
import { useField_unstable } from './useField';
import { useFieldStyles_unstable } from './useFieldStyles';

export const Field: ForwardRefComponent<FieldProps> = React.forwardRef((props, ref) => {
const state = useField_unstable(props, ref);
const contextValues = useFieldContextValues_unstable(state);
useFieldStyles_unstable(state);
return renderField_unstable(state);
return renderField_unstable(state, contextValues);
});

Field.displayName = 'Field';
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import type { InfoButtonContextValue } from '@fluentui/react-infobutton';
import { Label } from '@fluentui/react-label';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

Expand All @@ -21,6 +22,16 @@ export type FieldSlots = {
*/
label?: Slot<typeof Label>;

/**
* Container for an InfoButton associated with the field. If supplied, it should be a single `<InfoButton>` control.
*
* @example
* ```
* <Field label="..." infoButton={<InfoButton content="..." />} />
* ```
*/
infoButton?: Slot<'div'>;

/**
* A message about the validation state. By default, this is an error message, but it can be a success, warning,
* or custom message by setting `validationState`.
Expand Down Expand Up @@ -100,4 +111,8 @@ export type FieldProps = Omit<ComponentProps<FieldSlots>, 'children'> & {
* State used in rendering Field
*/
export type FieldState = ComponentState<Required<FieldSlots>> &
Required<Pick<FieldProps, 'orientation' | 'validationState'>>;
Required<Pick<FieldProps, 'orientation' | 'validationState' | 'size'>>;

export type FieldContextValues = {
infoButton: InfoButtonContextValue;
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import * as React from 'react';
import { InfoButtonContextProvider } from '@fluentui/react-infobutton';
import { getSlots } from '@fluentui/react-utilities';
import type { FieldSlots, FieldState } from './Field.types';
import type { FieldContextValues, FieldSlots, FieldState } from './Field.types';

/**
* Render the final JSX of Field
*/
export const renderField_unstable = (state: FieldState) => {
export const renderField_unstable = (state: FieldState, contextValues: FieldContextValues) => {
const { slots, slotProps } = getSlots<FieldSlots>(state);

return (
<slots.root {...slotProps.root}>
{slots.label && <slots.label {...slotProps.label} />}
{slots.infoButton ? (
<slots.infoButton {...slotProps.infoButton}>
{slots.label && <slots.label {...slotProps.label} />}
<InfoButtonContextProvider value={contextValues.infoButton}>
{slotProps.infoButton.children}
</InfoButtonContextProvider>
</slots.infoButton>
) : (
slots.label && <slots.label {...slotProps.label} />
)}
{slotProps.root.children}
{slots.validationMessage && (
<slots.validationMessage {...slotProps.validationMessage}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
orientation = 'vertical',
required,
validationState = props.validationMessage ? 'error' : 'none',
size,
size = 'medium',
} = props;

const baseId = useId('field-');
Expand All @@ -43,6 +43,8 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
},
});

const infoButton = resolveShorthand(props.infoButton);

const validationMessage = resolveShorthand(props.validationMessage, {
defaultProps: {
id: baseId + '__validationMessage',
Expand Down Expand Up @@ -102,14 +104,17 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
return {
orientation,
validationState,
size,
components: {
root: 'div',
infoButton: 'div',
label: Label,
validationMessage: 'div',
validationMessageIcon: 'span',
hint: 'div',
},
root,
infoButton,
label,
validationMessageIcon,
validationMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { FieldSlots, FieldState } from './Field.types';

export const fieldClassNames: SlotClassNames<FieldSlots> = {
root: `fui-Field`,
infoButton: `fui-Field__infoButton`,
label: `fui-Field__label`,
validationMessage: `fui-Field__validationMessage`,
validationMessageIcon: `fui-Field__validationMessageIcon`,
Expand Down Expand Up @@ -41,6 +42,7 @@ const useLabelStyles = makeStyles({
base: {
paddingTop: tokens.spacingVerticalXXS,
paddingBottom: tokens.spacingVerticalXXS,
verticalAlign: 'middle',
},

large: {
Expand Down Expand Up @@ -121,18 +123,33 @@ export const useFieldStyles_unstable = (state: FieldState) => {
);

const labelStyles = useLabelStyles();

// Class name applied to the either the infoButton wrapper (which contains the label and InfoButton) if present,
// or the label itself if there is no infoButton.
const labelContainerClassName = mergeClasses(
horizontal && labelStyles.horizontal,
!horizontal && labelStyles.vertical,
!horizontal && state.size === 'large' && labelStyles.verticalLarge,
);

if (state.label) {
state.label.className = mergeClasses(
fieldClassNames.label,
labelStyles.base,
horizontal && labelStyles.horizontal,
!horizontal && labelStyles.vertical,
!state.infoButton && labelContainerClassName,
state.label.size === 'large' && labelStyles.large,
!horizontal && state.label.size === 'large' && labelStyles.verticalLarge,
state.label.className,
);
}

if (state.infoButton) {
state.infoButton.className = mergeClasses(
fieldClassNames.infoButton,
labelContainerClassName,
state.infoButton.className,
);
}

const validationMessageIconBaseClassName = useValidationMessageIconBaseClassName();
const validationMessageIconStyles = useValidationMessageIconStyles();
if (state.validationMessageIcon) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useFieldContextValues';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

import { InfoButtonContextValue } from '@fluentui/react-infobutton/src/index';
import type { FieldContextValues, FieldState } from '../Field';

export const useFieldContextValues_unstable = (state: FieldState): FieldContextValues => {
const { size, label } = state;
const associatedLabelId = label?.id;

const infoButton: InfoButtonContextValue = React.useMemo(
() => ({
associatedLabelId,
size,
}),
[associatedLabelId, size],
);

return { infoButton };
};
2 changes: 2 additions & 0 deletions packages/react-components/react-field/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { Field, fieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from './Field';
export type { FieldProps, FieldSlots, FieldState } from './Field';

export { useFieldContextValues_unstable } from './contexts/useFieldContextValues';

// eslint-disable-next-line deprecation/deprecation
export { getDeprecatedFieldClassNames, makeDeprecatedField } from './util/makeDeprecatedField';
// eslint-disable-next-line deprecation/deprecation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

import { Input } from '@fluentui/react-components';
import { Field, InfoButton } from '@fluentui/react-components/unstable';

export const WithInfoButton = () => (
<Field label="Example" required infoButton={<InfoButton content="This is example content for an InfoButton." />}>
<Input />
</Field>
);

WithInfoButton.storyName = 'With InfoButton';
WithInfoButton.parameters = {
docs: {
description: {
story: 'The `infoButton` slot allows the addition of an `<InfoButton />` after the label.',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { Hint } from './FieldHint.stories';
export { Horizontal } from './FieldHorizontal.stories';
export { Required } from './FieldRequired.stories';
export { Size } from './FieldSize.stories';
export { WithInfoButton } from './FieldWithInfoButton.stories';
export { ComponentExamples } from './FieldComponentExamples.stories';
export { RenderFunction } from './FieldRenderFunction.stories';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type InfoButtonSlots = {
* InfoButton Props
*/
export type InfoButtonProps = Omit<ComponentProps<Partial<InfoButtonSlots>>, 'disabled'> & {
/**
* The ID of the control label associated with this InfoButton. Used to set the info button's aria-labelledby.
*/
associatedLabelId?: string;

/**
* Size of the InfoButton.
*
Expand Down
Loading