@telus-uds/components-base 1.59.2 → 1.61.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 +26 -2
- package/component-docs.json +526 -76
- package/lib/Autocomplete/Autocomplete.js +483 -0
- package/lib/Autocomplete/Loading.js +51 -0
- package/lib/Autocomplete/Suggestions.js +85 -0
- package/lib/Autocomplete/constants.js +14 -0
- package/lib/Autocomplete/dictionary.js +19 -0
- package/lib/Autocomplete/index.js +13 -0
- package/lib/Button/ButtonLink.js +7 -3
- package/lib/ExpandCollapse/Panel.js +7 -0
- package/lib/IconButton/IconButton.js +8 -0
- package/lib/Link/ChevronLink.js +9 -2
- package/lib/Link/LinkBase.js +14 -0
- package/lib/Link/TextButton.js +12 -1
- package/lib/Listbox/GroupControl.js +121 -0
- package/lib/Listbox/Listbox.js +198 -0
- package/lib/Listbox/ListboxGroup.js +142 -0
- package/lib/Listbox/ListboxItem.js +97 -0
- package/lib/Listbox/ListboxOverlay.js +106 -0
- package/lib/Listbox/PressableItem.js +0 -2
- package/lib/Listbox/index.js +5 -24
- package/lib/Pagination/dictionary.js +3 -3
- package/lib/Progress/ProgressBarBackground.js +2 -2
- package/lib/SideNav/Item.js +15 -5
- package/lib/Tags/Tags.js +6 -1
- package/lib/TextInput/TextInputBase.js +2 -0
- package/lib/Tooltip/Tooltip.js +6 -1
- package/lib/Tooltip/Tooltip.native.js +6 -1
- package/lib/Tooltip/shared.js +5 -0
- package/lib/index.js +17 -13
- package/lib/utils/useOverlaidPosition.js +6 -4
- package/lib-module/Autocomplete/Autocomplete.js +448 -0
- package/lib-module/Autocomplete/Loading.js +36 -0
- package/lib-module/Autocomplete/Suggestions.js +66 -0
- package/lib-module/Autocomplete/constants.js +4 -0
- package/lib-module/Autocomplete/dictionary.js +12 -0
- package/lib-module/Autocomplete/index.js +2 -0
- package/lib-module/Button/ButtonLink.js +4 -1
- package/lib-module/ExpandCollapse/Panel.js +7 -0
- package/lib-module/IconButton/IconButton.js +8 -0
- package/lib-module/Link/ChevronLink.js +10 -3
- package/lib-module/Link/LinkBase.js +14 -0
- package/lib-module/Link/TextButton.js +11 -1
- package/lib-module/Listbox/GroupControl.js +102 -0
- package/lib-module/Listbox/Listbox.js +172 -0
- package/lib-module/Listbox/ListboxGroup.js +117 -0
- package/lib-module/Listbox/ListboxItem.js +71 -0
- package/lib-module/Listbox/ListboxOverlay.js +80 -0
- package/lib-module/Listbox/PressableItem.js +0 -2
- package/lib-module/Listbox/index.js +2 -2
- package/lib-module/Pagination/dictionary.js +3 -3
- package/lib-module/Progress/ProgressBarBackground.js +2 -2
- package/lib-module/SideNav/Item.js +15 -5
- package/lib-module/Tags/Tags.js +6 -1
- package/lib-module/TextInput/TextInputBase.js +2 -0
- package/lib-module/Tooltip/Tooltip.js +6 -1
- package/lib-module/Tooltip/Tooltip.native.js +6 -1
- package/lib-module/Tooltip/shared.js +5 -0
- package/lib-module/index.js +2 -1
- package/lib-module/utils/useOverlaidPosition.js +5 -4
- package/package.json +5 -3
- package/src/Autocomplete/Autocomplete.jsx +411 -0
- package/src/Autocomplete/Loading.jsx +18 -0
- package/src/Autocomplete/Suggestions.jsx +54 -0
- package/src/Autocomplete/constants.js +4 -0
- package/src/Autocomplete/dictionary.js +12 -0
- package/src/Autocomplete/index.js +3 -0
- package/src/Button/ButtonLink.jsx +4 -1
- package/src/ExpandCollapse/Panel.jsx +11 -1
- package/src/IconButton/IconButton.jsx +7 -0
- package/src/Link/ChevronLink.jsx +10 -3
- package/src/Link/LinkBase.jsx +11 -0
- package/src/Link/TextButton.jsx +8 -2
- package/src/Listbox/GroupControl.jsx +93 -0
- package/src/Listbox/Listbox.jsx +165 -0
- package/src/Listbox/ListboxGroup.jsx +120 -0
- package/src/Listbox/ListboxItem.jsx +76 -0
- package/src/Listbox/ListboxOverlay.jsx +82 -0
- package/src/Listbox/PressableItem.jsx +0 -2
- package/src/Listbox/index.js +3 -2
- package/src/Pagination/dictionary.js +3 -3
- package/src/Progress/ProgressBarBackground.jsx +2 -2
- package/src/SideNav/Item.jsx +13 -5
- package/src/Tags/Tags.jsx +5 -1
- package/src/TextInput/TextInputBase.jsx +2 -0
- package/src/Tooltip/Tooltip.jsx +16 -2
- package/src/Tooltip/Tooltip.native.jsx +15 -2
- package/src/Tooltip/shared.js +4 -0
- package/src/index.js +2 -1
- package/src/utils/useOverlaidPosition.js +6 -5
|
@@ -107,6 +107,7 @@ const IconButton = forwardRef(
|
|
|
107
107
|
icon: IconComponent,
|
|
108
108
|
href,
|
|
109
109
|
hrefAttrs,
|
|
110
|
+
testID,
|
|
110
111
|
accessibilityRole = href ? 'link' : 'button',
|
|
111
112
|
...rawRest
|
|
112
113
|
},
|
|
@@ -133,6 +134,7 @@ const IconButton = forwardRef(
|
|
|
133
134
|
hrefAttrs={hrefAttrs}
|
|
134
135
|
style={getOuterStyle}
|
|
135
136
|
{...selectedProps}
|
|
137
|
+
testID={testID}
|
|
136
138
|
>
|
|
137
139
|
{(pressableState) => {
|
|
138
140
|
const themeTokens = getTokens(resolvePressableState(pressableState))
|
|
@@ -157,6 +159,11 @@ IconButton.propTypes = {
|
|
|
157
159
|
...selectedSystemPropTypes,
|
|
158
160
|
variant: variantProp.propType,
|
|
159
161
|
tokens: getTokensPropType('IconButton'),
|
|
162
|
+
/**
|
|
163
|
+
* A unique identifier for testing purposes.
|
|
164
|
+
* Will be added as a `data-testid` attribute.
|
|
165
|
+
*/
|
|
166
|
+
testID: PropTypes.string,
|
|
160
167
|
/**
|
|
161
168
|
* Defines the icon to be rendered
|
|
162
169
|
*/
|
package/src/Link/ChevronLink.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react'
|
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
|
|
4
4
|
import { useThemeTokensCallback } from '../ThemeProvider'
|
|
5
|
-
import { selectTokens, getTokensPropType } from '../utils'
|
|
5
|
+
import { selectTokens, getTokensPropType, linkProps } from '../utils'
|
|
6
6
|
import LinkBase from './LinkBase'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,7 +12,7 @@ import LinkBase from './LinkBase'
|
|
|
12
12
|
* ChevronLink is not intended to be deeply themable; variants passed to ChevronLink are forwarded to Link.
|
|
13
13
|
*/
|
|
14
14
|
const ChevronLink = forwardRef(
|
|
15
|
-
({ direction = 'right', children, tokens = {}, variant, dataSet, ...
|
|
15
|
+
({ direction = 'right', children, tokens = {}, variant, dataSet, ...otherlinkProps }, ref) => {
|
|
16
16
|
const getChevronTokens = useThemeTokensCallback('ChevronLink', tokens, variant)
|
|
17
17
|
const applyChevronTokens = (linkState) => {
|
|
18
18
|
const { leftIcon, rightIcon, iconDisplace, height, fontSize, ...otherTokens } =
|
|
@@ -32,7 +32,7 @@ const ChevronLink = forwardRef(
|
|
|
32
32
|
const getTokens = useThemeTokensCallback('Link', applyChevronTokens, variant)
|
|
33
33
|
return (
|
|
34
34
|
<LinkBase
|
|
35
|
-
{...
|
|
35
|
+
{...otherlinkProps}
|
|
36
36
|
iconPosition={direction}
|
|
37
37
|
tokens={getTokens}
|
|
38
38
|
dataSet={dataSet}
|
|
@@ -47,6 +47,13 @@ const ChevronLink = forwardRef(
|
|
|
47
47
|
ChevronLink.displayName = 'ChevronLink'
|
|
48
48
|
ChevronLink.propTypes = {
|
|
49
49
|
...LinkBase.propTypes,
|
|
50
|
+
children: PropTypes.node,
|
|
51
|
+
variant: PropTypes.exact({
|
|
52
|
+
size: PropTypes.oneOf(['large', 'small', 'micro']),
|
|
53
|
+
alternative: PropTypes.bool,
|
|
54
|
+
inverse: PropTypes.bool
|
|
55
|
+
}),
|
|
56
|
+
...linkProps.types,
|
|
50
57
|
tokens: getTokensPropType('ChevronLink', 'Link'),
|
|
51
58
|
/**
|
|
52
59
|
* Changes direction of chevron icon
|
package/src/Link/LinkBase.jsx
CHANGED
|
@@ -197,6 +197,17 @@ LinkBase.displayName = 'LinkBase'
|
|
|
197
197
|
LinkBase.propTypes = {
|
|
198
198
|
...selectedSystemPropTypes,
|
|
199
199
|
tokens: getTokensPropType('Link'),
|
|
200
|
+
/**
|
|
201
|
+
* href for the Link
|
|
202
|
+
*/
|
|
203
|
+
href: PropTypes.string,
|
|
204
|
+
/**
|
|
205
|
+
* AccesibilityRole for the link base
|
|
206
|
+
*/
|
|
207
|
+
accessibilityrole: PropTypes.string,
|
|
208
|
+
/**
|
|
209
|
+
* Children nodes that can be added
|
|
210
|
+
*/
|
|
200
211
|
variant: variantProp.propType,
|
|
201
212
|
/**
|
|
202
213
|
* Optional variant that may be passed down to the link's icon if there is one
|
package/src/Link/TextButton.jsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { forwardRef } from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
|
|
4
3
|
import { useThemeTokensCallback } from '../ThemeProvider'
|
|
5
4
|
import LinkBase from './LinkBase'
|
|
5
|
+
import { variantProp } from '../utils'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* `TextButton` is a button that looks like a Link. It uses the same theming and variants as
|
|
@@ -29,7 +29,13 @@ TextButton.displayName = 'TextButton'
|
|
|
29
29
|
|
|
30
30
|
TextButton.propTypes = {
|
|
31
31
|
...LinkBase.propTypes,
|
|
32
|
-
onPress
|
|
32
|
+
/** onPress function */
|
|
33
|
+
onPress: PropTypes.func.isRequired,
|
|
34
|
+
/** Children node that can be added */
|
|
35
|
+
children: PropTypes.node.isRequired,
|
|
36
|
+
variant: variantProp.propType,
|
|
37
|
+
/** Accesibility role for TextButton */
|
|
38
|
+
accessibilityRole: PropTypes.string
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export default TextButton
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { View, StyleSheet, Text } from 'react-native'
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import Icon from '../Icon'
|
|
6
|
+
import Spacer from '../Spacer'
|
|
7
|
+
import { useListboxContext } from './ListboxContext'
|
|
8
|
+
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
controlWrapper: {
|
|
11
|
+
width: '100%',
|
|
12
|
+
flex: 1,
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
justifyContent: 'space-between',
|
|
16
|
+
boxSizing: 'border-box'
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const GroupControl = ({ expanded, pressed, hover, focus, label, id }) => {
|
|
21
|
+
const { selectedId, setSelectedId } = useListboxContext()
|
|
22
|
+
const tokens = useThemeTokens(
|
|
23
|
+
'Listbox',
|
|
24
|
+
{},
|
|
25
|
+
{},
|
|
26
|
+
{
|
|
27
|
+
expanded,
|
|
28
|
+
pressed,
|
|
29
|
+
hover,
|
|
30
|
+
current: selectedId === id && id !== undefined,
|
|
31
|
+
focus
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
const {
|
|
35
|
+
groupFontName,
|
|
36
|
+
groupFontWeight,
|
|
37
|
+
groupFontSize,
|
|
38
|
+
groupColor,
|
|
39
|
+
groupBackgroundColor,
|
|
40
|
+
groupBorderColor,
|
|
41
|
+
groupBorderWidth,
|
|
42
|
+
groupBorderRadius,
|
|
43
|
+
groupPaddingLeft,
|
|
44
|
+
groupPaddingRight,
|
|
45
|
+
groupPaddingTop,
|
|
46
|
+
groupPaddingBottom,
|
|
47
|
+
itemTextDecoration,
|
|
48
|
+
itemOutline,
|
|
49
|
+
groupHeight
|
|
50
|
+
} = tokens
|
|
51
|
+
return (
|
|
52
|
+
<View
|
|
53
|
+
onPress={() => setSelectedId(id)}
|
|
54
|
+
style={[
|
|
55
|
+
styles.controlWrapper,
|
|
56
|
+
{
|
|
57
|
+
fontFamily: `${groupFontName}${groupFontWeight}normal`,
|
|
58
|
+
fontSize: groupFontSize,
|
|
59
|
+
color: groupColor,
|
|
60
|
+
textDecoration: itemTextDecoration,
|
|
61
|
+
backgroundColor: groupBackgroundColor,
|
|
62
|
+
outline: itemOutline,
|
|
63
|
+
height: groupHeight,
|
|
64
|
+
border: `${groupBorderWidth}px solid ${groupBorderColor}`,
|
|
65
|
+
borderRadius: groupBorderRadius,
|
|
66
|
+
paddingLeft: groupPaddingLeft - groupBorderWidth,
|
|
67
|
+
paddingRight: groupPaddingRight - groupBorderWidth,
|
|
68
|
+
paddingTop: groupPaddingTop - groupBorderWidth,
|
|
69
|
+
paddingBottom: groupPaddingBottom - groupBorderWidth
|
|
70
|
+
}
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<Text>{label}</Text>
|
|
74
|
+
<Spacer space={1} direction="row" />
|
|
75
|
+
<Icon
|
|
76
|
+
icon={tokens.groupIcon}
|
|
77
|
+
tokens={{ color: tokens.groupColor }}
|
|
78
|
+
variant={{ size: 'micro' }}
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
GroupControl.propTypes = {
|
|
85
|
+
id: PropTypes.string,
|
|
86
|
+
expanded: PropTypes.bool,
|
|
87
|
+
pressed: PropTypes.bool,
|
|
88
|
+
hover: PropTypes.bool,
|
|
89
|
+
focus: PropTypes.bool,
|
|
90
|
+
label: PropTypes.string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default GroupControl
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { View, StyleSheet, Platform } from 'react-native'
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import { withLinkRouter, getTokensPropType } from '../utils'
|
|
6
|
+
import ExpandCollapse from '../ExpandCollapse'
|
|
7
|
+
import ListboxGroup from './ListboxGroup'
|
|
8
|
+
import ListboxItem from './ListboxItem'
|
|
9
|
+
import { ListboxContext } from './ListboxContext'
|
|
10
|
+
import DropdownOverlay from './ListboxOverlay'
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
list: {
|
|
14
|
+
padding: 0,
|
|
15
|
+
margin: 0
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const getInitialOpen = (items, selectedId) =>
|
|
20
|
+
items
|
|
21
|
+
.filter(
|
|
22
|
+
(item) =>
|
|
23
|
+
item.items &&
|
|
24
|
+
item.items.some((nestedItem) => (nestedItem.id ?? nestedItem.label) === selectedId)
|
|
25
|
+
)
|
|
26
|
+
.map((item) => item.id ?? item.label)
|
|
27
|
+
|
|
28
|
+
const Listbox = ({
|
|
29
|
+
items = [],
|
|
30
|
+
firstItemRef = null, // focus will be moved to this one once within the menu
|
|
31
|
+
parentRef = null, // to return focus to after leaving the last menu item
|
|
32
|
+
selectedId: defaultSelectedId,
|
|
33
|
+
LinkRouter,
|
|
34
|
+
itemRouterProps,
|
|
35
|
+
onClose,
|
|
36
|
+
variant,
|
|
37
|
+
tokens
|
|
38
|
+
}) => {
|
|
39
|
+
const initialOpen = getInitialOpen(items, defaultSelectedId)
|
|
40
|
+
|
|
41
|
+
const [selectedId, setSelectedId] = useState(defaultSelectedId)
|
|
42
|
+
|
|
43
|
+
const { minHeight, minWidth } = useThemeTokens('Listbox', variant, tokens)
|
|
44
|
+
|
|
45
|
+
// We need to keep track of each item's ref in order to be able to
|
|
46
|
+
// focus on a specific item via keyboard navigation
|
|
47
|
+
const itemRefs = useRef([])
|
|
48
|
+
if (firstItemRef?.current) itemRefs.current[0] = firstItemRef.current
|
|
49
|
+
const [focusedIndex, setFocusedIndex] = useState(0)
|
|
50
|
+
const handleKeydown = useCallback(
|
|
51
|
+
(event) => {
|
|
52
|
+
const nextItemRef = itemRefs.current[focusedIndex + 1]
|
|
53
|
+
const prevItemRef = itemRefs.current[focusedIndex - 1]
|
|
54
|
+
if (event.key === 'ArrowUp' || (event.shiftKey && event.key === 'Tab')) {
|
|
55
|
+
// Move the focus to the previous item or to the parent one if on the first
|
|
56
|
+
if (prevItemRef) {
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
prevItemRef.focus()
|
|
59
|
+
} else if (parentRef) parentRef.current?.focus()
|
|
60
|
+
setFocusedIndex(focusedIndex - 1)
|
|
61
|
+
} else if ((event.key === 'ArrowDown' || event.key === 'Tab') && nextItemRef) {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
setFocusedIndex(focusedIndex + 1)
|
|
64
|
+
nextItemRef.focus()
|
|
65
|
+
} else if (event.key === 'Escape') {
|
|
66
|
+
// Close the dropdown
|
|
67
|
+
parentRef?.current?.click()
|
|
68
|
+
// Return focus to the dropdown control after leaving the last item
|
|
69
|
+
parentRef?.current?.focus()
|
|
70
|
+
if (onClose) onClose(event)
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[focusedIndex, onClose, parentRef]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Add listeners for mouse clicks outside and for key presses
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (Platform.OS === 'web') {
|
|
79
|
+
window.addEventListener('click', onClose)
|
|
80
|
+
window.addEventListener('keydown', handleKeydown)
|
|
81
|
+
window.addEventListener('touchstart', onClose)
|
|
82
|
+
return () => {
|
|
83
|
+
window.removeEventListener('click', onClose)
|
|
84
|
+
window.removeEventListener('keydown', handleKeydown)
|
|
85
|
+
window.removeEventListener('touchstart', onClose)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return () => {}
|
|
89
|
+
}, [onClose, handleKeydown])
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ListboxContext.Provider value={{ selectedId, setSelectedId }}>
|
|
93
|
+
<ExpandCollapse initialOpen={initialOpen} maxOpen={1}>
|
|
94
|
+
{(expandProps) => (
|
|
95
|
+
<View style={[styles.list, { minHeight, minWidth }]} role="listbox">
|
|
96
|
+
{items.map((item, index) => {
|
|
97
|
+
const { id, label, items: nestedItems } = item
|
|
98
|
+
const itemId = id ?? label
|
|
99
|
+
|
|
100
|
+
// Give the list of refs.
|
|
101
|
+
const itemRef = (ref) => {
|
|
102
|
+
itemRefs.current[index] = ref
|
|
103
|
+
return ref
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return nestedItems ? (
|
|
107
|
+
<ListboxGroup
|
|
108
|
+
{...item}
|
|
109
|
+
expandProps={expandProps}
|
|
110
|
+
LinkRouter={LinkRouter}
|
|
111
|
+
itemRouterProps={itemRouterProps}
|
|
112
|
+
prevItemRef={itemRefs.current[index - 1] ?? null}
|
|
113
|
+
nextItemRef={itemRefs.current[index + 1] ?? null}
|
|
114
|
+
ref={index === 0 ? firstItemRef : itemRef}
|
|
115
|
+
key={itemId}
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<ListboxItem
|
|
119
|
+
{...item}
|
|
120
|
+
key={itemId}
|
|
121
|
+
id={itemId}
|
|
122
|
+
LinkRouter={LinkRouter}
|
|
123
|
+
itemRouterProps={itemRouterProps}
|
|
124
|
+
prevItemRef={itemRefs.current[index - 1] ?? null}
|
|
125
|
+
nextItemRef={itemRefs.current[index + 1] ?? null}
|
|
126
|
+
ref={index === 0 ? firstItemRef : itemRef}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
})}
|
|
130
|
+
</View>
|
|
131
|
+
)}
|
|
132
|
+
</ExpandCollapse>
|
|
133
|
+
</ListboxContext.Provider>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Listbox.propTypes = {
|
|
138
|
+
...withLinkRouter.propTypes,
|
|
139
|
+
/**
|
|
140
|
+
* Focus will be moved to the item with this ref once within the menu.
|
|
141
|
+
*/
|
|
142
|
+
firstItemRef: PropTypes.object,
|
|
143
|
+
/**
|
|
144
|
+
* Focus will be returned to the dropdown control with this ref after leaving
|
|
145
|
+
* the last menu item.
|
|
146
|
+
*/
|
|
147
|
+
parentRef: PropTypes.object,
|
|
148
|
+
/**
|
|
149
|
+
* `Listbox` items
|
|
150
|
+
*/
|
|
151
|
+
items: PropTypes.array,
|
|
152
|
+
/**
|
|
153
|
+
* To select an item by default
|
|
154
|
+
*/
|
|
155
|
+
selectedId: PropTypes.string,
|
|
156
|
+
/**
|
|
157
|
+
* onClose event
|
|
158
|
+
*/
|
|
159
|
+
onClose: PropTypes.func,
|
|
160
|
+
tokens: getTokensPropType('Listbox')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Listbox.Overlay = DropdownOverlay
|
|
164
|
+
|
|
165
|
+
export default Listbox
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* eslint-disable react-native-a11y/has-valid-accessibility-role */
|
|
2
|
+
import React, { forwardRef } from 'react'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import { View, StyleSheet } from 'react-native'
|
|
5
|
+
import { withLinkRouter } from '../utils'
|
|
6
|
+
import ExpandCollapse from '../ExpandCollapse'
|
|
7
|
+
import ListboxItem from './ListboxItem'
|
|
8
|
+
import { useListboxContext } from './ListboxContext'
|
|
9
|
+
import GroupControl from './GroupControl'
|
|
10
|
+
|
|
11
|
+
const styles = StyleSheet.create({
|
|
12
|
+
groupWrapper: {
|
|
13
|
+
margin: 0,
|
|
14
|
+
padding: 0,
|
|
15
|
+
overflow: 'hidden'
|
|
16
|
+
},
|
|
17
|
+
list: {
|
|
18
|
+
margin: 0,
|
|
19
|
+
padding: 0
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const ListboxGroup = forwardRef(
|
|
24
|
+
(
|
|
25
|
+
{
|
|
26
|
+
id,
|
|
27
|
+
label,
|
|
28
|
+
items,
|
|
29
|
+
LinkRouter,
|
|
30
|
+
linkRouterProps,
|
|
31
|
+
expandProps,
|
|
32
|
+
onLastItemBlur,
|
|
33
|
+
nextItemRef,
|
|
34
|
+
prevItemRef
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
) => {
|
|
38
|
+
const { selectedId } = useListboxContext()
|
|
39
|
+
|
|
40
|
+
// TODO: implement keyboard navigation via refs for grouped items separately here
|
|
41
|
+
return (
|
|
42
|
+
<View id="test" style={styles.groupWrapper} accessibilityRole="listitem">
|
|
43
|
+
<ExpandCollapse.Panel
|
|
44
|
+
panelId={id ?? label}
|
|
45
|
+
controlTokens={{
|
|
46
|
+
icon: null,
|
|
47
|
+
paddingLeft: 0,
|
|
48
|
+
paddingRight: 0,
|
|
49
|
+
paddingTop: 0,
|
|
50
|
+
paddingBottom: 0,
|
|
51
|
+
backgroundColor: 'transparent',
|
|
52
|
+
borderColor: 'transparent',
|
|
53
|
+
textLine: 'none',
|
|
54
|
+
borderWidth: 0
|
|
55
|
+
}}
|
|
56
|
+
// TODO refactor
|
|
57
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
58
|
+
control={(controlProps) => (
|
|
59
|
+
<GroupControl id={id ?? label} {...controlProps} label={label} />
|
|
60
|
+
)}
|
|
61
|
+
{...expandProps}
|
|
62
|
+
tokens={{
|
|
63
|
+
contentPaddingLeft: 0,
|
|
64
|
+
contentPaddingRight: 0,
|
|
65
|
+
contentPaddingTop: 0,
|
|
66
|
+
contentPaddingBottom: 0,
|
|
67
|
+
borderColor: 'transparent',
|
|
68
|
+
borderRadius: 0,
|
|
69
|
+
borderWidth: 0,
|
|
70
|
+
marginBottom: 0
|
|
71
|
+
}}
|
|
72
|
+
controlRef={ref}
|
|
73
|
+
>
|
|
74
|
+
<View style={styles.list}>
|
|
75
|
+
{items.map((item, index) => {
|
|
76
|
+
return (
|
|
77
|
+
<ListboxItem
|
|
78
|
+
key={item.label}
|
|
79
|
+
id={item.id ?? item.label}
|
|
80
|
+
{...item}
|
|
81
|
+
selected={
|
|
82
|
+
(item.id && item.id === selectedId) || (item.label && item.label === selectedId)
|
|
83
|
+
}
|
|
84
|
+
isChild
|
|
85
|
+
LinkRouter={LinkRouter}
|
|
86
|
+
linkRouterProps={linkRouterProps}
|
|
87
|
+
{...(index === 0 && { prevItemRef })}
|
|
88
|
+
{...(index === items.length - 1 && { nextItemRef })}
|
|
89
|
+
{...(index === items.length - 1 && { onBlur: onLastItemBlur })}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
})}
|
|
93
|
+
</View>
|
|
94
|
+
</ExpandCollapse.Panel>
|
|
95
|
+
</View>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
ListboxGroup.displayName = 'ListboxGroup'
|
|
100
|
+
|
|
101
|
+
ListboxGroup.propTypes = {
|
|
102
|
+
...withLinkRouter.propTypes,
|
|
103
|
+
label: PropTypes.string,
|
|
104
|
+
items: PropTypes.arrayOf(
|
|
105
|
+
PropTypes.shape({
|
|
106
|
+
href: PropTypes.string,
|
|
107
|
+
label: PropTypes.string,
|
|
108
|
+
current: PropTypes.bool
|
|
109
|
+
})
|
|
110
|
+
),
|
|
111
|
+
expandProps: PropTypes.object,
|
|
112
|
+
nextItemRef: PropTypes.object,
|
|
113
|
+
prevItemRef: PropTypes.object,
|
|
114
|
+
/**
|
|
115
|
+
* Use this callback to redirect the focus after it leaves the last item of the group.
|
|
116
|
+
*/
|
|
117
|
+
onLastItemBlur: PropTypes.func
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default ListboxGroup
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* eslint-disable react/require-default-props */
|
|
2
|
+
import React, { forwardRef } from 'react'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import { View, StyleSheet } from 'react-native'
|
|
5
|
+
import { selectSystemProps, withLinkRouter, htmlAttrs } from '../utils'
|
|
6
|
+
import PressableItem from './PressableItem'
|
|
7
|
+
import { useThemeTokensCallback } from '../ThemeProvider'
|
|
8
|
+
|
|
9
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
10
|
+
|
|
11
|
+
const styles = StyleSheet.create({
|
|
12
|
+
itemContainer: {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
margin: 0
|
|
15
|
+
},
|
|
16
|
+
childContainer: {
|
|
17
|
+
paddingLeft: 16
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const ListboxItem = forwardRef(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
href,
|
|
25
|
+
label,
|
|
26
|
+
isChild = false,
|
|
27
|
+
onBlur,
|
|
28
|
+
nextItemRef,
|
|
29
|
+
prevItemRef,
|
|
30
|
+
tokens,
|
|
31
|
+
variant = {},
|
|
32
|
+
LinkRouter,
|
|
33
|
+
linkRouterProps,
|
|
34
|
+
id,
|
|
35
|
+
onPress = () => {},
|
|
36
|
+
...rest
|
|
37
|
+
},
|
|
38
|
+
ref
|
|
39
|
+
) => {
|
|
40
|
+
const selectedProps = selectProps({ href, ...rest })
|
|
41
|
+
|
|
42
|
+
const getTokens = useThemeTokensCallback('Listbox', tokens, variant, { isChild })
|
|
43
|
+
return (
|
|
44
|
+
<View style={[styles.itemContainer, isChild && styles.childContainer]} role="option">
|
|
45
|
+
<PressableItem
|
|
46
|
+
href={href}
|
|
47
|
+
isChild={isChild}
|
|
48
|
+
onPress={onPress}
|
|
49
|
+
onBlur={onBlur}
|
|
50
|
+
nextItemRef={nextItemRef}
|
|
51
|
+
prevItemRef={prevItemRef}
|
|
52
|
+
ref={ref}
|
|
53
|
+
tokens={getTokens}
|
|
54
|
+
selectedProps={selectedProps}
|
|
55
|
+
id={id}
|
|
56
|
+
>
|
|
57
|
+
{label}
|
|
58
|
+
</PressableItem>
|
|
59
|
+
</View>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
ListboxItem.displayName = 'ListboxItem'
|
|
64
|
+
|
|
65
|
+
ListboxItem.propTypes = {
|
|
66
|
+
...selectedSystemPropTypes,
|
|
67
|
+
...withLinkRouter.propTypes,
|
|
68
|
+
href: PropTypes.string,
|
|
69
|
+
isChild: PropTypes.bool,
|
|
70
|
+
label: PropTypes.node.isRequired,
|
|
71
|
+
nextItemRef: PropTypes.object,
|
|
72
|
+
prevItemRef: PropTypes.object,
|
|
73
|
+
onPress: PropTypes.func
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default withLinkRouter(ListboxItem)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable react/require-default-props */
|
|
2
|
+
import React, { forwardRef } from 'react'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import { View, StyleSheet, Platform } from 'react-native'
|
|
5
|
+
import { Portal } from '@gorhom/portal'
|
|
6
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
7
|
+
import Card from '../Card'
|
|
8
|
+
|
|
9
|
+
const staticStyles = StyleSheet.create({
|
|
10
|
+
positioner: {
|
|
11
|
+
flex: 1, // Grow to maxWidth when possible, shrink when not possible
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
zIndex: 1000000000000000 // Position on top of all the other overlays, including backdrops and modals
|
|
14
|
+
},
|
|
15
|
+
hidden: {
|
|
16
|
+
// Use opacity not visibility to hide the dropdown during positioning
|
|
17
|
+
// so on web, children may be focused from the first render
|
|
18
|
+
opacity: 0
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const paddingVertical = 0
|
|
23
|
+
const paddingHorizontal = 0
|
|
24
|
+
|
|
25
|
+
const DropdownOverlay = forwardRef(
|
|
26
|
+
({ children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout }, ref) => {
|
|
27
|
+
const systemTokens = useThemeTokens('Listbox', {}, {})
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View
|
|
31
|
+
ref={ref}
|
|
32
|
+
onLayout={onLayout}
|
|
33
|
+
style={[
|
|
34
|
+
overlaidPosition,
|
|
35
|
+
{ maxWidth, minWidth },
|
|
36
|
+
staticStyles.positioner,
|
|
37
|
+
!isReady && staticStyles.hidden
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
<Card
|
|
41
|
+
tokens={{
|
|
42
|
+
shadow: systemTokens.shadow,
|
|
43
|
+
paddingBottom: paddingVertical,
|
|
44
|
+
paddingTop: paddingVertical,
|
|
45
|
+
paddingLeft: paddingHorizontal,
|
|
46
|
+
paddingRight: paddingHorizontal
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</Card>
|
|
51
|
+
</View>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const withPortal = (Overlay) => {
|
|
57
|
+
// eslint-disable-next-line react/display-name, react/no-multi-comp
|
|
58
|
+
return (props) => {
|
|
59
|
+
return (
|
|
60
|
+
<Portal>
|
|
61
|
+
<Overlay {...props} />
|
|
62
|
+
</Portal>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
DropdownOverlay.displayName = 'DropdownOverlay'
|
|
68
|
+
|
|
69
|
+
DropdownOverlay.propTypes = {
|
|
70
|
+
children: PropTypes.node.isRequired,
|
|
71
|
+
isReady: PropTypes.bool,
|
|
72
|
+
overlaidPosition: PropTypes.shape({
|
|
73
|
+
top: PropTypes.number,
|
|
74
|
+
left: PropTypes.number,
|
|
75
|
+
width: PropTypes.number
|
|
76
|
+
}),
|
|
77
|
+
maxWidth: PropTypes.number,
|
|
78
|
+
minWidth: PropTypes.number,
|
|
79
|
+
onLayout: PropTypes.func
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay
|
|
@@ -18,7 +18,6 @@ const getItemStyles = ({
|
|
|
18
18
|
itemPaddingRight,
|
|
19
19
|
itemBackgroundColor,
|
|
20
20
|
itemColor,
|
|
21
|
-
itemDisplay,
|
|
22
21
|
itemOutline,
|
|
23
22
|
itemTextDecoration,
|
|
24
23
|
itemBorderLeftColor,
|
|
@@ -39,7 +38,6 @@ const getItemStyles = ({
|
|
|
39
38
|
width: '100%',
|
|
40
39
|
backgroundColor: itemBackgroundColor,
|
|
41
40
|
color: itemColor,
|
|
42
|
-
display: itemDisplay,
|
|
43
41
|
outline: itemOutline,
|
|
44
42
|
textDecoration: itemTextDecoration,
|
|
45
43
|
borderLeft: `${itemBorderLeftWidth}px solid ${itemBorderLeftColor}`,
|
package/src/Listbox/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import Listbox from './Listbox'
|
|
2
|
+
|
|
3
|
+
export default Listbox
|
|
@@ -8,11 +8,11 @@ export default {
|
|
|
8
8
|
nextText: 'Next'
|
|
9
9
|
},
|
|
10
10
|
fr: {
|
|
11
|
-
goToLabel: 'Aller
|
|
11
|
+
goToLabel: 'Aller à la page n°',
|
|
12
12
|
currentLabel: '(page actuelle)',
|
|
13
|
-
previousLabel: 'Aller
|
|
13
|
+
previousLabel: 'Aller à la page précédente',
|
|
14
14
|
previousText: 'Précédent',
|
|
15
|
-
nextLabel: 'Aller
|
|
15
|
+
nextLabel: 'Aller à la page suivante',
|
|
16
16
|
nextText: 'Suivant'
|
|
17
17
|
}
|
|
18
18
|
}
|