cozy-ui 83.2.0 → 83.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/react/Fab/ExtendableFab.jsx +30 -0
- package/react/Fab/Readme.md +38 -0
- package/react/Fab/index.js +1 -0
- package/react/helpers/ref.js +11 -0
- package/react/helpers/ref.spec.js +28 -0
- package/react/hooks/useEventListener.js +5 -3
- package/react/hooks/useEventListener.spec.js +26 -0
- package/react/hooks/useScroll.jsx +44 -0
- package/react/hooks/useScroll.md +34 -0
- package/react/hooks/useScroll.spec.jsx +56 -0
- package/react/index.js +1 -1
- package/transpiled/react/Fab/ExtendableFab.js +32 -0
- package/transpiled/react/Fab/index.js +2 -1
- package/transpiled/react/helpers/ref.js +6 -0
- package/transpiled/react/hooks/useEventListener.js +6 -3
- package/transpiled/react/hooks/useScroll.js +49 -0
- package/transpiled/react/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# [83.3.0](https://github.com/cozy/cozy-ui/compare/v83.2.0...v83.3.0) (2023-04-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Add event listener on ref ([5fcc981](https://github.com/cozy/cozy-ui/commit/5fcc981))
|
|
7
|
+
* Add ExtendableFab ([511b5fc](https://github.com/cozy/cozy-ui/commit/511b5fc))
|
|
8
|
+
* Add useScroll hook to get scroll position from an element ([fdf2a74](https://github.com/cozy/cozy-ui/commit/fdf2a74))
|
|
9
|
+
|
|
1
10
|
# [83.2.0](https://github.com/cozy/cozy-ui/compare/v83.1.1...v83.2.0) (2023-04-21)
|
|
2
11
|
|
|
3
12
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Icon from '../Icon'
|
|
4
|
+
import useScroll from '../hooks/useScroll'
|
|
5
|
+
import Fab from '.'
|
|
6
|
+
|
|
7
|
+
const ExtendableFab = ({
|
|
8
|
+
label,
|
|
9
|
+
icon,
|
|
10
|
+
follow,
|
|
11
|
+
topLimit = 50,
|
|
12
|
+
scrollOptions,
|
|
13
|
+
...rest
|
|
14
|
+
}) => {
|
|
15
|
+
const { scrollTop } = useScroll(follow, scrollOptions)
|
|
16
|
+
const isBelowTopLimit = scrollTop < topLimit
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Fab
|
|
20
|
+
aria-label={label}
|
|
21
|
+
variant={isBelowTopLimit ? 'extended' : 'circular'}
|
|
22
|
+
{...rest}
|
|
23
|
+
>
|
|
24
|
+
<Icon icon={icon} {...(isBelowTopLimit && { className: 'u-mr-half' })} />
|
|
25
|
+
{isBelowTopLimit && label}
|
|
26
|
+
</Fab>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default ExtendableFab
|
package/react/Fab/Readme.md
CHANGED
|
@@ -77,3 +77,41 @@ const props = [{ color: 'primary' }, { color: 'secondary' }, { color: 'inherit',
|
|
|
77
77
|
)}
|
|
78
78
|
</Grid>
|
|
79
79
|
```
|
|
80
|
+
|
|
81
|
+
### ExtendableFab
|
|
82
|
+
|
|
83
|
+
To increase discoverability, the FAB can be extended at first and then changed to standard when scrolling. The ExtendableFab will only revert to extended when the user has returned on to the top of the page.
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
import { ExtendableFab } from 'cozy-ui/transpiled/react/Fab'
|
|
87
|
+
import PlusIcon from 'cozy-ui/transpiled/react/Icons/Plus'
|
|
88
|
+
import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
89
|
+
import {useRef} from 'react'
|
|
90
|
+
|
|
91
|
+
const Demo = ({ onClick, className }) => {
|
|
92
|
+
const box = useRef(null)
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="u-h-4 u-ov-scroll" style={{border: '2px dotted red'}} ref={box}>
|
|
96
|
+
<ExtendableFab
|
|
97
|
+
label="Add"
|
|
98
|
+
follow={box}
|
|
99
|
+
color="primary"
|
|
100
|
+
className="u-mb-1"
|
|
101
|
+
icon={PlusIcon}
|
|
102
|
+
style={{position: 'sticky', top: 16, left: 16}}
|
|
103
|
+
/>
|
|
104
|
+
<div className="u-p-1">Scroll Horizontally</div>
|
|
105
|
+
<div className="u-h-8"></div>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
<DemoProvider>
|
|
111
|
+
<Demo />
|
|
112
|
+
</DemoProvider>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note:**
|
|
116
|
+
|
|
117
|
+
The element to follow for scrolling changes according to the screen size in general in Cozy applications. On mobile, the window should be targeted otherwhise it depends on the dom element that has a scroll overflow.
|
package/react/Fab/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isRef, unRef } from './ref'
|
|
2
|
+
|
|
3
|
+
describe('isRef', () => {
|
|
4
|
+
it('should return false when element is null', () => {
|
|
5
|
+
expect(isRef(null)).toBe(false)
|
|
6
|
+
})
|
|
7
|
+
it('should return false when element is an object without current', () => {
|
|
8
|
+
expect(isRef({ something: true })).toBe(false)
|
|
9
|
+
})
|
|
10
|
+
it('should return true when element is an object with current', () => {
|
|
11
|
+
expect(isRef({ current: 'test' })).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('unRef', () => {
|
|
16
|
+
it('should return the current property when it is a ref', () => {
|
|
17
|
+
const ref = { current: 'test' }
|
|
18
|
+
expect(unRef(ref)).toBe('test')
|
|
19
|
+
})
|
|
20
|
+
it('should return the element when it is only a string', () => {
|
|
21
|
+
const string = 'test'
|
|
22
|
+
expect(unRef(string)).toBe(string)
|
|
23
|
+
})
|
|
24
|
+
it('should return the element when it is only a object', () => {
|
|
25
|
+
const obj = { something: true }
|
|
26
|
+
expect(unRef(obj)).toBe(obj)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
|
+
import { unRef } from '../helpers/ref'
|
|
2
3
|
|
|
3
4
|
const useEventListener = (element, event, cb) => {
|
|
4
5
|
useEffect(() => {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const currentElement = unRef(element)
|
|
7
|
+
if (currentElement && event && cb) {
|
|
8
|
+
currentElement.addEventListener(event, cb)
|
|
7
9
|
|
|
8
10
|
return () => {
|
|
9
|
-
|
|
11
|
+
currentElement.removeEventListener(event, cb)
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
}, [event, cb, element])
|
|
@@ -22,6 +22,23 @@ describe('useEventListener', () => {
|
|
|
22
22
|
expect(cb).toHaveBeenCalledTimes(1)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
it('should subscribe to the given event on the given ref', async () => {
|
|
26
|
+
const cb = jest.fn()
|
|
27
|
+
const ref = {
|
|
28
|
+
current: document
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { unmount } = renderHook(() => useEventListener(ref, 'click', cb))
|
|
32
|
+
|
|
33
|
+
triggerEvent(document, 'click')
|
|
34
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
35
|
+
|
|
36
|
+
unmount()
|
|
37
|
+
|
|
38
|
+
triggerEvent(document, 'click')
|
|
39
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
40
|
+
})
|
|
41
|
+
|
|
25
42
|
it('should not subscribe for an undefined element', async () => {
|
|
26
43
|
const cb = jest.fn()
|
|
27
44
|
const { result } = renderHook(() =>
|
|
@@ -30,6 +47,15 @@ describe('useEventListener', () => {
|
|
|
30
47
|
expect(result.error).not.toBeDefined()
|
|
31
48
|
})
|
|
32
49
|
|
|
50
|
+
it('should not subscribe for an undefined ref element', async () => {
|
|
51
|
+
const cb = jest.fn()
|
|
52
|
+
const ref = {
|
|
53
|
+
current: undefined
|
|
54
|
+
}
|
|
55
|
+
const { result } = renderHook(() => useEventListener(ref, 'click', cb))
|
|
56
|
+
expect(result.error).not.toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
33
59
|
it('should not subscribe for an undefined event', async () => {
|
|
34
60
|
const cb = jest.fn()
|
|
35
61
|
const { result } = renderHook(() => useEventListener(window, undefined, cb))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import debounce from 'lodash/debounce'
|
|
3
|
+
|
|
4
|
+
import useEventListener from './useEventListener'
|
|
5
|
+
import { unRef } from '../helpers/ref'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get scrollTop & scrollLeft from scroll event
|
|
9
|
+
* @param {HTMLElement|React.RefObject} element - Element or Ref to listen scroll event
|
|
10
|
+
* @param {Object} options
|
|
11
|
+
* @param {number} options.delay - Delay in ms before calling the callback
|
|
12
|
+
* @returns {{ scrollTop: number, scrollLeft: number}} - Scroll state
|
|
13
|
+
*/
|
|
14
|
+
const useScroll = (element, { delay = 250 } = {}) => {
|
|
15
|
+
const [scroll, setScroll] = useState({
|
|
16
|
+
scrollTop: 0,
|
|
17
|
+
scrollLeft: 0
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const handleScroll = debounce(() => {
|
|
21
|
+
const mainElement = unRef(element)
|
|
22
|
+
|
|
23
|
+
if (mainElement === window) {
|
|
24
|
+
setScroll({
|
|
25
|
+
scrollTop: window.scrollY,
|
|
26
|
+
scrollLeft: window.scrollX
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
setScroll({
|
|
30
|
+
scrollTop: mainElement.scrollTop,
|
|
31
|
+
scrollLeft: mainElement.scrollLeft
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}, delay)
|
|
35
|
+
|
|
36
|
+
// For Desktop
|
|
37
|
+
useEventListener(element, 'scroll', handleScroll)
|
|
38
|
+
// For Mobile
|
|
39
|
+
useEventListener(element, 'touchmove', handleScroll)
|
|
40
|
+
|
|
41
|
+
return scroll
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default useScroll
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
React hook that get scrollTop and direction properties when the scroll position of a react ref or a dom node changes.
|
|
2
|
+
|
|
3
|
+
**A focus on performance**
|
|
4
|
+
|
|
5
|
+
Scroll events can be issued at a high frequency. To avoid massive re-rendering, the callback is only fire every 250ms by default. You can reduce this delay using the options, as in the example below.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import useScroll from 'cozy-ui/transpiled/react/hooks/useScroll'
|
|
9
|
+
import Fab from 'cozy-ui/transpiled/react/Fab'
|
|
10
|
+
import Icon from 'cozy-ui/transpiled/react/Icon'
|
|
11
|
+
import PlusIcon from 'cozy-ui/transpiled/react/Icons/Plus'
|
|
12
|
+
import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
13
|
+
import {useRef, useEffect} from 'react'
|
|
14
|
+
|
|
15
|
+
const Demo = ({ onClick, className }) => {
|
|
16
|
+
const box = useRef(null)
|
|
17
|
+
const { scrollTop, scrollLeft } = useScroll(box, { delay: 50 })
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="u-ta-center">
|
|
21
|
+
<div className="u-pb-1">Vertically scrolled of {scrollTop}px</div>
|
|
22
|
+
<div className="u-pb-1">Horizontally scrolled of {scrollLeft}px</div>
|
|
23
|
+
<div className="u-h-4 u-ov-scroll" style={{ border: '2px dotted red'}} ref={box}>
|
|
24
|
+
<div className="u-p-1"> Scroll Vertically and Horizontally</div>
|
|
25
|
+
<div className="u-h-8" style={{ width: '150vw'}}></div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
<DemoProvider>
|
|
32
|
+
<Demo />
|
|
33
|
+
</DemoProvider>
|
|
34
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks'
|
|
2
|
+
import useScroll from './useScroll'
|
|
3
|
+
|
|
4
|
+
jest.mock('lodash/debounce', () => fn => fn)
|
|
5
|
+
|
|
6
|
+
describe('useScroll', () => {
|
|
7
|
+
describe('horizontal scrolling', () => {
|
|
8
|
+
it('should detect from an element', () => {
|
|
9
|
+
const mockElement = document.createElement('div')
|
|
10
|
+
|
|
11
|
+
const { result } = renderHook(() => useScroll(mockElement))
|
|
12
|
+
|
|
13
|
+
act(() => {
|
|
14
|
+
mockElement.scrollTop = 50
|
|
15
|
+
mockElement.dispatchEvent(new CustomEvent('scroll'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(result.current.scrollTop).toBe(50)
|
|
19
|
+
})
|
|
20
|
+
it('should detect from a window', () => {
|
|
21
|
+
const { result } = renderHook(() => useScroll(window))
|
|
22
|
+
|
|
23
|
+
act(() => {
|
|
24
|
+
window.scrollY = 50
|
|
25
|
+
window.dispatchEvent(new CustomEvent('scroll'))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(result.current.scrollTop).toBe(50)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('vertical scrolling', () => {
|
|
33
|
+
it('should detect from an element', () => {
|
|
34
|
+
const mockElement = document.createElement('div')
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() => useScroll(mockElement))
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
mockElement.scrollLeft = 50
|
|
40
|
+
mockElement.dispatchEvent(new CustomEvent('scroll'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(result.current.scrollLeft).toBe(50)
|
|
44
|
+
})
|
|
45
|
+
it('should detect from a window', () => {
|
|
46
|
+
const { result } = renderHook(() => useScroll(window))
|
|
47
|
+
|
|
48
|
+
act(() => {
|
|
49
|
+
window.scrollX = 50
|
|
50
|
+
window.dispatchEvent(new CustomEvent('scroll'))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(result.current.scrollLeft).toBe(50)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
package/react/index.js
CHANGED
|
@@ -97,7 +97,7 @@ export { default as UploadQueue } from './UploadQueue'
|
|
|
97
97
|
export { default as CozyTheme } from './CozyTheme'
|
|
98
98
|
export { default as Paper } from './Paper'
|
|
99
99
|
export { default as ProgressionBanner } from './ProgressionBanner'
|
|
100
|
-
export { default as Fab } from './Fab'
|
|
100
|
+
export { default as Fab, ExtendableFab } from './Fab'
|
|
101
101
|
export { default as SquareAppIcon } from './SquareAppIcon'
|
|
102
102
|
export { default as FileImageLoader } from './FileImageLoader'
|
|
103
103
|
export { default as Radios } from './Radios'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
2
|
+
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
3
|
+
var _excluded = ["label", "icon", "follow", "topLimit", "scrollOptions"];
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import Icon from "cozy-ui/transpiled/react/Icon";
|
|
6
|
+
import useScroll from "cozy-ui/transpiled/react/hooks/useScroll";
|
|
7
|
+
import Fab from "cozy-ui/transpiled/react/Fab";
|
|
8
|
+
|
|
9
|
+
var ExtendableFab = function ExtendableFab(_ref) {
|
|
10
|
+
var label = _ref.label,
|
|
11
|
+
icon = _ref.icon,
|
|
12
|
+
follow = _ref.follow,
|
|
13
|
+
_ref$topLimit = _ref.topLimit,
|
|
14
|
+
topLimit = _ref$topLimit === void 0 ? 50 : _ref$topLimit,
|
|
15
|
+
scrollOptions = _ref.scrollOptions,
|
|
16
|
+
rest = _objectWithoutProperties(_ref, _excluded);
|
|
17
|
+
|
|
18
|
+
var _useScroll = useScroll(follow, scrollOptions),
|
|
19
|
+
scrollTop = _useScroll.scrollTop;
|
|
20
|
+
|
|
21
|
+
var isBelowTopLimit = scrollTop < topLimit;
|
|
22
|
+
return /*#__PURE__*/React.createElement(Fab, _extends({
|
|
23
|
+
"aria-label": label,
|
|
24
|
+
variant: isBelowTopLimit ? 'extended' : 'circular'
|
|
25
|
+
}, rest), /*#__PURE__*/React.createElement(Icon, _extends({
|
|
26
|
+
icon: icon
|
|
27
|
+
}, isBelowTopLimit && {
|
|
28
|
+
className: 'u-mr-half'
|
|
29
|
+
})), isBelowTopLimit && label);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default ExtendableFab;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { unRef } from "cozy-ui/transpiled/react/helpers/ref";
|
|
2
3
|
|
|
3
4
|
var useEventListener = function useEventListener(element, event, cb) {
|
|
4
5
|
useEffect(function () {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
var currentElement = unRef(element);
|
|
7
|
+
|
|
8
|
+
if (currentElement && event && cb) {
|
|
9
|
+
currentElement.addEventListener(event, cb);
|
|
7
10
|
return function () {
|
|
8
|
-
|
|
11
|
+
currentElement.removeEventListener(event, cb);
|
|
9
12
|
};
|
|
10
13
|
}
|
|
11
14
|
}, [event, cb, element]);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import debounce from 'lodash/debounce';
|
|
4
|
+
import useEventListener from "cozy-ui/transpiled/react/hooks/useEventListener";
|
|
5
|
+
import { unRef } from "cozy-ui/transpiled/react/helpers/ref";
|
|
6
|
+
/**
|
|
7
|
+
* Get scrollTop & scrollLeft from scroll event
|
|
8
|
+
* @param {HTMLElement|React.RefObject} element - Element or Ref to listen scroll event
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {number} options.delay - Delay in ms before calling the callback
|
|
11
|
+
* @returns {{ scrollTop: number, scrollLeft: number}} - Scroll state
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var useScroll = function useScroll(element) {
|
|
15
|
+
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
|
|
16
|
+
_ref$delay = _ref.delay,
|
|
17
|
+
delay = _ref$delay === void 0 ? 250 : _ref$delay;
|
|
18
|
+
|
|
19
|
+
var _useState = useState({
|
|
20
|
+
scrollTop: 0,
|
|
21
|
+
scrollLeft: 0
|
|
22
|
+
}),
|
|
23
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
24
|
+
scroll = _useState2[0],
|
|
25
|
+
setScroll = _useState2[1];
|
|
26
|
+
|
|
27
|
+
var handleScroll = debounce(function () {
|
|
28
|
+
var mainElement = unRef(element);
|
|
29
|
+
|
|
30
|
+
if (mainElement === window) {
|
|
31
|
+
setScroll({
|
|
32
|
+
scrollTop: window.scrollY,
|
|
33
|
+
scrollLeft: window.scrollX
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
setScroll({
|
|
37
|
+
scrollTop: mainElement.scrollTop,
|
|
38
|
+
scrollLeft: mainElement.scrollLeft
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, delay); // For Desktop
|
|
42
|
+
|
|
43
|
+
useEventListener(element, 'scroll', handleScroll); // For Mobile
|
|
44
|
+
|
|
45
|
+
useEventListener(element, 'touchmove', handleScroll);
|
|
46
|
+
return scroll;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default useScroll;
|
|
@@ -75,7 +75,7 @@ export { default as UploadQueue } from './UploadQueue';
|
|
|
75
75
|
export { default as CozyTheme } from './CozyTheme';
|
|
76
76
|
export { default as Paper } from './Paper';
|
|
77
77
|
export { default as ProgressionBanner } from './ProgressionBanner';
|
|
78
|
-
export { default as Fab } from './Fab';
|
|
78
|
+
export { default as Fab, ExtendableFab } from './Fab';
|
|
79
79
|
export { default as SquareAppIcon } from './SquareAppIcon';
|
|
80
80
|
export { default as FileImageLoader } from './FileImageLoader';
|
|
81
81
|
export { default as Radios } from './Radios';
|