diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist/ diff --git a/components/Avatar/Avatar.jsx b/components/Avatar/Avatar.jsx index db0af179a..796cd92ed 100644 --- a/components/Avatar/Avatar.jsx +++ b/components/Avatar/Avatar.jsx @@ -1,14 +1,14 @@ import React from 'react' +import { default as ReactAvatar } from 'react-avatar' require('./Avatar.scss') -const Avatar = ({ avatarUrl }) => { - - const src = avatarUrl || require('./place-holder.svg') - +const Avatar = ({ avatarUrl, userName, size }) => { + const s = size || 35 + const src = !avatarUrl && !userName ? require('./place-holder.svg') : avatarUrl return (
- +
) } diff --git a/components/Avatar/Avatar.scss b/components/Avatar/Avatar.scss index bddc3e9ce..5ebbaa72c 100644 --- a/components/Avatar/Avatar.scss +++ b/components/Avatar/Avatar.scss @@ -1,14 +1,17 @@ $avatar-diameter: 35px; -.Avatar { - width : $avatar-diameter; - height : $avatar-diameter; - border-radius : 50%; - background-color: #eee; - overflow : hidden; - - img { - width : 100%; - height: 100%; +:global { + .Avatar { + width : $avatar-diameter; + height : $avatar-diameter; + border-radius : 50%; + background-color: #eee; + overflow : hidden; + + img { + width : 100%; + height: 100%; + } } } + \ No newline at end of file diff --git a/components/Carousel/Carousel.jsx b/components/Carousel/Carousel.jsx index 538398382..78aaacd7b 100644 --- a/components/Carousel/Carousel.jsx +++ b/components/Carousel/Carousel.jsx @@ -4,8 +4,8 @@ import classNames from 'classnames' import React, { Component } from 'react' import ReactDOM from 'react-dom' -import LeftArrowIcon from '../Icons/LeftArrowIcon' -import RightArrowIcon from '../Icons/RightArrowIcon' +import IconArrowMinimalLeft from '../Icons/IconArrowMinimalLeft' +import IconArrowMinimalRight from '../Icons/IconArrowMinimalRight' export default class Carousel extends Component { componentWillMount() { @@ -107,13 +107,13 @@ export default class Carousel extends Component { return (
- +
{ this.props.children.map(carouselItem) }
- +
) diff --git a/components/Carousel/Carousel.scss b/components/Carousel/Carousel.scss index cb699725a..98f4ce479 100644 --- a/components/Carousel/Carousel.scss +++ b/components/Carousel/Carousel.scss @@ -1,45 +1,48 @@ -@import 'topcoder/tc-includes'; +@import '~tc-ui/src/styles/tc-includes'; $pager-bg-color: #737380; -.Carousel { - display: flex; - flex-direction: row; - - .page-down { - width: 20px; - margin-right: 15px; - background-color: $pager-bg-color; +:global { + .Carousel { display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - - &.hidden { - display: none; + flex-direction: row; + + .page-down { + width: 20px; + margin-right: 15px; + background-color: $pager-bg-color; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + + &.hidden { + display: none; + } } - } - - .page-up { - width: 20px; - background-color: $pager-bg-color; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - - &.hidden { - display: none; + + .page-up { + width: 20px; + background-color: $pager-bg-color; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + + &.hidden { + display: none; + } } - } - - .visible-area { - display: flex; - flex-direction: row; - overflow: hidden; - - .carousel-item:not(:first-child) { - margin-left: 30px; + + .visible-area { + display: flex; + flex-direction: row; + overflow: hidden; + + .carousel-item:not(:first-child) { + margin-left: 30px; + } } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/components/Carousel/CarouselExamples.scss b/components/Carousel/CarouselExamples.scss index d6d4c8b34..b09736905 100644 --- a/components/Carousel/CarouselExamples.scss +++ b/components/Carousel/CarouselExamples.scss @@ -1,17 +1,20 @@ -@import 'topcoder/tc-includes'; +@import '~tc-ui/src/styles/tc-includes'; -.Carousel { - .StandardListItem { - padding: 0px; +:global { + .Carousel { + .StandardListItem { + padding: 0px; + } } -} - -.CarouselExamples { - > p { - border: 1px solid $accent-gray; - margin: 20px 0px; + + .CarouselExamples { + > p { + border: 1px solid $tc-gray-40; + margin: 20px 0px; + } + .limited-width { + width: 200px; + } } - .limited-width { - width: 200px; - } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/components/Checkbox/Checkbox.scss b/components/Checkbox/Checkbox.scss index 02f1f6cac..548a96ada 100644 --- a/components/Checkbox/Checkbox.scss +++ b/components/Checkbox/Checkbox.scss @@ -1,15 +1,16 @@ -@import "work/work-includes"; -.Checkbox { - label { - margin-left: 10px; +:global { + .Checkbox { + label { + margin-left: 10px; + } + + .icon, button { + width : 24px; + height : 24px; + /* fixing jumping issues on webkit */ + outline : none; + overflow: hidden; + } } - - .icon, button { - width : 24px; - height : 24px; - /* fixing jumping issues on webkit */ - outline : none; - overflow: hidden; - } -} \ No newline at end of file +} diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index 2a06bfc35..381dedfe3 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -1,111 +1,186 @@ require('./Dropdown.scss') -import React, { Component, PropTypes } from 'react' +import React, { PropTypes } from 'react' +import classNames from 'classnames' +import enhanceDropdown from './enhanceDropdown' -class Dropdown extends Component { +class Dropdown extends React.Component { constructor(props) { super(props) - - this.state = { isHidden: true } - - this.onClickOutside = this.onClickOutside.bind(this) - this.onClick = this.onClick.bind(this) - this.onClickOtherDropdown = this.onClickOtherDropdown.bind(this) } - onClickOutside(evt) { - let currNode = evt.target - let isDropdown = false - - do { - if(currNode.className.indexOf('dropdown-wrap') > -1) { - isDropdown = true - break + render() { + const props = this.props + const { children, className, pointerShadow, noPointer, pointerLeft, isOpen, handleClick, theme, noAutoclose, handleKeyboardNavigation } = props + const ddClasses = classNames('dropdown-wrap', { + [`${className}`] : true, + [`${ theme }`] : true + }) + const ndClasses = classNames('Dropdown', { + 'pointer-shadow' : pointerShadow, + 'pointer-hide' : noPointer, + 'pointer-left' : pointerLeft, + 'no-autoclose' : noAutoclose, + hide : !isOpen + }) + + let childSelectionIndex = -1 + const focusOnNextChild = () => { + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + childSelectionIndex += 1 + if (childSelectionIndex >= listChild.length) { + childSelectionIndex -= 1 + } else { + listChild[childSelectionIndex].focus() } - - currNode = currNode.parentNode - - if(!currNode) - break - } while(currNode.tagName) - - if(!isDropdown) { - this.setState({ isHidden: true }) } - } - - onClick(evt) { - const dropdownClicked = new Event('dropdownClicked') - - document.dispatchEvent(dropdownClicked) - - this.setState({ isHidden: !this.state.isHidden }) - evt.stopPropagation() - } - - onClickOtherDropdown() { - this.setState({ isHidden: true }) - } - - componentDidMount() { - document.removeEventListener('click', this.onClickOutside) - document.removeEventListener('dropdownClicked', this.onClickOtherDropdown) - - document.addEventListener('click', this.onClickOutside) - document.addEventListener('dropdownClicked', this.onClickOtherDropdown) - } - - componentWillUnmount() { - document.removeEventListener('click', this.onClickOutside) - document.removeEventListener('dropdownClicked', this.onClickOtherDropdown) - } - - render() { - const pointerShadow = this.props.pointerShadow - const noPointer = this.props.noPointer - const pointerLeft = this.props.pointerLeft - let ndClasses = 'Dropdown' - - if (pointerShadow) { - ndClasses += ' pointer-shadow' + const focusOnPreviousChild = () => { + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + childSelectionIndex -= 1 + if (childSelectionIndex < 0) { + childSelectionIndex = 0 + } else { + listChild[childSelectionIndex].focus() + } } - - if (noPointer) { - ndClasses += ' pointer-hide' + let searchKey = '' + let timer + const focusOnCharacter = (value) => { + searchKey += value + if (timer) { + clearTimeout(timer) + } + timer = setTimeout(() => { searchKey = '' }, 500) + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + const length = listChild.length + for (let i = 0; i < length; i++) { + let textContent = listChild[i].textContent + if (textContent && textContent.length > 0) { + textContent = textContent.toLowerCase() + const search = searchKey.toLowerCase() + if (textContent.startsWith(search)) { + childSelectionIndex = i + listChild[i].focus() + return true + } + } + } + return false } - - if (pointerLeft) { - ndClasses += ' pointer-left' + const onFocus = () => { + this.containerRef.classList.add('focused') } - - if (this.state.isHidden) { - ndClasses += ' hide' + const onBlur = () => { + this.containerRef.classList.remove('focused') } + const onKeydown = (e) => { + if (!handleKeyboardNavigation) { + return + } + const keyCode = e.keyCode + if (keyCode === 32 || keyCode === 38 || keyCode === 40) { // space or Up/Down + // open dropdown menu + if (!noAutoclose && !isOpen) { + e.preventDefault() + handleClick(event) + } else { + if (keyCode === 40) { + focusOnNextChild() + } else if (keyCode === 38) { + focusOnPreviousChild() + } + e.preventDefault() + } + } else if (isOpen) { + const value = String.fromCharCode(e.keyCode) + if (focusOnCharacter(value)) { + e.preventDefault() + } + } + } + const onChildKeydown = (e) => { + if (!handleKeyboardNavigation) { + return + } + const keyCode = e.keyCode + if (keyCode === 38 || keyCode === 40 || keyCode === 13) { // Up/Down or enter + if (keyCode === 40) { + focusOnNextChild() + } else if (keyCode === 38) { + focusOnPreviousChild() + } else if (keyCode === 13) { // enter + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + listChild[childSelectionIndex].click() + this.handleKeyboardRef.focus() + } + e.preventDefault() + } else { + const value = String.fromCharCode(e.keyCode) + if (focusOnCharacter(value)) { + e.preventDefault() + } + } + } + + const setListRef = (c) => this.listRef = c + const setContainerRef = (c) => this.containerRef = c + const setHandleKeyboardRef = (c) => this.handleKeyboardRef = c + const childrenWithProps = React.Children.map(children, child => + React.cloneElement(child, {onKeyDown: onChildKeydown}) + ) return ( -
+
{ } : handleClick}> + {handleKeyboardNavigation && ()} { - this.props.children.map((child) => { - if(child.props.className === 'dropdown-menu-header') - return child + childrenWithProps.map((child, index) => { + if (child.props.className.indexOf('dropdown-menu-header') > -1) + return noAutoclose ? React.cloneElement(child, { + onClick: handleClick, + key: child.props.key || index + }) : child }) } - -
+
{ - this.props.children.map((child) => { - if(child.props.className === 'dropdown-menu-list') + childrenWithProps.map((child) => { + if (child.props.className.indexOf('dropdown-menu-list') > -1) return child }) }
) + } } Dropdown.propTypes = { - children: PropTypes.array.isRequired + children: PropTypes.array.isRequired, + /* + If true, prevents dropdown closing when clicked inside dropdown + */ + noAutoclose: PropTypes.bool, + /* + If true, prevents handle keyboard event + */ + handleKeyboardNavigation: PropTypes.bool +} + +Dropdown.defaultProps = { + handleKeyboardNavigation: false } -export default Dropdown +export default enhanceDropdown(Dropdown) diff --git a/components/Dropdown/Dropdown.scss b/components/Dropdown/Dropdown.scss index 876e386de..86a5bc9c1 100644 --- a/components/Dropdown/Dropdown.scss +++ b/components/Dropdown/Dropdown.scss @@ -1,59 +1,188 @@ -@import "topcoder/tc-includes"; +@import '~tc-ui/src/styles/tc-includes'; -.Dropdown { - margin-top: 30px; - background-color: #fff; - box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.17); - border-radius: 5px; - display: inline-block; - position: absolute; - left: 0; - width: 100%; - ul { - height: 100%; - width: 100%; +:global { + .dropdown-wrap { + cursor: pointer; position: relative; - z-index: 10; + } + + .Dropdown { background-color: #fff; - padding: 11px 20px; + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.17); border-radius: 5px; - - li a { - color: #394146; - font-size: "Roboto", Arial, Helvetica, sans-serif; - font-size: 12px; - line-height: 26px; + display: inline-block; + position: absolute; + left: 0; + top: 5px; + width: 100%; + z-index: 2; + + ul { + height: 100%; + width: 100%; + position: relative; + z-index: 10; + background-color: #fff; + padding: 11px 20px; + border-radius: 5px; + + li { + list-style: none; + } + + li a { + color: #394146; + font-family: "Roboto", Arial, Helvetica, sans-serif; + font-size: 12px; + display: block; + line-height: 26px; + } } } -} - -.Dropdown.hide { - display: none; -} - -.Dropdown.pointer-left:before { - right: initial; - left: 15px; -} + + .dropdown-wrap.default { + border: 1px solid $tc-gray-20; + display: flex; + align-items: center; + padding: calc(2 * #{$base_unit}); + position: relative; + + .Dropdown { + ul.dropdown-menu-list { + padding: 10px 0px; + li { + padding: 0 20px; + @include ellipsis; + } + + li:focus, + li:hover { + background-color: $tc-gray-neutral-dark; + outline: none; + } + } + } + } + + .dropdown-wrap.default::after { + content: " "; + width: 10px; + height: 10px; + display: block; + right: 10px; + top: 50%; + position: absolute; + transform: translateY(-50%) rotate(45deg); + border-bottom: 2px solid $tc-gray-20; + border-right: 2px solid $tc-gray-20; + } -.Dropdown:before { - content: " "; - width: 15px; - height: 15px; - display: block; - transform: rotate(45deg); - background-color: #fff; - position: absolute; - right: 15px; - top: -6px; - border-radius: 5px; -} + .dropdown-wrap { + &.focused { + box-shadow: 0 0 2px 0 rgba(6, 129, 255, 0.7); + border: 1px solid $tc-dark-blue-100!important; + } + .handle-keyboard { + position: absolute; + width: 100%; + max-height: 40px; + top: 0; + left: 0; + height: 100%; -.Dropdown.pointer-hide:before { - display: none; + &:focus { + outline: none; + } + } + } + + .Dropdown.hide { + display: none; + } + + .Dropdown.pointer-left:before { + right: initial; + left: 15px; + } + + .UserDropdownMenu .Dropdown.pointer-shadow { + margin-top: 35px; + + &:before { + content: ''; + display: block; + position: absolute; + top: -6px; + right: 24px; + width: 12px; + height: 12px; + background: #FFFFFF; + border-right: 1px solid $tc-gray-20;; + border-bottom: 1px solid $tc-gray-20;; + transform: rotate(-135deg); + z-index:999; + } + } + + .Dropdown.pointer-hide:before { + display: none; + } + + .Dropdown.no-autoclose { + cursor: default; + } + + .new-theme { + text-align: left; + height: 30px; + color: $tc-black; + background: $tc-gray-neutral-light; + border: 1px solid $tc-gray-20; + @include roboto; + font-size: 13px; + line-height: 20px; + width: 100%; + border-radius: 2px; + position: relative; + + .dropdown-menu-header { + width: 100%; + border: 0; + height: 28px; + line-height: 28px; + margin: 0; + padding: 0 0 0 10px; + color: $tc-gray-50; + font-size: 13px; + } + &:after{ + display: block; + content: ''; + position: absolute; + width: 10px; + height: 14px; + right: 11px; + top: 50%; + margin-top: -7px; + background: url("./icon-select.png") left top no-repeat; + background-size: 10px 14px; + z-index:2; + } + .Dropdown { + ul.dropdown-menu-list { + padding: 10px 0px; + li { + padding: 0 20px; + @include ellipsis; + } + li:focus, + li:hover { + background-color: $tc-gray-neutral-dark; + outline: none; + } + } + } + } } - -.Dropdown.pointer-shadow:before { - box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.17); -} \ No newline at end of file + \ No newline at end of file diff --git a/components/Dropdown/DropdownExamples.jsx b/components/Dropdown/DropdownExamples.jsx index e6324037f..33af951d6 100644 --- a/components/Dropdown/DropdownExamples.jsx +++ b/components/Dropdown/DropdownExamples.jsx @@ -19,7 +19,7 @@ const DropdownExamples = { @@ -32,7 +32,7 @@ const DropdownExamples = { @@ -45,7 +45,7 @@ const DropdownExamples = { @@ -58,7 +58,7 @@ const DropdownExamples = { @@ -71,7 +71,7 @@ const DropdownExamples = { @@ -84,7 +84,7 @@ const DropdownExamples = { diff --git a/components/Dropdown/DropdownExamples.scss b/components/Dropdown/DropdownExamples.scss index b86dd3e10..21075cd66 100644 --- a/components/Dropdown/DropdownExamples.scss +++ b/components/Dropdown/DropdownExamples.scss @@ -1,31 +1,33 @@ -/** - * Containers of dropdown should be positioned relative so that the dropdown stay within the bounds of it - * Pointer is positioned 15px from the right by default, but this can be overriden in styling - */ - -.dropdown-example { - margin-bottom: 50px; - position: relative; - padding: 15px; - background-color: #DDD; +:global { + /** + * Containers of dropdown should be positioned relative so that the dropdown stay within the bounds of it + * Pointer is positioned 15px from the right by default, but this can be overriden in styling + */ + + .dropdown-example { + margin-bottom: 50px; + position: relative; + padding: 15px; + background-color: #DDD; + } + + .full-width { + width: 100%; + + .Dropdown:before { + right: initial; + left: 30px; + } + } + + .limited-width { + width: 25%; + display: inline-block; + margin-right: 25px; + text-align: right; + } + + .limited-width.pointer-left-example { + text-align: left; + } } - -.full-width { - width: 100%; - - .Dropdown:before { - right: initial; - left: 30px; - } -} - -.limited-width { - width: 25%; - display: inline-block; - margin-right: 25px; - text-align: right; -} - -.limited-width.pointer-left-example { - text-align: left; -} \ No newline at end of file diff --git a/components/Dropdown/DropdownItem.jsx b/components/Dropdown/DropdownItem.jsx new file mode 100644 index 000000000..eb8169e1d --- /dev/null +++ b/components/Dropdown/DropdownItem.jsx @@ -0,0 +1,34 @@ +import React, { PropTypes } from 'react' +import cn from 'classnames' + +const DropdownItem = ({item, onItemClick, currentSelection}) => { + const _onClick = () => onItemClick(item.val) + const activeClass = cn({ + active: item.val === currentSelection + }) + return ( +
  • + { item.label } +
  • + ) +} + +DropdownItem.propTypes = { + // item must have at least these 2 properties + item: PropTypes.shape({ + // label that is displayed in the dropdown + label: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.element + ]).isRequired, + // value to be provided when item is clicked + val: PropTypes.any.isRequired + }).isRequired, + // function to be invoked when an item is clicked + onItemClick: PropTypes.func.isRequired, + // current selection used to set active class + currentSelection: PropTypes.any +} + +export default DropdownItem diff --git a/components/Dropdown/arrow-dropdown.png b/components/Dropdown/arrow-dropdown.png new file mode 100644 index 000000000..2aa5059e9 Binary files /dev/null and b/components/Dropdown/arrow-dropdown.png differ diff --git a/components/Dropdown/enhanceDropdown.js b/components/Dropdown/enhanceDropdown.js new file mode 100644 index 000000000..e725c3dba --- /dev/null +++ b/components/Dropdown/enhanceDropdown.js @@ -0,0 +1,111 @@ +import React, { Component } from 'react' + +const enhanceDropdown = (CompositeComponent) => class extends Component { + constructor(props) { + super(props) + this.state = { isOpen: false } + this.handleClick = this.handleClick.bind(this) + // this.onSelect = this.onSelect.bind(this) + this.onClickOutside = this.onClickOutside.bind(this) + this.onClickOtherDropdown = this.onClickOtherDropdown.bind(this) + this.refreshEventHandlers = this.refreshEventHandlers.bind(this) + } + + refreshEventHandlers() { + if (this.state.isOpen) { + document.addEventListener('click', this.onClickOutside) + document.addEventListener('touchstart', this.onClickOutside) + document.addEventListener('dropdownClicked', this.onClickOtherDropdown) + } else { + document.removeEventListener('click', this.onClickOutside) + document.removeEventListener('touchstart', this.onClickOutside) + document.removeEventListener('dropdownClicked', this.onClickOtherDropdown) + } + } + + handleClick() { + const dropdownClicked = document.createEvent('Event') + dropdownClicked.initEvent('dropdownClicked', true, false) + + document.dispatchEvent(dropdownClicked) + + this.setState({ isOpen: !this.state.isOpen }, () => { + this.refreshEventHandlers() + }) + } + + // onSelect(value) { + // this.handleClick() + // if (this.props.onSelect) this.props.onSelect(value) + // } + + onClickOutside(evt) { + let currNode = evt.target + let isDropdown = false + console.log('onClickOutside') + + do { + if (currNode.className + && currNode.className.indexOf + && currNode.className.indexOf('dropdown-wrap') > -1) { + isDropdown = true + break + } + + currNode = currNode.parentNode + + if (!currNode) + break + } while (currNode.tagName) + + if (!isDropdown) { + this.setState({ isOpen: false }, () => { + this.refreshEventHandlers() + }) + } + } + + onClickOtherDropdown() { + this.setState({ isOpen: false }, () => { + this.refreshEventHandlers() + }) + } + + componentDidMount() { + document.removeEventListener('click', this.onClickOutside) + document.removeEventListener('touchstart', this.onClickOutside) + document.removeEventListener('dropdownClicked', this.onClickOtherDropdown) + + if (this.state.isOpen) { + document.addEventListener('click', this.onClickOutside) + document.addEventListener('touchstart', this.onClickOutside) + document.addEventListener('dropdownClicked', this.onClickOtherDropdown) + } + } + + componentWillUnmount() { + document.removeEventListener('click', this.onClickOutside) + document.removeEventListener('touchstart', this.onClickOutside) + document.removeEventListener('dropdownClicked', this.onClickOtherDropdown) + } + + stopEventPropagation(e) { + e.stopPropagation() + } + + render() { + const { isOpen } = this.state + return ( +
    + +
    + ) + } +} + +export default enhanceDropdown diff --git a/components/Dropdown/icon-select.png b/components/Dropdown/icon-select.png new file mode 100644 index 000000000..8b58102b3 Binary files /dev/null and b/components/Dropdown/icon-select.png differ diff --git a/components/ExampleApp/ExampleApp.scss b/components/ExampleApp/ExampleApp.scss index 703006c42..6b540a01d 100644 --- a/components/ExampleApp/ExampleApp.scss +++ b/components/ExampleApp/ExampleApp.scss @@ -1,18 +1,21 @@ -@import "work/work-styles"; +@import '~tc-ui/src/styles/tc-styles'; -body { - background-color: $grey-lighter; - padding: 15px; - - .invisible { - opacity: .3; - visibility: visible; - } - - h1 { - margin : 80px 0 40px 0; - text-align : center; - padding-bottom: 10px; - border-bottom : 1px solid $grey-light; +:global { + body { + background-color: $tc-gray-neutral-light; + padding: 15px; + + .invisible { + opacity: .3; + visibility: visible; + } + + h1 { + margin : 80px 0 40px 0; + text-align : center; + padding-bottom: 10px; + border-bottom : 1px solid $tc-gray-neutral-dark; + } } } + \ No newline at end of file diff --git a/components/ExampleComponent/ExampleComponent.scss b/components/ExampleComponent/ExampleComponent.scss index e8dd37392..c4cb24064 100644 --- a/components/ExampleComponent/ExampleComponent.scss +++ b/components/ExampleComponent/ExampleComponent.scss @@ -1,35 +1,38 @@ -// Whenever you need predefined color variables or mixins -// Add the following line to the top of your .scss file -@import 'topcoder/tc-includes'; - -// Declare local variables, if necessary -$peaches: #ffdab9; - -/* Please do not overly nest (except media queries): -** ul { -** &.item-list { -** li { -** &.peaches-color { -** background-color: $white; -** } -** } -** } -** } -*/ - -// Prefer flat over nested structure -.item-list { - background-color: $white; -} - -.peaches-color { - color: $peaches -} - -// Always nest media queries -.on-sale { - border: 3px solid $primary; - @media screen and (max-width: 700px) { - border-width: 2px; +:global { + // Whenever you need predefined color variables or mixins + // Add the following line to the top of your .scss file + @import '~tc-ui/src/styles/tc-includes'; + + // Declare local variables, if necessary + $peaches: #ffdab9; + + /* Please do not overly nest (except media queries): + ** ul { + ** &.item-list { + ** li { + ** &.peaches-color { + ** background-color: $white; + ** } + ** } + ** } + ** } + */ + + // Prefer flat over nested structure + .item-list { + background-color: white; + } + + .peaches-color { + color: $peaches + } + + // Always nest media queries + .on-sale { + border: 3px solid $tc-dark-blue; + @media screen and (max-width: 700px) { + border-width: 2px; + } } } + \ No newline at end of file diff --git a/components/ExampleNav/ExampleNav.jsx b/components/ExampleNav/ExampleNav.jsx index 93703cfd5..2a023100c 100644 --- a/components/ExampleNav/ExampleNav.jsx +++ b/components/ExampleNav/ExampleNav.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' require('./ExampleNavStyle.scss') diff --git a/components/ExampleNav/ExampleNavContainer.js b/components/ExampleNav/ExampleNavContainer.js index ded1d026c..c748f0a6b 100644 --- a/components/ExampleNav/ExampleNavContainer.js +++ b/components/ExampleNav/ExampleNavContainer.js @@ -25,11 +25,19 @@ const navs = { 'LoaderExamples', 'PanelExamples', 'StandardListItemExamples', - 'TooltipExamples' + 'TooltipExamples', + 'RadioGroupExample' ], ManageSteps: [ 'ManageStepsExamples', 'StepRowExamples' + ], + RichDataTable: [ + 'RichDataTableExample' + ], + Screen: [ + 'LoginScreenExamples', + 'WizardExamples' ] } diff --git a/components/ExampleNav/ExampleNavStyle.scss b/components/ExampleNav/ExampleNavStyle.scss index a13a8a4a5..b2793fe03 100644 --- a/components/ExampleNav/ExampleNavStyle.scss +++ b/components/ExampleNav/ExampleNavStyle.scss @@ -1,21 +1,24 @@ -@import "work/work-includes"; +@import '~tc-ui/src/styles/tc-includes'; -.ExampleNav { - position: fixed; - bottom: 0; - background-color: #fff; - opacity: 0.6; - - li { - display: inline-block; - } - - a { - margin: 10px; - display: inline-block; - } - - .back { - color: $grey-dark; +:global { + .ExampleNav { + position: fixed; + bottom: 0; + background-color: #fff; + opacity: 0.6; + + li { + display: inline-block; + } + + a { + margin: 10px; + display: inline-block; + } + + .back { + color: $tc-gray-30; + } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/components/FilePicker/FilePicker.jsx b/components/FilePicker/FilePicker.jsx new file mode 100644 index 000000000..7ad8a1618 --- /dev/null +++ b/components/FilePicker/FilePicker.jsx @@ -0,0 +1,119 @@ +import React, {PropTypes} from 'react' +import _ from 'lodash' +import * as filepicker from 'filestack-js' + +require('./FilePicker.scss') + +class FilePicker extends React.Component { + constructor(props) { + super(props) + this.state = {dragText: props.options.dragText} + this.onChange = this.onChange.bind(this) + } + + onChange(event) { + this.props.onSuccess(this.props.options.multiple ? event.fpfiles : event.fpfile) + } + + componentWillReceiveProps(nextProps) { + this.setState({dragText: nextProps.options.dragText}) + } + + componentDidMount() { + + const filepickerElement = this.refs.filepicker + const filepickerButton = this.refs.filepickerButton + const filepickerProgress = this.refs.filepickerProgress + + const apikey = this.props.apiKey + const clientOptions = {} + if (this.props.options.cname) { clientOptions.cname = this.props.options.cname } + const client = filepicker.init(apikey, clientOptions) + + const opts = {} + opts.displayMode = 'dropPane' + opts.container = 'filepicker-drag-drop-pane' + opts.maxFiles = 4 + + opts.storeTo = {} + opts.storeTo.container = this.props.options.storeContainer + opts.storeTo.region = 'us-east-1' + + opts.dropPane = {} + opts.dropPane.customText = ' ' + opts.dropPane.overlay = false + opts.dropPane.showIcon = false + opts.dropPane.disableClick = true + opts.dropPane.onDragEnter = () => { + this.setState({dragText: 'Drop to upload'}) + filepickerElement.classList.add('drag-entered') + } + opts.dropPane.onDragLeave = () => { + this.setState({dragText: this.props.options.dragText}) + filepickerElement.classList.remove('drag-entered') + } + opts.dropPane.onSuccess = (files) => { + this.setState({dragText: this.props.options.dragText}) + filepickerElement.classList.remove('in-progress') + this.props.onSuccess(this.props.options.multiple ? files : files[0]) + } + opts.dropPane.onError = () => { + filepickerElement.classList.remove('in-progress') + this.setState({dragText: this.props.options.dragText}) + } + opts.dropPane.onProgress = (percentage) => { + filepickerElement.classList.remove('drag-entered') + filepickerElement.classList.add('in-progress') + filepickerProgress.style.width = percentage + '%' + } + opts.dropPane.onClick = () => { + const overlayOpts = {} + overlayOpts.maxFiles = opts.maxFiles + overlayOpts.uploadInBackground = false + overlayOpts.onFileUploadFinished = (files) => { + this.props.onSuccess(this.props.options.multiple ? files : files[0]) + } + overlayOpts.storeTo = opts.storeTo + overlayOpts.fromSources = this.props.options.fromSources + client.picker(overlayOpts).open() + } + + client.picker(opts).open(); + + filepickerElement.addEventListener('change', this.onChange, false) + } + + componentWillUnmount() { + this.refs.filepicker.removeEventListener('change', this.onChange, false) + } + + render() { + const {mode, options} = this.props + const {dragText} = this.state + + // add data-fp- prefix to all keys + const opts = _.mapKeys(options, (v, k) => { + const hyphenated = k.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase() + return `data-fp-${hyphenated}` + }) + return ( +
    + +
    + { dragText } + +
    +
    +
    + ) + } +} + +FilePicker.propTypes = { + apiKey: PropTypes.string.isRequired, + mode: PropTypes.string.isRequired, + options: PropTypes.object.isRequired, + onSuccess: PropTypes.func.isRequired +} + +export default FilePicker diff --git a/components/FilePicker/FilePicker.scss b/components/FilePicker/FilePicker.scss new file mode 100644 index 000000000..9b91cc9c9 --- /dev/null +++ b/components/FilePicker/FilePicker.scss @@ -0,0 +1,62 @@ +@import '~tc-ui/src/styles/tc-includes'; + + + +:global { + .filepicker { + position: relative; + height: 130px; + + .filepicker-drag-drop-pane { + @include roboto-medium; + position: absolute; + display: block; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: $base-unit*4; + background: $tc-gray-neutral-light; + font-size: $tc-label-lg; + color: $tc-gray-80; + text-align: center; + border: 1px dashed $tc-gray-40; + } + + .filepicker-progress { + display: none; + } + + &.in-progress { + .filepicker-progress { + display: block; + } + + .filepicker-drag-drop-text, + .filepicker-picker + button { + display: none; + } + } + + &.drag-entered { + .filepicker-drag-drop-pane { + border: 1px solid $tc-gray-80; + } + } + + .filepicker-progress { + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 0; + background-color: $tc-dark-blue-70; + } + + .fsp-drop-pane__container { + background-color: transparent; + border-style: none; + } + } +} + \ No newline at end of file diff --git a/components/FileUploader/FileUploaderStyles.scss b/components/FileUploader/FileUploaderStyles.scss index 6d40856b8..973be615a 100644 --- a/components/FileUploader/FileUploaderStyles.scss +++ b/components/FileUploader/FileUploaderStyles.scss @@ -1,36 +1,39 @@ -@import "work/work-includes"; +@import '~tc-ui/src/styles/tc-includes'; -.FileUploader { - position: relative; - - .Loader { - z-index: 1; - } - - .UploadedFiles { - margin-bottom: 20px; - } - - .Dropzone { - display: inline-block; - } - - .drag-and-drop { +:global { + .FileUploader { + position: relative; + + .Loader { + z-index: 1; + } + + .UploadedFiles { + margin-bottom: 20px; + } + .Dropzone { - text-align: center; - border : 3px dashed $grey; - margin : 10px; - display : block; - - p { - font-size: 20px; - margin: 30px 0px; - color: $grey; + display: inline-block; + } + + .drag-and-drop { + .Dropzone { + text-align: center; + border : 3px dashed $tc-gray-20; + margin : 10px; + display : block; + + p { + font-size: 20px; + margin: 30px 0px; + color: $tc-gray-20; + } } } + + .dropzone-container { + text-align: center; + } } - - .dropzone-container { - text-align: center; - } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/components/Forms/images/check-white.svg b/components/Forms/images/check-white.svg new file mode 100644 index 000000000..bd787b05a --- /dev/null +++ b/components/Forms/images/check-white.svg @@ -0,0 +1,16 @@ + + + + Fill 89 + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/components/Formsy/Checkbox.jsx b/components/Formsy/Checkbox.jsx new file mode 100644 index 000000000..7136cc6fa --- /dev/null +++ b/components/Formsy/Checkbox.jsx @@ -0,0 +1,52 @@ +import React, { Component } from 'react' +import { HOC as hoc } from 'formsy-react' +import classNames from 'classnames' + +class Checkbox extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + } + + changeValue(e) { + const value = e.target.checked + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + render() { + const { label, name } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const classes = classNames('tc-checkbox', {error: hasError}) + const disabled = this.props.isFormDisabled() || this.props.disabled + const errorMessage = this.props.getErrorMessage() || this.props.validationError + const setRef = (c) => this.element = c + const groupClasses = classNames('checkbox-group-item', {'checkbox-item-checked': this.props.getValue()===true}) + + return ( +
    +
    + +
    + + { hasError ? (

    {errorMessage}

    ) : null} +
    + ) + } +} + +Checkbox.defaultProps = { + onChange: () => {} +} + +export default hoc(Checkbox) diff --git a/components/Formsy/CheckboxGroup.jsx b/components/Formsy/CheckboxGroup.jsx new file mode 100644 index 000000000..e21e1d4c3 --- /dev/null +++ b/components/Formsy/CheckboxGroup.jsx @@ -0,0 +1,83 @@ +import React, { Component, PropTypes } from 'react' +import { HOC as hoc } from 'formsy-react' +import cn from 'classnames' +import { numberWithCommas } from './format' + +class CheckboxGroup extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + } + + changeValue() { + const value = [] + this.props.options.forEach((option, key) => { + if (this['element-' + key].checked) { + value.push(option.value) + } + }) + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + render() { + const { label, name, options, layout, wrapperClass } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const disabled = this.props.isFormDisabled() || this.props.disabled + const errorMessage = this.props.getErrorMessage() || this.props.validationError + + const renderOption = (cb, key) => { + const curValue = this.props.getValue() || [] + const checked = curValue.indexOf(cb.value) !== -1 + const disabled = this.props.isFormDisabled() || cb.disabled || this.props.disabled + const rClass = cn('checkbox-group-item', { disabled, selected: checked }) + const id = name+'-opt-'+key + const setRef = (c) => this['element-' + key] = c + return ( +
    +
    + +
    + + { + cb.quoteUp && !checked &&
    {`+ $${numberWithCommas(cb.quoteUp)}`}
    + } + { + cb.description && checked &&
    {cb.description}
    + } +
    + ) + } + const chkGrpClass = cn('checkbox-group', wrapperClass, { + horizontal: layout === 'horizontal', + vertical: layout === 'vertical' + }) + return ( +
    + +
    {options.map(renderOption)}
    + { hasError ? (

    {errorMessage}

    ) : null} +
    + ) + } +} + +CheckboxGroup.PropTypes = { + options: PropTypes.arrayOf(PropTypes.object).isRequired +} + +CheckboxGroup.defaultProps = { + onChange: () => {} +} + +export default hoc(CheckboxGroup) diff --git a/components/Formsy/FormFields.scss b/components/Formsy/FormFields.scss new file mode 100644 index 000000000..e4b828675 --- /dev/null +++ b/components/Formsy/FormFields.scss @@ -0,0 +1,328 @@ +@import '~tc-ui/src/styles/tc-includes'; + +:global { + .row { + margin-bottom: 20px; + &.center{ + display: table; + margin: 0 auto; + label{ + display: inline; + } + input{ + display: inline; + width: 100px; + } + } + } + label{ + display:block; + margin: 10px auto; + text-transform: none; + @include tc-label-md; + color: $tc-gray-70; + font-size: 13px; + } + + .error-message{ + display:block; + margin: 5px auto; + @include roboto; + + color: $tc-gray-70; + font-size: 13px; + line-height:20px; + font-style:italic; + border: 1px solid $tc-red-30; + background: $tc-red-10; + color: $tc-red-70; + padding:10px; + border-radius: 2px; + strong{ + @include roboto-bold; + } + } + + input { + display:block; + margin: 0 auto; + color: $tc-black; + background: $tc-gray-neutral-light; + border-color: $tc-gray-20; + @include tc-label-md; + + &[disabled]{ + color: $tc-gray-20; + background: $tc-white; + } + + &:hover{ + border-color: $tc-gray-40; + background: $tc-gray-neutral-light; + } + + &:focus{ + background: $tc-white!important; + border-color: $tc-dark-blue-100!important; + } + + &.error{ + border: 1px solid $tc-red-70!important; + background: $tc-gray-neutral-light!important; + &:focus{ + background: $tc-white!important; + // border-color: $tc-dark-blue-100!important; + } + } + } + + textarea{ + display:block; + margin: 0 auto; + height: 100px; + color: $tc-black; + background: $tc-gray-neutral-light; + border-color: $tc-gray-20; + @include tc-label-md; + font-size:15px; + line-height:20px; + box-shadow: none; + + &[disabled]{ + color: $tc-gray-20; + background: $tc-white; + } + + &:hover{ + border-color: $tc-gray-40; + } + + &:focus{ + background: $tc-white!important; + border-color: $tc-dark-blue-100!important; + } + + &.error{ + background: $tc-gray-neutral-light!important; + border: 1px solid $tc-red-70!important; + &:focus{ + background: $tc-white!important; + // border-color: $tc-dark-blue-100!important; + } + } + } + + .checkbox-group .checkbox-group-options { + display: flex; + flex-direction: column; + } + .checkbox-group.horizontal .checkbox-group-options { + flex-direction: row; + justify-content: space-between; + } + .checkbox-group.vertical .checkbox-group-options { + flex-direction: column; + } + + .checkbox-group-item { + display: inline-block; + margin-right: 42px; + label { + @include roboto-bold; + color: $tc-gray-80; + margin-right: 0; + } + } + + .radio-group-input { + .radio-group-label { + @include tc-label-md; + margin-right: 20px; + } + .radio-group-options { + display: flex; + flex-direction: row; + .radio { + margin: 0; + input[type="radio"] { + opacity: 0; + display: none; + } + label { + display: inline-block; + line-height: 19px; + padding-left: 26px; + position: relative; + @include roboto-bold; + font-size:14px; + color: $tc-black; + margin-right: 30px; + &:before { + border: 1px solid $tc-gray-20; + border-radius: 50%; + background-color: #fff; + content: ""; + display: inline-block; + height: 19px; + left: 0; + position: absolute; + top: 0; + width: 19px; + } + } + input[type="radio"]:checked + label:after { + background: $tc-dark-blue-100; + border-radius: 50%; + content: ""; + display: inline-block; + left: 4px; + position: absolute; + top: 4px; + height: 11px; + width: 11px; + } + } + } + } + + .tiled-group-row { + display: flex; + flex-flow: row wrap; + justify-content: center; + } + a.tiled-group-item { + position: relative; + -webkit-flex: initial; + flex: initial; + margin-right: 20px; + height:155px; + width:135px; + border: 1px solid $tc-gray-20; + border-radius: 4px; + cursor: pointer; + border: 1px solid #dcdce0; + border-radius: 4px; + display: inline-block; + margin: 10px 10px 0; + position: relative; + vertical-align: top; + + &:hover{ + border: 1px solid $tc-dark-blue-70; + } + + &.active{ + background: $tc-gray-10; + border-color: $tc-gray-10; + &:after { + content: ''; + display: block; + position: absolute; + top: 10px; + right: 10px; + width: 15px; + height: 15px; + background: #1A85FF; + border-radius: 2px; + } + &:before { + content: ''; + position: absolute; + z-index: 1; + top: 12px; + right: 15px; + width: 5px; + border-width: 0 2px 2px 0; + border-style: solid; + border-color: #fff; + height: 10px; + transform: rotate(45deg); + -o-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + } + &:hover { + border-color: $tc-gray-10; + } + } + + + span.title{ + display:block; + margin-top: 20px; + @include roboto-bold; + color: $tc-black; + font-size: 13px; + text-align:center; + } + + span.icon { + text-align: center; + display:block; + height: 50px; + margin:30px auto 20px auto; + } + small { + display:block; + font-size: 11px; + text-align:center; + color: $tc-gray-50; + @include roboto; + margin-top:10px; + } + .check-mark { + text-align: center; + background: $tc-dark-blue-100; + border-radius: 2px; + display: none; + height: 15px; + position: absolute; + right: 15px; + top: 15px; + width: 15px; + } + } + + .SliderRadioGroup { + margin: 25px auto 0px auto; + .rc-slider-dot, + .rc-slider-handle { + background: $tc-white; + border: 4px solid $tc-gray-10; + border-radius: 18px; + width: 20px; + height: 20px; + bottom: -7px; + } + + .rc-slider-handle { + border-color: $tc-dark-blue-100; + margin-left: -4px; + bottom: -2px; + display: none; + } + + &:not(.null-value) .rc-slider-dot-active { + border: none; + background: $tc-dark-blue-100 url('./images/check-white.svg') no-repeat 2px 3px; + // bottom: -2px; + // margin-left: -5px; + } + + .rc-slider-track, + .rc-slider-rail { + background-color: $tc-gray-10; + } + + .rc-slider-mark { + top: -30px; + .rc-slider-mark-text { + @include tc-label-lg; + line-height: 5 * $base_unit; + color: $tc-gray-80; + letter-spacing: 0; + } + } + } +} + \ No newline at end of file diff --git a/components/Formsy/PasswordInput.jsx b/components/Formsy/PasswordInput.jsx new file mode 100644 index 000000000..8253f05f5 --- /dev/null +++ b/components/Formsy/PasswordInput.jsx @@ -0,0 +1,143 @@ +import React, { Component } from 'react' +import PT from 'prop-types' +import { HOC as hoc } from 'formsy-react' +import classNames from 'classnames' + +import HelpIcon from '../HelpIcon/HelpIcon' +import IconUICheckSimple from '../Icons/IconUICheckSimple' + +import styles from './PasswordInput.scss' + +class PasswordInput extends Component { + + constructor(props) { + super(props) + + this.changeValue = this.changeValue.bind(this) + this.toggleShowHide = this.toggleShowHide.bind(this) + this.isValidInput= this.isValidInput.bind(this) + this.onFocus= this.onFocus.bind(this) + this.onBlur= this.onBlur.bind(this) + this.state = { + isShowPassword: false, + type: 'password', + isFocus: false + } + } + + onFocus() { + this.setState({isFocus: true}) + } + + onBlur() { + this.setState({isFocus: false}) + } + + isValidInput() { + const value = this.props.getValue() + const hasError = !this.props.isPristine() && !this.props.isValid() + return (!this.props.forceErrorMessage && value && !hasError) + } + + toggleShowHide() { + this.setState({ + isShowPassword: !this.state.isShowPassword, + type: this.state.isShowPassword ? 'password' : 'text' + }) + } + + changeValue(e) { + const value = e.target.value + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + render() { + const { label, name, minValue, maxValue, placeholder, wrapperClass, maxLength, theme, + labelHelpTooltip, readonly, readonlyValueTooltip, showCheckMark } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const disabled = this.props.isFormDisabled() || this.props.disabled + const wrapperClasses = classNames(wrapperClass, theme, { + [styles['readonly-wrapper']]: readonly, + 'password-input-container': true, + focus: this.state.isFocus + }) + const classes = classNames('tc-file-field__inputs', {error: hasError}, {empty: this.props.getValue() === ''}) + const errorMessage = this.props.getErrorMessage() || this.props.validationError + + return ( +
    + + + {this.isValidInput() && showCheckMark && ( + + )} +
    + {this.state.isShowPassword ? 'Hide' : 'Show'} +
    + {readonly && ( +
    + {this.props.getValue()} + {readonlyValueTooltip && } +
    + )} + + { hasError ? (

    {errorMessage}

    ) : (this.props.forceErrorMessage && (

    {this.props.forceErrorMessage}

    ))} +
    + ) + } +} + +PasswordInput.defaultProps = { + onChange: () => {}, + forceErrorMessage: null, + showCheckMark: false +} + +PasswordInput.propTypes = { + /** + * The difference from `disabled` is that instead of showing disabled input + * we show value using
    which let us position something immediately after the value + */ + readonly: PT.bool, + + /** + * Show help icon next to the label with the tooltip defined by this prop + */ + labelHelpTooltip: PT.node, + + /** + * Show help icon next to the value with the tooltip defined by this prop + * This only has any effect if `readonly` is set to `true` + */ + readonlyValueTooltip: PT.node, + + /** + * Show error message without any condition + */ + forceErrorMessage: PT.string, + + /** + * should show check mark icon when valid input + */ + showCheckMark: PT.bool + +} + +export default hoc(PasswordInput) diff --git a/components/Formsy/PasswordInput.scss b/components/Formsy/PasswordInput.scss new file mode 100644 index 000000000..c1b305891 --- /dev/null +++ b/components/Formsy/PasswordInput.scss @@ -0,0 +1,54 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.readonly-wrapper { + :global(input.tc-file-field__inputs) { + display: none; + } +} + +.readonly-value { + color: $tc-gray-60; + display: block; + height: 40px; + line-height: 40px; + margin-bottom: 2 * $base-unit; + padding: 0 2 * $base-unit; + width: 100%; +} + + +:global { + .password-input-container { + position: relative; + + &.focus { + .show-hide-button { + background: $tc-dark-blue-70; + } + } + + .tc-file-field__inputs { + padding-right: 66px; + } + + .show-hide-button { + position: absolute; + margin-top: -39px; + right: 1px; + height: 28px; + background: #D7D3D0; + width: 60px; + border-radius: 0 3px 3px 0; + color: #FFFFFF; + font-size: 13px; + font-weight: 400; + text-align: center; + + &:focus{ + background: $tc-red!important; + border-color: $tc-red!important; + } + } + } + +} \ No newline at end of file diff --git a/components/Formsy/PhoneInput.jsx b/components/Formsy/PhoneInput.jsx new file mode 100644 index 000000000..b9367b4b3 --- /dev/null +++ b/components/Formsy/PhoneInput.jsx @@ -0,0 +1,177 @@ +import React, { Component } from 'react' +import PT from 'prop-types' +import { HOC as hoc } from 'formsy-react' +import classNames from 'classnames' + +import HelpIcon from '../HelpIcon/HelpIcon' +import Dropdown from '../Dropdown/Dropdown' +import IconDown from '../Icons/IconTcCarretDown' +import IconUICheckSimple from '../Icons/IconUICheckSimple' + +import styles from './PhoneInput.scss' +import { AsYouType } from 'libphonenumber-js' + +class PhoneInput extends Component { + + constructor(props) { + super(props) + + this.changeValue = this.changeValue.bind(this) + this.choseCountry = this.choseCountry.bind(this) + this.isValidInput = this.isValidInput.bind(this) + this.state = { + currentCountry: {}, + asYouType: {} + } + } + + isValidInput() { + const value = this.props.getValue() + const hasError = !this.props.isPristine() && !this.props.isValid() + return (!this.props.forceErrorMessage && value && !hasError && this.state.currentCountry.code) + } + + changeValue(e) { + const value = e.target.value + this.props.setValue(value) + let currentCountry + const asYouType = new AsYouType() + asYouType.input(value[0] === '+' ? value : '+' + value) + if (asYouType.country) { + currentCountry = _.filter(this.props.listCountry, { alpha2: asYouType.country })[0] + if (currentCountry) { + this.setState({ asYouType, currentCountry }) + } + } + + this.props.onChange(this.props.name, value) + this.props.onChangeCountry({ + phone: value, + country: currentCountry || {} + }) + } + + choseCountry(country) { + if (country.code !== this.state.currentCountry.code) { + const asYouTypeTmp = new AsYouType(country.alpha2) + const { asYouType } = this.state + let phoneNumber = '' + if (asYouType && asYouType.getNationalNumber) { + phoneNumber = ` ${asYouType.getNationalNumber()}` + } + + if (asYouTypeTmp.countryCallingCode) { + this.setState({ currentCountry: country }) + this.changeValue({ target: { value: `+${asYouTypeTmp.countryCallingCode}${phoneNumber}` } }) + } + } + } + + render() { + const { label, name, type, minValue, maxValue, placeholder, wrapperClass, maxLength, theme, + labelHelpTooltip, readonly, readonlyValueTooltip, showCheckMark } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const disabled = this.props.isFormDisabled() || this.props.disabled + const wrapperClasses = classNames(wrapperClass, theme, { + [styles['readonly-wrapper']]: readonly, + 'phone-input-container': true + }) + const classes = classNames('tc-file-field__inputs', { error: hasError }, { empty: this.props.getValue() === '' }) + const errorMessage = this.props.getErrorMessage() || this.props.validationError + + return ( +
    + +
    + + +
    {this.state.currentCountry ? this.state.currentCountry.alpha3 : ''} +
    +
      + { + this.props.listCountry.map((country, i) => { + /* eslint-disable react/jsx-no-bind */ + return
    • this.choseCountry(country)} key={i}>{country.name}
    • + }) + } +
    +
    +
    + {this.isValidInput() && showCheckMark && ( + + )} + {readonly && ( +
    + {this.props.getValue()} + {readonlyValueTooltip && } +
    + )} + + {hasError ? (

    {errorMessage}

    ) : (this.props.forceErrorMessage && (

    {this.props.forceErrorMessage}

    ))} +
    + ) + } +} + +PhoneInput.defaultProps = { + onChange: () => { }, + forceErrorMessage: null, + listCountry: [], + showCheckMark: false +} + +PhoneInput.propTypes = { + /** + * The difference from `disabled` is that instead of showing disabled input + * we show value using
    which let us position something immediately after the value + */ + readonly: PT.bool, + + /** + * Show help icon next to the label with the tooltip defined by this prop + */ + labelHelpTooltip: PT.node, + + /** + * Show help icon next to the value with the tooltip defined by this prop + * This only has any effect if `readonly` is set to `true` + */ + readonlyValueTooltip: PT.node, + + /** + * Show error message without any condition + */ + forceErrorMessage: PT.string, + + /** + * country list + */ + listCountry: PT.array, + + /** + * event when change phone + */ + onChangeCountry: PT.func, + + /** + * should show check mark icon when valid input + */ + showCheckMark: PT.bool + +} + +export default hoc(PhoneInput) diff --git a/components/Formsy/PhoneInput.scss b/components/Formsy/PhoneInput.scss new file mode 100644 index 000000000..5cba08053 --- /dev/null +++ b/components/Formsy/PhoneInput.scss @@ -0,0 +1,82 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.readonly-wrapper { + :global(input.tc-file-field__inputs) { + display: none; + } +} + +.readonly-value { + color: $tc-gray-60; + display: block; + height: 40px; + line-height: 40px; + margin-bottom: 2 * $base-unit; + padding: 0 2 * $base-unit; + width: 100%; +} + + +:global { + .phone-input-container { + position: relative; + + .tc-file-field__inputs { + padding-right: 66px; + } + + .input-container { + position: relative; + } + + .dropdown-wrap { + position: absolute; + bottom: 1px; + right: 1px; + height: 28px; + background: #D7D3D0; + width: 60px; + border-radius: 0 3px 3px 0; + + .dropdown-wrap { + top: 0; + + .dropdown-menu-list { + max-height: 300px; + overflow-y: scroll; + + li { + white-space: nowrap; + + &.selected { + background-color: lightgray; + } + } + } + + .dropdown-menu-header { + color: #FFFFFF; + font-size: 13px; + font-weight: 400; + text-align: center; + height: 100%; + width: 100%; + + .arrow { + transform: scaleY(-1); + margin-top: -5px; + } + } + + .Dropdown { + width: auto; + margin-left: -150px; + margin-top: 30px; + color: black; + text-align: center; + } + } + } + } + +} \ No newline at end of file diff --git a/components/Formsy/RadioGroup.jsx b/components/Formsy/RadioGroup.jsx new file mode 100644 index 000000000..197046e48 --- /dev/null +++ b/components/Formsy/RadioGroup.jsx @@ -0,0 +1,88 @@ +import React, { Component, PropTypes } from 'react' +import { HOC as hoc } from 'formsy-react' +import cn from 'classnames' +import { find } from "lodash"; +import { numberWithCommas } from './format' + +class RadioGroup extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + } + + changeValue(e) { + const value = e.target.value + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + getSelectedOption() { + const {options = [], getValue} = this.props; + const value = getValue() + return find(options, o => value === o.value) + } + + render() { + const { label, name, wrapperClass, options } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const disabled = this.props.isFormDisabled() || this.props.disabled + const errorMessage = this.props.getErrorMessage() || this.props.validationError + const selectedOption = this.getSelectedOption() + const hasPrice = find(options, o => o.quoteUp) + + const renderOption = (radio, key) => { + const relativePrice = (selectedOption, radio) => { + const price = (radio.quoteUp || 0) - (selectedOption.quoteUp || 0) + return (price < 0 ? '-' : '+') + ' $' + numberWithCommas(Math.abs(price)) + } + const checked = (selectedOption && selectedOption.value === radio.value) + const disabled = this.props.isFormDisabled() || radio.disabled || this.props.disabled + const rClass = cn('radio', { disabled, selected: checked }) + const id = name+'-opt-'+key + const setRef = (c) => this['element-' + key] = c + return ( +
    + + + { + hasPrice && + !checked && + (radio.quoteUp || selectedOption) && +
    {selectedOption ? relativePrice(selectedOption, radio) : `$${numberWithCommas(radio.quoteUp)}`}
    + } + { + radio.description && checked &&
    {radio.description}
    + } +
    + ) + } + + return ( +
    + +
    {options.map(renderOption)}
    + { hasError ? (

    {errorMessage}

    ) : null} +
    + ) + } +} + + +RadioGroup.PropTypes = { + options: PropTypes.arrayOf(PropTypes.object).isRequired +} + +RadioGroup.defaultProps = { + onChange: () => {} +} + +export default hoc(RadioGroup) diff --git a/components/Formsy/RadioGroupExample.jsx b/components/Formsy/RadioGroupExample.jsx new file mode 100644 index 000000000..5f8eba349 --- /dev/null +++ b/components/Formsy/RadioGroupExample.jsx @@ -0,0 +1,90 @@ +import React from 'react' +import TiledRadioGroup from './TiledRadioGroup' +import Formsy from 'formsy-react' +import './RadioGroupExample.scss' + +const opts = [ + { + title: 'ASAP', + value: 'opt ASAP', + desc: null + }, + { + title: '1 - 2 months', + value: 'opt 1 - 2 months', + desc: null + }, + { + title: '2 - 3 months', + value: 'opt 2 - 3 months', + desc: null + } +] + + +class RadioGroupExample extends React.Component { + constructor(props) { + super(props) + this.state = { + radioGroup1value: ['opt 2 - 3 months'], + radioGroup2value: ['opt ASAP'] + } + + this.onChange = this.onChange.bind(this) + } + + onChange(name, value) { + if (name === 'radioGroup1') { + this.setState({radioGroup1value: value}) + } + if (name === 'radioGroup2') { + this.setState({radioGroup2value: value}) + } + } + + render() { + return ( +
    +
    +
    + Single Option: +
    + + + +
    + Selected value: {this.state.radioGroup1value} +
    +
    + +
    +
    + Multiple Option: +
    + + + +
    + Selected value: {this.state.radioGroup2value.join(', ')} +
    +
    +
    + + ) + } +} + +export default RadioGroupExample diff --git a/components/Formsy/RadioGroupExample.scss b/components/Formsy/RadioGroupExample.scss new file mode 100644 index 000000000..4a20b8495 --- /dev/null +++ b/components/Formsy/RadioGroupExample.scss @@ -0,0 +1,33 @@ +@import '~tc-ui/src/styles/tc-includes'; + +:global { + .radio-group-container { + .single-action-radio-group { + margin-bottom: 50px; + } + .checkbox-title { + font-size: 20px; + font-weight: 200; + margin-bottom: 20px; + } + .test { + display: flex; + flex-direction: row; + } + .tiled-group-item { + margin-right: 30px; + display: flex; + flex-direction: column; + align-items: center; + } + .check-mark { + background: blue; + border-radius: 100%; + width: 20px; + height: 20px; + align-items: center; + display: flex; + justify-content: center; + } + } +} diff --git a/components/Formsy/SliderRadioGroup.jsx b/components/Formsy/SliderRadioGroup.jsx new file mode 100644 index 000000000..941d9ad17 --- /dev/null +++ b/components/Formsy/SliderRadioGroup.jsx @@ -0,0 +1,66 @@ +'use strict' + +import React, { Component, PropTypes } from 'react' +import Slider from 'rc-slider' +import 'rc-slider/assets/index.css' +import cn from 'classnames' +import _ from 'lodash' +import { HOC as hoc } from 'formsy-react' + +class SliderRadioGroup extends Component { + constructor(props) { + super(props) + this.onChange = this.onChange.bind(this) + } + + onChange(value) { + const {name, options} = this.props + const newValue = options[value].value + this.props.setValue(newValue) + this.props.onChange(name, newValue) + } + + noOp() {} + + getIndexFromValue(val) { + return _.findIndex(this.props.options, (t) => t.value === val) + } + + render() { + const { options, min, max, step} = this.props + const value = this.props.getValue() + const valueIdx = this.getIndexFromValue(value) + const marks = {} + for(let i=0; i < options.length; i++) { + marks[i] = options[i].title + } + return ( +
    + +
    + ) + } +} + +SliderRadioGroup.propTypes = { + options: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired, + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + step: PropTypes.number.isRequired +} +SliderRadioGroup.defaultProps = { + onChange: () => {} +} +export default hoc(SliderRadioGroup) \ No newline at end of file diff --git a/components/Formsy/TextInput.jsx b/components/Formsy/TextInput.jsx new file mode 100644 index 000000000..25efe8765 --- /dev/null +++ b/components/Formsy/TextInput.jsx @@ -0,0 +1,133 @@ +import React, { Component } from 'react' +import PT from 'prop-types' +import { HOC as hoc } from 'formsy-react' +import classNames from 'classnames' + +import HelpIcon from '../HelpIcon/HelpIcon' +import IconUICheckSimple from '../Icons/IconUICheckSimple' + +import styles from './TextInput.scss' + +class TextInput extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + this.callValidator= this.callValidator.bind(this) + this.isValidInput= this.isValidInput.bind(this) + this.previousValue = null + } + + componentDidUpdate() { + setTimeout(() => { + this.callValidator() + }, 100) + } + + changeValue(e) { + const value = e.target.value + this.props.setValue(value) + this.props.onChange(this.props.name, value) + this.isUpdatedValue = true + } + + callValidator() { + const { validator } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const value = this.props.getValue() + if (!hasError && value && value !== this.previousValue && this.isUpdatedValue) { + validator(value) + this.previousValue = value + } + } + + isValidInput() { + const value = this.props.getValue() + const hasError = !this.props.isPristine() && !this.props.isValid() + return (!this.props.forceErrorMessage && !!value && !hasError) + } + + render() { + const { label, name, type, minValue, maxValue, placeholder, wrapperClass, maxLength, theme, + labelHelpTooltip, readonly, readonlyValueTooltip, showCheckMark } = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const disabled = this.props.isFormDisabled() || this.props.disabled + const wrapperClasses = classNames(wrapperClass, theme, { [styles['readonly-wrapper']]: readonly }) + const classes = classNames('tc-file-field__inputs', {error: hasError}, {empty: this.props.getValue() === ''}) + const errorMessage = this.props.getErrorMessage() || this.props.validationError + const value = this.props.getValue() + return ( +
    + + + {this.isValidInput() && showCheckMark && ( + + )} + {readonly && ( +
    + {this.props.getValue()} + {readonlyValueTooltip && } +
    + )} + { hasError ? (

    {errorMessage}

    ) : (this.props.forceErrorMessage && (

    {this.props.forceErrorMessage}

    ))} +
    + ) + } +} + +TextInput.defaultProps = { + onChange: () => {}, + forceErrorMessage: null, + validator: (() => {}), + showCheckMark: false +} + +TextInput.propTypes = { + /** + * The difference from `disabled` is that instead of showing disabled input + * we show value using
    which let us position something immediately after the value + */ + readonly: PT.bool, + + /** + * Show help icon next to the label with the tooltip defined by this prop + */ + labelHelpTooltip: PT.node, + + /** + * Show help icon next to the value with the tooltip defined by this prop + * This only has any effect if `readonly` is set to `true` + */ + readonlyValueTooltip: PT.node, + + /** + * Show error message without any condition + */ + forceErrorMessage: PT.string, + + /** + * validator functionn from outside + */ + validator: PT.func, + + /** + * should show check mark icon when valid input + */ + showCheckMark: PT.bool +} + +export default hoc(TextInput) diff --git a/components/Formsy/TextInput.scss b/components/Formsy/TextInput.scss new file mode 100644 index 000000000..a00a7b7e8 --- /dev/null +++ b/components/Formsy/TextInput.scss @@ -0,0 +1,17 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.readonly-wrapper { + :global(input.tc-file-field__inputs) { + display: none; + } +} + +.readonly-value { + color: $tc-gray-60; + display: block; + // height: 40px; + // line-height: 40px; + // margin-bottom: 2 * $base-unit; + // padding: 0 2 * $base-unit; + width: 100%; +} \ No newline at end of file diff --git a/components/Formsy/Textarea.jsx b/components/Formsy/Textarea.jsx new file mode 100644 index 000000000..3df33e735 --- /dev/null +++ b/components/Formsy/Textarea.jsx @@ -0,0 +1,77 @@ +import React, { Component } from 'react' +import { HOC as hoc } from 'formsy-react' +import classNames from 'classnames' +import AutoresizeTextarea from 'react-textarea-autosize' + +class Textarea extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + } + + changeValue(e) { + const value = e.target.value + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + heightChanged(height, instance) { + if(!instance.state || !instance.state._sizeInitialized) { + setTimeout(() => { + instance._resizeComponent() + }) + instance.setState({ + _sizeInitialized: true + }) + } + } + + render() { + const { label, name, rows, cols, placeholder, wrapperClass} = this.props + const hasError = !this.props.isPristine() && !this.props.isValid() + const classes = classNames('tc-textarea', {error: hasError}, {empty: this.props.getValue() === ''}) + const disabled = this.props.isFormDisabled() || this.props.disabled + const errorMessage = this.props.getErrorMessage() || this.props.validationError + + return ( +
    + + { + this.props.autoResize ? + : +