this.zoom = zoom()
.scaleExtent([1, 10])
.on('zoom', this.zoomed);
- // TODO: Check why as any cast is necessary now.
select(this.node).call(this.zoom as any);
};
});
};
- resetZoom = (event: React.MouseEvent<Link>) => {
- event.stopPropagation();
- event.preventDefault();
+ resetZoom = (e: React.MouseEvent<Link>) => {
+ e.stopPropagation();
+ e.preventDefault();
if (this.zoom && this.node) {
- // TODO: Check why as any cast is necessary now.
select(this.node).call(this.zoom.transform as any, zoomIdentity);
}
};
}
getTicks(scale: Scale, format: (d: number) => string) {
- const zoom = Math.ceil(this.state.transform.k);
- const ticks = scale.ticks(TICKS_COUNT * zoom).map(tick => format(tick));
+ const zoomAmount = Math.ceil(this.state.transform.k);
+ const ticks = scale.ticks(TICKS_COUNT * zoomAmount).map(tick => format(tick));
const uniqueTicksCount = uniq(ticks).length;
const ticksCount =
- uniqueTicksCount < TICKS_COUNT * zoom ? uniqueTicksCount - 1 : TICKS_COUNT * zoom;
+ uniqueTicksCount < TICKS_COUNT * zoomAmount ? uniqueTicksCount - 1 : TICKS_COUNT * zoomAmount;
return scale.ticks(ticksCount);
}
}
function Bubble<T>(props: BubbleProps<T>) {
- const handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
+ const handleClick = (e: React.MouseEvent<SVGCircleElement>) => {
if (props.onClick) {
- event.stopPropagation();
- event.preventDefault();
+ e.stopPropagation();
+ e.preventDefault();
props.onClick(props.data);
}
};
const width = Math.round(xScale(d)) + /* minimum bar width */ 1;
const x = xScale.range()[0] + (alignTicks ? padding[3] : 0);
- const y = Math.round(yScale(index)! + yScale.bandwidth() / 2);
+ const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2);
return <rect className="bar-chart-bar" height={BAR_HEIGHT} width={width} x={x} y={y} />;
}
}
const x = xScale(d) + (alignTicks ? padding[3] : 0);
- const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+ const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
return (
<Tooltip overlay={this.props.yTooltips && this.props.yTooltips[index]}>
}
const x = xScale.range()[0];
- const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+ const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
const historyTickClass = alignTicks ? 'histogram-tick-start' : 'histogram-tick';
return (
}
type XScale = ScaleTime<number, number>;
-// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+// It should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but in order
+// to make it work, we need to write a lot of type guards :-(. This introduces a lot of unnecessary code,
+// not to mention overhead at runtime. The simplest is just to cast to any, and rely on D3's internals
+// to make it work.
type YScale = any;
export default class ZoomTimeLine extends React.PureComponent<Props, State> {
this.handleZoomUpdate(xScale, xDim);
};
- handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta?: boolean) => (
+ handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta = false) => (
_: MouseEvent,
data: DraggableData
) => {
fixedX: number,
xDim: number[],
handleDirection: string,
- checkDelta?: boolean
+ checkDelta = false
) => (_: MouseEvent, data: DraggableData) => {
if (!checkDelta || data.deltaX) {
const x = Math.max(xDim[0], Math.min(data.x, xDim[1]));
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { mount } from 'enzyme';
+import { select } from 'd3-selection';
+import { zoom } from 'd3-zoom';
+import { shallow } from 'enzyme';
import * as React from 'react';
-import { AutoSizerProps } from 'react-virtualized';
+import { Link } from 'react-router';
+import { AutoSizer, AutoSizerProps } from 'react-virtualized/dist/commonjs/AutoSizer';
+import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component';
+import { mockHtmlElement } from '../../../helpers/mocks/dom';
+import { mockEvent } from '../../../helpers/testMocks';
+import { click } from '../../../helpers/testUtils';
import BubbleChart from '../BubbleChart';
jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN })
}));
+jest.mock('d3-selection', () => ({
+ event: { transform: { x: 10, y: 10, k: 20 } },
+ select: jest.fn().mockReturnValue({ call: jest.fn() })
+}));
+
+jest.mock('d3-zoom', () => ({
+ ...jest.requireActual('d3-zoom'),
+ zoom: jest.fn()
+}));
+
+beforeEach(jest.clearAllMocks);
+
it('should display bubbles', () => {
- const items = [
- { x: 1, y: 10, size: 7 },
- { x: 2, y: 30, size: 5 }
- ];
- const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
- chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
-
- chart.setProps({ height: 120 });
+ const wrapper = shallowRender();
+ wrapper
+ .find(AutoSizer)
+ .dive()
+ .find('Bubble')
+ .forEach(bubble => {
+ expect(bubble.dive()).toMatchSnapshot();
+ });
});
it('should render bubble links', () => {
- const items = [
- { x: 1, y: 10, size: 7, link: 'foo' },
- { x: 2, y: 30, size: 5, link: 'bar' }
- ];
- const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
- chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
+ const wrapper = shallowRender({
+ items: [
+ { x: 1, y: 10, size: 7, link: 'foo' },
+ { x: 2, y: 30, size: 5, link: 'bar' }
+ ]
+ });
+ wrapper
+ .find(AutoSizer)
+ .dive()
+ .find('Bubble')
+ .forEach(bubble => {
+ expect(bubble.dive()).toMatchSnapshot();
+ });
});
it('should render bubbles with click handlers', () => {
- const onClick = jest.fn();
- const items = [
- { x: 1, y: 10, size: 7, data: 'foo' },
- { x: 2, y: 30, size: 5, data: 'bar' }
- ];
- const chart = mount(
- <BubbleChart height={100} items={items} onBubbleClick={onClick} padding={[0, 0, 0, 0]} />
- );
- chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot());
+ const onBubbleClick = jest.fn();
+ const wrapper = shallowRender({ onBubbleClick });
+ wrapper
+ .find(AutoSizer)
+ .dive()
+ .find('Bubble')
+ .forEach(bubble => {
+ click(bubble.dive().find('circle'));
+ expect(bubble.dive()).toMatchSnapshot();
+ });
+ expect(onBubbleClick).toBeCalledTimes(2);
+ expect(onBubbleClick).toHaveBeenLastCalledWith(mockComponentMeasureEnhanced());
+});
+
+it('should correctly handle zooming', () => {
+ class ZoomBehaviorMock {
+ on = () => this;
+ scaleExtent = () => this;
+ translateExtent = () => this;
+ }
+
+ const call = jest.fn();
+ const zoomBehavior = new ZoomBehaviorMock();
+ (select as jest.Mock).mockReturnValueOnce({ call });
+ (zoom as jest.Mock).mockReturnValueOnce(zoomBehavior);
+
+ return new Promise<void>((resolve, reject) => {
+ const wrapper = shallowRender({ padding: [5, 5, 5, 5] });
+ wrapper.instance().boundNode(
+ mockHtmlElement<SVGSVGElement>({
+ getBoundingClientRect: () => ({ width: 100, height: 100 } as DOMRect)
+ })
+ );
+
+ // Call zoom event handler.
+ wrapper.instance().zoomed();
+ expect(wrapper.state().transform).toEqual({
+ x: 105,
+ y: 105,
+ k: 20
+ });
+
+ // Reset Zoom levels.
+ const resetZoomClick = wrapper
+ .find('div.bubble-chart-zoom')
+ .find(Link)
+ .props().onClick;
+ if (!resetZoomClick) {
+ reject();
+ return;
+ }
+
+ const stopPropagation = jest.fn();
+ const preventDefault = jest.fn();
+ resetZoomClick(mockEvent({ stopPropagation, preventDefault }));
+ expect(stopPropagation).toBeCalled();
+ expect(preventDefault).toBeCalled();
+ expect(call).toHaveBeenCalledWith(zoomBehavior);
+
+ resolve();
+ });
});
+
+function shallowRender(props: Partial<BubbleChart<T.ComponentMeasureEnhanced>['props']> = {}) {
+ return shallow<BubbleChart<T.ComponentMeasureEnhanced>>(
+ <BubbleChart
+ height={100}
+ items={[
+ { x: 1, y: 10, size: 7, data: mockComponentMeasureEnhanced() },
+ { x: 2, y: 30, size: 5, data: mockComponentMeasureEnhanced() }
+ ]}
+ padding={[0, 0, 0, 0]}
+ {...props}
+ />
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { scaleBand } from 'd3-scale';
import { shallow } from 'enzyme';
import * as React from 'react';
import Histogram from '../Histogram';
-it('renders', () => {
- expect(shallow(<Histogram bars={[100, 75, 150]} height={75} width={100} />)).toMatchSnapshot();
+jest.mock('d3-scale', () => {
+ const d3 = jest.requireActual('d3-scale');
+ return {
+ ...d3,
+ scaleBand: jest.fn(d3.scaleBand)
+ };
});
-it('renders with yValues', () => {
- expect(
- shallow(
- <Histogram
- bars={[100, 75, 150]}
- height={75}
- width={100}
- yValues={['100.0', '75.0', '150.0']}
- />
- )
- ).toMatchSnapshot();
-});
+beforeEach(jest.clearAllMocks);
-it('renders with yValues and yTicks', () => {
+it('renders correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ alignTicks: true })).toMatchSnapshot('align ticks');
+ expect(shallowRender({ yValues: ['100.0', '75.0', '150.0'] })).toMatchSnapshot('with yValues');
expect(
- shallow(
- <Histogram
- bars={[100, 75, 150]}
- height={75}
- width={100}
- yTicks={['a', 'b', 'c']}
- yValues={['100.0', '75.0', '150.0']}
- />
- )
- ).toMatchSnapshot();
+ shallowRender({ yTicks: ['a', 'b', 'c'], yValues: ['100.0', '75.0', '150.0'] })
+ ).toMatchSnapshot('with yValues and yTicks');
+ expect(
+ shallowRender({
+ yTicks: ['a', 'b', 'c'],
+ yTooltips: ['a - 100', 'b - 75', 'c - 150'],
+ yValues: ['100.0', '75.0', '150.0']
+ })
+ ).toMatchSnapshot('with yValues, yTicks and yTooltips');
});
-it('renders with yValues, yTicks and yTooltips', () => {
+it('correctly handles yScale() returning undefined', () => {
+ const yScale = () => undefined;
+ yScale.bandwidth = () => 1;
+
+ (scaleBand as jest.Mock).mockReturnValueOnce({
+ domain: () => ({ rangeRound: () => yScale })
+ });
+
expect(
- shallow(
- <Histogram
- bars={[100, 75, 150]}
- height={75}
- width={100}
- yTicks={['a', 'b', 'c']}
- yTooltips={['a - 100', 'b - 75', 'c - 150']}
- yValues={['100.0', '75.0', '150.0']}
- />
- )
+ shallowRender({ yValues: ['100.0', '75.0', '150.0'], yTicks: ['a', 'b', 'c'] })
).toMatchSnapshot();
});
+
+function shallowRender(props: Partial<Histogram['props']> = {}) {
+ return shallow<Histogram>(<Histogram bars={[100, 75, 150]} height={75} width={100} {...props} />);
+}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should display bubbles 1`] = `
-<Bubble
- key="0"
- r={45}
- scale={1}
- x={33.21428571428571}
- y={70.07936507936509}
->
- <Tooltip>
- <g>
- <circle
- className="bubble-chart-bubble"
- r={45}
- style={
- Object {
- "fill": undefined,
- "stroke": undefined,
- }
+<Tooltip>
+ <g>
+ <circle
+ className="bubble-chart-bubble"
+ r={45}
+ style={
+ Object {
+ "fill": undefined,
+ "stroke": undefined,
}
- transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
- />
- </g>
- </Tooltip>
-</Bubble>
+ }
+ transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+ />
+ </g>
+</Tooltip>
`;
exports[`should display bubbles 2`] = `
-<Bubble
- key="1"
- r={33.57142857142858}
- scale={1}
- x={66.42857142857142}
- y={33.57142857142858}
->
- <Tooltip>
- <g>
- <circle
- className="bubble-chart-bubble"
- r={33.57142857142858}
- style={
- Object {
- "fill": undefined,
- "stroke": undefined,
- }
+<Tooltip>
+ <g>
+ <circle
+ className="bubble-chart-bubble"
+ r={33.57142857142858}
+ style={
+ Object {
+ "fill": undefined,
+ "stroke": undefined,
}
- transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
- />
- </g>
- </Tooltip>
-</Bubble>
+ }
+ transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+ />
+ </g>
+</Tooltip>
`;
exports[`should render bubble links 1`] = `
-<Bubble
- key="0"
- link="foo"
- r={45}
- scale={1}
- x={33.21428571428571}
- y={70.07936507936509}
->
- <Tooltip>
- <g>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="foo"
- >
- <a
- onClick={[Function]}
- style={Object {}}
- >
- <circle
- className="bubble-chart-bubble"
- r={45}
- style={
- Object {
- "fill": undefined,
- "stroke": undefined,
- }
- }
- transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
- />
- </a>
- </Link>
- </g>
- </Tooltip>
-</Bubble>
-`;
-
-exports[`should render bubble links 2`] = `
-<Bubble
- key="1"
- link="bar"
- r={33.57142857142858}
- scale={1}
- x={66.42857142857142}
- y={33.57142857142858}
->
- <Tooltip>
- <g>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="bar"
- >
- <a
- onClick={[Function]}
- style={Object {}}
- >
- <circle
- className="bubble-chart-bubble"
- r={33.57142857142858}
- style={
- Object {
- "fill": undefined,
- "stroke": undefined,
- }
- }
- transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
- />
- </a>
- </Link>
- </g>
- </Tooltip>
-</Bubble>
-`;
-
-exports[`should render bubbles with click handlers 1`] = `
-<Bubble
- data="foo"
- key="0"
- onClick={[MockFunction]}
- r={45}
- scale={1}
- x={33.21428571428571}
- y={70.07936507936509}
->
- <Tooltip>
- <g>
+<Tooltip>
+ <g>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="foo"
+ >
<circle
className="bubble-chart-bubble"
- onClick={[Function]}
r={45}
style={
Object {
}
transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
/>
- </g>
- </Tooltip>
-</Bubble>
+ </Link>
+ </g>
+</Tooltip>
`;
-exports[`should render bubbles with click handlers 2`] = `
-<Bubble
- data="bar"
- key="1"
- onClick={[MockFunction]}
- r={33.57142857142858}
- scale={1}
- x={66.42857142857142}
- y={33.57142857142858}
->
- <Tooltip>
- <g>
+exports[`should render bubble links 2`] = `
+<Tooltip>
+ <g>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="bar"
+ >
<circle
className="bubble-chart-bubble"
- onClick={[Function]}
r={33.57142857142858}
style={
Object {
}
transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
/>
- </g>
- </Tooltip>
-</Bubble>
+ </Link>
+ </g>
+</Tooltip>
+`;
+
+exports[`should render bubbles with click handlers 1`] = `
+<Tooltip>
+ <g>
+ <circle
+ className="bubble-chart-bubble"
+ onClick={[Function]}
+ r={45}
+ style={
+ Object {
+ "fill": undefined,
+ "stroke": undefined,
+ }
+ }
+ transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+ />
+ </g>
+</Tooltip>
+`;
+
+exports[`should render bubbles with click handlers 2`] = `
+<Tooltip>
+ <g>
+ <circle
+ className="bubble-chart-bubble"
+ onClick={[Function]}
+ r={33.57142857142858}
+ style={
+ Object {
+ "fill": undefined,
+ "stroke": undefined,
+ }
+ }
+ transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+ />
+ </g>
+</Tooltip>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders 1`] = `
+exports[`correctly handles yScale() returning undefined 1`] = `
+<svg
+ className="bar-chart"
+ height={75}
+ width={100}
+>
+ <g
+ transform="translate(10, 10)"
+ >
+ <g>
+ <g
+ key="0"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={54}
+ x={0}
+ y={1}
+ />
+ <Tooltip>
+ <text
+ className="bar-chart-tick histogram-value"
+ dx="1em"
+ dy="0.3em"
+ x={53.33333333333333}
+ y={6}
+ >
+ 100.0
+ </text>
+ </Tooltip>
+ <text
+ className="bar-chart-tick histogram-tick"
+ dx="-1em"
+ dy="0.3em"
+ x={0}
+ y={6}
+ >
+ a
+ </text>
+ </g>
+ <g
+ key="1"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={41}
+ x={0}
+ y={1}
+ />
+ <Tooltip>
+ <text
+ className="bar-chart-tick histogram-value"
+ dx="1em"
+ dy="0.3em"
+ x={40}
+ y={6}
+ >
+ 75.0
+ </text>
+ </Tooltip>
+ <text
+ className="bar-chart-tick histogram-tick"
+ dx="-1em"
+ dy="0.3em"
+ x={0}
+ y={6}
+ >
+ b
+ </text>
+ </g>
+ <g
+ key="2"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={81}
+ x={0}
+ y={1}
+ />
+ <Tooltip>
+ <text
+ className="bar-chart-tick histogram-value"
+ dx="1em"
+ dy="0.3em"
+ x={80}
+ y={6}
+ >
+ 150.0
+ </text>
+ </Tooltip>
+ <text
+ className="bar-chart-tick histogram-tick"
+ dx="-1em"
+ dy="0.3em"
+ x={0}
+ y={6}
+ >
+ c
+ </text>
+ </g>
+ </g>
+ </g>
+</svg>
+`;
+
+exports[`renders correctly: align ticks 1`] = `
+<svg
+ className="bar-chart"
+ height={75}
+ width={100}
+>
+ <g
+ transform="translate(4, 10)"
+ >
+ <g>
+ <g
+ key="0"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={54}
+ x={10}
+ y={10}
+ />
+ </g>
+ <g
+ key="1"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={41}
+ x={10}
+ y={28}
+ />
+ </g>
+ <g
+ key="2"
+ >
+ <rect
+ className="bar-chart-bar"
+ height={10}
+ width={81}
+ x={10}
+ y={46}
+ />
+ </g>
+ </g>
+ </g>
+</svg>
+`;
+
+exports[`renders correctly: default 1`] = `
<svg
className="bar-chart"
height={75}
</svg>
`;
-exports[`renders with yValues 1`] = `
+exports[`renders correctly: with yValues 1`] = `
<svg
className="bar-chart"
height={75}
</svg>
`;
-exports[`renders with yValues and yTicks 1`] = `
+exports[`renders correctly: with yValues and yTicks 1`] = `
<svg
className="bar-chart"
height={75}
</svg>
`;
-exports[`renders with yValues, yTicks and yTooltips 1`] = `
+exports[`renders correctly: with yValues, yTicks and yTooltips 1`] = `
<svg
className="bar-chart"
height={75}
handleSubmit = () => {
const result = this.props.onConfirm(this.props.confirmData);
if (result) {
- return result.then(this.props.onClose, () => {});
+ return result.then(this.props.onClose, () => {
+ /* noop */
+ });
}
this.props.onClose();
return undefined;
placement?: PopupPlacement;
}
-// TODO use the same styling for <Select />
-// TODO use the same styling for <DateInput />
-
export class DropdownOverlay extends React.Component<OverlayProps> {
get placement() {
return this.props.placement || PopupPlacement.Bottom;
import { cutLongWords } from '../../helpers/path';
import { ClearButton } from './buttons';
-interface Message {
+interface IMessage {
id: string;
level: 'ERROR' | 'SUCCESS';
message: string;
export interface GlobalMessagesProps {
closeGlobalMessage: (id: string) => void;
- messages: Message[];
+ messages: IMessage[];
}
export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) {
export class GlobalMessage extends React.PureComponent<{
closeGlobalMessage: (id: string) => void;
- message: Message;
+ message: IMessage;
}> {
handleClose = () => {
this.props.closeGlobalMessage(this.props.message.id);
}
`;
-const Message = styled.div<Pick<Message, 'level'>>`
+const Message = styled.div<Pick<IMessage, 'level'>>`
position: relative;
padding: 0 30px 0 10px;
line-height: ${sizes.controlHeight};
}
`;
-const CloseButton = styled(ClearButton)<Pick<Message, 'level'>>`
+const CloseButton = styled(ClearButton)<Pick<IMessage, 'level'>>`
position: absolute;
top: calc(${sizes.gridSize} / 4);
right: calc(${sizes.gridSize} / 4);
id?: string;
label?: React.ReactNode;
name: string;
- onBlur: (event: React.FocusEvent<any>) => void;
- onChange: (event: React.ChangeEvent<any>) => void;
+ onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
touched: boolean | undefined;
type?: string;
renderOption = (option: Option) => {
const checked = option.value === this.props.value;
- const htmlId = this.props.name + '__' + option.value;
+ const htmlId = `${this.props.name}__${option.value}`;
return (
<li key={option.value.toString()}>
<input
topFix?: number;
}
+const EDGE_MARGIN = rawSizes.grid / 2;
+
export default class ScreenPositionFixer extends React.Component<Props, Fixes> {
throttledPosition: () => void;
};
position = () => {
- const edgeMargin = 0.5 * rawSizes.grid;
-
// eslint-disable-next-line react/no-find-dom-node
const node = findDOMNode(this);
if (node && node instanceof Element) {
const { clientHeight, clientWidth } = document.documentElement;
let leftFix = 0;
- if (left < edgeMargin) {
- leftFix = edgeMargin - left;
- } else if (left + width > clientWidth - edgeMargin) {
- leftFix = clientWidth - edgeMargin - left - width;
+ if (left < EDGE_MARGIN) {
+ leftFix = EDGE_MARGIN - left;
+ } else if (left + width > clientWidth - EDGE_MARGIN) {
+ leftFix = clientWidth - EDGE_MARGIN - left - width;
}
let topFix = 0;
- if (top < edgeMargin) {
- topFix = edgeMargin - top;
- } else if (top + height > clientHeight - edgeMargin) {
- topFix = clientHeight - edgeMargin - top - height;
+ if (top < EDGE_MARGIN) {
+ topFix = EDGE_MARGIN - top;
+ } else if (top + height > clientHeight - EDGE_MARGIN) {
+ topFix = clientHeight - EDGE_MARGIN - top - height;
}
this.setState({ leftFix, topFix });
<div
className={classNames('search-box', this.props.className)}
id={this.props.id}
- title={tooShort ? translateWithParameters('select2.tooShort', minLength!) : ''}>
+ title={
+ tooShort && minLength !== undefined
+ ? translateWithParameters('select2.tooShort', minLength)
+ : ''
+ }>
<input
aria-label={translate('search_verb')}
autoComplete="off"
/>
)}
- {tooShort && (
+ {tooShort && minLength !== undefined && (
<span className="search-box-note">
- {translateWithParameters('select2.tooShort', minLength!)}
+ {translateWithParameters('select2.tooShort', minLength)}
</span>
)}
</div>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import {
- defaultFilterOptions as reactSelectDefaultFilterOptions,
+import ReactSelectClass, {
ReactAsyncSelectProps,
ReactCreatableSelectProps,
ReactSelectProps
import { ClearButton } from './buttons';
import './Select.css';
-declare module 'react-select' {
- export function defaultFilterOptions(...args: any[]): any;
-}
-
const ReactSelectLib = import('react-select');
const ReactSelect = lazyLoadComponent(() => ReactSelectLib);
const ReactCreatable = lazyLoadComponent(() =>
return <ClearButton className="button-tiny spacer-left text-middle" iconProps={{ size: 12 }} />;
}
-interface WithInnerRef {
- innerRef?: (element: React.Component) => void;
+export interface WithInnerRef {
+ innerRef?: React.Ref<ReactSelectClass<unknown>>;
}
export default function Select({ innerRef, ...props }: WithInnerRef & ReactSelectProps) {
- // TODO try to define good defaults, if any
- // ReactSelect doesn't declare `clearRenderer` prop
- const ReactSelectAny = ReactSelect as any;
// hide the "x" icon when select is empty
const clearable = props.clearable ? Boolean(props.value) : false;
return (
- <ReactSelectAny {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
+ <ReactSelect {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
);
}
-export const defaultFilterOptions = reactSelectDefaultFilterOptions;
-
export function Creatable(props: ReactCreatableSelectProps) {
- // ReactSelect doesn't declare `clearRenderer` prop
- const ReactCreatableAny = ReactCreatable as any;
- return <ReactCreatableAny {...props} clearRenderer={renderInput} />;
+ return <ReactCreatable {...props} clearRenderer={renderInput} />;
}
-// TODO figure out why `ref` prop is incompatible
-export function AsyncSelect(props: ReactAsyncSelectProps & { ref?: any }) {
+export function AsyncSelect(props: ReactAsyncSelectProps & WithInnerRef) {
return <ReactAsync {...props} />;
}
border-radius: 4px;
overflow: hidden;
word-break: break-word;
-}
-
-.tooltip-inner {
padding: 12px 17px;
color: #fff;
background-color: #475760;
}
const { classNameSpace = 'tooltip' } = this.props;
- const placement = this.getPlacement();
+ const currentPlacement = this.getPlacement();
const style = isMeasured(this.state)
? {
left: this.state.left + leftFix,
return (
<div
- className={`${classNameSpace} ${placement}`}
+ className={`${classNameSpace} ${currentPlacement}`}
onMouseEnter={this.handleOverlayMouseEnter}
onMouseLeave={this.handleOverlayMouseLeave}
ref={this.tooltipNodeRef}
className={`${classNameSpace}-arrow`}
style={
isMeasured(this.state)
- ? this.adjustArrowPosition(placement, { leftFix, topFix })
+ ? this.adjustArrowPosition(currentPlacement, { leftFix, topFix })
: undefined
}
/>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { ReactAsyncSelectProps, ReactCreatableSelectProps, ReactSelectProps } from 'react-select';
+import Select, { AsyncSelect, Creatable, WithInnerRef } from '../Select';
+
+describe('Select', () => {
+ it('should render correctly', () => {
+ return new Promise<void>((resolve, reject) => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ clearable: true, value: undefined })).toMatchSnapshot(
+ 'disable clear button if no value'
+ );
+
+ const clearRenderFn = shallowRender().props().clearRenderer;
+ if (!clearRenderFn) {
+ reject();
+ return;
+ }
+ expect(clearRenderFn()).toMatchSnapshot('clear button');
+
+ resolve();
+ });
+ });
+
+ function shallowRender(props: Partial<WithInnerRef & ReactSelectProps> = {}) {
+ return shallow<WithInnerRef & ReactSelectProps>(<Select value="foo" {...props} />);
+ }
+});
+
+describe('Creatable', () => {
+ it('should render correctly', () => {
+ return new Promise<void>((resolve, reject) => {
+ expect(shallowRender()).toMatchSnapshot('default');
+
+ const clearRenderFn = shallowRender().props().clearRenderer;
+ if (!clearRenderFn) {
+ reject();
+ return;
+ }
+ expect(clearRenderFn()).toMatchSnapshot('clear button');
+
+ resolve();
+ });
+ });
+
+ function shallowRender(props: Partial<ReactCreatableSelectProps> = {}) {
+ return shallow<ReactCreatableSelectProps>(<Creatable {...props} />);
+ }
+});
+
+describe('AsyncSelect', () => {
+ it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ });
+
+ function shallowRender(props: Partial<WithInnerRef & ReactAsyncSelectProps> = {}) {
+ return shallow<WithInnerRef & ReactAsyncSelectProps>(
+ <AsyncSelect loadOptions={jest.fn()} {...props} />
+ );
+ }
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AsyncSelect should render correctly: default 1`] = `
+<LazyComponentWrapper
+ loadOptions={[MockFunction]}
+/>
+`;
+
+exports[`Creatable should render correctly: clear button 1`] = `
+<ClearButton
+ className="button-tiny spacer-left text-middle"
+ iconProps={
+ Object {
+ "size": 12,
+ }
+ }
+/>
+`;
+
+exports[`Creatable should render correctly: default 1`] = `
+<LazyComponentWrapper
+ clearRenderer={[Function]}
+/>
+`;
+
+exports[`Select should render correctly: clear button 1`] = `
+<ClearButton
+ className="button-tiny spacer-left text-middle"
+ iconProps={
+ Object {
+ "size": 12,
+ }
+ }
+/>
+`;
+
+exports[`Select should render correctly: default 1`] = `
+<LazyComponentWrapper
+ clearRenderer={[Function]}
+ clearable={false}
+ value="foo"
+/>
+`;
+
+exports[`Select should render correctly: disable clear button if no value 1`] = `
+<LazyComponentWrapper
+ clearRenderer={[Function]}
+ clearable={false}
+/>
+`;
onClick?: () => void;
preventDefault?: boolean;
stopPropagation?: boolean;
- type?: 'button' | 'submit' | 'reset' | undefined;
+ type?: 'button' | 'submit' | 'reset';
}
export class Button extends React.PureComponent<ButtonProps> {
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingAddMembersIcon({ fill, size = 64, ...iconProps }: IconProps) {
- return (
- <Icon height={(size / 64) * 80} viewBox="0 0 64 80" width={size} {...iconProps}>
- <g>
- <path
- d="M49 34c0 9.389-7.611 17-17 17s-17-7.611-17-17 7.611-17 17-17 17 7.611 17 17z"
- style={{ fill: 'none', stroke: fill || colors.darkBlue, strokeWidth: 2 }}
- />
- <path
- d="M36 32c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm4 39a8 8 0 1 1-16 0 8 8 0 0 1 16 0z"
- style={{ fill: 'none', stroke: fill || colors.darkBlue, strokeWidth: 2 }}
- />
- <path
- d="M33 70h2v2h-2v2h-2v-2h-2v-2h2v-2h2v2zm-5-14l-.072-.001c-1.521-.054-2.834-1.337-2.925-2.855L25 50h2c0 1.745-.532 3.91.952 3.999L28 54h8v.002l.072-.005c.506-.042.922-.489.928-1.003V50h2c0 1.024.011 2.048-.001 3.072-.054 1.518-1.337 2.834-2.855 2.925l-.072.002L36 56v8h-2v-7.982c-1.333.007-2.667.007-4 0V64h-2v-8zm-7 0H1V10 0h62v56H43v-2h18V10H3v44h18v2zm38-4H43v-2h14V14H7v36h14v2H5V12h54v40zm-19-9l1 .017c-.03 1.79-2.454 2.506-3.918 2.717-4.074.584-8.503.911-12.176-.477-.949-.358-1.887-1.119-1.906-2.24l.191-.017H23v-3.566l5.38-3.228.913-.913 1.414 1.414-1.087 1.087L25 40.566v2.438c.067 1.304 10.98 2.117 13.844.157.076-.052.152-.172.156-.178v-2.417l-4.62-2.772-1.087-1.087 1.414-1.414.913.913L41 39.434V43h-1zm14-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm20.198-10.999c3.529.062 6.837 1.669 9.386 4.169l-1.289 1.539c-4.178-4.152-11.167-5.254-16.359-.228l-.231.228-1.41-1.418c2.633-2.617 6.031-4.313 9.903-4.29zM3 2v6h58V2H3zm56 4H17V4h42v2zM11 6H9V4h2v2zM7 6H5V4h2v2zm8 0h-2V4h2v2z"
- style={{ fill: fill || colors.darkBlue, fillRule: 'nonzero' }}
- />
- </g>
- </Icon>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingProjectIcon({ fill, size = 64, ...iconProps }: IconProps) {
- return (
- <Icon size={size} viewBox="0 0 64 64" {...iconProps}>
- <g fill="none" fillRule="evenodd" stroke={fill || colors.darkBlue} strokeWidth="2">
- <path d="M2 59h60V13H2zm0-46h60V5H2zm3-4h2m2 0h2m2 0h2m2 0h42" />
- <path d="M59 34h-6l-2-4h-6l-2 5h-6l-2 2h-6l-2-4h-6l-2 5h-6l-2 4H5m1 14v-9m4 9v-6m4 6V43m4 13V45m4 11V42m4 14V39m4 17V41m4 15V46m4 10V40m4 16V44m4 12V37m4 19V38m4 18V43m4 13V39m-3-18h-2m-2 0h-2m-2 0h-2M9 29h14M9 33h7m17-12h8m-14 4h8m-8-4h4m-21 4h12v-4H10z" />
- <path d="M58 31V17H6v22" />
- </g>
- </Icon>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { colors } from '../../app/theme';
-import Icon, { IconProps } from './Icon';
-
-export default function OnboardingTeamIcon({ fill, size = 64, ...iconProps }: IconProps) {
- return (
- <Icon size={size} viewBox="0 0 64 64" {...iconProps}>
- <g fill="none" fillRule="evenodd" stroke={fill || colors.darkBlue} strokeWidth="2">
- <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
- <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
- <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
- </g>
- </Icon>
- );
-}
return null;
}
- const Icon = severityIcons[severity.toLowerCase()];
- return Icon ? <Icon {...iconProps} /> : null;
+ const DesiredIcon = severityIcons[severity.toLowerCase()];
+ return DesiredIcon ? <DesiredIcon {...iconProps} /> : null;
}
function BlockerSeverityIcon(iconProps: IconProps) {
};
export default function StatusIcon({ status, ...iconProps }: Props) {
- const Icon = statusIcons[status.toLowerCase()];
- return Icon ? <Icon {...iconProps} /> : null;
+ const DesiredStatusIcon = statusIcons[status.toLowerCase()];
+ return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
}
function OpenStatusIcon(iconProps: IconProps) {
};
export default function TestStatusIcon({ status, ...iconProps }: Props) {
- const Icon = statusIcons[status.toLowerCase()];
- return Icon ? <Icon {...iconProps} /> : null;
+ const DesiredStatusIcon = statusIcons[status.toLowerCase()];
+ return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
}
function OkTestStatusIcon(iconProps: IconProps) {
backGroundColor: string;
}
+const DOUBLE = 2;
+const QUADRUPLE = 4;
+
const StyledAlertIcon = styled.div<{ isBanner: boolean; variantInfo: AlertVariantInformation }>`
flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
- width: calc(${({ isBanner }) => (isBanner ? 2 : 4)} * ${sizes.gridSize});
+ width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize});
border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')};
border-color: ${({ variantInfo }) => variantInfo.borderColor};
`;
import * as classNames from 'classnames';
import * as React from 'react';
import './ContextNavBar.css';
-import NavBar from './NavBar';
+import NavBar, { NavBarProps } from './NavBar';
-interface Props {
+interface Props extends NavBarProps {
className?: string;
+ id?: string;
height: number;
- [attr: string]: any;
}
export default function ContextNavBar({ className, ...other }: Props) {
box-sizing: border-box;
}
-.navbar {
-}
-
.navbar-inner {
position: fixed;
left: 0;
import * as React from 'react';
import './NavBarTabs.css';
-interface Props {
- children?: any;
+interface Props extends React.HTMLAttributes<HTMLUListElement> {
+ children?: React.ReactNode;
className?: string;
- [attr: string]: any;
}
export default function NavBarTabs({ children, className, ...other }: Props) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+
+export function mockHtmlElement<T extends Element>(overrides: Partial<T> = {}): T {
+ return {
+ getBoundingClientRect: () => ({
+ bottom: 0,
+ height: 100,
+ width: 50,
+ left: 0,
+ right: 0,
+ top: 10,
+ x: 12,
+ y: 23,
+ toJSON: () => ''
+ }),
+ ...overrides
+ } as T;
+}
toggleBodyClass(CLASS_NO_FOOTER_PAGE, false);
}
-function toggleBodyClass(className: string, force?: boolean) {
+function toggleBodyClass(className: string, force: boolean) {
document.body.classList.toggle(className, force);
if (document.documentElement) {
document.documentElement.classList.toggle(className, force);