-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[TextField] Implemented masking #3783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Lambert Wang (lambertwang-zz)
merged 31 commits into
microsoft:master
from
lambertwang-zz:magellan/textFieldMasking
Apr 19, 2018
Merged
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 71cea3f
Changed example
lambertwang-zz 4bf0059
Change file
lambertwang-zz ab1c2c7
Moved functions to util. Added tests
lambertwang-zz cfd5e0a
Added selection deletion tests
lambertwang-zz c61c77c
Refactored textfield into separate components
lambertwang-zz b932927
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz e2462f0
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz 257ea8a
Merge branch 'master' of github.com:lambertwang/office-ui-fabric-reac…
lambertwang-zz 86e3260
Deleted base and default textfields
lambertwang-zz 76c3d1b
Revert additional changes
lambertwang-zz 322141d
Fixed refs
lambertwang-zz 7ae09a0
Merge branch 'master' of https://github.com/officedev/office-ui-fabri…
dzearing 794d948
Update magellan-textFieldMasking_2018-01-24-01-35.json
dzearing 2ac854f
Merge branch 'master' of github.com:lambertwang/office-ui-fabric-reac…
lambertwang-zz 17cff11
Merge branch 'magellan/textFieldMasking' of github.com:lambertwang/of…
lambertwang-zz b66bdb9
Added enzyme and snapshot tests for the component
lambertwang-zz 3ea706d
Fixed typo
lambertwang-zz 218637f
Added character overflow test
lambertwang-zz fc57283
Undo changes to textfield
lambertwang-zz ed138ae
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz f47a481
Fixed linting error
lambertwang-zz 1cfbdf7
Update TextField.Basic.Example.tsx
lambertwang 7060aee
Added comments to inputMask.ts
lambertwang 5eb7615
Removed trailing whitespace
lambertwang 7776a30
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz ade5b57
Fix build errors for Lambert
cliffkoh 41353cd
Update jest snapshot
cliffkoh ef1e721
Merge pull request #1 from cliffkoh/lambertwang-magellan/textFieldMas…
lambertwang ed03b12
Merge branch 'master' of github.com:OfficeDev/office-ui-fabric-react …
lambertwang-zz 5087641
Fix build errors
lambertwang-zz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
common/changes/office-ui-fabric-react/magellan-textFieldMasking_2018-01-24-01-35.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "office-ui-fabric-react", | ||
| "email": "law@microsoft.com" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
325 changes: 325 additions & 0 deletions
325
packages/office-ui-fabric-react/src/components/TextField/MaskedTextField/MaskedTextField.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,325 @@ | ||
| import * as React from 'react'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| }; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo