Skip to content

Commit a06fbed

Browse files
committed
feat(TouchSupportSensor): Add the TouchSupportSensor HOC and func
1 parent 7e05ccb commit a06fbed

File tree

9 files changed

+249
-4
lines changed

9 files changed

+249
-4
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const MyComponent = mock();
6969
- [`<ScrollSensor>`](./docs/en/ScrollSensor.md)
7070
- [`<SizeSensor>`](./docs/en/SizeSensor.md), [`withSize()`](./docs/en/SizeSensor.md#withsize-hoc), and [`@withSize`](./docs/en/SizeSensor.md#withsize-decorator) &mdash; [**example**](https://codesandbox.io/s/0y2qjm210p)
7171
- [`<WidthSensor>`](./docs/en/WidthSensor.md), [`withWidth()`](./docs/en/WidthSensor.md#withwidth-hoc-and-withwidth-decorator), and [`@withWidth`](./docs/en/WidthSensor.md#withwidth-hoc-and-withwidth-decorator)
72+
- [`<TouchSupportSensor>`](./docs/en/TouchSupportSensor.md)
7273
- [`<ViewportSensor>`](./docs/en/ViewportSensor.md), [`withViewport()`](./docs/en/ViewportSensor.md#withviewport-hoc), and [`@withViewport`](./docs/en/ViewportSensor.md#withviewport-decorator)
7374
- [`<ViewportScrollSensor>`](./docs/en/ViewportSensor.md#viewportscrollsensor) and [`<ViewportObserverSensor>`](./docs/en/ViewportSensor.md#viewportobserversensor)
7475
- [`<WindowScrollSensor>`](./docs/en/WindowScrollSensor.md), [`withWindowScroll()`](./docs/en/WindowScrollSensor.md#withwindowscroll-hoc), and [`@withWindowScroll`](./docs/en/WindowScrollSensor.md#withwindowscroll-decorator)
@@ -114,6 +115,7 @@ const MyComponent = mock();
114115
- [`<Resolve>`](./docs/en/Resolve.md)
115116
- [`<Sms>`](./docs/en/Sms.md), [`<Mailto>`](./docs/en/Mailto.md)
116117
- [`getDisplayName()`](./docs/en/getDisplayName.md)
118+
- [`touchSupported()`](./docs/en/TouchSupportSensor.md)
117119

118120

119121
## License

docs/en/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- [`<ScrollSensor>`](./ScrollSensor.md)
3636
- [`<SizeSensor>`](./SizeSensor.md), [`withSize()`](./SizeSensor.md#withsize-hoc), and [`@withSize`](./SizeSensor.md#withsize-decorator)
3737
- [`<WidthSensor>`](./WidthSensor.md), [`withWidth()`](./WidthSensor.md#withwidth-hoc-and-withwidth-decorator), and [`@withWidth`](./WidthSensor.md#withwidth-hoc-and-withwidth-decorator)
38+
- [`<TouchSupportSensor>`](./TouchSupportSensor.md)
3839
- [`<ViewportSensor>`](./ViewportSensor.md), [`withViewport()`](./ViewportSensor.md#withviewport-hoc), and [`@withViewport`](./ViewportSensor.md#withviewport-decorator)
3940
- [`<ViewportScrollSensor>`](./ViewportSensor.md#viewportscrollsensor) and [`<ViewportObserverSensor>`](./ViewportSensor.md#viewportobserversensor)
4041
- [`<WindowScrollSensor>`](./WindowScrollSensor.md), [`withWindowScroll()`](./WindowScrollSensor.md#withwindowscroll-hoc), and [`@withWindowScroll`](./WindowScrollSensor.md#withwindowscroll-decorator)
@@ -81,3 +82,4 @@
8182
- [`<Resolve>`](./Resolve.md)
8283
- [`<Sms>`](./Sms.md), [`<Mailto>`](./Mailto.md), and `<Tel>`
8384
- [`getDisplayName()`](./getDisplayName.md)
85+
- [`touchSupported()`](./TouchSupportSensor.md)

docs/en/TouchSupportSensor.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# `<TouchSupportSensor>`
2+
3+
[![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface)
4+
5+
Render prop that detects if touch interactions are available or not.
6+
It's important to remember that touch detection can be [fallible](http://www.stucox.com/blog/you-cant-detect-a-touchscreen/).
7+
8+
## Example
9+
10+
Use it as FaCC, attach to root element
11+
12+
```jsx
13+
import {TouchSupportSensor} from 'libreact/lib/TouchSupportSensor';
14+
15+
<TouchSupportSensor>{({touchSupported}) =>
16+
<div>{touchSupported ? 'touch interactions available' : 'touch interactions not available'}</div>
17+
}</TouchSupportSensor>
18+
```
19+
20+
Use it as a plain function
21+
22+
```js
23+
import { touchSupported } from 'libreact/lib/TouchSupportSensor';
24+
25+
console.log(touchSupported())
26+
```
27+
28+
29+
## Props
30+
31+
Prop signature
32+
33+
```ts
34+
interface ITouchSupportSensorProps {
35+
onlyMouse?: boolean;
36+
onlyTouch?: boolean;
37+
}
38+
```
39+
40+
, where
41+
42+
- `onlyMouse` - optional, boolean, will only render children if touch support is not detected.
43+
- `onlyTouch` - optional, boolean, will only render children if touch support is detected.

src/MediaSensor/__story__/story.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {storiesOf} from '@storybook/react';
33
import {MediaSensor, withMedia} from '..';
44
import ShowDocs from '../../ShowDocs'
55

6-
const IsBig = ({isBig}) => h('div', null, `WIDTH IS GREATED THAN 480PX: ${isBig}`);
6+
const IsBig = ({isBig}) => h('div', null, `WIDTH IS GREATER THAN 480PX: ${isBig}`);
77

88
const IsBigWithMedia = withMedia(IsBig, 'isBig', {
99
query: '(min-width: 480px)'
@@ -14,7 +14,7 @@ const IsBigWithMedia = withMedia(IsBig, 'isBig', {
1414
})
1515
class IsBigClass extends Component<any, any> {
1616
render () {
17-
return h('div', null, `WIDTH IS GREATED THAN 480PX: ${this.props.isBig.matches}`);
17+
return h('div', null, `WIDTH IS GREATER THAN 480PX: ${this.props.isBig.matches}`);
1818
}
1919
}
2020

@@ -29,7 +29,7 @@ storiesOf('Sensors/MediaSensor', module)
2929
border: '1px solid red'
3030
}
3131
},
32-
`WIDTH IS GREATED THAN 480PX: ${matches}`
32+
`WIDTH IS GREATER THAN 480PX: ${matches}`
3333
)
3434
)
3535
)
@@ -42,7 +42,7 @@ storiesOf('Sensors/MediaSensor', module)
4242
border: '1px solid red'
4343
}
4444
},
45-
`WIDTH IS GREATED THAN 480PX: ${matches}`
45+
`WIDTH IS GREATER THAN 480PX: ${matches}`
4646
)
4747
})
4848
)
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {createElement as h} from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {TouchSupportSensor} from '..';
4+
import ShowDocs from '../../ShowDocs'
5+
6+
storiesOf('Sensors/TouchSupportSensor', module)
7+
.add('Documentation', () => h(ShowDocs, {md: require('../../../docs/en/TouchSupportSensor.md')}))
8+
.add('Generic', () =>
9+
<TouchSupportSensor>{({touchSupported}) =>
10+
<div style={{
11+
border: '1px solid tomato',
12+
padding: 30
13+
}}>
14+
{touchSupported ? 'TOUCH SUPPORTED' : '...'}
15+
</div>
16+
}</TouchSupportSensor>
17+
)
18+
.add('Only render on touch devices', () =>
19+
<TouchSupportSensor onlyTouch>{({touchSupported}) =>
20+
<div style={{
21+
border: '1px solid tomato',
22+
padding: 30
23+
}}>
24+
{touchSupported ? 'TOUCH SUPPORTED' : '...'}
25+
<div style={{
26+
background: 'tomato',
27+
color: 'white',
28+
padding: 30
29+
}}>
30+
You won't see this on your desktop computer!
31+
</div>
32+
</div>
33+
}</TouchSupportSensor>
34+
)
35+
.add('Only render on non-touch devices', () =>
36+
<TouchSupportSensor onlyMouse>{({touchSupported}) =>
37+
<div style={{
38+
border: '1px solid tomato',
39+
padding: 30
40+
}}>
41+
{touchSupported ? '...' : 'TOUCH NOT SUPPORTED'}
42+
<div style={{
43+
background: 'tomato',
44+
color: 'white',
45+
padding: 30
46+
}}>
47+
You won't see this on your touch screen devices!
48+
</div>
49+
</div>
50+
}</TouchSupportSensor>
51+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<TouchSupportSensor> returns expected default state 1`] = `
4+
Object {
5+
"touchSupported": false,
6+
}
7+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { mount } from "enzyme";
2+
import { h } from "../../util";
3+
import { TouchSupportSensor } from "..";
4+
5+
declare const jsdom;
6+
7+
describe.only("<TouchSupportSensor>", () => {
8+
beforeEach(() => {
9+
jsdom.reconfigure({
10+
windowTop: window,
11+
url: "https://example.com/"
12+
});
13+
});
14+
15+
it("is a component", () => {
16+
expect(TouchSupportSensor).toBeInstanceOf(Function);
17+
});
18+
19+
it("renders without crashing", () => {
20+
mount(<TouchSupportSensor>{() => null}</TouchSupportSensor>);
21+
});
22+
23+
it("returns expected default state", () => {
24+
mount(
25+
<TouchSupportSensor>
26+
{touchSupport => {
27+
expect(touchSupport).toMatchSnapshot();
28+
29+
return null;
30+
}}
31+
</TouchSupportSensor>
32+
);
33+
});
34+
35+
it("renders nothing when onlyTouch is true and the device doesn't support touch", () => {
36+
const component = mount(<TouchSupportSensor onlyTouch>(location) => <div /></TouchSupportSensor>)
37+
38+
expect(component.find('div')).toHaveLength(0)
39+
})
40+
41+
it("renders nothing when onlyMouse is true and the device does support touch", () => {
42+
window.ontouchstart = () => null
43+
44+
const component = mount(<TouchSupportSensor onlyMouse>(location) => <div /></TouchSupportSensor>)
45+
46+
expect(component.find('div')).toHaveLength(0)
47+
48+
delete window.ontouchstart
49+
})
50+
51+
it("renders children when onlyTouch is true and the device does support touch", () => {
52+
window.ontouchstart = () => null
53+
54+
const component = mount(<TouchSupportSensor onlyTouch>(location) => <div /></TouchSupportSensor>)
55+
56+
expect(component.find('div')).toHaveLength(1)
57+
58+
delete window.ontouchstart
59+
})
60+
61+
it("renders children when onlyMouse is true and the device doesn't support touch", () => {
62+
const component = mount(<TouchSupportSensor onlyMouse>(location) => <div /></TouchSupportSensor>)
63+
64+
expect(component.find('div')).toHaveLength(1)
65+
})
66+
});

src/TouchSupportSensor/index.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as React from "react";
2+
import renderProp from "../util/renderProp";
3+
import { IUniversalInterfaceProps } from "../typing";
4+
5+
export interface DocumentTouchConstructor extends Document {
6+
[key: string]: any;
7+
new (): DocumentTouchConstructor;
8+
readonly prototype: Document;
9+
}
10+
11+
export interface TouchWindow extends Window {
12+
DocumentTouch?: DocumentTouchConstructor;
13+
}
14+
15+
let DocumentTouch: DocumentTouchConstructor;
16+
17+
export interface ITouchSupportSensorState {
18+
touchSupported: boolean;
19+
}
20+
21+
export interface ITouchSupportSensorProps
22+
extends IUniversalInterfaceProps<ITouchSupportSensorState> {
23+
onlyTouch?: boolean;
24+
onlyMouse?: boolean;
25+
}
26+
27+
export const touchSupported = () => {
28+
const prefixes = " -webkit- -moz- -o- -ms- ".split(" ");
29+
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
30+
// https://git.io/vznFH
31+
const query = ["(", prefixes.join("touch-enabled),("), "heartz", ")"].join(
32+
""
33+
);
34+
35+
if (
36+
"ontouchstart" in window ||
37+
((window as TouchWindow).DocumentTouch && document instanceof DocumentTouch)
38+
) {
39+
return true;
40+
}
41+
42+
// TypeScript seems to think "ontouchstart" will always be on window, causing incorrect type narrowing
43+
return (window as Window).matchMedia(query).matches;
44+
};
45+
46+
export class TouchSupportSensor extends React.Component<
47+
ITouchSupportSensorProps,
48+
ITouchSupportSensorState
49+
> {
50+
constructor(props, context) {
51+
super(props, context);
52+
53+
if (props.onlyMouse && props.onlyTouch) {
54+
console.warn(
55+
"You're using both `onlyMouse` and `onlyTouch` on the TouchSupportSensor component. This is unsupported and may lead to unexpected results."
56+
);
57+
}
58+
59+
this.state = { touchSupported: touchSupported() };
60+
}
61+
62+
render() {
63+
if (this.props.onlyMouse && this.state.touchSupported) {
64+
return null;
65+
}
66+
67+
if (this.props.onlyTouch && !this.state.touchSupported) {
68+
return null;
69+
}
70+
71+
return renderProp(this.props, this.state);
72+
}
73+
}

src/__tests__/setup.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ configure({
77

88
if (typeof window === 'object') {
99
global.requestAnimationFrame = window.requestAnimationFrame = (callback) => setTimeout(callback, 17);
10+
global.matchMedia = window.matchMedia = (() => { return { matches: false, addListener: () => {}, removeListener: () => {}, }; });
1011
}

0 commit comments

Comments
 (0)