Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
251e839
TextField: Implemented masking
lambertwang-zz Jan 24, 2018
71cea3f
Changed example
lambertwang-zz Jan 24, 2018
4bf0059
Change file
lambertwang-zz Jan 24, 2018
ab1c2c7
Moved functions to util. Added tests
lambertwang-zz Jan 24, 2018
cfd5e0a
Added selection deletion tests
lambertwang-zz Jan 24, 2018
c61c77c
Refactored textfield into separate components
lambertwang-zz Jan 26, 2018
b932927
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz Jan 26, 2018
e2462f0
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz Jan 30, 2018
257ea8a
Merge branch 'master' of github.com:lambertwang/office-ui-fabric-reac…
lambertwang-zz Feb 21, 2018
86e3260
Deleted base and default textfields
lambertwang-zz Feb 21, 2018
76c3d1b
Revert additional changes
lambertwang-zz Feb 21, 2018
322141d
Fixed refs
lambertwang-zz Feb 21, 2018
7ae09a0
Merge branch 'master' of https://github.com/officedev/office-ui-fabri…
dzearing Mar 10, 2018
794d948
Update magellan-textFieldMasking_2018-01-24-01-35.json
dzearing Mar 13, 2018
2ac854f
Merge branch 'master' of github.com:lambertwang/office-ui-fabric-reac…
lambertwang-zz Mar 26, 2018
17cff11
Merge branch 'magellan/textFieldMasking' of github.com:lambertwang/of…
lambertwang-zz Mar 26, 2018
b66bdb9
Added enzyme and snapshot tests for the component
lambertwang-zz Mar 27, 2018
3ea706d
Fixed typo
lambertwang-zz Mar 27, 2018
218637f
Added character overflow test
lambertwang-zz Mar 27, 2018
fc57283
Undo changes to textfield
lambertwang-zz Mar 27, 2018
ed138ae
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz Mar 27, 2018
f47a481
Fixed linting error
lambertwang-zz Mar 27, 2018
1cfbdf7
Update TextField.Basic.Example.tsx
lambertwang Mar 28, 2018
7060aee
Added comments to inputMask.ts
lambertwang Mar 28, 2018
5eb7615
Removed trailing whitespace
lambertwang Mar 28, 2018
7776a30
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz Apr 18, 2018
ade5b57
Fix build errors for Lambert
cliffkoh Apr 19, 2018
41353cd
Update jest snapshot
cliffkoh Apr 19, 2018
ef1e721
Merge pull request #1 from cliffkoh/lambertwang-magellan/textFieldMas…
lambertwang Apr 19, 2018
ed03b12
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz Apr 19, 2018
5087641
Fix build errors
lambertwang-zz Apr 19, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "TextField: Inmplemented input masking.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

"type": "minor"
}
],
"packageName": "office-ui-fabric-react",
"email": "law@microsoft.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ export class LayoutGroupBasicExample extends React.Component<{}, {}> {
}
/>
<LayoutGroup layoutGap={ 20 } direction='horizontal' justify='fill'>
<TextField label='TextField with a placeholder' placeholder='Now I am a Placeholder' ariaLabel='Please enter text here' />
<TextField label='TextField with an icon' iconProps={ { iconName: 'Calendar' } } />
<TextField
label='TextField with a placeholder'
placeholder='Now I am a Placeholder'
ariaLabel='Please enter text here'
/>
<TextField
label='TextField with an icon'
iconProps={ { iconName: 'Calendar' } }
/>
</LayoutGroup>

<LayoutGroup layoutGap={ 20 } direction='horizontal' justify='fill'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
import * as React from 'react';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing tests. Needs a bunch of enzyme tests. And probably a snapshot test or two.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

import { TextField } from '../TextField';
import {
ITextField,
ITextFieldProps
} from '../TextField.types';
import {
autobind,
BaseComponent,
KeyCodes,
} from '../../../Utilities';

import {
clearNext,
clearPrev,
clearRange,
getLeftFormatIndex,
getMaskDisplay,
getRightFormatIndex,
IMaskValue,
insertString,
parseMask
} from '@uifabric/utilities/lib/inputMask';

/**
* props.mask:
* The string containing the prompt and format characters.
* Example:
* 'Phone Number: (999) 9999'
*
* _maskCharData
* An array of data containing information regarding the format characters,
* their indices inside the display text, and their corresponding values.
* Example:
* [
* { value: '1', displayIndex: 16, format: /[0-9]/ },
* { value: '2', displayIndex: 17, format: /[0-9]/ },
* { displayIndex: 18, format: /[0-9]/ },
* { value: '4', displayIndex: 22, format: /[0-9]/ },
* ...
* ]
*/
export interface IMaskedTextFieldState {
/**
* The mask string formatted with the input value.
* This is what is displayed inside the TextField
* Example:
* 'Phone Number: 12_ - 4___'
*/
displayValue: string;
/** The index into the rendered value of the first unfilled format character */
maskCursorPosition?: number;
}

export interface IMaskOptions {
maskPrefix: string;
maskDisplay: string;
lastEditablePos: number;
permanents: IMaskValue[];
}

export class MaskedTextField extends BaseComponent<ITextFieldProps, IMaskedTextFieldState> {
/**
* Tell BaseComponent to bypass resolution of componentRef.
*/
protected _shouldUpdateComponentRef = false;

private _textField: ITextField;
private _maskCharData: IMaskValue[];
// True if the TextField is focused
private _isFocused: boolean;
// True if the TextField was not focused and it was clicked into
private _moveCursorOnMouseUp: boolean;

// The input data associated with a delete or backspace change
private _charRemovalData: {
keyCode: number,
selectionStart: number,
selectionEnd: number
} | null;

// The clipboard data associated with a paste event
private _pasteData: {
selectionStart: number,
selectionEnd: number
} | null;

constructor(props: ITextFieldProps) {
super(props);

// Translate mask into charData
this._maskCharData = parseMask(props.mask);
this._isFocused = false;
this._moveCursorOnMouseUp = false;

this.state = {
displayValue: getMaskDisplay(
this.props.mask,
this._maskCharData,
this.props.maskChar),
};
}

public componentDidUpdate() {
// Move the cursor to the start of the mask format on update
if (this.state.maskCursorPosition) {
this._textField.setSelectionRange(this.state.maskCursorPosition, this.state.maskCursorPosition);
}
}

public render() {
return (
<TextField
{ ...this.props }
onFocus={ this._onFocus }
onBlur={ this._onBlur }
onMouseDown={ this._onMouseDown }
onMouseUp={ this._onMouseUp }
onChanged={ this._onInputChange }
onKeyDown={ this._onKeyDown }
onPaste={ this._onPaste }
value={ this.state.displayValue }
ref={ this._resolveRef('_textField') }
/>
);
}

/**
* @return The value of all filled format characters or undefined if not all format characters are filled
*/
public value(): string | undefined {
let value = '';

for (let i = 0; i < this._maskCharData.length; i++) {
if (!this._maskCharData[i].value) {
return undefined;
}
value += this._maskCharData[i].value;
}
return value;
}

@autobind
private _onFocus(event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) {
const {
mask,
onFocus
} = this.props;

if (onFocus) {
onFocus(event);
}

this._isFocused = true;

// Move the cursor position to the rightmost unfilled position
for (let i = 0; i < this._maskCharData.length; i++) {
if (!this._maskCharData[i].value) {
this.setState({
maskCursorPosition: this._maskCharData[i].displayIndex
});
break;
}
}
}

@autobind
private _onBlur(event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) {
const {
onBlur
} = this.props;

if (onBlur) {
onBlur(event);
}

this._isFocused = false;
this._moveCursorOnMouseUp = true;
}

@autobind
private _onMouseDown(event: React.MouseEvent<HTMLInputElement>) {
if (!this._isFocused) {
this._moveCursorOnMouseUp = true;
}
}

@autobind
private _onMouseUp(event: React.MouseEvent<HTMLInputElement>) {
const {
mask
} = this.props;

// Move the cursor on mouseUp after focusing the textField
if (this._moveCursorOnMouseUp) {
this._moveCursorOnMouseUp = false;
// Move the cursor position to the rightmost unfilled position
for (let i = 0; i < this._maskCharData.length; i++) {
if (!this._maskCharData[i].value) {
this.setState({
maskCursorPosition: this._maskCharData[i].displayIndex
});
break;
}
}
}
}

@autobind
private _onInputChange(value: string) {
const { displayValue } = this.state;
const selectionEnd = this._textField.selectionEnd;

// The initial value of cursorPos does not matter
let cursorPos = selectionEnd;

if (this._pasteData) {
const charsSelected = this._pasteData.selectionEnd - this._pasteData.selectionStart,
charCount = value.length + charsSelected - displayValue.length,
startPos = selectionEnd - charCount,
pastedString = value.substr(startPos, charCount);

// Clear any selected characters
if (charsSelected) {
this._maskCharData = clearRange(this._maskCharData, this._pasteData.selectionStart, charsSelected);
}
cursorPos = insertString(this._maskCharData, startPos, pastedString);

this._pasteData = null;
} else if (this._charRemovalData) {
// isDel is true If the characters are removed LTR, otherwise RTL
const isDel = this._charRemovalData.keyCode === KeyCodes.del,
charCount = this._charRemovalData.selectionEnd - this._charRemovalData.selectionStart;

if (charCount) { // charCount is > 0 if range was deleted
this._maskCharData = clearRange(this._maskCharData, this._charRemovalData.selectionStart, charCount);
cursorPos = getRightFormatIndex(this._maskCharData, this._charRemovalData.selectionStart);
} else { // If charCount === 0, there was no selection and a single character was deleted
if (isDel) {
this._maskCharData = clearNext(this._maskCharData, this._charRemovalData.selectionStart);
cursorPos = getRightFormatIndex(this._maskCharData, this._charRemovalData.selectionStart);
} else {
this._maskCharData = clearPrev(this._maskCharData, this._charRemovalData.selectionStart);
cursorPos = getLeftFormatIndex(this._maskCharData, this._charRemovalData.selectionStart);
}
}
this._charRemovalData = null;
} else if (value.length > displayValue.length) {
// This case is if the user added characters
const charCount = value.length - displayValue.length,
startPos = selectionEnd - charCount,
enteredString = value.substr(startPos, charCount);

cursorPos = insertString(this._maskCharData, startPos, enteredString);
} else if (value.length <= displayValue.length) {
/**
* This case is reached only if the user has selected a block of 1 or more
* characters and input a character replacing the characters they've selected.
*/
const charCount = 1,
selectCount = displayValue.length + charCount - value.length,
startPos = selectionEnd - charCount,
enteredString = value.substr(startPos, charCount);

// Clear the selected range
this._maskCharData = clearRange(this._maskCharData, startPos, selectCount);
// Insert the printed character
cursorPos = insertString(this._maskCharData, startPos, enteredString);
}

this.setState({
displayValue: getMaskDisplay(
this.props.mask,
this._maskCharData,
this.props.maskChar),
maskCursorPosition: cursorPos
});
}

@autobind
private _onKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
this._charRemovalData = null;
if (this._textField.value) {
const {
keyCode,
ctrlKey,
metaKey
} = event;

// Ignore ctrl and meta keydown
if (ctrlKey || metaKey) {
return;
}

// On backspace or delete, store the selection and the keyCode
if (keyCode === KeyCodes.backspace || keyCode === KeyCodes.del) {
const selectionStart = (event.target as HTMLInputElement).selectionStart,
selectionEnd = (event.target as HTMLInputElement).selectionEnd;

// Check if backspace or delete press is valid.
if (!(keyCode === KeyCodes.backspace && selectionEnd > 0)
&& !(keyCode === KeyCodes.del && selectionStart < this._textField.value.length)) {
return;
}

this._charRemovalData = {
keyCode,
selectionStart,
selectionEnd
};
}
}
}

@autobind
private _onPaste(event: React.ClipboardEvent<HTMLInputElement>) {
const selectionStart = (event.target as HTMLInputElement).selectionStart,
selectionEnd = (event.target as HTMLInputElement).selectionEnd;
// Store the paste selection range
this._pasteData = {
selectionStart,
selectionEnd
};
}
}
Loading