Layout improvements for modals and other areas

This commit is contained in:
Eric Schultz 2019-04-16 17:15:26 -05:00
parent f55d777c55
commit c3414f1754
17 changed files with 1177 additions and 117 deletions

View file

@ -0,0 +1,6 @@
/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */
.focusable_item {
&:focus {
outline: 1px dotted black
}
}

View file

@ -43,3 +43,5 @@
@import 'common/z_indices';
@import 'common/ios_hack';
@import 'common/minimal';
@import 'common/focusable'

View file

@ -0,0 +1,16 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import { observer } from 'mobx-react';
class BootstrapWrapper extends React.Component<{}, {}> {
render() {
return <div className={"tw-bs"}>
{this.props.children}
</div>;
}
}
export default observer(BootstrapWrapper)

View file

@ -0,0 +1,115 @@
import React = require("react");
import { action, observable } from "mobx";
import { Transition } from "react-transition-group";
import { CloseButton } from "./svg/CloseButton";
import color = require("color");
import { observer } from "mobx-react";
import ScreenReaderOnlyText from "./ScreenReaderOnlyText";
interface DefaultCloseButtonProps {
onClick?: () => void
}
const mainColor = '#969696'
const darkenedColor = color(mainColor).darken(0.1).hex()
const defaultStyles = {
foreground: {
fill: mainColor
},
background: {
stroke: mainColor,
fill: '#FFFFFF'
}
}
const states: { [state: string]: any } = {
entering: {
foreground: {
fill: darkenedColor,
transition: 'fill 250ms ease-in-out'
},
background: {
stroke: darkenedColor,
transition: 'stroke 250ms ease-in-out'
}
},
entered: {
foreground: {
fill: darkenedColor,
},
background: {
stroke: darkenedColor,
}
},
exiting: {
foreground: {
fill: mainColor,
transition: 'fill 250ms ease-in-out'
},
background: {
stroke: mainColor,
transition: 'stroke 250ms ease-in-out'
}
},
exited: {
foreground: {
fill: mainColor,
},
background: {
stroke: mainColor,
}
}
}
@observer
export class DefaultCloseButton extends React.Component<DefaultCloseButtonProps, {}> {
@observable
hovering: boolean
@observable
focusing: boolean
@action.bound
mouseEnter() {
this.hovering = true;
}
@action.bound
mouseLeave() {
this.hovering = false;
}
@action.bound
keyDown(event: React.KeyboardEvent<HTMLAnchorElement>) {
if (event.key == 'Enter') {
event.preventDefault();
this.props.onClick();
}
}
render() {
return <Transition in={this.hovering} timeout={250}>
{(hoverState) => {
const backgroundStyle = {
...defaultStyles.background,
...((states[hoverState] && states[hoverState].background) || {})
}
const foregroundStyle =
{
...defaultStyles.foreground,
...((states[hoverState] && states[hoverState].foreground) || {})
}
return <a onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave} onClick={this.props.onClick} onKeyDown={this.keyDown} tabIndex={0} className={'focusable_item'}>
<CloseButton backgroundCircleStyle={backgroundStyle}
foregroundCircleStyle={foregroundStyle}
/><ScreenReaderOnlyText>Close modal</ScreenReaderOnlyText>
</a>
}
}
</Transition>
}
}

View file

@ -2,9 +2,9 @@
import * as React from 'react';
import 'jest';
import Modal, {ModalProps} from './Modal'
import {shallow} from "enzyme";
import {toJS} from "mobx";
import {shallow, mount, ReactWrapper} from "enzyme";
import toJson from "enzyme-to-json";
import { DefaultCloseButton } from './DefaultCloseButton';
describe('Modal', () => {
test('nothing displayed if inactive', () => {
@ -13,16 +13,47 @@ describe('Modal', () => {
expect(toJson(modal)).toMatchSnapshot()
})
test('active modal displays', () => {
describe('active modal displays', () => {
let onCloseWasCalled = false
let modal = shallow(<Modal titleText={"title text"}
let modal:ReactWrapper
beforeEach(() => {
onCloseWasCalled = false;
modal = mount(<Modal titleText={"title text"}
focusDialog={true}
modalActive={true}
onClose={() => { onCloseWasCalled = true}}
childGenerator={() => <div/>}/>)
})
it('matches snapshot', () => {
expect(toJson(modal)).toMatchSnapshot()
})
it('closes on modal component close', () => {
let modalComponent = modal.instance() as React.Component<ModalProps, {}> //casting to modal didn't work for reasons?
modalComponent.props.onClose()
expect(onCloseWasCalled).toBeTruthy()
})
it('closes on closeButtonClick', () => {
let closeButton = modal.find('DefaultCloseButton')
let instanceButton = closeButton.instance() as DefaultCloseButton
instanceButton.props.onClick();
expect(onCloseWasCalled).toBeTruthy()
})
})
it('doesnt have a close button if we ask for none', () => {
let onCloseWasCalled = false
let modal = mount(<Modal titleText={"title text"}
focusDialog={true}
modalActive={true}
showCloseButton={false}
onClose={() => { onCloseWasCalled = true}}
childGenerator={() => <div/>}/>)
expect(toJson(modal)).toMatchSnapshot()
let modalComponent = modal.instance() as React.Component<ModalProps, {}> //casting to modal didn't work for reasons?
modalComponent.props.onClose()
expect(onCloseWasCalled).toBeTruthy()
childGenerator={() => <div/>}
/>)
expect(modal.find('DefaultCloseButton').exists()).toBeFalsy()
})
})

View file

@ -5,6 +5,9 @@ import AriaModal = require('react-aria-modal');
import { VelocityTransitionGroup } from 'velocity-react';
import 'velocity-animate';
import 'velocity-animate/velocity.ui';
import { DefaultCloseButton } from './DefaultCloseButton';
import BootstrapWrapper from './BootstrapWrapper';
import { Row, Column } from './layout';
export interface ModalProps {
onClose?: () => void // if you want your modal to close, this needs to set modalActive to false
@ -12,25 +15,44 @@ export interface ModalProps {
titleText?: string
focusDialog?: boolean
dialogStyle?: any
showCloseButton?: boolean
childGenerator: () => any
}
class Modal extends React.Component<ModalProps, {}> {
static defaultProps = {
dialogStyle: { minWidth: '768px' }
dialogStyle: { minWidth: '768px' },
showCloseButton: true
}
render() {
const innerModal = this.props.modalActive ? <AriaModal mounted={this.props.modalActive} titleText={this.props.titleText} focusDialog={this.props.focusDialog}
onExit={this.props.onClose} dialogStyle={this.props.dialogStyle}>
<header className='modal-header'>
<h4 className='modal-header-title'>{this.props.titleText}</h4>
</header>
<div className="modal-body">
{this.props.childGenerator()}
</div>
<BootstrapWrapper>
<header className='modal-header' style={{
position: 'relative',
padding: '12px 10px 12px 20px'
}}>
<Row>
<Column colSpan={11} breakSize={'xs'}>
<h3 className='modal-header-title' style={{ margin: 0 }}>{this.props.titleText}</h3>
</Column>
{this.props.showCloseButton ?
<Column colSpan={1} breakSize={'xs'}>
<div style={{ textAlign: 'right' }}>
<DefaultCloseButton onClick={() => this.props.onClose()} />
</div>
</Column> : false
}
</Row>
</header>
<div className="modal-body">
<div style={{ position: 'relative' }}>
{this.props.childGenerator()}
</div>
</div>
</BootstrapWrapper>
</AriaModal> : false
const modal =
@ -61,7 +83,7 @@ class Modal extends React.Component<ModalProps, {}> {
}
}
}
runOnMount={true}>
{innerModal}
</VelocityTransitionGroup>;

View file

@ -0,0 +1,13 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import 'jest';
import ScreenReaderOnlyText from './ScreenReaderOnlyText'
import toJson from 'enzyme-to-json';
import { shallow } from 'enzyme';
describe('ScreenReaderOnlyText', () => {
it('renders properly', () => {
let text = shallow(<ScreenReaderOnlyText>Test</ScreenReaderOnlyText>)
expect(toJson(text)).toMatchSnapshot()
})
})

View file

@ -0,0 +1,29 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
export interface ScreenReaderOnlyTextProps
{
}
class ScreenReaderOnlyText extends React.Component<ScreenReaderOnlyTextProps, {}> {
render() {
const style:React.CSSProperties = {
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
border: 0
}
return <span style={style}>{this.props.children}</span>
}
}
export default ScreenReaderOnlyText;

View file

@ -1,68 +1,418 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal active modal displays 1`] = `
<VelocityTransitionGroup
enter={
exports[`Modal active modal displays matches snapshot 1`] = `
<Modal
childGenerator={[Function]}
dialogStyle={
Object {
"animation": "fadeIn",
"style": Object {
"left": "0px",
"position": "fixed",
"top": "0px",
"zIndex": "5000",
},
"minWidth": "768px",
}
}
enterHideStyle={
Object {
"display": "none",
}
}
enterShowStyle={
Object {
"display": "",
}
}
leave={
Object {
"animation": "fadeOut",
"style": Object {
"left": "0px",
"position": "fixed",
"top": "0px",
"zIndex": "5000",
},
}
}
runOnMount={true}
focusDialog={true}
modalActive={true}
onClose={[Function]}
showCloseButton={true}
titleText="title text"
>
<Displaced
dialogStyle={
<VelocityTransitionGroup
enter={
Object {
"minWidth": "768px",
"animation": "fadeIn",
"style": Object {
"left": "0px",
"position": "fixed",
"top": "0px",
"zIndex": "5000",
},
}
}
focusDialog={true}
mounted={true}
onExit={[Function]}
titleText="title text"
enterHideStyle={
Object {
"display": "none",
}
}
enterShowStyle={
Object {
"display": "",
}
}
leave={
Object {
"animation": "fadeOut",
"style": Object {
"left": "0px",
"position": "fixed",
"top": "0px",
"zIndex": "5000",
},
}
}
runOnMount={true}
>
<header
className="modal-header"
<TransitionGroup
childFactory={[Function]}
component="div"
>
<h4
className="modal-header-title"
>
title text
</h4>
</header>
<div
className="modal-body"
>
<div />
</div>
</Displaced>
</VelocityTransitionGroup>
<div>
<Displaced
dialogStyle={
Object {
"minWidth": "768px",
}
}
focusDialog={true}
in={true}
key=".0"
mounted={true}
onExit={[Function]}
onExited={[Function]}
titleText="title text"
>
<Portal
containerInfo={
<div>
<div>
<div
style="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 1050; overflow-x: hidden; overflow-y: auto; text-align: center; background: rgba(0, 0, 0, 0.5); cursor: pointer;"
>
<div
aria-label="title text"
id="react-aria-modal-dialog"
role="dialog"
style="display: inline-block; text-align: left; top: 0px; max-width: 100%; cursor: default; outline: 0; min-width: 768px;"
tabindex="-1"
>
<div
class="tw-bs"
>
<header
class="modal-header"
style="position: relative; padding: 12px 10px 12px 20px;"
>
<div
class="row"
>
<h3
class="col-xs-11 modal-header-title"
style="margin: 0px;"
>
title text
</h3>
<div
class="col-xs-1 "
style="text-align: right;"
>
<a
class="focusable_item"
tabindex="0"
>
<svg
height="24"
version="1.1"
viewBox="0 0 24 24"
width="24"
>
<circle
cx="12"
cy="12"
r="10.65625"
style="stroke: #969696; fill: #FFFFFF;"
/>
<circle
cx="12"
cy="12"
r="9.25"
style="fill: #969696;"
/>
<path
d="M 7.130438,16.869562 16.869562,7.130438"
style="stroke: #ffffff; stroke-width: 1.375; stroke-linecap: square;"
/>
<path
d="M 16.869562,16.869562 7.130438,7.130438"
style="stroke: #ffffff; stroke-width: 1.375; stroke-linecap: square;"
/>
</svg>
<span
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); border: 0px;"
>
Close modal
</span>
</a>
</div>
</div>
</header>
<div
class="modal-body"
>
<div
style="position: relative;"
>
<div />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
>
<Modal
dialogId="react-aria-modal-dialog"
dialogStyle={
Object {
"minWidth": "768px",
}
}
escapeExits={true}
focusDialog={true}
focusTrapPaused={false}
in={true}
includeDefaultStyles={true}
mounted={true}
onExit={[Function]}
onExited={[Function]}
scrollDisabled={true}
titleText="title text"
underlayClickExits={true}
underlayColor="rgba(0,0,0,0.5)"
underlayProps={Object {}}
>
<FocusTrap
_createFocusTrap={[Function]}
active={true}
focusTrapOptions={
Object {
"escapeDeactivates": true,
"initialFocus": "#react-aria-modal-dialog",
}
}
paused={false}
tag="div"
>
<div>
<div
onMouseDown={[Function]}
style={
Object {
"WebkitOverflowScrolling": "touch",
"background": "rgba(0,0,0,0.5)",
"cursor": "pointer",
"height": "100%",
"left": 0,
"overflowX": "hidden",
"overflowY": "auto",
"position": "fixed",
"textAlign": "center",
"top": 0,
"width": "100%",
"zIndex": 1050,
}
}
>
<div
aria-label="title text"
id="react-aria-modal-dialog"
key="b"
role="dialog"
style={
Object {
"cursor": "default",
"display": "inline-block",
"maxWidth": "100%",
"minWidth": "768px",
"outline": 0,
"textAlign": "left",
"top": 0,
}
}
tabIndex="-1"
>
<BootstrapWrapper>
<div
className="tw-bs"
>
<header
className="modal-header"
style={
Object {
"padding": "12px 10px 12px 20px",
"position": "relative",
}
}
>
<Row>
<div
className="row"
>
<Column
breakSize="xs"
colSpan={11}
>
<h3
className="col-xs-11 modal-header-title"
style={
Object {
"margin": 0,
}
}
>
title text
</h3>
</Column>
<Column
breakSize="xs"
colSpan={1}
>
<div
className="col-xs-1 "
style={
Object {
"textAlign": "right",
}
}
>
<DefaultCloseButton
onClick={[Function]}
>
<Transition
appear={false}
enter={true}
exit={true}
in={false}
mountOnEnter={false}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
timeout={250}
unmountOnExit={false}
>
<a
className="focusable_item"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
tabIndex={0}
>
<Component
backgroundCircleStyle={
Object {
"fill": "#FFFFFF",
"stroke": "#969696",
}
}
foregroundCircleStyle={
Object {
"fill": "#969696",
}
}
>
<svg
height="24"
version="1.1"
viewBox="0 0 24 24"
width="24"
>
<circle
cx="12"
cy="12"
r="10.65625"
style={
Object {
"fill": "#FFFFFF",
"stroke": "#969696",
}
}
/>
<circle
cx="12"
cy="12"
r="9.25"
style={
Object {
"fill": "#969696",
}
}
/>
<path
d="M 7.130438,16.869562 16.869562,7.130438"
style={
Object {
"stroke": "#ffffff",
"strokeLinecap": "square",
"strokeWidth": 1.375,
}
}
/>
<path
d="M 16.869562,16.869562 7.130438,7.130438"
style={
Object {
"stroke": "#ffffff",
"strokeLinecap": "square",
"strokeWidth": 1.375,
}
}
/>
</svg>
</Component>
<ScreenReaderOnlyText>
<span
style={
Object {
"border": 0,
"clip": "rect(0,0,0,0)",
"height": "1px",
"margin": "-1px",
"overflow": "hidden",
"padding": 0,
"position": "absolute",
"width": "1px",
}
}
>
Close modal
</span>
</ScreenReaderOnlyText>
</a>
</Transition>
</DefaultCloseButton>
</div>
</Column>
</div>
</Row>
</header>
<div
className="modal-body"
>
<div
style={
Object {
"position": "relative",
}
}
>
<div />
</div>
</div>
</div>
</BootstrapWrapper>
</div>
</div>
</div>
</FocusTrap>
</Modal>
</Portal>
</Displaced>
</div>
</TransitionGroup>
</VelocityTransitionGroup>
</Modal>
`;
exports[`Modal nothing displayed if inactive 1`] = `

View file

@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScreenReaderOnlyText renders properly 1`] = `
<span
style={
Object {
"border": 0,
"clip": "rect(0,0,0,0)",
"height": "1px",
"margin": "-1px",
"overflow": "hidden",
"padding": 0,
"position": "absolute",
"width": "1px",
}
}
>
Test
</span>
`;

View file

@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`layout Row renders multiple children properly 1`] = `
<Row>
<div
className="row"
>
<SimpleCheckerComponent>
<div />
</SimpleCheckerComponent>
<SimpleCheckerComponent>
<div />
</SimpleCheckerComponent>
</div>
</Row>
`;
exports[`layout Row renders single child properly 1`] = `
<Row>
<div
className="row"
>
<SimpleCheckerComponent>
<div />
</SimpleCheckerComponent>
</div>
</Row>
`;
exports[`layout ThreeColumnFields renders multiple children properly 1`] = `
<ThreeColumnFields>
<Row>
<div
className="row"
>
<Column
breakSize="sm"
colSpan={4}
>
<SimpleCheckerComponent
className="col-sm-4 u-paddingRight--10"
>
<div />
</SimpleCheckerComponent>
</Column>
<Column
breakSize="sm"
colSpan={4}
>
<SimpleCheckerComponent
className="col-sm-4 u-paddingRight--10"
>
<div />
</SimpleCheckerComponent>
</Column>
<Column
breakSize="sm"
colSpan={4}
>
<SimpleCheckerComponent
className="col-sm-4 "
>
<div />
</SimpleCheckerComponent>
</Column>
</div>
</Row>
</ThreeColumnFields>
`;
exports[`layout ThreeColumnFields renders single child properly 1`] = `
<ThreeColumnFields>
<Row>
<div
className="row"
>
<Column
breakSize="sm"
colSpan={4}
>
<SimpleCheckerComponent
className="col-sm-4 "
>
<div />
</SimpleCheckerComponent>
</Column>
</div>
</Row>
</ThreeColumnFields>
`;
exports[`layout TwoColumnFields renders multiple children properly 1`] = `
<TwoColumnFields>
<Row>
<div
className="row"
>
<Column
breakSize="sm"
colSpan={6}
>
<SimpleCheckerComponent
className="col-sm-6 u-paddingRight--10"
>
<div />
</SimpleCheckerComponent>
</Column>
<Column
breakSize="sm"
colSpan={6}
>
<SimpleCheckerComponent
className="col-sm-6 "
>
<div />
</SimpleCheckerComponent>
</Column>
</div>
</Row>
</TwoColumnFields>
`;
exports[`layout TwoColumnFields renders single child properly 1`] = `
<TwoColumnFields>
<Row>
<div
className="row"
>
<Column
breakSize="sm"
colSpan={6}
>
<SimpleCheckerComponent
className="col-sm-6 "
>
<div />
</SimpleCheckerComponent>
</Column>
</div>
</Row>
</TwoColumnFields>
`;

View file

@ -0,0 +1,107 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import 'jest';
import Modal, { ModalProps } from './Modal'
import { shallow, mount, ReactWrapper } from "enzyme";
import toJson from "enzyme-to-json";
import { DefaultCloseButton } from './DefaultCloseButton';
import { Column, Row, ThreeColumnFields, TwoColumnFields } from './layout';
const SimpleCheckerComponent = (props: { className?: string }) => {
return <div></div>
}
describe('layout', () => {
describe('Column', () => {
it('sets proper class without none passed in', () => {
const column = shallow(<Column colSpan={12} breakSize={'sm'}>
<SimpleCheckerComponent />
</Column>)
expect(column.find(SimpleCheckerComponent).props()['className']).toBe('col-sm-12 ')
})
it('sets proper class with one passed in', () => {
const column = shallow(<Column colSpan={1} breakSize={'lg'}>
<SimpleCheckerComponent className="another_class" />
</Column>)
expect(column.find(SimpleCheckerComponent).props()['className'])
.toBe('col-lg-1 another_class')
})
it('sets proper display name', () => {
const column = shallow(<Column colSpan={1} breakSize={'lg'}>
<SimpleCheckerComponent className="another_class" />
</Column>)
expect(Column.displayName).toBe('Column')
})
})
describe('Row', () => {
it('renders single child properly', () => {
const row = mount(<Row>
<SimpleCheckerComponent/>
</Row>)
expect(toJson(row)).toMatchSnapshot()
})
it('renders multiple children properly', () => {
const row = mount(<Row>
<SimpleCheckerComponent/>
<SimpleCheckerComponent/>
</Row>)
expect(toJson(row)).toMatchSnapshot()
})
it('has correct display name', () => {
const row = mount(<Row>
<SimpleCheckerComponent/>
</Row>)
expect(Row.displayName).toBe('Row')
})
})
describe('ThreeColumnFields',() => {
it('renders single child properly', () => {
const fields = mount(<ThreeColumnFields>
<SimpleCheckerComponent/>
</ThreeColumnFields>)
expect(toJson(fields)).toMatchSnapshot()
})
it('renders multiple children properly', () => {
const fields = mount(<ThreeColumnFields>
<SimpleCheckerComponent/>
<SimpleCheckerComponent/>
<SimpleCheckerComponent/>
</ThreeColumnFields>)
expect(toJson(fields)).toMatchSnapshot()
})
})
describe('TwoColumnFields',() => {
it('renders single child properly', () => {
const fields = mount(<TwoColumnFields>
<SimpleCheckerComponent/>
</TwoColumnFields>)
expect(toJson(fields)).toMatchSnapshot()
})
it('renders multiple children properly', () => {
const fields = mount(<TwoColumnFields>
<SimpleCheckerComponent/>
<SimpleCheckerComponent/>
</TwoColumnFields>)
expect(toJson(fields)).toMatchSnapshot()
})
})
})

View file

@ -3,36 +3,78 @@ import * as React from 'react';
import {observer} from "mobx-react";
import * as _ from 'lodash'
export const TwoColumnFields = observer((props:{children:Array<React.ReactElement<any>>}) => {
return <div className="row">
function arrayify<T>(items: Array<T>|T){
return items instanceof Array ? items : [items]
}
export const TwoColumnFields: React.StatelessComponent<{}> = (props:{children:Array<React.ReactElement<any>>|React.ReactElement<any>}) => {
const children = arrayify(props.children)
return <Row>
{
_.take(props.children, 2).map((i:React.ReactElement<any>) => {
let className = "col-sm-6"
if (_.last(props.children) !== i){
children.map((i:React.ReactElement<any>) => {
let className = ""
if (_.last(children) !== i){
className += " u-paddingRight--10"
}
if (i.props['className']){
className += i.props['className']
}
return React.cloneElement(i, {wrapperClassName: className})
return <Column colSpan={6} breakSize={'sm'}>
{React.cloneElement(i, {className: className})}
</Column>
})}
</div>
})
</Row>
}
export const ThreeColumnFields = observer((props:{children:React.ReactElement<any>[]}) => {
return <div className="row">
TwoColumnFields.displayName = 'TwoColumnFields'
export const ThreeColumnFields: React.StatelessComponent<{}> = (props:{children:Array<React.ReactElement<any>>|React.ReactElement<any>}) => {
const children = arrayify(props.children)
return <Row>
{
_.take(props.children, 3).map((i:React.ReactElement<any>) => {
let className = "col-sm-4"
if (_.last(props.children) !== i){
children.map((i:React.ReactElement<any>) => {
let className = ""
if (_.last(children) !== i){
className += " u-paddingRight--10"
}
if (i.props['className']){
className += i.props['className']
}
return React.cloneElement(i, {wrapperClassName: className})
return <Column colSpan={4} breakSize={'sm'}>
{React.cloneElement(i, {className: className})}
</Column>
})}
</Row>
}
ThreeColumnFields.displayName = 'ThreeColumnFields'
export const Row: React.StatelessComponent<{}> = (props:{children:Array<React.ReactElement<any>>|React.ReactElement<any>}) => {
return <div className="row">
{props.children}
</div>
})
}
Row.displayName = 'Row'
type ColumnBreakSize = 'xs'| 'sm'|'md'|'lg'
type ColumnSpan = 1|2|3|4|5|6|7|8|9|10|11|12
interface ColumnProps {
children:React.ReactElement<any>
colSpan:ColumnSpan,
breakSize:ColumnBreakSize
}
export const Column: React.StatelessComponent<ColumnProps> = (props) => {
let className = `col-${props.breakSize}-${props.colSpan} `
props.children.props
if (props.children.props.className){
className += props.children.props['className']
}
return React.cloneElement(props.children, {className: className})
}
Column.displayName = 'Column'

View file

@ -0,0 +1,39 @@
// License: LGPL-3.0-or-later
import React = require("react");
interface CloseButtonProps {
backgroundCircleStyle:React.CSSProperties
foregroundCircleStyle:React.CSSProperties
}
export const CloseButton = (props: CloseButtonProps) => {
return <svg
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1">
<circle
cx="12"
cy="12"
r="10.65625"
style={props.backgroundCircleStyle} />
<circle
cx="12"
cy="12"
r="9.25"
style={props.foregroundCircleStyle} />
<path
d="M 7.130438,16.869562 16.869562,7.130438"
style={{
stroke: '#ffffff',
strokeWidth:1.375,
strokeLinecap:'square'
}} />
<path
d="M 16.869562,16.869562 7.130438,7.130438"
style={{
stroke: '#ffffff',
strokeWidth:1.375,
strokeLinecap:'square'
}} />
</svg>
}

View file

@ -0,0 +1,6 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
export const Checkbox = (props:React.SVGProps<SVGPathElement>) => {
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="10px" width="10px"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z" {...props}/></svg>
}

171
package-lock.json generated
View file

@ -204,17 +204,17 @@
}
},
"@babel/runtime": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz",
"integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==",
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz",
"integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==",
"requires": {
"regenerator-runtime": "^0.12.0"
"regenerator-runtime": "^0.13.2"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
}
}
},
@ -4974,6 +4974,30 @@
"integrity": "sha512-+T9qBbqe/jXtTjzVddArZExahoPPmt8eq3O1ZuCKZXjBVxf/ciUYNXrIDZJEVgYvpELnv6VlPRCfLzufRxpAag==",
"dev": true
},
"@types/color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.0.tgz",
"integrity": "sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q==",
"dev": true,
"requires": {
"@types/color-convert": "*"
}
},
"@types/color-convert": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-1.9.0.tgz",
"integrity": "sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg==",
"dev": true,
"requires": {
"@types/color-name": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/enzyme": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.1.10.tgz",
@ -5104,6 +5128,15 @@
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-hP7vUaZMVSWKxo133P8U51U6UZ7+pbY+eAQb8+p6SZ2rB1rj3mOTDgTzhhi+R2SCB4S+sWekAAGoxdiZPG0ReQ==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/sinon": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",
@ -7402,7 +7435,8 @@
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
"clone-deep": {
"version": "2.0.2",
@ -7448,21 +7482,29 @@
}
},
"color": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/color/-/color-0.11.3.tgz",
"integrity": "sha1-S60dDVJJndANvW8IaEQkZ+STlOY=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.1.0.tgz",
"integrity": "sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==",
"requires": {
"clone": "^1.0.2",
"color-convert": "^1.3.0",
"color-string": "^0.3.0"
"color-convert": "^1.9.1",
"color-string": "^1.5.2"
},
"dependencies": {
"color-convert": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "^1.1.1"
"color-name": "1.1.3"
}
},
"color-string": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
"integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
}
}
@ -7481,6 +7523,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz",
"integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=",
"dev": true,
"requires": {
"color-name": "^1.0.0"
}
@ -7494,6 +7537,28 @@
"color": "^0.11.0",
"css-color-names": "0.0.4",
"has": "^1.0.1"
},
"dependencies": {
"color": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
"integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
"dev": true,
"requires": {
"clone": "^1.0.2",
"color-convert": "^1.3.0",
"color-string": "^0.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
}
}
},
"colors": {
@ -7853,6 +7918,26 @@
"integrity": "sha1-tQS9BYabOSWd0MXvw12EMXbczEo=",
"dev": true
},
"color": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
"integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
"dev": true,
"requires": {
"clone": "^1.0.2",
"color-convert": "^1.3.0",
"color-string": "^0.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@ -15239,6 +15324,28 @@
"postcss": "^5.0.4",
"postcss-message-helpers": "^2.0.0",
"reduce-function-call": "^1.0.1"
},
"dependencies": {
"color": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
"integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
"dev": true,
"requires": {
"clone": "^1.0.2",
"color-convert": "^1.3.0",
"color-string": "^0.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
}
}
},
"postcss-color-hex-alpha": {
@ -16732,11 +16839,11 @@
}
},
"react-transition-group": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz",
"integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
"requires": {
"dom-helpers": "^3.3.1",
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
@ -16759,11 +16866,6 @@
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"react-is": {
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.1.tgz",
"integrity": "sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA=="
}
}
},
@ -17507,6 +17609,21 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
},
"dependencies": {
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
}
}
},
"sinon": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.7.tgz",

View file

@ -18,6 +18,7 @@
"generate-api-js": "rm -rf javascripts/api && npm run get-codegen-cli && npm run generate-openapi && java -jar .bin/swagger-codegen-cli.jar generate -l 'typescript-jquery' -i tmp/openapi.json -o javascripts/api --config 'swagger.json' -t lib/swagger-typescript-jquery"
},
"devDependencies": {
"@types/color": "^3.0.0",
"@types/enzyme": "^3.1.9",
"@types/enzyme-to-json": "^1.5.1",
"@types/es6-promise": "^3.3.0",
@ -32,6 +33,7 @@
"@types/react-intl": "^2.3.7",
"@types/react-test-renderer": "^16.0.1",
"@types/react-text-mask": "^5.4.2",
"@types/react-transition-group": "^2.9.0",
"@types/sinon": "^4.3.3",
"@types/validator": "^9.4.1",
"@types/velocity-animate": "^1.2.33",
@ -87,7 +89,7 @@
"attr-binder": "0.3.1",
"aws-sdk": "^2.402.0",
"chart.js": "2.1.4",
"color": "0.11.3",
"color": "^3.1.0",
"commons.css": "0.1.8",
"cookie": "0.3.1",
"cropperjs": "1.0.0-beta.2",
@ -138,6 +140,7 @@
"react-dom": "^16.3.1",
"react-intl": "^2.4.0",
"react-text-mask": "^5.3.0",
"react-transition-group": "^2.9.0",
"shuffle-array": "1.0.1",
"snabbdom": "0.3.0",
"snake-case": "2.1.0",