diff --git a/packages/react-components/react-field/.storybook/main.js b/packages/react-components/react-field/.storybook/main.js index f57cfd09509e78..bea03d1fb52af8 100644 --- a/packages/react-components/react-field/.storybook/main.js +++ b/packages/react-components/react-field/.storybook/main.js @@ -2,7 +2,12 @@ const rootMain = require('../../../../.storybook/main'); module.exports = /** @type {Omit} */ ({ ...rootMain, - stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/index.stories.@(ts|tsx)'], + stories: [ + ...rootMain.stories, + '../src/**/*.stories.mdx', + '../src/stories/Field/index.stories.@(ts|tsx)', + '../src/**/index.stories.@(ts|tsx)', + ], addons: [...rootMain.addons], webpackFinal: (config, options) => { const localConfig = { ...rootMain.webpackFinal(config, options) }; 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 aaccee2cfc2db6..0296d4d74c1fd3 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,53 @@ /// +import { Checkbox } from '@fluentui/react-checkbox'; +import type { CheckboxProps } from '@fluentui/react-checkbox'; +import { Combobox } from '@fluentui/react-combobox'; 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 { SlotRenderFunction } from '@fluentui/react-utilities'; +import { 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 (undocumented) +export const ComboboxField: ForwardRefComponent; + +// @public (undocumented) +export const comboboxFieldClassNames: SlotClassNames>; + +// @public (undocumented) +export type ComboboxFieldProps = FieldProps; + +// @public +export type FieldConfig = { + component: T; + classNames: SlotClassNames>; + labelConnection?: 'htmlFor' | 'aria-labelledby'; +}; // @public export type FieldProps = ComponentProps>, 'control'> & { @@ -50,20 +87,502 @@ 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 -export const useField_unstable: (params: UseFieldParams) => FieldState; +// @public (undocumented) +export const SelectField: ForwardRefComponent; // @public (undocumented) -export type UseFieldParams = { - props: FieldProps & OptionalFieldComponentProps; - ref: React_2.Ref; - component: T; - classNames: SlotClassNames>; - labelConnection?: 'htmlFor' | 'aria-labelledby'; -}; +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: (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>) & { + orientation?: "vertical" | "horizontal" | undefined; + validationState?: "error" | "warning" | "success" | undefined; +} & OptionalFieldComponentProps, ref: React_2.Ref, params: FieldConfig) => FieldState; // @public export const useFieldStyles_unstable: (state: FieldState) => void; diff --git a/packages/react-components/react-field/package.json b/packages/react-components/react-field/package.json index 3e756c1fa68e34..3c8f8bfcdf9433 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 00000000000000..dc829ee3cc9d76 --- /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 00000000000000..932370c08f79f4 --- /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 00000000000000..0be3f52f494388 --- /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 00000000000000..935b8affe46761 --- /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 00000000000000..dae84d11937a01 --- /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 00000000000000..6167e470dcb899 --- /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 00000000000000..2edf7a34e6e14b --- /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 00000000000000..758c2c06394bb1 --- /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 00000000000000..5830ee5e0c4457 --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.test.tsx @@ -0,0 +1,34 @@ +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', () => { + isConformant({ + Component: CheckboxField, + displayName: 'CheckboxField', + primarySlot: 'control', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + fieldLabel: 'fieldLabel', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + expectedClassNames: checkboxFieldClassNames, + }, + ], + }, + }); + + 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 00000000000000..f8c26120b101bf --- /dev/null +++ b/packages/react-components/react-field/src/components/CheckboxField/CheckboxField.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import type { CheckboxProps } from '@fluentui/react-checkbox'; +import { Checkbox } 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) => { + const { fieldLabel, required, label, control, ...restOfProps } = props; + + props = { + // Use the fieldLabel prop as the Field's label + label: fieldLabel, + // Use the label prop as the Checkbox's label + control: { label, required, ...control }, + ...restOfProps, + }; + + const state = useField_unstable(props, ref, { component: 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 00000000000000..fdbca22d583f07 --- /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 00000000000000..032e30e3a8b12b --- /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.test.tsx b/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.test.tsx new file mode 100644 index 00000000000000..87ef7c557d89c1 --- /dev/null +++ b/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.test.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { ComboboxField, comboboxFieldClassNames } from './ComboboxField'; + +describe('ComboboxField', () => { + isConformant({ + Component: ComboboxField, + displayName: 'ComboboxField', + primarySlot: 'control', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'label', + validationState: 'error', + validationMessage: 'validationMessage', + hint: 'hint', + }, + expectedClassNames: comboboxFieldClassNames, + }, + ], + }, + }); + + beforeEach(resetIdsForTests); + + it('renders a default state', () => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); +}); 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 00000000000000..88690183f3a9c5 --- /dev/null +++ b/packages/react-components/react-field/src/components/ComboboxField/ComboboxField.tsx @@ -0,0 +1,17 @@ +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, { component: Combobox, classNames: comboboxFieldClassNames }); + useFieldStyles_unstable(state); + return renderField_unstable(state); +}); + +ComboboxField.displayName = 'ComboboxField'; diff --git a/packages/react-components/react-field/src/components/ComboboxField/__snapshots__/ComboboxField.test.tsx.snap b/packages/react-components/react-field/src/components/ComboboxField/__snapshots__/ComboboxField.test.tsx.snap new file mode 100644 index 00000000000000..4a7d12a8450a74 --- /dev/null +++ b/packages/react-components/react-field/src/components/ComboboxField/__snapshots__/ComboboxField.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ComboboxField renders a default state 1`] = ` +
+
+
+ + + + +
+
+
+`; 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 00000000000000..5dcd2a0df1f8f7 --- /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/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts index 00b40ba45174fe..f22bedacf0c454 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 @@ -24,9 +24,6 @@ export type FieldSlots = { /** * 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. */ control: SlotComponent; @@ -92,6 +89,32 @@ export type OptionalFieldComponentProps = { size?: 'small' | 'medium' | 'large' | number; }; +/** + * Configuration parameters for a Field class, passed to useField_unstable + */ +export type FieldConfig = { + /** + * The underlying input component that this field is wrapping. + */ + component: T; + + /** + * Class names for this component, created by `getFieldClassNames`. + */ + classNames: SlotClassNames>; + + /** + * How the label be connected to the control. + * * 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'; +}; + /** * State used in rendering Field */ 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 3d56d18d5e45d4..7e04e8020e40ee 100644 --- a/packages/react-components/react-field/src/components/Field/useField.tsx +++ b/packages/react-components/react-field/src/components/Field/useField.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import type { FieldComponent, FieldProps, FieldSlots, FieldState, OptionalFieldComponentProps } from './Field.types'; +import type { FieldComponent, FieldConfig, FieldProps, 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'; +import { getNativeElementProps, resolveShorthand, useId } from '@fluentui/react-utilities'; const validationMessageIcons = { error: , @@ -50,60 +50,33 @@ export const getPartitionedFieldProps = = { - /** - * Props passed to this Field - */ - props: FieldProps & OptionalFieldComponentProps; - - /** - * Ref to be passed to the control slot (primary slot) - */ - ref: React.Ref; - - /** - * The underlying input component that this field is wrapping. - */ - component: T; - - /** - * Class names for this component, created by `getFieldClassNames`. - */ - classNames: SlotClassNames>; - - /** - * How the label be connected to the control. - * * 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'; -}; - /** * 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 passed to this field + * @param ref - Ref to the control slot (primary slot) * @param params - Configuration parameters for this Field */ -export const useField_unstable = (params: UseFieldParams): FieldState => { - const [props, controlProps] = getPartitionedFieldProps(params.props); +export const useField_unstable = ( + props: FieldProps & OptionalFieldComponentProps, + ref: React.Ref, + params: FieldConfig, +): FieldState => { + const [fieldProps, controlProps] = getPartitionedFieldProps(props); const baseId = useId('field-'); - const { orientation = 'vertical', validationState } = props; + const { orientation = 'vertical', validationState } = fieldProps; - const root = resolveShorthand(props.root, { + const root = resolveShorthand(fieldProps.root, { required: true, - defaultProps: getNativeElementProps('div', props), + defaultProps: getNativeElementProps('div', fieldProps), }); - const label = resolveShorthand(props.label, { + const label = resolveShorthand(fieldProps.label, { defaultProps: { id: baseId + '__label', required: controlProps.required, @@ -112,19 +85,19 @@ export const useField_unstable = (params: UseFieldPara }, }); - const validationMessage = resolveShorthand(props.validationMessage, { + const validationMessage = resolveShorthand(fieldProps.validationMessage, { defaultProps: { id: baseId + '__validationMessage', }, }); - const hint = resolveShorthand(props.hint, { + const hint = resolveShorthand(fieldProps.hint, { defaultProps: { id: baseId + '__hint', }, }); - const validationMessageIcon = resolveShorthand(props.validationMessageIcon, { + const validationMessageIcon = resolveShorthand(fieldProps.validationMessageIcon, { required: !!validationState, defaultProps: { children: validationState ? validationMessageIcons[validationState] : undefined, @@ -134,10 +107,10 @@ export const useField_unstable = (params: UseFieldPara const { labelConnection = 'htmlFor' } = params; const hasError = validationState === 'error'; - const control = resolveShorthand(props.control, { + const control = resolveShorthand(fieldProps.control, { required: true, defaultProps: { - ref: params.ref, + ref, // Add a default ID only if required for label's htmlFor prop id: label && labelConnection === 'htmlFor' ? baseId + '__control' : undefined, // Add aria-labelledby only if not using the label's htmlFor 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 a7fef70a4ff1b7..3bb40228bb88e0 100644 --- a/packages/react-components/react-field/src/components/InputField/InputField.tsx +++ b/packages/react-components/react-field/src/components/InputField/InputField.tsx @@ -9,7 +9,7 @@ export type InputFieldProps = FieldProps; export const inputFieldClassNames = getFieldClassNames('InputField'); export const InputField: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useField_unstable({ props, ref, component: Input, classNames: inputFieldClassNames }); + const state = useField_unstable(props, ref, { component: Input, classNames: inputFieldClassNames }); useFieldStyles_unstable(state); return renderField_unstable(state); }); 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 00000000000000..cd822236242b12 --- /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: 'control', + 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 00000000000000..418ea74b9b1a94 --- /dev/null +++ b/packages/react-components/react-field/src/components/RadioGroupField/RadioGroupField.tsx @@ -0,0 +1,21 @@ +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, { + component: 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 00000000000000..80383853bcb31d --- /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 00000000000000..cb66f6ac27f53e --- /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 00000000000000..c19b2e47745697 --- /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: 'control', + 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 00000000000000..b7c72dc584521c --- /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, { component: 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 00000000000000..6a0565e787c055 --- /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 00000000000000..4caba157da4d3f --- /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 00000000000000..d1931b35146798 --- /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: 'control', + 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 00000000000000..7a4137415fc3f3 --- /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, { component: 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 00000000000000..3683ca0b26040f --- /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 00000000000000..918d7e878d6a07 --- /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 00000000000000..6ce4a9110d9028 --- /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: 'control', + 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 00000000000000..fe89e23fdf2f8a --- /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, { component: 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 00000000000000..04b8182551d722 --- /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 00000000000000..095c0353dbffbb --- /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 00000000000000..3bd272296e467e --- /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: 'control', + 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 00000000000000..86b140e534d8a4 --- /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, { component: 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 00000000000000..bb93441402a817 --- /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`] = ` +
+
+ +