@telus-uds/components-base 1.9.0 → 1.12.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 +41 -2
- package/component-docs.json +650 -27
- package/lib/Carousel/Carousel.js +672 -0
- package/lib/Carousel/CarouselContext.js +59 -0
- package/lib/Carousel/CarouselItem/CarouselItem.js +92 -0
- package/lib/Carousel/CarouselItem/index.js +13 -0
- package/lib/Carousel/dictionary.js +23 -0
- package/lib/Carousel/index.js +32 -0
- package/lib/InputSupports/InputSupports.js +10 -3
- package/lib/InputSupports/useInputSupports.js +3 -2
- package/lib/Modal/Modal.js +4 -0
- package/lib/Skeleton/Skeleton.js +1 -0
- package/lib/StepTracker/StepTracker.js +15 -12
- package/lib/TextInput/TextInput.js +3 -1
- package/lib/index.js +23 -0
- package/lib/utils/index.js +9 -0
- package/lib/utils/props/clickProps.js +2 -2
- package/lib/utils/props/handlerProps.js +77 -31
- package/lib/utils/useScrollBlocking.js +66 -0
- package/lib/utils/useScrollBlocking.native.js +11 -0
- package/lib-module/Carousel/Carousel.js +617 -0
- package/lib-module/Carousel/CarouselContext.js +43 -0
- package/lib-module/Carousel/CarouselItem/CarouselItem.js +75 -0
- package/lib-module/Carousel/CarouselItem/index.js +2 -0
- package/lib-module/Carousel/dictionary.js +16 -0
- package/lib-module/Carousel/index.js +2 -0
- package/lib-module/InputSupports/InputSupports.js +10 -3
- package/lib-module/InputSupports/useInputSupports.js +3 -2
- package/lib-module/Modal/Modal.js +3 -0
- package/lib-module/Skeleton/Skeleton.js +1 -0
- package/lib-module/StepTracker/StepTracker.js +14 -12
- package/lib-module/TextInput/TextInput.js +3 -1
- package/lib-module/index.js +2 -0
- package/lib-module/utils/index.js +1 -0
- package/lib-module/utils/props/clickProps.js +2 -2
- package/lib-module/utils/props/handlerProps.js +78 -31
- package/lib-module/utils/useScrollBlocking.js +58 -0
- package/lib-module/utils/useScrollBlocking.native.js +2 -0
- package/package.json +4 -4
- package/src/Carousel/Carousel.jsx +649 -0
- package/src/Carousel/CarouselContext.jsx +30 -0
- package/src/Carousel/CarouselItem/CarouselItem.jsx +66 -0
- package/src/Carousel/CarouselItem/index.js +3 -0
- package/src/Carousel/dictionary.js +16 -0
- package/src/Carousel/index.js +2 -0
- package/src/InputSupports/InputSupports.jsx +18 -3
- package/src/InputSupports/useInputSupports.js +2 -2
- package/src/Modal/Modal.jsx +3 -1
- package/src/Skeleton/Skeleton.jsx +1 -0
- package/src/StepTracker/StepTracker.jsx +21 -8
- package/src/TextInput/TextInput.jsx +1 -1
- package/src/index.js +2 -0
- package/src/utils/index.js +1 -0
- package/src/utils/props/clickProps.js +2 -2
- package/src/utils/props/handlerProps.js +64 -16
- package/src/utils/useScrollBlocking.js +57 -0
- package/src/utils/useScrollBlocking.native.js +2 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { View, Platform } from 'react-native'
|
|
4
|
+
import {
|
|
5
|
+
layoutTags,
|
|
6
|
+
getA11yPropsFromHtmlTag,
|
|
7
|
+
selectSystemProps,
|
|
8
|
+
a11yProps,
|
|
9
|
+
viewProps
|
|
10
|
+
} from '../../utils'
|
|
11
|
+
import { useCarousel } from '../CarouselContext'
|
|
12
|
+
|
|
13
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `Carousel.Item` is used to wrap the content of an individual slide and is suppsoed to be the
|
|
17
|
+
* only top-level component passed to the `Carousel`
|
|
18
|
+
*/
|
|
19
|
+
const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) => {
|
|
20
|
+
const { width, activeIndex } = useCarousel()
|
|
21
|
+
const selectedProps = selectProps({
|
|
22
|
+
...rest,
|
|
23
|
+
...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const focusabilityProps = activeIndex === elementIndex ? {} : a11yProps.nonFocusableProps
|
|
27
|
+
const style = { width }
|
|
28
|
+
if (hidden && Platform.OS === 'web') {
|
|
29
|
+
// On web, visibility: hidden makes all children non-focusable. It doesn't exist on native.
|
|
30
|
+
style.visibility = 'hidden'
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<View style={style} {...selectedProps} {...focusabilityProps}>
|
|
34
|
+
{children}
|
|
35
|
+
</View>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
CarouselItem.propTypes = {
|
|
40
|
+
...selectedSystemPropTypes,
|
|
41
|
+
/**
|
|
42
|
+
* Index of the current slide
|
|
43
|
+
* Don't pass this prop when using `Carousel.Item` as it is already being passed by `Carousel` top-level component
|
|
44
|
+
*/
|
|
45
|
+
elementIndex: PropTypes.number,
|
|
46
|
+
/**
|
|
47
|
+
* Provide custom accessibilityLabelledBy for Carousel slide
|
|
48
|
+
*/
|
|
49
|
+
accessibilityLabelledBy: PropTypes.string,
|
|
50
|
+
/**
|
|
51
|
+
* Content of the slide
|
|
52
|
+
*/
|
|
53
|
+
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
|
54
|
+
/**
|
|
55
|
+
* Sets the HTML tag of the outer container. By default `'li'` so that assistive technology sees
|
|
56
|
+
* the Carousel as a list of items.
|
|
57
|
+
*
|
|
58
|
+
* Carousel's innermost container defaults to `'ul'` which can be overridden. If the tag of either
|
|
59
|
+
* `Carousel` or `Carousel.Item` is overriden, the other should be too, to avoid producing invalid HTML.
|
|
60
|
+
*/
|
|
61
|
+
tag: PropTypes.oneOf(layoutTags)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
CarouselItem.displayName = 'Carousel.Item'
|
|
65
|
+
|
|
66
|
+
export default CarouselItem
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 'stepLabel' and 'stepTrackerLabel' are passed down to StepTracker
|
|
2
|
+
export default {
|
|
3
|
+
en: {
|
|
4
|
+
carouselLabel: '%{stepCount} items',
|
|
5
|
+
iconButtonLabel: 'Show %{itemLabel} %{targetStep} of %{stepCount}',
|
|
6
|
+
stepLabel: '%{itemLabel} %{stepNumber}',
|
|
7
|
+
stepTrackerLabel: '%{itemLabel} %{stepNumber} of %{stepCount}'
|
|
8
|
+
},
|
|
9
|
+
fr: {
|
|
10
|
+
// TODO: French translations here
|
|
11
|
+
carouselLabel: '(fr) %{stepCount} items',
|
|
12
|
+
iconButtonLabel: '(fr) Show %{itemLabel} %{targetStep} of %{stepCount}',
|
|
13
|
+
stepLabel: '(fr) %{itemLabel} %{stepNumber}',
|
|
14
|
+
stepTrackerLabel: '(fr) %{itemLabel} %{stepNumber} of %{stepCount}'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -9,7 +9,17 @@ import useInputSupports from './useInputSupports'
|
|
|
9
9
|
|
|
10
10
|
const InputSupports = forwardRef(
|
|
11
11
|
(
|
|
12
|
-
{
|
|
12
|
+
{
|
|
13
|
+
children,
|
|
14
|
+
copy = 'en',
|
|
15
|
+
label,
|
|
16
|
+
hint,
|
|
17
|
+
hintPosition = 'inline',
|
|
18
|
+
feedback,
|
|
19
|
+
tooltip,
|
|
20
|
+
validation,
|
|
21
|
+
nativeID
|
|
22
|
+
},
|
|
13
23
|
ref
|
|
14
24
|
) => {
|
|
15
25
|
const { space } = useThemeTokens('InputSupports')
|
|
@@ -18,7 +28,8 @@ const InputSupports = forwardRef(
|
|
|
18
28
|
feedback,
|
|
19
29
|
hint,
|
|
20
30
|
label,
|
|
21
|
-
validation
|
|
31
|
+
validation,
|
|
32
|
+
nativeID
|
|
22
33
|
})
|
|
23
34
|
|
|
24
35
|
return (
|
|
@@ -72,7 +83,11 @@ InputSupports.propTypes = {
|
|
|
72
83
|
/**
|
|
73
84
|
* Use to visually mark an input as valid or invalid.
|
|
74
85
|
*/
|
|
75
|
-
validation: PropTypes.oneOf(['error', 'success'])
|
|
86
|
+
validation: PropTypes.oneOf(['error', 'success']),
|
|
87
|
+
/**
|
|
88
|
+
* ID for DOM element on web
|
|
89
|
+
*/
|
|
90
|
+
nativeID: PropTypes.string
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
export default InputSupports
|
|
@@ -2,7 +2,7 @@ import useUniqueId from '../utils/useUniqueId'
|
|
|
2
2
|
|
|
3
3
|
const joinDefined = (array) => array.filter((item) => item !== undefined).join(' ')
|
|
4
4
|
|
|
5
|
-
const useInputSupports = ({ label, feedback, validation, hint }) => {
|
|
5
|
+
const useInputSupports = ({ label, feedback, validation, hint, nativeID }) => {
|
|
6
6
|
const hasValidationError = validation === 'error'
|
|
7
7
|
|
|
8
8
|
const inputId = useUniqueId('input')
|
|
@@ -20,7 +20,7 @@ const useInputSupports = ({ label, feedback, validation, hint }) => {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
return {
|
|
23
|
-
inputId,
|
|
23
|
+
inputId: nativeID || inputId,
|
|
24
24
|
hintId,
|
|
25
25
|
feedbackId,
|
|
26
26
|
a11yProps
|
package/src/Modal/Modal.jsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useViewport } from '../ViewportProvider'
|
|
23
23
|
import IconButton from '../IconButton'
|
|
24
24
|
import dictionary from './dictionary'
|
|
25
|
+
import useScrollBlocking from '../utils/useScrollBlocking'
|
|
25
26
|
|
|
26
27
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
27
28
|
|
|
@@ -89,6 +90,7 @@ const Modal = forwardRef(
|
|
|
89
90
|
({ children, isOpen, onClose, maxWidth, tokens, variant, copy, closeButton, ...rest }, ref) => {
|
|
90
91
|
const viewport = useViewport()
|
|
91
92
|
const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth })
|
|
93
|
+
const modalRef = useScrollBlocking(isOpen)
|
|
92
94
|
|
|
93
95
|
const { closeIcon: CloseIconComponent } = themeTokens
|
|
94
96
|
|
|
@@ -113,7 +115,7 @@ const Modal = forwardRef(
|
|
|
113
115
|
|
|
114
116
|
return (
|
|
115
117
|
<NativeModal transparent {...selectProps(rest)}>
|
|
116
|
-
<View style={[staticStyles.positioningContainer]}>
|
|
118
|
+
<View style={[staticStyles.positioningContainer]} ref={modalRef}>
|
|
117
119
|
<View
|
|
118
120
|
style={[staticStyles.sizingContainer, selectContainerStyles(themeTokens)]}
|
|
119
121
|
pointerEvents="box-none" // don't capture backdrop press events
|
|
@@ -95,10 +95,17 @@ const StepTracker = forwardRef(
|
|
|
95
95
|
}
|
|
96
96
|
)
|
|
97
97
|
const getCopy = useCopy({ dictionary, copy })
|
|
98
|
-
const stepTrackerLabel =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const stepTrackerLabel = showStepTrackerLabel
|
|
99
|
+
? getCopy('stepTrackerLabel')
|
|
100
|
+
.replace('%{stepNumber}', current < steps.length ? current + 1 : steps.length)
|
|
101
|
+
.replace('%{stepCount}', steps.length)
|
|
102
|
+
.replace(
|
|
103
|
+
'%{stepLabel}',
|
|
104
|
+
current < steps.length ? steps[current] : steps[steps.length - 1]
|
|
105
|
+
)
|
|
106
|
+
: ''
|
|
107
|
+
const getStepLabel = (index) =>
|
|
108
|
+
themeTokens.showStepLabel ? getCopy('stepLabel').replace('%{stepNumber}', index + 1) : ''
|
|
102
109
|
if (!steps.length) return null
|
|
103
110
|
const selectedProps = selectProps({
|
|
104
111
|
accessibilityLabel: stepTrackerLabel,
|
|
@@ -123,7 +130,7 @@ const StepTracker = forwardRef(
|
|
|
123
130
|
status={current}
|
|
124
131
|
key={label}
|
|
125
132
|
label={label}
|
|
126
|
-
name={
|
|
133
|
+
name={getStepLabel(index)}
|
|
127
134
|
stepIndex={index}
|
|
128
135
|
stepCount={steps.length}
|
|
129
136
|
tokens={themeTokens}
|
|
@@ -148,13 +155,19 @@ const StepTracker = forwardRef(
|
|
|
148
155
|
)
|
|
149
156
|
StepTracker.displayName = 'StepTracker'
|
|
150
157
|
|
|
158
|
+
// If a language dictionary entry is provided, it must contain every key
|
|
159
|
+
const dictionaryContentShape = PropTypes.shape({
|
|
160
|
+
stepLabel: PropTypes.string.isRequired,
|
|
161
|
+
stepTrackerLabel: PropTypes.string.isRequired
|
|
162
|
+
})
|
|
163
|
+
|
|
151
164
|
StepTracker.propTypes = {
|
|
152
165
|
...selectedSystemPropTypes,
|
|
153
166
|
current: PropTypes.number,
|
|
154
|
-
copy: PropTypes.oneOf(['en', 'fr']),
|
|
167
|
+
copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
|
|
155
168
|
dictionary: PropTypes.shape({
|
|
156
|
-
en:
|
|
157
|
-
fr:
|
|
169
|
+
en: dictionaryContentShape,
|
|
170
|
+
fr: dictionaryContentShape
|
|
158
171
|
}),
|
|
159
172
|
steps: PropTypes.arrayOf(PropTypes.string),
|
|
160
173
|
tokens: getTokensPropType('StepTracker'),
|
|
@@ -51,7 +51,7 @@ const TextInput = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
<InputSupports {...supportsProps}>
|
|
54
|
+
<InputSupports nativeID={selectedProps.nativeID} {...supportsProps}>
|
|
55
55
|
{({ inputId, ...props }) => (
|
|
56
56
|
<TextInputBase ref={ref} {...inputProps} nativeID={inputId} {...props} />
|
|
57
57
|
)}
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as ActivityIndicator } from './ActivityIndicator'
|
|
|
3
3
|
export { default as Box } from './Box'
|
|
4
4
|
export * from './Button'
|
|
5
5
|
export { default as Card, PressableCardBase } from './Card'
|
|
6
|
+
export * from './Carousel'
|
|
6
7
|
export { default as Checkbox } from './Checkbox'
|
|
7
8
|
export * from './Checkbox'
|
|
8
9
|
export { default as Divider } from './Divider'
|
|
@@ -16,6 +17,7 @@ export { default as Icon } from './Icon'
|
|
|
16
17
|
export * from './Icon'
|
|
17
18
|
export { default as IconButton } from './IconButton'
|
|
18
19
|
export { default as InputLabel } from './InputLabel'
|
|
20
|
+
export { default as InputSupports } from './InputSupports'
|
|
19
21
|
export * from './Link'
|
|
20
22
|
export { default as List, ListItem, ListBase } from './List'
|
|
21
23
|
export { default as Modal } from './Modal'
|
package/src/utils/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { default as useCopy } from './useCopy'
|
|
|
10
10
|
export { default as useHash } from './useHash'
|
|
11
11
|
export { default as useSpacingScale } from './useSpacingScale'
|
|
12
12
|
export { default as useResponsiveProp } from './useResponsiveProp'
|
|
13
|
+
export { default as useScrollBlocking } from './useScrollBlocking'
|
|
13
14
|
export * from './useResponsiveProp'
|
|
14
15
|
export { default as useUniqueId } from './useUniqueId'
|
|
15
16
|
export { default as withLinkRouter } from './withLinkRouter'
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
|
+
import { Platform } from 'react-native'
|
|
3
|
+
import getPropSelector from './getPropSelector'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
const focusHandlerProps = {
|
|
4
6
|
types: {
|
|
5
7
|
/**
|
|
6
8
|
* onBlur handler
|
|
@@ -10,14 +12,11 @@ export const focusHandlerProps = {
|
|
|
10
12
|
* onFocus handler
|
|
11
13
|
*/
|
|
12
14
|
onFocus: PropTypes.func
|
|
13
|
-
}
|
|
14
|
-
select: ({ onBlur, onFocus }) => ({
|
|
15
|
-
onBlur,
|
|
16
|
-
onFocus
|
|
17
|
-
})
|
|
15
|
+
}
|
|
18
16
|
}
|
|
17
|
+
focusHandlerProps.select = getPropSelector(focusHandlerProps.types)
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
const textInputHandlerProps = {
|
|
21
20
|
types: {
|
|
22
21
|
/**
|
|
23
22
|
* onChange handler
|
|
@@ -34,14 +33,63 @@ export const textInputHandlerProps = {
|
|
|
34
33
|
/**
|
|
35
34
|
* onSubmitEditing handler
|
|
36
35
|
*/
|
|
37
|
-
onSubmitEditing: PropTypes.func
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
onSubmitEditing: PropTypes.func,
|
|
37
|
+
/**
|
|
38
|
+
* onContentSizeChange handler
|
|
39
|
+
*/
|
|
40
|
+
onContentSizeChange: PropTypes.func,
|
|
41
|
+
/**
|
|
42
|
+
* onEndEditing handler
|
|
43
|
+
*/
|
|
44
|
+
onEndEditing: PropTypes.func,
|
|
45
|
+
/**
|
|
46
|
+
* onScroll handler
|
|
47
|
+
*/
|
|
48
|
+
onScroll: PropTypes.func,
|
|
49
|
+
/**
|
|
50
|
+
* onSelectionChange handler
|
|
51
|
+
*/
|
|
52
|
+
onSelectionChange: PropTypes.func,
|
|
53
|
+
/**
|
|
54
|
+
* onKeyPress handler
|
|
55
|
+
*/
|
|
56
|
+
onKeyPress: PropTypes.func,
|
|
57
|
+
/**
|
|
58
|
+
* onKeyUp handler (only supported on Web)
|
|
59
|
+
*/
|
|
60
|
+
onKeyUp: PropTypes.func,
|
|
61
|
+
/**
|
|
62
|
+
* onKeyDown handler (only supported on Web)
|
|
63
|
+
*/
|
|
64
|
+
onKeyDown: PropTypes.func
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const selectTextInputHandlers = getPropSelector(textInputHandlerProps.types)
|
|
68
|
+
textInputHandlerProps.select = (props) => {
|
|
69
|
+
// Support for onKeyPress/onKeyUp/onKeyDown is inconsistent between React Native and React Native Web
|
|
70
|
+
const { onKeyPress, onKeyUp, onKeyDown, ...resolvedProps } = selectTextInputHandlers(props)
|
|
71
|
+
if (onKeyPress || onKeyUp || onKeyDown) {
|
|
72
|
+
if (Platform.OS !== 'web') {
|
|
73
|
+
// React Native only supports onKeyPress. Call any key handlers supplied in expected order.
|
|
74
|
+
resolvedProps.onKeyPress = (event) => {
|
|
75
|
+
if (typeof onKeyDown === 'function') onKeyDown(event)
|
|
76
|
+
if (typeof onKeyPress === 'function') onKeyPress(event)
|
|
77
|
+
if (typeof onKeyUp === 'function') onKeyUp(event)
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
// React Native Web supports onKeyUp the normal way.
|
|
81
|
+
if (onKeyUp) resolvedProps.onKeyUp = onKeyUp
|
|
82
|
+
// React Native Web doesn't support the `onKeyDown` prop name, but maps a supplied onKeyPress handler
|
|
83
|
+
// to the onKeyDown event and calls it with a keydown event. Make React Native Web call either or both.
|
|
84
|
+
if (onKeyPress || onKeyDown) {
|
|
85
|
+
resolvedProps.onKeyPress = (event) => {
|
|
86
|
+
if (typeof onKeyDown === 'function') onKeyDown(event)
|
|
87
|
+
if (typeof onKeyPress === 'function') onKeyPress(event)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return resolvedProps
|
|
45
93
|
}
|
|
46
94
|
|
|
47
|
-
export
|
|
95
|
+
export { focusHandlerProps, textInputHandlerProps }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const addScrollBlocking = (preventScrolling, stopPropagation, ref) => {
|
|
4
|
+
document.body.addEventListener('touchmove', preventScrolling, { passive: false })
|
|
5
|
+
ref.current?.addEventListener('touchmove', stopPropagation)
|
|
6
|
+
document.body.style.overflow = 'hidden'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const removeScrollBlocking = (preventScrolling, stopPropagation, ref) => {
|
|
10
|
+
document.body.removeEventListener('touchmove', preventScrolling)
|
|
11
|
+
ref.current?.removeEventListener('touchmove', stopPropagation)
|
|
12
|
+
document.body.style.overflow = 'inherit'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Disables scrolling when passed `true` or an array where all items are `true`.
|
|
17
|
+
*
|
|
18
|
+
* Returns an optional callback ref. Pass this to an element if it or its children
|
|
19
|
+
* should allow touch-based scrolling within that element's bounds.
|
|
20
|
+
*
|
|
21
|
+
* @param {boolean | boolean[]} conditionProps
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
const useScrollBlocking = (conditionProps) => {
|
|
25
|
+
// useRef refs are null on first render and don't trigger a re-render when they get their
|
|
26
|
+
// element. Force re-run when ref mounts to ensure the stopPropagation listener is attached.
|
|
27
|
+
const ref = useRef()
|
|
28
|
+
const [refIsMounted, setRefIsMounted] = useState(false)
|
|
29
|
+
const callbackRef = useCallback((element) => {
|
|
30
|
+
ref.current = element
|
|
31
|
+
setRefIsMounted(Boolean(element))
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const conditionsMet = Array.isArray(conditionProps)
|
|
35
|
+
? conditionProps.every((condition) => condition)
|
|
36
|
+
: Boolean(conditionProps)
|
|
37
|
+
|
|
38
|
+
const preventScrolling = useCallback((event) => event.preventDefault(), [])
|
|
39
|
+
const stopPropagation = useCallback((event) => event.stopPropagation(), [])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const cleanup = () => removeScrollBlocking(preventScrolling, stopPropagation, ref)
|
|
43
|
+
|
|
44
|
+
if (conditionsMet) {
|
|
45
|
+
addScrollBlocking(preventScrolling, stopPropagation, ref)
|
|
46
|
+
} else {
|
|
47
|
+
cleanup()
|
|
48
|
+
}
|
|
49
|
+
return cleanup
|
|
50
|
+
// preventScrolling and stopPropagation are stable callbacks with no deps, so this
|
|
51
|
+
// will re-run when conditionsMet or refIsMounted flip between true and false.
|
|
52
|
+
}, [preventScrolling, conditionsMet, stopPropagation, refIsMounted])
|
|
53
|
+
|
|
54
|
+
return callbackRef
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default useScrollBlocking
|