Skip to content

Commit

Permalink
Migrate Checkbox to functional component (#12614)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>

* Update packages/react-hooks/README.md

Co-Authored-By: Elizabeth Craig <[email protected]>

* 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 <[email protected]>
Co-authored-by: David Zearing <[email protected]>
  • Loading branch information
3 people authored Apr 30, 2020
1 parent 1c286ff commit 24f53d1
Show file tree
Hide file tree
Showing 24 changed files with 1,805 additions and 8 deletions.
1 change: 1 addition & 0 deletions apps/test-bundles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions apps/test-bundles/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "prerelease",
"comment": "Migrate Checkbox to functional component",
"packageName": "@fluentui/react-next",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2020-04-22T17:09:48.443Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const tableSlotClassNames: TableSlotClassNames = {
header: `${tableClassName}__header`,
};

export type TableStylesProps = never
export type TableStylesProps = never;

export const Table: React.FC<WithAsProp<TableProps>> &
FluentComponentStaticProps<TableProps> & {
Expand Down
153 changes: 152 additions & 1 deletion packages/react-next/etc/react-next.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICheckboxProps>;

// @public (undocumented)
export const CheckboxBase: React.ForwardRefExoticComponent<ICheckboxProps & React.RefAttributes<HTMLDivElement>>;

// @public
export interface ICheckbox {
checked: boolean;
focus: () => void;
indeterminate: boolean;
}

// @public
export interface ICheckboxProps extends React.ButtonHTMLAttributes<HTMLElement | HTMLInputElement> {
ariaDescribedBy?: string;
ariaLabel?: string;
ariaLabelledBy?: string;
ariaPositionInSet?: number;
ariaSetSize?: number;
boxSide?: 'start' | 'end';
checked?: boolean;
checkmarkIconProps?: IIconProps;
className?: string;
componentRef?: IRefObject<ICheckbox>;
defaultChecked?: boolean;
defaultIndeterminate?: boolean;
disabled?: boolean;
indeterminate?: boolean;
inputProps?: React.ButtonHTMLAttributes<HTMLElement | HTMLButtonElement>;
keytipProps?: IKeytipProps;
label?: string;
onChange?: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
onRenderLabel?: IRenderFunction<ICheckboxProps>;
styles?: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles>;
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)

Expand Down
2 changes: 1 addition & 1 deletion packages/react-next/src/Checkbox.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from 'office-ui-fabric-react/lib/Checkbox';
export * from './components/Checkbox/index';
149 changes: 149 additions & 0 deletions packages/react-next/src/components/Checkbox/Checkbox.base.tsx
Original file line number Diff line number Diff line change
@@ -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<ICheckboxStyleProps, ICheckboxStyles>();

export const CheckboxBase = React.forwardRef((props: ICheckboxProps, forwardedRef: React.Ref<HTMLDivElement>) => {
const {
className,
disabled,
inputProps,
name,
boxSide = 'start',
theme,
ariaLabel,
ariaLabelledBy,
ariaDescribedBy,
styles,
checkmarkIconProps,
ariaPositionInSet,
ariaSetSize,
keytipProps,
title,
label,
onChange,
} = props;

const rootRef = React.useRef<HTMLDivElement | null>(null);
const mergedRootRefs = useMergedRefs(rootRef, forwardedRef);
const checkBox = React.useRef<HTMLInputElement>(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 ? (
<span aria-hidden="true" className={classNames.text} title={title}>
{label}
</span>
) : null;
};

const _onChange = (ev: React.ChangeEvent<HTMLElement>): 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 (
<KeytipData keytipProps={keytipProps} disabled={disabled}>
{(keytipAttributes: any): JSX.Element => (
<div className={classNames.root} title={title} ref={mergedRootRefs}>
<input
type="checkbox"
{...inputProps}
data-ktp-execute-target={keytipAttributes['data-ktp-execute-target']}
checked={!!isChecked}
disabled={disabled}
className={classNames.input}
ref={checkBox}
name={name}
id={id}
title={title}
onChange={_onChange}
onFocus={inputProps?.onFocus}
onBlur={inputProps?.onBlur}
aria-disabled={disabled}
aria-label={ariaLabel || label}
aria-labelledby={ariaLabelledBy}
aria-describedby={mergeAriaAttributeValues(ariaDescribedBy, keytipAttributes['aria-describedby'])}
aria-posinset={ariaPositionInSet}
aria-setsize={ariaSetSize}
aria-checked={isIndeterminate ? 'mixed' : isChecked ? 'true' : 'false'}
/>
<label className={classNames.label} htmlFor={id}>
<div className={classNames.checkbox} data-ktp-target={keytipAttributes['data-ktp-target']}>
<Icon iconName="CheckMark" {...checkmarkIconProps} className={classNames.checkmark} />
</div>
{(props.onRenderLabel || onRenderLabel)(props, onRenderLabel)}
</label>
</div>
)}
</KeytipData>
);
});

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<HTMLInputElement>,
) {
React.useImperativeHandle(
props.componentRef,
() => ({
get checked() {
return !!isChecked;
},
get indeterminate() {
return !!isIndeterminate;
},
focus() {
if (checkBox.current) {
checkBox.current.focus();
}
},
}),
[isChecked, isIndeterminate],
);
}
Loading

0 comments on commit 24f53d1

Please sign in to comment.