From ccf54f5bd12ad61075bef146bd8d922cfb4c1be8 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Thu, 11 Oct 2018 16:45:57 -0700 Subject: [PATCH 01/12] [wip] updating input colors for dark theme --- .../teams-dark/components/Input/inputVariables.ts | 11 +++++++++++ src/themes/teams/components/Input/inputStyles.ts | 1 + src/themes/teams/components/Input/inputVariables.ts | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/themes/teams-dark/components/Input/inputVariables.ts diff --git a/src/themes/teams-dark/components/Input/inputVariables.ts b/src/themes/teams-dark/components/Input/inputVariables.ts new file mode 100644 index 0000000000..2aa5b68ccd --- /dev/null +++ b/src/themes/teams-dark/components/Input/inputVariables.ts @@ -0,0 +1,11 @@ +export default (siteVars: any) => { + const vars: any = {} + + vars.backgroundColor = siteVars.gray10 + + vars.fontColor = siteVars.gray02 + vars.inputFocusBorderColor = siteVars.brand + vars.iconCOlor = siteVars.gray02 + + return vars +} diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index f37ad16842..257a05a939 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -38,6 +38,7 @@ const inputStyles: IComponentPartStylesInput = { return { position: variables.iconPosition, right: variables.iconRight, + color: variables.iconColor, outline: 0, } }, diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 060bd6b50d..19c4eb6d47 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -7,7 +7,7 @@ export default (siteVars: any) => { vars.borderBottom = `${pxToRem(2)} solid transparent` vars.backgroundColor = siteVars.gray10 - vars.fontColor = siteVars.bodyColor + vars.fontColor = siteVars.gray02 vars.fontSize = siteVars.fontSizes.medium vars.inputPadding = `${pxToRem(6)} ${pxToRem(24)} ${pxToRem(6)} ${pxToRem(12)}` @@ -16,6 +16,7 @@ export default (siteVars: any) => { vars.iconPosition = 'absolute' vars.iconRight = `${pxToRem(2)}` + vars.iconColor = siteVars.gray02 return vars } From 65e59e48e59e9dce846ecc0124849cd53bedb8cf Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Wed, 17 Oct 2018 16:38:17 -0700 Subject: [PATCH 02/12] updating Placeholder & other colors --- src/themes/teams-dark/componentVariables.ts | 3 ++- .../components/Input/inputVariables.ts | 16 +++++++--------- .../teams-high-contrast/componentVariables.ts | 3 ++- .../components/Input/inputVariables.ts | 11 +++++++++++ src/themes/teams/components/Input/inputStyles.ts | 5 ++++- .../teams/components/Input/inputVariables.ts | 1 + 6 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 src/themes/teams-high-contrast/components/Input/inputVariables.ts diff --git a/src/themes/teams-dark/componentVariables.ts b/src/themes/teams-dark/componentVariables.ts index 34c15933b6..88956dba25 100644 --- a/src/themes/teams-dark/componentVariables.ts +++ b/src/themes/teams-dark/componentVariables.ts @@ -1,3 +1,4 @@ export { default as Button } from './components/Button/buttonVariables' -export { default as Text } from './components/Text/textVariables' export { default as Divider } from './components/Divider/dividerVariables' +export { default as Input } from './components/Input/inputVariables' +export { default as Text } from './components/Text/textVariables' diff --git a/src/themes/teams-dark/components/Input/inputVariables.ts b/src/themes/teams-dark/components/Input/inputVariables.ts index 2aa5b68ccd..284536cf61 100644 --- a/src/themes/teams-dark/components/Input/inputVariables.ts +++ b/src/themes/teams-dark/components/Input/inputVariables.ts @@ -1,11 +1,9 @@ export default (siteVars: any) => { - const vars: any = {} - - vars.backgroundColor = siteVars.gray10 - - vars.fontColor = siteVars.gray02 - vars.inputFocusBorderColor = siteVars.brand - vars.iconCOlor = siteVars.gray02 - - return vars + return { + backgroundColor: siteVars.gray10, + borderStyle: '0', + fontColor: siteVars.gray02, + inputFocusBorderColor: siteVars.brand, + iconColor: siteVars.gray02, + } } diff --git a/src/themes/teams-high-contrast/componentVariables.ts b/src/themes/teams-high-contrast/componentVariables.ts index f975eed9e2..956e3451f0 100644 --- a/src/themes/teams-high-contrast/componentVariables.ts +++ b/src/themes/teams-high-contrast/componentVariables.ts @@ -1,2 +1,3 @@ -export { default as Text } from './components/Text/textVariables' export { default as Divider } from './components/Divider/dividerVariables' +export { default as Input } from './components/Input/inputVariables' +export { default as Text } from './components/Text/textVariables' diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts new file mode 100644 index 0000000000..f7b71d9043 --- /dev/null +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -0,0 +1,11 @@ +import { pxToRem } from '../../../../lib' + +export default (siteVars: any) => { + return { + backgroundColor: siteVars.bodyBackground, + borderStyle: `${pxToRem(1)} solid ${siteVars.bodyColor}`, + fontColor: siteVars.bodyColor, + inputFocusBorderColor: siteVars.accessibleYellow, + iconColor: siteVars.bodyColor, + } +} diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 257a05a939..149cdeff05 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -19,7 +19,7 @@ const inputStyles: IComponentPartStylesInput = { return { outline: 0, - border: 0, + border: variables.borderStyle, borderRadius: variables.borderRadius, borderBottom: variables.borderBottom, color: variables.fontColor, @@ -27,6 +27,9 @@ const inputStyles: IComponentPartStylesInput = { padding: variables.inputPadding, ...(fluid && { width: '100%' }), ...(inline && { float: 'left' }), + '::placeholder': { + color: variables.fontColor, + }, ':focus': { borderColor: variables.inputFocusBorderColor, borderRadius: variables.inputFocusBorderRadius, diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 19c4eb6d47..6c0bd58e1b 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -4,6 +4,7 @@ export default (siteVars: any) => { const vars: any = {} vars.borderRadius = `${pxToRem(3)}` + vars.borderStyle = 0 vars.borderBottom = `${pxToRem(2)} solid transparent` vars.backgroundColor = siteVars.gray10 From 9088f2c0c412222fdd1fe0d881beaf74c07ab73a Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Thu, 18 Oct 2018 16:53:06 -0700 Subject: [PATCH 03/12] updating colors --- .../teams-high-contrast/components/Input/inputVariables.ts | 4 ++-- src/themes/teams/components/Input/inputStyles.ts | 4 ++-- src/themes/teams/components/Input/inputVariables.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts index f7b71d9043..7bc18d55b6 100644 --- a/src/themes/teams-high-contrast/components/Input/inputVariables.ts +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -3,9 +3,9 @@ import { pxToRem } from '../../../../lib' export default (siteVars: any) => { return { backgroundColor: siteVars.bodyBackground, - borderStyle: `${pxToRem(1)} solid ${siteVars.bodyColor}`, - fontColor: siteVars.bodyColor, + border: `${pxToRem(1)} solid ${siteVars.bodyColor}`, inputFocusBorderColor: siteVars.accessibleYellow, + fontColor: siteVars.bodyColor, iconColor: siteVars.bodyColor, } } diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 149cdeff05..b7edae2913 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -19,7 +19,7 @@ const inputStyles: IComponentPartStylesInput = { return { outline: 0, - border: variables.borderStyle, + border: variables.border, borderRadius: variables.borderRadius, borderBottom: variables.borderBottom, color: variables.fontColor, @@ -31,7 +31,7 @@ const inputStyles: IComponentPartStylesInput = { color: variables.fontColor, }, ':focus': { - borderColor: variables.inputFocusBorderColor, + borderBottomColor: variables.inputFocusBorderColor, borderRadius: variables.inputFocusBorderRadius, }, } diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 6c0bd58e1b..21527316f7 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -3,8 +3,8 @@ import { pxToRem } from '../../../../lib' export default (siteVars: any) => { const vars: any = {} + vars.border = '0' vars.borderRadius = `${pxToRem(3)}` - vars.borderStyle = 0 vars.borderBottom = `${pxToRem(2)} solid transparent` vars.backgroundColor = siteVars.gray10 From a3fdefc7558f59300be74988fb252305ffc8f8eb Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Mon, 22 Oct 2018 15:35:37 -0700 Subject: [PATCH 04/12] border bottom --- .../teams-high-contrast/components/Input/inputVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts index 7bc18d55b6..f3b2d63a59 100644 --- a/src/themes/teams-high-contrast/components/Input/inputVariables.ts +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -4,7 +4,7 @@ export default (siteVars: any) => { return { backgroundColor: siteVars.bodyBackground, border: `${pxToRem(1)} solid ${siteVars.bodyColor}`, - inputFocusBorderColor: siteVars.accessibleYellow, + borderBottom: `${pxToRem(2)} solid ${siteVars.accessibleYellow}`, fontColor: siteVars.bodyColor, iconColor: siteVars.bodyColor, } From 0498889f2c4ca803351f2822241c417951e8f466 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Mon, 22 Oct 2018 16:59:20 -0700 Subject: [PATCH 05/12] Updating colors --- .../teams-dark/components/Input/inputVariables.ts | 1 - .../components/Input/inputVariables.ts | 3 ++- src/themes/teams/components/Input/inputStyles.ts | 13 ++++++++++++- src/themes/teams/components/Input/inputVariables.ts | 12 +++++++----- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/themes/teams-dark/components/Input/inputVariables.ts b/src/themes/teams-dark/components/Input/inputVariables.ts index 284536cf61..6882243c45 100644 --- a/src/themes/teams-dark/components/Input/inputVariables.ts +++ b/src/themes/teams-dark/components/Input/inputVariables.ts @@ -1,7 +1,6 @@ export default (siteVars: any) => { return { backgroundColor: siteVars.gray10, - borderStyle: '0', fontColor: siteVars.gray02, inputFocusBorderColor: siteVars.brand, iconColor: siteVars.gray02, diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts index f3b2d63a59..b5f954165c 100644 --- a/src/themes/teams-high-contrast/components/Input/inputVariables.ts +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -4,8 +4,9 @@ export default (siteVars: any) => { return { backgroundColor: siteVars.bodyBackground, border: `${pxToRem(1)} solid ${siteVars.bodyColor}`, - borderBottom: `${pxToRem(2)} solid ${siteVars.accessibleYellow}`, + borderBottomColor: siteVars.accessibleYellow, fontColor: siteVars.bodyColor, iconColor: siteVars.bodyColor, + inputFocusBorderColor: siteVars.accessibleYellow, } } diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index b99ea1a744..1175042fe4 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -2,6 +2,7 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { InputProps } from '../../../../components/Input/Input' import { InputVariables } from './inputVariables' import { PositionProperty } from 'csstype' +import { pxToRem } from '../../../../lib' const inputStyles: ComponentSlotStylesInput = { root: ({ props: p }): ICSSInJSStyle => ({ @@ -14,10 +15,12 @@ const inputStyles: ComponentSlotStylesInput = { input: ({ props: p, variables: v }): ICSSInJSStyle => ({ outline: 0, + border: v.border, borderRadius: v.borderRadius, borderBottom: v.borderBottom, color: v.fontColor, backgroundColor: v.backgroundColor, + position: 'relative', padding: v.inputPadding, ...(p.fluid && { width: '100%' }), ...(p.inline && { float: 'left' }), @@ -25,8 +28,16 @@ const inputStyles: ComponentSlotStylesInput = { color: v.fontColor, }, ':focus': { - borderColor: v.inputFocusBorderColor, + borderBottomColor: v.inputFocusBorderColor, + }, + '&::after': { borderRadius: v.inputFocusBorderRadius, + borderBottom: `${pxToRem(2)} solid transparent`, + content: '', + position: 'absolute', + bottom: '0', + left: '0', + right: '0', }, }), diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 6c9e9aa900..a52c8ffe53 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -1,13 +1,14 @@ import { pxToRem } from '../../../../lib' export interface InputVariables { - borderRadius: string - borderBottom: string backgroundColor: string + border: string + borderBottom: string + borderRadius: string fontColor: string fontSize: string + iconColor: string iconPosition: string iconRight: string - iconColor: string inputPadding: string inputFocusBorderColor: string inputFocusBorderRadius: string @@ -18,9 +19,10 @@ const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [2, 3, 6, ) export default (siteVars): InputVariables => ({ - borderRadius: _3px_asRem, - borderBottom: `${_2px_asRem} solid transparent`, backgroundColor: siteVars.gray10, + border: '0', + borderBottom: `${_2px_asRem} solid transparent`, + borderRadius: _3px_asRem, fontColor: siteVars.bodyColor, fontSize: siteVars.fontSizes.medium, From 7d29e0ee4e42c3acb624fb49f5d95a2f7c9fdf47 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Wed, 24 Oct 2018 15:38:51 -0700 Subject: [PATCH 06/12] merge conflict --- CHANGELOG.md | 14 + .../components/ComponentDoc/ComponentDoc.tsx | 20 +- .../ComponentDoc/ComponentDocLinks.tsx | 5 +- .../ComponentExample/ComponentExample.tsx | 12 +- .../ComponentDoc/ContributionPrompt.tsx | 6 +- docs/src/components/Sidebar/Sidebar.tsx | 305 ------------------ .../States/IconExampleDisabled.shorthand.tsx | 3 - .../components/Icon/Variations/index.tsx | 5 + .../Portal/Types/PortalExample.shorthand.tsx | 6 +- .../components/Portal/Types/PortalExample.tsx | 6 +- .../PortalExampleControlled.shorthand.tsx | 6 +- .../Portal/Types/PortalExampleControlled.tsx | 6 +- .../components/Portal/Types/index.tsx | 5 + docs/src/utils/index.tsx | 1 - src/components/Accordion/Accordion.tsx | 4 +- src/components/Avatar/Avatar.tsx | 1 - src/components/Button/Button.tsx | 2 +- src/components/Chat/Chat.tsx | 3 + src/components/Divider/Divider.tsx | 3 +- src/components/Grid/Grid.tsx | 2 +- src/components/Header/Header.tsx | 3 +- src/components/Header/HeaderDescription.tsx | 2 +- src/components/Icon/Icon.tsx | 3 + src/components/Input/Input.tsx | 22 +- src/components/ItemLayout/ItemLayout.tsx | 3 + src/components/Portal/Portal.tsx | 15 +- src/components/Provider/Provider.tsx | 2 +- src/components/RadioGroup/RadioGroup.tsx | 3 + src/components/Text/Text.tsx | 2 +- src/index.ts | 5 + .../Behaviors/Popup/popupBehavior.ts | 2 - src/lib/accessibility/FocusZone/CHANGELOG.md | 19 +- src/lib/accessibility/FocusZone/FocusZone.tsx | 30 +- .../FocusZone/FocusZone.types.ts | 6 - .../accessibility/FocusZone/focusUtilities.ts | 44 +++ src/lib/accessibility/FocusZone/index.ts | 2 + src/lib/accessibility/types.ts | 7 +- src/lib/eventStack/EventTarget.tsx | 16 +- src/lib/eventStack/eventStack.tsx | 8 +- src/lib/index.ts | 1 + src/lib/renderComponent.tsx | 6 + .../teams/components/Icon/iconStyles.ts | 45 ++- .../teams/components/Icon/iconVariables.ts | 8 +- .../teams/components/Icon/svg/icons/index.ts | 10 + .../commonTests/handlesAccessibility.tsx | 22 +- .../htmlIsAccessibilityCompliant.ts | 11 +- .../implementsCollectionShorthandProp.tsx | 2 +- .../commonTests/implementsShorthandProp.tsx | 8 +- .../commonTests/implementsWrapperProp.tsx | 3 +- test/specs/commonTests/isConformant.tsx | 19 +- .../specs/commonTests/stylesFunction-test.tsx | 4 +- test/specs/components/Button/Button-test.tsx | 32 +- .../components/Chat/ChatMessage-test.tsx | 2 +- test/specs/components/Form/FormField-test.tsx | 2 +- test/specs/components/Icon/Icon-test.tsx | 6 +- test/specs/components/Image/Image-test.tsx | 6 +- test/specs/components/Input/Input-test.tsx | 3 +- test/specs/components/Label/Label-test.tsx | 2 +- test/specs/components/Menu/Menu-test.tsx | 13 +- test/specs/components/Menu/MenuItem-test.tsx | 40 +-- test/specs/components/Portal/Portal-test.tsx | 97 +++--- test/specs/components/Slot/Slot-test.ts | 2 +- test/utils/index.ts | 2 +- 63 files changed, 387 insertions(+), 568 deletions(-) delete mode 100644 docs/src/components/Sidebar/Sidebar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 762127b7f3..7c294031b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Fix build on Windows @jurokapsiar ([#383](https://github.com/stardust-ui/react/pull/383)) +- Add warning for rendering components outside provider @Bugaa92 ([#378](https://github.com/stardust-ui/react/pull/378)) +- Fix icon colors for Teams theme @codepretty ([#384](https://github.com/stardust-ui/react/pull/384)) + +### Features +- Export `mergeThemes` @levithomason ([#285](https://github.com/stardust-ui/react/pull/285)) +- Add Focus Trap Zone @sophieH29 ([#239](https://github.com/stardust-ui/react/pull/239)) +- Add compose icons to Teams theme @joheredi ([#396](https://github.com/stardust-ui/react/pull/396)) +- Expose access to input element of `Input` via `inputRef` prop @silviuavram ([#377](https://github.com/stardust-ui/react/pull/377)) + +### Documentation +- Add `Provider` examples @levithomason ([#285](https://github.com/stardust-ui/react/pull/285)) + +### Documentation +- Add component descriptions and fix accessibility errors @levithomason ([#387](https://github.com/stardust-ui/react/pull/387)) ## [v0.10.0](https://github.com/stardust-ui/react/tree/v0.10.0) (2018-10-19) diff --git a/docs/src/components/ComponentDoc/ComponentDoc.tsx b/docs/src/components/ComponentDoc/ComponentDoc.tsx index ad423068fe..bde132c4b9 100644 --- a/docs/src/components/ComponentDoc/ComponentDoc.tsx +++ b/docs/src/components/ComponentDoc/ComponentDoc.tsx @@ -3,17 +3,17 @@ import PropTypes from 'prop-types' import * as React from 'react' import DocumentTitle from 'react-document-title' import { withRouter } from 'react-router' -import { Grid, Header, Icon } from 'semantic-ui-react' +import { Grid, Icon } from 'semantic-ui-react' +import { Header } from '@stardust-ui/react' import componentInfoShape from 'docs/src/utils/componentInfoShape' import { scrollToAnchor, examplePathToHash, getFormattedHash } from 'docs/src/utils' -import { accessibilityErrorMessage } from 'docs/src/constants' import ComponentDocLinks from './ComponentDocLinks' import ComponentDocSee from './ComponentDocSee' import ComponentExamples from './ComponentExamples' import ComponentProps from './ComponentProps' import ComponentSidebar from './ComponentSidebar' -import ComponentDocTag from './ComponentDocTag' +import ComponentAccessibility from './ComponentDocAccessibility' const topRowStyle = { margin: '1em' } const exampleEndStyle: React.CSSProperties = { @@ -80,17 +80,9 @@ class ComponentDoc extends React.Component { -
- +
+

{_.join(info.docblock.description, ' ')}

+ { content={ diff --git a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx index ee1a26c049..3b93d4c2df 100644 --- a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx +++ b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx @@ -8,15 +8,9 @@ import * as copyToClipboard from 'copy-to-clipboard' import { Divider, Form, Grid, Menu, Segment, Visibility } from 'semantic-ui-react' import { Provider, themes } from '@stardust-ui/react' -import { - examplePathToHash, - getFormattedHash, - knobsContext, - repoURL, - scrollToAnchor, -} from 'docs/src/utils' +import { examplePathToHash, getFormattedHash, knobsContext, scrollToAnchor } from 'docs/src/utils' import evalTypeScript from 'docs/src/utils/evalTypeScript' -import { callable, doesNodeContainClick, pxToRem } from 'src/lib' +import { callable, doesNodeContainClick, pxToRem, constants } from 'src/lib' import Editor, { EDITOR_BACKGROUND_COLOR, EDITOR_GUTTER_COLOR } from 'docs/src/components/Editor' import ComponentControls from '../ComponentControls' import ComponentExampleTitle from './ComponentExampleTitle' @@ -437,7 +431,7 @@ class ComponentExample extends React.Component ( {children &&
{children}
}

If there's no{' '} - + pull request {' '} open for this, you should{' '} - + contribute {' '} one! diff --git a/docs/src/components/Sidebar/Sidebar.tsx b/docs/src/components/Sidebar/Sidebar.tsx deleted file mode 100644 index e6858a640f..0000000000 --- a/docs/src/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import keyboardKey from 'keyboard-key' -import * as _ from 'lodash/fp' -import PropTypes from 'prop-types' -import * as React from 'react' -import { findDOMNode } from 'react-dom' -import { NavLink } from 'react-router-dom' -import { withRouter } from 'react-router' -import { Icon, Input as SemanticUIInput, Menu } from 'semantic-ui-react' - -import Logo from 'docs/src/components/Logo/Logo' -import { getComponentPathname, repoURL, typeOrder } from 'docs/src/utils' -import { themes } from '@stardust-ui/react' -import { ThemeContext } from '../../context/theme-context' - -const pkg = require('../../../../package.json') -const componentMenu = require('docs/src/componentMenu') -const behaviorMenu = require('docs/src/behaviorMenu') - -const selectedItemLabelStyle: any = { color: '#35bdb2', float: 'right' } -const selectedItemLabel = Press Enter -type ComponentMenuItem = { displayName: string; type: string } - -class Sidebar extends React.Component { - static propTypes = { - match: PropTypes.object.isRequired, - location: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - style: PropTypes.object, - } - state: any = { query: '' } - _searchInput: any - selectedRoute: any - filteredMenu = componentMenu - - componentDidMount() { - document.addEventListener('keydown', this.handleDocumentKeyDown) - this.setSearchInput() - } - - componentDidUpdate() { - this.setSearchInput() - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleDocumentKeyDown) - } - - setSearchInput() { - // TODO: Replace findDOMNode with Ref component when it will be merged - this._searchInput = (findDOMNode(this) as any).querySelector('.ui.input input') - } - - handleDocumentKeyDown = e => { - const code = keyboardKey.getCode(e) - const isAZ = code >= 65 && code <= 90 - const hasModifier = e.altKey || e.ctrlKey || e.metaKey - const bodyHasFocus = document.activeElement === document.body - - if (!hasModifier && isAZ && bodyHasFocus) this._searchInput.focus() - } - - handleItemClick = () => { - const { query } = this.state - - if (query) this.setState({ query: '' }) - if (document.activeElement === this._searchInput) this._searchInput.blur() - } - - handleSearchChange = e => - this.setState({ - selectedItemIndex: 0, - query: e.target.value, - }) - - handleSearchKeyDown = e => { - const { history } = this.props - const { selectedItemIndex } = this.state - const code = keyboardKey.getCode(e) - - if (code === keyboardKey.Enter && this.selectedRoute) { - e.preventDefault() - history.push(this.selectedRoute) - this.selectedRoute = null - this._searchInput.blur() - this.setState({ query: '' }) - } - - if (code === keyboardKey.ArrowDown) { - e.preventDefault() - const next = _.min([selectedItemIndex + 1, this.filteredMenu.length - 1]) - this.selectedRoute = getComponentPathname(this.filteredMenu[next]) - this.setState({ selectedItemIndex: next }) - } - - if (code === keyboardKey.ArrowUp) { - e.preventDefault() - const next = _.max([selectedItemIndex - 1, 0]) - this.selectedRoute = getComponentPathname(this.filteredMenu[next]) - this.setState({ selectedItemIndex: next }) - } - } - - menuItemsByType = _.map(nextType => { - const items = _.flow( - _.filter(({ type }) => type === nextType), - _.map(info => ( - - )), - )([...componentMenu, ...behaviorMenu]) - - return ( - - {_.capitalize(nextType)}s - {items} - - ) - }, typeOrder) - - renderSearchItems = () => { - const { selectedItemIndex, query } = this.state - if (!query) return undefined - - let itemIndex = -1 - const startsWithMatches: ComponentMenuItem[] = [] - const containsMatches: ComponentMenuItem[] = [] - const escapedQuery = _.escapeRegExp(query) - - _.each(info => { - if (new RegExp(`^${escapedQuery}`, 'i').test(info.displayName)) { - startsWithMatches.push(info) - } else if (new RegExp(escapedQuery, 'i').test(info.displayName)) { - containsMatches.push(info) - } - }, componentMenu) - - this.filteredMenu = [...startsWithMatches, ...containsMatches] - const menuItems = _.map(info => { - itemIndex += 1 - const isSelected = itemIndex === selectedItemIndex - - if (isSelected) this.selectedRoute = getComponentPathname(info) - - return ( - - {info.displayName} - {isSelected && selectedItemLabel} - - ) - }, this.filteredMenu) - - return {menuItems} - } - - render() { - const { style } = this.props - const { query } = this.state - return ( - - {({ themeName, changeTheme }) => ( -

- - - - Stardust UI React   - - {pkg.version} - - - - - GitHub - - - CHANGELOG - - - - {process.env.NODE_ENV !== 'production' && ( - -

Theme:

- -
- )} - - Concepts - - - Introduction - - - Shorthand Props - - - - - Guides - - - Quick Start - - - Glossary - - - Accessibility - - - Theming - - - Theming Examples - - - - {process.env.NODE_ENV !== 'production' && ( - - Prototypes - - - Chat Pane - - - Async Shorthand - - - Employee Card - - - Meeting Options - - - - )} - - - - {query ? this.renderSearchItems() : this.menuItemsByType} -
- )} - - ) - } - - private getThemeOptions = () => { - return Object.keys(themes).map(key => ({ - text: _.startCase(key), - value: key, - })) - } -} - -export default withRouter(Sidebar) diff --git a/docs/src/examples/components/Icon/States/IconExampleDisabled.shorthand.tsx b/docs/src/examples/components/Icon/States/IconExampleDisabled.shorthand.tsx index 587fee7e9e..395d07095f 100644 --- a/docs/src/examples/components/Icon/States/IconExampleDisabled.shorthand.tsx +++ b/docs/src/examples/components/Icon/States/IconExampleDisabled.shorthand.tsx @@ -4,9 +4,6 @@ import { Icon } from '@stardust-ui/react' const IconExampleDisabled = () => (
- - -
) diff --git a/docs/src/examples/components/Icon/Variations/index.tsx b/docs/src/examples/components/Icon/Variations/index.tsx index 0cb86f7721..1f80f5d24f 100644 --- a/docs/src/examples/components/Icon/Variations/index.tsx +++ b/docs/src/examples/components/Icon/Variations/index.tsx @@ -9,6 +9,11 @@ const Variations = () => ( description="An icon can have space before, after or on both sides. 'none' value removes the default space around the icon." examplePath="components/Icon/Variations/IconExampleSpace" /> +
This is a basic portal
diff --git a/docs/src/examples/components/Portal/Types/PortalExample.tsx b/docs/src/examples/components/Portal/Types/PortalExample.tsx index 116cd883fa..9d3ef90b1e 100644 --- a/docs/src/examples/components/Portal/Types/PortalExample.tsx +++ b/docs/src/examples/components/Portal/Types/PortalExample.tsx @@ -20,11 +20,15 @@ class PortalExamplePortal extends React.Component { }>
This is a basic portal
diff --git a/docs/src/examples/components/Portal/Types/PortalExampleControlled.shorthand.tsx b/docs/src/examples/components/Portal/Types/PortalExampleControlled.shorthand.tsx index e3ba1e8f86..97aa049f27 100644 --- a/docs/src/examples/components/Portal/Types/PortalExampleControlled.shorthand.tsx +++ b/docs/src/examples/components/Portal/Types/PortalExampleControlled.shorthand.tsx @@ -37,11 +37,15 @@ class PortalExampleControlled extends React.Component { content={
This is a controlled portal
diff --git a/docs/src/examples/components/Portal/Types/PortalExampleControlled.tsx b/docs/src/examples/components/Portal/Types/PortalExampleControlled.tsx index 19ee275558..e9b16d66cf 100644 --- a/docs/src/examples/components/Portal/Types/PortalExampleControlled.tsx +++ b/docs/src/examples/components/Portal/Types/PortalExampleControlled.tsx @@ -35,11 +35,15 @@ class PortalExampleControlled extends React.Component {
This is a controlled portal
diff --git a/docs/src/examples/components/Portal/Types/index.tsx b/docs/src/examples/components/Portal/Types/index.tsx index f13c543c83..943e6e8f73 100644 --- a/docs/src/examples/components/Portal/Types/index.tsx +++ b/docs/src/examples/components/Portal/Types/index.tsx @@ -15,6 +15,11 @@ const PortalTypesExamples = () => ( description="A controlled portal." examplePath="components/Portal/Types/PortalExampleControlled" /> + ) diff --git a/docs/src/utils/index.tsx b/docs/src/utils/index.tsx index 836502454d..5e8a3dcf88 100644 --- a/docs/src/utils/index.tsx +++ b/docs/src/utils/index.tsx @@ -1,4 +1,3 @@ -export * from './constants' export { default as componentInfoContext } from './componentInfoContext' export { default as componentInfoShape } from './componentInfoShape' export { default as exampleContext } from './exampleContext' diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index 0216c5d219..1b5a472329 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -36,9 +36,7 @@ export interface AccordionProps { } /** - * A standard Accordion. - * @accessibility - * Concern: how do we optimally navigate through an Accordion element with nested children? + * An accordion allows users to toggle the display of sections of content. */ class Accordion extends AutoControlledComponent, any> { static displayName = 'Accordion' diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 0c7b5b578d..e9107f4c4d 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -24,7 +24,6 @@ export interface AvatarProps { /** * An avatar is a graphic representation of user. - * @accessibility To be discussed */ class Avatar extends UIComponent, any> { static create: Function diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 563c51ea11..68aa964e0b 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -44,7 +44,7 @@ export interface ButtonState { } /** - * A button. + * A button indicates a possible user action. * @accessibility * Other considerations: * - for disabled buttons, add 'disabled' attribute so that the state is properly recognized by the screen reader diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 848752ff09..5cb3cdc6ee 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -26,6 +26,9 @@ export interface ChatProps { variables?: ComponentVariablesInput } +/** + * A Chat displays messages between users. + */ class Chat extends UIComponent, any> { static className = 'ui-chat' diff --git a/src/components/Divider/Divider.tsx b/src/components/Divider/Divider.tsx index c1e5db178c..b8a4f90c46 100644 --- a/src/components/Divider/Divider.tsx +++ b/src/components/Divider/Divider.tsx @@ -19,8 +19,7 @@ export interface DividerProps { } /** - * @accessibility - * This is shown at the top. + * A divider visually segments content into groups. */ class Divider extends UIComponent, any> { static displayName = 'Divider' diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx index 3d4249395b..be587abee7 100644 --- a/src/components/Grid/Grid.tsx +++ b/src/components/Grid/Grid.tsx @@ -17,7 +17,7 @@ export interface GridProps { } /** - * A grid. + * A grid is used to harmonize negative space in a layout. * @accessibility This is example usage of the accessibility tag. * This should be replaced with the actual description after the PR is merged */ diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 2a81746ee4..65e4ecd193 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -24,12 +24,11 @@ export interface HeaderProps { } /** - * A header provides a short summary of content + * A header provides a short summary of content. * @accessibility * Headings communicate the organization of the content on the page. Web browsers, plug-ins, and assistive technologies can use them to provide in-page navigation. * Nest headings by their rank (or level). The most important heading has the rank 1 (

), the least important heading rank 6 (

). Headings with an equal or higher rank start a new section, headings with a lower rank start new subsections that are part of the higher ranked section. * - * * Other considerations: * - when the description property is used in header, readers will narrate both header content and description within the element. * In addition to that, both will be displayed in the list of headings. diff --git a/src/components/Header/HeaderDescription.tsx b/src/components/Header/HeaderDescription.tsx index dd9f57444a..e76ad8a30c 100644 --- a/src/components/Header/HeaderDescription.tsx +++ b/src/components/Header/HeaderDescription.tsx @@ -15,7 +15,7 @@ export interface HeaderDescriptionProps { } /** - * Headers may contain description. + * A header's description provides more detailed information. */ class HeaderDescription extends UIComponent, any> { static create: Function diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index f7415ccb00..83c101ad2e 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -33,6 +33,9 @@ export interface IconProps { variables?: ComponentVariablesInput } +/** + * An icon is a glyph used to represent something else. + */ class Icon extends UIComponent, any> { static create: Function diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index de4455f6f6..ae9d8a77df 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -37,6 +37,7 @@ export interface InputProps { renderWrapper?: ShorthandRenderFunction styles?: ComponentSlotStyle type?: string + inputRef?: (node: HTMLElement) => void value?: React.ReactText variables?: ComponentVariablesInput wrapper?: ShorthandValue @@ -47,7 +48,7 @@ export interface InputState { } /** - * An Input + * An input is a field used to elicit a response from a user. * @accessibility * For good screen reader experience set aria-label or aria-labelledby attribute for input. * @@ -89,6 +90,13 @@ class Input extends AutoControlledComponent, InputState> /** Shorthand for the input component. */ input: customPropTypes.itemShorthand, + /** + * Ref callback with an input DOM node. + * + * @param {JSX.Element} node - input DOM node. + */ + inputRef: PropTypes.func, + /** An input can be used inline with text. */ inline: PropTypes.bool, @@ -168,11 +176,7 @@ class Input extends AutoControlledComponent, InputState> className: cx(Input.className, className), children: ( <> - - (this.inputDomElement = inputDomElement as HTMLInputElement) - } - > + {Slot.createHTMLInput(input || type, { defaultProps: { ...htmlInputProps, @@ -201,6 +205,12 @@ class Input extends AutoControlledComponent, InputState> }) } + private handleInputRef = (inputNode: HTMLElement) => { + this.inputDomElement = inputNode as HTMLInputElement + + _.invoke(this.props, 'inputRef', inputNode) + } + private handleIconOverrides = predefinedProps => ({ onClick: (e: React.SyntheticEvent) => { this.handleOnClear() diff --git a/src/components/ItemLayout/ItemLayout.tsx b/src/components/ItemLayout/ItemLayout.tsx index fbb5008aae..d6f00475c6 100644 --- a/src/components/ItemLayout/ItemLayout.tsx +++ b/src/components/ItemLayout/ItemLayout.tsx @@ -50,6 +50,9 @@ export interface ItemLayoutProps { variables?: ComponentVariablesInput } +/** + * The Item Layout handles layout styles for menu items, list items and other similar item templates. + */ class ItemLayout extends UIComponent, any> { static create: Function diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx index f988f33335..57d82b058c 100644 --- a/src/components/Portal/Portal.tsx +++ b/src/components/Portal/Portal.tsx @@ -12,6 +12,7 @@ import { import { ShorthandValue, ReactChildren } from '../../../types/utils' import Ref from '../Ref/Ref' import PortalInner from './PortalInner' +import { FocusTrapZone, FocusTrapZoneProps } from '../../lib/accessibility/FocusZone' import { AccessibilityAttributes, OnKeyDownHandler } from '../../lib/accessibility/types' type ReactMouseEvent = React.MouseEvent @@ -28,6 +29,7 @@ export interface PortalProps { onUnmount?: (props: PortalProps) => void open?: boolean trigger?: JSX.Element + trapFocus?: FocusTrapZoneProps | boolean triggerAccessibility?: TriggerAccessibility triggerRef?: (node: HTMLElement) => void onTriggerClick?: (e: ReactMouseEvent) => void @@ -103,6 +105,9 @@ class Portal extends AutoControlledComponent { * @param {object} data - All props. */ onOutsideClick: PropTypes.func, + + /** Controls whether or not focus trap should be applied, using boolean or FocusTrapZoneProps type value */ + trapFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), } public static defaultProps: PortalProps = { @@ -119,14 +124,20 @@ class Portal extends AutoControlledComponent { } private renderPortal(): JSX.Element | undefined { - const { children, content } = this.props + const { children, content, trapFocus } = this.props const { open } = this.state + const contentToRender = childrenExist(children) ? children : content + const focusTrapZoneProps = (_.keys(trapFocus).length && trapFocus) || {} return ( open && ( - {childrenExist(children) ? children : content} + {trapFocus ? ( + {contentToRender} + ) : ( + contentToRender + )} ) diff --git a/src/components/Provider/Provider.tsx b/src/components/Provider/Provider.tsx index f06d57203d..2c775c4e6f 100644 --- a/src/components/Provider/Provider.tsx +++ b/src/components/Provider/Provider.tsx @@ -21,7 +21,7 @@ export interface ProviderProps { } /** - * The Provider passes the CSS in JS renderer and theme down context. + * The Provider passes the CSS in JS renderer and theme to your components. */ class Provider extends React.Component { static propTypes = { diff --git a/src/components/RadioGroup/RadioGroup.tsx b/src/components/RadioGroup/RadioGroup.tsx index a3c8e4a5f0..b0991e3449 100644 --- a/src/components/RadioGroup/RadioGroup.tsx +++ b/src/components/RadioGroup/RadioGroup.tsx @@ -31,6 +31,9 @@ export interface RadioGroupProps { vertical?: boolean } +/** + * A radio group allows a user to select a value from a small set of options. + */ class RadioGroup extends AutoControlledComponent, any> { static displayName = 'RadioGroup' diff --git a/src/components/Text/Text.tsx b/src/components/Text/Text.tsx index 81796bdf07..2dfd1d0b0a 100644 --- a/src/components/Text/Text.tsx +++ b/src/components/Text/Text.tsx @@ -26,7 +26,7 @@ export interface TextProps { } /** - * A component containing text + * A Text component formats occurrences of text consistently. * @accessibility * Text is how people read the content on your website. * Ensure that a contrast ratio of at least 4.5:1 exists between text and the background behind the text. diff --git a/src/index.ts b/src/index.ts index d22c6a8aa5..dbed4569fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,3 +118,8 @@ export { export { default as chatMessageEnterEscBehavior, } from './lib/accessibility/Behaviors/Chat/chatMessageEnterEscBehavior' + +// +// Utilities +// +export { default as mergeThemes } from './lib/mergeThemes' diff --git a/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts b/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts index de54f6dc83..ba83a7ab82 100644 --- a/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts +++ b/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts @@ -6,14 +6,12 @@ import * as keyboardKey from 'keyboard-key' * Adds role='button' to 'trigger' component's part, if it is not focusable element and no role attribute provided. * Adds tabIndex='0' to 'trigger' component's part, if it is not tabbable element and no tabIndex attribute provided. * Adds attribute 'aria-disabled=true' to 'trigger' component's part based on the property 'disabled'. - * Adds attribute 'aria-haspopup=true' to 'trigger' component's part. */ const popupBehavior: Accessibility = (props: any) => ({ attributes: { trigger: { role: getAriaAttributeFromProps('role', props, 'button'), tabIndex: getAriaAttributeFromProps('tabIndex', props, '0'), - 'aria-haspopup': 'true', 'aria-disabled': !!props['disabled'], }, }, diff --git a/src/lib/accessibility/FocusZone/CHANGELOG.md b/src/lib/accessibility/FocusZone/CHANGELOG.md index 511ff2dbd7..620e8c7e3c 100644 --- a/src/lib/accessibility/FocusZone/CHANGELOG.md +++ b/src/lib/accessibility/FocusZone/CHANGELOG.md @@ -2,7 +2,9 @@ This is a list of changes made to this Stardust copy of FocusZone in comparison with the original [Fabric FocusZone @ 0f567e05952c6b50c691df2fb72d100b5e525d9e](https://github.com/OfficeDev/office-ui-fabric-react/blob/0f567e05952c6b50c691df2fb72d100b5e525d9e/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx). -### fix(FocusZone)with defaultTabbableElement prop set tabindexes are not updated accordingly [#342](https://github.com/stardust-ui/react/pull/342) +### fixes +- With `defaultTabbableElement` prop set tab indexes are not updated accordingly ([#342](https://github.com/stardust-ui/react/pull/342)) +- Remove unused prop `componentRef` ([#397](https://github.com/stardust-ui/react/pull/397)) ### feat(FocusZone): Add embed mode for FocusZone and new Chat behavior [#233](https://github.com/stardust-ui/react/pull/233) - Replaced `onFocusNotification` with a regular `onFocus` event callback to pass unit tests with embed. @@ -35,4 +37,17 @@ This is a list of changes made to this Stardust copy of FocusZone in comparison - `getParent` replaced with vanilla JS approach. - `getRTL` replaced with an `isRtl` property. - `createRef` replaced with a custom object and a callback which is necessary anyway because of custom component handling, see above for details. - - Focus related utilities moved to `focusUtilities.ts`. \ No newline at end of file + - Focus related utilities moved to `focusUtilities.ts`. + +## FocusTrapZone Changelog + +This is a list of changes made to the Stardust copy of FocusTrapZone in comparison with the original [Fabric FocusTrapZone @ 0f567e05952c6b50c691df2fb72d100b5e525d9e](https://github.com/OfficeDev/office-ui-fabric-react/blob/0f567e05952c6b50c691df2fb72d100b5e525d9e/packages/office-ui-fabric-react/src/components/FocusTrapZone/FocusTrapZone.tsx). + +### feat(Accessibility): Add focus trap zone [#239](https://github.com/stardust-ui/react/pull/239) +- Used Stardust utils instead of Fabric utilities: + - Used `eventStack`. + - Extended `React.Component` instead of Fabric `BaseComponent`. + - Used `ReactDOM.findDOMNode` reference instead of `createRef` for `_root`. +- Got rid of `componentWillMount` as it deprecated in higher versions of React. +- Added `aria-hidden` to the body children outside of the Popup to prevent screen reader from reading background information. +- Renamed `focus` method to `_findElementAndFocusAsync`, made it private and removed `IFocusTrapZone` interface as it's no longer needed. \ No newline at end of file diff --git a/src/lib/accessibility/FocusZone/FocusZone.tsx b/src/lib/accessibility/FocusZone/FocusZone.tsx index 0184fe3b17..92c4dc371d 100644 --- a/src/lib/accessibility/FocusZone/FocusZone.tsx +++ b/src/lib/accessibility/FocusZone/FocusZone.tsx @@ -16,6 +16,7 @@ import { isElementFocusZone, isElementFocusSubZone, isElementTabbable, + getWindow, IS_FOCUSABLE_ATTRIBUTE, FOCUSZONE_ID_ATTRIBUTE, } from './focusUtilities' @@ -42,9 +43,10 @@ function getParent(child: HTMLElement): HTMLElement | null { export class FocusZone extends React.Component implements IFocusZone { static propTypes = { - componentRef: PropTypes.object, className: PropTypes.string, direction: PropTypes.number, + defaultTabbableElement: PropTypes.string, + shouldFocusOnMount: PropTypes.bool, disabled: PropTypes.bool, as: customPropTypes.as, isCircularNavigation: PropTypes.bool, @@ -69,27 +71,6 @@ export class FocusZone extends React.Component implements IFocus static displayName = 'FocusZone' static className = 'ms-FocusZone' - static handledProps = [ - 'componentRef', - 'className', - 'direction', - 'defaultTabbableElement', - 'shouldFocusOnMount', - 'disabled', - 'as', - 'isCircularNavigation', - 'shouldEnterInnerZone', - 'onActiveElementChanged', - 'shouldReceiveFocus', - 'allowFocusRoot', - 'handleTabKey', - 'shouldInputLoseFocusOnArrowKey', - 'stopFocusPropagation', - 'onFocus', - 'preventDefaultWhenHandled', - 'isRtl', - ] - private _root: { current: HTMLElement | null } = { current: null } private _id: string /** The most recently focused child element. */ @@ -123,7 +104,7 @@ export class FocusZone extends React.Component implements IFocus this.setRef(this) // called here to support functional components, we only need HTMLElement ref anyway if (this._root.current) { - this.windowElement = this._root.current.ownerDocument.defaultView + this.windowElement = getWindow(this._root.current) let parentElement = getParent(this._root.current) @@ -166,8 +147,7 @@ export class FocusZone extends React.Component implements IFocus render() { const { className } = this.props const ElementType = getElementType({ defaultProps: FocusZone.defaultProps }, this.props) - - const rest = getUnhandledProps({ handledProps: FocusZone.handledProps }, this.props) + const rest = getUnhandledProps({ handledProps: [..._.keys(FocusZone.propTypes)] }, this.props) return ( { - /** - * Optional callback to access the FocusZone interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: React.RefObject - /** * Additional class name to provide on the root element, in addition to the ms-FocusZone class. */ diff --git a/src/lib/accessibility/FocusZone/focusUtilities.ts b/src/lib/accessibility/FocusZone/focusUtilities.ts index e2f66b5306..f6d83f480d 100644 --- a/src/lib/accessibility/FocusZone/focusUtilities.ts +++ b/src/lib/accessibility/FocusZone/focusUtilities.ts @@ -2,6 +2,7 @@ export const IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable' export const IS_VISIBLE_ATTRIBUTE = 'data-is-visible' export const FOCUSZONE_ID_ATTRIBUTE = 'data-focuszone-id' export const FOCUSZONE_SUB_ATTRIBUTE = 'data-is-sub-focuszone' +export const HIDDEN_FROM_ACC_TREE = 'data-is-hidden-from-acc-tree' export const FOCUSZONE_WRAP_ATTRIBUTE = 'data-focuszone-wrap' /** @@ -399,3 +400,46 @@ export function isElementFocusSubZone(element?: HTMLElement): boolean { element.getAttribute(FOCUSZONE_SUB_ATTRIBUTE) === 'true' ) } + +let targetToFocusOnNextRepaint: HTMLElement | { focus: () => void } | null | undefined = undefined + +/** + * Sets focus to an element asynchronously. The focus will be set at the next browser repaint, + * meaning it won't cause any extra recalculations. If more than one focusAsync is called during one frame, + * only the latest called focusAsync element will actually be focused + * @param element The element to focus + */ +export function focusAsync(element: HTMLElement | { focus: () => void } | undefined | null): void { + if (element) { + // An element was already queued to be focused, so replace that one with the new element + if (targetToFocusOnNextRepaint) { + targetToFocusOnNextRepaint = element + return + } + + targetToFocusOnNextRepaint = element + + const win = getWindow(element as Element) + + if (win) { + // element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe + win.requestAnimationFrame(() => { + targetToFocusOnNextRepaint && targetToFocusOnNextRepaint.focus() + + // We are done focusing for this frame, so reset the queued focus element + targetToFocusOnNextRepaint = undefined + }) + } + } +} + +/** + * Helper to get the window object. + * + * @public + */ +export function getWindow(rootElement?: Element | null): Window | undefined { + return ( + (rootElement && rootElement.ownerDocument && rootElement.ownerDocument.defaultView) || window + ) +} diff --git a/src/lib/accessibility/FocusZone/index.ts b/src/lib/accessibility/FocusZone/index.ts index e6fd20a72c..2b2b001c73 100644 --- a/src/lib/accessibility/FocusZone/index.ts +++ b/src/lib/accessibility/FocusZone/index.ts @@ -1,2 +1,4 @@ export * from './FocusZone' export * from './FocusZone.types' +export * from './FocusTrapZone' +export * from './FocusTrapZone.types' diff --git a/src/lib/accessibility/types.ts b/src/lib/accessibility/types.ts index d724d09b31..99f5f2dafe 100644 --- a/src/lib/accessibility/types.ts +++ b/src/lib/accessibility/types.ts @@ -1,4 +1,4 @@ -import { FocusZoneProps } from './FocusZone' +import { FocusTrapZoneProps, FocusZoneProps } from './FocusZone' export type AriaWidgetRole = | 'button' @@ -140,12 +140,17 @@ export type FocusZoneDefinition = { props?: FocusZoneProps } +export type FocusTrapZoneDefinition = { + props?: FocusTrapZoneProps +} + export type KeyActions = { [partName: string]: { [actionName: string]: KeyAction } } export interface AccessibilityDefinition { attributes?: AccessibilityAttributesBySlot keyActions?: KeyActions handledProps?: (keyof AccessibilityAttributes)[] focusZone?: FocusZoneDefinition + focusTrapZone?: FocusTrapZoneDefinition } export interface AccessibilityBehavior extends AccessibilityDefinition { diff --git a/src/lib/eventStack/EventTarget.tsx b/src/lib/eventStack/EventTarget.tsx index 9b4b4c722a..3018b8efb7 100644 --- a/src/lib/eventStack/EventTarget.tsx +++ b/src/lib/eventStack/EventTarget.tsx @@ -32,19 +32,19 @@ export default class EventTarget { // Listeners handling // ------------------------------------ - _listen = name => { + _listen = (name, useCapture = false) => { if (_.has(this._handlers, name)) return const handler = this._emit(name) - this.target.addEventListener(name, handler) + this.target.addEventListener(name, handler, useCapture) this._handlers[name] = handler } - _unlisten = name => { + _unlisten = (name, useCapture = false) => { if (_.some(this._pools, name)) return const { [name]: handler } = this._handlers - this.target.removeEventListener(name, handler) + this.target.removeEventListener(name, handler, useCapture) delete this._handlers[name] } @@ -54,17 +54,17 @@ export default class EventTarget { empty = () => _.isEmpty(this._handlers) - sub = (name, handlers, pool = 'default') => { + sub = (name, handlers, pool = 'default', useCapture = false) => { const events = _.uniq([ ..._.get(this._pools, `${pool}.${name}`, []), ...this._normalize(handlers), ]) - this._listen(name) + this._listen(name, useCapture) _.set(this._pools, `${pool}.${name}`, events) } - unsub = (name, handlers, pool = 'default') => { + unsub = (name, handlers, pool = 'default', useCapture = false) => { const events = _.without( _.get(this._pools, `${pool}.${name}`, []), ...this._normalize(handlers), @@ -76,6 +76,6 @@ export default class EventTarget { } _.set(this._pools, `${pool}.${name}`, undefined) - this._unlisten(name) + this._unlisten(name, useCapture) } } diff --git a/src/lib/eventStack/eventStack.tsx b/src/lib/eventStack/eventStack.tsx index 68519ffd4d..c4c7e720ac 100644 --- a/src/lib/eventStack/eventStack.tsx +++ b/src/lib/eventStack/eventStack.tsx @@ -34,20 +34,20 @@ class EventStack { sub = (name, handlers, options: any = {}) => { if (!isBrowser()) return - const { target = document, pool = 'default' } = options + const { target = document, pool = 'default', useCapture = false } = options const eventTarget = this._find(target) - eventTarget.sub(name, handlers, pool) + eventTarget.sub(name, handlers, pool, useCapture) } unsub = (name, handlers, options: any = {}) => { if (!isBrowser()) return - const { target = document, pool = 'default' } = options + const { target = document, pool = 'default', useCapture = false } = options const eventTarget = this._find(target, false) if (eventTarget) { - eventTarget.unsub(name, handlers, pool) + eventTarget.unsub(name, handlers, pool, useCapture) if (eventTarget.empty()) this._remove(target) } } diff --git a/src/lib/index.ts b/src/lib/index.ts index 80a0063c64..8a06f59b93 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -9,6 +9,7 @@ export { default as toCompactArray } from './toCompactArray' export * from './factories' export { default as callable } from './callable' +export { default as constants } from './constants' export { default as getClasses } from './getClasses' export { default as getElementType } from './getElementType' export { default as getUnhandledProps } from './getUnhandledProps' diff --git a/src/lib/renderComponent.tsx b/src/lib/renderComponent.tsx index cd3d7eee18..f77d7fa6a7 100644 --- a/src/lib/renderComponent.tsx +++ b/src/lib/renderComponent.tsx @@ -1,5 +1,6 @@ import * as cx from 'classnames' import * as React from 'react' +import * as _ from 'lodash' import { FelaTheme } from 'react-fela' import callable from './callable' @@ -7,6 +8,7 @@ import felaRenderer from './felaRenderer' import getClasses from './getClasses' import getElementType from './getElementType' import getUnhandledProps from './getUnhandledProps' +import logProviderMissingWarning from './providerMissingHandler' import { ComponentStyleFunctionParam, ComponentVariablesObject, @@ -132,6 +134,10 @@ const renderComponent =

( return ( { + if (_.isEmpty(theme)) { + logProviderMissingWarning() + } + const { siteVariables = { fontSizes: {} }, componentVariables = {}, diff --git a/src/themes/teams/components/Icon/iconStyles.ts b/src/themes/teams/components/Icon/iconStyles.ts index 407c471606..5238a2745d 100644 --- a/src/themes/teams/components/Icon/iconStyles.ts +++ b/src/themes/teams/components/Icon/iconStyles.ts @@ -1,6 +1,6 @@ import fontAwesomeIcons from './fontAwesomeIconStyles' import { callable } from '../../../../lib' -import { disabledStyle, fittedStyle } from '../../../../styles/customCSS' +import { fittedStyle } from '../../../../styles/customCSS' import { ComponentSlotStylesInput, ICSSInJSStyle, FontIconSpec } from '../../../types' import { ResultOf } from '../../../../../types/utils' import { IconXSpacing, IconProps } from '../../../../components/Icon/Icon' @@ -30,22 +30,12 @@ const getFontStyles = (iconName: string, themeIcon?: ResultOf): IC return { fontFamily, - width: '1.18em', - fontStyle: 'normal', - fontWeight: 400, - textDecoration: 'inherit', textAlign: 'center', - - '-webkit-font-smoothing': 'antialiased', - '-moz-osx-font-smoothing': 'grayscale', - backfaceVisibility: 'hidden', - lineHeight: 1, + width: '1.18em', '::before': { content, - boxSizing: 'inherit', - background: '0 0', }, } } @@ -67,7 +57,6 @@ const getBorderedStyles = (isFontBased, circular, borderColor, color): ICSSInJSS return { ...getPaddedStyle(isFontBased), - // TODO: "black" here should actually match the Icon's fill or text color boxShadow: `0 0 0 0.05em ${borderColor || color || 'black'} inset`, ...(circular ? { borderRadius: '50%' } : {}), } @@ -87,34 +76,42 @@ const iconStyles: ComponentSlotStylesInput = { }): ICSSInJSStyle => { const iconSpec = theme.icons[name] const isFontBased = !iconSpec || !iconSpec.isSvg + const iconColor = v.color || 'currentColor' return { + backgroundColor: v.backgroundColor, display: 'inline-block', fontSize: getSize(size), - + margin: v.margin, + speak: 'none', + verticalAlign: 'middle', + overflow: 'hidden', width: '1em', height: '1em', ...(isFontBased && getFontStyles(name, callable(iconSpec && (iconSpec.icon as FontIconSpec))())), - ...(isFontBased && { color: v.color }), - backgroundColor: v.backgroundColor, + ...(isFontBased && { + color: iconColor, - opacity: 1, - margin: v.margin, + ...(disabled && { + color: v.disabledColor, + }), + }), - speak: 'none', + ...(!isFontBased && { + fill: iconColor, - verticalAlign: 'middle', - overflow: 'hidden', + ...(disabled && { + fill: v.disabledColor, + }), + }), ...getXSpacingStyles(xSpacing, v.horizontalSpace), ...((bordered || v.borderColor || circular) && getBorderedStyles(isFontBased, circular, v.borderColor, v.color)), - - ...(disabled && disabledStyle), } }, @@ -122,8 +119,8 @@ const iconStyles: ComponentSlotStylesInput = { g: getSvgStyle('g'), - /* additional SVG styles for different paths could be added/used in the same way */ path: getSvgStyle('path'), + secondaryPath: getSvgStyle('secondaryPath'), } diff --git a/src/themes/teams/components/Icon/iconVariables.ts b/src/themes/teams/components/Icon/iconVariables.ts index f222c2534e..11132ed0f3 100644 --- a/src/themes/teams/components/Icon/iconVariables.ts +++ b/src/themes/teams/components/Icon/iconVariables.ts @@ -9,15 +9,15 @@ export interface IconVariables { horizontalSpace: string margin: string secondaryColor: string + disabledColor: string } -export default (): IconVariables => ({ +export default (siteVars): IconVariables => ({ color: undefined, - // TODO move initial variable discovery to JSON files - // similar to how components have an info.json file backgroundColor: undefined, borderColor: undefined, horizontalSpace: pxToRem(10), margin: '0 0.25em 0 0', - secondaryColor: 'white', + secondaryColor: siteVars.white, + disabledColor: siteVars.gray06, }) diff --git a/src/themes/teams/components/Icon/svg/icons/index.ts b/src/themes/teams/components/Icon/svg/icons/index.ts index da52583cbd..1aca8c2efc 100644 --- a/src/themes/teams/components/Icon/svg/icons/index.ts +++ b/src/themes/teams/components/Icon/svg/icons/index.ts @@ -12,6 +12,11 @@ import more from './more' import teamCreate from './teamCreate' import umbrella from './umbrella' import teams from './teams' +import format from './format' +import giphy from './giphy' +import fontColor from './fontColor' +import fontSize from './fontSize' +import highlight from './highlight' export default { call, @@ -26,4 +31,9 @@ export default { 'team-create': teamCreate, umbrella, teams, + format, + giphy, + highlight, + 'font-color': fontColor, + 'font-size': fontSize, } as { [iconName: string]: TeamsSvgIconSpec } diff --git a/test/specs/commonTests/handlesAccessibility.tsx b/test/specs/commonTests/handlesAccessibility.tsx index 43dc1cfd8f..44ca8d25e3 100644 --- a/test/specs/commonTests/handlesAccessibility.tsx +++ b/test/specs/commonTests/handlesAccessibility.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { getTestingRenderedComponent } from 'test/utils' +import { mountWithProviderAndGetComponent } from 'test/utils' import { defaultBehavior } from 'src/lib/accessibility' import { Accessibility, AriaRole, FocusZoneMode } from 'src/lib/accessibility/types' import { FocusZone } from 'src/lib/accessibility/FocusZone' @@ -46,13 +46,13 @@ export default (Component, options: any = {}) => { } = options test('gets default accessibility when no override used', () => { - const rendered = getTestingRenderedComponent(Component, ) + const rendered = mountWithProviderAndGetComponent(Component, ) const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(defaultRootRole) }) test('does not get role when overrides to default', () => { - const rendered = getTestingRenderedComponent( + const rendered = mountWithProviderAndGetComponent( Component, , ) @@ -63,7 +63,7 @@ export default (Component, options: any = {}) => { if (!partSelector) { // temporarily disabled as we do not support overriding of attributes applied to parts test('gets correct role when overrides accessibility', () => { - const rendered = getTestingRenderedComponent( + const rendered = mountWithProviderAndGetComponent( Component, , ) @@ -73,7 +73,7 @@ export default (Component, options: any = {}) => { test('gets correct role when overrides role', () => { const testRole = 'test-role' - const rendered = getTestingRenderedComponent( + const rendered = mountWithProviderAndGetComponent( Component, , ) @@ -83,7 +83,7 @@ export default (Component, options: any = {}) => { test('gets correct role when overrides both accessibility and role', () => { const testRole = 'test-role' - const rendered = getTestingRenderedComponent( + const rendered = mountWithProviderAndGetComponent( Component, , ) @@ -95,7 +95,10 @@ export default (Component, options: any = {}) => { if (focusZoneDefinition) { if (focusZoneDefinition.mode === FocusZoneMode.Wrap) { test('gets wrapped in FocusZone', () => { - const rendered = getTestingRenderedComponent(Component, ) + const rendered = mountWithProviderAndGetComponent( + Component, + , + ) const focusZone = rendered.childAt(0).childAt(0) // skip thru FelaTheme expect(focusZone.type()).toEqual(FocusZone) @@ -106,7 +109,10 @@ export default (Component, options: any = {}) => { }) } else if (focusZoneDefinition.mode === FocusZoneMode.Embed) { test('gets embedded with FocusZone', () => { - const rendered = getTestingRenderedComponent(Component, ) + const rendered = mountWithProviderAndGetComponent( + Component, + , + ) const focusZone = rendered.childAt(0).childAt(0) // skip thru FelaTheme expect(focusZone.type()).toEqual(FocusZone) diff --git a/test/specs/commonTests/htmlIsAccessibilityCompliant.ts b/test/specs/commonTests/htmlIsAccessibilityCompliant.ts index 3f643fe318..47145a7b61 100644 --- a/test/specs/commonTests/htmlIsAccessibilityCompliant.ts +++ b/test/specs/commonTests/htmlIsAccessibilityCompliant.ts @@ -1,7 +1,6 @@ -const ReactDOMServer = require('react-dom/server') -const { axe } = require('jest-axe') - -const { toHaveNoViolations } = require('jest-axe') +import * as ReactDOMServer from 'react-dom/server' +import { axe, toHaveNoViolations } from 'jest-axe' +import { withProvider } from 'test/utils' type AxeMatcher = jest.Matchers & { toHaveNoViolations: () => R @@ -14,8 +13,8 @@ interface AxeExpect extends jest.Expect { const expectAxe = expect as AxeExpect expect.extend(toHaveNoViolations) -export default async (jsx: React.ReactNode) => { - const html = ReactDOMServer.renderToString(jsx) +export default async (jsx: React.ReactElement) => { + const html = ReactDOMServer.renderToString(withProvider(jsx)) const results = await axe(html) expectAxe(results).toHaveNoViolations() } diff --git a/test/specs/commonTests/implementsCollectionShorthandProp.tsx b/test/specs/commonTests/implementsCollectionShorthandProp.tsx index 9e447f6ec9..4ce7ea9edc 100644 --- a/test/specs/commonTests/implementsCollectionShorthandProp.tsx +++ b/test/specs/commonTests/implementsCollectionShorthandProp.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { mount } from './isConformant' +import { mountWithProvider as mount } from 'test/utils' import * as _ from 'lodash' export type CollectionShorthandTestOptions = { diff --git a/test/specs/commonTests/implementsShorthandProp.tsx b/test/specs/commonTests/implementsShorthandProp.tsx index 57f2fe426c..b498e532cc 100644 --- a/test/specs/commonTests/implementsShorthandProp.tsx +++ b/test/specs/commonTests/implementsShorthandProp.tsx @@ -1,5 +1,6 @@ import * as React from 'react' -import { mount, ReactWrapper } from 'enzyme' +import { ReactWrapper } from 'enzyme' +import { mountWithProvider } from 'test/utils' import { Props } from '../../../types/utils' export type ShorthandTestOptions = { @@ -34,7 +35,10 @@ export default Component => { const matchedProps = typeof withProps === 'string' ? { [mapsValueToProp]: withProps } : withProps - expectContainsSingleShorthandElement(mount(), matchedProps) + expectContainsSingleShorthandElement( + mountWithProvider(), + matchedProps, + ) } describe(`shorthand property '${shorthandProp}' with default value of '${displayName}' component`, () => { diff --git a/test/specs/commonTests/implementsWrapperProp.tsx b/test/specs/commonTests/implementsWrapperProp.tsx index 5245f3db95..9aaaa34f9a 100644 --- a/test/specs/commonTests/implementsWrapperProp.tsx +++ b/test/specs/commonTests/implementsWrapperProp.tsx @@ -1,5 +1,6 @@ import * as React from 'react' -import { mount, ReactWrapper } from 'enzyme' +import { ReactWrapper } from 'enzyme' +import { mountWithProvider as mount } from 'test/utils' import Slot from 'src/components/Slot/Slot' import { ShorthandValue } from 'utils' diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx index e8f24524c9..5829470a44 100644 --- a/test/specs/commonTests/isConformant.tsx +++ b/test/specs/commonTests/isConformant.tsx @@ -1,15 +1,19 @@ import * as _ from 'lodash' import * as React from 'react' -import { mount as enzymeMount, ReactWrapper } from 'enzyme' +import { ReactWrapper } from 'enzyme' import * as ReactDOMServer from 'react-dom/server' -import { ThemeProvider } from 'react-fela' import isExportedAtTopLevel from './isExportedAtTopLevel' -import { assertBodyContains, consoleUtil, syntheticEvent } from 'test/utils' +import { + assertBodyContains, + consoleUtil, + mountWithProvider as mount, + syntheticEvent, +} from 'test/utils' import helpers from './commonHelpers' import * as stardust from 'src/' -import { felaRenderer } from 'src/lib' + import { FocusZone } from 'src/lib/accessibility/FocusZone' import { FOCUSZONE_WRAP_ATTRIBUTE } from 'src/lib/accessibility/FocusZone/focusUtilities' @@ -20,13 +24,6 @@ export interface Conformant { rendersPortal?: boolean } -export const mount = (node, options?) => { - return enzymeMount( - {node}, - options, - ) -} - /** * Assert Component conforms to guidelines that are applicable to all components. * @param {React.Component|Function} Component A component that should conform. diff --git a/test/specs/commonTests/stylesFunction-test.tsx b/test/specs/commonTests/stylesFunction-test.tsx index c3496b4682..151aedd329 100644 --- a/test/specs/commonTests/stylesFunction-test.tsx +++ b/test/specs/commonTests/stylesFunction-test.tsx @@ -4,7 +4,7 @@ import * as _ from 'lodash' import { UIComponent } from 'src/lib' import { Extendable } from 'types/utils' import { ICSSInJSStyle } from 'src/themes/types' -import { getTestingRenderedComponent } from 'test/utils' +import { mountWithProviderAndGetComponent } from 'test/utils' type AttrValue = 'props' | 'state' @@ -52,7 +52,7 @@ const testStylesForComponent = ({ /> ) - getTestingRenderedComponent(TestStylesComponent, ) + mountWithProviderAndGetComponent(TestStylesComponent, ) } describe('styles function', () => { diff --git a/test/specs/components/Button/Button-test.tsx b/test/specs/components/Button/Button-test.tsx index fcefcc9053..5ba96cde98 100644 --- a/test/specs/components/Button/Button-test.tsx +++ b/test/specs/components/Button/Button-test.tsx @@ -7,7 +7,7 @@ import { implementsShorthandProp, getRenderedAttribute, } from 'test/specs/commonTests' -import { getTestingRenderedComponent, mountWithProvider } from 'test/utils' +import { mountWithProvider, mountWithProviderAndGetComponent } from 'test/utils' import { toggleButtonBehavior } from '../../../../src/lib/accessibility' import Button from 'src/components/Button/Button' @@ -35,12 +35,12 @@ describe('Button', () => { describe('aria-disabled', () => { test('is set to true, if disabled attribute is provided', () => { - const renderedComponent = getTestingRenderedComponent(Button, }) - testPortalInnerIsOpen(false) + const { wrapper } = mountPortal({ trigger: }) + testPortalInnerIsOpen(wrapper, false) wrapper.find('button').simulate('click') - testPortalInnerIsOpen(true) + testPortalInnerIsOpen(wrapper, true) }) it('closes the portal on click when set', () => { - mountPortal({ trigger: - mountPortal({ trigger }) + const { wrapper } = mountPortal({ trigger }) expect(wrapper.find('button').length).toBe(1) expect(wrapper.text()).toEqual(text) diff --git a/test/specs/components/Slot/Slot-test.ts b/test/specs/components/Slot/Slot-test.ts index af47fd7eef..148c0c24d3 100644 --- a/test/specs/components/Slot/Slot-test.ts +++ b/test/specs/components/Slot/Slot-test.ts @@ -1,4 +1,4 @@ -import { mount } from 'enzyme' +import { mountWithProvider as mount } from 'test/utils' import Slot from 'src/components/Slot/Slot' import { isConformant } from 'test/specs/commonTests' diff --git a/test/utils/index.ts b/test/utils/index.ts index f28c9024ba..0d47ac7c29 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -4,4 +4,4 @@ export { default as assertWithTimeout } from './assertWithTimeout' export { default as consoleUtil } from './consoleUtil' export { default as domEvent } from './domEvent' export { default as syntheticEvent } from './syntheticEvent' -export * from './testingComponents' +export { withProvider, mountWithProvider, mountWithProviderAndGetComponent } from './withProvider' From 67b02d5697c2c368186da0cbc2ea761b1895090d Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Wed, 24 Oct 2018 15:42:41 -0700 Subject: [PATCH 07/12] fixing merge conflict... --- .../ComponentDocAccessibility.tsx | 36 ++ .../Variations/IconExampleColor.shorthand.tsx | 13 + .../PortalExampleFocusTrapped.shorthand.tsx | 73 +++ .../Types/PortalExampleFocusTrapped.tsx | 70 +++ .../Types/ProviderExample.shorthand.tsx | 20 + .../components/Provider/Types/index.tsx | 15 + .../examples/components/Provider/index.tsx | 10 + .../accessibility/FocusZone/FocusTrapZone.tsx | 345 ++++++++++++ .../FocusZone/FocusTrapZone.types.tsx | 12 + src/lib/constants.ts | 10 + src/lib/providerMissingHandler.ts | 17 + .../components/Icon/svg/icons/fontColor.tsx | 23 + .../components/Icon/svg/icons/fontSize.tsx | 31 ++ .../components/Icon/svg/icons/format.tsx | 23 + .../teams/components/Icon/svg/icons/giphy.tsx | 22 + .../components/Icon/svg/icons/highlight.tsx | 23 + .../components/Provider/Provider-test.tsx | 12 + .../Provider/ProviderConsumer-test.tsx | 74 +++ test/specs/lib/FocusTrapZone-test.tsx | 497 ++++++++++++++++++ test/utils/withProvider.tsx | 22 + 20 files changed, 1348 insertions(+) create mode 100644 docs/src/components/ComponentDoc/ComponentDocAccessibility.tsx create mode 100644 docs/src/examples/components/Icon/Variations/IconExampleColor.shorthand.tsx create mode 100644 docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx create mode 100644 docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.tsx create mode 100644 docs/src/examples/components/Provider/Types/ProviderExample.shorthand.tsx create mode 100644 docs/src/examples/components/Provider/Types/index.tsx create mode 100644 docs/src/examples/components/Provider/index.tsx create mode 100644 src/lib/accessibility/FocusZone/FocusTrapZone.tsx create mode 100644 src/lib/accessibility/FocusZone/FocusTrapZone.types.tsx create mode 100644 src/lib/constants.ts create mode 100644 src/lib/providerMissingHandler.ts create mode 100644 src/themes/teams/components/Icon/svg/icons/fontColor.tsx create mode 100644 src/themes/teams/components/Icon/svg/icons/fontSize.tsx create mode 100644 src/themes/teams/components/Icon/svg/icons/format.tsx create mode 100644 src/themes/teams/components/Icon/svg/icons/giphy.tsx create mode 100644 src/themes/teams/components/Icon/svg/icons/highlight.tsx create mode 100644 test/specs/components/Provider/Provider-test.tsx create mode 100644 test/specs/components/Provider/ProviderConsumer-test.tsx create mode 100644 test/specs/lib/FocusTrapZone-test.tsx create mode 100644 test/utils/withProvider.tsx diff --git a/docs/src/components/ComponentDoc/ComponentDocAccessibility.tsx b/docs/src/components/ComponentDoc/ComponentDocAccessibility.tsx new file mode 100644 index 0000000000..1b7e6d39c8 --- /dev/null +++ b/docs/src/components/ComponentDoc/ComponentDocAccessibility.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import * as _ from 'lodash' +import { Header } from '@stardust-ui/react' + +const behaviorMenu = require('docs/src/behaviorMenu') + +const ComponentDocAccessibility = ({ info }) => { + const description = _.get(_.find(info.docblock.tags, { title: 'accessibility' }), 'description') + const defaultValue = _.get(_.find(info.props, { name: 'accessibility' }), 'defaultValue') + + const stem = defaultValue && defaultValue.split('.').pop() + const filename = stem && stem + '.ts' + + const behaviorName = behaviorMenu.reduce((acc, next) => { + return _.find(next.variations, { name: filename }) ? next.displayName : acc + }, null) + + if (!behaviorName && !description) return null + + return ( + <> +

+ + {behaviorName && ( +

+ Default behavior:{' '} + {behaviorName} +

+ )} + + {description &&

{description}

} + + ) +} + +export default ComponentDocAccessibility diff --git a/docs/src/examples/components/Icon/Variations/IconExampleColor.shorthand.tsx b/docs/src/examples/components/Icon/Variations/IconExampleColor.shorthand.tsx new file mode 100644 index 0000000000..231e3bb8ae --- /dev/null +++ b/docs/src/examples/components/Icon/Variations/IconExampleColor.shorthand.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Icon } from '@stardust-ui/react' + +const IconExampleColor = () => ( +
+ + + + +
+) + +export default IconExampleColor diff --git a/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx new file mode 100644 index 0000000000..cc3482d16c --- /dev/null +++ b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { Button, Header, Portal } from '@stardust-ui/react' + +class PortalExampleFocusTrapped extends React.Component { + state = { open: false } + + openPortal = () => { + this.setState({ open: true }) + } + + closePortal = () => { + this.setState({ open: false }) + } + + render() { + const { open } = this.state + const btnContent = open ? 'Close Portal' : 'Open Portal' + + return ( +
+
+ } + /> +
+ ) + } +} + +export default PortalExampleFocusTrapped diff --git a/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.tsx b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.tsx new file mode 100644 index 0000000000..b05754d672 --- /dev/null +++ b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { Button, Header, Portal } from '@stardust-ui/react' + +class PortalExamplePortal extends React.Component { + state = { open: false } + + openPortal = () => { + this.setState({ open: true }) + } + + closePortal = () => { + this.setState({ open: false }) + } + + render() { + const { open } = this.state + const btnContent = open ? 'Close Portal' : 'Open Portal' + + return ( +
+
+
+
+ ) + } +} + +export default PortalExamplePortal diff --git a/docs/src/examples/components/Provider/Types/ProviderExample.shorthand.tsx b/docs/src/examples/components/Provider/Types/ProviderExample.shorthand.tsx new file mode 100644 index 0000000000..a09d4ab1ae --- /dev/null +++ b/docs/src/examples/components/Provider/Types/ProviderExample.shorthand.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Provider } from '@stardust-ui/react' + +const theme = { siteVariables: { brand: 'cornflowerblue' } } + +const ProviderExampleShorthand = () => ( + +
+

+ Use the Provider.Consumer to access the theme: +

+ + theme.siteVariables.brand = {theme.siteVariables.brand}} + /> +
+
+) + +export default ProviderExampleShorthand diff --git a/docs/src/examples/components/Provider/Types/index.tsx b/docs/src/examples/components/Provider/Types/index.tsx new file mode 100644 index 0000000000..57b5e357df --- /dev/null +++ b/docs/src/examples/components/Provider/Types/index.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const Types = () => ( + + + +) + +export default Types diff --git a/docs/src/examples/components/Provider/index.tsx b/docs/src/examples/components/Provider/index.tsx new file mode 100644 index 0000000000..bb4e2aaa06 --- /dev/null +++ b/docs/src/examples/components/Provider/index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import Types from './Types' + +const ProviderExamples = () => ( +
+ +
+) + +export default ProviderExamples diff --git a/src/lib/accessibility/FocusZone/FocusTrapZone.tsx b/src/lib/accessibility/FocusZone/FocusTrapZone.tsx new file mode 100644 index 0000000000..443768d156 --- /dev/null +++ b/src/lib/accessibility/FocusZone/FocusTrapZone.tsx @@ -0,0 +1,345 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import * as keyboardKey from 'keyboard-key' +import * as PropTypes from 'prop-types' +import * as _ from 'lodash' +import eventStack from '../../eventStack' + +import { + getNextElement, + getFirstTabbable, + getLastTabbable, + getWindow, + focusAsync, + HIDDEN_FROM_ACC_TREE, +} from './focusUtilities' + +import { FocusTrapZoneProps } from './FocusTrapZone.types' +import getUnhandledProps from '../../getUnhandledProps' +import getElementType from '../../getElementType' +import * as customPropTypes from '../../customPropTypes' + +/** FocusTrapZone is used to trap the focus in any html element placed in body + * and hide other elements outside of Focus Trap Zone from accessibility tree. + * Pressing tab will circle focus within the inner focusable elements of the FocusTrapZone. */ +export class FocusTrapZone extends React.Component { + private static _focusStack: FocusTrapZone[] = [] + private _root: { current: HTMLElement | null } = { current: null } + private _previouslyFocusedElementOutsideTrapZone: HTMLElement + private _previouslyFocusedElementInTrapZone?: HTMLElement + private windowElement: Window | null + + private createRef = elem => (this._root.current = ReactDOM.findDOMNode(elem) as HTMLElement) + + static propTypes = { + /** + * Element type the root element will use. Default is "div". + */ + as: customPropTypes.as, + + /** Additional CSS class name(s) to apply. */ + className: PropTypes.string, + + /** + * Sets the HTMLElement to focus on when exiting the FocusTrapZone. + * @default The element.target that triggered the FTZ. + */ + elementToFocusOnDismiss: PropTypes.object, + + /** + * Sets the aria-labelledby attribute. + */ + ariaLabelledBy: PropTypes.string, + + /** + * Indicates if this Trap Zone will allow clicks outside the FocusTrapZone + * @default false + */ + isClickableOutsideFocusTrap: PropTypes.bool, + + /** + * Indicates if this Trap Zone will ignore keeping track of HTMLElement that activated the Zone. + * @default false + */ + ignoreExternalFocusing: PropTypes.bool, + + /** + * Indicates whether focus trap zone should force focus inside the focus trap zone + * @default true + */ + forceFocusInsideTrap: PropTypes.bool, + + /** + * Indicates the selector for first focusable item. Only applies if focusPreviouslyFocusedInnerElement == false. + */ + firstFocusableSelector: PropTypes.string, + + /** + * Do not put focus onto first element when render focus trap zone + * @default false + */ + disableFirstFocus: PropTypes.bool, + + /** + * Specifies the algorithm used to determine which descendant element to focus when focus() is called. + * If false, the first focusable descendant, filtered by the firstFocusableSelector property if present, is chosen. + * If true, the element that was focused when the Trap Zone last had a focused descendant is chosen. + * If it has never had a focused descendant before, behavior falls back to the first focused descendant. + * @default false + */ + focusPreviouslyFocusedInnerElement: PropTypes.bool, + } + + static defaultProps: FocusTrapZoneProps = { + as: 'div', + isClickableOutsideFocusTrap: true, + } + + public componentDidMount(): void { + FocusTrapZone._focusStack.push(this) + const { disableFirstFocus = false } = this.props + + this.windowElement = getWindow(this._root.current) + this._previouslyFocusedElementOutsideTrapZone = this._getPreviouslyFocusedElementOutsideTrapZone() + + if ( + !this._root.current.contains(this._previouslyFocusedElementOutsideTrapZone) && + !disableFirstFocus + ) { + this._findElementAndFocusAsync() + } + + this._hideContentFromAccessibilityTree() + this._subscribeToEvents() + } + + public render(): JSX.Element { + const { className, ariaLabelledBy } = this.props + const rest = getUnhandledProps( + { handledProps: [..._.keys(FocusTrapZone.propTypes)] }, + this.props, + ) + const ElementType = getElementType({ defaultProps: FocusTrapZone.defaultProps }, this.props) + + return ( + + {this.props.children} + + ) + } + + public componentWillUnmount(): void { + const { ignoreExternalFocusing } = this.props + + FocusTrapZone._focusStack = FocusTrapZone._focusStack.filter((value: FocusTrapZone) => { + return this !== value + }) + + const activeElement = document.activeElement as HTMLElement + if ( + !ignoreExternalFocusing && + this._previouslyFocusedElementOutsideTrapZone && + (this._root.current.contains(activeElement) || activeElement === document.body) + ) { + focusAsync(this._previouslyFocusedElementOutsideTrapZone) + } + + this._unsubscribeFromEvents() + const lastActiveFocusTrap = + FocusTrapZone._focusStack.length && + FocusTrapZone._focusStack[FocusTrapZone._focusStack.length - 1] + + if (!lastActiveFocusTrap) { + this._showContentInAccessibilityTree() + } else if ( + lastActiveFocusTrap._root.current && + lastActiveFocusTrap._root.current.hasAttribute(HIDDEN_FROM_ACC_TREE) + ) { + lastActiveFocusTrap._root.current.removeAttribute(HIDDEN_FROM_ACC_TREE) + lastActiveFocusTrap._root.current.removeAttribute('aria-hidden') + } + } + + private _findElementAndFocusAsync = () => { + if (!this._root.current) return + const { focusPreviouslyFocusedInnerElement, firstFocusableSelector } = this.props + + if ( + focusPreviouslyFocusedInnerElement && + this._previouslyFocusedElementInTrapZone && + this._root.current.contains(this._previouslyFocusedElementInTrapZone) + ) { + // focus on the last item that had focus in the zone before we left the zone + focusAsync(this._previouslyFocusedElementInTrapZone) + return + } + + const focusSelector = + firstFocusableSelector && + (typeof firstFocusableSelector === 'string' + ? firstFocusableSelector + : firstFocusableSelector()) + + const firstFocusableChild = focusSelector + ? (this._root.current.querySelector('.' + focusSelector) as HTMLElement) + : getNextElement( + this._root.current, + this._root.current.firstChild as HTMLElement, + true, + false, + false, + true, + ) + + firstFocusableChild && focusAsync(firstFocusableChild) + } + + private _onFocusCapture = (ev: React.FocusEvent) => { + this.props.onFocusCapture && this.props.onFocusCapture(ev) + if (ev.target !== ev.currentTarget) { + // every time focus changes within the trap zone, remember the focused element so that + // it can be restored if focus leaves the pane and returns via keystroke (i.e. via a call to this.focus(true)) + this._previouslyFocusedElementInTrapZone = ev.target as HTMLElement + } + } + + private _onKeyboardHandler = (ev: React.KeyboardEvent): void => { + this.props.onKeyDown && this.props.onKeyDown(ev) + + if ( + ev.isDefaultPrevented() || + keyboardKey.getCode(ev) !== keyboardKey.Tab || + !this._root.current + ) { + return + } + + const _firstTabbableChild = getFirstTabbable( + this._root.current, + this._root.current.firstChild as HTMLElement, + true, + ) + const _lastTabbableChild = getLastTabbable( + this._root.current, + this._root.current.lastChild as HTMLElement, + true, + ) + + if (ev.shiftKey && _firstTabbableChild === ev.target) { + focusAsync(_lastTabbableChild) + ev.preventDefault() + ev.stopPropagation() + } else if (!ev.shiftKey && _lastTabbableChild === ev.target) { + focusAsync(_firstTabbableChild) + ev.preventDefault() + ev.stopPropagation() + } + } + + private _forceFocusInTrap = (ev: Event, triggeredElement: HTMLElement) => { + if ( + FocusTrapZone._focusStack.length && + this === FocusTrapZone._focusStack[FocusTrapZone._focusStack.length - 1] + ) { + if (!this._root.current.contains(triggeredElement)) { + this._findElementAndFocusAsync() + ev.preventDefault() + ev.stopPropagation() + } + } + } + + private _handleOutsideFocus = (ev: FocusEvent): void => { + const focusedElement = document.activeElement as HTMLElement + focusedElement && this._forceFocusInTrap(ev, focusedElement) + } + + private _handleOutsideClick = (ev: MouseEvent): void => { + const clickedElement = ev.target as HTMLElement + clickedElement && this._forceFocusInTrap(ev, clickedElement) + } + + private _subscribeToEvents = () => { + const { forceFocusInsideTrap, isClickableOutsideFocusTrap } = this.props + if (forceFocusInsideTrap) { + eventStack.sub('focus', this._handleOutsideFocus, { + target: this.windowElement, + useCapture: true, + }) + } + + if (!isClickableOutsideFocusTrap) { + eventStack.sub('click', this._handleOutsideClick, { + target: this.windowElement, + useCapture: true, + }) + } + } + + private _unsubscribeFromEvents = () => { + const { forceFocusInsideTrap, isClickableOutsideFocusTrap } = this.props + if (forceFocusInsideTrap) { + eventStack.unsub('focus', this._handleOutsideFocus, { + target: this.windowElement, + useCapture: true, + }) + } + + if (!isClickableOutsideFocusTrap) { + eventStack.unsub('click', this._handleOutsideClick, { + target: this.windowElement, + useCapture: true, + }) + } + } + + private _getPreviouslyFocusedElementOutsideTrapZone = () => { + const { elementToFocusOnDismiss } = this.props + let previouslyFocusedElement = this._previouslyFocusedElementOutsideTrapZone + + if (elementToFocusOnDismiss && previouslyFocusedElement !== elementToFocusOnDismiss) { + previouslyFocusedElement = elementToFocusOnDismiss + } else if (!previouslyFocusedElement) { + previouslyFocusedElement = document.activeElement as HTMLElement + } + + return previouslyFocusedElement + } + + private _hideContentFromAccessibilityTree = () => { + const bodyChildren = (document.body && document.body.children) || [] + + if (bodyChildren.length && !document.body.contains(this._root.current)) { + // In case popup render options will change + console.warn( + 'Body element does not contain trap zone element. Please, ensure the trap zone element is placed inside body, so it will work properly.', + ) + } + + // loop through all body's children, except the last one - which is the popup + for (let index = 0; index < bodyChildren.length - 1; index++) { + const element = bodyChildren[index] + + if (element.getAttribute('aria-hidden') !== 'true') { + element.setAttribute('aria-hidden', 'true') + element.setAttribute(HIDDEN_FROM_ACC_TREE, 'true') + } + } + } + + private _showContentInAccessibilityTree = () => { + const hiddenElements = document.querySelectorAll(`[${HIDDEN_FROM_ACC_TREE}="true"]`) + for (let index = 0; index < hiddenElements.length; index++) { + const element = hiddenElements[index] + element.removeAttribute('aria-hidden') + element.removeAttribute(HIDDEN_FROM_ACC_TREE) + } + } +} diff --git a/src/lib/accessibility/FocusZone/FocusTrapZone.types.tsx b/src/lib/accessibility/FocusZone/FocusTrapZone.types.tsx new file mode 100644 index 0000000000..56ff89382d --- /dev/null +++ b/src/lib/accessibility/FocusZone/FocusTrapZone.types.tsx @@ -0,0 +1,12 @@ +export interface FocusTrapZoneProps extends React.HTMLAttributes { + as?: React.ReactType + className?: string + elementToFocusOnDismiss?: HTMLElement + ariaLabelledBy?: string + isClickableOutsideFocusTrap?: boolean + ignoreExternalFocusing?: boolean + forceFocusInsideTrap?: boolean + firstFocusableSelector?: string | (() => string) + disableFirstFocus?: boolean + focusPreviouslyFocusedInnerElement?: boolean +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000000..49e2b0b05c --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,10 @@ +const docSiteUrl = 'https://stardust-ui.github.io/react' + +const constants = { + docSiteUrl, + quickStartUrl: `${docSiteUrl}/quick-start`, + repoURL: 'https://github.com/stardust-ui/react', + typeOrder: ['component', 'behavior'], +} + +export default constants diff --git a/src/lib/providerMissingHandler.ts b/src/lib/providerMissingHandler.ts new file mode 100644 index 0000000000..6601716a76 --- /dev/null +++ b/src/lib/providerMissingHandler.ts @@ -0,0 +1,17 @@ +import constants from './constants' + +const warning = { + message: `You probably forgot to use a 'Provider'. Check ${ + constants.quickStartUrl + } for correct usage.`, + wasLogged: false, +} + +const logProviderMissingWarning = (): void => { + if (!warning.wasLogged) { + console.error(warning.message) + warning.wasLogged = true + } +} + +export default logProviderMissingWarning diff --git a/src/themes/teams/components/Icon/svg/icons/fontColor.tsx b/src/themes/teams/components/Icon/svg/icons/fontColor.tsx new file mode 100644 index 0000000000..93f202a87e --- /dev/null +++ b/src/themes/teams/components/Icon/svg/icons/fontColor.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { TeamsSvgIconSpec } from '../types' + +export default { + icon: ({ classes }) => ( + + + + + + ), + styles: { + svg: ({ variables: v }) => ({ + fill: v.color, + }), + }, +} as TeamsSvgIconSpec diff --git a/src/themes/teams/components/Icon/svg/icons/fontSize.tsx b/src/themes/teams/components/Icon/svg/icons/fontSize.tsx new file mode 100644 index 0000000000..b47f344454 --- /dev/null +++ b/src/themes/teams/components/Icon/svg/icons/fontSize.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { TeamsSvgIconSpec } from '../types' + +export default { + icon: ({ classes }) => ( + + + + + + ), + styles: { + svg: ({ variables: v }) => ({ + fill: v.color, + }), + }, +} as TeamsSvgIconSpec diff --git a/src/themes/teams/components/Icon/svg/icons/format.tsx b/src/themes/teams/components/Icon/svg/icons/format.tsx new file mode 100644 index 0000000000..fae36ab921 --- /dev/null +++ b/src/themes/teams/components/Icon/svg/icons/format.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { TeamsSvgIconSpec } from '../types' + +export default { + icon: ({ classes }) => ( + + + + ), + styles: { + svg: ({ variables: v }) => ({ + fill: v.color, + }), + }, +} as TeamsSvgIconSpec diff --git a/src/themes/teams/components/Icon/svg/icons/giphy.tsx b/src/themes/teams/components/Icon/svg/icons/giphy.tsx new file mode 100644 index 0000000000..0039efaebc --- /dev/null +++ b/src/themes/teams/components/Icon/svg/icons/giphy.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { TeamsSvgIconSpec } from '../types' + +export default { + icon: ({ classes }) => ( + + + + ), + styles: { + svg: ({ variables: v }) => ({ + fill: v.color, + }), + }, +} as TeamsSvgIconSpec diff --git a/src/themes/teams/components/Icon/svg/icons/highlight.tsx b/src/themes/teams/components/Icon/svg/icons/highlight.tsx new file mode 100644 index 0000000000..ed9b8c963b --- /dev/null +++ b/src/themes/teams/components/Icon/svg/icons/highlight.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { TeamsSvgIconSpec } from '../types' + +export default { + icon: ({ classes }) => ( + + + + + + ), + styles: { + svg: ({ variables: v }) => ({ + fill: v.color, + }), + }, +} as TeamsSvgIconSpec diff --git a/test/specs/components/Provider/Provider-test.tsx b/test/specs/components/Provider/Provider-test.tsx new file mode 100644 index 0000000000..4c3b185e0a --- /dev/null +++ b/test/specs/components/Provider/Provider-test.tsx @@ -0,0 +1,12 @@ +import Provider from 'src/components/Provider/Provider' +import ProviderConsumer from 'src/components/Provider/ProviderConsumer' + +describe('Provider', () => { + test('is exported', () => { + expect(require('src/index.ts').Provider).toEqual(Provider) + }) + + test('has a ProviderConsumer subcomponent', () => { + expect(require('src/index.ts').Provider.Consumer).toEqual(ProviderConsumer) + }) +}) diff --git a/test/specs/components/Provider/ProviderConsumer-test.tsx b/test/specs/components/Provider/ProviderConsumer-test.tsx new file mode 100644 index 0000000000..391630117e --- /dev/null +++ b/test/specs/components/Provider/ProviderConsumer-test.tsx @@ -0,0 +1,74 @@ +import * as React from 'react' +import { mount } from 'enzyme' + +import Provider from 'src/components/Provider/Provider' +import ProviderConsumer from 'src/components/Provider/ProviderConsumer' +import { IThemeInput } from 'types/theme' + +describe('ProviderConsumer', () => { + test('is exported', () => { + expect(require('src/index.ts').ProviderConsumer).toEqual(ProviderConsumer) + }) + + test('is a subcomponent of the Provider', () => { + expect(Provider.Consumer).toEqual(ProviderConsumer) + }) + + describe('render', () => { + test('is a callback that receives the prepared theme', () => { + expect.assertions(13) + + const inputTheme: IThemeInput = { + siteVariables: { a: 'b' }, + componentVariables: { Button: { color: 'red' } }, + componentStyles: { Button: { root: { color: 'red' } } }, + fontFaces: [{ name: 'name', paths: ['path.woff2'], style: { fontWeight: 400 } }], + staticStyles: ['body{margin:0;}', { body: { margin: 0 } }], + icons: { + user: { icon: { content: '\\f1', fontFamily: 'i' } }, + userFunc: { icon: () => ({ content: '\\f1', fontFamily: 'i' }) }, + userSvg: { isSvg: true, icon: }, + userSvgFunc: { isSvg: true, icon: () => }, + }, + } + + mount( + + { + // siteVariables + expect(preparedTheme).toHaveProperty('siteVariables.a', 'b') + + // componentVariables + expect(preparedTheme).toHaveProperty('componentVariables.Button') + expect(preparedTheme.componentVariables.Button).toBeInstanceOf(Function) + expect(preparedTheme.componentVariables.Button()).toMatchObject( + inputTheme.componentVariables.Button, + ) + + // componentStyles + expect(preparedTheme).toHaveProperty('componentStyles.Button.root') + expect(preparedTheme.componentStyles.Button.root).toBeInstanceOf(Function) + expect(preparedTheme.componentStyles.Button.root()).toMatchObject( + inputTheme.componentStyles.Button.root, + ) + + // fontFaces + expect(preparedTheme).toHaveProperty('fontFaces') + expect(preparedTheme.fontFaces).toMatchObject(inputTheme.fontFaces) + + // staticStyles + expect(preparedTheme).toHaveProperty('staticStyles') + expect(preparedTheme.staticStyles).toMatchObject(inputTheme.staticStyles) + + // icons + expect(preparedTheme).toHaveProperty('icons') + expect(preparedTheme.icons).toMatchObject(inputTheme.icons) + return null + }} + /> + , + ) + }) + }) +}) diff --git a/test/specs/lib/FocusTrapZone-test.tsx b/test/specs/lib/FocusTrapZone-test.tsx new file mode 100644 index 0000000000..42495251e2 --- /dev/null +++ b/test/specs/lib/FocusTrapZone-test.tsx @@ -0,0 +1,497 @@ +import * as React from 'react' +import * as ReactTestUtils from 'react-dom/test-utils' +import * as keyboardKey from 'keyboard-key' +import { + FocusTrapZone, + FocusZone, + FocusZoneDirection, +} from '../../../src/lib/accessibility/FocusZone' + +// rAF does not exist in node - let's mock it +window.requestAnimationFrame = (callback: FrameRequestCallback) => { + const r = window.setTimeout(callback, 0) + jest.runAllTimers() + return r +} + +const animationFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve)) +jest.useFakeTimers() + +class FocusTrapZoneTestComponent extends React.Component< + {}, + { isShowingFirst: boolean; isShowingSecond: boolean } +> { + constructor(props) { + super(props) + this.state = { isShowingFirst: true, isShowingSecond: false } + } + + public render() { + return ( +
+ + + + + + {this.state.isShowingFirst && ( + + First + + )} + {this.state.isShowingSecond && ( + + First + + )} +
+ ) + } + + private _toggleFirst = () => this.setState({ isShowingFirst: !this.state.isShowingFirst }) + private _toggleSecond = () => this.setState({ isShowingSecond: !this.state.isShowingSecond }) +} + +describe('FocusTrapZone', () => { + let lastFocusedElement: HTMLElement | undefined + + const _onFocus = (ev: any): void => (lastFocusedElement = ev.target) + + const setupElement = ( + element: HTMLElement, + { + clientRect, + isVisible = true, + }: { + clientRect: { + top: number + left: number + bottom: number + right: number + } + isVisible?: boolean + }, + ): void => { + element.getBoundingClientRect = () => ({ + top: clientRect.top, + left: clientRect.left, + bottom: clientRect.bottom, + right: clientRect.right, + width: clientRect.right - clientRect.left, + height: clientRect.bottom - clientRect.top, + }) + + element.setAttribute('data-is-visible', String(isVisible)) + element.focus = () => ReactTestUtils.Simulate.focus(element) + } + + beforeEach(() => { + lastFocusedElement = undefined + }) + + describe('Tab and shift-tab wrap at extreme ends of the FTZ', () => { + it('can tab across FocusZones with different button structures', async () => { + expect.assertions(3) + + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+
+
, + ) as HTMLElement + + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + const buttonC = topLevelDiv.querySelector('.c') as HTMLElement + const buttonD = topLevelDiv.querySelector('.d') as HTMLElement + const buttonE = topLevelDiv.querySelector('.e') as HTMLElement + const buttonF = topLevelDiv.querySelector('.f') as HTMLElement + + // Assign bounding locations to buttons. + setupElement(buttonA, { clientRect: { top: 0, bottom: 30, left: 0, right: 30 } }) + setupElement(buttonB, { clientRect: { top: 0, bottom: 30, left: 30, right: 60 } }) + setupElement(buttonC, { clientRect: { top: 0, bottom: 30, left: 60, right: 90 } }) + setupElement(buttonD, { clientRect: { top: 30, bottom: 60, left: 0, right: 30 } }) + setupElement(buttonE, { clientRect: { top: 30, bottom: 60, left: 30, right: 60 } }) + setupElement(buttonF, { clientRect: { top: 30, bottom: 60, left: 60, right: 90 } }) + + // Focus the first button. + ReactTestUtils.Simulate.focus(buttonA) + await animationFrame() + expect(lastFocusedElement).toBe(buttonA) + + // Pressing shift + tab should go to d. + ReactTestUtils.Simulate.keyDown(buttonA, { which: keyboardKey.Tab, shiftKey: true }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonD) + + // Pressing tab should go to a. + ReactTestUtils.Simulate.keyDown(buttonD, { which: keyboardKey.Tab }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonA) + }) + + it('can tab across a FocusZone with different button structures', async () => { + expect.assertions(3) + + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ +
+ +
+ +
+ +
+
+
+ + + +
+
+
+
+
, + ) as HTMLElement + + const buttonX = topLevelDiv.querySelector('.x') as HTMLElement + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + const buttonC = topLevelDiv.querySelector('.c') as HTMLElement + const buttonD = topLevelDiv.querySelector('.d') as HTMLElement + + // Assign bounding locations to buttons. + setupElement(buttonX, { clientRect: { top: 0, bottom: 30, left: 0, right: 30 } }) + setupElement(buttonA, { clientRect: { top: 0, bottom: 30, left: 0, right: 30 } }) + setupElement(buttonB, { clientRect: { top: 0, bottom: 30, left: 30, right: 60 } }) + setupElement(buttonC, { clientRect: { top: 0, bottom: 30, left: 60, right: 90 } }) + setupElement(buttonD, { clientRect: { top: 30, bottom: 60, left: 0, right: 30 } }) + + // Focus the first button. + ReactTestUtils.Simulate.focus(buttonX) + await animationFrame() + expect(lastFocusedElement).toBe(buttonX) + + // Pressing shift + tab should go to a. + ReactTestUtils.Simulate.keyDown(buttonX, { which: keyboardKey.Tab, shiftKey: true }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonA) + + // Pressing tab should go to x. + ReactTestUtils.Simulate.keyDown(buttonA, { which: keyboardKey.Tab }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonX) + }) + + it('can trap focus when FTZ bookmark elements are FocusZones, and those elements have inner elements focused that are not the first inner element', async () => { + expect.assertions(4) + + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ + + + + + + + + + + + + + + +
, + ) as HTMLElement + + const buttonZ1 = topLevelDiv.querySelector('.z1') as HTMLElement + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + const buttonC = topLevelDiv.querySelector('.c') as HTMLElement + const buttonD = topLevelDiv.querySelector('.d') as HTMLElement + const buttonE = topLevelDiv.querySelector('.e') as HTMLElement + const buttonF = topLevelDiv.querySelector('.f') as HTMLElement + const buttonG = topLevelDiv.querySelector('.g') as HTMLElement + const buttonZ2 = topLevelDiv.querySelector('.z2') as HTMLElement + + // Assign bounding locations to buttons. + setupElement(buttonZ1, { clientRect: { top: 0, bottom: 10, left: 0, right: 10 } }) + setupElement(buttonA, { clientRect: { top: 10, bottom: 30, left: 0, right: 10 } }) + setupElement(buttonB, { clientRect: { top: 10, bottom: 30, left: 10, right: 20 } }) + setupElement(buttonC, { clientRect: { top: 10, bottom: 30, left: 20, right: 30 } }) + setupElement(buttonD, { clientRect: { top: 30, bottom: 40, left: 0, right: 10 } }) + setupElement(buttonE, { clientRect: { top: 40, bottom: 60, left: 0, right: 10 } }) + setupElement(buttonF, { clientRect: { top: 40, bottom: 60, left: 10, right: 20 } }) + setupElement(buttonG, { clientRect: { top: 40, bottom: 60, left: 20, right: 30 } }) + setupElement(buttonZ2, { clientRect: { top: 60, bottom: 70, left: 0, right: 10 } }) + + // Focus the middle button in the first FZ. + ReactTestUtils.Simulate.focus(buttonA) + await animationFrame() + ReactTestUtils.Simulate.keyDown(buttonA, { which: keyboardKey.ArrowRight }) + expect(lastFocusedElement).toBe(buttonB) + + // Focus the middle button in the second FZ. + ReactTestUtils.Simulate.focus(buttonE) + await animationFrame() + ReactTestUtils.Simulate.keyDown(buttonE, { which: keyboardKey.ArrowRight }) + expect(lastFocusedElement).toBe(buttonF) + + // Pressing tab should go to B the last focused element in FZ1. + ReactTestUtils.Simulate.keyDown(buttonF, { which: keyboardKey.Tab }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + + // Pressing shift-tab should go to F the last focused element in FZ2. + ReactTestUtils.Simulate.keyDown(buttonB, { which: keyboardKey.Tab, shiftKey: true }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonF) + }) + }) + + describe('Tab and shift-tab do nothing (keep focus where it is) when the FTZ contains 0 tabbable items', () => { + function setupTest() { + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ + + + + + + +
, + ) as HTMLElement + + const buttonZ1 = topLevelDiv.querySelector('.z1') as HTMLElement + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + const buttonC = topLevelDiv.querySelector('.c') as HTMLElement + const buttonZ2 = topLevelDiv.querySelector('.z2') as HTMLElement + + // Assign bounding locations to buttons. + setupElement(buttonZ1, { clientRect: { top: 0, bottom: 10, left: 0, right: 10 } }) + setupElement(buttonA, { clientRect: { top: 10, bottom: 20, left: 0, right: 10 } }) + setupElement(buttonB, { clientRect: { top: 20, bottom: 30, left: 0, right: 10 } }) + setupElement(buttonC, { clientRect: { top: 30, bottom: 40, left: 0, right: 10 } }) + setupElement(buttonZ2, { clientRect: { top: 40, bottom: 50, left: 0, right: 10 } }) + + return { buttonZ1, buttonA, buttonB, buttonC, buttonZ2 } + } + + it('does not move when pressing tab', async () => { + expect.assertions(2) + + const { buttonB } = setupTest() + + // Focus the middle button in the FTZ, even though it has tabIndex=-1 + ReactTestUtils.Simulate.focus(buttonB) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + + // Pressing tab should stay where you are. + ReactTestUtils.Simulate.keyDown(buttonB, { which: keyboardKey.Tab }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + }) + + it('does not move when pressing shift-tab', async () => { + expect.assertions(2) + + const { buttonB } = setupTest() + + // Focus the middle button in the FTZ, even though it has tabIndex=-1 + ReactTestUtils.Simulate.focus(buttonB) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + + // Pressing shift-tab should stay where you are. + ReactTestUtils.Simulate.keyDown(buttonB, { which: keyboardKey.Tab, shiftKey: true }) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + }) + }) + + describe('Focusing the FTZ', () => { + function setupTest(focusPreviouslyFocusedInnerElement: boolean) { + let focusTrapZoneRef: FocusTrapZone | null = null + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ { + focusTrapZoneRef = ftz + }} + > + + + + + + + +
, + ) as HTMLElement + + const buttonF = topLevelDiv.querySelector('.f') as HTMLElement + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + const buttonZ = topLevelDiv.querySelector('.z') as HTMLElement + + // Assign bounding locations to buttons. + setupElement(buttonF, { clientRect: { top: 0, bottom: 10, left: 0, right: 10 } }) + setupElement(buttonA, { clientRect: { top: 10, bottom: 20, left: 0, right: 10 } }) + setupElement(buttonB, { clientRect: { top: 20, bottom: 30, left: 0, right: 10 } }) + setupElement(buttonZ, { clientRect: { top: 30, bottom: 40, left: 0, right: 10 } }) + + return { focusTrapZone: focusTrapZoneRef, buttonF, buttonA, buttonB, buttonZ } + } + + it('goes to previously focused element when focusing the FTZ', async () => { + expect.assertions(4) + + const { focusTrapZone, buttonF, buttonB, buttonZ } = setupTest( + true /*focusPreviouslyFocusedInnerElement*/, + ) + + // By calling `componentDidMount`, FTZ will behave as just initialized and focus needed element + focusTrapZone.componentDidMount() + await animationFrame() + expect(lastFocusedElement).toBe(buttonF) + + // Focus inside the trap zone, not the first element. + ReactTestUtils.Simulate.focus(buttonB) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + + // Focus outside the trap zone + ReactTestUtils.Simulate.focus(buttonZ) + await animationFrame() + expect(lastFocusedElement).toBe(buttonZ) + + // By calling `componentDidMount`, FTZ will behave as just initialized and focus needed element + // FTZ should return to originally focused inner element. + focusTrapZone.componentDidMount() + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + }) + + it('goes to first focusable element when focusing the FTZ', async () => { + expect.assertions(4) + + const { focusTrapZone, buttonF, buttonB, buttonZ } = setupTest( + false /*focusPreviouslyFocusedInnerElement*/, + ) + + // By calling `componentDidMount`, FTZ will behave as just initialized and focus needed element + // Focus within should go to 1st focusable inner element. + focusTrapZone.componentDidMount() + await animationFrame() + expect(lastFocusedElement).toBe(buttonF) + + // Focus inside the trap zone, not the first element. + ReactTestUtils.Simulate.focus(buttonB) + await animationFrame() + expect(lastFocusedElement).toBe(buttonB) + + // Focus outside the trap zone + ReactTestUtils.Simulate.focus(buttonZ) + await animationFrame() + expect(lastFocusedElement).toBe(buttonZ) + + // By calling `componentDidMount`, FTZ will behave as just initialized and focus needed element + // Focus should go to the first focusable element + focusTrapZone.componentDidMount() + await animationFrame() + expect(lastFocusedElement).toBe(buttonF) + }) + }) + + describe('Nested FocusTrapZones Stack Behavior', () => { + const getFocusStack = (): FocusTrapZone[] => (FocusTrapZone as any)._focusStack + beforeAll(() => (getFocusStack().length = 0)) + + it('FocusTrapZone maintains a proper stack of FocusTrapZones as more are mounted/unmounted.', async () => { + let focusTrapZoneFocusStack: FocusTrapZone[] = getFocusStack() + const topLevelDiv = ReactTestUtils.renderIntoDocument( +
+ +
, + ) as HTMLElement + const buttonA = topLevelDiv.querySelector('.a') as HTMLElement + + const buttonB = topLevelDiv.querySelector('.b') as HTMLElement + + expect(focusTrapZoneFocusStack.length).toBe(2) + const baseFocusTrapZone = focusTrapZoneFocusStack[0] + expect(baseFocusTrapZone.props.forceFocusInsideTrap).toBe(true) + expect(baseFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(false) + + const firstFocusTrapZone = focusTrapZoneFocusStack[1] + expect(firstFocusTrapZone.props.forceFocusInsideTrap).toBe(false) + expect(firstFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(false) + + // There should be now 3 focus trap zones (base/first/second) + ReactTestUtils.Simulate.click(buttonB) + expect(focusTrapZoneFocusStack.length).toBe(3) + expect(focusTrapZoneFocusStack[0]).toBe(baseFocusTrapZone) + expect(focusTrapZoneFocusStack[1]).toBe(firstFocusTrapZone) + const secondFocusTrapZone = focusTrapZoneFocusStack[2] + expect(secondFocusTrapZone.props.forceFocusInsideTrap).toBe(false) + expect(secondFocusTrapZone.props.isClickableOutsideFocusTrap).toBe(true) + + // we remove the middle one + // unmounting a focus trap zone should remove it from the focus stack. + // but we also check that it removes the right focustrapzone (the middle one) + ReactTestUtils.Simulate.click(buttonA) + focusTrapZoneFocusStack = getFocusStack() + + expect(focusTrapZoneFocusStack.length).toBe(2) + expect(focusTrapZoneFocusStack[0]).toBe(baseFocusTrapZone) + expect(focusTrapZoneFocusStack[1]).toBe(secondFocusTrapZone) + + // finally remove the last focus trap zone. + ReactTestUtils.Simulate.click(buttonB) + focusTrapZoneFocusStack = getFocusStack() + + expect(focusTrapZoneFocusStack.length).toBe(1) + expect(focusTrapZoneFocusStack[0]).toBe(baseFocusTrapZone) + }) + }) +}) diff --git a/test/utils/withProvider.tsx b/test/utils/withProvider.tsx new file mode 100644 index 0000000000..11130ee100 --- /dev/null +++ b/test/utils/withProvider.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { mount } from 'enzyme' +import { ThemeProvider } from 'react-fela' +import { felaRenderer } from 'src/lib' +import { ThemeInput } from 'src/themes/types' + +export const withProvider = (node: React.ReactElement, theme?: ThemeInput) => ( + {node} +) + +export const mountWithProvider = (node, options?, theme?: ThemeInput) => { + return mount(withProvider(node, theme), options) +} + +export const mountWithProviderAndGetComponent = ( + Component, + elementToMount, + options?: {}, + theme?: ThemeInput, +) => { + return mountWithProvider(elementToMount, options, theme).find(Component) +} From 5ae08247aa394d62e87324be5e9274ab5c0d815c Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Thu, 25 Oct 2018 16:20:20 -0700 Subject: [PATCH 08/12] updating borders --- .../components/Input/inputVariables.ts | 4 +--- src/themes/teams/components/Input/inputStyles.ts | 14 ++------------ .../teams/components/Input/inputVariables.ts | 8 ++++---- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts index b5f954165c..0e33c6c7b8 100644 --- a/src/themes/teams-high-contrast/components/Input/inputVariables.ts +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -1,9 +1,7 @@ -import { pxToRem } from '../../../../lib' - export default (siteVars: any) => { return { backgroundColor: siteVars.bodyBackground, - border: `${pxToRem(1)} solid ${siteVars.bodyColor}`, + borderColor: 'white', borderBottomColor: siteVars.accessibleYellow, fontColor: siteVars.bodyColor, iconColor: siteVars.bodyColor, diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 1175042fe4..25f1eeb912 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -2,7 +2,6 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { InputProps } from '../../../../components/Input/Input' import { InputVariables } from './inputVariables' import { PositionProperty } from 'csstype' -import { pxToRem } from '../../../../lib' const inputStyles: ComponentSlotStylesInput = { root: ({ props: p }): ICSSInJSStyle => ({ @@ -15,9 +14,9 @@ const inputStyles: ComponentSlotStylesInput = { input: ({ props: p, variables: v }): ICSSInJSStyle => ({ outline: 0, - border: v.border, + borderColor: v.borderColor, + borderWidth: '1px 1px 2px 1px', borderRadius: v.borderRadius, - borderBottom: v.borderBottom, color: v.fontColor, backgroundColor: v.backgroundColor, position: 'relative', @@ -30,15 +29,6 @@ const inputStyles: ComponentSlotStylesInput = { ':focus': { borderBottomColor: v.inputFocusBorderColor, }, - '&::after': { - borderRadius: v.inputFocusBorderRadius, - borderBottom: `${pxToRem(2)} solid transparent`, - content: '', - position: 'absolute', - bottom: '0', - left: '0', - right: '0', - }, }), icon: ({ variables: v }): ICSSInJSStyle => ({ diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index a52c8ffe53..abe686a58c 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -1,8 +1,8 @@ import { pxToRem } from '../../../../lib' export interface InputVariables { backgroundColor: string - border: string - borderBottom: string + borderBottomColor: string + borderColor: string borderRadius: string fontColor: string fontSize: string @@ -20,8 +20,8 @@ const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [2, 3, 6, export default (siteVars): InputVariables => ({ backgroundColor: siteVars.gray10, - border: '0', - borderBottom: `${_2px_asRem} solid transparent`, + borderColor: 'transparent', + borderBottomColor: 'transparent', borderRadius: _3px_asRem, fontColor: siteVars.bodyColor, From a95e4a2c30d68b82ec1dd88088988d57da893d39 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Fri, 2 Nov 2018 15:57:01 -0700 Subject: [PATCH 09/12] updating the borders --- .../components/Input/inputVariables.ts | 8 ++++--- .../teams/components/Input/inputStyles.ts | 6 ++--- .../teams/components/Input/inputVariables.ts | 23 +++++++++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/themes/teams-high-contrast/components/Input/inputVariables.ts b/src/themes/teams-high-contrast/components/Input/inputVariables.ts index 0e33c6c7b8..6480f71931 100644 --- a/src/themes/teams-high-contrast/components/Input/inputVariables.ts +++ b/src/themes/teams-high-contrast/components/Input/inputVariables.ts @@ -1,10 +1,12 @@ +import { pxToRem } from '../../../../lib' + export default (siteVars: any) => { return { backgroundColor: siteVars.bodyBackground, - borderColor: 'white', - borderBottomColor: siteVars.accessibleYellow, + border: `${pxToRem(1)} solid ${siteVars.bodyColor}`, + boxShadow: `0 ${pxToRem(1)} 0 ${siteVars.accessibleYellow}`, fontColor: siteVars.bodyColor, iconColor: siteVars.bodyColor, - inputFocusBorderColor: siteVars.accessibleYellow, + inputFocusBorderBottomColor: siteVars.accessibleYellow, } } diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 25f1eeb912..3954ab0e7d 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -14,8 +14,7 @@ const inputStyles: ComponentSlotStylesInput = { input: ({ props: p, variables: v }): ICSSInJSStyle => ({ outline: 0, - borderColor: v.borderColor, - borderWidth: '1px 1px 2px 1px', + border: v.border, borderRadius: v.borderRadius, color: v.fontColor, backgroundColor: v.backgroundColor, @@ -27,7 +26,8 @@ const inputStyles: ComponentSlotStylesInput = { color: v.fontColor, }, ':focus': { - borderBottomColor: v.inputFocusBorderColor, + borderBottomColor: v.inputFocusBorderBottomColor, + boxShadow: v.boxShadow, }, }), diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index abe686a58c..8c85dcd103 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -1,28 +1,33 @@ import { pxToRem } from '../../../../lib' export interface InputVariables { backgroundColor: string - borderBottomColor: string - borderColor: string + border: string borderRadius: string + boxShadow: string fontColor: string fontSize: string iconColor: string iconPosition: string iconRight: string inputPadding: string - inputFocusBorderColor: string + inputFocusBorderBottomColor: string inputFocusBorderRadius: string } -const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [2, 3, 6, 12, 24].map(v => - pxToRem(v), -) +const [_1px_asRem, _2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [ + 1, + 2, + 3, + 6, + 12, + 24, +].map(v => pxToRem(v)) export default (siteVars): InputVariables => ({ backgroundColor: siteVars.gray10, - borderColor: 'transparent', - borderBottomColor: 'transparent', + border: `${_1px_asRem} solid transparent`, borderRadius: _3px_asRem, + boxShadow: `0 ${_1px_asRem} 0 ${siteVars.brand}`, fontColor: siteVars.bodyColor, fontSize: siteVars.fontSizes.medium, @@ -32,6 +37,6 @@ export default (siteVars): InputVariables => ({ iconColor: siteVars.bodyColor, inputPadding: `${_6px_asRem} ${_24px_asRem} ${_6px_asRem} ${_12px_asRem}`, - inputFocusBorderColor: siteVars.brand, + inputFocusBorderBottomColor: siteVars.brand, inputFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, }) From dee136c9945754d086bdd2ac3257d9e5c2c489b2 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Fri, 2 Nov 2018 16:15:01 -0700 Subject: [PATCH 10/12] removing trailing space --- src/themes/teams-high-contrast/componentVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/teams-high-contrast/componentVariables.ts b/src/themes/teams-high-contrast/componentVariables.ts index c8a02e8bbd..f55c1caa73 100644 --- a/src/themes/teams-high-contrast/componentVariables.ts +++ b/src/themes/teams-high-contrast/componentVariables.ts @@ -1,7 +1,7 @@ export { default as Input } from './components/Input/inputVariables' export { default as Avatar } from './components/Avatar/avatarVariables' - + export { default as Button } from './components/Button/buttonVariables' export { default as Divider } from './components/Divider/dividerVariables' From 471c0b180b84e13706356bf2d318525e71e6f5c2 Mon Sep 17 00:00:00 2001 From: Levi Thomason Date: Tue, 6 Nov 2018 14:34:43 -0800 Subject: [PATCH 11/12] cleanup nits --- .../teams-high-contrast/componentVariables.ts | 4 ++-- .../teams/components/Input/inputVariables.ts | 21 ++++++------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/themes/teams-high-contrast/componentVariables.ts b/src/themes/teams-high-contrast/componentVariables.ts index f55c1caa73..f1c2b68dac 100644 --- a/src/themes/teams-high-contrast/componentVariables.ts +++ b/src/themes/teams-high-contrast/componentVariables.ts @@ -1,9 +1,9 @@ -export { default as Input } from './components/Input/inputVariables' - export { default as Avatar } from './components/Avatar/avatarVariables' export { default as Button } from './components/Button/buttonVariables' export { default as Divider } from './components/Divider/dividerVariables' +export { default as Input } from './components/Input/inputVariables' + export { default as Text } from './components/Text/textVariables' diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 8c85dcd103..d16068812f 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -14,29 +14,20 @@ export interface InputVariables { inputFocusBorderRadius: string } -const [_1px_asRem, _2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [ - 1, - 2, - 3, - 6, - 12, - 24, -].map(v => pxToRem(v)) - export default (siteVars): InputVariables => ({ backgroundColor: siteVars.gray10, - border: `${_1px_asRem} solid transparent`, - borderRadius: _3px_asRem, - boxShadow: `0 ${_1px_asRem} 0 ${siteVars.brand}`, + border: `${pxToRem(1)} solid transparent`, + borderRadius: pxToRem(3), + boxShadow: `0 ${pxToRem(1)} 0 ${siteVars.brand}`, fontColor: siteVars.bodyColor, fontSize: siteVars.fontSizes.medium, iconPosition: 'absolute', - iconRight: _2px_asRem, + iconRight: pxToRem(2), iconColor: siteVars.bodyColor, - inputPadding: `${_6px_asRem} ${_24px_asRem} ${_6px_asRem} ${_12px_asRem}`, + inputPadding: `${pxToRem(6)} ${pxToRem(4)} ${pxToRem(6)} ${pxToRem(2)}`, inputFocusBorderBottomColor: siteVars.brand, - inputFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, + inputFocusBorderRadius: `${pxToRem(3)} ${pxToRem(3)} ${pxToRem(2)} ${pxToRem(2)}`, }) From 1aae2a3aa11a34fb67f2ee6a2709ae0d5e1642a3 Mon Sep 17 00:00:00 2001 From: Andrew Martin Date: Tue, 6 Nov 2018 15:45:05 -0800 Subject: [PATCH 12/12] updating input styles to use pxToRem --- src/themes/teams/components/Input/inputVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index d16068812f..0c2153322f 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -27,7 +27,7 @@ export default (siteVars): InputVariables => ({ iconRight: pxToRem(2), iconColor: siteVars.bodyColor, - inputPadding: `${pxToRem(6)} ${pxToRem(4)} ${pxToRem(6)} ${pxToRem(2)}`, + inputPadding: `${pxToRem(6)} ${pxToRem(24)} ${pxToRem(6)} ${pxToRem(12)}`, inputFocusBorderBottomColor: siteVars.brand, inputFocusBorderRadius: `${pxToRem(3)} ${pxToRem(3)} ${pxToRem(2)} ${pxToRem(2)}`, })