@@ -102,7 +102,6 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> | |||
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); | |||
}; | |||
@@ -118,11 +117,10 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> | |||
}); | |||
}; | |||
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); | |||
} | |||
}; | |||
@@ -144,11 +142,11 @@ export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> | |||
} | |||
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); | |||
} | |||
@@ -360,10 +358,10 @@ interface BubbleProps<T> { | |||
} | |||
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); | |||
} | |||
}; |
@@ -47,7 +47,7 @@ export default class Histogram extends React.PureComponent<Props> { | |||
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} />; | |||
} | |||
@@ -62,7 +62,7 @@ export default class Histogram extends React.PureComponent<Props> { | |||
} | |||
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]}> | |||
@@ -83,7 +83,7 @@ export default class Histogram extends React.PureComponent<Props> { | |||
} | |||
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 ( |
@@ -49,7 +49,10 @@ interface State { | |||
} | |||
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> { | |||
@@ -114,7 +117,7 @@ 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 | |||
) => { | |||
@@ -129,7 +132,7 @@ export default class ZoomTimeLine extends React.PureComponent<Props, State> { | |||
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])); |
@@ -17,43 +17,135 @@ | |||
* 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} | |||
/> | |||
); | |||
} |
@@ -17,52 +17,50 @@ | |||
* 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} />); | |||
} |
@@ -1,148 +1,51 @@ | |||
// 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 { | |||
@@ -152,26 +55,21 @@ exports[`should render bubbles with click handlers 1`] = ` | |||
} | |||
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 { | |||
@@ -181,7 +79,45 @@ exports[`should render bubbles with click handlers 2`] = ` | |||
} | |||
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> | |||
`; |
@@ -1,6 +1,162 @@ | |||
// 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} | |||
@@ -48,7 +204,7 @@ exports[`renders 1`] = ` | |||
</svg> | |||
`; | |||
exports[`renders with yValues 1`] = ` | |||
exports[`renders correctly: with yValues 1`] = ` | |||
<svg | |||
className="bar-chart" | |||
height={75} | |||
@@ -129,7 +285,7 @@ exports[`renders with yValues 1`] = ` | |||
</svg> | |||
`; | |||
exports[`renders with yValues and yTicks 1`] = ` | |||
exports[`renders correctly: with yValues and yTicks 1`] = ` | |||
<svg | |||
className="bar-chart" | |||
height={75} | |||
@@ -237,7 +393,7 @@ exports[`renders with yValues and yTicks 1`] = ` | |||
</svg> | |||
`; | |||
exports[`renders with yValues, yTicks and yTooltips 1`] = ` | |||
exports[`renders correctly: with yValues, yTicks and yTooltips 1`] = ` | |||
<svg | |||
className="bar-chart" | |||
height={75} |
@@ -54,7 +54,9 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props< | |||
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; |
@@ -119,9 +119,6 @@ interface OverlayProps { | |||
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; |
@@ -24,7 +24,7 @@ import { colors, sizes, zIndexes } from '../../app/theme'; | |||
import { cutLongWords } from '../../helpers/path'; | |||
import { ClearButton } from './buttons'; | |||
interface Message { | |||
interface IMessage { | |||
id: string; | |||
level: 'ERROR' | 'SUCCESS'; | |||
message: string; | |||
@@ -32,7 +32,7 @@ interface Message { | |||
export interface GlobalMessagesProps { | |||
closeGlobalMessage: (id: string) => void; | |||
messages: Message[]; | |||
messages: IMessage[]; | |||
} | |||
export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) { | |||
@@ -60,7 +60,7 @@ const MessagesContainer = styled.div` | |||
export class GlobalMessage extends React.PureComponent<{ | |||
closeGlobalMessage: (id: string) => void; | |||
message: Message; | |||
message: IMessage; | |||
}> { | |||
handleClose = () => { | |||
this.props.closeGlobalMessage(this.props.message.id); | |||
@@ -94,7 +94,7 @@ const appearAnim = keyframes` | |||
} | |||
`; | |||
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}; | |||
@@ -112,7 +112,7 @@ const Message = styled.div<Pick<Message, 'level'>>` | |||
} | |||
`; | |||
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); |
@@ -31,8 +31,8 @@ interface Props { | |||
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; |
@@ -46,7 +46,7 @@ export default class RadioToggle extends React.PureComponent<Props> { | |||
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 |
@@ -43,6 +43,8 @@ interface Fixes { | |||
topFix?: number; | |||
} | |||
const EDGE_MARGIN = rawSizes.grid / 2; | |||
export default class ScreenPositionFixer extends React.Component<Props, Fixes> { | |||
throttledPosition: () => void; | |||
@@ -82,8 +84,6 @@ export default class ScreenPositionFixer extends React.Component<Props, Fixes> { | |||
}; | |||
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) { | |||
@@ -91,17 +91,17 @@ export default class ScreenPositionFixer extends React.Component<Props, Fixes> { | |||
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 }); |
@@ -135,7 +135,11 @@ export default class SearchBox extends React.PureComponent<Props, State> { | |||
<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" | |||
@@ -165,9 +169,9 @@ export default class SearchBox extends React.PureComponent<Props, State> { | |||
/> | |||
)} | |||
{tooShort && ( | |||
{tooShort && minLength !== undefined && ( | |||
<span className="search-box-note"> | |||
{translateWithParameters('select2.tooShort', minLength!)} | |||
{translateWithParameters('select2.tooShort', minLength)} | |||
</span> | |||
)} | |||
</div> |
@@ -18,8 +18,7 @@ | |||
* 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 | |||
@@ -28,10 +27,6 @@ import { lazyLoadComponent } from '../lazyLoadComponent'; | |||
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(() => | |||
@@ -43,30 +38,22 @@ function renderInput() { | |||
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} />; | |||
} |
@@ -56,9 +56,6 @@ | |||
border-radius: 4px; | |||
overflow: hidden; | |||
word-break: break-word; | |||
} | |||
.tooltip-inner { | |||
padding: 12px 17px; | |||
color: #fff; | |||
background-color: #475760; |
@@ -334,7 +334,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> { | |||
} | |||
const { classNameSpace = 'tooltip' } = this.props; | |||
const placement = this.getPlacement(); | |||
const currentPlacement = this.getPlacement(); | |||
const style = isMeasured(this.state) | |||
? { | |||
left: this.state.left + leftFix, | |||
@@ -346,7 +346,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> { | |||
return ( | |||
<div | |||
className={`${classNameSpace} ${placement}`} | |||
className={`${classNameSpace} ${currentPlacement}`} | |||
onMouseEnter={this.handleOverlayMouseEnter} | |||
onMouseLeave={this.handleOverlayMouseLeave} | |||
ref={this.tooltipNodeRef} | |||
@@ -356,7 +356,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> { | |||
className={`${classNameSpace}-arrow`} | |||
style={ | |||
isMeasured(this.state) | |||
? this.adjustArrowPosition(placement, { leftFix, topFix }) | |||
? this.adjustArrowPosition(currentPlacement, { leftFix, topFix }) | |||
: undefined | |||
} | |||
/> |
@@ -0,0 +1,81 @@ | |||
/* | |||
* 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} /> | |||
); | |||
} | |||
}); |
@@ -0,0 +1,50 @@ | |||
// 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} | |||
/> | |||
`; |
@@ -41,7 +41,7 @@ interface ButtonProps extends AllowedButtonAttributes { | |||
onClick?: () => void; | |||
preventDefault?: boolean; | |||
stopPropagation?: boolean; | |||
type?: 'button' | 'submit' | 'reset' | undefined; | |||
type?: 'button' | 'submit' | 'reset'; | |||
} | |||
export class Button extends React.PureComponent<ButtonProps> { |
@@ -1,43 +0,0 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -38,8 +38,8 @@ export default function SeverityIcon({ severity, ...iconProps }: Props) { | |||
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) { |
@@ -37,8 +37,8 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = { | |||
}; | |||
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) { |
@@ -33,8 +33,8 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = { | |||
}; | |||
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) { |
@@ -44,12 +44,15 @@ interface AlertVariantInformation { | |||
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}; | |||
`; |
@@ -20,12 +20,12 @@ | |||
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) { |
@@ -23,9 +23,6 @@ | |||
box-sizing: border-box; | |||
} | |||
.navbar { | |||
} | |||
.navbar-inner { | |||
position: fixed; | |||
left: 0; |
@@ -21,10 +21,9 @@ import * as classNames from 'classnames'; | |||
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) { |
@@ -17,18 +17,20 @@ | |||
* 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> | |||
); | |||
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; | |||
} |
@@ -49,7 +49,7 @@ export function removeNoFooterPageClass() { | |||
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); |