Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Touch crosshair for line graphs #2524

Merged
merged 17 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ the various packages, please execute the following:
make init
```

> please note that it will take a while as this project uses a lot of dependencies…
> please note that it will take a while as this project uses a lot of dependencies…'

### Windows

If you want to build this project on Windows, it is recommended to use either WSL 2, or Git bash + `choco install make`.

## Development

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fmt-check: ##@0 global check if files were all formatted using prettier

test: ##@0 global run all checks/tests (packages, website)
@$(MAKE) fmt-check
@$(MAKE) lint
@$(MAKE) pkgs-lint
@$(MAKE) pkgs-test

vercel-build: ##@0 global Build the website and storybook to vercel
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"keywords": [],
"devDependencies": {
"@babel/core": "^7.21.5",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.5",
"@ekino/config": "^0.3.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.2",
Expand All @@ -27,6 +30,7 @@
"@types/lodash": "^4.14.170",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.6",
Expand All @@ -35,6 +39,7 @@
"babel-loader": "^8.2.3",
"chalk": "^5.2.0",
"chalk-template": "^1.0.0",
"cypress": "^12.11.0",
"enzyme": "^3.11.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
Expand All @@ -57,16 +62,14 @@
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-test-renderer": "^18.2.0",
"@types/react-test-renderer": "^18.0.0",
"resize-observer-polyfill": "^1.5.1",
"rollup": "^3.21.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-size": "^0.3.1",
"rollup-plugin-strip-banner": "^3.0.0",
"rollup-plugin-visualizer": "^5.5.2",
"serve": "^13.0.2",
"typescript": "^4.9.5",
"cypress": "^12.11.0"
"typescript": "^4.9.5"
},
"resolutions": {
"@types/react": "^18.2.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,10 @@ export function usePropertyAccessor<Datum, Value>(
accessor: PropertyAccessor<Datum, Value>
): (datum: Datum) => Value

export function getRelativeCursor(element: Element, event: React.MouseEvent): [number, number]
export function getRelativeCursor(
element: Element,
event: React.MouseEvent | React.TouchEvent
): [number, number]
export function isCursorInRect(
x: number,
y: number,
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/lib/interactivity/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export * from './detect'
* give us the scaling factor to calculate the proper mouse position.
*/
export const getRelativeCursor = (el, event) => {
const { clientX, clientY } = event
const { clientX, clientY } = 'touches' in event ? event.touches[0] : event

// Get the dimensions of the element, in case it has
// been scaled using a transform for example, we get
// the scaled dimensions, not the original ones.
Expand All @@ -36,8 +37,10 @@ export const getRelativeCursor = (el, event) => {
} else {
// Other elements.
originalBox = {
width: el.offsetWidth,
height: el.offsetHeight,
// These should be here, except when we are running in jsdom.
// https://github.com/jsdom/jsdom/issues/135
width: el.offsetWidth || 0,
height: el.offsetHeight || 0,
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/line/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface Point {
export type AccessorFunc = (datum: Point['data']) => string

export type PointMouseHandler = (point: Point, event: React.MouseEvent) => void
export type PointTouchHandler = (point: Point, event: React.TouchEvent) => void

export interface PointTooltipProps {
point: Point
Expand Down Expand Up @@ -185,6 +186,9 @@ export interface LineProps {
onMouseMove?: PointMouseHandler
onMouseLeave?: PointMouseHandler
onClick?: PointMouseHandler
onTouchStart?: PointTouchHandler
plouc marked this conversation as resolved.
Show resolved Hide resolved
onTouchMove?: PointTouchHandler
onTouchEnd?: PointTouchHandler

debugMesh?: boolean

Expand All @@ -197,6 +201,7 @@ export interface LineProps {

enableCrosshair?: boolean
crosshairType?: CrosshairType
enableTouchCrosshair?: boolean

legends?: LegendProps[]
}
Expand Down
11 changes: 11 additions & 0 deletions packages/line/src/Line.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const Line = props => {
onMouseMove,
onMouseLeave,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,

tooltip = PointTooltip,

Expand All @@ -110,6 +113,7 @@ const Line = props => {

enableCrosshair = true,
crosshairType = 'bottom-left',
enableTouchCrosshair = false,

role = 'img',
} = props
Expand Down Expand Up @@ -241,6 +245,9 @@ const Line = props => {
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
/>
)
}
Expand Down Expand Up @@ -303,7 +310,11 @@ const Line = props => {
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
tooltip={tooltip}
enableTouchCrosshair={enableTouchCrosshair}
debug={debugMesh}
/>
)
Expand Down
45 changes: 44 additions & 1 deletion packages/line/src/Mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ const Mesh = ({
onMouseMove,
onMouseLeave,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,
tooltip,
debug,
enableTouchCrosshair,
}) => {
const { showTooltipAt, hideTooltip } = useTooltip()

Expand All @@ -49,7 +53,7 @@ const Mesh = ({
setCurrent(point)
onMouseMove && onMouseMove(point, event)
},
[setCurrent, showTooltipAt, tooltip, onMouseMove]
[showTooltipAt, tooltip, margin.left, margin.top, setCurrent, onMouseMove]
)

const handleMouseLeave = useCallback(
Expand All @@ -68,6 +72,41 @@ const Mesh = ({
[onClick]
)

const handleTouchStart = useCallback(
plouc marked this conversation as resolved.
Show resolved Hide resolved
(point, event) => {
showTooltipAt(
createElement(tooltip, { point }),
[point.x + margin.left, point.y + margin.top],
'top'
)
setCurrent(point)
onTouchStart && onTouchStart(point, event)
},
[margin.left, margin.top, onTouchStart, setCurrent, showTooltipAt, tooltip]
)

const handleTouchMove = useCallback(
(point, event) => {
showTooltipAt(
createElement(tooltip, { point }),
[point.x + margin.left, point.y + margin.top],
'top'
)
setCurrent(point)
onTouchMove && onTouchMove(point, event)
},
[margin.left, margin.top, onTouchMove, setCurrent, showTooltipAt, tooltip]
)

const handleTouchEnd = useCallback(
(point, event) => {
hideTooltip()
setCurrent(null)
onTouchEnd && onTouchEnd(point, event)
},
[onTouchEnd, hideTooltip, setCurrent]
)

return (
<BaseMesh
nodes={points}
Expand All @@ -77,6 +116,10 @@ const Mesh = ({
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
enableTouchCrosshair={enableTouchCrosshair}
debug={debug}
/>
)
Expand Down
2 changes: 2 additions & 0 deletions packages/line/src/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const LinePropTypes = {
enablePointLabel: PropTypes.bool.isRequired,
role: PropTypes.string.isRequired,
useMesh: PropTypes.bool.isRequired,
enableTouchCrosshair: PropTypes.bool.isRequired,
...motionPropTypes,
...defsPropTypes,
}
Expand Down Expand Up @@ -202,6 +203,7 @@ export const LineDefaultProps = {
...commonDefaultProps,
enablePointLabel: false,
useMesh: false,
enableTouchCrosshair: false,
animate: true,
motionConfig: 'gentle',
defs: [],
Expand Down
63 changes: 61 additions & 2 deletions packages/line/tests/Line.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,20 @@ describe('mouse events on slices', () => {
it('should call onMouseEnter', () => {
const onMouseEnter = jest.fn()
const wrapper = mount(<Line {...baseProps} onMouseEnter={onMouseEnter} />)
wrapper.find(`[data-testid='slice-0']`).simulate('mouseenter')
wrapper.find(`[data-testid='slice-0']`).simulate('mouseenter', {
clientX: 100,
clientY: 100,
})
expect(onMouseEnter).toHaveBeenCalledTimes(1)
})

it('should call onMouseMove', () => {
const onMouseMove = jest.fn()
const wrapper = mount(<Line {...baseProps} onMouseMove={onMouseMove} />)
wrapper.find(`[data-testid='slice-0']`).simulate('mousemove')
wrapper.find(`[data-testid='slice-0']`).simulate('mousemove', {
clientX: 100,
clientY: 100,
})
expect(onMouseMove).toHaveBeenCalledTimes(1)
})

Expand All @@ -198,3 +204,56 @@ describe('mouse events on slices', () => {
expect(onClick).toHaveBeenCalledTimes(1)
})
})

describe('touch events with useMesh', () => {
const data = [
{
id: 'A',
data: [
{ x: 0, y: 3 },
{ x: 1, y: 7 },
{ x: 2, y: 11 },
{ x: 3, y: 9 },
{ x: 4, y: 8 },
],
},
]
const baseProps = {
width: 500,
height: 300,
data: data,
animate: false,
useMesh: true,
enableTouchCrosshair: true,
}

it('should call onTouchStart', () => {
const onTouchStart = jest.fn()
const wrapper = mount(<Line {...baseProps} onTouchStart={onTouchStart} />)
wrapper.find(`[data-testid='mesh-interceptor']`).simulate('touchstart', {
touches: [{ clientX: 50, clientY: 50 }],
})
expect(onTouchStart).toHaveBeenCalledTimes(1)
})

it('should call onTouchMove', () => {
const onTouchMove = jest.fn()
const wrapper = mount(<Line {...baseProps} onTouchMove={onTouchMove} />)
wrapper.find(`[data-testid='mesh-interceptor']`).simulate('touchmove', {
touches: [{ clientX: 50, clientY: 50 }],
})
expect(onTouchMove).toHaveBeenCalledTimes(1)
})

it('should call onTouchEnd', () => {
const onTouchEnd = jest.fn()
const wrapper = mount(<Line {...baseProps} onTouchEnd={onTouchEnd} />)
wrapper
.find(`[data-testid='mesh-interceptor']`)
.simulate('touchstart', {
touches: [{ clientX: 50, clientY: 50 }],
})
.simulate('touchend')
expect(onTouchEnd).toHaveBeenCalledTimes(1)
})
})
6 changes: 5 additions & 1 deletion packages/tooltip/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export interface TooltipActionsContextData {
position: [number, number],
anchor?: TooltipAnchor
) => void
showTooltipFromEvent: (content: JSX.Element, event: MouseEvent, anchor?: TooltipAnchor) => void
showTooltipFromEvent: (
plouc marked this conversation as resolved.
Show resolved Hide resolved
content: JSX.Element,
event: MouseEvent | TouchEvent,
anchor?: TooltipAnchor
) => void
hideTooltip: () => void
}

Expand Down
7 changes: 4 additions & 3 deletions packages/tooltip/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const useTooltipHandlers = (container: MutableRefObject<HTMLDivElement>)
)

const showTooltipFromEvent: TooltipActionsContextData['showTooltipFromEvent'] = useCallback(
(content: JSX.Element, event: MouseEvent, anchor: TooltipAnchor = 'top') => {
(content: JSX.Element, event: MouseEvent | TouchEvent, anchor: TooltipAnchor = 'top') => {
const bounds = container.current.getBoundingClientRect()
const offsetWidth = container.current.offsetWidth
// In a normal situation mouse enter / mouse leave events
Expand All @@ -35,8 +35,9 @@ export const useTooltipHandlers = (container: MutableRefObject<HTMLDivElement>)
// width give us the scaling factor to calculate
// ok mouse position
const scaling = offsetWidth === bounds.width ? 1 : offsetWidth / bounds.width
const x = (event.clientX - bounds.left) * scaling
const y = (event.clientY - bounds.top) * scaling
const { clientX, clientY } = 'touches' in event ? event.touches[0] : event
const x = (clientX - bounds.left) * scaling
const y = (clientY - bounds.top) * scaling

if (anchor === 'left' || anchor === 'right') {
if (x < bounds.width / 2) anchor = 'right'
Expand Down
Loading
Loading