Skip to content

Commit

Permalink
Polyfill react create ref (#4182)
Browse files Browse the repository at this point in the history
* Initial implementation of React.createRef polyfill

* Updates based on feedback

* Updates to components with refs

* Fix Type

* Fix lint

* Revert to original Experiment

* Fix typing

* Add typedef to docs

* Add change files

* Remove empty change files
  • Loading branch information
Markionium authored and dzearing committed Mar 6, 2018
1 parent 4368015 commit 25e736f
Show file tree
Hide file tree
Showing 21 changed files with 219 additions and 121 deletions.
17 changes: 11 additions & 6 deletions apps/todo-app/src/components/TodoForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { autobind, BaseComponent, IBaseProps } from 'office-ui-fabric-react/lib/Utilities';
import { autobind, BaseComponent, IBaseProps, createRef, RefObject } from 'office-ui-fabric-react/lib/Utilities';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { TextField, ITextField } from 'office-ui-fabric-react/lib/TextField';
import * as stylesImport from './Todo.scss';
Expand Down Expand Up @@ -42,7 +42,7 @@ export interface ITodoFormState {
* Button: https://fabricreact.azurewebsites.net/fabric-react/master/#/examples/button
*/
export default class TodoForm extends BaseComponent<ITodoFormProps, ITodoFormState> {
private _textField!: ITextField;
private _textField: RefObject<ITextField> = createRef<ITextField>();

constructor(props: ITodoFormProps) {
super(props);
Expand All @@ -62,7 +62,7 @@ export default class TodoForm extends BaseComponent<ITodoFormProps, ITodoFormSta
<TextField
className={ styles.textField }
value={ this.state.inputValue }
componentRef={ this._resolveRef('_textField') }
componentRef={ this._textField }
placeholder={ strings.inputBoxPlaceholder }
onBeforeChange={ this._onBeforeTextFieldChange }
autoComplete='off'
Expand All @@ -82,18 +82,23 @@ export default class TodoForm extends BaseComponent<ITodoFormProps, ITodoFormSta
private _onSubmit(event: React.FormEvent<HTMLElement>): void {
event.preventDefault();

if (!this._getTitleErrorMessage(this._textField.value || '')) {
const { value: textField } = this._textField;
if (!textField) {
return;
}

if (!this._getTitleErrorMessage(textField.value || '')) {
this.setState({
inputValue: ''
} as ITodoFormState);

this.props.onSubmit(this._textField.value || '');
this.props.onSubmit(textField.value || '');
} else {
this.setState({
errorMessage: this._getTitleErrorMessage(this.state.inputValue)
} as ITodoFormState);

this._textField.focus();
textField.focus();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/utilities",
"comment": "Adds createRef polyfil to prepare for object refs.",
"type": "minor"
}
],
"packageName": "@uifabric/utilities",
"email": "mark@thedutchies.com"
}
8 changes: 5 additions & 3 deletions ghdocs/BESTPRACTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,15 @@ public render() {
}
```

Best, use _resolveRef in BaseComponent:
Best, use createRef:
```typescript
import { createRef, RefObject } from 'office-ui-fabric-react/lib/Utilities';

class Foo extends BaseComponent<...> {
private _root: HTMLElement;
private _root: RefObject<HTMLElement> = createRef<HTMLElement>();

public render() {
return <div ref={ this._resolveRef('_root') } />;
return <div ref={ _root } />;
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { registerLanguage, highlightBlock } from 'highlight.js';
import * as javascript from 'highlight.js/lib/languages/javascript';
import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities';
import { RefObject, createRef } from '@uifabric/utilities/lib/createRef';

registerLanguage('javascript', javascript);

Expand All @@ -10,13 +11,13 @@ export interface IHighlightProps extends React.HTMLAttributes<HTMLDivElement> {
}

export class Highlight extends BaseComponent<IHighlightProps, {}> {
private _codeElement: HTMLElement;
private _codeElement: RefObject<HTMLElement> = createRef<HTMLElement>();

public render(): JSX.Element {
return (
<pre>
<code
ref={ this._resolveRef('_codeElement') }
ref={ this._codeElement }
className='javascript'
>
{ this.props.children }
Expand All @@ -30,6 +31,8 @@ export class Highlight extends BaseComponent<IHighlightProps, {}> {
}

public componentDidMount(): void {
highlightBlock(this._codeElement);
if (this._codeElement.value) {
highlightBlock(this._codeElement.value);
}
}
}
20 changes: 12 additions & 8 deletions packages/experiments/src/components/CommandBar/CommandBar.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { OverflowSet, IOverflowSet } from 'office-ui-fabric-react/lib/OverflowSe
import { ResizeGroup, IResizeGroup } from 'office-ui-fabric-react/lib/ResizeGroup';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import {
classNamesFunction
classNamesFunction,
createRef,
RefObject
} from '../../Utilities';

const getClassNames = classNamesFunction<ICommandBarStyleProps, ICommandBarStyles>();
Expand Down Expand Up @@ -54,8 +56,8 @@ export class CommandBarBase extends BaseComponent<ICommandBarProps, {}> implemen
elipisisIconProps: { iconName: 'More' }
};

private _overflowSet: IOverflowSet;
private _resizeGroup: IResizeGroup;
private _overflowSet: RefObject<IOverflowSet> = createRef<IOverflowSet>();
private _resizeGroup: RefObject<IResizeGroup> = createRef<IResizeGroup>();
private _classNames: {[key in keyof ICommandBarStyles]: string };

public render(): JSX.Element {
Expand Down Expand Up @@ -88,7 +90,7 @@ export class CommandBarBase extends BaseComponent<ICommandBarProps, {}> implemen

return (
<ResizeGroup
componentRef={ this._resolveRef('_resizeGroup') }
componentRef={ this._resizeGroup }
className={ className }
data={ commandBardata }
onReduceData={ onReduceData }
Expand All @@ -100,7 +102,7 @@ export class CommandBarBase extends BaseComponent<ICommandBarProps, {}> implemen

{/*Primary Items*/ }
<OverflowSet
componentRef={ this._resolveRef('_overflowSet') }
componentRef={ this._overflowSet }
className={ css(this._classNames.primarySet) }
items={ data.primaryItems }
overflowItems={ data.overflowItems.length ? data.overflowItems : undefined }
Expand Down Expand Up @@ -134,11 +136,13 @@ export class CommandBarBase extends BaseComponent<ICommandBarProps, {}> implemen
}

public focus(): void {
this._overflowSet.focus();
const { value: overflowSet } = this._overflowSet;

overflowSet && overflowSet.focus();
}

public remeasure(): void {
this._resizeGroup.remeasure();
this._resizeGroup.value && this._resizeGroup.value.remeasure();
}

private _computeCacheKey(primaryItems: ICommandBarItemProps[], farItems: ICommandBarItemProps[], overflow: boolean): string {
Expand Down Expand Up @@ -235,6 +239,6 @@ export class CommandBarBase extends BaseComponent<ICommandBarProps, {}> implemen
@autobind
private _onRenderButton(props: ICommandBarItemProps): JSX.Element {
// tslint:disable-next-line:no-any
return <CommandBarButton {...props as any} />;
return <CommandBarButton { ...props as any } />;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class BaseExtendedPicker<T, P extends IBaseExtendedPickerProps<T>> extend
this.focusZone.focus();
}

public get inputElement(): HTMLInputElement {
public get inputElement(): HTMLInputElement | null {
return this.input.inputElement;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
KeyCodes,
autobind,
css,
getRTL
getRTL,
createRef,
RefObject
} from '../../Utilities';
import { Callout, DirectionalHint } from 'office-ui-fabric-react/lib/Callout';
import { Suggestions, ISuggestionsProps, SuggestionsController, IBasePickerSuggestionsProps, ISuggestionModel }
Expand All @@ -30,12 +32,12 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
implements IBaseFloatingPicker {
protected selection: Selection;

protected root: HTMLElement;
protected suggestionElement: Suggestions<T>;
protected root: RefObject<HTMLDivElement> = createRef<HTMLDivElement>();
protected suggestionElement: RefObject<Suggestions<T>> = createRef<Suggestions<T>>();

protected suggestionStore: SuggestionsController<T>;
protected SuggestionOfProperType: new (props: ISuggestionsProps<T>) => Suggestions<T> =
Suggestions as new (props: ISuggestionsProps<T>) => Suggestions<T>;
Suggestions as new (props: ISuggestionsProps<T>) => Suggestions<T>;
protected loadingTimer: number | undefined;
// tslint:disable-next-line:no-any
protected currentPromise: PromiseLike<any>;
Expand Down Expand Up @@ -154,7 +156,7 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
let { className } = this.props;
return (
<div
ref={ this._resolveRef('root') }
ref={ this.root }
className={ css('ms-BasePicker', className ? className : '') }
>
{ this.renderSuggestions() }
Expand Down Expand Up @@ -187,15 +189,15 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
onSuggestionClick={ this.onSuggestionClick }
onSuggestionRemove={ this.onSuggestionRemove }
suggestions={ this.suggestionStore.getSuggestions() }
ref={ this._resolveRef('suggestionElement') }
ref={ this.suggestionElement }
onGetMoreResults={ this.onGetMoreResults }
moreSuggestionsAvailable={ this.state.moreSuggestionsAvailable }
isLoading={ this.state.suggestionsLoading }
isSearching={ this.state.isSearching }
isMostRecentlyUsedVisible={ this.state.isMostRecentlyUsedVisible }
isResultsFooterVisible={ this.state.isResultsFooterVisible }
refocusSuggestions={ this.refocusSuggestions }
{...this.props.pickerSuggestionsProps as IBasePickerSuggestionsProps}
{ ...this.props.pickerSuggestionsProps as IBasePickerSuggestionsProps }
/>
</Callout>
) : null;
Expand Down Expand Up @@ -324,7 +326,7 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
ev: React.MouseEvent<HTMLElement>,
item: T,
index: number
): void {
): void {
this.onChange(item);
}

Expand All @@ -333,7 +335,7 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
ev: React.MouseEvent<HTMLElement>,
item: T,
index: number
): void {
): void {
if (this.props.onRemoveSuggestion) {
(this.props.onRemoveSuggestion as ((item: T) => void))(item);
}
Expand All @@ -348,6 +350,7 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
return;
}
let keyCode = ev.which;
let { value: suggestionElement } = this.suggestionElement;
switch (keyCode) {
case KeyCodes.escape:
this.setState({ suggestionsVisible: false });
Expand All @@ -357,8 +360,8 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend

case KeyCodes.tab:
case KeyCodes.enter:
if (this.suggestionElement.hasSuggestedActionSelected()) {
this.suggestionElement.executeSelectedAction();
if (suggestionElement && suggestionElement.hasSuggestedActionSelected()) {
suggestionElement.executeSelectedAction();
} else if (!ev.shiftKey &&
!ev.ctrlKey &&
this.suggestionStore.hasSelectedSuggestion()) {
Expand Down Expand Up @@ -386,16 +389,16 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
break;

case KeyCodes.up:
if (this.suggestionElement.tryHandleKeyDown(keyCode, this.suggestionStore.currentIndex)) {
if (suggestionElement && suggestionElement.tryHandleKeyDown(keyCode, this.suggestionStore.currentIndex)) {
ev.preventDefault();
ev.stopPropagation();
} else {
if (this.suggestionElement.hasSuggestedAction() &&
if (suggestionElement && suggestionElement.hasSuggestedAction() &&
this.suggestionStore.currentIndex === 0
) {
ev.preventDefault();
ev.stopPropagation();
this.suggestionElement.focusAboveSuggestions();
suggestionElement.focusAboveSuggestions();
this.suggestionStore.deselectAllSuggestions();
this.forceUpdate();
} else {
Expand All @@ -409,18 +412,19 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
break;

case KeyCodes.down:
if (this.suggestionElement.tryHandleKeyDown(keyCode, this.suggestionStore.currentIndex)) {
if (suggestionElement && suggestionElement.tryHandleKeyDown(keyCode, this.suggestionStore.currentIndex)) {
ev.preventDefault();
ev.stopPropagation();
} else {
if (
this.suggestionElement.hasSuggestedAction() &&
suggestionElement &&
suggestionElement.hasSuggestedAction() &&
this.suggestionStore.currentIndex + 1 ===
this.suggestionStore.suggestions.length
) {
ev.preventDefault();
ev.stopPropagation();
this.suggestionElement.focusBelowSuggestions();
suggestionElement.focusBelowSuggestions();
this.suggestionStore.deselectAllSuggestions();
this.forceUpdate();
} else {
Expand Down Expand Up @@ -496,7 +500,7 @@ export class BaseFloatingPicker<T, P extends IBaseFloatingPickerProps<T>> extend
) => ISuggestionModel<T>))(
this.state.queryString,
(this.props.onValidateInput as ((input: string) => boolean))(this.state.queryString)
);
);
this.suggestionStore.createGenericSuggestion(itemToConvert);
this.completeSuggestion();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface IBaseFloatingPicker {
// displaying persona's than type T could either be of Persona or Ipersona props
// tslint:disable-next-line:no-any
export interface IBaseFloatingPickerProps<T> extends React.Props<any> {
componentRef?: (component?: IBaseFloatingPicker) => void;
componentRef?: (component?: IBaseFloatingPicker | null) => void;

/** The suggestions controller */
suggestionsController: SuggestionsController<T>;
Expand All @@ -34,7 +34,7 @@ export interface IBaseFloatingPickerProps<T> extends React.Props<any> {
/**
* The input element to listen on events
*/
inputElement?: HTMLElement;
inputElement?: HTMLElement | null;

/**
* Function that specifies how an individual suggestion item will appear.
Expand Down

0 comments on commit 25e736f

Please sign in to comment.