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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "83.2.0",
3
+ "version": "83.3.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -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
@@ -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.
@@ -1,3 +1,4 @@
1
1
  import Fab from '@material-ui/core/Fab'
2
2
 
3
3
  export default Fab
4
+ export { default as ExtendableFab } from './ExtendableFab'
@@ -0,0 +1,11 @@
1
+ export function isRef(obj) {
2
+ return (
3
+ obj !== null &&
4
+ typeof obj === 'object' &&
5
+ Object.prototype.hasOwnProperty.call(obj, 'current')
6
+ )
7
+ }
8
+
9
+ export function unRef(element) {
10
+ return isRef(element) ? element.current : element
11
+ }
@@ -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
- if (element && event && cb) {
6
- element.addEventListener(event, cb)
6
+ const currentElement = unRef(element)
7
+ if (currentElement && event && cb) {
8
+ currentElement.addEventListener(event, cb)
7
9
 
8
10
  return () => {
9
- element.removeEventListener(event, cb)
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,2 +1,3 @@
1
1
  import Fab from '@material-ui/core/Fab';
2
- export default Fab;
2
+ export default Fab;
3
+ export { default as ExtendableFab } from './ExtendableFab';
@@ -0,0 +1,6 @@
1
+ export function isRef(obj) {
2
+ return obj !== null && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, 'current');
3
+ }
4
+ export function unRef(element) {
5
+ return isRef(element) ? element.current : element;
6
+ }
@@ -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
- if (element && event && cb) {
6
- element.addEventListener(event, cb);
6
+ var currentElement = unRef(element);
7
+
8
+ if (currentElement && event && cb) {
9
+ currentElement.addEventListener(event, cb);
7
10
  return function () {
8
- element.removeEventListener(event, cb);
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';