From 24f53d16c79a7659f75ed835bf056da9ca2be076 Mon Sep 17 00:00:00 2001 From: Michael Loughry Date: Thu, 30 Apr 2020 11:24:11 -0700 Subject: [PATCH] Migrate Checkbox to functional component (#12614) * Migrate Checkbox to functional component * Refactor * Remove extraneous state interface. * Change files * Add new hooks used in migrating Fabric components to functional components * Change files * Refactor further to move onChange logic to hook * Cherry pick revision from MLoughry:functional/checkbox * Fix lint error * Address comments from dzearing * Fix launch.json for debugging tests * Use common onChange call signature * Fix hook * Update Checkbox to use updated hook definition * Fix useControllableValue types in case with no onChange handler * Export ChangeCallback type * Persist the controlled vs uncontrolled state * Update packages/react-hooks/src/useMergedRefs.ts Co-Authored-By: Elizabeth Craig * Update README * Allow for undefined events in the onChange callback * CRLF -> LF * Fix error in isControlled value * Delete test for deprecated behavior * Update packages/react-hooks/README.md Co-Authored-By: Elizabeth Craig * Update packages/react-hooks/README.md Co-Authored-By: Elizabeth Craig * Migrate changes to react-next * Change files * Delete extraneous changefiles * Add React.memo * Update api.md * Copy existing lint config from OUFR * Should add react-next to bundle validation. * Pass handlers directly * useFocusRects * Don't memoize callback passed to DOM element * Remove FocusRects * Remove React.memo * Add display name * Address David's comment * Update API file Co-authored-by: Elizabeth Craig Co-authored-by: David Zearing --- apps/test-bundles/package.json | 1 + apps/test-bundles/webpack.config.js | 8 +- ...20-04-22-17-09-48-functional-checkbox.json | 8 + .../src/components/Table/Table.tsx | 2 +- packages/react-next/etc/react-next.api.md | 153 +++++- packages/react-next/src/Checkbox.ts | 2 +- .../src/components/Checkbox/Checkbox.base.tsx | 149 ++++++ .../src/components/Checkbox/Checkbox.doc.tsx | 40 ++ .../components/Checkbox/Checkbox.styles.ts | 302 +++++++++++ .../src/components/Checkbox/Checkbox.test.tsx | 233 +++++++++ .../src/components/Checkbox/Checkbox.tsx | 11 + .../src/components/Checkbox/Checkbox.types.ts | 194 +++++++ .../__snapshots__/Checkbox.test.tsx.snap | 475 ++++++++++++++++++ .../components/Checkbox/docs/CheckboxDonts.md | 3 + .../components/Checkbox/docs/CheckboxDos.md | 1 + .../Checkbox/docs/CheckboxOverview.md | 9 + .../docs/new/CheckboxBestPractices.md | 12 + .../Checkbox/docs/new/CheckboxOverview.md | 5 + .../examples/Checkbox.Basic.Example.tsx | 25 + .../Checkbox.Indeterminate.Example.tsx | 45 ++ .../examples/Checkbox.Other.Example.tsx | 43 ++ .../src/components/Checkbox/index.ts | 3 + packages/react-next/src/index.ts | 83 ++- packages/react-next/tslint.json | 6 +- 24 files changed, 1805 insertions(+), 8 deletions(-) create mode 100644 change/@fluentui-react-next-2020-04-22-17-09-48-functional-checkbox.json create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.base.tsx create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.doc.tsx create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.styles.ts create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.test.tsx create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.tsx create mode 100644 packages/react-next/src/components/Checkbox/Checkbox.types.ts create mode 100644 packages/react-next/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap create mode 100644 packages/react-next/src/components/Checkbox/docs/CheckboxDonts.md create mode 100644 packages/react-next/src/components/Checkbox/docs/CheckboxDos.md create mode 100644 packages/react-next/src/components/Checkbox/docs/CheckboxOverview.md create mode 100644 packages/react-next/src/components/Checkbox/docs/new/CheckboxBestPractices.md create mode 100644 packages/react-next/src/components/Checkbox/docs/new/CheckboxOverview.md create mode 100644 packages/react-next/src/components/Checkbox/examples/Checkbox.Basic.Example.tsx create mode 100644 packages/react-next/src/components/Checkbox/examples/Checkbox.Indeterminate.Example.tsx create mode 100644 packages/react-next/src/components/Checkbox/examples/Checkbox.Other.Example.tsx create mode 100644 packages/react-next/src/components/Checkbox/index.ts diff --git a/apps/test-bundles/package.json b/apps/test-bundles/package.json index 9232f242b86e17..3d5006969aadc9 100644 --- a/apps/test-bundles/package.json +++ b/apps/test-bundles/package.json @@ -34,6 +34,7 @@ "@uifabric/set-version": "^7.0.11", "@uifabric/styling": "^7.12.0", "office-ui-fabric-react": "^7.109.1", + "@fluentui/react-next": "^8.0.0-alpha.0", "react": "16.8.6", "react-app-polyfill": "~1.0.1", "react-dom": "16.8.6", diff --git a/apps/test-bundles/webpack.config.js b/apps/test-bundles/webpack.config.js index 14b18df2747065..346c1986bd06e6 100644 --- a/apps/test-bundles/webpack.config.js +++ b/apps/test-bundles/webpack.config.js @@ -12,6 +12,9 @@ const resolvePath = (packageName, entryFileName = 'index.js') => // Create entries for all top level fabric imports. const Entries = _buildEntries('office-ui-fabric-react'); +// Create entries for all top level fabric imports. +_buildEntries('@fluentui/react-next', Entries); + // Add entry for keyboard-key package. Entries['keyboard-key'] = resolvePath('@fluentui/keyboard-key'); @@ -63,8 +66,7 @@ module.exports = Object.keys(Entries).map( /** * Build webpack entries based on top level imports available in a package. */ -function _buildEntries(packageName) { - const entries = {}; +function _buildEntries(packageName, entries = {}) { let packagePath = ''; try { @@ -85,7 +87,7 @@ function _buildEntries(packageName) { // Replace commonjs paths with lib paths. const entryPath = path.join(packagePath, itemName); - entries[`${packageName}-${entryName}`] = entryPath; + entries[`${packageName.replace('@', '').replace('/', '-')}-${entryName}`] = entryPath; } }); diff --git a/change/@fluentui-react-next-2020-04-22-17-09-48-functional-checkbox.json b/change/@fluentui-react-next-2020-04-22-17-09-48-functional-checkbox.json new file mode 100644 index 00000000000000..b82fc82471558a --- /dev/null +++ b/change/@fluentui-react-next-2020-04-22-17-09-48-functional-checkbox.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Migrate Checkbox to functional component", + "packageName": "@fluentui/react-next", + "email": "miclo@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-04-22T17:09:48.443Z" +} \ No newline at end of file diff --git a/packages/fluentui/react-northstar/src/components/Table/Table.tsx b/packages/fluentui/react-northstar/src/components/Table/Table.tsx index 088c372042e721..8f6a31ac5af178 100644 --- a/packages/fluentui/react-northstar/src/components/Table/Table.tsx +++ b/packages/fluentui/react-northstar/src/components/Table/Table.tsx @@ -58,7 +58,7 @@ export const tableSlotClassNames: TableSlotClassNames = { header: `${tableClassName}__header`, }; -export type TableStylesProps = never +export type TableStylesProps = never; export const Table: React.FC> & FluentComponentStaticProps & { diff --git a/packages/react-next/etc/react-next.api.md b/packages/react-next/etc/react-next.api.md index 253520e1a37b9a..62ac7132539931 100644 --- a/packages/react-next/etc/react-next.api.md +++ b/packages/react-next/etc/react-next.api.md @@ -4,8 +4,159 @@ ```ts +import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; +import { IKeytipProps } from 'office-ui-fabric-react/lib/Keytip'; +import { IRefObject } from 'office-ui-fabric-react/lib/Utilities'; +import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; +import { IStyle } from 'office-ui-fabric-react/lib/Styling'; +import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities'; +import { ITheme } from 'office-ui-fabric-react/lib/Styling'; +import * as React from 'react'; -export * from "office-ui-fabric-react"; +// @public (undocumented) +export const Checkbox: React.FunctionComponent; + +// @public (undocumented) +export const CheckboxBase: React.ForwardRefExoticComponent>; + +// @public +export interface ICheckbox { + checked: boolean; + focus: () => void; + indeterminate: boolean; +} + +// @public +export interface ICheckboxProps extends React.ButtonHTMLAttributes { + ariaDescribedBy?: string; + ariaLabel?: string; + ariaLabelledBy?: string; + ariaPositionInSet?: number; + ariaSetSize?: number; + boxSide?: 'start' | 'end'; + checked?: boolean; + checkmarkIconProps?: IIconProps; + className?: string; + componentRef?: IRefObject; + defaultChecked?: boolean; + defaultIndeterminate?: boolean; + disabled?: boolean; + indeterminate?: boolean; + inputProps?: React.ButtonHTMLAttributes; + keytipProps?: IKeytipProps; + label?: string; + onChange?: (ev?: React.FormEvent, checked?: boolean) => void; + onRenderLabel?: IRenderFunction; + styles?: IStyleFunctionOrObject; + theme?: ITheme; +} + +// @public (undocumented) +export interface ICheckboxStyleProps { + // (undocumented) + checked?: boolean; + // (undocumented) + className?: string; + // (undocumented) + disabled?: boolean; + // (undocumented) + indeterminate?: boolean; + // (undocumented) + isUsingCustomLabelRender: boolean; + // (undocumented) + reversed?: boolean; + // (undocumented) + theme: ITheme; +} + +// @public (undocumented) +export interface ICheckboxStyles { + checkbox?: IStyle; + checkmark?: IStyle; + input?: IStyle; + label?: IStyle; + root?: IStyle; + text?: IStyle; +} + + +export * from "office-ui-fabric-react/lib/ActivityItem"; +export * from "office-ui-fabric-react/lib/Announced"; +export * from "office-ui-fabric-react/lib/Autofill"; +export * from "office-ui-fabric-react/lib/Breadcrumb"; +export * from "office-ui-fabric-react/lib/Button"; +export * from "office-ui-fabric-react/lib/Calendar"; +export * from "office-ui-fabric-react/lib/Callout"; +export * from "office-ui-fabric-react/lib/Check"; +export * from "office-ui-fabric-react/lib/ChoiceGroup"; +export * from "office-ui-fabric-react/lib/Coachmark"; +export * from "office-ui-fabric-react/lib/Color"; +export * from "office-ui-fabric-react/lib/ColorPicker"; +export * from "office-ui-fabric-react/lib/ComboBox"; +export * from "office-ui-fabric-react/lib/CommandBar"; +export * from "office-ui-fabric-react/lib/ContextualMenu"; +export * from "office-ui-fabric-react/lib/DatePicker"; +export * from "office-ui-fabric-react/lib/DetailsList"; +export * from "office-ui-fabric-react/lib/Dialog"; +export * from "office-ui-fabric-react/lib/Divider"; +export * from "office-ui-fabric-react/lib/DocumentCard"; +export * from "office-ui-fabric-react/lib/Dropdown"; +export * from "office-ui-fabric-react/lib/ExtendedPicker"; +export * from "office-ui-fabric-react/lib/Fabric"; +export * from "office-ui-fabric-react/lib/Facepile"; +export * from "office-ui-fabric-react/lib/FloatingPicker"; +export * from "office-ui-fabric-react/lib/FocusTrapZone"; +export * from "office-ui-fabric-react/lib/FocusZone"; +export * from "office-ui-fabric-react/lib/Grid"; +export * from "office-ui-fabric-react/lib/GroupedList"; +export * from "office-ui-fabric-react/lib/HoverCard"; +export * from "office-ui-fabric-react/lib/Icon"; +export * from "office-ui-fabric-react/lib/Icons"; +export * from "office-ui-fabric-react/lib/Image"; +export * from "office-ui-fabric-react/lib/Keytip"; +export * from "office-ui-fabric-react/lib/KeytipData"; +export * from "office-ui-fabric-react/lib/KeytipLayer"; +export * from "office-ui-fabric-react/lib/Label"; +export * from "office-ui-fabric-react/lib/Layer"; +export * from "office-ui-fabric-react/lib/Link"; +export * from "office-ui-fabric-react/lib/List"; +export * from "office-ui-fabric-react/lib/MarqueeSelection"; +export * from "office-ui-fabric-react/lib/MessageBar"; +export * from "office-ui-fabric-react/lib/Modal"; +export * from "office-ui-fabric-react/lib/Nav"; +export * from "office-ui-fabric-react/lib/OverflowSet"; +export * from "office-ui-fabric-react/lib/Overlay"; +export * from "office-ui-fabric-react/lib/Panel"; +export * from "office-ui-fabric-react/lib/Persona"; +export * from "office-ui-fabric-react/lib/Pickers"; +export * from "office-ui-fabric-react/lib/Pivot"; +export * from "office-ui-fabric-react/lib/Popup"; +export * from "office-ui-fabric-react/lib/PositioningContainer"; +export * from "office-ui-fabric-react/lib/ProgressIndicator"; +export * from "office-ui-fabric-react/lib/Rating"; +export * from "office-ui-fabric-react/lib/ResizeGroup"; +export * from "office-ui-fabric-react/lib/ScrollablePane"; +export * from "office-ui-fabric-react/lib/SearchBox"; +export * from "office-ui-fabric-react/lib/SelectableOption"; +export * from "office-ui-fabric-react/lib/SelectedItemsList"; +export * from "office-ui-fabric-react/lib/Selection"; +export * from "office-ui-fabric-react/lib/Separator"; +export * from "office-ui-fabric-react/lib/Shimmer"; +export * from "office-ui-fabric-react/lib/ShimmeredDetailsList"; +export * from "office-ui-fabric-react/lib/Slider"; +export * from "office-ui-fabric-react/lib/SpinButton"; +export * from "office-ui-fabric-react/lib/Spinner"; +export * from "office-ui-fabric-react/lib/Stack"; +export * from "office-ui-fabric-react/lib/Sticky"; +export * from "office-ui-fabric-react/lib/Styling"; +export * from "office-ui-fabric-react/lib/SwatchColorPicker"; +export * from "office-ui-fabric-react/lib/TeachingBubble"; +export * from "office-ui-fabric-react/lib/Text"; +export * from "office-ui-fabric-react/lib/TextField"; +export * from "office-ui-fabric-react/lib/ThemeGenerator"; +export * from "office-ui-fabric-react/lib/Toggle"; +export * from "office-ui-fabric-react/lib/Tooltip"; +export * from "office-ui-fabric-react/lib/Utilities"; // (No @packageDocumentation comment for this package) diff --git a/packages/react-next/src/Checkbox.ts b/packages/react-next/src/Checkbox.ts index 36b71f0e231659..0c5e8af240aff5 100644 --- a/packages/react-next/src/Checkbox.ts +++ b/packages/react-next/src/Checkbox.ts @@ -1 +1 @@ -export * from 'office-ui-fabric-react/lib/Checkbox'; +export * from './components/Checkbox/index'; diff --git a/packages/react-next/src/components/Checkbox/Checkbox.base.tsx b/packages/react-next/src/components/Checkbox/Checkbox.base.tsx new file mode 100644 index 00000000000000..23dc2c1221e1d6 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.base.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { classNamesFunction, mergeAriaAttributeValues, warnMutuallyExclusive } from '../../Utilities'; +import { Icon } from '../../Icon'; +import { ICheckboxProps, ICheckboxStyleProps, ICheckboxStyles } from './Checkbox.types'; +import { KeytipData } from '../../KeytipData'; +import { useId, useControllableValue, useMergedRefs } from '@uifabric/react-hooks'; +import { useFocusRects } from 'office-ui-fabric-react'; + +const getClassNames = classNamesFunction(); + +export const CheckboxBase = React.forwardRef((props: ICheckboxProps, forwardedRef: React.Ref) => { + const { + className, + disabled, + inputProps, + name, + boxSide = 'start', + theme, + ariaLabel, + ariaLabelledBy, + ariaDescribedBy, + styles, + checkmarkIconProps, + ariaPositionInSet, + ariaSetSize, + keytipProps, + title, + label, + onChange, + } = props; + + const rootRef = React.useRef(null); + const mergedRootRefs = useMergedRefs(rootRef, forwardedRef); + const checkBox = React.useRef(null); + const [isChecked, setIsChecked] = useControllableValue(props.checked, props.defaultChecked, onChange); + const [isIndeterminate, setIsIndeterminate] = useControllableValue(props.indeterminate, props.defaultIndeterminate); + + useFocusRects(rootRef); + useDebugWarning(props); + useComponentRef(props, isChecked, isIndeterminate, checkBox); + + const id = useId('checkbox-', props.id); + const classNames: { [key in keyof ICheckboxStyles]: string } = getClassNames(styles!, { + theme: theme!, + className, + disabled, + indeterminate: isIndeterminate, + checked: isChecked, + reversed: boxSide !== 'start', + isUsingCustomLabelRender: !!props.onRenderLabel, + }); + + const onRenderLabel = (): JSX.Element | null => { + return label ? ( + + ) : null; + }; + + const _onChange = (ev: React.ChangeEvent): void => { + if (!isIndeterminate) { + setIsChecked(!isChecked, ev); + } else { + // If indeterminate, clicking the checkbox *only* removes the indeterminate state (or if + // controlled, lets the consumer know to change it by calling onChange). It doesn't + // change the checked state. + setIsChecked(!!isChecked, ev); + setIsIndeterminate(false); + } + }; + + return ( + + {(keytipAttributes: any): JSX.Element => ( +
+ + +
+ )} +
+ ); +}); + +CheckboxBase.displayName = 'CheckboxBase'; + +function useDebugWarning(props: ICheckboxProps) { + if (process.env.NODE_ENV !== 'production') { + // This is a build-time conditional that will be constant at runtime + // tslint:disable-next-line:react-hooks-nesting + React.useEffect(() => { + warnMutuallyExclusive('Checkbox', props, { + checked: 'defaultChecked', + indeterminate: 'defaultIndeterminate', + }); + }, []); + } +} + +function useComponentRef( + props: ICheckboxProps, + isChecked: boolean | undefined, + isIndeterminate: boolean | undefined, + checkBox: React.RefObject, +) { + React.useImperativeHandle( + props.componentRef, + () => ({ + get checked() { + return !!isChecked; + }, + get indeterminate() { + return !!isIndeterminate; + }, + focus() { + if (checkBox.current) { + checkBox.current.focus(); + } + }, + }), + [isChecked, isIndeterminate], + ); +} diff --git a/packages/react-next/src/components/Checkbox/Checkbox.doc.tsx b/packages/react-next/src/components/Checkbox/Checkbox.doc.tsx new file mode 100644 index 00000000000000..cb513e9e6c64a5 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.doc.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { CheckboxBasicExample } from './examples/Checkbox.Basic.Example'; +import { CheckboxIndeterminateExample } from './examples/Checkbox.Indeterminate.Example'; +import { CheckboxOtherExample } from './examples/Checkbox.Other.Example'; + +import { IDocPageProps } from 'office-ui-fabric-react/lib/common/DocPage.types'; + +const CheckboxBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/examples/Checkbox.Basic.Example.tsx') as string; +const CheckboxOtherExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/examples/Checkbox.Other.Example.tsx') as string; +const CheckboxIndeterminateExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/examples/Checkbox.Indeterminate.Example.tsx') as string; + +export const CheckboxPageProps: IDocPageProps = { + title: 'Checkbox', + componentName: 'Checkbox', + componentUrl: + 'https://github.com/microsoft/fluentui/tree/master/packages/office-ui-fabric-react/src/components/Checkbox', + examples: [ + { + title: 'Basic Checkboxes', + code: CheckboxBasicExampleCode, + view: , + }, + { + title: 'Other Implementation Examples', + code: CheckboxOtherExampleCode, + view: , + }, + { + title: 'Indeterminate Checkboxes', + code: CheckboxIndeterminateExampleCode, + view: , + }, + ], + overview: require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/docs/CheckboxOverview.md'), + bestPractices: '', + dos: require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/docs/CheckboxDos.md'), + donts: require('!raw-loader!office-ui-fabric-react/src/components/Checkbox/docs/CheckboxDonts.md'), + isHeaderVisible: true, + isFeedbackVisible: true, +}; diff --git a/packages/react-next/src/components/Checkbox/Checkbox.styles.ts b/packages/react-next/src/components/Checkbox/Checkbox.styles.ts new file mode 100644 index 00000000000000..f68447e85e24fb --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.styles.ts @@ -0,0 +1,302 @@ +import { ICheckboxStyleProps, ICheckboxStyles } from './Checkbox.types'; +import { HighContrastSelector, getGlobalClassNames, IStyle } from '../../Styling'; +import { IsFocusVisibleClassName } from '../../Utilities'; + +const GlobalClassNames = { + root: 'ms-Checkbox', + label: 'ms-Checkbox-label', + checkbox: 'ms-Checkbox-checkbox', + checkmark: 'ms-Checkbox-checkmark', + text: 'ms-Checkbox-text', +}; + +const MS_CHECKBOX_LABEL_SIZE = '20px'; +const MS_CHECKBOX_TRANSITION_DURATION = '200ms'; +const MS_CHECKBOX_TRANSITION_TIMING = 'cubic-bezier(.4, 0, .23, 1)'; + +export const getStyles = (props: ICheckboxStyleProps): ICheckboxStyles => { + const { className, theme, reversed, checked, disabled, isUsingCustomLabelRender, indeterminate } = props; + const { semanticColors, effects, palette, fonts } = theme; + + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + const checkmarkFontColor = semanticColors.inputForegroundChecked; + // TODO: after updating the semanticColors slots mapping this needs to be semanticColors.inputBorder + const checkmarkFontColorHovered = palette.neutralSecondary; + // TODO: after updating the semanticColors slots mapping this needs to be semanticColors.smallInputBorder + const checkboxBorderColor = palette.neutralPrimary; + const checkboxBorderIndeterminateColor = semanticColors.inputBackgroundChecked; + const checkboxBorderColorChecked = semanticColors.inputBackgroundChecked; + const checkboxBorderColorDisabled = semanticColors.disabledBodySubtext; + const checkboxBorderHoveredColor = semanticColors.inputBorderHovered; + const checkboxBorderIndeterminateHoveredColor = semanticColors.inputBackgroundCheckedHovered; + const checkboxBackgroundChecked = semanticColors.inputBackgroundChecked; + // TODO: after updating the semanticColors slots mapping the following 2 tokens need to be + // semanticColors.inputBackgroundCheckedHovered + const checkboxBackgroundCheckedHovered = semanticColors.inputBackgroundCheckedHovered; + const checkboxBorderColorCheckedHovered = semanticColors.inputBackgroundCheckedHovered; + const checkboxHoveredTextColor = semanticColors.inputTextHovered; + const checkboxBackgroundDisabledChecked = semanticColors.disabledBodySubtext; + const checkboxTextColor = semanticColors.bodyText; + const checkboxTextColorDisabled = semanticColors.disabledText; + + const indeterminateDotStyles: IStyle = [ + { + content: '""', + borderRadius: effects.roundedCorner2, + position: 'absolute', + width: 10, + height: 10, + top: 4, + left: 4, + boxSizing: 'border-box', + borderWidth: 5, + borderStyle: 'solid', + borderColor: disabled ? checkboxBorderColorDisabled : checkboxBorderIndeterminateColor, + transitionProperty: 'border-width, border, border-color', + transitionDuration: MS_CHECKBOX_TRANSITION_DURATION, + transitionTimingFunction: MS_CHECKBOX_TRANSITION_TIMING, + }, + ]; + + return { + root: [ + classNames.root, + { + position: 'relative', + display: 'flex', + }, + reversed && 'reversed', + checked && 'is-checked', + !disabled && 'is-enabled', + disabled && 'is-disabled', + !disabled && [ + !checked && { + selectors: { + [`:hover .${classNames.checkbox}`]: { + borderColor: checkboxBorderHoveredColor, + selectors: { + [HighContrastSelector]: { + borderColor: 'Highlight', + }, + }, + }, + [`:focus .${classNames.checkbox}`]: { borderColor: checkboxBorderHoveredColor }, + [`:hover .${classNames.checkmark}`]: { + color: checkmarkFontColorHovered, + opacity: '1', + selectors: { + [HighContrastSelector]: { + color: 'Highlight', + }, + }, + }, + }, + }, + checked && + !indeterminate && { + selectors: { + [`:hover .${classNames.checkbox}`]: { + background: checkboxBackgroundCheckedHovered, + borderColor: checkboxBorderColorCheckedHovered, + }, + [`:focus .${classNames.checkbox}`]: { + background: checkboxBackgroundCheckedHovered, + borderColor: checkboxBorderColorCheckedHovered, + }, + [`.${classNames.checkbox}`]: { + background: checkboxBorderColorChecked, + borderColor: checkboxBorderColorChecked, + }, + [HighContrastSelector]: { + selectors: { + [`:hover .${classNames.checkbox}`]: { + background: 'Window', + borderColor: 'Highlight', + }, + [`:focus .${classNames.checkbox}`]: { + background: 'Highlight', + }, + [`:focus:hover .${classNames.checkbox}`]: { + background: 'Highlight', + }, + [`:focus:hover .${classNames.checkmark}`]: { + color: 'Window', + }, + [`:hover .${classNames.checkmark}`]: { + color: 'Highlight', + }, + }, + }, + }, + }, + indeterminate && { + selectors: { + [`:hover .${classNames.checkbox}, :hover .${classNames.checkbox}:after`]: { + borderColor: checkboxBorderIndeterminateHoveredColor, + }, + [`:focus .${classNames.checkbox}`]: { + borderColor: checkboxBorderIndeterminateHoveredColor, + }, + [`:hover .${classNames.checkmark}`]: { + opacity: '0', + }, + }, + }, + { + selectors: { + [`:hover .${classNames.text}`]: { color: checkboxHoveredTextColor }, + [`:focus .${classNames.text}`]: { color: checkboxHoveredTextColor }, + }, + }, + ], + className, + ], + input: { + position: 'absolute', + background: 'none', + + opacity: 0, + selectors: { + [`.${IsFocusVisibleClassName} &:focus + label::before`]: { + outline: '1px solid ' + theme.palette.neutralSecondary, + outlineOffset: '2px', + selectors: { + [HighContrastSelector]: { + outline: '1px solid ActiveBorder', + }, + }, + }, + }, + }, + label: [ + classNames.label, + theme.fonts.medium, + { + display: 'flex', + alignItems: isUsingCustomLabelRender ? 'center' : 'flex-start', + cursor: disabled ? 'default' : 'pointer', + position: 'relative', + userSelect: 'none', + textAlign: 'left', + }, + reversed && { + flexDirection: 'row-reverse', + justifyContent: 'flex-end', + }, + { + selectors: { + '&::before': { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + content: '""', + pointerEvents: 'none', + }, + }, + }, + ], + checkbox: [ + classNames.checkbox, + { + position: 'relative', + display: 'flex', + flexShrink: 0, + alignItems: 'center', + justifyContent: 'center', + height: MS_CHECKBOX_LABEL_SIZE, + width: MS_CHECKBOX_LABEL_SIZE, + border: `1px solid ${checkboxBorderColor}`, + borderRadius: effects.roundedCorner2, + boxSizing: 'border-box', + transitionProperty: 'background, border, border-color', + transitionDuration: MS_CHECKBOX_TRANSITION_DURATION, + transitionTimingFunction: MS_CHECKBOX_TRANSITION_TIMING, + + /* in case the icon is bigger than the box */ + overflow: 'hidden', + selectors: { + ':after': indeterminate ? indeterminateDotStyles : null, + }, + }, + indeterminate && { + borderColor: checkboxBorderIndeterminateColor, + }, + !reversed + ? // This margin on the checkbox is for backwards compat. Notably it has the effect where a customRender + // is used, there will be only a 4px margin from checkbox to label. The label by default would have + // another 4px margin for a total of 8px margin between checkbox and label. We don't combine the two + // (and move it into the text) to not incur a breaking change for everyone using custom render atm. + { + marginRight: 4, + } + : { + marginLeft: 4, + }, + !disabled && + !indeterminate && + checked && { + background: checkboxBackgroundChecked, + borderColor: checkboxBorderColorChecked, + selectors: { + [HighContrastSelector]: { + background: 'Highlight', + borderColor: 'Highlight', + }, + }, + }, + disabled && { + borderColor: checkboxBorderColorDisabled, + selectors: { + [HighContrastSelector]: { + borderColor: 'InactiveBorder', + }, + }, + }, + checked && + disabled && { + background: checkboxBackgroundDisabledChecked, + borderColor: checkboxBorderColorDisabled, + }, + ], + checkmark: [ + classNames.checkmark, + { + opacity: checked ? '1' : '0', + color: checkmarkFontColor, + selectors: { + [HighContrastSelector]: { + color: disabled ? 'InactiveBorder' : 'Window', + MsHighContrastAdjust: 'none', + }, + }, + }, + ], + text: [ + classNames.text, + { + color: disabled ? checkboxTextColorDisabled : checkboxTextColor, + fontSize: fonts.medium.fontSize, + lineHeight: '20px', + }, + !reversed + ? { + marginLeft: 4, + } + : { + marginRight: 4, + }, + disabled && { + selectors: { + [HighContrastSelector]: { + // backwards compat for the color of the text when the checkbox was rendered + // using a Button. + color: 'InactiveBorder', + }, + }, + }, + ], + }; +}; diff --git a/packages/react-next/src/components/Checkbox/Checkbox.test.tsx b/packages/react-next/src/components/Checkbox/Checkbox.test.tsx new file mode 100644 index 00000000000000..280cdc12a395b5 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.test.tsx @@ -0,0 +1,233 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import { mount, ReactWrapper } from 'enzyme'; + +import { Checkbox } from './Checkbox'; +import { IRefObject } from 'office-ui-fabric-react/lib/Utilities'; +import { ICheckbox } from './Checkbox.types'; + +let checkbox: ICheckbox | undefined; +/** Use this as the componentRef when rendering a Checkbox. */ +const checkboxRef: IRefObject = (ref: ICheckbox | null) => { + checkbox = ref!; +}; + +const IndeterminateControlledCheckbox: React.FunctionComponent = () => { + const [indeterminate, setIndeterminate] = React.useState(true); + const [checked, setChecked] = React.useState(false); + const onChange = (ev: React.FormEvent, newChecked: boolean): void => { + // On first change, clear the indeterminate state and don't modify the checked state + indeterminate ? setIndeterminate(false) : setChecked(!!newChecked); + }; + + return ; +}; + +describe('Checkbox', () => { + let renderedComponent: renderer.ReactTestRenderer | undefined; + let component: ReactWrapper | undefined; + + afterEach(() => { + checkbox = undefined; + if (renderedComponent) { + renderedComponent.unmount(); + renderedComponent = undefined; + } + if (component) { + component.unmount(); + component = undefined; + } + }); + + it('renders unchecked correctly', () => { + renderedComponent = renderer.create(); + const tree = renderedComponent.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders checked correctly', () => { + renderedComponent = renderer.create(); + const tree = renderedComponent.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders indeterminate correctly', () => { + renderedComponent = renderer.create(); + const tree = renderedComponent.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('respects id prop', () => { + component = mount(); + expect(component.find('input').prop('id')).toEqual('my-checkbox'); + }); + + it('defaults to unchecked non-indeterminate', () => { + component = mount(); + + const input = component.find('input'); + expect(input.prop('checked')).toBe(false); + // Mainly testing aria-checked because later in the indeterminate cases, it's the only way to + // tell from a rendered prop that the checkbox is indeterminate + expect(input.prop('aria-checked')).toBe('false'); + expect(checkbox!.checked).toBe(false); + expect(checkbox!.indeterminate).toBe(false); + }); + + it('respects defaultChecked prop', () => { + component = mount(); + + const input = component.find('input'); + expect(input.prop('checked')).toBe(true); + expect(input.prop('aria-checked')).toBe('true'); + expect(checkbox!.checked).toBe(true); + }); + + it('ignores defaultChecked updates', () => { + component = mount(); + component.setProps({ defaultChecked: false }); + expect(component.find('input').prop('checked')).toBe(true); + expect(checkbox!.checked).toBe(true); + component.unmount(); + + component = mount(); + component.setProps({ defaultChecked: true }); + expect(component.find('input').prop('checked')).toBe(false); + expect(checkbox!.checked).toBe(false); + }); + + it('respects checked prop', () => { + component = mount(); + + const input = component.find('input'); + expect(input.prop('checked')).toBe(true); + expect(input.prop('aria-checked')).toBe('true'); + expect(checkbox!.checked).toBe(true); + }); + + it('respects checked updates', () => { + component = mount(); + + component.setProps({ checked: false }); + expect(component.find('input').prop('checked')).toBe(false); + expect(checkbox!.checked).toBe(false); + }); + + it('automatically updates on change when uncontrolled', () => { + const onChange = jest.fn(); + component = mount(); + + component.find('input').simulate('change'); + + expect(component.find('input').prop('checked')).toBe(true); + expect(checkbox!.checked).toBe(true); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('does not automatically update on change when controlled', () => { + const onChange = jest.fn(); + component = mount(); + + component.find('input').simulate('change'); + + // doesn't update automatically (but calls onChange) + expect(component.find('input').prop('checked')).toBe(false); + expect(checkbox!.checked).toBe(false); + expect(onChange).toHaveBeenCalledTimes(1); + + // updates when props update + component.setProps({ checked: true }); + expect(component.find('input').prop('checked')).toBe(true); + expect(checkbox!.checked).toBe(true); + // doesn't call onChange for props update + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('respects defaultIndeterminate prop', () => { + component = mount(); + + expect(component.find('input').prop('aria-checked')).toBe('mixed'); + expect(checkbox!.indeterminate).toEqual(true); + }); + + it('respects defaultIndeterminate prop when defaultChecked is true', () => { + component = mount(); + + expect(component.find('input').prop('aria-checked')).toBe('mixed'); + expect(checkbox!.indeterminate).toEqual(true); + }); + + it('ignores defaultIndeterminate updates', () => { + component = mount(); + component.setProps({ defaultIndeterminate: false }); + expect(component.find('input').prop('aria-checked')).toBe('mixed'); + expect(checkbox!.indeterminate).toEqual(true); + component.unmount(); + + component = mount(); + component.setProps({ defaultIndeterminate: true }); + expect(component.find('input').prop('aria-checked')).toBe('false'); + expect(checkbox!.indeterminate).toEqual(false); + }); + + it('removes uncontrolled indeterminate state', () => { + component = mount(); + + let input = component.find('input'); + expect(input.prop('aria-checked')).toBe('mixed'); + expect(input.prop('checked')).toBe(false); + expect(checkbox!.indeterminate).toEqual(true); + + input.simulate('change'); + + // get an updated ReactWrapper for the input (otherwise it would be out of sync) + input = component.find('input'); + expect(input.prop('aria-checked')).toBe('false'); + expect(input.prop('checked')).toBe(false); + expect(checkbox!.indeterminate).toEqual(false); + }); + + it('renders with indeterminate when controlled', () => { + component = mount(); + + let input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(true); + expect(input.prop('aria-checked')).toBe('mixed'); + + input.simulate('change', { target: { checked: true } }); + + input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(false); + expect(input.prop('aria-checked')).toBe('false'); + }); + + it('removes controlled indeterminate', () => { + component = mount(); + + let input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(true); + expect(checkbox!.checked).toEqual(false); + expect(input.prop('aria-checked')).toBe('mixed'); + + input.simulate('change'); + + input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(false); + expect(checkbox!.checked).toEqual(false); + expect(input.prop('aria-checked')).toBe('false'); + }); + + it("doesn't remove controlled indeterminate when no onChange provided", () => { + component = mount(); + + let input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(true); + expect(input.prop('aria-checked')).toBe('mixed'); + + input.simulate('change'); + + input = component.find('input'); + expect(checkbox!.indeterminate).toEqual(true); + expect(input.prop('aria-checked')).toBe('mixed'); + }); +}); diff --git a/packages/react-next/src/components/Checkbox/Checkbox.tsx b/packages/react-next/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 00000000000000..68359f4002ea7f --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { styled } from '../../Utilities'; +import { CheckboxBase } from './Checkbox.base'; +import { getStyles } from './Checkbox.styles'; +import { ICheckboxProps, ICheckboxStyleProps, ICheckboxStyles } from './Checkbox.types'; + +export const Checkbox: React.FunctionComponent = styled< + ICheckboxProps, + ICheckboxStyleProps, + ICheckboxStyles +>(CheckboxBase, getStyles, undefined, { scope: 'Checkbox' }); diff --git a/packages/react-next/src/components/Checkbox/Checkbox.types.ts b/packages/react-next/src/components/Checkbox/Checkbox.types.ts new file mode 100644 index 00000000000000..97b6ac8034d590 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/Checkbox.types.ts @@ -0,0 +1,194 @@ +import * as React from 'react'; +import { IStyle, ITheme } from '../../Styling'; +import { IRefObject, IRenderFunction, IStyleFunctionOrObject } from '../../Utilities'; +import { IIconProps } from '../../Icon'; +import { IKeytipProps } from '../../Keytip'; + +/** + * Checkbox class interface. + * {@docCategory Checkbox} + */ +export interface ICheckbox { + /** Gets the current indeterminate state. */ + indeterminate: boolean; + + /** Gets the current checked state. */ + checked: boolean; + + /** Sets focus to the checkbox. */ + focus: () => void; +} + +/** + * Checkbox properties. + * {@docCategory Checkbox} + */ +export interface ICheckboxProps extends React.ButtonHTMLAttributes { + /** + * Optional callback to access the ICheckbox interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: IRefObject; + + /** + * Additional class name to provide on the root element, in addition to the ms-Checkbox class. + */ + className?: string; + + /** + * Checked state. Mutually exclusive to "defaultChecked". Use this if you control the checked state at a higher + * level and plan to pass in the correct value based on handling onChange events and re-rendering. + */ + checked?: boolean; + + /** + * Default checked state. Mutually exclusive to "checked". Use this if you want an uncontrolled component, and + * want the Checkbox instance to maintain its own state. + */ + defaultChecked?: boolean; + + /** + * Label to display next to the checkbox. + */ + label?: string; + + /** + * Disabled state of the checkbox. + */ + disabled?: boolean; + + /** + * Callback that is called when the checked value has changed. + */ + onChange?: (ev?: React.FormEvent, checked?: boolean) => void; + + /** + * Optional input props that will be mixed into the input element, *before* other props are applied. This allows + * you to extend the input element with additional attributes, such as data-automation-id needed for automation. + * Note that if you provide, for example, "disabled" as well as "inputProps.disabled", the former will take + * precedence over the later. + */ + inputProps?: React.ButtonHTMLAttributes; + + /** + * Allows you to set the checkbox to be at the before (start) or after (end) the label. + * @defaultvalue 'start' + */ + boxSide?: 'start' | 'end'; + + /** + * Theme provided by HOC. + */ + theme?: ITheme; + + /** + * Accessible label for the checkbox. + */ + ariaLabel?: string; + + /** + * ID for element that contains label information for the checkbox. + */ + ariaLabelledBy?: string; + + /** + * ID for element that provides extended information for the checkbox. + */ + ariaDescribedBy?: string; + + /** + * The position in the parent set (if in a set) for aria-posinset. + */ + ariaPositionInSet?: number; + + /** + * The total size of the parent set (if in a set) for aria-setsize. + */ + ariaSetSize?: number; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + styles?: IStyleFunctionOrObject; + + /** + * Custom render function for the label. + */ + onRenderLabel?: IRenderFunction; + + /** + * Custom icon props for the check mark rendered by the checkbox + */ + checkmarkIconProps?: IIconProps; + + /** + * Optional keytip for this checkbox + */ + keytipProps?: IKeytipProps; + + /** + * Optional controlled indeterminate visual state for checkbox. Setting indeterminate state takes visual precedence + * over checked or defaultChecked props given but does not affect checked state. + * This should not be a toggleable state. On load the checkbox will receive indeterminate visual state + * and after the first user click it should be removed by your supplied onChange callback + * function exposing the true state of the checkbox. + */ + indeterminate?: boolean; + + /** + * Optional uncontrolled indeterminate visual state for checkbox. Setting indeterminate state takes visual precedence + * over checked or defaultChecked props given but does not affect checked state. + * This is not a toggleable state. On load the checkbox will receive indeterminate visual state + * and after the user's first click it will be removed exposing the true state of the checkbox. + */ + defaultIndeterminate?: boolean; +} + +/** + * {@docCategory Checkbox} + */ +export interface ICheckboxStyleProps { + theme: ITheme; + className?: string; + disabled?: boolean; + checked?: boolean; + reversed?: boolean; + indeterminate?: boolean; + isUsingCustomLabelRender: boolean; +} + +/** + * {@docCategory Checkbox} + */ +export interface ICheckboxStyles { + /** + * Style for the root element (a button) of the checkbox component in the default enabled/unchecked state. + */ + root?: IStyle; + + /** + * INTERNAL: This is mostly an internal implementation detail which you should avoid styling. + * This refers to the element that is typically hidden and not rendered on screen. + */ + input?: IStyle; + + /** + * Style for the label part (contains the customized checkbox + text) when enabled. + */ + label?: IStyle; + + /** + * Style for checkbox in its default unchecked/enabled state. + */ + checkbox?: IStyle; + + /** + * Style for the checkmark in the default enabled/unchecked state. + */ + checkmark?: IStyle; + + /** + * Style for text appearing with the checkbox in its default enabled state. + */ + text?: IStyle; +} diff --git a/packages/react-next/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/packages/react-next/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap new file mode 100644 index 00000000000000..01f3f065cb68d9 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -0,0 +1,475 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Checkbox renders checked correctly 1`] = ` +
+ + +
+`; + +exports[`Checkbox renders indeterminate correctly 1`] = ` +
+ + +
+`; + +exports[`Checkbox renders unchecked correctly 1`] = ` +
+ + +
+`; diff --git a/packages/react-next/src/components/Checkbox/docs/CheckboxDonts.md b/packages/react-next/src/components/Checkbox/docs/CheckboxDonts.md new file mode 100644 index 00000000000000..7d64709bef2830 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/docs/CheckboxDonts.md @@ -0,0 +1,3 @@ +- Don't use a Checkbox as an on/off control. Instead use a toggle switch. +- Don’t use a Checkbox when the user can choose only one option from the group, use radio buttons instead. +- Don't put two groups of Checkboxes next to each other. Separate the two groups with labels. diff --git a/packages/react-next/src/components/Checkbox/docs/CheckboxDos.md b/packages/react-next/src/components/Checkbox/docs/CheckboxDos.md new file mode 100644 index 00000000000000..5dbbb19fedc3a6 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/docs/CheckboxDos.md @@ -0,0 +1 @@ +- Allow users to choose any combination of options when several Checkboxes are grouped together. diff --git a/packages/react-next/src/components/Checkbox/docs/CheckboxOverview.md b/packages/react-next/src/components/Checkbox/docs/CheckboxOverview.md new file mode 100644 index 00000000000000..b1fe374026c4c9 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/docs/CheckboxOverview.md @@ -0,0 +1,9 @@ +A Checkbox is a UI element that allows users to switch between two mutually exclusive options (checked or unchecked, on or off) through a single click or tap. It can also be used to indicate a subordinate setting or preference when paired with another control. + +A Checkbox is used to select or deselect action items. It can be used for a single item or for a list of multiple items that a user can choose from. The control has two selection states: unselected and selected. + +Use a single Checkbox for a subordinate setting, such as with a "Remember me?" login scenario or with a terms of service agreement. + +For a binary choice, the main difference between a Checkbox and a toggle switch is that the Checkbox is for status and the toggle switch is for action. You can delay committing a Checkbox interaction (as part of a form submit, for example), while you should immediately commit a toggle switch interaction. Also, only Checkboxes allow for multi-selection. + +Use multiple Checkboxes for multi-select scenarios in which a user chooses one or more items from a group of choices that are not mutually exclusive. diff --git a/packages/react-next/src/components/Checkbox/docs/new/CheckboxBestPractices.md b/packages/react-next/src/components/Checkbox/docs/new/CheckboxBestPractices.md new file mode 100644 index 00000000000000..e94548bffde600 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/docs/new/CheckboxBestPractices.md @@ -0,0 +1,12 @@ +### Layout + +- Use a single check box when there's only one selection to make or choice to confirm. Selecting a blank check box selects it. Selecting it again clears the check box. +- Use multiple check boxes when one or more options can be selected from a group. Unlike radio buttons, selecting one check box will not clear another check box. + +### Content + +- Separate two groups of check boxes with headings rather than positioning them one after the other. +- Use sentence-style capitalization. +- Don't use end punctuation (unless the check box label absolutely requires multiple sentences). +- Use a sentence fragment for the label, rather than a full sentence. +- Make it easy for people to understand what will happen if they select or clear a check box. diff --git a/packages/react-next/src/components/Checkbox/docs/new/CheckboxOverview.md b/packages/react-next/src/components/Checkbox/docs/new/CheckboxOverview.md new file mode 100644 index 00000000000000..b9614423cb3950 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/docs/new/CheckboxOverview.md @@ -0,0 +1,5 @@ +Check boxes (code name: `Checkbox`) give people a way to select one or more items from a group, or switch between two mutually exclusive options (checked or unchecked, on or off). + +[Sketch toolkit]() + +[Figma toolkit]() diff --git a/packages/react-next/src/components/Checkbox/examples/Checkbox.Basic.Example.tsx b/packages/react-next/src/components/Checkbox/examples/Checkbox.Basic.Example.tsx new file mode 100644 index 00000000000000..83d34c45ff43d5 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/examples/Checkbox.Basic.Example.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; + +// Used to add spacing between example checkboxes +const stackTokens = { childrenGap: 10 }; + +export const CheckboxBasicExample: React.FunctionComponent = () => { + // These checkboxes are uncontrolled because they don't set the `checked` prop. + return ( + + + + + + + + + + ); +}; + +function _onChange(ev: React.FormEvent, isChecked: boolean) { + console.log(`The option has been changed to ${isChecked}.`); +} diff --git a/packages/react-next/src/components/Checkbox/examples/Checkbox.Indeterminate.Example.tsx b/packages/react-next/src/components/Checkbox/examples/Checkbox.Indeterminate.Example.tsx new file mode 100644 index 00000000000000..148c43a9dcdf05 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/examples/Checkbox.Indeterminate.Example.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; + +// Used to add spacing between example checkboxes +const stackTokens = { childrenGap: 10 }; + +export const CheckboxIndeterminateExample: React.FunctionComponent = () => { + // Only used for the controlled checkbox (the last one) + const [isIndeterminate, setIsIndeterminate] = React.useState(true); + const [isChecked, setIsChecked] = React.useState(false); + const onChange = React.useCallback( + (ev: React.FormEvent, newChecked: boolean) => { + if (isIndeterminate) { + // If the checkbox was indeterminate, the first click should remove the indeterminate state + // without affecting the checked state + setIsIndeterminate(false); + } else { + setIsChecked(newChecked); + } + }, + [isIndeterminate], + ); + + return ( + + + + + + + + + + ); +}; diff --git a/packages/react-next/src/components/Checkbox/examples/Checkbox.Other.Example.tsx b/packages/react-next/src/components/Checkbox/examples/Checkbox.Other.Example.tsx new file mode 100644 index 00000000000000..3129c2393fdc3a --- /dev/null +++ b/packages/react-next/src/components/Checkbox/examples/Checkbox.Other.Example.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { Checkbox, ICheckboxProps } from 'office-ui-fabric-react/lib/Checkbox'; +import { Link } from 'office-ui-fabric-react/lib/Link'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; + +// Optional extra props to pass through to the input element +const inputProps: ICheckboxProps['inputProps'] = { + onFocus: () => console.log('Checkbox is focused'), + onBlur: () => console.log('Checkbox is blurred'), +}; +// Used to add spacing between example checkboxes +const stackTokens = { childrenGap: 10 }; + +export const CheckboxOtherExample: React.FunctionComponent = () => { + // Only for the first checkbox, which is controlled + const [isChecked, setIsChecked] = React.useState(true); + const onChange = React.useCallback((ev: React.FormEvent, checked: boolean): void => { + setIsChecked(!!checked); + }, []); + + return ( + + + + + + + + + + ); +}; + +function _renderLabelWithLink() { + return ( + + Custom-rendered label with a{' '} + + link + + + ); +} diff --git a/packages/react-next/src/components/Checkbox/index.ts b/packages/react-next/src/components/Checkbox/index.ts new file mode 100644 index 00000000000000..a321308b610f96 --- /dev/null +++ b/packages/react-next/src/components/Checkbox/index.ts @@ -0,0 +1,3 @@ +export * from './Checkbox'; +export * from './Checkbox.base'; +export * from './Checkbox.types'; diff --git a/packages/react-next/src/index.ts b/packages/react-next/src/index.ts index 20c47e0289586c..8bdc8ab525bf31 100644 --- a/packages/react-next/src/index.ts +++ b/packages/react-next/src/index.ts @@ -1,2 +1,83 @@ +export * from './ActivityItem'; +export * from './Autofill'; +export * from './Announced'; +export * from './Breadcrumb'; +export * from './Button'; +export * from './Calendar'; +export * from './Callout'; +export * from './Check'; +export * from './Checkbox'; +export * from './ChoiceGroup'; +// export * from './ChoiceGroupOption'; // exported by ChoiceGroup +export * from './Coachmark'; +export * from './Color'; +export * from './ColorPicker'; +export * from './ComboBox'; +export * from './CommandBar'; +export * from './ContextualMenu'; +export * from './DatePicker'; +export * from './DetailsList'; +export * from './Dialog'; +export * from './Divider'; +export * from './DocumentCard'; +export * from './Dropdown'; +export * from './ExtendedPicker'; +export * from './Fabric'; +export * from './Facepile'; +export * from './FloatingPicker'; +export * from './FocusTrapZone'; +export * from './FocusZone'; +export * from './Grid'; +export * from './GroupedList'; +export * from './HoverCard'; +export * from './Icon'; +export * from './Icons'; +export * from './Image'; +export * from './Keytip'; +export * from './KeytipData'; +export * from './KeytipLayer'; +export * from './Label'; +export * from './Layer'; +export * from './Link'; +export * from './List'; +export * from './MarqueeSelection'; +export * from './MessageBar'; +export * from './Modal'; +export * from './Nav'; +export * from './OverflowSet'; +export * from './Overlay'; +export * from './Panel'; +export * from './Persona'; +export * from './PersonaCoin'; +// export * from './PersonaPresence'; (Exported as part of Persona) +export * from './Pickers'; +export * from './Pivot'; +export * from './Popup'; +export * from './PositioningContainer'; +export * from './ProgressIndicator'; +export * from './Rating'; +export * from './ResizeGroup'; +export * from './ScrollablePane'; +export * from './SearchBox'; +export * from './SelectableOption'; +export * from './SelectedItemsList'; +export * from './Selection'; +export * from './Separator'; +export * from './Shimmer'; +export * from './ShimmeredDetailsList'; +export * from './Slider'; +export * from './SpinButton'; +export * from './Spinner'; +export * from './Stack'; +export * from './Sticky'; +export * from './Styling'; +export * from './SwatchColorPicker'; +export * from './TeachingBubble'; +export * from './Text'; +export * from './TextField'; +export * from './ThemeGenerator'; +export * from './Toggle'; +export * from './Tooltip'; +export * from './Utilities'; + import './version'; -export * from 'office-ui-fabric-react'; diff --git a/packages/react-next/tslint.json b/packages/react-next/tslint.json index 06e306229f8738..d28f17c68e6b32 100644 --- a/packages/react-next/tslint.json +++ b/packages/react-next/tslint.json @@ -1,4 +1,8 @@ { "extends": ["@uifabric/tslint-rules"], - "rules": {} + "rules": { + "jsx-ban-props": false, + "no-any": false, + "import-blacklist": [true, { "../../Styling": ["FontSizes"] }] + } }