diff --git a/docs/src/examples/components/Checkbox/States/CheckboxExampleIndeterminate.shorthand.tsx b/docs/src/examples/components/Checkbox/States/CheckboxExampleIndeterminate.shorthand.tsx new file mode 100644 index 0000000000..a0724a3572 --- /dev/null +++ b/docs/src/examples/components/Checkbox/States/CheckboxExampleIndeterminate.shorthand.tsx @@ -0,0 +1,12 @@ +import { Checkbox } from '@fluentui/react' +import * as React from 'react' + +const CheckboxExampleIndeterminate = () => { + return ( + <> + + + ) +} + +export default CheckboxExampleIndeterminate diff --git a/docs/src/examples/components/Checkbox/States/index.tsx b/docs/src/examples/components/Checkbox/States/index.tsx index bcc90bd532..239c950fd9 100644 --- a/docs/src/examples/components/Checkbox/States/index.tsx +++ b/docs/src/examples/components/Checkbox/States/index.tsx @@ -15,6 +15,11 @@ const States = () => ( description="A checkbox can be read-only and unable to change states." examplePath="components/Checkbox/States/CheckboxExampleDisabled" /> + ) diff --git a/packages/accessibility/src/behaviors/Checkbox/checkboxBehavior.ts b/packages/accessibility/src/behaviors/Checkbox/checkboxBehavior.ts index c19a674928..7cc3e5f493 100644 --- a/packages/accessibility/src/behaviors/Checkbox/checkboxBehavior.ts +++ b/packages/accessibility/src/behaviors/Checkbox/checkboxBehavior.ts @@ -13,7 +13,7 @@ import { IS_FOCUSABLE_ATTRIBUTE } from '../../attributes' const checkboxBehavior: Accessibility = props => ({ attributes: { root: { - 'aria-checked': !!props.checked, + 'aria-checked': props.indeterminate ? 'mixed' : props.checked ? 'true' : 'false', 'aria-disabled': props.disabled, role: 'checkbox', tabIndex: 0, @@ -36,4 +36,6 @@ type CheckboxBehaviorProps = { checked: boolean /** If the checkbox is in disabled state. */ disabled?: boolean + /** If the checkbox is in indetermiante state. */ + indeterminate?: boolean } diff --git a/packages/react/src/components/Checkbox/Checkbox.tsx b/packages/react/src/components/Checkbox/Checkbox.tsx index 8ae819f263..fe52d9ba1d 100644 --- a/packages/react/src/components/Checkbox/Checkbox.tsx +++ b/packages/react/src/components/Checkbox/Checkbox.tsx @@ -33,6 +33,12 @@ export interface CheckboxProps extends UIComponentProps, ChildrenComponentProps /** A checkbox's checked state can be controlled. */ checked?: SupportedIntrinsicInputProps['checked'] + /** A checkbox can be indetermiante by default - uncontrolled state. */ + defaultIndeterminate?: SupportedIntrinsicInputProps['defaultIndeterminate'] + + /** A checkbox's indeterminate state can be controlled. */ + indeterminate?: SupportedIntrinsicInputProps['indeterminate'] + /** A checkbox can appear disabled and be unable to change states. */ disabled?: SupportedIntrinsicInputProps['disabled'] @@ -65,6 +71,7 @@ export interface CheckboxProps extends UIComponentProps, ChildrenComponentProps export interface CheckboxState { checked: CheckboxProps['checked'] + indeterminate: CheckboxProps['indeterminate'] } class Checkbox extends AutoControlledComponent, CheckboxState> { @@ -82,8 +89,10 @@ class Checkbox extends AutoControlledComponent, Checkb }), checked: PropTypes.bool, defaultChecked: PropTypes.bool, + defaultIndeterminate: PropTypes.bool, disabled: PropTypes.bool, icon: customPropTypes.itemShorthandWithoutJSX, + indeterminate: PropTypes.bool, label: customPropTypes.itemShorthand, labelPosition: PropTypes.oneOf(['start', 'end']), onChange: PropTypes.func, @@ -107,30 +116,42 @@ class Checkbox extends AutoControlledComponent, Checkb } getInitialAutoControlledState(): CheckboxState { - return { checked: false } + return { checked: false, indeterminate: false } } handleChange = (e: React.ChangeEvent) => { // Checkbox component doesn't present any `input` component in markup, however all of our // components should handle events transparently. - const { disabled } = this.props + const { disabled, indeterminate } = this.props const checked = !this.state.checked if (!disabled) { - this.setState({ checked }) - _.invoke(this.props, 'onChange', e, { ...this.props, checked }) + this.setState({ checked, indeterminate }) + _.invoke(this.props, 'onChange', e, { + ...this.props, + checked, + indetermiante: !this.state.indeterminate, + }) } } handleClick = (e: React.MouseEvent | React.KeyboardEvent) => { - const { disabled } = this.props + const { disabled, indeterminate } = this.props const checked = !this.state.checked if (!disabled) { - this.setState({ checked }) - - _.invoke(this.props, 'onClick', e, { ...this.props, checked }) - _.invoke(this.props, 'onChange', e, { ...this.props, checked }) + this.setState({ checked, indeterminate }) + + _.invoke(this.props, 'onClick', e, { + ...this.props, + checked, + indetermiante: !this.state.indeterminate, + }) + _.invoke(this.props, 'onChange', e, { + ...this.props, + checked, + indetermiante: !this.state.indeterminate, + }) } } @@ -164,7 +185,11 @@ class Checkbox extends AutoControlledComponent, Checkb outline: toggle && !this.state.checked, size: toggle ? 'medium' : 'smaller', className: Checkbox.slotClassNames.indicator, - name: toggle ? 'icon-circle' : 'icon-checkmark', + name: toggle + ? 'icon-circle' + : this.state.indeterminate + ? 'icon-square' + : 'icon-checkmark', styles: toggle ? styles.toggle : styles.checkbox, }), })} diff --git a/packages/react/src/themes/teams/components/Checkbox/checkboxStyles.ts b/packages/react/src/themes/teams/components/Checkbox/checkboxStyles.ts index 55249efc44..165a9ce349 100644 --- a/packages/react/src/themes/teams/components/Checkbox/checkboxStyles.ts +++ b/packages/react/src/themes/teams/components/Checkbox/checkboxStyles.ts @@ -84,6 +84,13 @@ const checkboxStyles: ComponentSlotStylesPrepared< background: v.disabledBackgroundChecked, borderColor: 'transparent', }), + + // TODO: in the case of indeterminate, set icon-square + // to be smaller on all sides inside the larger input box. + // ...(p.indeterminate && { + // boxSizing: 'border-box', + // flexShrink: 0, + // }), }), toggle: ({ props: p, variables: v }): ICSSInJSStyle => ({ diff --git a/packages/react/src/themes/teams/components/Checkbox/checkboxVariables.ts b/packages/react/src/themes/teams/components/Checkbox/checkboxVariables.ts index d1c9342c8d..da0456b8a5 100644 --- a/packages/react/src/themes/teams/components/Checkbox/checkboxVariables.ts +++ b/packages/react/src/themes/teams/components/Checkbox/checkboxVariables.ts @@ -122,4 +122,5 @@ export default (siteVars: any): CheckboxVariables => ({ 'colorScheme.default.foregroundDisabled', defaultValue, ), + // TODO: add variables for indeterminate state. }) diff --git a/packages/react/src/utils/htmlPropsUtils.tsx b/packages/react/src/utils/htmlPropsUtils.tsx index 432d7d43e5..1740821320 100644 --- a/packages/react/src/utils/htmlPropsUtils.tsx +++ b/packages/react/src/utils/htmlPropsUtils.tsx @@ -55,9 +55,11 @@ export type HtmlInputAttrs = | 'autoCorrect' | 'autoFocus' | 'checked' + | 'defaultIndeterminate' | 'disabled' | 'form' | 'id' + | 'indeterminate' | 'list' | 'max' | 'maxLength' @@ -92,9 +94,11 @@ export const htmlInputAttrs: HtmlInputAttrs[] = [ 'autoCorrect', 'autoFocus', 'checked', + 'defaultIndeterminate', 'disabled', 'form', 'id', + 'indeterminate', 'list', 'max', 'maxLength',