diff --git a/common/changes/@uifabric/utilities/revert-choicegroup_2018-05-23-14-52.json b/common/changes/@uifabric/utilities/revert-choicegroup_2018-05-23-14-52.json new file mode 100644 index 00000000000000..ae80ecb4b043ad --- /dev/null +++ b/common/changes/@uifabric/utilities/revert-choicegroup_2018-05-23-14-52.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/utilities", + "comment": "Reverting the ChoiceGroup styling update along with updates to utilities to avoid potentially breaking changes.", + "type": "minor" + } + ], + "packageName": "@uifabric/utilities", + "email": "dzearing@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/revert-choicegroup_2018-05-23-14-52.json b/common/changes/office-ui-fabric-react/revert-choicegroup_2018-05-23-14-52.json new file mode 100644 index 00000000000000..1a80214b64b850 --- /dev/null +++ b/common/changes/office-ui-fabric-react/revert-choicegroup_2018-05-23-14-52.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "ChoiceGroup: Reverting the updates to ChoiceGroup styling. We found some breaking changes in it, so we'd like to minimize partner impact by moving this to the 6.0 (next) release. Sorry for the trouble.", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "dzearing@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.base.tsx b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.base.tsx deleted file mode 100644 index b36af01b0d5ca2..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.base.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import * as React from 'react'; -import { Label } from '../../Label'; -import { ChoiceGroupOption, OnFocusCallback, OnChangeCallback } from './ChoiceGroupOption'; -import { IChoiceGroupOption, IChoiceGroupProps, IChoiceGroupStyleProps, IChoiceGroupStyles } from './ChoiceGroup.types'; -import { - BaseComponent, - customizable, - classNamesFunction, - createRef, - getId -} from '../../Utilities'; - -const getClassNames = classNamesFunction(); - -export interface IChoiceGroupState { - keyChecked: string | number; - - /** Is true when the control has focus. */ - keyFocused?: string | number; -} - -@customizable('ChoiceGroup', ['theme']) -export class ChoiceGroupBase extends BaseComponent { - public static defaultProps: IChoiceGroupProps = { - options: [] - }; - - private _id: string; - private _labelId: string; - private _inputElement = createRef(); - private focusedVars: { [key: string]: OnFocusCallback } = {}; - private changedVars: { [key: string]: OnChangeCallback } = {}; - - constructor(props: IChoiceGroupProps, ) { - super(props); - - this._warnDeprecations({ 'onChanged': 'onChange' }); - this._warnMutuallyExclusive({ - selectedKey: 'defaultSelectedKey' - }); - - this.state = { - keyChecked: (props.defaultSelectedKey === undefined) ? - this._getKeyChecked(props)! : - props.defaultSelectedKey, - keyFocused: undefined - }; - - this._id = getId('ChoiceGroup'); - this._labelId = getId('ChoiceGroupLabel'); - } - - public componentWillReceiveProps(newProps: IChoiceGroupProps): void { - const newKeyChecked = this._getKeyChecked(newProps); - const oldKeyCheched = this._getKeyChecked(this.props); - - if (newKeyChecked !== oldKeyCheched) { - this.setState({ - keyChecked: newKeyChecked!, - }); - } - } - - public render(): JSX.Element { - const { - className, - theme, - getStyles, - options, - label, - required, - disabled, - name - } = this.props; - const { keyChecked, keyFocused } = this.state; - - const classNames = getClassNames(getStyles!, { - theme: theme!, - className, - optionsContainIconOrImage: options!.some(option => Boolean(option.iconProps || option.imageSrc)) - }); - - const ariaLabelledBy = label ? this._id + '-label' : (this.props as any)['aria-labelledby']; - - return ( - // Need to assign role application on containing div because JAWS doesn't call OnKeyDown without this role -
-
- { label && () } -
- { options!.map((option: IChoiceGroupOption) => { - - const innerOptionProps = { - ...option, - focused: option.key === keyFocused, - checked: option.key === keyChecked, - disabled: option.disabled || disabled, - id: `${this._id}-${option.key}`, - labelId: `${this._labelId}-${option.key}`, - name: name || this._id, - required - }; - - return ( - - ); - }) } -
-
-
- ); - - } - - public focus() { - if (this._inputElement.current) { - this._inputElement.current.focus(); - } - } - - private _onFocus = (key: string) => - this.focusedVars[key] ? this.focusedVars[key] : this.focusedVars[key] = - (ev: React.FocusEvent, option: IChoiceGroupOption) => { - this.setState({ - keyFocused: key, - keyChecked: this.state.keyChecked - }); - } - - private _onBlur = (ev: React.FocusEvent, option: IChoiceGroupOption) => { - this.setState({ - keyFocused: undefined, - keyChecked: this.state.keyChecked - }); - } - - private _onChange = (key: string) => - this.changedVars[key] ? this.changedVars[key] : this.changedVars[key] = - (evt, option: IChoiceGroupOption) => { - const { onChanged, onChange, selectedKey, options } = this.props; - - // Only manage state in uncontrolled scenarios. - if (selectedKey === undefined) { - this.setState({ - keyChecked: key - }); - } - - const originalOption = options!.find((value: IChoiceGroupOption) => value.key === key); - - // TODO: onChanged deprecated, remove else if after 07/17/2017 when onChanged has been removed. - if (onChange) { - onChange(evt, originalOption); - } else if (onChanged) { - onChanged(originalOption!); - } - } - - private _getKeyChecked(props: IChoiceGroupProps): string | number | undefined { - if (props.selectedKey !== undefined) { - return props.selectedKey; - } - - const optionsChecked = props.options!.filter((option: IChoiceGroupOption) => { - return option.checked; - }); - - if (optionsChecked.length === 0) { - return undefined; - } else { - return optionsChecked[0].key; - } - } - -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.scss b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.scss new file mode 100644 index 00000000000000..995efecd1ef249 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.scss @@ -0,0 +1,389 @@ +@import '../../common/common'; +@import '../../common/_focusBorder'; + +// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. + + +// -------------------------------------------------- +// ChoiceField semantic slots + +$radioButton-background-color: $bodyBackgroundColor; + +$radioButton-text-color: $bodyTextColor; +$radioButton-text-hover-color: $bodyTextCheckedColor; + +$radioButton-dot-color: $inputBackgroundCheckedColor; + +$radioButton-border-color: $smallInputBorderColor; +$radioButton-border-hover-color: $inputBorderHoveredColor; +$radioButton-border-selected-color: $radioButton-dot-color; +$radioButton-border-selected-hover-color: $inputBackgroundCheckedHoveredColor; + +$radioButton-text-disabled-color: $disabledBodyTextColor; +$radioButton-background-unselected-disabled-color: $disabledTextColor; +$radioButton-dot-disabled-color: $radioButton-background-unselected-disabled-color; +$radioButton-border-disabled-color: $radioButton-background-unselected-disabled-color; + +// +// -------------------------------------------------- +// ChoiceField styles + +$ms-choiceField-field-size: 20px; +$ms-choiceField-image-size: 32px; +$ms-choiceField-iconField-size: 96px; +$ms-choiceField-transition-duration: 200ms; +$ms-choiceField-transition-duration-inner: 150ms; +$ms-choiceField-transition-timing: cubic-bezier(.4, 0, .23, 1); + +//== Component: Choicefield group +// + +.root { + display: block; +} + +.optionsContainIconOrImage { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.choiceField { + display: flex; + align-items: center; + box-sizing: border-box; + color: $radioButton-text-color; + font-size: $ms-font-size-m; + font-weight: $ms-font-weight-regular; + min-height: 26px; + border: none; + position: relative; + margin-top: 8px; + + :global(.ms-Label) { + font-size: $ms-font-size-m; + @include ms-padding(0, 0, 0, 26px); + display: inline-block; + } +} + +// The hidden input +.input { + position: absolute; + opacity: 0; + top: 8px; +} + +// The circle +.field::before { + content: ''; + display: inline-block; + background-color: $radioButton-background-color; + border: 1px solid $radioButton-border-color; + width: $ms-choiceField-field-size; + height: $ms-choiceField-field-size; + font-weight: normal; + position: absolute; + top: 0; + @include ms-left(0); + box-sizing: border-box; + transition-property: border-color; + transition-duration: $ms-choiceField-transition-duration; + transition-timing-function: $ms-choiceField-transition-timing; + border-radius: 50%; +} + +// The dot +.field::after { + content: ''; + width: 0; + height: 0; + border-radius: 50%; + position: absolute; + @include ms-left($ms-choiceField-field-size / 2); + @include ms-right(0); + transition-property: border-width; + transition-duration: $ms-choiceField-transition-duration-inner; + transition-timing-function: $ms-choiceField-transition-timing; + box-sizing: border-box; +} + +.field { + display: inline-block; + cursor: pointer; + margin-top: 0; + position: relative; + vertical-align: top; + user-select: none; + min-height: 20px; + + &:hover, + &:focus { + &::before { + border-color: $radioButton-border-hover-color; + + @include high-contrast { + border-color: Highlight; + } + } + + :global(.ms-Label) { + color: $radioButton-text-hover-color; + + @include high-contrast { + color: Highlight; + } + } + } + + //== State: A choiceField is checked + // + &.fieldIsChecked { + &::before { // the circle + border: 1px solid $radioButton-border-selected-color; + + @include high-contrast { + border-color: Highlight; + } + } + + &::after { // the dot + border: 5px solid $radioButton-dot-color; + @include ms-left(5px); + top: 5px; + width: 10px; + height: 10px; + + @include high-contrast { + border-color: Highlight; + } + } + + &:hover, + &:focus { + &::before { + border-color: $radioButton-border-selected-hover-color; + } + } + } + + //== State: A disabled choiceField + // + &.fieldIsDisabled { + cursor: default; + + &::before { + background-color: $radioButton-background-unselected-disabled-color; + border-color: $radioButton-border-disabled-color; + + @include high-contrast { + border-color: GrayText; + } + } + + :global(.ms-Label) { + color: $radioButton-text-disabled-color; + + @include high-contrast { + color: GrayText; + } + } + } + + &.fieldIsChecked.fieldIsDisabled { + &::before { + background-color: $radioButton-background-color; + border-color: $radioButton-border-disabled-color; + } + &::after { + background-color: $radioButton-dot-disabled-color; + border-color: $radioButton-dot-disabled-color; + } + } +} + +.choiceFieldIsImage, .choiceFieldIsIcon { + display: inline-flex; + font-size: 0; + @include ms-margin(0, 4px, 4px, 0); + @include ms-padding-left(0px); + @include ms-bgColor-neutralLighter; + height: 100%; + + $radioButtonSpacing: 3px; + $radioButtonInnerSize: 5px; + + .fieldIsImage, .fieldIsIcon { + + display: inline-block; + box-sizing: content-box; + cursor: pointer; + padding-top: 22px; + margin: 0; + text-align: center; + transition: all $ms-choiceField-transition-duration ease; + border: 2px solid transparent; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + + &.fieldIsDisabled { + cursor: default; + + .innerField { + opacity: 0.25; + + @include high-contrast { + color: GrayText; + opacity: 1; + } + } + } + + &.imageIsLarge { + .innerField { + padding-left: 24px; + padding-right: 24px; + } + } + + .innerField { + position: relative; + display: inline-block; + padding-left: 30px; + padding-right: 30px; + + .imageWrapper { + padding-bottom: 2px; + transition: opacity $ms-choiceField-transition-duration ease; + + &.imageWrapperIsHidden { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; + opacity: 0; + } + + :global(.ms-Image) { + display: inline-block; + border-style: none; + } + } + } + + .labelWrapper { + $lineHeight: 15px; + display: block; + position: relative; + margin: 4px 8px; + height: $lineHeight*2; + line-height: $lineHeight; + overflow: hidden; + white-space: pre-wrap; + text-overflow: ellipsis; + @include ms-font-m; + + :global(.ms-Label) { + padding: 0; + } + } + + &::before { + top: $radioButtonSpacing; + @include ms-right($radioButtonSpacing); + @include ms-left(auto); // To reset the value of 'left' to its default value, so that 'right' works + opacity: 0; + } + + &::after { + top: $radioButtonSpacing + ($radioButtonInnerSize * 2); + @include ms-right($radioButtonSpacing + ($radioButtonInnerSize * 2)); + @include ms-left(auto); // To reset the value of 'left' to its default value, so that 'right' works + } + + &:not(.fieldIsDisabled) { + &:hover, + &:focus { + border-color: $ms-color-neutralTertiaryAlt; + + @include high-contrast { + border-color: Highlight; + color: Highlight; + } + + &::before { + opacity: 1; + } + } + + + &.fieldIsChecked { + border-color: $ms-color-themePrimary; + + &::before { + opacity: 1; + } + + &::after { + top: $radioButtonSpacing + $radioButtonInnerSize; + @include ms-right($radioButtonSpacing + $radioButtonInnerSize); + } + + &:hover, + &:focus { + border-color: $ms-color-themeDark; + + &::before { + border-color: $ms-color-themeDark; + } + &::after { + background-color: $ms-color-themeDark; + } + } + } + } + } + + // Hidden input for icon and image + .inputHasImage, .inputHasIcon { + top: 0; + right: 0; + opacity: 0; + width: 100%; + height: 100%; + margin: 0; + } +} + +.choiceFieldIsIcon { + $iconSize: 32px; + + .iconWrapper { + font-size: $iconSize; + line-height: $iconSize; + height: $iconSize; + } +} + +:global(.ms-Fabric--isFocusVisible) .choiceFieldIsInFocus .choiceFieldWrapper { + @include focus-border(-2px, $ms-color-neutralPrimary, 1px, false); + + @include high-contrast { + @include focus-border(-2px, WindowText, 2px, false); + } +} + +:global(.ms-Fabric--isFocusVisible) .choiceFieldIsInFocus { + &.choiceFieldIsImage .choiceFieldWrapper, + &.choiceFieldIsIcon .choiceFieldWrapper { + @include focus-border(-2px, $ms-color-neutralSecondary, 1px, false); + + @include high-contrast { + @include focus-border(-2px, WindowText, 1px, false); + } + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.styles.ts b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.styles.ts deleted file mode 100644 index ee9485e65c5cc3..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.styles.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IChoiceGroupStyleProps, IChoiceGroupStyles } from './ChoiceGroup.types'; -import { getGlobalClassNames } from '../../Styling'; - -const GlobalClassNames = { - root: 'ms-ChoiceFieldGroup', - flexContainer: 'ms-ChoiceFieldGroup-flexContainer' -}; - -export const getStyles = (props: IChoiceGroupStyleProps): IChoiceGroupStyles => { - const { className, optionsContainIconOrImage, theme } = props; - - const classNames = getGlobalClassNames(GlobalClassNames, theme); - - return { - applicationRole: className, - root: [ - classNames.root, - { - display: 'block' - } - ], - label: className, - flexContainer: [ - classNames.flexContainer, - optionsContainIconOrImage && { - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap' - } - ] - }; -}; \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.test.tsx b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.test.tsx index 7106f76898f0cd..7748805c52eb5e 100644 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.test.tsx @@ -1,13 +1,13 @@ /* tslint:disable:no-unused-variable */ import * as React from 'react'; +import * as ReactDOM from 'react-dom'; /* tslint:enable:no-unused-variable */ -import { mount } from 'enzyme'; + import * as ReactTestUtils from 'react-dom/test-utils'; import * as renderer from 'react-test-renderer'; import { ChoiceGroup } from './ChoiceGroup'; import { IChoiceGroupOption } from './ChoiceGroup.types'; -import { resetIds } from '../../Utilities'; const TEST_OPTIONS: IChoiceGroupOption[] = [ { key: '1', text: '1', 'data-automation-id': 'auto1' } as IChoiceGroupOption, @@ -18,11 +18,6 @@ const QUERY_SELECTOR = '.ms-ChoiceField-input'; describe('ChoiceGroup', () => { - beforeEach(() => { - // Resetting ids to create predictability in generated ids. - resetIds(); - }); - it('renders ChoiceGroup correctly', () => { const component = renderer.create( { }); it('can change options', () => { - const choiceGroup = mount( - ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); - - expect(choiceOptions.length).toBe(3); + const options: IChoiceGroupOption[] = [ + { key: '1', text: '1' }, + { key: '2', text: '2' }, + { key: '3', text: '3' } + ]; + let threwException = false; + let choiceGroup; + try { + choiceGroup = ReactTestUtils.renderIntoDocument( + + ); + } catch (e) { + threwException = true; + } + expect(threwException).toEqual(false); + + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); expect((choiceOptions[0] as HTMLInputElement).checked).toEqual(false); expect((choiceOptions[1] as HTMLInputElement).checked).toEqual(false); @@ -70,17 +77,28 @@ describe('ChoiceGroup', () => { }); it('An individual choice option can be disabled', () => { - const options = { ...TEST_OPTIONS }; - options[0].disabled = true; - - const choiceGroup = mount( - ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); + const options: IChoiceGroupOption[] = [ + { key: '1', text: '1', disabled: true }, + { key: '2', text: '2' }, + { key: '3', text: '3' } + ]; + let threwException = false; + let choiceGroup; + try { + choiceGroup = ReactTestUtils.renderIntoDocument( + + ); + } catch (e) { + threwException = true; + } + expect(threwException).toEqual(false); + + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); expect((choiceOptions[0] as HTMLInputElement).disabled).toEqual(true); expect((choiceOptions[1] as HTMLInputElement).disabled).toEqual(false); @@ -88,16 +106,29 @@ describe('ChoiceGroup', () => { }); it('renders all choice options as disabled when disabled', () => { - const choiceGroup = mount( - - ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); + const options: IChoiceGroupOption[] = [ + { key: '1', text: '1' }, + { key: '2', text: '2' }, + { key: '3', text: '3' } + ]; + let threwException = false; + let choiceGroup; + try { + choiceGroup = ReactTestUtils.renderIntoDocument( + + ); + } catch (e) { + threwException = true; + } + expect(threwException).toEqual(false); + + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); expect((choiceOptions[0] as HTMLInputElement).disabled).toEqual(true); expect((choiceOptions[1] as HTMLInputElement).disabled).toEqual(true); @@ -105,14 +136,14 @@ describe('ChoiceGroup', () => { }); it('can act as an uncontrolled component', () => { - const choiceGroup = mount( + const choiceGroup = ReactTestUtils.renderIntoDocument( ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); expect((choiceOptions[0] as HTMLInputElement).checked).toEqual(true); @@ -127,15 +158,15 @@ describe('ChoiceGroup', () => { _selectedItem = item; }; - const choiceGroup = mount( + const choiceGroup = ReactTestUtils.renderIntoDocument( ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); expect((choiceOptions[0] as HTMLInputElement).checked).toEqual(true); @@ -150,14 +181,14 @@ describe('ChoiceGroup', () => { it('extra attributes appear in dom if specified', () => { const onChange = (ev: React.FormEvent, item: IChoiceGroupOption | undefined): void => undefined; - const choiceGroup = mount( + const choiceGroup = ReactTestUtils.renderIntoDocument( ); - - const choiceOptions = choiceGroup.getDOMNode().querySelectorAll(QUERY_SELECTOR); + const renderedDOM = ReactDOM.findDOMNode(choiceGroup as React.ReactInstance) as Element; + const choiceOptions = renderedDOM.querySelectorAll(QUERY_SELECTOR); const extraAttributeGetter: (index: number) => string | null = (index: number): string | null => { const input: HTMLInputElement = choiceOptions[index] as HTMLInputElement; diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.tsx b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.tsx index 7347ca0cf384f1..d08aa2d6fcaead 100644 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.tsx +++ b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.tsx @@ -1,6 +1,282 @@ -import { styled } from '../../Utilities'; -import { ChoiceGroupBase } from './ChoiceGroup.base'; -import { IChoiceGroupProps } from './ChoiceGroup.types'; -import { getStyles } from './ChoiceGroup.styles'; +import * as React from 'react'; +import { Image } from '../../Image'; +import { Label } from '../../Label'; +import { Icon } from '../../Icon'; +import { IChoiceGroupOption, IChoiceGroupProps } from './ChoiceGroup.types'; +import { + assign, + BaseComponent, + css, + getId, + getNativeProps, + inputProperties, + createRef +} from '../../Utilities'; +import * as stylesImport from './ChoiceGroup.scss'; +const styles: any = stylesImport; -export const ChoiceGroup: (props: IChoiceGroupProps) => JSX.Element = styled(ChoiceGroupBase, getStyles); +export interface IChoiceGroupState { + keyChecked: string | number; + + /** Is true when the control has focus. */ + keyFocused?: string | number; +} + +export class ChoiceGroup extends BaseComponent { + public static defaultProps: IChoiceGroupProps = { + options: [] + }; + + private _id: string; + private _labelId: string; + private _inputElement = createRef(); + + constructor(props: IChoiceGroupProps, ) { + super(props); + + this._warnDeprecations({ 'onChanged': 'onChange' }); + this._warnMutuallyExclusive({ + selectedKey: 'defaultSelectedKey' + }); + + this.state = { + keyChecked: (props.defaultSelectedKey === undefined) ? + this._getKeyChecked(props)! : + props.defaultSelectedKey, + keyFocused: undefined + }; + + this._id = getId('ChoiceGroup'); + this._labelId = getId('ChoiceGroupLabel'); + } + + public componentWillReceiveProps(newProps: IChoiceGroupProps): void { + const newKeyChecked = this._getKeyChecked(newProps); + const oldKeyCheched = this._getKeyChecked(this.props); + + if (newKeyChecked !== oldKeyCheched) { + this.setState({ + keyChecked: newKeyChecked!, + }); + } + } + + public render(): JSX.Element { + const { label, options, className, required } = this.props; + const { keyChecked, keyFocused } = this.state; + + return ( + // Need to assign role application on containing div because JAWS doesn't call OnKeyDown without this role +
+
+ { this.props.label && ( + + ) } +
Boolean(option.iconProps || option.imageSrc) + ) && styles.optionsContainIconOrImage) } + > + { options!.map((option: IChoiceGroupOption) => { + const { + onRenderField = this._onRenderField, + onRenderLabel = this._onRenderLabel + } = option; + + // Merge internal props into option + assign(option, { + checked: option.key === keyChecked, + disabled: option.disabled || this.props.disabled, + id: `${this._id}-${option.key}`, + labelId: `${this._labelId}-${option.key}`, + onRenderLabel + }); + + return ( +
+
+ + { onRenderField(option, this._onRenderField) } +
+
+ ); + }) } +
+
+
+ ); + } + + public focus() { + if (this._inputElement.current) { + this._inputElement.current.focus(); + } + } + + private _onFocus(option: IChoiceGroupOption, ev: React.FocusEvent): void { + this.setState({ + keyFocused: option.key, + keyChecked: this.state.keyChecked + }); + } + + private _onBlur(option: IChoiceGroupOption, ev: React.FocusEvent): void { + this.setState({ + keyFocused: undefined, + keyChecked: this.state.keyChecked + }); + } + + private _onRenderField(option: IChoiceGroupOption): JSX.Element { + + const { onRenderLabel } = option; + const imageSize = option.imageSize ? option.imageSize : { width: 32, height: 32 }; + const imageIsLarge: boolean = imageSize.width > 71 || imageSize.height > 71; + + return ( + + ); + } + + private _onRenderLabel(option: IChoiceGroupOption): JSX.Element { + return ( + { option.text } + ); + } + + private _onChange(option: IChoiceGroupOption, evt: React.FormEvent): void { + const { onChanged, onChange, selectedKey } = this.props; + + // Only manage state in uncontrolled scenarios. + if (selectedKey === undefined) { + this.setState({ + keyChecked: option.key + }); + } + + // TODO: onChanged deprecated, remove else if after 07/17/2017 when onChanged has been removed. + if (onChange) { + onChange(evt, option); + } else if (onChanged) { + onChanged(option); + } + } + + /** + * If all the isChecked property of options are falsy values, return undefined; + * Else return the key of the first option with the truthy isChecked property. + */ + private _getKeyChecked(props: IChoiceGroupProps): string | number | undefined { + if (props.selectedKey !== undefined) { + return props.selectedKey; + } + + const optionsChecked = props.options!.filter((option: IChoiceGroupOption) => { + return option.checked; + }); + + if (optionsChecked.length === 0) { + return undefined; + } else { + return optionsChecked[0].key; + } + } +} diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.types.ts b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.types.ts index 1a7792757e09f3..b6555eba09455b 100644 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.types.ts +++ b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroup.types.ts @@ -1,7 +1,6 @@ import * as React from 'react'; import { IIconProps } from '../../Icon'; -import { IRenderFunction, IStyleFunction } from '../../Utilities'; -import { ITheme, IStyle } from '../../Styling'; +import { IRenderFunction } from '../../Utilities'; export interface IChoiceGroup { @@ -45,16 +44,6 @@ export interface IChoiceGroupProps extends React.InputHTMLAttributes) => void; - - /** - * Theme (provided through customization.) - */ - theme?: ITheme; - - /** - * Call to provide customized styling that will layer on top of the variant rules. - */ - getStyles?: IStyleFunction; } export interface IChoiceGroupOption extends React.HTMLAttributes { @@ -124,16 +113,3 @@ export interface IChoiceGroupOption extends React.HTMLAttributes(); - -@customizable('ChoiceGroupOption', ['theme']) -export class ChoiceGroupOptionBase extends BaseComponent { - private _inputElement = createRef(); - private _classNames: IClassNames; - - constructor(props: IChoiceGroupOptionProps) { - super(props); - } - - public render(): JSX.Element { - const { - focused, - required, - theme, - iconProps, - imageSrc, - imageSize = { width: 32, height: 32 }, - disabled, - checked, - id, - labelId, - getStyles, - name, - onRenderField = this._onRenderField, - } = this.props; - - this._classNames = getClassNames(getStyles!, { - theme: theme!, - hasIcon: !!iconProps, - hasImage: !!imageSrc, - checked, - disabled, - imageIsLarge: !!imageSrc && (imageSize.width > 71 || imageSize.height > 71), - focused - }); - - return ( -
-
- - { onRenderField(this.props, this._onRenderField) } -
-
- ); - } - - private _onChange(props: IChoiceGroupOptionProps, evt: React.FormEvent): void { - const { onChange } = props; - if (onChange) { - onChange(evt, props); - } - } - - private _onBlur(props: IChoiceGroupOptionProps, evt: React.FocusEvent) { - const { onBlur } = props; - if (onBlur) { - onBlur(evt, props); - } - } - - private _onFocus(props: IChoiceGroupOptionProps, evt: React.FocusEvent) { - const { onFocus } = props; - if (onFocus) { - onFocus(evt, props); - } - } - - private _onRenderField = (props: IChoiceGroupOptionProps): JSX.Element => { - const { - onRenderLabel = this._onRenderLabel, - id, - imageSrc, - imageAlt, - selectedImageSrc, - iconProps, - } = props; - - const imageSize = props.imageSize ? props.imageSize : { width: 32, height: 32 }; - - return ( - - ); - } - - private _onRenderLabel = (props: IChoiceGroupOptionProps): JSX.Element => { - return ( - - { props.text } - - ); - } -} diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.styles.ts b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.styles.ts deleted file mode 100644 index bfc05f1ce602e8..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.styles.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { FontSizes, FontWeights, HighContrastSelector, IStyle, IPalette, getGlobalClassNames } from '../../../Styling'; -import { - IChoiceGroupOptionStyleProps, - IChoiceGroupOptionStyles -} from './ChoiceGroupOption.types'; - -const GlobalClassNames = { - root: 'ms-ChoiceField', - choiceFieldWrapper: 'ms-ChoiceField-wrapper', - input: 'ms-ChoiceField-input', - field: 'ms-ChoiceField-field', - innerField: 'ms-ChoiceField-innerField', - imageWrapper: 'ms-ChoiceField-imageWrapper', - iconWrapper: 'ms-ChoiceField-iconWrapper', - labelWrapper: 'ms-ChoiceField-labelWrapper' -}; - -const labelWrapperLineHeight = 15; -const iconSize = 32; -const choiceFieldSize = 20; -const choiceFieldTransitionDuration = '200ms'; -const choiceFieldTransitionTiming = 'cubic-bezier(.4, 0, .23, 1)'; -const radioButtonSpacing = 3; -const radioButtonInnerSize = 5; - -function getChoiceGroupFocusStyle(palette: Partial, hasIconOrImage?: boolean): IStyle { - return [ - 'is-inFocus', - { - selectors: { - '.ms-Fabric.is-focusVisible &': { - position: 'relative', - outline: 'transparent', - selectors: { - '::-moz-focus-inner': { - border: 0 - }, - ':after': { - content: '""', - top: -2, - right: -2, - bottom: -2, - left: -2, - pointerEvents: 'none', - border: '1px solid ' + (hasIconOrImage ? palette.neutralSecondary : palette.neutralPrimary), - position: 'absolute', - selectors: { - [HighContrastSelector]: { - borderColor: 'WindowText', - borderWidth: hasIconOrImage ? 1 : 2 - } - } - } - } - } - }, - }, - ]; -} - -function getImageWrapperStyle(isSelectedImageWrapper: boolean, className?: string, checked?: boolean): IStyle { - return [ - className, - { - paddingBottom: 2, - transitionProperty: 'opacity', - transitionDuration: choiceFieldTransitionDuration, - transitionTimingFunction: 'ease', - selectors: { - '.ms-Image': { - display: 'inline-block', - borderStyle: 'none' - } - } - }, - (checked ? !isSelectedImageWrapper : isSelectedImageWrapper) && [ - 'is-hidden', - { - position: 'absolute', - left: 0, - top: 0, - width: '100%', - height: '100%', - overflow: 'hidden', - opacity: 0 - } - ] - ]; -} - -export const getStyles = (props: IChoiceGroupOptionStyleProps): IChoiceGroupOptionStyles => { - const { - theme, - hasIcon, - hasImage, - checked, - disabled, - imageIsLarge, - focused - } = props; - const { palette, semanticColors } = theme; - - const classNames = getGlobalClassNames(GlobalClassNames, theme); - - const fieldHoverOrFocusProperties = { - selectors: { - '.ms-Label': { - color: semanticColors.bodyTextChecked - }, - ':before': { - borderColor: checked ? semanticColors.inputBackgroundCheckedHovered : semanticColors.inputBorderHovered - } - } - }; - - const enabledFieldWithImageHoverOrFocusProperties = { - borderColor: checked ? palette.themeDark : palette.neutralTertiaryAlt, - selectors: { - ':before': { - opacity: 1, - borderColor: checked ? palette.themeDark : semanticColors.inputBorderHovered - } - } - }; - - const circleAreaProperties: IStyle = [ - { - content: '""', - display: 'inline-block', - backgroundColor: semanticColors.bodyBackground, - borderWidth: 1, - borderStyle: 'solid', - borderColor: semanticColors.smallInputBorder, - width: choiceFieldSize, - height: choiceFieldSize, - fontWeight: 'normal', - position: 'absolute', - top: 0, - left: 0, - boxSizing: 'border-box', - transitionProperty: 'border-color', - transitionDuration: choiceFieldTransitionDuration, - transitionTimingFunction: choiceFieldTransitionTiming, - borderRadius: '50%', - }, - disabled && { - backgroundColor: checked ? semanticColors.bodyBackground : semanticColors.disabledText, - borderColor: semanticColors.disabledText, - selectors: { - [HighContrastSelector]: { - color: 'GrayText' - } - } - }, - checked && { - borderWidth: 1, - borderStyle: 'solid', - borderColor: semanticColors.inputBackgroundChecked, - selectors: { - [HighContrastSelector]: { - borderColor: 'Highlight' - } - } - }, - (hasIcon || hasImage) && { - top: radioButtonSpacing, - right: radioButtonSpacing, - left: 'auto', // To reset the value of 'left' to its default value, so that 'right' works - opacity: !disabled && checked ? 1 : 0, - } - ]; - - const dotAreaProperties: IStyle = [ - { - content: '""', - width: 0, - height: 0, - borderRadius: '50%', - position: 'absolute', - left: choiceFieldSize / 2, - right: 0, - transitionProperty: 'border-width', - transitionDuration: choiceFieldTransitionDuration, - transitionTimingFunction: choiceFieldTransitionTiming, - boxSizing: 'border-box' - }, - checked && { - borderWidth: 5, - borderStyle: 'solid', - borderColor: semanticColors.inputBackgroundChecked, - left: 5, - top: 5, - width: 10, - height: 10, - selectors: { - [HighContrastSelector]: { - borderColor: 'Highlight' - } - } - }, - checked && (hasIcon || hasImage) && { - top: radioButtonSpacing + radioButtonInnerSize, - right: radioButtonSpacing + radioButtonInnerSize, - left: 'auto' // To reset the value of 'left' to its default value, so that 'right' works - } - ]; - - return { - root: [ - classNames.root, - { - display: 'flex', - alignItems: 'center', - boxSizing: 'border-box', - color: semanticColors.bodyText, - fontSize: FontSizes.medium, - fontWeight: FontWeights.regular, - minHeight: 26, - border: 'none', - position: 'relative', - marginTop: 8, - selectors: { - '.ms-Label': { - fontSize: FontSizes.medium, - padding: '0 0 0 26px', - display: 'inline-block' - } - } - }, - hasImage && 'ms-ChoiceField--image', - hasIcon && 'ms-ChoiceField--icon', - (hasIcon || hasImage) && { - display: 'inline-flex', - fontSize: 0, - margin: '0 4px 4px 0', - paddingLeft: 0, - backgroundColor: palette.neutralLighter, - height: '100%' - }, - ], - choiceFieldWrapper: [ - classNames.choiceFieldWrapper, - focused && getChoiceGroupFocusStyle(palette, hasIcon || hasImage) - ], - // The hidden input - input: [ - classNames.input, - { - position: 'absolute', - opacity: 0, - top: 8, - }, - (hasIcon || hasImage) && { - top: 0, - right: 0, - opacity: 0, - width: '100%', - height: '100%', - margin: 0 - } - ], - field: [ - classNames.field, - { - display: 'inline-block', - cursor: 'pointer', - marginTop: 0, - position: 'relative', - verticalAlign: 'top', - userSelect: 'none', - minHeight: 20, - selectors: { - ':hover': !disabled && fieldHoverOrFocusProperties, - ':focus': !disabled && fieldHoverOrFocusProperties, - - // The circle - ':before': circleAreaProperties, - - // The dot - ':after': dotAreaProperties - } - }, - hasIcon && 'ms-ChoiceField--icon', - hasImage && 'ms-ChoiceField-field--image', - (hasIcon || hasImage) && { - boxSizing: 'content-box', - cursor: 'pointer', - paddingTop: 22, - margin: 0, - textAlign: 'center', - transitionProperty: 'all', - transitionDuration: choiceFieldTransitionDuration, - transitionTimingFunction: 'ease', - border: '2px solid transparent', - justifyContent: 'center', - alignItems: 'center', - display: 'flex', - flexDirection: 'column', - }, - checked && { - borderColor: palette.themePrimary - }, - (hasIcon || hasImage) && !disabled && { - selectors: { - ':hover': enabledFieldWithImageHoverOrFocusProperties, - ':focus': enabledFieldWithImageHoverOrFocusProperties - } - }, - disabled && { - cursor: 'default', - selectors: { - '.ms-Label': { - color: semanticColors.disabledBodyText - }, - [HighContrastSelector]: { - color: 'GrayText' - } - } - } - ], - innerField: [ - classNames.innerField, - (hasIcon || hasImage) && { - position: 'relative', - display: 'inline-block', - paddingLeft: 30, - paddingRight: 30 - }, - (hasIcon || hasImage) && imageIsLarge && { - paddingLeft: 24, - paddingRight: 24 - }, - (hasIcon || hasImage) && disabled && { - opacity: 0.25, - selectors: { - [HighContrastSelector]: { - color: 'GrayText', - opacity: 1 - } - } - } - ], - imageWrapper: getImageWrapperStyle(false, classNames.imageWrapper, checked), - selectedImageWrapper: getImageWrapperStyle(true, classNames.imageWrapper, checked), - iconWrapper: [ - classNames.iconWrapper, - { - fontSize: iconSize, - lineHeight: iconSize, - height: iconSize - } - ], - labelWrapper: [ - classNames.labelWrapper, - (hasIcon || hasImage) && { - display: 'block', - position: 'relative', - margin: '4px 8px', - height: labelWrapperLineHeight * 2, - lineHeight: labelWrapperLineHeight, - overflow: 'hidden', - whiteSpace: 'pre-wrap', - textOverflow: 'ellipsis', - fontSize: FontSizes.medium, - fontWeight: FontWeights.regular, - selectors: { - '.ms-Label': { - padding: 0 - } - } - } - ] - }; -}; diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.test.tsx b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.test.tsx deleted file mode 100644 index 1acbf987951b74..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import * as React from 'react'; -/* tslint:enable:no-unused-variable */ -import * as renderer from 'react-test-renderer'; - -import { ChoiceGroupOption } from './ChoiceGroupOption'; - -describe('ChoiceGroupOption', () => { - - it('renders ChoiceGroup correctly', () => { - const component = renderer.create( -
- - - - - - - - - -
- ); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.tsx b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.tsx deleted file mode 100644 index fc23e19c51600d..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { styled } from '../../../Utilities'; -import { ChoiceGroupOptionBase } from './ChoiceGroupOption.base'; -import { IChoiceGroupOptionProps } from './ChoiceGroupOption.types'; -import { getStyles } from './ChoiceGroupOption.styles'; - -export const ChoiceGroupOption: (props: IChoiceGroupOptionProps) => JSX.Element = styled(ChoiceGroupOptionBase, getStyles); diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.types.ts b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.types.ts deleted file mode 100644 index d9616c1b101998..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/ChoiceGroupOption.types.ts +++ /dev/null @@ -1,148 +0,0 @@ -import * as React from 'react'; -import { IIconProps } from '../../../Icon'; -import { ITheme, IStyle } from '../../../Styling'; -import { IRenderFunction, IStyleFunction } from '../../../Utilities'; -import { IChoiceGroupOption } from '../../ChoiceGroup/ChoiceGroup.types'; - -export type OnFocusCallback = (ev?: React.FocusEvent, props?: IChoiceGroupOption) => void | undefined; -export type OnChangeCallback = (evt?: React.FormEvent, props?: IChoiceGroupOption) => void; - -export interface IChoiceGroupOptionProps extends IChoiceGroupOption { - - /** - * A required key to uniquely identify the option. - */ - key: string; - - /** - * Optional callback to access the IChoiceGroup interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: IChoiceGroupOption) => void; - - /** - * The text string for the option. - */ - text: string; - - /** - * Optional override of option render - */ - onRenderField?: IRenderFunction; - - /** - * Optional override of option render - */ - onRenderLabel?: (props: IChoiceGroupOption) => JSX.Element; - - /** - * A callback for receiving a notification when the choice has been changed. - */ - onChange?: OnChangeCallback; - - /** - * A callback for receiving a notification when the choice has received focus. - */ - onFocus?: OnFocusCallback; - - /** - * A callback for receiving a notification when the choice has lost focus. - */ - onBlur?: ( - ev: React.FocusEvent, - props?: IChoiceGroupOption - ) => void; - - /** - * The Icon component props for choice field - */ - iconProps?: IIconProps; - - /** - * The src of image for choice field. - */ - imageSrc?: string; - - /** - * The alt of image for choice field. Defaults to '' if not set. - */ - imageAlt?: string; - - /** - * The src of image for choice field which is selected. - */ - selectedImageSrc?: string; - - /** - * The width and height of the image in px for choice field. - * @default { width: 32, height: 32 } - */ - imageSize?: { width: number; height: number }; - - /** - * Whether or not the option is disabled. - */ - disabled?: boolean; - - /** - * This value is maintained by the component and is accessible during onRenderField - */ - checked?: boolean; - - /** - * Indicates if the ChoiceGroupOption should appear focused, visually - */ - focused?: boolean; - - /** - * This value is maintained by the component and is accessible during onRenderField - */ - id?: string; - - /** - * This value is maintained by the component and is accessible during onRenderField - */ - labelId?: string; - - /** - * Theme (provided through customization.) - */ - theme?: ITheme; - - /** - * Call to provide customized styling that will layer on top of the variant rules. - */ - getStyles?: IStyleFunction; - - /** - * If true, it specifies that an option must be selected in the ChoiceGroup before submitting the form - */ - required?: boolean; - - /** - * This value is used to group each ChoiceGroupOption into the same logical ChoiceGroup - */ - name?: string; -} - -export interface IChoiceGroupOptionStyleProps { - theme: ITheme; - hasIcon?: boolean; - hasImage?: boolean; - checked?: boolean; - disabled?: boolean; - imageIsLarge?: boolean; - focused?: boolean; -} - -export interface IChoiceGroupOptionStyles { - root?: IStyle; - choiceFieldWrapper?: IStyle; - input?: IStyle; - field?: IStyle; - innerField?: IStyle; - imageWrapper?: IStyle; - selectedImageWrapper?: IStyle; - iconWrapper?: IStyle; - labelWrapper?: IStyle; -} diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/__snapshots__/ChoiceGroupOption.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/__snapshots__/ChoiceGroupOption.test.tsx.snap deleted file mode 100644 index 36d4f73653b61e..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/__snapshots__/ChoiceGroupOption.test.tsx.snap +++ /dev/null @@ -1,1555 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ChoiceGroupOption renders ChoiceGroup correctly 1`] = ` -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-`; diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/index.ts b/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/index.ts deleted file mode 100644 index 17631a07411cb0..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/ChoiceGroupOption/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ChoiceGroupOption'; -export * from './ChoiceGroupOption.types'; diff --git a/packages/office-ui-fabric-react/src/components/ChoiceGroup/__snapshots__/ChoiceGroup.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ChoiceGroup/__snapshots__/ChoiceGroup.test.tsx.snap index c45811854f6f9b..9eaa246cf399c3 100644 --- a/packages/office-ui-fabric-react/src/components/ChoiceGroup/__snapshots__/ChoiceGroup.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/ChoiceGroup/__snapshots__/ChoiceGroup.test.tsx.snap @@ -2,63 +2,27 @@ exports[`ChoiceGroup renders ChoiceGroup correctly 1`] = `