From 06d17256afac12f2b9a1f7b1d9218c384e6513ed Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Tue, 16 Aug 2022 17:41:17 -0700 Subject: [PATCH 01/22] Initial implementation of Field and InputField --- .../react-field/etc/react-field.api.md | 485 +++++++++++++++++- .../react-components/react-field/package.json | 12 + .../react-field/src/InputField.ts | 1 + .../src/components/Field/Field.test.tsx | 18 - .../src/components/Field/Field.tsx | 18 - .../src/components/Field/Field.types.ts | 97 +++- .../components/Field/SlotComponent.types.ts | 25 + .../Field/__snapshots__/Field.test.tsx.snap | 11 - .../react-field/src/components/Field/index.ts | 1 - .../src/components/Field/renderField.tsx | 22 +- .../src/components/Field/useField.ts | 28 - .../src/components/Field/useField.tsx | 159 ++++++ .../src/components/Field/useFieldStyles.ts | 144 +++++- .../components/InputField/InputField.test.tsx | 32 ++ .../src/components/InputField/InputField.tsx | 22 + .../__snapshots__/InputField.test.tsx.snap | 19 + .../src/components/InputField/index.ts | 1 + .../react-components/react-field/src/index.ts | 8 +- .../stories/Field/FieldDefault.stories.tsx | 4 - .../src/stories/Field/FieldDescription.md | 0 .../src/stories/Field/index.stories.tsx | 18 - .../InputFieldBestPractices.md} | 0 .../InputField/InputFieldDefault.stories.tsx | 12 + .../InputField/InputFieldDescription.md | 3 + .../InputFieldHelperText.stories.tsx | 14 + .../InputFieldHorizontal.stories.tsx | 40 ++ .../InputField/InputFieldLabel.stories.tsx | 12 + .../InputField/InputFieldRequired.stories.tsx | 14 + .../InputField/InputFieldSize.stories.tsx | 22 + .../InputField/InputFieldStatus.stories.tsx | 57 ++ .../src/stories/InputField/index.stories.tsx | 24 + 31 files changed, 1177 insertions(+), 146 deletions(-) create mode 100644 packages/react-components/react-field/src/InputField.ts delete mode 100644 packages/react-components/react-field/src/components/Field/Field.test.tsx delete mode 100644 packages/react-components/react-field/src/components/Field/Field.tsx create mode 100644 packages/react-components/react-field/src/components/Field/SlotComponent.types.ts delete mode 100644 packages/react-components/react-field/src/components/Field/__snapshots__/Field.test.tsx.snap delete mode 100644 packages/react-components/react-field/src/components/Field/useField.ts create mode 100644 packages/react-components/react-field/src/components/Field/useField.tsx create mode 100644 packages/react-components/react-field/src/components/InputField/InputField.test.tsx create mode 100644 packages/react-components/react-field/src/components/InputField/InputField.tsx create mode 100644 packages/react-components/react-field/src/components/InputField/__snapshots__/InputField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/InputField/index.ts delete mode 100644 packages/react-components/react-field/src/stories/Field/FieldDefault.stories.tsx delete mode 100644 packages/react-components/react-field/src/stories/Field/FieldDescription.md delete mode 100644 packages/react-components/react-field/src/stories/Field/index.stories.tsx rename packages/react-components/react-field/src/stories/{Field/FieldBestPractices.md => InputField/InputFieldBestPractices.md} (100%) create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldLabel.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldRequired.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldSize.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/index.stories.tsx diff --git a/packages/react-components/react-field/etc/react-field.api.md b/packages/react-components/react-field/etc/react-field.api.md index 4833ea08b380e..4aec5f4918bc5 100644 --- a/packages/react-components/react-field/etc/react-field.api.md +++ b/packages/react-components/react-field/etc/react-field.api.md @@ -4,41 +4,494 @@ ```ts +/// + import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { Input } from '@fluentui/react-input'; +import { Label } from '@fluentui/react-label'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; -import type { SlotClassNames } from '@fluentui/react-utilities'; +import { SlotClassNames } from '@fluentui/react-utilities'; +import { SlotRenderFunction } from '@fluentui/react-utilities'; +import { SlotShorthandValue } from '@fluentui/react-utilities'; // @public -export const Field: ForwardRefComponent; - -// @public (undocumented) -export const fieldClassName = "fui-Field"; +export type FieldProps = ComponentProps>, 'fieldComponent'> & { + fieldOrientation?: 'vertical' | 'horizontal'; + status?: 'error' | 'warning' | 'success'; +}; -// @public (undocumented) -export const fieldClassNames: SlotClassNames; +// @public +export type FieldSlots = { + root: NonNullable>; + fieldComponent: SlotComponent; + label?: SlotComponent; + statusText?: Slot<'span'>; + statusIcon?: Slot<'span'>; + helperText?: Slot<'span'>; +}; // @public -export type FieldProps = ComponentProps & {}; +export type FieldState = ComponentState>> & Pick, 'fieldOrientation' | 'status'> & { + classNames: SlotClassNames>; +}; // @public (undocumented) -export type FieldSlots = { - root: Slot<'div'>; -}; +export const InputField: ForwardRefComponent; -// @public -export type FieldState = ComponentState; +// @public (undocumented) +export const inputFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type InputFieldProps = FieldProps; // @public -export const renderField_unstable: (state: FieldState) => JSX.Element; +export const renderField_unstable: (state: FieldState) => JSX.Element; // @public -export const useField_unstable: (props: FieldProps, ref: React_2.Ref) => FieldState; +export const useField_unstable: (props: Omit>, never> & ("ref" extends keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined> & keyof Exclude>, (T extends React_2.ComponentType ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> ? (Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined>, keyof Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined> & "ref"> : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined>) | (Exclude>, (T extends React_2.ComponentType ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit>, (T extends React_2.ComponentType ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude>, (T extends React_2.ComponentType ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) : Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never, SlotShorthandValue | null | undefined> | Exclude>, (T extends React_2.ComponentType ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { + children?: unknown; +} ? Props : Props & { + children?: undefined; +}) & { + children?: (Props extends { + children?: unknown; + } ? Props : Props & { + children?: undefined; + })["children"] | SlotRenderFunction | undefined; +} : never)["children"]>, SlotShorthandValue | null | undefined>) & { + fieldOrientation?: "vertical" | "horizontal" | undefined; + status?: "error" | "warning" | "success" | undefined; +} & OptionalFieldComponentProps, ref: React_2.Ref, params: FieldParams) => FieldState; // @public -export const useFieldStyles_unstable: (state: FieldState) => FieldState; +export const useFieldStyles_unstable: (state: FieldState) => void; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-field/package.json b/packages/react-components/react-field/package.json index 3a0b6f8773d85..3c8f8bfcdf943 100644 --- a/packages/react-components/react-field/package.json +++ b/packages/react-components/react-field/package.json @@ -32,6 +32,18 @@ "@fluentui/scripts": "^1.0.0" }, "dependencies": { + "@fluentui/react-checkbox": "^9.0.4", + "@fluentui/react-combobox": "^9.0.0-beta.8", + "@fluentui/react-context-selector": "^9.0.2", + "@fluentui/react-icons": "^2.0.175", + "@fluentui/react-input": "^9.0.4", + "@fluentui/react-label": "^9.0.4", + "@fluentui/react-radio": "^9.0.4", + "@fluentui/react-select": "9.0.0-beta.8", + "@fluentui/react-slider": "^9.0.3", + "@fluentui/react-spinbutton": "^9.0.0", + "@fluentui/react-switch": "^9.0.4", + "@fluentui/react-textarea": "^9.0.4", "@fluentui/react-theme": "^9.0.0", "@fluentui/react-utilities": "^9.0.2", "@griffel/react": "^1.3.0", diff --git a/packages/react-components/react-field/src/InputField.ts b/packages/react-components/react-field/src/InputField.ts new file mode 100644 index 0000000000000..a6548ce9f2f9d --- /dev/null +++ b/packages/react-components/react-field/src/InputField.ts @@ -0,0 +1 @@ +export * from './components/InputField/index'; diff --git a/packages/react-components/react-field/src/components/Field/Field.test.tsx b/packages/react-components/react-field/src/components/Field/Field.test.tsx deleted file mode 100644 index 7f09ee09b58c6..0000000000000 --- a/packages/react-components/react-field/src/components/Field/Field.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { Field } from './Field'; -import { isConformant } from '../../common/isConformant'; - -describe('Field', () => { - isConformant({ - Component: Field, - displayName: 'Field', - }); - - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - - it('renders a default state', () => { - const result = render(Default Field); - expect(result.container).toMatchSnapshot(); - }); -}); diff --git a/packages/react-components/react-field/src/components/Field/Field.tsx b/packages/react-components/react-field/src/components/Field/Field.tsx deleted file mode 100644 index 46d8d9cfc9629..0000000000000 --- a/packages/react-components/react-field/src/components/Field/Field.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useField_unstable } from './useField'; -import { renderField_unstable } from './renderField'; -import { useFieldStyles_unstable } from './useFieldStyles'; -import type { FieldProps } from './Field.types'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; - -/** - * Field component - TODO: add more docs - */ -export const Field: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useField_unstable(props, ref); - - useFieldStyles_unstable(state); - return renderField_unstable(state); -}); - -Field.displayName = 'Field'; diff --git a/packages/react-components/react-field/src/components/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts index 949bf781eb4ff..217b815228336 100644 --- a/packages/react-components/react-field/src/components/Field/Field.types.ts +++ b/packages/react-components/react-field/src/components/Field/Field.types.ts @@ -1,17 +1,100 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import * as React from 'react'; +import { Label } from '@fluentui/react-label'; +import type { ComponentProps, ComponentState, Slot, SlotClassNames } from '@fluentui/react-utilities'; +import type { SlotComponent } from './SlotComponent.types'; -export type FieldSlots = { - root: Slot<'div'>; +/** + * The minimum requirement for a component used by Field. + * + * Note: the use of VoidFunctionComponent means that component is not *required* to have a children prop, + * but it is still allowed to have a children prop. + */ +export type FieldComponent = React.VoidFunctionComponent< + Pick< + React.HTMLAttributes, + 'id' | 'className' | 'style' | 'aria-labelledby' | 'aria-describedby' | 'aria-invalid' | 'aria-errormessage' + > +>; + +/** + * Slots added by Field + */ +export type FieldSlots = { + root: NonNullable>; + + /** + * The underlying component wrapped by this field. + * + * This is the PRIMARY slot: all intrinsic HTML properties will be applied to this slot, + * except `className` and `style`, which remain on the root slot. + */ + fieldComponent: SlotComponent; + + /** + * The label associated with the field. + */ + label?: SlotComponent; + + /** + * A status or validation message. The appearance of the statusText depends on the value of the `status` prop. + */ + statusText?: Slot<'span'>; + + /** + * The icon associated with the status. If the `status` prop is set, this will default to a corresponding icon. + * + * This will only be displayed if `statusText` is set. + */ + statusIcon?: Slot<'span'>; + + /** + * Additional text below the field. + */ + helperText?: Slot<'span'>; }; /** * Field Props */ -export type FieldProps = ComponentProps & {}; +export type FieldProps = ComponentProps>, 'fieldComponent'> & { + /** + * The orientation of the label relative to the field component. + * This only affects the label, and not the statusText or helperText (which always appear below the field component). + * + * @default vertical + */ + fieldOrientation?: 'vertical' | 'horizontal'; + + /** + * The status affects the color of the statusText, the statusIcon, and for some field components, an error status + * causes the border to become red. + * + * @default undefined + */ + status?: 'error' | 'warning' | 'success'; +}; + +/** + * Props that are supported by Field, but not required to be supported by the component that implements field. + */ +export type OptionalFieldComponentProps = { + /** + * Whether the field label should be marked as required. + */ + required?: boolean; + + /** + * Size of the field label. + * + * Number sizes will be ignored, but are allowed because the HTML `` element has a prop `size?: number`. + */ + size?: 'small' | 'medium' | 'large' | number; +}; /** * State used in rendering Field */ -export type FieldState = ComponentState; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from FieldProps. -// & Required> +export type FieldState = ComponentState>> & + Pick, 'fieldOrientation' | 'status'> & { + classNames: SlotClassNames>; + }; diff --git a/packages/react-components/react-field/src/components/Field/SlotComponent.types.ts b/packages/react-components/react-field/src/components/Field/SlotComponent.types.ts new file mode 100644 index 0000000000000..9b90a044a10a6 --- /dev/null +++ b/packages/react-components/react-field/src/components/Field/SlotComponent.types.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import type { SlotShorthandValue, SlotRenderFunction } from '@fluentui/react-utilities'; + +// +// TEMPORARY definition of the SlotComponent type, until it is available from react-utilities +// + +export type SlotComponent = WithSlotShorthandValue< + Type extends React.ComponentType + ? // If this is a VoidFunctionComponent that doesn't allow children, add { children?: never } + WithSlotRenderFunction + : never +>; + +// +// TEMPORARY copied versions of the non-exported helper types from react-utilities +// + +type WithSlotShorthandValue = + | Props + | Extract; + +type WithSlotRenderFunction = Props & { + children?: Props['children'] | SlotRenderFunction; +}; diff --git a/packages/react-components/react-field/src/components/Field/__snapshots__/Field.test.tsx.snap b/packages/react-components/react-field/src/components/Field/__snapshots__/Field.test.tsx.snap deleted file mode 100644 index 7126776f33687..0000000000000 --- a/packages/react-components/react-field/src/components/Field/__snapshots__/Field.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Field renders a default state 1`] = ` -
-
- Default Field -
-
-`; diff --git a/packages/react-components/react-field/src/components/Field/index.ts b/packages/react-components/react-field/src/components/Field/index.ts index 32e3d5f549d7c..e2b74fd17a285 100644 --- a/packages/react-components/react-field/src/components/Field/index.ts +++ b/packages/react-components/react-field/src/components/Field/index.ts @@ -1,4 +1,3 @@ -export * from './Field'; export * from './Field.types'; export * from './renderField'; export * from './useField'; diff --git a/packages/react-components/react-field/src/components/Field/renderField.tsx b/packages/react-components/react-field/src/components/Field/renderField.tsx index 858515b2b4fbb..13b0860a83f18 100644 --- a/packages/react-components/react-field/src/components/Field/renderField.tsx +++ b/packages/react-components/react-field/src/components/Field/renderField.tsx @@ -1,13 +1,25 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; -import type { FieldState, FieldSlots } from './Field.types'; +import type { FieldComponent, FieldSlots, FieldState } from './Field.types'; /** * Render the final JSX of Field */ -export const renderField_unstable = (state: FieldState) => { - const { slots, slotProps } = getSlots(state); +export const renderField_unstable = (state: FieldState) => { + const { slots, slotProps } = getSlots>(state as FieldState); - // TODO Add additional slots in the appropriate place - return ; + return ( + + {slots.label && } + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {slots.fieldComponent && } + {slots.statusText && ( + + {slots.statusIcon && } + {slotProps.statusText.children} + + )} + {slots.helperText && } + + ); }; diff --git a/packages/react-components/react-field/src/components/Field/useField.ts b/packages/react-components/react-field/src/components/Field/useField.ts deleted file mode 100644 index f5f26387f6def..0000000000000 --- a/packages/react-components/react-field/src/components/Field/useField.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; -import type { FieldProps, FieldState } from './Field.types'; - -/** - * Create the state required to render Field. - * - * The returned state can be modified with hooks such as useFieldStyles_unstable, - * before being passed to renderField_unstable. - * - * @param props - props from this instance of Field - * @param ref - reference to root HTMLElement of Field - */ -export const useField_unstable = (props: FieldProps, ref: React.Ref): FieldState => { - 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-field/src/components/Field/useField.tsx b/packages/react-components/react-field/src/components/Field/useField.tsx new file mode 100644 index 0000000000000..6dee9429b0f01 --- /dev/null +++ b/packages/react-components/react-field/src/components/Field/useField.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import type { FieldComponent, FieldProps, FieldSlots, FieldState, OptionalFieldComponentProps } from './Field.types'; +import { CheckmarkCircle12Filled, ErrorCircle12Filled, Warning12Filled } from '@fluentui/react-icons'; +import { Label } from '@fluentui/react-label'; +import { getNativeElementProps, resolveShorthand, SlotClassNames, useId } from '@fluentui/react-utilities'; + +const statusIcons = { + error: , + warning: , + success: , +} as const; + +/** + * Merge two possibly-undefined IDs for aria-describedby. If both IDs are defined, combines + * them into a string separated by a space. Otherwise, returns just the defined ID (if any). + */ +const mergeAriaDescribedBy = (a?: string, b?: string) => (a && b ? `${a} ${b}` : a || b); + +/** + * Partition the props used by the Field itself, from the props that are passed to the underlying field component. + */ +export const getPartitionedFieldProps = >(props: Props) => { + const { + className, + fieldComponent, + fieldOrientation, + helperText, + label, + root, + status, + statusIcon, + statusText, + style, + ...restOfProps + } = props; + + const fieldProps = { + className, + fieldComponent, + fieldOrientation, + helperText, + label, + root, + status, + statusIcon, + statusText, + style, + }; + + return [fieldProps, restOfProps] as const; +}; + +export type FieldParams = { + /** + * The underlying input component that this field is wrapping. + */ + fieldComponent: T; + + /** + * Class names for this component, created by `getFieldClassNames`. + */ + classNames: SlotClassNames>; +}; + +/** + * Create the state required to render Field. + * + * The returned state can be modified with hooks such as useFieldStyles_unstable, + * before being passed to renderField_unstable. + * + * @param props - props from this instance of Field + * @param ref - reference to root HTMLElement of Field + * @param params - Configuration parameters for this Field + */ +export const useField_unstable = ( + props: FieldProps & OptionalFieldComponentProps, + ref: React.Ref, + params: FieldParams, +): FieldState => { + const [fieldProps, componentProps] = getPartitionedFieldProps(props); + + const baseId = useId('field-'); + + const { fieldOrientation = 'vertical', status } = fieldProps; + + const root = resolveShorthand(fieldProps.root, { + required: true, + defaultProps: getNativeElementProps('div', fieldProps), + }); + + const label = resolveShorthand(fieldProps.label, { + defaultProps: { + id: baseId + '__label', + required: componentProps.required, + size: typeof componentProps.size === 'string' ? componentProps.size : undefined, + // htmlFor is set below + }, + }); + + const statusText = resolveShorthand(fieldProps.statusText, { + defaultProps: { + id: baseId + '__statusText', + }, + }); + + const helperText = resolveShorthand(fieldProps.helperText, { + defaultProps: { + id: baseId + '__helperText', + }, + }); + + const statusIcon = resolveShorthand(fieldProps.statusIcon, { + required: !!status, + defaultProps: { + children: status ? statusIcons[status] : undefined, + }, + }); + + const fieldComponent = resolveShorthand(fieldProps.fieldComponent, { + required: true, + defaultProps: { + ref, + 'aria-labelledby': label?.id, + 'aria-describedby': status !== 'error' ? mergeAriaDescribedBy(statusText?.id, helperText?.id) : helperText?.id, + 'aria-errormessage': status === 'error' ? statusText?.id : undefined, + 'aria-invalid': status === 'error' ? true : undefined, + ...componentProps, + }, + }); + + if (label && !label.htmlFor) { + if (!fieldComponent.id) { + fieldComponent.id = baseId + '__fieldComponent'; + } + label.htmlFor = fieldComponent.id; + } + + const state: FieldState = { + fieldOrientation, + status, + classNames: params.classNames, + components: { + root: 'div', + fieldComponent: params.fieldComponent, + label: Label, + statusText: 'span', + statusIcon: 'span', + helperText: 'span', + }, + root, + fieldComponent, + label, + statusIcon, + statusText, + helperText, + }; + + return state as FieldState; +}; diff --git a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts index 9efd955895889..580b1dc349add 100644 --- a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts +++ b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts @@ -1,34 +1,144 @@ import { makeStyles, mergeClasses } from '@griffel/react'; -import type { FieldSlots, FieldState } from './Field.types'; +import type { FieldComponent, FieldProps, FieldSlots, FieldState } from './Field.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { tokens, typographyStyles } from '@fluentui/react-theme'; -export const fieldClassName = 'fui-Field'; -export const fieldClassNames: SlotClassNames = { - root: 'fui-Field', - // TODO: add class names for all slots on FieldSlots. - // Should be of the form `: 'fui-Field__` -}; +export const getFieldClassNames = (name: string): SlotClassNames> => ({ + root: `fui-${name}`, + fieldComponent: `fui-${name}__fieldComponent`, + label: `fui-${name}__label`, + statusText: `fui-${name}__statusText`, + statusIcon: `fui-${name}__statusIcon`, + helperText: `fui-${name}__helperText`, +}); /** * Styles for the root slot */ -const useStyles = makeStyles({ - root: { - // TODO Add default styles for the root element +const useRootStyles = makeStyles({ + base: { + display: 'inline-grid', + gridAutoFlow: 'row', + justifyItems: 'start', + }, + + horizontal: { + gridTemplateRows: 'auto auto auto auto', + gridTemplateColumns: '1fr 2fr', + }, + + secondColumn: { + gridColumnStart: '2', + }, +}); + +const useLabelStyles = makeStyles({ + base: { + marginTop: tokens.spacingVerticalXXS, + marginBottom: tokens.spacingVerticalXXS, + }, + + horizontal: { + gridRowStart: '1', + gridRowEnd: '-1', + marginRight: tokens.spacingHorizontalM, + alignSelf: 'start', + justifySelf: 'stretch', + }, +}); + +const useSecondaryTextStyles = makeStyles({ + base: { + marginTop: tokens.spacingVerticalXXS, + color: tokens.colorNeutralForeground3, + ...typographyStyles.caption1, }, - // TODO add additional classes for different states and/or slots + error: { + color: tokens.colorPaletteRedForeground1, + }, +}); + +const useStatusIconStyles = makeStyles({ + base: { + fontSize: '12px', + lineHeight: '12px', + verticalAlign: 'middle', + marginRight: tokens.spacingHorizontalXS, + }, + + error: { + color: tokens.colorPaletteRedForeground1, + }, + warning: { + color: tokens.colorPaletteDarkOrangeForeground1, + }, + success: { + color: tokens.colorPaletteGreenForeground1, + }, }); /** * Apply styling to the Field slots based on the state */ -export const useFieldStyles_unstable = (state: FieldState): FieldState => { - const styles = useStyles(); - state.root.className = mergeClasses(fieldClassName, styles.root, state.root.className); +export const useFieldStyles_unstable = (state: FieldState) => { + const classNames = state.classNames; + const status: FieldProps['status'] = state.status; + const horizontal = state.fieldOrientation === 'horizontal'; + + const rootStyles = useRootStyles(); + state.root.className = mergeClasses( + classNames.root, + rootStyles.base, + horizontal && rootStyles.horizontal, + state.root.className, + ); + + if (state.fieldComponent) { + state.fieldComponent.className = mergeClasses( + classNames.fieldComponent, + horizontal && rootStyles.secondColumn, + state.fieldComponent.className, + ); + } + + const labelStyles = useLabelStyles(); + if (state.label) { + state.label.className = mergeClasses( + classNames.label, + labelStyles.base, + horizontal && labelStyles.horizontal, + state.label.className, + ); + } + + const statusIconStyles = useStatusIconStyles(); + if (state.statusIcon) { + state.statusIcon.className = mergeClasses( + classNames.statusIcon, + statusIconStyles.base, + !!status && statusIconStyles[status], + state.statusIcon.className, + ); + } - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + const secondaryTextStyles = useSecondaryTextStyles(); + if (state.statusText) { + state.statusText.className = mergeClasses( + classNames.statusText, + secondaryTextStyles.base, + horizontal && rootStyles.secondColumn, + status === 'error' && secondaryTextStyles.error, + state.statusText.className, + ); + } - return state; + if (state.helperText) { + state.helperText.className = mergeClasses( + classNames.helperText, + secondaryTextStyles.base, + horizontal && rootStyles.secondColumn, + state.helperText.className, + ); + } }; diff --git a/packages/react-components/react-field/src/components/InputField/InputField.test.tsx b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx new file mode 100644 index 0000000000000..d748b5840bf6d --- /dev/null +++ b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { InputField } from './InputField'; + +describe('InputField', () => { + isConformant({ + Component: InputField, + displayName: 'InputField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + status: 'error', + statusText: 'statusText', + helperText: 'helperText', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/InputField/InputField.tsx b/packages/react-components/react-field/src/components/InputField/InputField.tsx new file mode 100644 index 0000000000000..b283c5b81201e --- /dev/null +++ b/packages/react-components/react-field/src/components/InputField/InputField.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Input } from '@fluentui/react-input'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + getFieldClassNames, + FieldProps, + renderField_unstable, + useFieldStyles_unstable, + useField_unstable, +} from '../../Field'; + +export const inputFieldClassNames = getFieldClassNames('InputField'); + +export type InputFieldProps = FieldProps; + +export const InputField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable(props, ref, { fieldComponent: Input, classNames: inputFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +InputField.displayName = 'InputField'; diff --git a/packages/react-components/react-field/src/components/InputField/__snapshots__/InputField.test.tsx.snap b/packages/react-components/react-field/src/components/InputField/__snapshots__/InputField.test.tsx.snap new file mode 100644 index 0000000000000..36bfbad27f7ea --- /dev/null +++ b/packages/react-components/react-field/src/components/InputField/__snapshots__/InputField.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InputField renders a default state 1`] = ` +
+
+ + + +
+
+`; diff --git a/packages/react-components/react-field/src/components/InputField/index.ts b/packages/react-components/react-field/src/components/InputField/index.ts new file mode 100644 index 0000000000000..9cd7022484cce --- /dev/null +++ b/packages/react-components/react-field/src/components/InputField/index.ts @@ -0,0 +1 @@ +export * from './InputField'; diff --git a/packages/react-components/react-field/src/index.ts b/packages/react-components/react-field/src/index.ts index f40e2c50f7b06..86a5e43a3e35d 100644 --- a/packages/react-components/react-field/src/index.ts +++ b/packages/react-components/react-field/src/index.ts @@ -1,3 +1,5 @@ -// TODO: replace with real exports -export {}; -export * from './Field'; +export { renderField_unstable, useFieldStyles_unstable, useField_unstable } from './Field'; +export type { FieldProps, FieldSlots, FieldState } from './Field'; + +export { InputField, inputFieldClassNames } from './InputField'; +export type { InputFieldProps } from './InputField'; diff --git a/packages/react-components/react-field/src/stories/Field/FieldDefault.stories.tsx b/packages/react-components/react-field/src/stories/Field/FieldDefault.stories.tsx deleted file mode 100644 index b8a93a1dfb9f6..0000000000000 --- a/packages/react-components/react-field/src/stories/Field/FieldDefault.stories.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from 'react'; -import { Field, FieldProps } from '@fluentui/react-field'; - -export const Default = (props: Partial) => ; diff --git a/packages/react-components/react-field/src/stories/Field/FieldDescription.md b/packages/react-components/react-field/src/stories/Field/FieldDescription.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/packages/react-components/react-field/src/stories/Field/index.stories.tsx b/packages/react-components/react-field/src/stories/Field/index.stories.tsx deleted file mode 100644 index 650828caa907f..0000000000000 --- a/packages/react-components/react-field/src/stories/Field/index.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Field } from '@fluentui/react-field'; - -import descriptionMd from './FieldDescription.md'; -import bestPracticesMd from './FieldBestPractices.md'; - -export { Default } from './FieldDefault.stories'; - -export default { - title: 'Components/Field', - component: Field, - parameters: { - docs: { - description: { - component: [descriptionMd, bestPracticesMd].join('\n'), - }, - }, - }, -}; diff --git a/packages/react-components/react-field/src/stories/Field/FieldBestPractices.md b/packages/react-components/react-field/src/stories/InputField/InputFieldBestPractices.md similarity index 100% rename from packages/react-components/react-field/src/stories/Field/FieldBestPractices.md rename to packages/react-components/react-field/src/stories/InputField/InputFieldBestPractices.md diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx new file mode 100644 index 0000000000000..51b9bd937bc95 --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { InputField, InputFieldProps } from '@fluentui/react-field'; + +export const Default = (props: Partial) => ( + +); diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md new file mode 100644 index 0000000000000..1c0dac8d1fd9c --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md @@ -0,0 +1,3 @@ +InputField is a combination of Label, an Input, and status and helper text. + +InputField does not handle input validation, but it does allow a validation status message to be displayed. diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx new file mode 100644 index 0000000000000..475465b6ca10a --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { InputField } from '@fluentui/react-field'; + +export const HelperText = () => ( + +); + +HelperText.parameters = { + docs: { + description: { + story: 'Helper text provides additional descriptive information about the field', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx new file mode 100644 index 0000000000000..d5ce567dfe606 --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import { InputField } from '@fluentui/react-field'; + +const useStyles = makeStyles({ + stack: { + display: 'inline-grid', + rowGap: tokens.spacingVerticalM, + width: '400px', + }, +}); + +export const Horizontal = () => { + const styles = useStyles(); + return ( +
+ + + + +
+ ); +}; + +Horizontal.storyName = 'Field orientation: horizontal'; +Horizontal.parameters = { + docs: { + description: { + story: + 'The field can have a horizontal orientation. If multiple fields are stacked together and all the same ' + + 'width, the inputs will be vertically aligned as well.', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldLabel.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldLabel.stories.tsx new file mode 100644 index 0000000000000..e943bf7f39d5e --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldLabel.stories.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { InputField } from '@fluentui/react-field'; + +export const Label = () => ; + +Label.parameters = { + docs: { + description: { + story: 'The field label is placed above the field component by default.', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldRequired.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldRequired.stories.tsx new file mode 100644 index 0000000000000..2259f02bc7997 --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldRequired.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { InputField } from '@fluentui/react-field'; + +export const Required = () => ; + +Required.parameters = { + docs: { + description: { + story: + 'When a field is marked as `required`, the label has a red asterisk, ' + + 'and the input gets the required property for accessiblity tools.', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldSize.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldSize.stories.tsx new file mode 100644 index 0000000000000..7ad08b50c362d --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldSize.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import { InputField } from '@fluentui/react-field'; + +const useStyles = makeStyles({ + stack: { + display: 'inline-grid', + rowGap: tokens.spacingVerticalM, + width: '400px', + }, +}); + +export const Size = () => { + const styles = useStyles(); + return ( +
+ + + +
+ ); +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx new file mode 100644 index 0000000000000..66719035b7512 --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import { InputField } from '@fluentui/react-field'; +import { SparkleFilled } from '@fluentui/react-icons'; + +const useStyles = makeStyles({ + stack: { + display: 'inline-grid', + rowGap: tokens.spacingVerticalM, + width: '400px', + }, +}); + +export const Status = () => { + const styles = useStyles(); + return ( +
+ + + + } + statusText="This status message has a custom icon" + fieldOrientation="horizontal" + /> +
+ ); +}; + +Status.parameters = { + docs: { + description: { + story: + 'The `status` property modifies the appearance of the status text, and for some input types, ' + + 'an error status also applies visual indication such as a red border.' + + '
' + + 'Use the `statusText` property to display an associated message. ' + + 'You can optionally override the default icon with `statusIcon`.', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/index.stories.tsx b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx new file mode 100644 index 0000000000000..6cc1bc9f327ce --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx @@ -0,0 +1,24 @@ +import { InputField } from '@fluentui/react-field'; + +import descriptionMd from './InputFieldDescription.md'; +import bestPracticesMd from './InputFieldBestPractices.md'; + +export { Default } from './InputFieldDefault.stories'; +export { Label } from './InputFieldLabel.stories'; +export { Horizontal } from './InputFieldHorizontal.stories'; +export { Required } from './InputFieldRequired.stories'; +export { Status } from './InputFieldStatus.stories'; +export { Size } from './InputFieldSize.stories'; +export { HelperText } from './InputFieldHelperText.stories'; + +export default { + title: 'Components/Field/InputField', + component: InputField, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + }, + }, + }, +}; From 84ff5e970b32621ed1f8715c57ed4bf6ff114280 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Tue, 16 Aug 2022 17:45:47 -0700 Subject: [PATCH 02/22] Revert some dependencies in react-field for now --- packages/react-components/react-field/package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/react-components/react-field/package.json b/packages/react-components/react-field/package.json index 3c8f8bfcdf943..3e756c1fa68e3 100644 --- a/packages/react-components/react-field/package.json +++ b/packages/react-components/react-field/package.json @@ -32,18 +32,10 @@ "@fluentui/scripts": "^1.0.0" }, "dependencies": { - "@fluentui/react-checkbox": "^9.0.4", - "@fluentui/react-combobox": "^9.0.0-beta.8", "@fluentui/react-context-selector": "^9.0.2", "@fluentui/react-icons": "^2.0.175", "@fluentui/react-input": "^9.0.4", "@fluentui/react-label": "^9.0.4", - "@fluentui/react-radio": "^9.0.4", - "@fluentui/react-select": "9.0.0-beta.8", - "@fluentui/react-slider": "^9.0.3", - "@fluentui/react-spinbutton": "^9.0.0", - "@fluentui/react-switch": "^9.0.4", - "@fluentui/react-textarea": "^9.0.4", "@fluentui/react-theme": "^9.0.0", "@fluentui/react-utilities": "^9.0.2", "@griffel/react": "^1.3.0", From 4189e9a958a6bb0d24ebe5abaa67e56f560871b5 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 18 Aug 2022 16:36:24 -0700 Subject: [PATCH 03/22] Rename fieldOrientation to orientation, and add UseFieldParams argument to useField --- .../react-field/etc/react-field.api.md | 461 +----------------- .../src/components/Field/Field.types.ts | 10 +- .../src/components/Field/useField.tsx | 55 ++- .../src/components/Field/useFieldStyles.ts | 2 +- .../src/components/InputField/InputField.tsx | 15 +- .../react-components/react-field/src/index.ts | 4 +- .../InputFieldHorizontal.stories.tsx | 8 +- .../InputField/InputFieldStatus.stories.tsx | 13 +- 8 files changed, 75 insertions(+), 493 deletions(-) diff --git a/packages/react-components/react-field/etc/react-field.api.md b/packages/react-components/react-field/etc/react-field.api.md index 4aec5f4918bc5..b170e60391396 100644 --- a/packages/react-components/react-field/etc/react-field.api.md +++ b/packages/react-components/react-field/etc/react-field.api.md @@ -14,12 +14,12 @@ import { Label } from '@fluentui/react-label'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import { SlotClassNames } from '@fluentui/react-utilities'; -import { SlotRenderFunction } from '@fluentui/react-utilities'; -import { SlotShorthandValue } from '@fluentui/react-utilities'; +import type { SlotRenderFunction } from '@fluentui/react-utilities'; +import type { SlotShorthandValue } from '@fluentui/react-utilities'; // @public export type FieldProps = ComponentProps>, 'fieldComponent'> & { - fieldOrientation?: 'vertical' | 'horizontal'; + orientation?: 'vertical' | 'horizontal'; status?: 'error' | 'warning' | 'success'; }; @@ -27,17 +27,20 @@ export type FieldProps = ComponentProps = { root: NonNullable>; fieldComponent: SlotComponent; - label?: SlotComponent; + label?: Slot; statusText?: Slot<'span'>; statusIcon?: Slot<'span'>; helperText?: Slot<'span'>; }; // @public -export type FieldState = ComponentState>> & Pick, 'fieldOrientation' | 'status'> & { +export type FieldState = ComponentState>> & Pick, 'orientation' | 'status'> & { classNames: SlotClassNames>; }; +// @public (undocumented) +export const getFieldClassNames: (name: string) => SlotClassNames>; + // @public (undocumented) export const InputField: ForwardRefComponent; @@ -51,444 +54,16 @@ export type InputFieldProps = FieldProps; export const renderField_unstable: (state: FieldState) => JSX.Element; // @public -export const useField_unstable: (props: Omit>, never> & ("ref" extends keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined> & keyof Exclude>, (T extends React_2.ComponentType ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> & keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> ? (Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined>, keyof Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined> & "ref"> : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined>) | (Exclude>, (T extends React_2.ComponentType ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit>, (T extends React_2.ComponentType ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude>, (T extends React_2.ComponentType ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) | (Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> extends unknown ? Omit ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>, "ref"> : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) : Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never, SlotShorthandValue | null | undefined> | Exclude>, (T extends React_2.ComponentType ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined> | Exclude ? (Props extends { - children?: unknown; -} ? Props : Props & { - children?: undefined; -}) & { - children?: (Props extends { - children?: unknown; - } ? Props : Props & { - children?: undefined; - })["children"] | SlotRenderFunction | undefined; -} : never)["children"]>, SlotShorthandValue | null | undefined>) & { - fieldOrientation?: "vertical" | "horizontal" | undefined; - status?: "error" | "warning" | "success" | undefined; -} & OptionalFieldComponentProps, ref: React_2.Ref, params: FieldParams) => FieldState; +export const useField_unstable: (params: UseFieldParams) => FieldState; + +// @public (undocumented) +export type UseFieldParams = { + props: FieldProps & OptionalFieldComponentProps; + ref: React_2.Ref; + fieldComponent: T; + classNames: SlotClassNames>; + labelConnection?: 'htmlFor' | 'aria-labelledby'; +}; // @public export const useFieldStyles_unstable: (state: FieldState) => void; diff --git a/packages/react-components/react-field/src/components/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts index 217b815228336..1e03041485058 100644 --- a/packages/react-components/react-field/src/components/Field/Field.types.ts +++ b/packages/react-components/react-field/src/components/Field/Field.types.ts @@ -33,7 +33,7 @@ export type FieldSlots = { /** * The label associated with the field. */ - label?: SlotComponent; + label?: Slot; /** * A status or validation message. The appearance of the statusText depends on the value of the `status` prop. @@ -63,11 +63,11 @@ export type FieldProps = ComponentProps = ComponentState>> & - Pick, 'fieldOrientation' | 'status'> & { + Pick, 'orientation' | 'status'> & { classNames: SlotClassNames>; }; diff --git a/packages/react-components/react-field/src/components/Field/useField.tsx b/packages/react-components/react-field/src/components/Field/useField.tsx index 6dee9429b0f01..a695843f82976 100644 --- a/packages/react-components/react-field/src/components/Field/useField.tsx +++ b/packages/react-components/react-field/src/components/Field/useField.tsx @@ -23,9 +23,9 @@ export const getPartitionedFieldProps = = { +export type UseFieldParams = { + /** + * Props passed to this Field + */ + props: FieldProps & OptionalFieldComponentProps; + + /** + * Ref to the underlying fieldComponent + */ + ref: React.Ref; + /** * The underlying input component that this field is wrapping. */ @@ -60,6 +70,17 @@ export type FieldParams = { * Class names for this component, created by `getFieldClassNames`. */ classNames: SlotClassNames>; + + /** + * How the label be connected to the fieldComponent. + * * htmlFor - Set the Label's htmlFor prop to the component's ID (and generate an ID if not provided). + * This is the preferred method for components that use the underlying tag. + * * aria-labelledby - Set the component's aria-labelledby prop to the Label's ID. Use this for components + * that are not directly elements (such as RadioGroup). + * + * @default htmlFor + */ + labelConnection?: 'htmlFor' | 'aria-labelledby'; }; /** @@ -68,20 +89,14 @@ export type FieldParams = { * The returned state can be modified with hooks such as useFieldStyles_unstable, * before being passed to renderField_unstable. * - * @param props - props from this instance of Field - * @param ref - reference to root HTMLElement of Field * @param params - Configuration parameters for this Field */ -export const useField_unstable = ( - props: FieldProps & OptionalFieldComponentProps, - ref: React.Ref, - params: FieldParams, -): FieldState => { - const [fieldProps, componentProps] = getPartitionedFieldProps(props); +export const useField_unstable = (params: UseFieldParams): FieldState => { + const [fieldProps, componentProps] = getPartitionedFieldProps(params.props); const baseId = useId('field-'); - const { fieldOrientation = 'vertical', status } = fieldProps; + const { orientation = 'vertical', status } = fieldProps; const root = resolveShorthand(fieldProps.root, { required: true, @@ -116,11 +131,16 @@ export const useField_unstable = ( }, }); + const { labelConnection = 'htmlFor' } = params; + const fieldComponent = resolveShorthand(fieldProps.fieldComponent, { required: true, defaultProps: { - ref, - 'aria-labelledby': label?.id, + ref: params.ref, + // Add a default ID only if required for label's htmlFor prop + id: label && labelConnection === 'htmlFor' ? baseId + '__fieldComponent' : undefined, + // Add aria-labelledby only if not using the label's htmlFor + 'aria-labelledby': labelConnection !== 'htmlFor' ? label?.id : undefined, 'aria-describedby': status !== 'error' ? mergeAriaDescribedBy(statusText?.id, helperText?.id) : helperText?.id, 'aria-errormessage': status === 'error' ? statusText?.id : undefined, 'aria-invalid': status === 'error' ? true : undefined, @@ -128,15 +148,12 @@ export const useField_unstable = ( }, }); - if (label && !label.htmlFor) { - if (!fieldComponent.id) { - fieldComponent.id = baseId + '__fieldComponent'; - } + if (labelConnection === 'htmlFor' && label && !label.htmlFor) { label.htmlFor = fieldComponent.id; } const state: FieldState = { - fieldOrientation, + orientation, status, classNames: params.classNames, components: { diff --git a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts index 580b1dc349add..3f10d3427eeb8 100644 --- a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts +++ b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts @@ -84,7 +84,7 @@ const useStatusIconStyles = makeStyles({ export const useFieldStyles_unstable = (state: FieldState) => { const classNames = state.classNames; const status: FieldProps['status'] = state.status; - const horizontal = state.fieldOrientation === 'horizontal'; + const horizontal = state.orientation === 'horizontal'; const rootStyles = useRootStyles(); state.root.className = mergeClasses( diff --git a/packages/react-components/react-field/src/components/InputField/InputField.tsx b/packages/react-components/react-field/src/components/InputField/InputField.tsx index b283c5b81201e..baf7b24e71884 100644 --- a/packages/react-components/react-field/src/components/InputField/InputField.tsx +++ b/packages/react-components/react-field/src/components/InputField/InputField.tsx @@ -1,20 +1,15 @@ import * as React from 'react'; import { Input } from '@fluentui/react-input'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { - getFieldClassNames, - FieldProps, - renderField_unstable, - useFieldStyles_unstable, - useField_unstable, -} from '../../Field'; - -export const inputFieldClassNames = getFieldClassNames('InputField'); +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; export type InputFieldProps = FieldProps; +export const inputFieldClassNames = getFieldClassNames('InputField'); + export const InputField: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useField_unstable(props, ref, { fieldComponent: Input, classNames: inputFieldClassNames }); + const state = useField_unstable({ props, ref, fieldComponent: Input, classNames: inputFieldClassNames }); useFieldStyles_unstable(state); return renderField_unstable(state); }); diff --git a/packages/react-components/react-field/src/index.ts b/packages/react-components/react-field/src/index.ts index 86a5e43a3e35d..9c68f80cd9e54 100644 --- a/packages/react-components/react-field/src/index.ts +++ b/packages/react-components/react-field/src/index.ts @@ -1,5 +1,5 @@ -export { renderField_unstable, useFieldStyles_unstable, useField_unstable } from './Field'; -export type { FieldProps, FieldSlots, FieldState } from './Field'; +export { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from './Field'; +export type { FieldProps, FieldSlots, FieldState, UseFieldParams } from './Field'; export { InputField, inputFieldClassNames } from './InputField'; export type { InputFieldProps } from './InputField'; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx index d5ce567dfe606..615fc1911be83 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx @@ -16,14 +16,14 @@ export const Horizontal = () => {
- - - + + +
); }; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx index 66719035b7512..7b3b4121ab877 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx @@ -15,29 +15,24 @@ export const Status = () => { const styles = useStyles(); return (
- + } statusText="This status message has a custom icon" - fieldOrientation="horizontal" + orientation="horizontal" />
); From 4dba2aed7dd33d1c83ebaf3360b4bef1b11900da Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 18 Aug 2022 16:38:54 -0700 Subject: [PATCH 04/22] Rename helperText to hint --- .../src/components/Field/Field.types.ts | 6 +++--- .../src/components/Field/renderField.tsx | 2 +- .../react-field/src/components/Field/useField.tsx | 14 +++++++------- .../src/components/Field/useFieldStyles.ts | 10 +++++----- .../src/components/InputField/InputField.test.tsx | 2 +- .../InputField/InputFieldDefault.stories.tsx | 2 +- .../stories/InputField/InputFieldDescription.md | 2 +- .../InputField/InputFieldHelperText.stories.tsx | 14 -------------- .../stories/InputField/InputFieldHint.stories.tsx | 12 ++++++++++++ .../InputField/InputFieldHorizontal.stories.tsx | 2 +- .../src/stories/InputField/index.stories.tsx | 2 +- 11 files changed, 33 insertions(+), 35 deletions(-) delete mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldHint.stories.tsx diff --git a/packages/react-components/react-field/src/components/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts index 1e03041485058..0d409453737b5 100644 --- a/packages/react-components/react-field/src/components/Field/Field.types.ts +++ b/packages/react-components/react-field/src/components/Field/Field.types.ts @@ -48,9 +48,9 @@ export type FieldSlots = { statusIcon?: Slot<'span'>; /** - * Additional text below the field. + * Additional hint text below the field. */ - helperText?: Slot<'span'>; + hint?: Slot<'span'>; }; /** @@ -59,7 +59,7 @@ export type FieldSlots = { export type FieldProps = ComponentProps>, 'fieldComponent'> & { /** * The orientation of the label relative to the field component. - * This only affects the label, and not the statusText or helperText (which always appear below the field component). + * This only affects the label, and not the statusText or hint (which always appear below the field component). * * @default vertical */ diff --git a/packages/react-components/react-field/src/components/Field/renderField.tsx b/packages/react-components/react-field/src/components/Field/renderField.tsx index 13b0860a83f18..ae6c1f9f06c27 100644 --- a/packages/react-components/react-field/src/components/Field/renderField.tsx +++ b/packages/react-components/react-field/src/components/Field/renderField.tsx @@ -19,7 +19,7 @@ export const renderField_unstable = (state: FieldState {slotProps.statusText.children} )} - {slots.helperText && } + {slots.hint && }
); }; diff --git a/packages/react-components/react-field/src/components/Field/useField.tsx b/packages/react-components/react-field/src/components/Field/useField.tsx index a695843f82976..e35bb5e8365ae 100644 --- a/packages/react-components/react-field/src/components/Field/useField.tsx +++ b/packages/react-components/react-field/src/components/Field/useField.tsx @@ -23,7 +23,7 @@ export const getPartitionedFieldProps = (params: UseFieldPara }, }); - const helperText = resolveShorthand(fieldProps.helperText, { + const hint = resolveShorthand(fieldProps.hint, { defaultProps: { - id: baseId + '__helperText', + id: baseId + '__hint', }, }); @@ -141,7 +141,7 @@ export const useField_unstable = (params: UseFieldPara id: label && labelConnection === 'htmlFor' ? baseId + '__fieldComponent' : undefined, // Add aria-labelledby only if not using the label's htmlFor 'aria-labelledby': labelConnection !== 'htmlFor' ? label?.id : undefined, - 'aria-describedby': status !== 'error' ? mergeAriaDescribedBy(statusText?.id, helperText?.id) : helperText?.id, + 'aria-describedby': status !== 'error' ? mergeAriaDescribedBy(statusText?.id, hint?.id) : hint?.id, 'aria-errormessage': status === 'error' ? statusText?.id : undefined, 'aria-invalid': status === 'error' ? true : undefined, ...componentProps, @@ -162,14 +162,14 @@ export const useField_unstable = (params: UseFieldPara label: Label, statusText: 'span', statusIcon: 'span', - helperText: 'span', + hint: 'span', }, root, fieldComponent, label, statusIcon, statusText, - helperText, + hint, }; return state as FieldState; diff --git a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts index 3f10d3427eeb8..1a76dcf7bc55e 100644 --- a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts +++ b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts @@ -9,7 +9,7 @@ export const getFieldClassNames = (name: string): SlotClassNames(state: FieldSt ); } - if (state.helperText) { - state.helperText.className = mergeClasses( - classNames.helperText, + if (state.hint) { + state.hint.className = mergeClasses( + classNames.hint, secondaryTextStyles.base, horizontal && rootStyles.secondColumn, - state.helperText.className, + state.hint.className, ); } }; diff --git a/packages/react-components/react-field/src/components/InputField/InputField.test.tsx b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx index d748b5840bf6d..c343d66c06e70 100644 --- a/packages/react-components/react-field/src/components/InputField/InputField.test.tsx +++ b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx @@ -16,7 +16,7 @@ describe('InputField', () => { label: 'label', status: 'error', statusText: 'statusText', - helperText: 'helperText', + hint: 'hint', }, }, ], diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx index 51b9bd937bc95..0671fb7173a2c 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx @@ -6,7 +6,7 @@ export const Default = (props: Partial) => ( label="Example field" status="success" statusText="This is a success message" - helperText="Fields can have helper text, but it should be used sparingly" + hint="Fields can have hint text, but it should be used sparingly" {...props} /> ); diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md index 1c0dac8d1fd9c..e90545bd6c18c 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md @@ -1,3 +1,3 @@ -InputField is a combination of Label, an Input, and status and helper text. +InputField is a combination of Label, an Input, and status and hint text. InputField does not handle input validation, but it does allow a validation status message to be displayed. diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx deleted file mode 100644 index 475465b6ca10a..0000000000000 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldHelperText.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { InputField } from '@fluentui/react-field'; - -export const HelperText = () => ( - -); - -HelperText.parameters = { - docs: { - description: { - story: 'Helper text provides additional descriptive information about the field', - }, - }, -}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHint.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHint.stories.tsx new file mode 100644 index 0000000000000..074c449d53f1b --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHint.stories.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { InputField } from '@fluentui/react-field'; + +export const Hint = () => ; + +Hint.parameters = { + docs: { + description: { + story: 'Hint text provides additional descriptive information about the field', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx index 615fc1911be83..b0a9507444992 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx @@ -19,7 +19,7 @@ export const Horizontal = () => { orientation="horizontal" status="success" statusText="Status text appears below the input" - helperText="Helper text does too" + hint="Hint text does too" /> diff --git a/packages/react-components/react-field/src/stories/InputField/index.stories.tsx b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx index 6cc1bc9f327ce..dd2fa588a2fc9 100644 --- a/packages/react-components/react-field/src/stories/InputField/index.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx @@ -9,7 +9,7 @@ export { Horizontal } from './InputFieldHorizontal.stories'; export { Required } from './InputFieldRequired.stories'; export { Status } from './InputFieldStatus.stories'; export { Size } from './InputFieldSize.stories'; -export { HelperText } from './InputFieldHelperText.stories'; +export { Hint } from './InputFieldHint.stories'; export default { title: 'Components/Field/InputField', From 704089780fe768f0488c0f5e4190fbb8ddc29620 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 18 Aug 2022 16:48:06 -0700 Subject: [PATCH 05/22] Rename status to validationState, statusText to validationMessage, and statusIcon to validationMessageIcon --- .../react-field/etc/react-field.api.md | 10 ++-- .../src/components/Field/Field.types.ts | 21 +++---- .../src/components/Field/renderField.tsx | 10 ++-- .../src/components/Field/useField.tsx | 43 +++++++------- .../src/components/Field/useFieldStyles.ts | 32 +++++------ .../components/InputField/InputField.test.tsx | 4 +- .../InputField/InputFieldDefault.stories.tsx | 4 +- .../InputField/InputFieldDescription.md | 4 +- .../InputFieldHorizontal.stories.tsx | 4 +- .../InputField/InputFieldStatus.stories.tsx | 52 ----------------- .../InputFieldValidationState.stories.tsx | 57 +++++++++++++++++++ .../src/stories/InputField/index.stories.tsx | 2 +- 12 files changed, 125 insertions(+), 118 deletions(-) delete mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx create mode 100644 packages/react-components/react-field/src/stories/InputField/InputFieldValidationState.stories.tsx diff --git a/packages/react-components/react-field/etc/react-field.api.md b/packages/react-components/react-field/etc/react-field.api.md index b170e60391396..73ee0493ce0f1 100644 --- a/packages/react-components/react-field/etc/react-field.api.md +++ b/packages/react-components/react-field/etc/react-field.api.md @@ -20,7 +20,7 @@ import type { SlotShorthandValue } from '@fluentui/react-utilities'; // @public export type FieldProps = ComponentProps>, 'fieldComponent'> & { orientation?: 'vertical' | 'horizontal'; - status?: 'error' | 'warning' | 'success'; + validationState?: 'error' | 'warning' | 'success'; }; // @public @@ -28,13 +28,13 @@ export type FieldSlots = { root: NonNullable>; fieldComponent: SlotComponent; label?: Slot; - statusText?: Slot<'span'>; - statusIcon?: Slot<'span'>; - helperText?: Slot<'span'>; + validationMessage?: Slot<'span'>; + validationMessageIcon?: Slot<'span'>; + hint?: Slot<'span'>; }; // @public -export type FieldState = ComponentState>> & Pick, 'orientation' | 'status'> & { +export type FieldState = ComponentState>> & Pick, 'orientation' | 'validationState'> & { classNames: SlotClassNames>; }; diff --git a/packages/react-components/react-field/src/components/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts index 0d409453737b5..bc4379510361c 100644 --- a/packages/react-components/react-field/src/components/Field/Field.types.ts +++ b/packages/react-components/react-field/src/components/Field/Field.types.ts @@ -36,16 +36,17 @@ export type FieldSlots = { label?: Slot; /** - * A status or validation message. The appearance of the statusText depends on the value of the `status` prop. + * A message about the validation state. The appearance of the `validationMessage` depends on `validationState`. */ - statusText?: Slot<'span'>; + validationMessage?: Slot<'span'>; /** - * The icon associated with the status. If the `status` prop is set, this will default to a corresponding icon. + * The icon associated with the `validationMessage`. If the `validationState` prop is set, this will default to an + * icon corresponding to that state. * - * This will only be displayed if `statusText` is set. + * This will only be displayed if `validationMessage` is set. */ - statusIcon?: Slot<'span'>; + validationMessageIcon?: Slot<'span'>; /** * Additional hint text below the field. @@ -59,19 +60,19 @@ export type FieldSlots = { export type FieldProps = ComponentProps>, 'fieldComponent'> & { /** * The orientation of the label relative to the field component. - * This only affects the label, and not the statusText or hint (which always appear below the field component). + * This only affects the label, and not the validationMessage or hint (which always appear below the field component). * * @default vertical */ orientation?: 'vertical' | 'horizontal'; /** - * The status affects the color of the statusText, the statusIcon, and for some field components, an error - * status causes the border to become red. + * The `validationState` affects the color of the `validationMessage`, the `validationMessageIcon`, and for some + * field components, an `validationState="error"` causes the border to become red. * * @default undefined */ - status?: 'error' | 'warning' | 'success'; + validationState?: 'error' | 'warning' | 'success'; }; /** @@ -95,6 +96,6 @@ export type OptionalFieldComponentProps = { * State used in rendering Field */ export type FieldState = ComponentState>> & - Pick, 'orientation' | 'status'> & { + Pick, 'orientation' | 'validationState'> & { classNames: SlotClassNames>; }; diff --git a/packages/react-components/react-field/src/components/Field/renderField.tsx b/packages/react-components/react-field/src/components/Field/renderField.tsx index ae6c1f9f06c27..0ddadc2905ecf 100644 --- a/packages/react-components/react-field/src/components/Field/renderField.tsx +++ b/packages/react-components/react-field/src/components/Field/renderField.tsx @@ -13,11 +13,11 @@ export const renderField_unstable = (state: FieldState {slots.label && } {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {slots.fieldComponent && } - {slots.statusText && ( - - {slots.statusIcon && } - {slotProps.statusText.children} - + {slots.validationMessage && ( + + {slots.validationMessageIcon && } + {slotProps.validationMessage.children} + )} {slots.hint && } diff --git a/packages/react-components/react-field/src/components/Field/useField.tsx b/packages/react-components/react-field/src/components/Field/useField.tsx index e35bb5e8365ae..25dd3c53898de 100644 --- a/packages/react-components/react-field/src/components/Field/useField.tsx +++ b/packages/react-components/react-field/src/components/Field/useField.tsx @@ -4,7 +4,7 @@ import { CheckmarkCircle12Filled, ErrorCircle12Filled, Warning12Filled } from '@ import { Label } from '@fluentui/react-label'; import { getNativeElementProps, resolveShorthand, SlotClassNames, useId } from '@fluentui/react-utilities'; -const statusIcons = { +const validationMessageIcons = { error: , warning: , success: , @@ -27,10 +27,10 @@ export const getPartitionedFieldProps = (params: UseFieldPara const baseId = useId('field-'); - const { orientation = 'vertical', status } = fieldProps; + const { orientation = 'vertical', validationState } = fieldProps; const root = resolveShorthand(fieldProps.root, { required: true, @@ -112,9 +112,9 @@ export const useField_unstable = (params: UseFieldPara }, }); - const statusText = resolveShorthand(fieldProps.statusText, { + const validationMessage = resolveShorthand(fieldProps.validationMessage, { defaultProps: { - id: baseId + '__statusText', + id: baseId + '__validationMessage', }, }); @@ -124,14 +124,15 @@ export const useField_unstable = (params: UseFieldPara }, }); - const statusIcon = resolveShorthand(fieldProps.statusIcon, { - required: !!status, + const validationMessageIcon = resolveShorthand(fieldProps.validationMessageIcon, { + required: !!validationState, defaultProps: { - children: status ? statusIcons[status] : undefined, + children: validationState ? validationMessageIcons[validationState] : undefined, }, }); const { labelConnection = 'htmlFor' } = params; + const hasError = validationState === 'error'; const fieldComponent = resolveShorthand(fieldProps.fieldComponent, { required: true, @@ -141,9 +142,9 @@ export const useField_unstable = (params: UseFieldPara id: label && labelConnection === 'htmlFor' ? baseId + '__fieldComponent' : undefined, // Add aria-labelledby only if not using the label's htmlFor 'aria-labelledby': labelConnection !== 'htmlFor' ? label?.id : undefined, - 'aria-describedby': status !== 'error' ? mergeAriaDescribedBy(statusText?.id, hint?.id) : hint?.id, - 'aria-errormessage': status === 'error' ? statusText?.id : undefined, - 'aria-invalid': status === 'error' ? true : undefined, + 'aria-describedby': hasError ? hint?.id : mergeAriaDescribedBy(validationMessage?.id, hint?.id), + 'aria-errormessage': hasError ? validationMessage?.id : undefined, + 'aria-invalid': hasError ? true : undefined, ...componentProps, }, }); @@ -154,21 +155,21 @@ export const useField_unstable = (params: UseFieldPara const state: FieldState = { orientation, - status, + validationState, classNames: params.classNames, components: { root: 'div', fieldComponent: params.fieldComponent, label: Label, - statusText: 'span', - statusIcon: 'span', + validationMessage: 'span', + validationMessageIcon: 'span', hint: 'span', }, root, fieldComponent, label, - statusIcon, - statusText, + validationMessageIcon, + validationMessage, hint, }; diff --git a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts index 1a76dcf7bc55e..440ec3256a310 100644 --- a/packages/react-components/react-field/src/components/Field/useFieldStyles.ts +++ b/packages/react-components/react-field/src/components/Field/useFieldStyles.ts @@ -7,8 +7,8 @@ export const getFieldClassNames = (name: string): SlotClassNames(state: FieldState) => { const classNames = state.classNames; - const status: FieldProps['status'] = state.status; + const validationState: FieldProps['validationState'] = state.validationState; const horizontal = state.orientation === 'horizontal'; const rootStyles = useRootStyles(); @@ -112,24 +112,24 @@ export const useFieldStyles_unstable = (state: FieldSt ); } - const statusIconStyles = useStatusIconStyles(); - if (state.statusIcon) { - state.statusIcon.className = mergeClasses( - classNames.statusIcon, - statusIconStyles.base, - !!status && statusIconStyles[status], - state.statusIcon.className, + const validationMessageIconStyles = useValidationMessageIconStyles(); + if (state.validationMessageIcon) { + state.validationMessageIcon.className = mergeClasses( + classNames.validationMessageIcon, + validationMessageIconStyles.base, + !!validationState && validationMessageIconStyles[validationState], + state.validationMessageIcon.className, ); } const secondaryTextStyles = useSecondaryTextStyles(); - if (state.statusText) { - state.statusText.className = mergeClasses( - classNames.statusText, + if (state.validationMessage) { + state.validationMessage.className = mergeClasses( + classNames.validationMessage, secondaryTextStyles.base, horizontal && rootStyles.secondColumn, - status === 'error' && secondaryTextStyles.error, - state.statusText.className, + validationState === 'error' && secondaryTextStyles.error, + state.validationMessage.className, ); } diff --git a/packages/react-components/react-field/src/components/InputField/InputField.test.tsx b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx index c343d66c06e70..ac11d86bb54a1 100644 --- a/packages/react-components/react-field/src/components/InputField/InputField.test.tsx +++ b/packages/react-components/react-field/src/components/InputField/InputField.test.tsx @@ -14,8 +14,8 @@ describe('InputField', () => { { props: { label: 'label', - status: 'error', - statusText: 'statusText', + validationState: 'error', + validationMessage: 'validationMessage', hint: 'hint', }, }, diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx index 0671fb7173a2c..f1ccbda6e2f32 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDefault.stories.tsx @@ -4,8 +4,8 @@ import { InputField, InputFieldProps } from '@fluentui/react-field'; export const Default = (props: Partial) => ( diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md index e90545bd6c18c..71f31c59b2dac 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldDescription.md @@ -1,3 +1,3 @@ -InputField is a combination of Label, an Input, and status and hint text. +InputField is a combination of Label, an Input, and validation and hint text. -InputField does not handle input validation, but it does allow a validation status message to be displayed. +InputField does not handle input validation, but it does allow a validation message to be displayed. diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx index b0a9507444992..fd6e9c66e1cbd 100644 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldHorizontal.stories.tsx @@ -17,8 +17,8 @@ export const Horizontal = () => { diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx deleted file mode 100644 index 7b3b4121ab877..0000000000000 --- a/packages/react-components/react-field/src/stories/InputField/InputFieldStatus.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react'; -import { makeStyles, tokens } from '@fluentui/react-components'; -import { InputField } from '@fluentui/react-field'; -import { SparkleFilled } from '@fluentui/react-icons'; - -const useStyles = makeStyles({ - stack: { - display: 'inline-grid', - rowGap: tokens.spacingVerticalM, - width: '400px', - }, -}); - -export const Status = () => { - const styles = useStyles(); - return ( -
- - - - } - statusText="This status message has a custom icon" - orientation="horizontal" - /> -
- ); -}; - -Status.parameters = { - docs: { - description: { - story: - 'The `status` property modifies the appearance of the status text, and for some input types, ' + - 'an error status also applies visual indication such as a red border.' + - '
' + - 'Use the `statusText` property to display an associated message. ' + - 'You can optionally override the default icon with `statusIcon`.', - }, - }, -}; diff --git a/packages/react-components/react-field/src/stories/InputField/InputFieldValidationState.stories.tsx b/packages/react-components/react-field/src/stories/InputField/InputFieldValidationState.stories.tsx new file mode 100644 index 0000000000000..333666e2bed27 --- /dev/null +++ b/packages/react-components/react-field/src/stories/InputField/InputFieldValidationState.stories.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import { InputField } from '@fluentui/react-field'; +import { SparkleFilled } from '@fluentui/react-icons'; + +const useStyles = makeStyles({ + stack: { + display: 'inline-grid', + rowGap: tokens.spacingVerticalM, + width: '400px', + }, +}); + +export const ValidationState = () => { + const styles = useStyles(); + return ( +
+ + + + } + validationMessage="This validation message has a custom icon" + orientation="horizontal" + /> +
+ ); +}; + +ValidationState.parameters = { + docs: { + description: { + story: + 'The `validationState` property modifies the appearance of the validation message, and for some input types, ' + + 'an error validationState also applies visual indication such as a red border.' + + '
' + + 'Use the `validationMessage` property to display an associated message. ' + + 'You can optionally override the default icon with `validationMessageIcon`.', + }, + }, +}; diff --git a/packages/react-components/react-field/src/stories/InputField/index.stories.tsx b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx index dd2fa588a2fc9..046108475c631 100644 --- a/packages/react-components/react-field/src/stories/InputField/index.stories.tsx +++ b/packages/react-components/react-field/src/stories/InputField/index.stories.tsx @@ -7,7 +7,7 @@ export { Default } from './InputFieldDefault.stories'; export { Label } from './InputFieldLabel.stories'; export { Horizontal } from './InputFieldHorizontal.stories'; export { Required } from './InputFieldRequired.stories'; -export { Status } from './InputFieldStatus.stories'; +export { ValidationState } from './InputFieldValidationState.stories'; export { Size } from './InputFieldSize.stories'; export { Hint } from './InputFieldHint.stories'; From 6ee6dcdc775aa0781afb46c2d1286f899c2c504a Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 18 Aug 2022 17:09:33 -0700 Subject: [PATCH 06/22] Add all Field components --- .../react-field/etc/react-field.api.md | 76 ++++++++++++++++++- .../react-components/react-field/package.json | 8 ++ .../react-field/src/CheckboxField.ts | 1 + .../react-field/src/ComboboxField.ts | 1 + .../react-field/src/RadioGroupField.ts | 1 + .../react-field/src/SelectField.ts | 1 + .../react-field/src/SliderField.ts | 1 + .../react-field/src/SpinButtonField.ts | 1 + .../react-field/src/SwitchField.ts | 1 + .../react-field/src/TextareaField.ts | 1 + .../CheckboxField/CheckboxField.test.tsx | 36 +++++++++ .../CheckboxField/CheckboxField.tsx | 38 ++++++++++ .../__snapshots__/CheckboxField.test.tsx.snap | 43 +++++++++++ .../src/components/CheckboxField/index.ts | 1 + .../ComboboxField/ComboboxField.tsx | 24 ++++++ .../src/components/ComboboxField/index.ts | 1 + .../RadioGroupField/RadioGroupField.test.tsx | 37 +++++++++ .../RadioGroupField/RadioGroupField.tsx | 23 ++++++ .../RadioGroupField.test.tsx.snap | 50 ++++++++++++ .../src/components/RadioGroupField/index.ts | 1 + .../SelectField/SelectField.test.tsx | 32 ++++++++ .../components/SelectField/SelectField.tsx | 17 +++++ .../__snapshots__/SelectField.test.tsx.snap | 35 +++++++++ .../src/components/SelectField/index.ts | 1 + .../SliderField/SliderField.test.tsx | 32 ++++++++ .../components/SliderField/SliderField.tsx | 17 +++++ .../__snapshots__/SliderField.test.tsx.snap | 26 +++++++ .../src/components/SliderField/index.ts | 1 + .../SpinButtonField/SpinButtonField.test.tsx | 32 ++++++++ .../SpinButtonField/SpinButtonField.tsx | 17 +++++ .../SpinButtonField.test.tsx.snap | 65 ++++++++++++++++ .../src/components/SpinButtonField/index.ts | 1 + .../SwitchField/SwitchField.test.tsx | 32 ++++++++ .../components/SwitchField/SwitchField.tsx | 17 +++++ .../__snapshots__/SwitchField.test.tsx.snap | 38 ++++++++++ .../src/components/SwitchField/index.ts | 1 + .../TextareaField/TextareaField.test.tsx | 32 ++++++++ .../TextareaField/TextareaField.tsx | 17 +++++ .../__snapshots__/TextareaField.test.tsx.snap | 17 +++++ .../src/components/TextareaField/index.ts | 1 + .../react-components/react-field/src/index.ts | 21 +++++ 41 files changed, 797 insertions(+), 1 deletion(-) create mode 100644 packages/react-components/react-field/src/CheckboxField.ts create mode 100644 packages/react-components/react-field/src/ComboboxField.ts create mode 100644 packages/react-components/react-field/src/RadioGroupField.ts create mode 100644 packages/react-components/react-field/src/SelectField.ts create mode 100644 packages/react-components/react-field/src/SliderField.ts create mode 100644 packages/react-components/react-field/src/SpinButtonField.ts create mode 100644 packages/react-components/react-field/src/SwitchField.ts create mode 100644 packages/react-components/react-field/src/TextareaField.ts create mode 100644 packages/react-components/react-field/src/components/CheckboxField/CheckboxField.test.tsx create mode 100644 packages/react-components/react-field/src/components/CheckboxField/CheckboxField.tsx create mode 100644 packages/react-components/react-field/src/components/CheckboxField/__snapshots__/CheckboxField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/CheckboxField/index.ts create mode 100644 packages/react-components/react-field/src/components/ComboboxField/ComboboxField.tsx create mode 100644 packages/react-components/react-field/src/components/ComboboxField/index.ts create mode 100644 packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.test.tsx create mode 100644 packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.tsx create mode 100644 packages/react-components/react-field/src/components/RadioGroupField/__snapshots__/RadioGroupField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/RadioGroupField/index.ts create mode 100644 packages/react-components/react-field/src/components/SelectField/SelectField.test.tsx create mode 100644 packages/react-components/react-field/src/components/SelectField/SelectField.tsx create mode 100644 packages/react-components/react-field/src/components/SelectField/__snapshots__/SelectField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/SelectField/index.ts create mode 100644 packages/react-components/react-field/src/components/SliderField/SliderField.test.tsx create mode 100644 packages/react-components/react-field/src/components/SliderField/SliderField.tsx create mode 100644 packages/react-components/react-field/src/components/SliderField/__snapshots__/SliderField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/SliderField/index.ts create mode 100644 packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.test.tsx create mode 100644 packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.tsx create mode 100644 packages/react-components/react-field/src/components/SpinButtonField/__snapshots__/SpinButtonField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/SpinButtonField/index.ts create mode 100644 packages/react-components/react-field/src/components/SwitchField/SwitchField.test.tsx create mode 100644 packages/react-components/react-field/src/components/SwitchField/SwitchField.tsx create mode 100644 packages/react-components/react-field/src/components/SwitchField/__snapshots__/SwitchField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/SwitchField/index.ts create mode 100644 packages/react-components/react-field/src/components/TextareaField/TextareaField.test.tsx create mode 100644 packages/react-components/react-field/src/components/TextareaField/TextareaField.tsx create mode 100644 packages/react-components/react-field/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap create mode 100644 packages/react-components/react-field/src/components/TextareaField/index.ts diff --git a/packages/react-components/react-field/etc/react-field.api.md b/packages/react-components/react-field/etc/react-field.api.md index 73ee0493ce0f1..7944218522a51 100644 --- a/packages/react-components/react-field/etc/react-field.api.md +++ b/packages/react-components/react-field/etc/react-field.api.md @@ -6,16 +6,36 @@ /// +import { Checkbox } from '@fluentui/react-checkbox'; +import { CheckboxProps } from '@fluentui/react-checkbox'; 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 { Input } from '@fluentui/react-input'; import { Label } from '@fluentui/react-label'; +import { RadioGroup } from '@fluentui/react-radio'; import * as React_2 from 'react'; +import { Select } from '@fluentui/react-select'; +import { Slider } from '@fluentui/react-slider'; import type { Slot } from '@fluentui/react-utilities'; import { SlotClassNames } from '@fluentui/react-utilities'; import type { SlotRenderFunction } from '@fluentui/react-utilities'; import type { SlotShorthandValue } from '@fluentui/react-utilities'; +import { SpinButton } from '@fluentui/react-spinButton'; +import { Switch } from '@fluentui/react-switch'; +import { Textarea } from '@fluentui/react-textarea'; + +// @public (undocumented) +export const CheckboxField: ForwardRefComponent; + +// @public (undocumented) +export const checkboxFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type CheckboxFieldProps = Omit, 'label'> & { + label?: CheckboxProps['label']; + fieldLabel?: FieldProps['label']; +}; // @public export type FieldProps = ComponentProps>, 'fieldComponent'> & { @@ -50,9 +70,63 @@ export const inputFieldClassNames: SlotClassNames>; // @public (undocumented) export type InputFieldProps = FieldProps; +// @public (undocumented) +export const RadioGroupField: ForwardRefComponent; + +// @public (undocumented) +export const radioGroupFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type RadioGroupFieldProps = FieldProps; + // @public export const renderField_unstable: (state: FieldState) => JSX.Element; +// @public (undocumented) +export const SelectField: ForwardRefComponent; + +// @public (undocumented) +export const selectFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type SelectFieldProps = FieldProps; + +// @public (undocumented) +export const SliderField: ForwardRefComponent; + +// @public (undocumented) +export const sliderFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type SliderFieldProps = FieldProps; + +// @public (undocumented) +export const SpinButtonField: ForwardRefComponent; + +// @public (undocumented) +export const spinButtonFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type SpinButtonFieldProps = FieldProps; + +// @public (undocumented) +export const SwitchField: ForwardRefComponent; + +// @public (undocumented) +export const switchFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type SwitchFieldProps = FieldProps; + +// @public (undocumented) +export const TextareaField: ForwardRefComponent; + +// @public (undocumented) +export const textareaFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type TextareaFieldProps = FieldProps; + // @public export const useField_unstable: (params: UseFieldParams) => FieldState; diff --git a/packages/react-components/react-field/package.json b/packages/react-components/react-field/package.json index 3e756c1fa68e3..3c8f8bfcdf943 100644 --- a/packages/react-components/react-field/package.json +++ b/packages/react-components/react-field/package.json @@ -32,10 +32,18 @@ "@fluentui/scripts": "^1.0.0" }, "dependencies": { + "@fluentui/react-checkbox": "^9.0.4", + "@fluentui/react-combobox": "^9.0.0-beta.8", "@fluentui/react-context-selector": "^9.0.2", "@fluentui/react-icons": "^2.0.175", "@fluentui/react-input": "^9.0.4", "@fluentui/react-label": "^9.0.4", + "@fluentui/react-radio": "^9.0.4", + "@fluentui/react-select": "9.0.0-beta.8", + "@fluentui/react-slider": "^9.0.3", + "@fluentui/react-spinbutton": "^9.0.0", + "@fluentui/react-switch": "^9.0.4", + "@fluentui/react-textarea": "^9.0.4", "@fluentui/react-theme": "^9.0.0", "@fluentui/react-utilities": "^9.0.2", "@griffel/react": "^1.3.0", diff --git a/packages/react-components/react-field/src/CheckboxField.ts b/packages/react-components/react-field/src/CheckboxField.ts new file mode 100644 index 0000000000000..dc829ee3cc9d7 --- /dev/null +++ b/packages/react-components/react-field/src/CheckboxField.ts @@ -0,0 +1 @@ +export * from './components/CheckboxField/index'; diff --git a/packages/react-components/react-field/src/ComboboxField.ts b/packages/react-components/react-field/src/ComboboxField.ts new file mode 100644 index 0000000000000..932370c08f79f --- /dev/null +++ b/packages/react-components/react-field/src/ComboboxField.ts @@ -0,0 +1 @@ +export * from './components/ComboboxField/index'; diff --git a/packages/react-components/react-field/src/RadioGroupField.ts b/packages/react-components/react-field/src/RadioGroupField.ts new file mode 100644 index 0000000000000..0be3f52f49438 --- /dev/null +++ b/packages/react-components/react-field/src/RadioGroupField.ts @@ -0,0 +1 @@ +export * from './components/RadioGroupField/index'; diff --git a/packages/react-components/react-field/src/SelectField.ts b/packages/react-components/react-field/src/SelectField.ts new file mode 100644 index 0000000000000..935b8affe4676 --- /dev/null +++ b/packages/react-components/react-field/src/SelectField.ts @@ -0,0 +1 @@ +export * from './components/SelectField/index'; diff --git a/packages/react-components/react-field/src/SliderField.ts b/packages/react-components/react-field/src/SliderField.ts new file mode 100644 index 0000000000000..dae84d11937a0 --- /dev/null +++ b/packages/react-components/react-field/src/SliderField.ts @@ -0,0 +1 @@ +export * from './components/SliderField/index'; diff --git a/packages/react-components/react-field/src/SpinButtonField.ts b/packages/react-components/react-field/src/SpinButtonField.ts new file mode 100644 index 0000000000000..6167e470dcb89 --- /dev/null +++ b/packages/react-components/react-field/src/SpinButtonField.ts @@ -0,0 +1 @@ +export * from './components/SpinButtonField/index'; diff --git a/packages/react-components/react-field/src/SwitchField.ts b/packages/react-components/react-field/src/SwitchField.ts new file mode 100644 index 0000000000000..2edf7a34e6e14 --- /dev/null +++ b/packages/react-components/react-field/src/SwitchField.ts @@ -0,0 +1 @@ +export * from './components/SwitchField/index'; diff --git a/packages/react-components/react-field/src/TextareaField.ts b/packages/react-components/react-field/src/TextareaField.ts new file mode 100644 index 0000000000000..758c2c06394bb --- /dev/null +++ b/packages/react-components/react-field/src/TextareaField.ts @@ -0,0 +1 @@ +export * from './components/TextareaField/index'; diff --git a/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.test.tsx b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.test.tsx new file mode 100644 index 0000000000000..14e62eb340775 --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.test.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { CheckboxField, checkboxFieldClassNames } from './CheckboxField'; + +describe('CheckboxField', () => { + // Checkbox doesn't use the Field's label, so remove it from the conformance test's expected class names + const { label: _ignored, ...expectedClassNames } = checkboxFieldClassNames; + + isConformant({ + Component: CheckboxField, + displayName: 'CheckboxField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + expectedClassNames: (expectedClassNames as unknown) as Record, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.tsx b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.tsx new file mode 100644 index 0000000000000..9dfae20f87e96 --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { Checkbox, CheckboxProps } from '@fluentui/react-checkbox'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type CheckboxFieldProps = Omit, 'label'> & { + /** + * The Checkbox's label. + */ + label?: CheckboxProps['label']; + + /** + * The label for the CheckboxField, which appears above or before the Checkbox, depending on the `orientation` prop. + * It is recommended to only set the `label` prop, and not `fieldLabel`. + */ + fieldLabel?: FieldProps['label']; +}; + +export const checkboxFieldClassNames = getFieldClassNames('CheckboxField'); + +export const CheckboxField: ForwardRefComponent = React.forwardRef((props, ref) => { + // Forward the label prop to the underlying Checkbox (fieldComponent) instead of the Field + props = { + ...props, + label: props.fieldLabel, + fieldComponent: { + label: props.label, + ...props.fieldComponent, + }, + }; + + const state = useField_unstable({ props, ref, fieldComponent: Checkbox, classNames: checkboxFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +CheckboxField.displayName = 'CheckboxField'; diff --git a/packages/react-components/react-field/src/components/CheckboxField/__snapshots__/CheckboxField.test.tsx.snap b/packages/react-components/react-field/src/components/CheckboxField/__snapshots__/CheckboxField.test.tsx.snap new file mode 100644 index 0000000000000..7f038e40a24a8 --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/__snapshots__/CheckboxField.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheckboxField renders a default state 1`] = ` +
+
+ + + + + +
+
+`; diff --git a/packages/react-components/react-field/src/components/CheckboxField/index.ts b/packages/react-components/react-field/src/components/CheckboxField/index.ts new file mode 100644 index 0000000000000..032e30e3a8b12 --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/index.ts @@ -0,0 +1 @@ +export * from './CheckboxField'; diff --git a/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.tsx b/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.tsx new file mode 100644 index 0000000000000..0a8c5fbeb46eb --- /dev/null +++ b/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.tsx @@ -0,0 +1,24 @@ +// import * as React from 'react'; +// import { Combobox } from '@fluentui/react-combobox'; +// import type { ForwardRefComponent } from '@fluentui/react-utilities'; +// import type { FieldProps } from '../../Field'; +// import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +// export type ComboboxFieldProps = FieldProps; + +// export const comboboxFieldClassNames = getFieldClassNames('ComboboxField'); + +// export const ComboboxField: ForwardRefComponent = React.forwardRef((props, ref) => { +// const state = useField_unstable({ props, ref, fieldComponent: Combobox, classNames: comboboxFieldClassNames }); +// useFieldStyles_unstable(state); +// return renderField_unstable(state); +// }); + +// ComboboxField.displayName = 'ComboboxField'; + +// -- + +// Temporary exports until Combobox is updated to make its children prop optional +export type ComboboxFieldProps = never; +export const comboboxFieldClassNames = undefined; +export const ComboboxField = undefined; diff --git a/packages/react-components/react-field/src/components/ComboboxField/index.ts b/packages/react-components/react-field/src/components/ComboboxField/index.ts new file mode 100644 index 0000000000000..5dcd2a0df1f8f --- /dev/null +++ b/packages/react-components/react-field/src/components/ComboboxField/index.ts @@ -0,0 +1 @@ +export * from './ComboboxField'; diff --git a/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.test.tsx b/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.test.tsx new file mode 100644 index 0000000000000..980409a692c14 --- /dev/null +++ b/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.test.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { Radio } from '@fluentui/react-radio'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { RadioGroupField } from './RadioGroupField'; + +describe('RadioGroupField', () => { + isConformant({ + Component: RadioGroupField, + displayName: 'RadioGroupField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render( + + + , + ); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.tsx b/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.tsx new file mode 100644 index 0000000000000..851c0aa04ac0a --- /dev/null +++ b/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { RadioGroup } from '@fluentui/react-radio'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type RadioGroupFieldProps = FieldProps; + +export const radioGroupFieldClassNames = getFieldClassNames('RadioGroupField'); + +export const RadioGroupField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable({ + props, + ref, + fieldComponent: RadioGroup, + classNames: radioGroupFieldClassNames, + labelConnection: 'aria-labelledby', + }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +RadioGroupField.displayName = 'RadioGroupField'; diff --git a/packages/react-components/react-field/src/components/RadioGroupField/__snapshots__/RadioGroupField.test.tsx.snap b/packages/react-components/react-field/src/components/RadioGroupField/__snapshots__/RadioGroupField.test.tsx.snap new file mode 100644 index 0000000000000..c8e59d9e9626b --- /dev/null +++ b/packages/react-components/react-field/src/components/RadioGroupField/__snapshots__/RadioGroupField.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadioGroupField renders a default state 1`] = ` +
+
+
+ + + + + +
+
+
+`; diff --git a/packages/react-components/react-field/src/components/RadioGroupField/index.ts b/packages/react-components/react-field/src/components/RadioGroupField/index.ts new file mode 100644 index 0000000000000..cb66f6ac27f53 --- /dev/null +++ b/packages/react-components/react-field/src/components/RadioGroupField/index.ts @@ -0,0 +1 @@ +export * from './RadioGroupField'; diff --git a/packages/react-components/react-field/src/components/SelectField/SelectField.test.tsx b/packages/react-components/react-field/src/components/SelectField/SelectField.test.tsx new file mode 100644 index 0000000000000..b56184faa819f --- /dev/null +++ b/packages/react-components/react-field/src/components/SelectField/SelectField.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { SelectField } from './SelectField'; + +describe('SelectField', () => { + isConformant({ + Component: SelectField, + displayName: 'SelectField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/SelectField/SelectField.tsx b/packages/react-components/react-field/src/components/SelectField/SelectField.tsx new file mode 100644 index 0000000000000..e7e66587861ee --- /dev/null +++ b/packages/react-components/react-field/src/components/SelectField/SelectField.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Select } from '@fluentui/react-select'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type SelectFieldProps = FieldProps; + +export const selectFieldClassNames = getFieldClassNames('SelectField'); + +export const SelectField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable({ props, ref, fieldComponent: Select, classNames: selectFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +SelectField.displayName = 'SelectField'; diff --git a/packages/react-components/react-field/src/components/SelectField/__snapshots__/SelectField.test.tsx.snap b/packages/react-components/react-field/src/components/SelectField/__snapshots__/SelectField.test.tsx.snap new file mode 100644 index 0000000000000..e79b27035d14a --- /dev/null +++ b/packages/react-components/react-field/src/components/SelectField/__snapshots__/SelectField.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectField renders a default state 1`] = ` +
+
+ + +
+
+
+
+
+`; diff --git a/packages/react-components/react-field/src/components/SliderField/index.ts b/packages/react-components/react-field/src/components/SliderField/index.ts new file mode 100644 index 0000000000000..4caba157da4d3 --- /dev/null +++ b/packages/react-components/react-field/src/components/SliderField/index.ts @@ -0,0 +1 @@ +export * from './SliderField'; diff --git a/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.test.tsx b/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.test.tsx new file mode 100644 index 0000000000000..ddbdbd4593811 --- /dev/null +++ b/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { SpinButtonField } from './SpinButtonField'; + +describe('SpinButtonField', () => { + isConformant({ + Component: SpinButtonField, + displayName: 'SpinButtonField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.tsx b/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.tsx new file mode 100644 index 0000000000000..b7c7a60f5bd90 --- /dev/null +++ b/packages/react-components/react-field/src/components/SpinButtonField/SpinButtonField.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { SpinButton } from '@fluentui/react-spinButton'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type SpinButtonFieldProps = FieldProps; + +export const spinButtonFieldClassNames = getFieldClassNames('SpinButtonField'); + +export const SpinButtonField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable({ props, ref, fieldComponent: SpinButton, classNames: spinButtonFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +SpinButtonField.displayName = 'SpinButtonField'; diff --git a/packages/react-components/react-field/src/components/SpinButtonField/__snapshots__/SpinButtonField.test.tsx.snap b/packages/react-components/react-field/src/components/SpinButtonField/__snapshots__/SpinButtonField.test.tsx.snap new file mode 100644 index 0000000000000..06c36bc568a4b --- /dev/null +++ b/packages/react-components/react-field/src/components/SpinButtonField/__snapshots__/SpinButtonField.test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpinButtonField renders a default state 1`] = ` +
+
+ + + + + +
+
+`; diff --git a/packages/react-components/react-field/src/components/SpinButtonField/index.ts b/packages/react-components/react-field/src/components/SpinButtonField/index.ts new file mode 100644 index 0000000000000..918d7e878d6a0 --- /dev/null +++ b/packages/react-components/react-field/src/components/SpinButtonField/index.ts @@ -0,0 +1 @@ +export * from './SpinButtonField'; diff --git a/packages/react-components/react-field/src/components/SwitchField/SwitchField.test.tsx b/packages/react-components/react-field/src/components/SwitchField/SwitchField.test.tsx new file mode 100644 index 0000000000000..0d33386b1f0ec --- /dev/null +++ b/packages/react-components/react-field/src/components/SwitchField/SwitchField.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { SwitchField } from './SwitchField'; + +describe('SwitchField', () => { + isConformant({ + Component: SwitchField, + displayName: 'SwitchField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/SwitchField/SwitchField.tsx b/packages/react-components/react-field/src/components/SwitchField/SwitchField.tsx new file mode 100644 index 0000000000000..e3562ad1f7f4c --- /dev/null +++ b/packages/react-components/react-field/src/components/SwitchField/SwitchField.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Switch } from '@fluentui/react-switch'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type SwitchFieldProps = FieldProps; + +export const switchFieldClassNames = getFieldClassNames('SwitchField'); + +export const SwitchField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable({ props, ref, fieldComponent: Switch, classNames: switchFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +SwitchField.displayName = 'SwitchField'; diff --git a/packages/react-components/react-field/src/components/SwitchField/__snapshots__/SwitchField.test.tsx.snap b/packages/react-components/react-field/src/components/SwitchField/__snapshots__/SwitchField.test.tsx.snap new file mode 100644 index 0000000000000..7c2e4bd65e9b4 --- /dev/null +++ b/packages/react-components/react-field/src/components/SwitchField/__snapshots__/SwitchField.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SwitchField renders a default state 1`] = ` +
+
+
+ + +
+
+
+`; diff --git a/packages/react-components/react-field/src/components/SwitchField/index.ts b/packages/react-components/react-field/src/components/SwitchField/index.ts new file mode 100644 index 0000000000000..095c0353dbffb --- /dev/null +++ b/packages/react-components/react-field/src/components/SwitchField/index.ts @@ -0,0 +1 @@ +export * from './SwitchField'; diff --git a/packages/react-components/react-field/src/components/TextareaField/TextareaField.test.tsx b/packages/react-components/react-field/src/components/TextareaField/TextareaField.test.tsx new file mode 100644 index 0000000000000..68d7b7f98f6c9 --- /dev/null +++ b/packages/react-components/react-field/src/components/TextareaField/TextareaField.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { TextareaField } from './TextareaField'; + +describe('TextareaField', () => { + isConformant({ + Component: TextareaField, + displayName: 'TextareaField', + primarySlot: 'fieldComponent', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-field/src/components/TextareaField/TextareaField.tsx b/packages/react-components/react-field/src/components/TextareaField/TextareaField.tsx new file mode 100644 index 0000000000000..2358160a45d9c --- /dev/null +++ b/packages/react-components/react-field/src/components/TextareaField/TextareaField.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Textarea } from '@fluentui/react-textarea'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from '../../Field'; +import { getFieldClassNames, renderField_unstable, useFieldStyles_unstable, useField_unstable } from '../../Field'; + +export type TextareaFieldProps = FieldProps; + +export const textareaFieldClassNames = getFieldClassNames('TextareaField'); + +export const TextareaField: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField_unstable({ props, ref, fieldComponent: Textarea, classNames: textareaFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +TextareaField.displayName = 'TextareaField'; diff --git a/packages/react-components/react-field/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap b/packages/react-components/react-field/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap new file mode 100644 index 0000000000000..2661863a99c9b --- /dev/null +++ b/packages/react-components/react-field/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TextareaField renders a default state 1`] = ` +
+
+ +