diff --git a/common/changes/@uifabric/experiments/textfieldStyles-jg_2018-07-19-23-30.json b/common/changes/@uifabric/experiments/textfieldStyles-jg_2018-07-19-23-30.json new file mode 100644 index 00000000000000..dd003f3afb5b30 --- /dev/null +++ b/common/changes/@uifabric/experiments/textfieldStyles-jg_2018-07-19-23-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/experiments", + "comment": "Comment out unused test.", + "type": "none" + } + ], + "packageName": "@uifabric/experiments", + "email": "jagore@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/@uifabric/merge-styles/textfieldStyles-jg_2018-07-19-22-14.json b/common/changes/@uifabric/merge-styles/textfieldStyles-jg_2018-07-19-22-14.json new file mode 100644 index 00000000000000..a12a7f874abc2d --- /dev/null +++ b/common/changes/@uifabric/merge-styles/textfieldStyles-jg_2018-07-19-22-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/merge-styles", + "comment": "Add resize rule to IRawStyleBase.", + "type": "minor" + } + ], + "packageName": "@uifabric/merge-styles", + "email": "jagore@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/textfieldStyles-jg_2018-07-19-22-14.json b/common/changes/office-ui-fabric-react/textfieldStyles-jg_2018-07-19-22-14.json new file mode 100644 index 00000000000000..f693adb83e2574 --- /dev/null +++ b/common/changes/office-ui-fabric-react/textfieldStyles-jg_2018-07-19-22-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "TextField: Convert to JS styling.", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "jagore@microsoft.com" +} \ No newline at end of file diff --git a/packages/experiments/src/components/Form/inputs/textInput/FormTextInput.test.tsx b/packages/experiments/src/components/Form/inputs/textInput/FormTextInput.test.tsx index 309631eed06d1d..69550807b2c53c 100644 --- a/packages/experiments/src/components/Form/inputs/textInput/FormTextInput.test.tsx +++ b/packages/experiments/src/components/Form/inputs/textInput/FormTextInput.test.tsx @@ -10,7 +10,6 @@ import { IFormProps } from '../../Form.types'; import { DEFAULT_DEBOUNCE } from '../../FormBaseInput'; import { FormTextInput } from './FormTextInput'; import { IFormTextInputProps } from './FormTextInput.types'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; // Utilities import { Validators } from '../../validators/Validators'; @@ -108,8 +107,10 @@ describe('FormTextInput Unit Tests', () => { ReactTestUtils.Simulate.submit(form); // Find the TextField component - const field = ReactTestUtils.findRenderedComponentWithType(renderedForm, TextField); - expect(field.state.errorMessage).toBeTruthy(); + // TODO: this test is not working as intended as even nonsense state names pass. + // commented out for now since test isn't executing when named 'xit' + // const field = ReactTestUtils.findRenderedComponentWithType(renderedForm, TextField); + // expect(field.state.errorMessage).toBeTruthy(); expect(result).toBeFalsy(); }); }); diff --git a/packages/merge-styles/src/IRawStyleBase.ts b/packages/merge-styles/src/IRawStyleBase.ts index 42ccb532e6d920..0cb360923e1a76 100644 --- a/packages/merge-styles/src/IRawStyleBase.ts +++ b/packages/merge-styles/src/IRawStyleBase.ts @@ -66,18 +66,18 @@ export interface IRawFontStyle { * See CSS 3 font-size property https://www.w3.org/TR/css-fonts-3/#propdef-font-size */ fontSize?: - | ICSSRule - | 'xx-small' - | 'x-small' - | 'small' - | 'medium' - | 'large' - | 'x-large' - | 'xx-large' - | 'larger' - | 'smaller' - | ICSSPixelUnitRule - | ICSSPercentageRule; + | ICSSRule + | 'xx-small' + | 'x-small' + | 'small' + | 'medium' + | 'large' + | 'x-large' + | 'xx-large' + | 'larger' + | 'smaller' + | ICSSPixelUnitRule + | ICSSPercentageRule; /** * The font-size-adjust property adjusts the font-size of the fallback fonts defined @@ -95,16 +95,16 @@ export interface IRawFontStyle { * https://drafts.csswg.org/css-fonts-3/#propdef-font-stretch */ fontStretch?: - | ICSSRule - | 'normal' - | 'ultra-condensed' - | 'extra-condensed' - | 'condensed' - | 'semi-condensed' - | 'semi-expanded' - | 'expanded' - | 'extra-expanded' - | 'ultra-expanded'; + | ICSSRule + | 'normal' + | 'ultra-condensed' + | 'extra-condensed' + | 'condensed' + | 'semi-condensed' + | 'semi-expanded' + | 'expanded' + | 'extra-expanded' + | 'ultra-expanded'; /** * The font-style property allows normal, italic, or oblique faces to be selected. @@ -1338,6 +1338,12 @@ export interface IRawStyleBase extends IRawFontStyle { */ regionFragment?: ICSSRule | string; + /** + * The resize CSS sets whether an element is resizable, and if so, in which direction(s). + */ + + resize?: ICSSRule | 'none' | 'both' | 'horizontal' | 'vertical' | 'block' | 'inline'; + /** * The rest-after property determines how long a speech media agent should pause after * presenting an element's main content, before presenting that element's exit cue diff --git a/packages/office-ui-fabric-react/src/components/ColorPicker/ColorPicker.base.tsx b/packages/office-ui-fabric-react/src/components/ColorPicker/ColorPicker.base.tsx index 44472a3b6c9b9b..f7d07f1c0d2baf 100644 --- a/packages/office-ui-fabric-react/src/components/ColorPicker/ColorPicker.base.tsx +++ b/packages/office-ui-fabric-react/src/components/ColorPicker/ColorPicker.base.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { BaseComponent, classNamesFunction } from '../../Utilities'; +import { BaseComponent, classNamesFunction, createRef } from '../../Utilities'; import { IColorPickerProps, IColorPickerStyleProps, IColorPickerStyles } from './ColorPicker.types'; -import { TextField } from '../../TextField'; +import { ITextField, TextField } from '../../TextField'; import { ColorRectangle } from './ColorRectangle/ColorRectangle'; import { ColorSlider } from './ColorSlider/ColorSlider'; import { @@ -30,11 +30,11 @@ export class ColorPickerBase extends BaseComponent(); + private _rText = createRef(); + private _gText = createRef(); + private _bText = createRef(); + private _aText = createRef(); constructor(props: IColorPickerProps) { super(props); @@ -97,7 +97,7 @@ export class ColorPickerBase extends BaseComponent (this.hexText = ref!)} + componentRef={this._hexText} onBlur={this._onHexChanged} spellCheck={false} ariaLabel={this.props.hexLabel} @@ -108,7 +108,7 @@ export class ColorPickerBase extends BaseComponent (this.rText = ref!)} + componentRef={this._rText} spellCheck={false} ariaLabel={this.props.redLabel} /> @@ -118,7 +118,7 @@ export class ColorPickerBase extends BaseComponent (this.gText = ref!)} + componentRef={this._gText} spellCheck={false} ariaLabel={this.props.greenLabel} /> @@ -128,7 +128,7 @@ export class ColorPickerBase extends BaseComponent (this.bText = ref!)} + componentRef={this._bText} spellCheck={false} ariaLabel={this.props.blueLabel} /> @@ -139,7 +139,7 @@ export class ColorPickerBase extends BaseComponent (this.aText = ref!)} + componentRef={this._aText} spellCheck={false} ariaLabel={this.props.alphaLabel} /> @@ -166,16 +166,16 @@ export class ColorPickerBase extends BaseComponent { - this._updateColor(getColorFromString('#' + this.hexText.value)); + this._updateColor(getColorFromString('#' + this._hexText.value)); }; private _onRGBAChanged = (): void => { this._updateColor( getColorFromRGBA({ - r: Number(this.rText.value), - g: Number(this.gText.value), - b: Number(this.bText.value), - a: Number((this.aText && this.aText.value) || 100) + r: Number(this._rText.value), + g: Number(this._gText.value), + b: Number(this._bText.value), + a: Number((this._aText && this._aText.value) || 100) }) ); }; diff --git a/packages/office-ui-fabric-react/src/components/ColorPicker/__snapshots__/ColorPicker.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ColorPicker/__snapshots__/ColorPicker.test.tsx.snap index 59db6bc79e7f16..faa3abc90e8017 100644 --- a/packages/office-ui-fabric-react/src/components/ColorPicker/__snapshots__/ColorPicker.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/ColorPicker/__snapshots__/ColorPicker.test.tsx.snap @@ -248,10 +248,23 @@ exports[`ColorPicker renders ColorPicker correctly 1`] = ` ms-ColorPicker-input { border: none; + box-shadow: none; box-sizing: border-box; height: 30px; + margin-bottom: 0px; + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + padding-top: 0px; + position: relative; width: 100%; } + @media screen and (-ms-high-contrast: active){& { + border-width: 2px; + } &.ms-TextField { padding-right: 2px; } @@ -265,16 +278,84 @@ exports[`ColorPicker renders ColorPicker correctly 1`] = ` } >
); } diff --git a/packages/office-ui-fabric-react/src/components/TextField/MaskedTextField/__snapshots__/MaskedTextField.test.tsx.snap b/packages/office-ui-fabric-react/src/components/TextField/MaskedTextField/__snapshots__/MaskedTextField.test.tsx.snap index 15b48529db857b..6785cdaffae51b 100644 --- a/packages/office-ui-fabric-react/src/components/TextField/MaskedTextField/__snapshots__/MaskedTextField.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/TextField/MaskedTextField/__snapshots__/MaskedTextField.test.tsx.snap @@ -2,10 +2,29 @@ exports[`MaskedTextField renders TextField correctly 1`] = `
(); + export interface ITextFieldState { value: string; /** Is true when the control has focus. */ - isFocused?: boolean; + isFocused: boolean; /** * The validation error message. @@ -27,12 +30,12 @@ export interface ITextFieldState { * - If there is no validation error or we have not validated the input value, errorMessage is an empty string. * - If we have done the validation and there is validation error, errorMessage is the validation error message. */ - errorMessage?: string; + errorMessage: string; } const DEFAULT_STATE_VALUE = ''; -export class TextField extends BaseComponent implements ITextField { +export class TextFieldBase extends BaseComponent implements ITextField { public static defaultProps: ITextFieldProps = { multiline: false, resizable: true, @@ -65,6 +68,7 @@ export class TextField extends BaseComponent i private _latestValidateValue: string | undefined; private _isDescriptionAvailable: boolean; private _textElement = createRef(); + private _classNames: IProcessedStyleSet; public constructor(props: ITextFieldProps) { super(props); @@ -153,18 +157,22 @@ export class TextField extends BaseComponent i public render(): JSX.Element { const { + borderless, className, description, disabled, iconClass, iconProps, + label, multiline, required, underlined, - borderless, addonString, // @deprecated prefix, + resizable, suffix, + theme, + styles, onRenderAddon = this._onRenderAddon, // @deprecated onRenderPrefix = this._onRenderPrefix, onRenderSuffix = this._onRenderSuffix, @@ -174,48 +182,41 @@ export class TextField extends BaseComponent i const { isFocused } = this.state; const errorMessage = this._errorMessage; + this._classNames = getClassNames(styles!, { + theme: theme!, + className, + disabled, + focused: isFocused, + required, + multiline, + hasLabel: !!label, + hasErrorMessage: !!errorMessage, + borderless, + resizable, + hasIcon: !!iconProps, + underlined, + iconClass + }); + // If a custom description render function is supplied then treat description as always available. // Otherwise defer to the presence of description or error message text. this._isDescriptionAvailable = Boolean(this.props.onRenderDescription || description || errorMessage); - const textFieldClassName = css('ms-TextField', styles.root, className, { - ['is-required ' + styles.rootIsRequiredLabel]: this.props.label && required, - ['is-required ' + styles.rootIsRequiredPlaceholderOnly]: !this.props.label && required, - ['is-disabled ' + styles.rootIsDisabled]: disabled, - ['is-active ' + styles.rootIsActive]: isFocused, - ['ms-TextField--multiline ' + styles.rootIsMultiline]: multiline, - ['ms-TextField--underlined ' + styles.rootIsUnderlined]: underlined, - ['ms-TextField--borderless ' + styles.rootIsBorderless]: borderless - }); - return ( -
-
+
+
{onRenderLabel(this.props, this._onRenderLabel)} -
+
{(addonString !== undefined || this.props.onRenderAddon) && ( -
- {onRenderAddon(this.props, this._onRenderAddon)} -
+
{onRenderAddon(this.props, this._onRenderAddon)}
)} {(prefix !== undefined || this.props.onRenderPrefix) && ( -
- {onRenderPrefix(this.props, this._onRenderPrefix)} -
+
{onRenderPrefix(this.props, this._onRenderPrefix)}
)} {multiline ? this._renderTextArea() : this._renderInput()} - {(iconClass || iconProps) && } + {(iconClass || iconProps) && } {(suffix !== undefined || this.props.onRenderSuffix) && ( -
- {onRenderSuffix(this.props, this._onRenderSuffix)} -
+
{onRenderSuffix(this.props, this._onRenderSuffix)}
)}
@@ -225,12 +226,8 @@ export class TextField extends BaseComponent i {errorMessage && (
-

- - {errorMessage} - +

+ {errorMessage}

@@ -325,15 +322,25 @@ export class TextField extends BaseComponent i } private _onRenderLabel = (props: ITextFieldProps): JSX.Element | null => { - if (props.label) { - return ; + const { label, required } = props; + // IProcessedStyleSet definition requires casting for what Label expects as its styles prop + const labelStyles = this._classNames.subComponentStyles + ? (this._classNames.subComponentStyles.label as IStyleFunctionOrObject) + : undefined; + + if (label) { + return ( + + ); } return null; }; private _onRenderDescription = (props: ITextFieldProps): JSX.Element | null => { if (props.description) { - return {props.description}; + return {props.description}; } return null; }; @@ -354,27 +361,9 @@ export class TextField extends BaseComponent i return {suffix}; } - private _getTextElementClassName(): string { - let textFieldClassName: string; - - if (this.props.multiline && !this.props.resizable) { - textFieldClassName = css( - 'ms-TextField-field ms-TextField-field--unresizable', - styles.field, - styles.fieldIsUnresizable - ); - } else { - textFieldClassName = css('ms-TextField-field', styles.field); - } - - return css(textFieldClassName, this.props.inputClassName, { - [styles.hasIcon]: !!this.props.iconClass - }); - } - private get _errorMessage(): string | undefined { let { errorMessage } = this.state; - if (!errorMessage) { + if (!errorMessage && this.props.errorMessage) { errorMessage = this.props.errorMessage; } @@ -392,7 +381,7 @@ export class TextField extends BaseComponent i value={this.state.value} onInput={this._onInputChange} onChange={this._onInputChange} - className={this._getTextElementClassName()} + className={this._classNames.field} aria-describedby={this._isDescriptionAvailable ? this._descriptionId : this.props['aria-describedby']} aria-invalid={!!this.state.errorMessage} aria-label={this.props.ariaLabel} @@ -417,7 +406,7 @@ export class TextField extends BaseComponent i value={this.state.value} onInput={this._onInputChange} onChange={this._onInputChange} - className={this._getTextElementClassName()} + className={this._classNames.field} aria-label={this.props.ariaLabel} aria-describedby={this._isDescriptionAvailable ? this._descriptionId : this.props['aria-describedby']} aria-invalid={!!this.state.errorMessage} diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.deprecated.test.tsx b/packages/office-ui-fabric-react/src/components/TextField/TextField.deprecated.test.tsx new file mode 100644 index 00000000000000..f984d38304a9ba --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.deprecated.test.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import * as WarnUtil from '@uifabric/utilities/lib-commonjs/warn'; + +import { resetIds } from '../../Utilities'; +import { TextField } from './TextField'; + +describe('TextField deprecated', () => { + beforeAll(() => { + // Prevent warn deprecations from failing test + jest.spyOn(WarnUtil, 'warnDeprecations').mockImplementation(() => { + /** no impl **/ + }); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + resetIds(); + }); + + it('renders with deprecated props affecting styling', () => { + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.doc.tsx b/packages/office-ui-fabric-react/src/components/TextField/TextField.doc.tsx index ab00c2537268b5..88979e9bca268a 100644 --- a/packages/office-ui-fabric-react/src/components/TextField/TextField.doc.tsx +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.doc.tsx @@ -11,6 +11,7 @@ import { TextFieldPlaceholderExample } from './examples/TextField.Placeholder.Ex import { TextFieldPrefixExample } from './examples/TextField.Prefix.Example'; import { TextFieldPrefixAndSuffixExample } from './examples/TextField.PrefixAndSuffix.Example'; import { TextFieldStatus } from './TextField.checklist'; +import { TextFieldStyledExample } from './examples/TextField.Styled.Example'; import { TextFieldSuffixExample } from './examples/TextField.Suffix.Example'; import { TextFieldUnderlinedExample } from './examples/TextField.Underlined.Example'; import { TextFieldAutoCompleteExample } from './examples/TextField.AutoComplete.Example'; @@ -25,6 +26,7 @@ const TextFieldMultilineExampleCode = require('!raw-loader!office-ui-fabric-reac const TextFieldPlaceholderExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.Placeholder.Example.tsx') as string; const TextFieldPrefixExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.Prefix.Example.tsx') as string; const TextFieldPrefixAndSuffixExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.PrefixAndSuffix.Example.tsx') as string; +const TextFieldStyledExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.Styled.Example.tsx') as string; const TextFieldSuffixExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.Suffix.Example.tsx') as string; const TextFieldUnderlinedExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.Underlined.Example.tsx') as string; const TextFieldAutoCompleteExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/TextField/examples/TextField.AutoComplete.Example.tsx') as string; @@ -103,6 +105,11 @@ export const TextFieldPageProps: IDocPageProps = { title: 'TextField error message variations', code: TextFieldErrorMessageExampleCode, view: + }, + { + title: 'TextField Subcomponent Styling', + code: TextFieldStyledExampleCode, + view: } ], propertiesTablesSources: [ diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.scss b/packages/office-ui-fabric-react/src/components/TextField/TextField.scss deleted file mode 100644 index 6fc9f58d76e4d3..00000000000000 --- a/packages/office-ui-fabric-react/src/components/TextField/TextField.scss +++ /dev/null @@ -1,330 +0,0 @@ -@import '../../common/common'; -@import '../../common/semanticSlots'; - -// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. - -// -// Office UI Fabric -// -------------------------------------------------- -// Single line (input) and multiline (textarea) form field styles -@import '../Label/LabelMixins.scss'; - -// component slots -$field-background-color: $inputBackgroundColor; -$field-background-disabled-color: $disabledBackgroundColor; - -$field-border-color: $inputBorderColor; -$field-border-disabled-color: $disabledBackgroundColor; -$field-border-hover-color: $inputBorderHoveredColor; -$field-border-focus-color: $inputFocusBorderAltColor; -$field-border-error-color: $errorTextColor; - -$field-text-color: $bodyTextColor; -$field-text-disabled-color: $disabledTextColor; - -$field-placeholder-color: $bodySubtextColor; -$field-placeholder-disabled-color: $disabledTextColor; - -$field-description-color: $bodySubtextColor; -$field-error-color: $errorTextColor; - -// Mixin for focus border, including high contrast -@mixin fieldFocusBorder { - border-color: $field-border-focus-color; - @include high-contrast { - border-width: 2px; - .field { - @include ms-padding(0, 11px, 0, 11px); - } - } -} - -// the box containing the label and input field -.root { - @include ms-normalize; - position: relative; /* Needed to position icon */ -} - -.screenReaderOnly { - @include ms-screenReaderOnly; -} - -//= State: default -.fieldGroup { - @include ms-normalize; - border: 1px solid $field-border-color; - background: $field-background-color; - height: 32px; - display: flex; - flex-direction: row; - align-items: stretch; - position: relative; - - &:hover { - border-color: $field-border-hover-color; - } - - &.fieldGroupIsFocused { - @include fieldFocusBorder(); - } - - &.fieldGroupIsFocused { - &.invalid { - border-color: $field-border-error-color; - } - } - - .rootIsDisabled & { - background-color: $field-background-disabled-color; - border-color: $field-border-disabled-color; - } - - &:hover, - &.fieldGroupIsFocused { - @include high-contrast { - border-color: Highlight; - } - } - - &::-ms-clear { - display: none; - } - - ::placeholder, - :-ms-input-placeholder { - color: $inputPlaceholderTextColor; - opacity: 1; - } -} - -//= State: A disabled textfield -.root.rootIsDisabled { - :global(.field) { - background-color: $field-background-disabled-color; - border-color: $field-border-disabled-color; - } -} - -.fieldPrefixSuffix { - align-items: center; - background: $ms-color-neutralLighter; - color: $ms-color-neutralSecondary; - display: flex; - line-height: 1; - padding: 0 10px; - white-space: nowrap; -} - -.field { - @include ms-normalize; - font-size: $ms-font-size-m; - border-radius: 0; - border: none; - background: none; - background-color: transparent; - color: $field-text-color; - @include ms-padding(0, 12px, 0, 12px); - width: 100%; - /** - * min-width is set to 1 in order to tell the browser to ignore the - * default value calculated from the size attribute and respect the - * width set on parent elements. - */ - min-width: 0; - text-overflow: ellipsis; - outline: 0; - &:active, - &:focus, - &:hover { - outline: 0; - } - &.hasIcon { - @include ms-padding-right(24px); - } - - &[disabled] { - background-color: transparent; - border-color: transparent; - } -} - -.field::placeholder { - color: $field-placeholder-color; -} - -//= State: A required textfield -.root.rootIsRequiredLabel { - :global(.ms-Label) { - @include ms-Label-is-required; - } -} - -.root.rootIsRequiredPlaceholderOnly { - :global(.ms-TextField-fieldGroup) { - &::after { - content: '*'; - color: $ms-color-error; - position: absolute; - top: -5px; - @include ms-right(-10px); - } - } -} - -//= State: An active textfield -.root.rootIsActive { - @include fieldFocusBorder(); -} - -.icon { - pointer-events: none; - position: absolute; - bottom: 5px; - @include right(8px); - top: auto; - font-size: 16px; - line-height: 18px; -} - -.description { - color: $field-description-color; - font-size: $ms-font-size-xs; -} - -.rootIsBorderless .fieldGroup { - border-color: transparent; - border-width: 0; -} - -//== Modifier: Single line (default), underlined -.root.rootIsUnderlined { - border: 0px solid $field-border-color; - - .wrapper { - display: flex; - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: inherit; - width: 100%; - - &.invalid, - &.invalid:focus, - &.invalid:hover { - border-bottom: 1px solid $field-border-error-color; - } - } - - :global(.ms-Label) { - font-size: $ms-font-size-m; - @include ms-margin-right(8px); - @include ms-padding-left(12px); - line-height: 22px; // 32px minus 5px padding top/bottom - height: 32px; - } - - .fieldGroup { - flex: 1 1 0px; - border-width: 0; - - @include ms-text-align(left); - } - - &.rootIsDisabled { - border-color: $field-border-disabled-color; - - :global(.ms-Label) { - @include ms-Label-is-disabled; - } - - .field { - background-color: transparent; - color: $field-text-disabled-color; - } - - .fieldGroup { - background-color: transparent; - } - } - - &:hover:not(.rootIsActive):not(.rootIsDisabled) { - border-color: $field-border-hover-color; - } - - &.rootIsActive { - @include fieldFocusBorder(); - } - - &:hover:not(.rootIsDisabled), - &.rootIsActive { - .wrapper { - @include high-contrast { - border-color: Highlight; - } - } - } -} - -//== Modifier: Multiline textfield -// -.root.rootIsMultiline { - .fieldGroup { - min-height: 60px; - height: auto; - display: flex; - } - - .field { - line-height: 17px; - flex-grow: 1; - padding-top: 6px; - overflow: auto; - width: 100%; - - &.hasIcon { - @include ms-padding-right(40px); - } - } -} - -// @todo: https://github.com/OfficeDev/Office-UI-Fabric/issues/359 -.errorMessage { - @include ms-font-s; - color: $field-error-color; - margin: 0; - padding-top: 5px; - display: flex; - align-items: center; -} - -.invalid, -.invalid:focus, -.invalid:hover { - border-color: $field-border-error-color; -} - -.root.rootIsUnderlined { - :global(.ms-Label) { - @include ms-padding-left(12px); - @include ms-padding-right(0); - } - - .field { - @include text-align(left); - } -} - -.root.rootIsMultiline { - .icon { - @include ms-padding-right(24px); - padding-bottom: 8px; - align-items: flex-end; - } - - .field.fieldIsUnresizable { - resize: none; - } -} - -.hidden { - display: none; -} diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.styles.tsx b/packages/office-ui-fabric-react/src/components/TextField/TextField.styles.tsx new file mode 100644 index 00000000000000..330c4a665cd8cf --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.styles.tsx @@ -0,0 +1,377 @@ +import { + AnimationClassNames, + FontSizes, + getGlobalClassNames, + HighContrastSelector, + IStyle, + normalize +} from '../../Styling'; +import { ILabelStyles } from '../../Label'; +import { ITextFieldStyleProps, ITextFieldStyles } from './TextField.types'; +import { IStyleFunctionOrObject } from '@uifabric/utilities'; +import { ILabelStyleProps } from 'office-ui-fabric-react/lib/components/Label'; + +const globalClassNames = { + root: 'ms-TextField', + description: 'ms-TextField-description', + errorMessage: 'ms-TextField-errorMessage', + field: 'ms-TextField-field', + fieldGroup: 'ms-TextField-fieldGroup', + prefix: 'ms-TextField-prefix', + suffix: 'ms-TextField-suffix', + wrapper: 'ms-TextField-wrapper', + + multiline: 'ms-TextField--multiline', + borderless: 'ms-TextField--borderless', + underlined: 'ms-TextField--underlined', + unresizable: 'ms-TextField--unresizable', + + required: 'is-required', + disabled: 'is-disabled', + active: 'is-active' +}; + +function getLabelStyles(props: ITextFieldStyleProps): IStyleFunctionOrObject { + const { underlined, disabled } = props; + return () => ({ + root: [ + underlined && + disabled && { + color: props.theme.palette.neutralTertiary + }, + underlined && { + fontSize: FontSizes.medium, + marginRight: 8, + paddingLeft: 12, + paddingRight: 0, + lineHeight: '22px', + height: 32 + } + ] + }); +} + +export function getStyles(props: ITextFieldStyleProps): ITextFieldStyles { + const { + theme, + className, + disabled, + focused, + required, + multiline, + hasLabel, + borderless, + underlined, + hasIcon, + resizable, + hasErrorMessage, + iconClass + } = props; + + const { semanticColors, palette } = theme; + + const classNames = getGlobalClassNames(globalClassNames, theme); + + const fieldPrefixSuffix: IStyle = { + background: palette.neutralLighter, + color: palette.neutralSecondary, + display: 'flex', + alignItems: 'center', + padding: '0 10px', + lineHeight: 1, + whiteSpace: 'nowrap' + }; + + return { + root: [ + classNames.root, + required && classNames.required, + disabled && classNames.disabled, + focused && classNames.active, + multiline && classNames.multiline, + borderless && classNames.borderless, + underlined && classNames.underlined, + normalize, + { + position: 'relative', + selectors: { + [HighContrastSelector]: { + borderWidth: 2 + } + } + }, + focused && { + borderColor: semanticColors.inputFocusBorderAlt + }, + underlined && + !focused && { + border: `0px solid ${semanticColors.inputBorder}` + }, + underlined && + !disabled && + !focused && { + selectors: { + ':hover': { + borderColor: semanticColors.inputBorderHovered + } + } + }, + className + ], + wrapper: [ + classNames.wrapper, + underlined && { + display: 'flex', + borderBottomWidth: 1, + borderBottomStyle: 'solid', + borderBottomColor: 'inherit', + width: '100%' + }, + hasErrorMessage && { + borderColor: semanticColors.errorText, + selectors: { + '&:focus, &:hover': { + borderColor: semanticColors.errorText + } + } + }, + hasErrorMessage && + underlined && + !disabled && { + borderBottom: `1px solid ${semanticColors.errorText}`, + selectors: { + ':focus': { + borderBottom: `1px solid ${semanticColors.errorText}` + }, + ':hover': { + borderBottom: `1px solid ${semanticColors.errorText}` + } + } + }, + underlined && + disabled && { + borderBottomColor: semanticColors.disabledBackground + }, + underlined && + !disabled && { + selectors: { + ':hover': { + selectors: { + [HighContrastSelector]: { + borderColor: 'Highlight' + } + } + } + } + }, + underlined && + focused && { + selectors: { + [HighContrastSelector]: { + borderColor: 'Highlight' + } + } + } + ], + fieldGroup: [ + classNames.fieldGroup, + normalize, + { + border: `1px solid ${semanticColors.inputBorder}`, + background: semanticColors.bodyBackground, + height: 32, + display: 'flex', + flexDirection: 'row', + alignItems: 'stretch', + position: 'relative', + selectors: { + ':-ms-clear': { + display: 'none' + }, + ':hover': { + selectors: { + [HighContrastSelector]: { + borderColor: 'Highlight' + } + } + }, + '::placeholder': { + color: semanticColors.inputPlaceholderText, + opacity: 1 + }, + ':-ms-input-placeholder': { + color: semanticColors.inputPlaceholderText, + opacity: 1 + } + } + }, + multiline && { + minHeight: '60px', + height: 'auto', + display: 'flex' + }, + borderless && { + borderColor: 'transparent', + borderWidth: 0 + }, + !focused && + !disabled && { + selectors: { + ':hover': { + borderColor: semanticColors.inputBorderHovered + } + } + }, + focused && { + borderColor: semanticColors.inputFocusBorderAlt, + selectors: { + [HighContrastSelector]: { + borderWidth: 2, + borderColor: 'Highlight' + } + } + }, + disabled && { + backgroundColor: semanticColors.disabledBackground, + borderColor: semanticColors.disabledBackground + }, + underlined && { + flex: '1 1 0px', + borderWidth: 0, + textAlign: 'left' + }, + underlined && + disabled && { + backgroundColor: 'transparent' + }, + hasErrorMessage && { + borderColor: semanticColors.errorText, + selectors: { + '&:focus, &:hover': { + borderColor: semanticColors.errorText + } + } + }, + hasErrorMessage && + focused && { + borderColor: semanticColors.errorText + }, + !hasLabel && + required && { + selectors: { + ':after': { + content: `'*'`, + color: semanticColors.errorText, + position: 'absolute', + top: -5, + right: -10 + } + } + } + ], + field: [ + classNames.field, + normalize, + { + fontSize: FontSizes.medium, + borderRadius: 0, + border: 'none', + background: 'none', + backgroundColor: 'transparent', + color: semanticColors.bodyText, + padding: '0 12px', + width: '100%', + minWidth: 0, + textOverflow: 'ellipsis', + outline: 0, + selectors: { + '&:active, &:focus, &:hover': { outline: 0 }, + '::placeholder': { + color: semanticColors.bodySubtext + } + } + }, + multiline && + !resizable && [ + classNames.unresizable, + { + resize: 'none' + } + ], + multiline && { + lineHeight: 17, + flexGrow: 1, + paddingTop: 6, + overflow: 'auto', + width: '100%' + }, + hasIcon && { + paddingRight: 24 + }, + multiline && + hasIcon && { + paddingRight: 40 + }, + disabled && { + backgroundColor: 'transparent', + borderColor: 'transparent' + }, + underlined && { + textAlign: 'left' + }, + underlined && + disabled && { + backgroundColor: 'transparent', + color: semanticColors.disabledText + }, + focused && { + selectors: { + [HighContrastSelector]: { + padding: '0 11px 0 11px' + } + } + } + ], + icon: [ + multiline && { + paddingRight: 24, + paddingBottom: 8, + alignItems: 'flex-end' + }, + { + pointerEvents: 'none', + position: 'absolute', + bottom: 5, + right: 8, + top: 'auto', + fontSize: 16, + lineHeight: 18 + }, + iconClass + ], + description: [ + classNames.description, + { + color: semanticColors.bodySubtext, + fontSize: FontSizes.xSmall + } + ], + errorMessage: [ + classNames.errorMessage, + AnimationClassNames.slideDownIn20, + theme.fonts.small, + { + color: semanticColors.errorText, + margin: 0, + paddingTop: 5, + display: 'flex', + alignItems: 'center' + } + ], + prefix: [classNames.prefix, fieldPrefixSuffix], + suffix: [classNames.suffix, fieldPrefixSuffix], + subComponentStyles: { + label: getLabelStyles(props) + } + }; +} diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.test.tsx b/packages/office-ui-fabric-react/src/components/TextField/TextField.test.tsx index bc4209263bd0a9..87d5f13ff1d199 100644 --- a/packages/office-ui-fabric-react/src/components/TextField/TextField.test.tsx +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.test.tsx @@ -5,9 +5,19 @@ import * as ReactTestUtils from 'react-dom/test-utils'; import * as renderer from 'react-test-renderer'; import { mount } from 'enzyme'; +import { createRef, resetIds } from '../../Utilities'; + import { TextField } from './TextField'; +import { TextFieldBase } from './TextField.base'; +import { ITextFieldStyles } from './TextField.types'; describe('TextField', () => { + const textFieldRef = createRef(); + + beforeEach(() => { + resetIds(); + }); + function renderIntoDocument(element: React.ReactElement): HTMLElement { const component = ReactTestUtils.renderIntoDocument(element); const renderedDOM = ReactDOM.findDOMNode(component as React.ReactInstance); @@ -31,38 +41,96 @@ describe('TextField', () => { expect(tree).toMatchSnapshot(); }); + it('renders TextField multiline unresizable correctly', () => { + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders TextField multiline resizable correctly', () => { + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders multiline TextField correctly with props affecting styling', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders multiline TextField correctly with errorMessage', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should resepect user component and subcomponent styling', () => { + const styles: Partial = { + root: 'root-testClassName', + subComponentStyles: { + label: { + root: 'label-testClassName' + } + } + }; + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('should render label and value to input element', () => { const exampleLabel = 'this is label'; const exampleValue = 'this is value'; - const renderedDOM: HTMLElement = renderIntoDocument(); - - // Assert on the input element. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - expect(inputDOM.value).toEqual(exampleValue); + const textField = mount(); - // Assert on the label element. - const labelDOM: HTMLLabelElement = renderedDOM.getElementsByTagName('label')[0]; - expect(labelDOM.textContent).toEqual(exampleLabel); + expect(textField.getDOMNode().querySelector('input')!.value).toEqual(exampleValue); + expect(textField.getDOMNode().querySelector('label')!.textContent).toEqual(exampleLabel); }); it('should render prefix in input element', () => { const examplePrefix = 'this is a prefix'; - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); // Assert on the prefix - const prefixDOM: Element = renderedDOM.getElementsByClassName('ms-TextField-prefix')[0]; + const prefixDOM: Element = textField.getDOMNode().getElementsByClassName('ms-TextField-prefix')[0]; expect(prefixDOM.textContent).toEqual(examplePrefix); }); it('should render suffix in input element', () => { const exampleSuffix = 'this is a suffix'; - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); // Assert on the suffix - const suffixDOM: Element = renderedDOM.getElementsByClassName('ms-TextField-suffix')[0]; + const suffixDOM: Element = textField.getDOMNode().getElementsByClassName('ms-TextField-suffix')[0]; expect(suffixDOM.textContent).toEqual(exampleSuffix); }); @@ -70,104 +138,91 @@ describe('TextField', () => { const examplePrefix = 'this is a prefix'; const exampleSuffix = 'this is a suffix'; - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); // Assert on the prefix and suffix - const prefixDOM: Element = renderedDOM.getElementsByClassName('ms-TextField-prefix')[0]; - const suffixDOM: Element = renderedDOM.getElementsByClassName('ms-TextField-suffix')[0]; + const prefixDOM: Element = textField.getDOMNode().getElementsByClassName('ms-TextField-prefix')[0]; + const suffixDOM: Element = textField.getDOMNode().getElementsByClassName('ms-TextField-suffix')[0]; expect(prefixDOM.textContent).toEqual(examplePrefix); expect(suffixDOM.textContent).toEqual(exampleSuffix); }); it('should render multiline as text area element', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const testText = 'This\nIs\nMultiline\nText\n'; + const textField = mount(); - // Assert on the input element. - const inputDOM: HTMLTextAreaElement = renderedDOM.getElementsByTagName('textarea')[0]; - expect(inputDOM.value).toBeDefined(); + expect(textField.getDOMNode().querySelector('textarea')!.value).toEqual(testText); }); it('should associate the label and input box', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - const labelDOM: HTMLLabelElement = renderedDOM.getElementsByTagName('label')[0]; + const inputDOM = textField.getDOMNode().querySelector('input'); + const labelDOM = textField.getDOMNode().querySelector('label'); // Assert the input ID and label FOR attribute are the same. - expect(inputDOM.id).toBeDefined(); - expect(inputDOM.id).toEqual(labelDOM.htmlFor); + expect(inputDOM!.id).toBeDefined(); + expect(inputDOM!.id).toEqual(labelDOM!.htmlFor); }); it('should render a disabled input element', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - // Assert the input box is disabled. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - expect(inputDOM.disabled).toEqual(true); + expect(textField.getDOMNode().querySelector('input')!.disabled).toEqual(true); }); it('should render a readonly input element', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - // Assert the input box is readOnly. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - expect(inputDOM.readOnly).toEqual(true); + expect(textField.getDOMNode().querySelector('input')!.readOnly).toEqual(true); }); it('should render a value of 0 when given the number 0', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - - // Assert on the input element. - expect(inputDOM.value).toEqual('0'); + expect(textField.getDOMNode().querySelector('input')!.value).toEqual('0'); }); it('should render a default value of 0 when given the number 0', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); - - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const textField = mount(); - // Assert on the input element. - expect(inputDOM.defaultValue).toEqual('0'); + expect(textField.getDOMNode().querySelector('input')!.defaultValue).toEqual('0'); }); it('should NOT update state when props value remains undefined on props update', () => { const stateValue = 'state value'; - const textField = mount(); - expect(textField.state('value')).toEqual(''); + const textField = mount(); + expect(textFieldRef.current!.state.value).toEqual(''); - textField.setState({ value: stateValue }); - expect(textField.state('value')).toEqual(stateValue); + textFieldRef.current!.setState({ value: stateValue }); + expect(textFieldRef.current!.state.value).toEqual(stateValue); // Trigger a props update, but value prop remains the same undefined value, // so state should not be affected. textField.setProps({ id: 'unimportantValue' }); - expect(textField.state('value')).toEqual(stateValue); + expect(textFieldRef.current!.state.value).toEqual(stateValue); }); it('should update state when props value changes from defined to undefined', () => { const propsValue = 'props value'; - const textField = mount(); - expect(textField.state('value')).toEqual(propsValue); + const textField = mount(); + expect(textFieldRef.current!.state.value).toEqual(propsValue); textField.setProps({ value: undefined }); - expect(textField.state('value')).toEqual(''); + expect(textFieldRef.current!.state.value).toEqual(''); }); describe('error message', () => { const errorMessage = 'The string is too long, should not exceed 3 characters.'; - function assertErrorMessage(renderedDOM: HTMLElement, expectedErrorMessage: string | boolean): void { - const errorMessageDOM: HTMLElement = renderedDOM.querySelector( - '[data-automation-id=error-message]' - ) as HTMLElement; + function assertErrorMessage(renderedDOM: Element, expectedErrorMessage: string | boolean): void { + const errorMessageDOM = renderedDOM.querySelector('[data-automation-id=error-message]'); if (expectedErrorMessage === false) { expect(errorMessageDOM).toBeNull(); // element not exists } else { - expect(errorMessageDOM.textContent).toEqual(expectedErrorMessage); + expect(errorMessageDOM!.textContent).toEqual(expectedErrorMessage); } } @@ -176,15 +231,15 @@ describe('TextField', () => { return value.length > 3 ? errorMessage : ''; } - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( ); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.change(inputDOM, mockEvent('the input value')); + const inputDOM = textField.getDOMNode().querySelector('input'); + ReactTestUtils.Simulate.change(inputDOM as Element, mockEvent('the input value')); // The value is delayed to validate, so it must to query error message after a while. - return delay(250).then(() => assertErrorMessage(renderedDOM, errorMessage)); + return delay(250).then(() => assertErrorMessage(textField.getDOMNode(), errorMessage)); }); it('should render error message when onGetErrorMessage returns a Promise', () => { @@ -192,19 +247,19 @@ describe('TextField', () => { return Promise.resolve(value.length > 3 ? errorMessage : ''); } - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( ); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.change(inputDOM, mockEvent('the input value')); + const inputDOM = textField.getDOMNode().querySelector('input'); + ReactTestUtils.Simulate.change(inputDOM as Element, mockEvent('the input value')); // The value is delayed to validate, so it must to query error message after a while. - return delay(250).then(() => assertErrorMessage(renderedDOM, errorMessage)); + return delay(250).then(() => assertErrorMessage(textField.getDOMNode(), errorMessage)); }); it('should render error message on first render when onGetErrorMessage returns a string', () => { - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( { /> ); - return delay(20).then(() => assertErrorMessage(renderedDOM, errorMessage)); + return delay(20).then(() => assertErrorMessage(textField.getDOMNode(), errorMessage)); }); it('should render error message on first render when onGetErrorMessage returns a Promise', () => { - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( { ); // The Promise based validation need to assert with async pattern. - return delay(20).then(() => assertErrorMessage(renderedDOM, errorMessage)); + return delay(20).then(() => assertErrorMessage(textField.getDOMNode(), errorMessage)); }); it('should not render error message when onGetErrorMessage return an empty string', () => { - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( { /> ); - delay(20).then(() => assertErrorMessage(renderedDOM, /* exist */ false)); + delay(20).then(() => assertErrorMessage(textField.getDOMNode(), /* exist */ false)); }); it('should not render error message when no value is provided', () => { let actualValue: string | undefined = undefined; - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( { /> ); - delay(20).then(() => assertErrorMessage(renderedDOM, /* exist */ false)); + delay(20).then(() => assertErrorMessage(textField.getDOMNode(), /* exist */ false)); expect(actualValue).toEqual(''); }); @@ -263,15 +318,13 @@ describe('TextField', () => { return value.length > 3 ? errorMessage : ''; } - const renderedDOM: HTMLElement = renderIntoDocument( - - ); + const textField = mount(); - delay(20).then(() => assertErrorMessage(renderedDOM, errorMessage)); + delay(20).then(() => assertErrorMessage(textField.getDOMNode(), errorMessage)); - ReactDOM.render(, renderedDOM.parentElement); + textField.setProps({ value: '' }); - return delay(250).then(() => assertErrorMessage(renderedDOM, /* exist */ false)); + return delay(250).then(() => assertErrorMessage(textField.getDOMNode(), /* exist */ false)); }); it('should trigger validation only on focus', () => { @@ -281,11 +334,9 @@ describe('TextField', () => { return value.length > 3 ? errorMessage : ''; }; - const renderedDOM: HTMLElement = renderIntoDocument( - - ); + const textField = mount(); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const inputDOM = textField.getDOMNode().querySelector('input') as Element; ReactTestUtils.Simulate.input(inputDOM, mockEvent('the input value')); expect(validationCallCount).toEqual(1); @@ -305,11 +356,9 @@ describe('TextField', () => { return value.length > 3 ? errorMessage : ''; }; - const renderedDOM: HTMLElement = renderIntoDocument( - - ); + const textField = mount(); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const inputDOM = textField.getDOMNode().querySelector('input') as Element; ReactTestUtils.Simulate.input(inputDOM, mockEvent('the input value')); expect(validationCallCount).toEqual(1); @@ -330,11 +379,11 @@ describe('TextField', () => { return value.length > 3 ? errorMessage : ''; }; - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( ); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const inputDOM = textField.getDOMNode().querySelector('input') as Element; ReactTestUtils.Simulate.input(inputDOM, mockEvent('value before focus')); expect(validationCallCount).toEqual(1); @@ -369,21 +418,25 @@ describe('TextField', () => { }); it('can render a default value', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - expect(renderedDOM.querySelector('input')!.value).toEqual('initial value'); + expect(textField.getDOMNode().querySelector('input')).toBeTruthy(); + expect(textField.getDOMNode().querySelector('input')!.value).toEqual('initial value'); }); it('can render a default value as a textarea', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); - expect(renderedDOM.querySelector('textarea')!.value).toEqual('initial value'); + expect(textField.getDOMNode().querySelector('textarea')).toBeTruthy(); + expect(textField.getDOMNode().querySelector('textarea')!.value).toEqual('initial value'); }); it('can render description text', () => { - const renderedDOM: HTMLElement = renderIntoDocument(); + const testDescription = 'A custom description'; + const textField = mount(); - expect(renderedDOM.querySelector('.ms-TextField-description')!.textContent).toEqual('A custom description'); + expect(textField.getDOMNode().querySelector('.ms-TextField-description')).toBeTruthy(); + expect(textField.getDOMNode().querySelector('.ms-TextField-description')!.textContent).toEqual(testDescription); }); it('can render a static custom description without description text', () => { @@ -404,7 +457,7 @@ describe('TextField', () => { callCount++; }; - const renderedDOM: HTMLElement = renderIntoDocument( + const textField = mount( { ); expect(callCount).toEqual(0); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const inputDOM = textField.getDOMNode().querySelector('input') as Element; ReactTestUtils.Simulate.input(inputDOM, mockEvent('value change')); ReactTestUtils.Simulate.change(inputDOM, mockEvent('value change')); @@ -431,10 +484,10 @@ describe('TextField', () => { callCount++; }; - const renderedDOM: HTMLElement = renderIntoDocument(); + const textField = mount(); expect(callCount).toEqual(0); - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const inputDOM = textField.getDOMNode().querySelector('input') as Element; ReactTestUtils.Simulate.input(inputDOM, mockEvent('')); ReactTestUtils.Simulate.change(inputDOM, mockEvent('')); @@ -442,7 +495,6 @@ describe('TextField', () => { }); it('should select a range of text', () => { - let textField: TextField | undefined; const initialValue = 'initial value'; const onSelect = () => { @@ -450,8 +502,8 @@ describe('TextField', () => { expect(selectedText).toEqual(initialValue); }; - renderIntoDocument( (textField = t!)} defaultValue={initialValue} onSelect={onSelect} />); + renderIntoDocument(); - textField!.setSelectionRange(0, initialValue.length); + textFieldRef.current!.setSelectionRange(0, initialValue.length); }); }); diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.ts b/packages/office-ui-fabric-react/src/components/TextField/TextField.ts new file mode 100644 index 00000000000000..652abb10c7a9e9 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.ts @@ -0,0 +1,7 @@ +import { styled } from '../../Utilities'; +import { TextFieldBase } from './TextField.base'; +import { ITextFieldProps, ITextFieldStyles, ITextFieldStyleProps } from './TextField.types'; +import { getStyles } from './TextField.styles'; +export { ITextField } from './TextField.types'; + +export const TextField = styled(TextFieldBase, getStyles); diff --git a/packages/office-ui-fabric-react/src/components/TextField/TextField.types.ts b/packages/office-ui-fabric-react/src/components/TextField/TextField.types.ts index 9af136d469bc8f..3ddcc19dfbb896 100644 --- a/packages/office-ui-fabric-react/src/components/TextField/TextField.types.ts +++ b/packages/office-ui-fabric-react/src/components/TextField/TextField.types.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { IRefObject, IRenderFunction } from '../../Utilities'; +import { IStyle, IStyleSet, ITheme } from '../../Styling'; +import { IRefObject, IRenderFunction, IStyleFunctionOrObject } from '../../Utilities'; import { IIconProps } from '../../Icon'; export interface ITextField { @@ -227,6 +227,16 @@ export interface ITextFieldProps extends React.AllHTMLAttributes; + /** * @deprecated * Deprecated; use iconProps instead. @@ -271,3 +281,79 @@ export interface ITextFieldProps extends React.AllHTMLAttributes> & + Pick< + ITextFieldProps, + 'className' | 'disabled' | 'required' | 'multiline' | 'borderless' | 'resizable' | 'underlined' | 'iconClass' + > & { + /** Element has an error message. */ + hasErrorMessage?: boolean; + /** Element has an icon. */ + hasIcon?: boolean; + /** Element has a label. */ + hasLabel?: boolean; + /** Element has focus. */ + focused?: boolean; + }; + +export interface ITextFieldSubComponentStyles { + /** + * Styling for Label child component. + */ + // TODO: this should be the interface once we're on TS 2.9.2 but otherwise causes errors in 2.8.4 + // label: IStyleFunctionOrObject; + label: IStyleFunctionOrObject; +} + +export interface ITextFieldStyles extends IStyleSet { + /** + * Style for root element. + */ + root: IStyle; + + /** + * Style for field group encompassing entry area (prefix, field, icon and suffix). + */ + fieldGroup: IStyle; + + /** + * Style for prefix element. + */ + prefix: IStyle; + + /** + * Style for suffix element. + */ + suffix: IStyle; + + /** + * Style for main field entry element. + */ + field: IStyle; + + /** + * Style for icon prop element. + */ + icon: IStyle; + + /** + * Style for description element. + */ + description: IStyle; + + /** + * Style for TextField wrapper element. + */ + wrapper: IStyle; + + /** + * Style for error message element. + */ + errorMessage: IStyle; + + /** + * Styling for subcomponents. + */ + subComponentStyles: ITextFieldSubComponentStyles; +} diff --git a/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.deprecated.test.tsx.snap b/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.deprecated.test.tsx.snap new file mode 100644 index 00000000000000..80a1775221157b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.deprecated.test.tsx.snap @@ -0,0 +1,158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TextField deprecated renders with deprecated props affecting styling 1`] = ` +
+
+
+
+ + test addonString + +
+ + +
+
+
+`; diff --git a/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.test.tsx.snap b/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.test.tsx.snap index 5b76275c217798..8a4f1af8d6f17e 100644 --- a/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/TextField/__snapshots__/TextField.test.tsx.snap @@ -2,10 +2,29 @@ exports[`TextField renders TextField correctly 1`] = `
`; + +exports[`TextField renders TextField multiline resizable correctly 1`] = ` +
+
+ +
+