diff options
Diffstat (limited to 'server/sonar-web/src/main/js/components/controls')
8 files changed, 392 insertions, 34 deletions
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx index c3be977b72d..0718757ff1a 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx @@ -18,40 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import ConfirmModal, { ConfirmModalProps } from './ConfirmModal'; import ModalButton, { ChildrenProps, ModalProps } from './ModalButton'; -import ConfirmModal from './ConfirmModal'; -export { ChildrenProps } from './ModalButton'; - -interface Props { +interface Props<T> extends ConfirmModalProps<T> { children: (props: ChildrenProps) => React.ReactNode; - cancelButtonText?: string; - confirmButtonText: string; - confirmData?: string; - confirmDisable?: boolean; - isDestructive?: boolean; modalBody: React.ReactNode; modalHeader: string; - onConfirm: (data?: string) => void | Promise<void>; } interface State { modal: boolean; } -export default class ConfirmButton extends React.PureComponent<Props, State> { +export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> { renderConfirmModal = ({ onClose }: ModalProps) => { + const { children, modalBody, modalHeader, ...confirmModalProps } = this.props; return ( - <ConfirmModal - cancelButtonText={this.props.cancelButtonText} - confirmButtonText={this.props.confirmButtonText} - confirmData={this.props.confirmData} - confirmDisable={this.props.confirmDisable} - header={this.props.modalHeader} - isDestructive={this.props.isDestructive} - onClose={onClose} - onConfirm={this.props.onConfirm}> - {this.props.modalBody} + <ConfirmModal header={modalHeader} onClose={onClose} {...confirmModalProps}> + {modalBody} </ConfirmModal> ); }; diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx index 8ac88d222ac..f51f9704aec 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx @@ -18,21 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { ModalProps } from './Modal'; import SimpleModal, { ChildrenProps } from './SimpleModal'; import DeferredSpinner from '../common/DeferredSpinner'; -import { translate } from '../../helpers/l10n'; import { SubmitButton, ResetButtonLink } from '../ui/buttons'; +import { translate } from '../../helpers/l10n'; -interface Props<T> { - children: React.ReactNode; +export interface ConfirmModalProps<T> extends ModalProps { cancelButtonText?: string; confirmButtonText: string; confirmData?: T; confirmDisable?: boolean; - header: string; isDestructive?: boolean; + onConfirm: (data?: T) => void | Promise<void | Response>; +} + +interface Props<T> extends ConfirmModalProps<T> { + header: string; onClose: () => void; - onConfirm: (data?: T) => void | Promise<void>; } export default class ConfirmModal<T = string> extends React.PureComponent<Props<T>> { @@ -82,9 +85,10 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props< }; render() { - const { header } = this.props; + const { header, onClose, medium, noBackdrop, large, simple } = this.props; + const modalProps = { header, onClose, medium, noBackdrop, large, simple }; return ( - <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}> + <SimpleModal onSubmit={this.handleSubmit} {...modalProps}> {this.renderModalContent} </SimpleModal> ); diff --git a/server/sonar-web/src/main/js/components/controls/Modal.tsx b/server/sonar-web/src/main/js/components/controls/Modal.tsx index 7f91bb4e082..e7b14c3a1df 100644 --- a/server/sonar-web/src/main/js/components/controls/Modal.tsx +++ b/server/sonar-web/src/main/js/components/controls/Modal.tsx @@ -23,7 +23,8 @@ import * as classNames from 'classnames'; ReactModal.setAppElement('#content'); -interface OwnProps { +export interface ModalProps { + children: React.ReactNode; medium?: boolean; noBackdrop?: boolean; large?: boolean; @@ -32,7 +33,7 @@ interface OwnProps { type MandatoryProps = Pick<ReactModal.Props, 'contentLabel'>; -type Props = Partial<ReactModal.Props> & MandatoryProps & OwnProps; +type Props = Partial<ReactModal.Props> & MandatoryProps & ModalProps; export default function Modal(props: Props) { return ( diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.css b/server/sonar-web/src/main/js/components/controls/RadioCard.css new file mode 100644 index 00000000000..a5099a9a239 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/RadioCard.css @@ -0,0 +1,115 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +.radio-card { + display: flex; + flex-direction: column; + width: 450px; + min-height: 210px; + background-color: #fff; + border: solid 1px var(--barBorderColor); + border-radius: 3px; + box-sizing: border-box; + margin-right: calc(2 * var(--gridSize)); + transition: all 0.2s ease; +} + +.radio-card.animated { + height: 0; + border-width: 0; + overflow: hidden; +} + +.radio-card.animated.open { + height: 210px; + border-width: 1px; +} + +.radio-card.highlight { + box-shadow: var(--defaultShadow); +} + +.radio-card:last-child { + margin-right: 0; +} + +.radio-card:focus { + outline: none; +} + +.radio-card-actionable { + cursor: pointer; +} + +.radio-card-actionable:not(.disabled):hover { + box-shadow: var(--defaultShadow); + transform: translateY(-2px); +} + +.radio-card-actionable.selected { + border-color: var(--darkBlue); +} + +.radio-card-actionable.selected .radio-card-recommended { + border: solid 1px var(--darkBlue); + border-top: none; +} + +.radio-card-actionable.disabled { + cursor: not-allowed; + background-color: var(--disableGrayBg); + border-color: var(--disableGrayBorder); +} + +.radio-card-actionable.disabled h2, +.radio-card-actionable.disabled ul { + color: var(--disableGrayText); +} + +.radio-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0; +} + +.radio-card-body { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)); +} + +.radio-card-body .alert { + margin-bottom: 0; +} + +.radio-card-recommended { + position: relative; + padding: 6px calc(var(--gridSize) * 2); + left: -1px; + bottom: -1px; + width: 450px; + color: #fff; + background-color: var(--blue); + border-radius: 0 0 3px 3px; + box-sizing: border-box; + font-size: var(--smallFontSize); +} diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.tsx b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx new file mode 100644 index 00000000000..96726369abf --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; +import RecommendedIcon from '../icons-components/RecommendedIcon'; +import { translate } from '../../helpers/l10n'; +import './RadioCard.css'; + +export interface RadioCardProps { + className?: string; + disabled?: boolean; + onClick?: () => void; + selected?: boolean; +} + +interface Props extends RadioCardProps { + children: React.ReactNode; + recommended?: string; + title: React.ReactNode; + titleInfo?: React.ReactNode; +} + +export default function RadioCard(props: Props) { + const { className, disabled, onClick, recommended, selected, titleInfo } = props; + const isActionable = Boolean(onClick); + return ( + <div + aria-checked={selected} + className={classNames( + 'radio-card', + { 'radio-card-actionable': isActionable, disabled, selected }, + className + )} + onClick={isActionable && !disabled ? onClick : undefined} + role="radio" + tabIndex={0}> + <h2 className="radio-card-header big-spacer-bottom"> + <span className="display-flex-center"> + {isActionable && ( + <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} /> + )} + {props.title} + </span> + {titleInfo} + </h2> + <div className="radio-card-body">{props.children}</div> + {recommended && ( + <div className="radio-card-recommended"> + <RecommendedIcon className="spacer-right" /> + <FormattedMessage + defaultMessage={recommended} + id={recommended} + values={{ recommended: <strong>{translate('recommended')}</strong> }} + /> + </div> + )} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx index a8a1ba1fe2d..9870f0e6ec7 100644 --- a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx +++ b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Modal from './Modal'; +import Modal, { ModalProps } from './Modal'; export interface ChildrenProps { onCloseClick: (event?: React.SyntheticEvent<HTMLElement>) => void; @@ -27,7 +27,7 @@ export interface ChildrenProps { submitting: boolean; } -interface Props { +interface Props extends ModalProps { children: (props: ChildrenProps) => React.ReactNode; header: string; onClose: () => void; @@ -86,9 +86,10 @@ export default class SimpleModal extends React.Component<Props, State> { }; render() { + const { children, header, onClose, onSubmit, ...modalProps } = this.props; return ( - <Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}> - {this.props.children({ + <Modal contentLabel={header} onRequestClose={onClose} {...modalProps}> + {children({ onCloseClick: this.handleCloseClick, onFormSubmit: this.handleFormSubmit, onSubmitClick: this.handleSubmitClick, diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx new file mode 100644 index 00000000000..da11ea3458c --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import RadioCard from '../RadioCard'; +import { click } from '../../../helpers/testUtils'; + +it('should render correctly', () => { + expect( + shallow( + <RadioCard recommended="Recommended for you" title="Radio Card" titleInfo="info"> + <div>content</div> + </RadioCard> + ) + ).toMatchSnapshot(); +}); + +it('should be actionable', () => { + const onClick = jest.fn(); + const wrapper = shallow( + <RadioCard onClick={onClick} title="Radio Card"> + <div>content</div> + </RadioCard> + ); + + expect(wrapper).toMatchSnapshot(); + click(wrapper); + wrapper.setProps({ selected: true, titleInfo: 'info' }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap new file mode 100644 index 00000000000..4394ef55ac3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should be actionable 1`] = ` +<div + className="radio-card radio-card-actionable" + onClick={[MockFunction]} + role="radio" + tabIndex={0} +> + <h2 + className="radio-card-header big-spacer-bottom" + > + <span + className="display-flex-center" + > + <i + className="icon-radio spacer-right" + /> + Radio Card + </span> + </h2> + <div + className="radio-card-body" + > + <div> + content + </div> + </div> +</div> +`; + +exports[`should be actionable 2`] = ` +<div + aria-checked={true} + className="radio-card radio-card-actionable selected" + onClick={ + [MockFunction] { + "calls": Array [ + Array [ + Object { + "currentTarget": Object { + "blur": [Function], + }, + "preventDefault": [Function], + "stopPropagation": [Function], + "target": Object { + "blur": [Function], + }, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + role="radio" + tabIndex={0} +> + <h2 + className="radio-card-header big-spacer-bottom" + > + <span + className="display-flex-center" + > + <i + className="icon-radio spacer-right is-checked" + /> + Radio Card + </span> + info + </h2> + <div + className="radio-card-body" + > + <div> + content + </div> + </div> +</div> +`; + +exports[`should render correctly 1`] = ` +<div + className="radio-card" + role="radio" + tabIndex={0} +> + <h2 + className="radio-card-header big-spacer-bottom" + > + <span + className="display-flex-center" + > + Radio Card + </span> + info + </h2> + <div + className="radio-card-body" + > + <div> + content + </div> + </div> + <div + className="radio-card-recommended" + > + <RecommendedIcon + className="spacer-right" + /> + <FormattedMessage + defaultMessage="Recommended for you" + id="Recommended for you" + values={ + Object { + "recommended": <strong> + recommended + </strong>, + } + } + /> + </div> +</div> +`; |