@atlaskit/react-select 2.2.0 → 2.2.1
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 +8 -0
- package/dist/cjs/emotion/components/containers.js +111 -0
- package/dist/cjs/emotion/components/control.js +110 -0
- package/dist/cjs/emotion/components/group.js +71 -0
- package/dist/cjs/emotion/components/index.js +52 -0
- package/dist/cjs/emotion/components/indicators.js +137 -0
- package/dist/cjs/emotion/components/input.js +94 -0
- package/dist/cjs/emotion/components/internal/a11y-text.js +36 -0
- package/dist/cjs/emotion/components/internal/dummy-input.js +44 -0
- package/dist/cjs/emotion/components/internal/index.js +34 -0
- package/dist/cjs/emotion/components/internal/notify-open-layer-observer.js +21 -0
- package/dist/cjs/emotion/components/internal/required-input.js +43 -0
- package/dist/cjs/emotion/components/internal/scroll-manager.js +57 -0
- package/dist/cjs/emotion/components/internal/use-scroll-capture.js +132 -0
- package/dist/cjs/emotion/components/internal/use-scroll-lock.js +149 -0
- package/dist/cjs/emotion/components/live-region.js +182 -0
- package/dist/cjs/emotion/components/menu.js +456 -0
- package/dist/cjs/emotion/components/multi-value.js +224 -0
- package/dist/cjs/emotion/components/option.js +82 -0
- package/dist/cjs/emotion/components/placeholder.js +34 -0
- package/dist/cjs/emotion/components/single-value.js +40 -0
- package/dist/es2019/emotion/components/containers.js +109 -0
- package/dist/es2019/emotion/components/control.js +107 -0
- package/dist/es2019/emotion/components/group.js +59 -0
- package/dist/es2019/emotion/components/index.js +41 -0
- package/dist/es2019/emotion/components/indicators.js +128 -0
- package/dist/es2019/emotion/components/input.js +86 -0
- package/dist/es2019/emotion/components/internal/a11y-text.js +27 -0
- package/dist/es2019/emotion/components/internal/dummy-input.js +37 -0
- package/dist/es2019/emotion/components/internal/index.js +4 -0
- package/dist/es2019/emotion/components/internal/notify-open-layer-observer.js +16 -0
- package/dist/es2019/emotion/components/internal/required-input.js +35 -0
- package/dist/es2019/emotion/components/internal/scroll-manager.js +49 -0
- package/dist/es2019/emotion/components/internal/use-scroll-capture.js +128 -0
- package/dist/es2019/emotion/components/internal/use-scroll-lock.js +143 -0
- package/dist/es2019/emotion/components/live-region.js +178 -0
- package/dist/es2019/emotion/components/menu.js +450 -0
- package/dist/es2019/emotion/components/multi-value.js +227 -0
- package/dist/es2019/emotion/components/option.js +78 -0
- package/dist/es2019/emotion/components/placeholder.js +28 -0
- package/dist/es2019/emotion/components/single-value.js +34 -0
- package/dist/esm/emotion/components/containers.js +105 -0
- package/dist/esm/emotion/components/control.js +103 -0
- package/dist/esm/emotion/components/group.js +65 -0
- package/dist/esm/emotion/components/index.js +43 -0
- package/dist/esm/emotion/components/indicators.js +132 -0
- package/dist/esm/emotion/components/input.js +89 -0
- package/dist/esm/emotion/components/internal/a11y-text.js +29 -0
- package/dist/esm/emotion/components/internal/dummy-input.js +38 -0
- package/dist/esm/emotion/components/internal/index.js +4 -0
- package/dist/esm/emotion/components/internal/notify-open-layer-observer.js +15 -0
- package/dist/esm/emotion/components/internal/required-input.js +36 -0
- package/dist/esm/emotion/components/internal/scroll-manager.js +49 -0
- package/dist/esm/emotion/components/internal/use-scroll-capture.js +126 -0
- package/dist/esm/emotion/components/internal/use-scroll-lock.js +143 -0
- package/dist/esm/emotion/components/live-region.js +175 -0
- package/dist/esm/emotion/components/menu.js +454 -0
- package/dist/esm/emotion/components/multi-value.js +217 -0
- package/dist/esm/emotion/components/option.js +75 -0
- package/dist/esm/emotion/components/placeholder.js +27 -0
- package/dist/esm/emotion/components/single-value.js +33 -0
- package/dist/types/emotion/components/containers.d.ts +54 -0
- package/dist/types/emotion/components/control.d.ts +42 -0
- package/dist/types/emotion/components/group.d.ts +52 -0
- package/dist/types/emotion/components/index.d.ts +67 -0
- package/dist/types/emotion/components/indicators.d.ts +73 -0
- package/dist/types/emotion/components/input.d.ts +37 -0
- package/dist/types/emotion/components/internal/a11y-text.d.ts +8 -0
- package/dist/types/emotion/components/internal/dummy-input.d.ts +9 -0
- package/dist/types/emotion/components/internal/index.d.ts +4 -0
- package/dist/types/emotion/components/internal/notify-open-layer-observer.d.ts +11 -0
- package/dist/types/emotion/components/internal/required-input.d.ts +10 -0
- package/dist/types/emotion/components/internal/scroll-manager.d.ts +17 -0
- package/dist/types/emotion/components/internal/use-scroll-capture.d.ts +12 -0
- package/dist/types/emotion/components/internal/use-scroll-lock.d.ts +9 -0
- package/dist/types/emotion/components/live-region.d.ts +25 -0
- package/dist/types/emotion/components/menu.d.ts +116 -0
- package/dist/types/emotion/components/multi-value.d.ts +47 -0
- package/dist/types/emotion/components/option.d.ts +49 -0
- package/dist/types/emotion/components/placeholder.d.ts +22 -0
- package/dist/types/emotion/components/single-value.d.ts +28 -0
- package/dist/types-ts4.5/emotion/components/containers.d.ts +54 -0
- package/dist/types-ts4.5/emotion/components/control.d.ts +42 -0
- package/dist/types-ts4.5/emotion/components/group.d.ts +52 -0
- package/dist/types-ts4.5/emotion/components/index.d.ts +67 -0
- package/dist/types-ts4.5/emotion/components/indicators.d.ts +73 -0
- package/dist/types-ts4.5/emotion/components/input.d.ts +37 -0
- package/dist/types-ts4.5/emotion/components/internal/a11y-text.d.ts +8 -0
- package/dist/types-ts4.5/emotion/components/internal/dummy-input.d.ts +9 -0
- package/dist/types-ts4.5/emotion/components/internal/index.d.ts +4 -0
- package/dist/types-ts4.5/emotion/components/internal/notify-open-layer-observer.d.ts +11 -0
- package/dist/types-ts4.5/emotion/components/internal/required-input.d.ts +10 -0
- package/dist/types-ts4.5/emotion/components/internal/scroll-manager.d.ts +17 -0
- package/dist/types-ts4.5/emotion/components/internal/use-scroll-capture.d.ts +12 -0
- package/dist/types-ts4.5/emotion/components/internal/use-scroll-lock.d.ts +9 -0
- package/dist/types-ts4.5/emotion/components/live-region.d.ts +25 -0
- package/dist/types-ts4.5/emotion/components/menu.d.ts +116 -0
- package/dist/types-ts4.5/emotion/components/multi-value.d.ts +47 -0
- package/dist/types-ts4.5/emotion/components/option.d.ts +49 -0
- package/dist/types-ts4.5/emotion/components/placeholder.d.ts +22 -0
- package/dist/types-ts4.5/emotion/components/single-value.d.ts +28 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
2
|
+
/**
|
|
3
|
+
* @jsxRuntime classic
|
|
4
|
+
* @jsx jsx
|
|
5
|
+
*/
|
|
6
|
+
import { css, jsx } from '@emotion/react';
|
|
7
|
+
|
|
8
|
+
// Assistive text to describe visual elements. Hidden for sighted users.
|
|
9
|
+
const styles = css({
|
|
10
|
+
width: 1,
|
|
11
|
+
height: 1,
|
|
12
|
+
padding: 0,
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
zIndex: 9999,
|
|
15
|
+
border: 0,
|
|
16
|
+
clip: 'rect(1px, 1px, 1px, 1px)',
|
|
17
|
+
label: 'a11yText',
|
|
18
|
+
overflow: 'hidden',
|
|
19
|
+
userSelect: 'none',
|
|
20
|
+
// while hidden text is sitting in the DOM, it should not be selectable
|
|
21
|
+
whiteSpace: 'nowrap'
|
|
22
|
+
});
|
|
23
|
+
const A11yText = props => jsx("span", _extends({
|
|
24
|
+
css: styles
|
|
25
|
+
// eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
|
|
26
|
+
}, props));
|
|
27
|
+
export default A11yText;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
2
|
+
/**
|
|
3
|
+
* @jsxRuntime classic
|
|
4
|
+
* @jsx jsx
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { css, jsx } from '@emotion/react';
|
|
8
|
+
import { removeProps } from '../../../utils';
|
|
9
|
+
const dummyInputStyles = css({
|
|
10
|
+
width: 1,
|
|
11
|
+
padding: 0,
|
|
12
|
+
position: 'relative',
|
|
13
|
+
background: 0,
|
|
14
|
+
border: 0,
|
|
15
|
+
caretColor: 'transparent',
|
|
16
|
+
color: 'transparent',
|
|
17
|
+
// eslint-disable-next-line @atlaskit/design-system/use-tokens-typography
|
|
18
|
+
fontSize: 'inherit',
|
|
19
|
+
gridArea: '1 / 1 / 2 / 3',
|
|
20
|
+
insetInlineStart: -100,
|
|
21
|
+
label: 'dummyInput',
|
|
22
|
+
opacity: 0,
|
|
23
|
+
outline: 0,
|
|
24
|
+
transform: 'scale(.01)'
|
|
25
|
+
});
|
|
26
|
+
export default function DummyInput({
|
|
27
|
+
innerRef,
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
30
|
+
// Remove animation props not meant for HTML elements
|
|
31
|
+
const filteredProps = removeProps(props, 'onExited', 'in', 'enter', 'exit', 'appear');
|
|
32
|
+
return jsx("input", _extends({
|
|
33
|
+
ref: innerRef
|
|
34
|
+
}, filteredProps, {
|
|
35
|
+
css: dummyInputStyles
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
2
|
+
/**
|
|
3
|
+
* Functional component wrapper around `useNotifyOpenLayerObserver`.
|
|
4
|
+
*
|
|
5
|
+
* This is needed as Select is a class component, which cannot use hooks directly.
|
|
6
|
+
*/
|
|
7
|
+
export function NotifyOpenLayerObserver({
|
|
8
|
+
isOpen,
|
|
9
|
+
onClose
|
|
10
|
+
}) {
|
|
11
|
+
useNotifyOpenLayerObserver({
|
|
12
|
+
isOpen,
|
|
13
|
+
onClose
|
|
14
|
+
});
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jsxRuntime classic
|
|
3
|
+
* @jsx jsx
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { css, jsx } from '@emotion/react';
|
|
7
|
+
import __noop from '@atlaskit/ds-lib/noop';
|
|
8
|
+
const styles = css({
|
|
9
|
+
width: '100%',
|
|
10
|
+
position: 'absolute',
|
|
11
|
+
insetBlockEnd: 0,
|
|
12
|
+
insetInlineEnd: 0,
|
|
13
|
+
insetInlineStart: 0,
|
|
14
|
+
label: 'requiredInput',
|
|
15
|
+
opacity: 0,
|
|
16
|
+
pointerEvents: 'none'
|
|
17
|
+
});
|
|
18
|
+
const RequiredInput = ({
|
|
19
|
+
name,
|
|
20
|
+
onFocus
|
|
21
|
+
}) => jsx("input", {
|
|
22
|
+
required: true,
|
|
23
|
+
name: name,
|
|
24
|
+
tabIndex: -1,
|
|
25
|
+
"aria-hidden": "true",
|
|
26
|
+
onFocus: onFocus,
|
|
27
|
+
css: styles
|
|
28
|
+
// Prevent `Switching from uncontrolled to controlled` error
|
|
29
|
+
,
|
|
30
|
+
value: "",
|
|
31
|
+
onChange: __noop
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line @repo/internal/react/require-jsdoc
|
|
35
|
+
export default RequiredInput;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jsxRuntime classic
|
|
3
|
+
* @jsx jsx
|
|
4
|
+
*/
|
|
5
|
+
import { Fragment } from 'react';
|
|
6
|
+
import { css, jsx } from '@emotion/react';
|
|
7
|
+
import useScrollCapture from './use-scroll-capture';
|
|
8
|
+
import useScrollLock from './use-scroll-lock';
|
|
9
|
+
const styles = css({
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
insetBlockEnd: 0,
|
|
12
|
+
insetBlockStart: 0,
|
|
13
|
+
insetInlineEnd: 0,
|
|
14
|
+
insetInlineStart: 0
|
|
15
|
+
});
|
|
16
|
+
const blurSelectInput = event => {
|
|
17
|
+
const element = event.target;
|
|
18
|
+
return element.ownerDocument.activeElement && element.ownerDocument.activeElement.blur();
|
|
19
|
+
};
|
|
20
|
+
export default function ScrollManager({
|
|
21
|
+
children,
|
|
22
|
+
lockEnabled,
|
|
23
|
+
captureEnabled = true,
|
|
24
|
+
onBottomArrive,
|
|
25
|
+
onBottomLeave,
|
|
26
|
+
onTopArrive,
|
|
27
|
+
onTopLeave
|
|
28
|
+
}) {
|
|
29
|
+
const setScrollCaptureTarget = useScrollCapture({
|
|
30
|
+
isEnabled: captureEnabled,
|
|
31
|
+
onBottomArrive,
|
|
32
|
+
onBottomLeave,
|
|
33
|
+
onTopArrive,
|
|
34
|
+
onTopLeave
|
|
35
|
+
});
|
|
36
|
+
const setScrollLockTarget = useScrollLock({
|
|
37
|
+
isEnabled: lockEnabled
|
|
38
|
+
});
|
|
39
|
+
const targetRef = element => {
|
|
40
|
+
setScrollCaptureTarget(element);
|
|
41
|
+
setScrollLockTarget(element);
|
|
42
|
+
};
|
|
43
|
+
return jsx(Fragment, null, lockEnabled &&
|
|
44
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
|
45
|
+
jsx("div", {
|
|
46
|
+
onClick: blurSelectInput,
|
|
47
|
+
css: styles
|
|
48
|
+
}), children(targetRef));
|
|
49
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { supportsPassiveEvents } from '../../../utils';
|
|
3
|
+
const cancelScroll = event => {
|
|
4
|
+
if (event.cancelable) {
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
}
|
|
7
|
+
event.stopPropagation();
|
|
8
|
+
};
|
|
9
|
+
// TODO: Fill in the hook {description}.
|
|
10
|
+
/**
|
|
11
|
+
* {description}.
|
|
12
|
+
*/
|
|
13
|
+
export default function useScrollCapture({
|
|
14
|
+
isEnabled,
|
|
15
|
+
onBottomArrive,
|
|
16
|
+
onBottomLeave,
|
|
17
|
+
onTopArrive,
|
|
18
|
+
onTopLeave
|
|
19
|
+
}) {
|
|
20
|
+
const isBottom = useRef(false);
|
|
21
|
+
const isTop = useRef(false);
|
|
22
|
+
const touchStart = useRef(0);
|
|
23
|
+
const scrollTarget = useRef(null);
|
|
24
|
+
const handleEventDelta = useCallback((event, delta) => {
|
|
25
|
+
if (scrollTarget.current === null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const {
|
|
29
|
+
scrollTop,
|
|
30
|
+
scrollHeight,
|
|
31
|
+
clientHeight
|
|
32
|
+
} = scrollTarget.current;
|
|
33
|
+
const target = scrollTarget.current;
|
|
34
|
+
const isDeltaPositive = delta > 0;
|
|
35
|
+
const availableScroll = scrollHeight - clientHeight - scrollTop;
|
|
36
|
+
let shouldCancelScroll = false;
|
|
37
|
+
|
|
38
|
+
// reset bottom/top flags
|
|
39
|
+
if (availableScroll > delta && isBottom.current) {
|
|
40
|
+
if (onBottomLeave) {
|
|
41
|
+
onBottomLeave(event);
|
|
42
|
+
}
|
|
43
|
+
isBottom.current = false;
|
|
44
|
+
}
|
|
45
|
+
if (isDeltaPositive && isTop.current) {
|
|
46
|
+
if (onTopLeave) {
|
|
47
|
+
onTopLeave(event);
|
|
48
|
+
}
|
|
49
|
+
isTop.current = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// bottom limit
|
|
53
|
+
if (isDeltaPositive && delta > availableScroll) {
|
|
54
|
+
if (onBottomArrive && !isBottom.current) {
|
|
55
|
+
onBottomArrive(event);
|
|
56
|
+
}
|
|
57
|
+
target.scrollTop = scrollHeight;
|
|
58
|
+
shouldCancelScroll = true;
|
|
59
|
+
isBottom.current = true;
|
|
60
|
+
|
|
61
|
+
// top limit
|
|
62
|
+
} else if (!isDeltaPositive && -delta > scrollTop) {
|
|
63
|
+
if (onTopArrive && !isTop.current) {
|
|
64
|
+
onTopArrive(event);
|
|
65
|
+
}
|
|
66
|
+
target.scrollTop = 0;
|
|
67
|
+
shouldCancelScroll = true;
|
|
68
|
+
isTop.current = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// cancel scroll
|
|
72
|
+
if (shouldCancelScroll) {
|
|
73
|
+
cancelScroll(event);
|
|
74
|
+
}
|
|
75
|
+
}, [onBottomArrive, onBottomLeave, onTopArrive, onTopLeave]);
|
|
76
|
+
const onWheel = useCallback(event => {
|
|
77
|
+
handleEventDelta(event, event.deltaY);
|
|
78
|
+
}, [handleEventDelta]);
|
|
79
|
+
const onTouchStart = useCallback(event => {
|
|
80
|
+
// set touch start so we can calculate touchmove delta
|
|
81
|
+
touchStart.current = event.changedTouches[0].clientY;
|
|
82
|
+
}, []);
|
|
83
|
+
const onTouchMove = useCallback(event => {
|
|
84
|
+
const deltaY = touchStart.current - event.changedTouches[0].clientY;
|
|
85
|
+
handleEventDelta(event, deltaY);
|
|
86
|
+
}, [handleEventDelta]);
|
|
87
|
+
const startListening = useCallback(el => {
|
|
88
|
+
// bail early if no element is available to attach to
|
|
89
|
+
if (!el) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const notPassive = supportsPassiveEvents ? {
|
|
93
|
+
passive: false
|
|
94
|
+
} : false;
|
|
95
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
96
|
+
el.addEventListener('wheel', onWheel, notPassive);
|
|
97
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
98
|
+
el.addEventListener('touchstart', onTouchStart, notPassive);
|
|
99
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
100
|
+
el.addEventListener('touchmove', onTouchMove, notPassive);
|
|
101
|
+
}, [onTouchMove, onTouchStart, onWheel]);
|
|
102
|
+
const stopListening = useCallback(el => {
|
|
103
|
+
// bail early if no element is available to detach from
|
|
104
|
+
if (!el) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
109
|
+
el.removeEventListener('wheel', onWheel, false);
|
|
110
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
111
|
+
el.removeEventListener('touchstart', onTouchStart, false);
|
|
112
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
113
|
+
el.removeEventListener('touchmove', onTouchMove, false);
|
|
114
|
+
}, [onTouchMove, onTouchStart, onWheel]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!isEnabled) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const element = scrollTarget.current;
|
|
120
|
+
startListening(element);
|
|
121
|
+
return () => {
|
|
122
|
+
stopListening(element);
|
|
123
|
+
};
|
|
124
|
+
}, [isEnabled, startListening, stopListening]);
|
|
125
|
+
return element => {
|
|
126
|
+
scrollTarget.current = element;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
const STYLE_KEYS = ['boxSizing', 'height', 'overflow', 'paddingRight', 'position'];
|
|
3
|
+
const LOCK_STYLES = {
|
|
4
|
+
boxSizing: 'border-box',
|
|
5
|
+
// account for possible declaration `width: 100%;` on body
|
|
6
|
+
overflow: 'hidden',
|
|
7
|
+
position: 'relative',
|
|
8
|
+
height: '100%'
|
|
9
|
+
};
|
|
10
|
+
function preventTouchMove(e) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
}
|
|
13
|
+
function allowTouchMove(e) {
|
|
14
|
+
e.stopPropagation();
|
|
15
|
+
}
|
|
16
|
+
function preventInertiaScroll() {
|
|
17
|
+
const top = this.scrollTop;
|
|
18
|
+
const totalScroll = this.scrollHeight;
|
|
19
|
+
const currentScroll = top + this.offsetHeight;
|
|
20
|
+
if (top === 0) {
|
|
21
|
+
this.scrollTop = 1;
|
|
22
|
+
} else if (currentScroll === totalScroll) {
|
|
23
|
+
this.scrollTop = top - 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// `ontouchstart` check works on most browsers
|
|
28
|
+
// `maxTouchPoints` works on IE10/11 and Surface
|
|
29
|
+
function isTouchDevice() {
|
|
30
|
+
// eslint-disable-next-line compat/compat
|
|
31
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints;
|
|
32
|
+
}
|
|
33
|
+
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
|
|
34
|
+
let activeScrollLocks = 0;
|
|
35
|
+
const listenerOptions = {
|
|
36
|
+
capture: false,
|
|
37
|
+
passive: false
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// TODO: Fill in the hook {description}.
|
|
41
|
+
/**
|
|
42
|
+
* {description}.
|
|
43
|
+
*/
|
|
44
|
+
export default function useScrollLock({
|
|
45
|
+
isEnabled,
|
|
46
|
+
accountForScrollbars = true
|
|
47
|
+
}) {
|
|
48
|
+
const originalStyles = useRef({});
|
|
49
|
+
const scrollTarget = useRef(null);
|
|
50
|
+
const addScrollLock = useCallback(touchScrollTarget => {
|
|
51
|
+
if (!canUseDOM) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const target = document.body;
|
|
55
|
+
const targetStyle = target && target.style;
|
|
56
|
+
if (accountForScrollbars) {
|
|
57
|
+
// store any styles already applied to the body
|
|
58
|
+
STYLE_KEYS.forEach(key => {
|
|
59
|
+
const val = targetStyle && targetStyle[key];
|
|
60
|
+
originalStyles.current[key] = val;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// apply the lock styles and padding if this is the first scroll lock
|
|
65
|
+
if (accountForScrollbars && activeScrollLocks < 1) {
|
|
66
|
+
const currentPadding = parseInt(originalStyles.current.paddingRight, 10) || 0;
|
|
67
|
+
const clientWidth = document.body ? document.body.clientWidth : 0;
|
|
68
|
+
const adjustedPadding = window.innerWidth - clientWidth + currentPadding || 0;
|
|
69
|
+
Object.keys(LOCK_STYLES).forEach(key => {
|
|
70
|
+
const val = LOCK_STYLES[key];
|
|
71
|
+
if (targetStyle) {
|
|
72
|
+
targetStyle[key] = val;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (targetStyle) {
|
|
76
|
+
targetStyle.paddingRight = `${adjustedPadding}px`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// account for touch devices
|
|
81
|
+
if (target && isTouchDevice()) {
|
|
82
|
+
// Mobile Safari ignores { overflow: hidden } declaration on the body.
|
|
83
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
84
|
+
target.addEventListener('touchmove', preventTouchMove, listenerOptions);
|
|
85
|
+
|
|
86
|
+
// Allow scroll on provided target
|
|
87
|
+
if (touchScrollTarget) {
|
|
88
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
89
|
+
touchScrollTarget.addEventListener('touchstart', preventInertiaScroll, listenerOptions);
|
|
90
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
91
|
+
touchScrollTarget.addEventListener('touchmove', allowTouchMove, listenerOptions);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// increment active scroll locks
|
|
96
|
+
activeScrollLocks += 1;
|
|
97
|
+
}, [accountForScrollbars]);
|
|
98
|
+
const removeScrollLock = useCallback(touchScrollTarget => {
|
|
99
|
+
if (!canUseDOM) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const target = document.body;
|
|
103
|
+
const targetStyle = target && target.style;
|
|
104
|
+
|
|
105
|
+
// safely decrement active scroll locks
|
|
106
|
+
activeScrollLocks = Math.max(activeScrollLocks - 1, 0);
|
|
107
|
+
|
|
108
|
+
// reapply original body styles, if any
|
|
109
|
+
if (accountForScrollbars && activeScrollLocks < 1) {
|
|
110
|
+
STYLE_KEYS.forEach(key => {
|
|
111
|
+
const val = originalStyles.current[key];
|
|
112
|
+
if (targetStyle) {
|
|
113
|
+
targetStyle[key] = val;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// remove touch listeners
|
|
119
|
+
if (target && isTouchDevice()) {
|
|
120
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
121
|
+
target.removeEventListener('touchmove', preventTouchMove, listenerOptions);
|
|
122
|
+
if (touchScrollTarget) {
|
|
123
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
124
|
+
touchScrollTarget.removeEventListener('touchstart', preventInertiaScroll, listenerOptions);
|
|
125
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
126
|
+
touchScrollTarget.removeEventListener('touchmove', allowTouchMove, listenerOptions);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, [accountForScrollbars]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!isEnabled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const element = scrollTarget.current;
|
|
135
|
+
addScrollLock(element);
|
|
136
|
+
return () => {
|
|
137
|
+
removeScrollLock(element);
|
|
138
|
+
};
|
|
139
|
+
}, [isEnabled, addScrollLock, removeScrollLock]);
|
|
140
|
+
return element => {
|
|
141
|
+
scrollTarget.current = element;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* eslint-disable @atlaskit/platform/ensure-feature-flag-prefix */
|
|
2
|
+
/**
|
|
3
|
+
* @jsxRuntime classic
|
|
4
|
+
* @jsx jsx
|
|
5
|
+
* @jsxFrag React.Fragment
|
|
6
|
+
*/
|
|
7
|
+
import React, { Fragment, useMemo, useRef } from 'react';
|
|
8
|
+
import { jsx } from '@emotion/react';
|
|
9
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
10
|
+
import { defaultAriaLiveMessages } from '../../accessibility';
|
|
11
|
+
import A11yText from './internal/a11y-text';
|
|
12
|
+
|
|
13
|
+
// ==============================
|
|
14
|
+
// Root Container
|
|
15
|
+
// ==============================
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @repo/internal/react/require-jsdoc
|
|
18
|
+
const LiveRegion = props => {
|
|
19
|
+
const {
|
|
20
|
+
ariaSelection,
|
|
21
|
+
focusedOption,
|
|
22
|
+
focusedValue,
|
|
23
|
+
focusableOptions,
|
|
24
|
+
isFocused,
|
|
25
|
+
selectValue,
|
|
26
|
+
selectProps,
|
|
27
|
+
id,
|
|
28
|
+
isAppleDevice
|
|
29
|
+
} = props;
|
|
30
|
+
const {
|
|
31
|
+
ariaLiveMessages,
|
|
32
|
+
getOptionLabel,
|
|
33
|
+
inputValue,
|
|
34
|
+
isMulti,
|
|
35
|
+
isOptionDisabled,
|
|
36
|
+
isSearchable,
|
|
37
|
+
label,
|
|
38
|
+
menuIsOpen,
|
|
39
|
+
options,
|
|
40
|
+
screenReaderStatus,
|
|
41
|
+
tabSelectsValue,
|
|
42
|
+
isLoading
|
|
43
|
+
} = selectProps;
|
|
44
|
+
const ariaLabel = selectProps['aria-label'] || label;
|
|
45
|
+
const ariaLive = selectProps['aria-live'];
|
|
46
|
+
|
|
47
|
+
// for safari, we will use minimum support from aria-live region
|
|
48
|
+
const isA11yImprovementEnabled = fg('design_system_select-a11y-improvement') && !isAppleDevice;
|
|
49
|
+
|
|
50
|
+
// Update aria live message configuration when prop changes
|
|
51
|
+
const messages = useMemo(() => ({
|
|
52
|
+
...defaultAriaLiveMessages,
|
|
53
|
+
...(ariaLiveMessages || {})
|
|
54
|
+
}), [ariaLiveMessages]);
|
|
55
|
+
|
|
56
|
+
// Update aria live selected option when prop changes
|
|
57
|
+
const ariaSelected = useMemo(() => {
|
|
58
|
+
let message = '';
|
|
59
|
+
if (isA11yImprovementEnabled && menuIsOpen) {
|
|
60
|
+
// we don't need to have selected message when the menu is open
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
if (ariaSelection && messages.onChange) {
|
|
64
|
+
const {
|
|
65
|
+
option,
|
|
66
|
+
options: selectedOptions,
|
|
67
|
+
removedValue,
|
|
68
|
+
removedValues,
|
|
69
|
+
value
|
|
70
|
+
} = ariaSelection;
|
|
71
|
+
// select-option when !isMulti does not return option so we assume selected option is value
|
|
72
|
+
const asOption = val => !Array.isArray(val) ? val : null;
|
|
73
|
+
|
|
74
|
+
// If there is just one item from the action then get its label
|
|
75
|
+
const selected = removedValue || option || asOption(value);
|
|
76
|
+
const label = selected ? getOptionLabel(selected) : '';
|
|
77
|
+
|
|
78
|
+
// If there are multiple items from the action then return an array of labels
|
|
79
|
+
const multiSelected = selectedOptions || removedValues || undefined;
|
|
80
|
+
const labels = multiSelected ? multiSelected.map(getOptionLabel) : [];
|
|
81
|
+
if (isA11yImprovementEnabled && !label && !labels.length) {
|
|
82
|
+
// return empty string if no labels provided
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
const onChangeProps = {
|
|
86
|
+
// multiSelected items are usually items that have already been selected
|
|
87
|
+
// or set by the user as a default value so we assume they are not disabled
|
|
88
|
+
isDisabled: selected && isOptionDisabled(selected, selectValue),
|
|
89
|
+
label,
|
|
90
|
+
labels,
|
|
91
|
+
...ariaSelection
|
|
92
|
+
};
|
|
93
|
+
message = messages.onChange(onChangeProps);
|
|
94
|
+
}
|
|
95
|
+
return message;
|
|
96
|
+
}, [ariaSelection, messages, isOptionDisabled, selectValue, getOptionLabel, isA11yImprovementEnabled, menuIsOpen]);
|
|
97
|
+
const prevInputValue = useRef('');
|
|
98
|
+
const ariaFocused = useMemo(() => {
|
|
99
|
+
let focusMsg = '';
|
|
100
|
+
const focused = focusedOption || focusedValue;
|
|
101
|
+
const isSelected = !!(focusedOption && selectValue && selectValue.includes(focusedOption));
|
|
102
|
+
if (inputValue === prevInputValue.current && isA11yImprovementEnabled) {
|
|
103
|
+
// only announce focus option when searching when ff is on and the input value changed
|
|
104
|
+
// for safari, we will announce for all
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
if (focused && messages.onFocus) {
|
|
108
|
+
const onFocusProps = {
|
|
109
|
+
focused,
|
|
110
|
+
label: getOptionLabel(focused),
|
|
111
|
+
isDisabled: isOptionDisabled(focused, selectValue),
|
|
112
|
+
isSelected,
|
|
113
|
+
options: focusableOptions,
|
|
114
|
+
context: focused === focusedOption ? 'menu' : 'value',
|
|
115
|
+
selectValue,
|
|
116
|
+
isMulti
|
|
117
|
+
};
|
|
118
|
+
focusMsg = messages.onFocus(onFocusProps);
|
|
119
|
+
}
|
|
120
|
+
prevInputValue.current = inputValue;
|
|
121
|
+
return focusMsg;
|
|
122
|
+
}, [inputValue, focusedOption, focusedValue, getOptionLabel, isOptionDisabled, messages, focusableOptions, selectValue, isA11yImprovementEnabled, isMulti]);
|
|
123
|
+
const ariaResults = useMemo(() => {
|
|
124
|
+
let resultsMsg = '';
|
|
125
|
+
if (menuIsOpen && options.length && !isLoading && messages.onFilter) {
|
|
126
|
+
const resultsMessage = screenReaderStatus({
|
|
127
|
+
count: focusableOptions.length
|
|
128
|
+
});
|
|
129
|
+
resultsMsg = messages.onFilter({
|
|
130
|
+
inputValue,
|
|
131
|
+
resultsMessage
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return resultsMsg;
|
|
135
|
+
}, [focusableOptions, inputValue, menuIsOpen, messages, options, screenReaderStatus, isLoading]);
|
|
136
|
+
const isInitialFocus = (ariaSelection === null || ariaSelection === void 0 ? void 0 : ariaSelection.action) === 'initial-input-focus';
|
|
137
|
+
const ariaGuidance = useMemo(() => {
|
|
138
|
+
if (fg('design_system_select-a11y-improvement')) {
|
|
139
|
+
// don't announce guidance at all when ff is on
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
let guidanceMsg = '';
|
|
143
|
+
if (messages.guidance) {
|
|
144
|
+
const context = focusedValue ? 'value' : menuIsOpen ? 'menu' : 'input';
|
|
145
|
+
guidanceMsg = messages.guidance({
|
|
146
|
+
'aria-label': ariaLabel,
|
|
147
|
+
context,
|
|
148
|
+
isDisabled: focusedOption && isOptionDisabled(focusedOption, selectValue),
|
|
149
|
+
isMulti,
|
|
150
|
+
isSearchable,
|
|
151
|
+
tabSelectsValue,
|
|
152
|
+
isInitialFocus
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return guidanceMsg;
|
|
156
|
+
}, [ariaLabel, focusedOption, focusedValue, isMulti, isOptionDisabled, isSearchable, menuIsOpen, messages, selectValue, tabSelectsValue, isInitialFocus]);
|
|
157
|
+
const ScreenReaderText = jsx(Fragment, null, jsx("span", {
|
|
158
|
+
id: "aria-selection"
|
|
159
|
+
}, ariaSelected), jsx("span", {
|
|
160
|
+
id: "aria-results"
|
|
161
|
+
}, ariaResults), !fg('design_system_select-a11y-improvement') && jsx(React.Fragment, null, jsx("span", {
|
|
162
|
+
id: "aria-focused"
|
|
163
|
+
}, ariaFocused), jsx("span", {
|
|
164
|
+
id: "aria-guidance"
|
|
165
|
+
}, ariaGuidance)));
|
|
166
|
+
return jsx(Fragment, null, jsx(A11yText, {
|
|
167
|
+
id: id
|
|
168
|
+
}, isInitialFocus && ScreenReaderText), jsx(A11yText, {
|
|
169
|
+
"aria-live": ariaLive // Should be undefined by default unless a specific use case requires it
|
|
170
|
+
,
|
|
171
|
+
"aria-atomic": fg('design_system_select-a11y-improvement') ? undefined : 'false',
|
|
172
|
+
"aria-relevant": fg('design_system_select-a11y-improvement') ? undefined : 'additions text',
|
|
173
|
+
role: fg('design_system_select-a11y-improvement') ? 'status' : 'log'
|
|
174
|
+
}, isFocused && !isInitialFocus && ScreenReaderText));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// eslint-disable-next-line @repo/internal/react/require-jsdoc
|
|
178
|
+
export default LiveRegion;
|