Last active
March 2, 2019 17:56
-
-
Save paularmstrong/368ba0d8440d405a508828b1656d5863 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2019 Paul Armstrong | |
*/ | |
import React from 'react'; | |
import { render } from 'react-testing-library'; | |
import Tooltip from '../Tooltip'; | |
import { Dimensions, MeasureOnSuccessCallback, View } from 'react-native'; | |
describe('Tooltip', () => { | |
let viewRef; | |
beforeEach(() => { | |
viewRef = React.createRef(); | |
render(<View ref={viewRef} />); | |
}); | |
test('renders a tooltip to a portal', () => { | |
const portal = document.createElement('div'); | |
portal.setAttribute('id', 'tooltipPortal'); | |
document.body.appendChild(portal); | |
jest.spyOn(View.prototype, 'measure').mockImplementation( | |
(fn: MeasureOnSuccessCallback): void => { | |
fn(0, 0, 45, 20, 0, 0); | |
} | |
); | |
jest.spyOn(viewRef.current, 'measureInWindow').mockImplementation( | |
(fn: (x: number, y: number, width: number, height: number) => void): void => { | |
fn(20, 100, 40, 30); | |
} | |
); | |
const { getByRole, queryAllByText } = render(<Tooltip relativeTo={viewRef} text="foobar" />, { | |
container: portal | |
}); | |
expect(getByRole('tooltip').style).toMatchObject({ | |
top: '136px', | |
left: '17.5px' | |
}); | |
expect(queryAllByText('foobar')).toHaveLength(1); | |
}); | |
test('renders directly without a portal available', () => { | |
jest.spyOn(View.prototype, 'measure').mockImplementation( | |
(fn: MeasureOnSuccessCallback): void => { | |
fn(0, 0, 45, 20, 0, 0); | |
} | |
); | |
jest.spyOn(viewRef.current, 'measureInWindow').mockImplementation( | |
(fn: (x: number, y: number, width: number, height: number) => void): void => { | |
fn(20, 100, 40, 30); | |
} | |
); | |
const { getByRole, queryAllByText } = render(<Tooltip relativeTo={viewRef} text="foobar" />); | |
expect(getByRole('tooltip').style).toMatchObject({ | |
top: '136px', | |
left: '17.5px' | |
}); | |
expect(queryAllByText('foobar')).toHaveLength(1); | |
}); | |
describe('window edge avoidance', () => { | |
test('avoids the left edge', () => { | |
jest.spyOn(Dimensions, 'get').mockReturnValue({ width: 400, height: 400, scale: 1, fontScale: 1 }); | |
jest.spyOn(View.prototype, 'measure').mockImplementation( | |
(fn: MeasureOnSuccessCallback): void => { | |
fn(0, 0, 45, 30, 0, 0); | |
} | |
); | |
jest.spyOn(viewRef.current, 'measureInWindow').mockImplementation( | |
(fn: (x: number, y: number, width: number, height: number) => void): void => { | |
fn(10, 200, 20, 10); | |
} | |
); | |
const { getByRole } = render(<Tooltip relativeTo={viewRef} text="foobar" />); | |
expect(getByRole('tooltip').style).toMatchObject({ | |
top: '190px', | |
left: '36px' | |
}); | |
}); | |
test('avoids the right edge', () => { | |
jest.spyOn(Dimensions, 'get').mockReturnValue({ width: 400, height: 400, scale: 1, fontScale: 1 }); | |
jest.spyOn(View.prototype, 'measure').mockImplementation( | |
(fn: MeasureOnSuccessCallback): void => { | |
fn(0, 0, 45, 30, 0, 0); | |
} | |
); | |
jest.spyOn(viewRef.current, 'measureInWindow').mockImplementation( | |
(fn: (x: number, y: number, width: number, height: number) => void): void => { | |
fn(380, 200, 50, 10); | |
} | |
); | |
const { getByRole } = render(<Tooltip relativeTo={viewRef} text="foobar" />); | |
expect(getByRole('tooltip').style).toMatchObject({ | |
top: '190px', | |
left: '329px' | |
}); | |
}); | |
test('avoids the bottom edge', () => { | |
jest.spyOn(Dimensions, 'get').mockReturnValue({ width: 400, height: 400, scale: 1, fontScale: 1 }); | |
jest.spyOn(View.prototype, 'measure').mockImplementation( | |
(fn: MeasureOnSuccessCallback): void => { | |
fn(0, 0, 45, 30, 0, 0); | |
} | |
); | |
jest.spyOn(viewRef.current, 'measureInWindow').mockImplementation( | |
(fn: (x: number, y: number, width: number, height: number) => void): void => { | |
fn(200, 380, 50, 10); | |
} | |
); | |
const { getByRole } = render(<Tooltip relativeTo={viewRef} text="foobar" />); | |
expect(getByRole('tooltip').style).toMatchObject({ | |
top: '344px', | |
left: '202.5px' | |
}); | |
}); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2019 Paul Armstrong | |
*/ | |
import * as Theme from '../theme'; | |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { Dimensions, StyleSheet, Text, View } from 'react-native'; | |
interface Props { | |
relativeTo: React.RefObject<View>; | |
text: string; | |
} | |
const tipSpace = 6; | |
const Tooltip = (props: Props): React.ReactElement => { | |
const { relativeTo, text } = props; | |
const [position, setPosition] = React.useState({ top: -999, left: 0 }); | |
const portalRoot = document.getElementById('tooltipPortal'); | |
const ref: React.RefObject<View> = React.createRef(); | |
React.useEffect(() => { | |
if (relativeTo.current) { | |
const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); | |
ref.current.measure( | |
(_x: number, _y: number, tipWidth: number, tipHeight: number): void => { | |
relativeTo.current.measureInWindow( | |
(x: number, y: number, width: number, height: number): void => { | |
let top = y + height + tipSpace; | |
let left = x + width / 2 - tipWidth / 2; | |
// too far right when underneath | |
if (left + tipWidth > windowWidth) { | |
left = x - tipWidth - tipSpace; | |
top = y + height / 2 - tipHeight / 2; | |
} | |
// too far left when underneath | |
else if (left < 0) { | |
left = x + width + tipSpace; | |
top = y + height / 2 - tipHeight / 2; | |
} | |
// too close to bottom | |
else if (top + tipHeight > windowHeight) { | |
top = y - tipHeight - tipSpace; | |
} | |
setPosition({ left, top }); | |
} | |
); | |
} | |
); | |
} | |
}); | |
const tooltip = ( | |
<View | |
// @ts-ignore | |
accessibilityRole="tooltip" | |
ref={ref} | |
style={[styles.root, { top: position.top, left: position.left }, position.top > 0 && styles.show]} | |
> | |
<Text style={styles.text}>{text}</Text> | |
</View> | |
); | |
return portalRoot ? ReactDOM.createPortal(tooltip, portalRoot) : tooltip; | |
}; | |
const styles = StyleSheet.create({ | |
root: { | |
// @ts-ignore | |
position: 'absolute', | |
backgroundColor: Theme.Color.Gray50, | |
borderRadius: Theme.BorderRadius.Normal, | |
paddingHorizontal: Theme.Spacing.Small, | |
paddingVertical: Theme.Spacing.Xsmall, | |
// @ts-ignore | |
transitionProperty: 'transform, opacity', | |
transitionDuration: '0.1s', | |
transitionTimingFunction: Theme.MotionTiming.Accelerate, | |
transform: [{ scale: 0.75 }], | |
opacity: 0 | |
}, | |
show: { | |
transform: [{ scale: 1 }], | |
opacity: 1 | |
}, | |
// @ts-ignore | |
text: { | |
color: Theme.TextColor.Gray50, | |
fontSize: Theme.FontSize.Xsmall, | |
textAlign: 'center' | |
} | |
}); | |
export default Tooltip; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment