@telus-uds/components-base 1.16.0 → 1.18.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 +32 -2
- package/component-docs.json +708 -120
- package/lib/BaseProvider/HydrationContext.js +74 -0
- package/lib/BaseProvider/index.js +14 -6
- package/lib/Button/ButtonBase.js +2 -1
- package/lib/List/List.js +11 -8
- package/lib/List/PressableListItemBase.js +5 -9
- package/lib/QuickLinks/QuickLinks.js +91 -0
- package/lib/QuickLinks/QuickLinksCard.js +47 -0
- package/lib/QuickLinks/QuickLinksItem.js +73 -0
- package/lib/QuickLinks/index.js +16 -0
- package/lib/StackView/StackWrap.js +16 -12
- package/lib/Timeline/Timeline.js +193 -0
- package/lib/Timeline/index.js +13 -0
- package/lib/ViewportProvider/useViewportListener.js +5 -18
- package/lib/index.js +28 -1
- package/lib/utils/animation/useVerticalExpandAnimation.js +3 -1
- package/lib/utils/index.js +9 -0
- package/lib/utils/useSafeLayoutEffect.js +40 -0
- package/lib-module/BaseProvider/HydrationContext.js +51 -0
- package/lib-module/BaseProvider/index.js +12 -6
- package/lib-module/Button/ButtonBase.js +2 -1
- package/lib-module/List/List.js +12 -8
- package/lib-module/List/PressableListItemBase.js +6 -10
- package/lib-module/QuickLinks/QuickLinks.js +71 -0
- package/lib-module/QuickLinks/QuickLinksCard.js +33 -0
- package/lib-module/QuickLinks/QuickLinksItem.js +50 -0
- package/lib-module/QuickLinks/index.js +4 -0
- package/lib-module/StackView/StackWrap.js +16 -13
- package/lib-module/Timeline/Timeline.js +174 -0
- package/lib-module/Timeline/index.js +2 -0
- package/lib-module/ViewportProvider/useViewportListener.js +5 -18
- package/lib-module/index.js +4 -1
- package/lib-module/utils/animation/useVerticalExpandAnimation.js +4 -3
- package/lib-module/utils/index.js +1 -0
- package/lib-module/utils/useSafeLayoutEffect.js +30 -0
- package/package.json +6 -5
- package/src/BaseProvider/HydrationContext.jsx +44 -0
- package/src/BaseProvider/index.jsx +11 -7
- package/src/Button/ButtonBase.jsx +2 -2
- package/src/List/List.jsx +9 -13
- package/src/List/PressableListItemBase.jsx +7 -9
- package/src/QuickLinks/QuickLinks.jsx +61 -0
- package/src/QuickLinks/QuickLinksCard.jsx +26 -0
- package/src/QuickLinks/QuickLinksItem.jsx +46 -0
- package/src/QuickLinks/index.js +6 -0
- package/src/StackView/StackWrap.jsx +20 -13
- package/src/Timeline/Timeline.jsx +148 -0
- package/src/Timeline/index.js +3 -0
- package/src/ViewportProvider/useViewportListener.js +4 -16
- package/src/index.js +3 -0
- package/src/utils/animation/useVerticalExpandAnimation.js +4 -2
- package/src/utils/index.js +1 -0
- package/src/utils/useSafeLayoutEffect.js +31 -0
package/src/List/List.jsx
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import React, { cloneElement, forwardRef, Children } from 'react'
|
|
2
2
|
import { View, Platform } from 'react-native'
|
|
3
3
|
import PropTypes from 'prop-types'
|
|
4
|
-
import {
|
|
5
|
-
a11yProps,
|
|
6
|
-
componentPropType,
|
|
7
|
-
getTokensPropType,
|
|
8
|
-
selectSystemProps,
|
|
9
|
-
variantProp,
|
|
10
|
-
viewProps
|
|
11
|
-
} from '../utils'
|
|
12
|
-
import ListItem from './ListItem'
|
|
4
|
+
import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
|
|
13
5
|
|
|
14
6
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
15
7
|
|
|
8
|
+
const isListItem = (element) => {
|
|
9
|
+
const elementName = element?.type?.displayName || element?.type?.name
|
|
10
|
+
// Match our own ListItem, and also, custom list items
|
|
11
|
+
return Boolean(elementName.match(/Item/))
|
|
12
|
+
}
|
|
13
|
+
|
|
16
14
|
/**
|
|
17
15
|
* An unordered List component has a child a ListItem that
|
|
18
16
|
* allows icon, dividers and customized typography
|
|
@@ -31,9 +29,7 @@ const List = forwardRef(
|
|
|
31
29
|
) => {
|
|
32
30
|
const items = Children.map(children, (child, index) => {
|
|
33
31
|
// Pass ListItem-specific props to children (by name so teams can add their own ListItems)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (isListItem(child?.type?.displayName) || isListItem(child?.type?.name)) {
|
|
32
|
+
if (isListItem(child)) {
|
|
37
33
|
return cloneElement(child, {
|
|
38
34
|
showDivider,
|
|
39
35
|
isLastItem: index + 1 === Children.count(children),
|
|
@@ -58,7 +54,7 @@ List.propTypes = {
|
|
|
58
54
|
...selectedSystemPropTypes,
|
|
59
55
|
tokens: getTokensPropType('List'),
|
|
60
56
|
variant: variantProp.propType,
|
|
61
|
-
children:
|
|
57
|
+
children: PropTypes.node,
|
|
62
58
|
/**
|
|
63
59
|
* In case it is not the last item allow display divider
|
|
64
60
|
*/
|
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
clickProps,
|
|
10
10
|
linkProps,
|
|
11
11
|
hrefAttrsProp,
|
|
12
|
+
selectTokens,
|
|
12
13
|
withLinkRouter
|
|
13
14
|
} from '../utils'
|
|
14
15
|
|
|
15
16
|
import ListItemBase from './ListItemBase'
|
|
16
|
-
import ListItemContent
|
|
17
|
-
import ListItemMark
|
|
17
|
+
import ListItemContent from './ListItemContent'
|
|
18
|
+
import ListItemMark from './ListItemMark'
|
|
18
19
|
|
|
19
20
|
const selectPressableStyles = ({
|
|
20
21
|
backgroundColor,
|
|
@@ -39,8 +40,10 @@ const PressableListItemBase = forwardRef(
|
|
|
39
40
|
const { hrefAttrs, rest: listItemProps } = hrefAttrsProp.bundle(props)
|
|
40
41
|
const handlePress = linkProps.handleHref({ href, onPress })
|
|
41
42
|
|
|
43
|
+
const listItemTokens = selectTokens('List', typeof tokens === 'function' ? tokens() : tokens)
|
|
44
|
+
|
|
42
45
|
return (
|
|
43
|
-
<ListItemBase ref={listItemRef} tokens={
|
|
46
|
+
<ListItemBase ref={listItemRef} tokens={listItemTokens} {...listItemProps}>
|
|
44
47
|
{({ isLastItem }) => {
|
|
45
48
|
const getTokens = (pressableState) =>
|
|
46
49
|
resolvePressableTokens(tokens, pressableState, { last: isLastItem })
|
|
@@ -81,18 +84,13 @@ const staticStyles = StyleSheet.create({
|
|
|
81
84
|
itemContainer: {
|
|
82
85
|
flexDirection: 'row',
|
|
83
86
|
flex: 1
|
|
84
|
-
},
|
|
85
|
-
tokens: {
|
|
86
|
-
...contentTokenTypes,
|
|
87
|
-
...markTokenTypes
|
|
88
87
|
}
|
|
89
88
|
})
|
|
90
89
|
|
|
91
90
|
PressableListItemBase.propTypes = {
|
|
91
|
+
...withLinkRouter.propTypes,
|
|
92
92
|
href: PropTypes.string,
|
|
93
93
|
onPress: PropTypes.func,
|
|
94
|
-
// TODO - type this better, maybe import the subcomponent token types and run it through util
|
|
95
|
-
// eslint-disable-next-line react/forbid-prop-types
|
|
96
94
|
tokens: PropTypes.any,
|
|
97
95
|
icon: PropTypes.elementType,
|
|
98
96
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import { useViewport } from '../ViewportProvider'
|
|
6
|
+
import { getTokensPropType, variantProp } from '../utils'
|
|
7
|
+
|
|
8
|
+
import List from '../List'
|
|
9
|
+
import StackWrap from '../StackView/StackWrap'
|
|
10
|
+
import QuickLinksCard from './QuickLinksCard'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* QuickLinks renders a list of interactive items. How it renders these items depends on theme options:
|
|
14
|
+
* - If the theme returns `list` token as true, it renders an ordered list based on List
|
|
15
|
+
* - If the theme returns `button` token as true and `list` as false, it renders a wrapping horizontal bar of buttons
|
|
16
|
+
* - If the theme returns `card` token as true, it wraps the above with a `Card`.
|
|
17
|
+
*/
|
|
18
|
+
const QuickLinks = forwardRef(
|
|
19
|
+
({ tokens, variant, listTokens, cardTokens, children, tag = 'ul', ...rest }, ref) => {
|
|
20
|
+
const viewport = useViewport()
|
|
21
|
+
const { dividers, list, card, stackSpace, stackGap, stackJustify } = useThemeTokens(
|
|
22
|
+
'QuickLinks',
|
|
23
|
+
tokens,
|
|
24
|
+
variant,
|
|
25
|
+
{
|
|
26
|
+
viewport
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const content = (list && (
|
|
31
|
+
<List ref={ref} tokens={listTokens} showDivider={dividers} tag={tag} {...rest}>
|
|
32
|
+
{children}
|
|
33
|
+
</List>
|
|
34
|
+
)) || (
|
|
35
|
+
<StackWrap
|
|
36
|
+
space={stackSpace}
|
|
37
|
+
gap={stackGap}
|
|
38
|
+
tokens={{ justifyContent: stackJustify }}
|
|
39
|
+
tag={tag}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</StackWrap>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return card ? <QuickLinksCard tokens={cardTokens}>{content}</QuickLinksCard> : content
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
QuickLinks.displayName = 'QuickLinks'
|
|
51
|
+
|
|
52
|
+
QuickLinks.propTypes = {
|
|
53
|
+
tokens: getTokensPropType('QuickLinks'),
|
|
54
|
+
cardTokens: getTokensPropType('Card'),
|
|
55
|
+
listTokens: getTokensPropType('QuickLinksList'),
|
|
56
|
+
tag: PropTypes.string,
|
|
57
|
+
variant: variantProp.propType,
|
|
58
|
+
children: PropTypes.node
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default QuickLinks
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import { getTokensPropType, variantProp } from '../utils'
|
|
6
|
+
import CardBase from '../Card/CardBase'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Private subcomponent for use within QuickLinks.
|
|
10
|
+
*
|
|
11
|
+
* Restyled Card with identical behaviour to Card, but themed according to the
|
|
12
|
+
* QuickLinksCard theme rather than the Card theme.
|
|
13
|
+
*/
|
|
14
|
+
const QuickLinksList = ({ tokens, variant, children }) => {
|
|
15
|
+
const themeTokens = useThemeTokens('QuickLinksCard', tokens, variant)
|
|
16
|
+
|
|
17
|
+
return <CardBase tokens={themeTokens}>{children}</CardBase>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
QuickLinksList.propTypes = {
|
|
21
|
+
tokens: getTokensPropType('QuickLinksCard'),
|
|
22
|
+
variant: variantProp.propType,
|
|
23
|
+
children: PropTypes.node
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default QuickLinksList
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { getTokensPropType, variantProp, withLinkRouter } from '../utils'
|
|
5
|
+
import { useViewport } from '../ViewportProvider'
|
|
6
|
+
import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider'
|
|
7
|
+
|
|
8
|
+
import PressableListItemBase from '../List/PressableListItemBase'
|
|
9
|
+
import ButtonBase from '../Button/ButtonBase'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public component exported as QuickLinks.Item, for use as children of QuickLinks.
|
|
13
|
+
*
|
|
14
|
+
* Receives props injected by QuickLinks and renders the appropriate child component.
|
|
15
|
+
*/
|
|
16
|
+
const QuickLinksItem = forwardRef(({ tokens, variant, children, ...rest }, ref) => {
|
|
17
|
+
const viewport = useViewport()
|
|
18
|
+
const { list } = useThemeTokens('QuickLinks', tokens, variant, { viewport })
|
|
19
|
+
|
|
20
|
+
const themeName = list ? 'QuickLinksList' : 'QuickLinksButton'
|
|
21
|
+
|
|
22
|
+
const getTokens = useThemeTokensCallback(themeName, tokens, variant)
|
|
23
|
+
|
|
24
|
+
return list ? (
|
|
25
|
+
<PressableListItemBase ref={ref} tokens={getTokens} {...rest}>
|
|
26
|
+
{children}
|
|
27
|
+
</PressableListItemBase>
|
|
28
|
+
) : (
|
|
29
|
+
<ButtonBase ref={ref} tokens={getTokens} {...rest}>
|
|
30
|
+
{children}
|
|
31
|
+
</ButtonBase>
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
QuickLinksItem.displayName = 'QuickLinksItem'
|
|
36
|
+
|
|
37
|
+
QuickLinksItem.propTypes = {
|
|
38
|
+
...withLinkRouter.propTypes,
|
|
39
|
+
...PressableListItemBase.propTypes,
|
|
40
|
+
...ButtonBase.propTypes,
|
|
41
|
+
tokens: getTokensPropType('QuickLinksList', 'QuickLinksButton'),
|
|
42
|
+
variant: variantProp.propType,
|
|
43
|
+
children: PropTypes.node
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default withLinkRouter(QuickLinksItem)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react'
|
|
1
|
+
import React, { forwardRef, useState } from 'react'
|
|
2
2
|
import { Platform } from 'react-native'
|
|
3
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../utils/useSafeLayoutEffect'
|
|
4
5
|
import StackWrapBox from './StackWrapBox'
|
|
5
6
|
import StackWrapGap from './StackWrapGap'
|
|
6
7
|
|
|
7
8
|
// In Jest/CI/SSR, global CSS isn't always available and doesn't always have .supports method
|
|
8
|
-
const cssSupports = (
|
|
9
|
+
const cssSupports = (property, value) =>
|
|
10
|
+
Platform.OS === 'web' &&
|
|
9
11
|
typeof window !== 'undefined' &&
|
|
10
12
|
typeof window.CSS?.supports === 'function' &&
|
|
11
|
-
window.CSS.supports(
|
|
13
|
+
window.CSS.supports(property, value)
|
|
12
14
|
|
|
13
15
|
// CSS.supports needs an example of the type of value you intend to use.
|
|
14
16
|
// Will be an integer appended `px` after hooks and JSX styles are resolved.
|
|
@@ -22,19 +24,24 @@ const exampleGapValue = '1px'
|
|
|
22
24
|
* If a different spacing is desired between wrapped lines, pass a spacing value to the `gap` prop.
|
|
23
25
|
*/
|
|
24
26
|
const StackWrap = forwardRef((props, ref) => {
|
|
27
|
+
const [canUseCSSGap, setCanUseCSSGap] = useState(false)
|
|
25
28
|
const { space } = props
|
|
26
29
|
// Don't apply separate gap if `null` or `undefined`, so can be unset in Storybook etc
|
|
27
30
|
const gap = props.gap ?? space
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
31
|
+
const gapEqualsSpace = gap === space
|
|
32
|
+
|
|
33
|
+
// If possible, use the cleaner implementation that applies CSS `gap` styles to the container,
|
|
34
|
+
// preserving direct parent-child relationships between the container and each item, which
|
|
35
|
+
// can result in clearer descriptions on some screen readers (e.g. radio "X of Y" on MacOS).
|
|
36
|
+
// Else, use the fallback implementation which renders a `Box` component around each child.
|
|
37
|
+
const Component = canUseCSSGap ? StackWrapGap : StackWrapBox
|
|
38
|
+
// In SSR, the type of implementation must match the server during hydration, but
|
|
39
|
+
// the server can't know if gap is supported, so never use it until after hydration.
|
|
40
|
+
useSafeLayoutEffect(() => {
|
|
41
|
+
setCanUseCSSGap(gapEqualsSpace && cssSupports('gap', exampleGapValue))
|
|
42
|
+
}, [gapEqualsSpace])
|
|
43
|
+
|
|
44
|
+
return <Component ref={ref} {...props} />
|
|
38
45
|
})
|
|
39
46
|
StackWrap.displayName = 'StackWrap'
|
|
40
47
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { forwardRef } from 'react'
|
|
3
|
+
import { View } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
6
|
+
import {
|
|
7
|
+
getTokensPropType,
|
|
8
|
+
variantProp,
|
|
9
|
+
a11yProps,
|
|
10
|
+
viewProps,
|
|
11
|
+
selectSystemProps,
|
|
12
|
+
getA11yPropsFromHtmlTag,
|
|
13
|
+
layoutTags
|
|
14
|
+
} from '../utils'
|
|
15
|
+
import { useViewport } from '../ViewportProvider'
|
|
16
|
+
|
|
17
|
+
const selectDotStyles = ({ dotWidth, timelineColor, dotBorderWidth, dotColor }) => ({
|
|
18
|
+
width: dotWidth,
|
|
19
|
+
height: dotWidth,
|
|
20
|
+
borderRadius: dotWidth / 2,
|
|
21
|
+
backgroundColor: dotColor,
|
|
22
|
+
borderWidth: dotBorderWidth,
|
|
23
|
+
borderColor: timelineColor
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const selectConnectorStyles = ({ timelineColor, connectorHeight, connectorWidth }) => ({
|
|
27
|
+
width: connectorWidth,
|
|
28
|
+
height: connectorHeight,
|
|
29
|
+
backgroundColor: timelineColor
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const selectTimelineContainerStyle = ({ timelineContainerDirection }) => ({
|
|
33
|
+
flexDirection: timelineContainerDirection
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const selectLineItemStyles = ({
|
|
37
|
+
lineItemAlign,
|
|
38
|
+
lineItemDirection,
|
|
39
|
+
lineItemMarginBottom,
|
|
40
|
+
lineItemMarginRight
|
|
41
|
+
}) => ({
|
|
42
|
+
alignItems: lineItemAlign,
|
|
43
|
+
flexDirection: lineItemDirection,
|
|
44
|
+
marginBottom: lineItemMarginBottom,
|
|
45
|
+
marginRight: lineItemMarginRight,
|
|
46
|
+
overflow: 'hidden'
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const selectLineItemContainer = ({ lineItemContainerDirection, lineContainerFlexSize }) => ({
|
|
50
|
+
flexDirection: lineItemContainerDirection,
|
|
51
|
+
flex: lineContainerFlexSize
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const selectItemContentStyles = (
|
|
55
|
+
{ itemContentFlexSize, itemContentMarginBottom, itemContentMarginRight },
|
|
56
|
+
isLastChild
|
|
57
|
+
) => {
|
|
58
|
+
return {
|
|
59
|
+
flex: itemContentFlexSize,
|
|
60
|
+
marginBottom: !isLastChild && itemContentMarginBottom,
|
|
61
|
+
marginRight: !isLastChild && itemContentMarginRight
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Timeline is a component that displays either a horizontal or vertical list of the
|
|
69
|
+
* children components passed by props
|
|
70
|
+
*
|
|
71
|
+
* ## Component API
|
|
72
|
+
*
|
|
73
|
+
* - `horizontal` In order to display the Component list horizontally
|
|
74
|
+
*
|
|
75
|
+
*
|
|
76
|
+
* ## A11y guidelines
|
|
77
|
+
* Timeline link supports all the common a11y props.
|
|
78
|
+
*/
|
|
79
|
+
const Timeline = forwardRef(
|
|
80
|
+
(
|
|
81
|
+
{ tokens, variant = {}, children, accessibilityLabel, tag = 'ul', childrenTag = 'li', ...rest },
|
|
82
|
+
ref
|
|
83
|
+
) => {
|
|
84
|
+
const viewport = useViewport()
|
|
85
|
+
const themeTokens = useThemeTokens('Timeline', tokens, variant, { viewport })
|
|
86
|
+
|
|
87
|
+
const containerProps = {
|
|
88
|
+
...selectProps(rest),
|
|
89
|
+
...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole || 'list'),
|
|
90
|
+
accessibilityLabel
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<View {...containerProps} ref={ref} style={selectTimelineContainerStyle(themeTokens)}>
|
|
95
|
+
{children.map((child, index) => {
|
|
96
|
+
const childrenProps = {
|
|
97
|
+
...getA11yPropsFromHtmlTag(childrenTag, child?.props?.accessibilityRole || 'listitem')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View
|
|
102
|
+
style={selectLineItemContainer(themeTokens)}
|
|
103
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
104
|
+
key={`timeline-${index}-${child.displayName}`}
|
|
105
|
+
{...childrenProps}
|
|
106
|
+
>
|
|
107
|
+
<View style={selectLineItemStyles(themeTokens)}>
|
|
108
|
+
<View style={selectDotStyles(themeTokens)} />
|
|
109
|
+
<View style={selectConnectorStyles(themeTokens)} />
|
|
110
|
+
</View>
|
|
111
|
+
<View style={selectItemContentStyles(themeTokens, index + 1 === children.length)}>
|
|
112
|
+
{child}
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
</View>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
Timeline.displayName = 'Timeline'
|
|
123
|
+
|
|
124
|
+
Timeline.propTypes = {
|
|
125
|
+
...selectedSystemPropTypes,
|
|
126
|
+
tokens: getTokensPropType('Timeline'),
|
|
127
|
+
variant: variantProp.propType,
|
|
128
|
+
/**
|
|
129
|
+
* A list of components that will be rendered either horizontally or vertically
|
|
130
|
+
*/
|
|
131
|
+
children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
|
132
|
+
/**
|
|
133
|
+
* A required accessibility label that needs to be passed to be used on List
|
|
134
|
+
* which is applied as normal for a React Native accessibilityLabel prop.
|
|
135
|
+
*/
|
|
136
|
+
accessibilityLabel: PropTypes.string.isRequired,
|
|
137
|
+
/**
|
|
138
|
+
* Sets the HTML tag of the outer container and the children. By default `'li'` for the children
|
|
139
|
+
* and `'ul'` for the container
|
|
140
|
+
*
|
|
141
|
+
* If either `tag` or `childrenTag` is overridden, the other should be too, to avoid producing invalid HTML.
|
|
142
|
+
*
|
|
143
|
+
*/
|
|
144
|
+
tag: PropTypes.oneOf(layoutTags),
|
|
145
|
+
childrenTag: PropTypes.oneOf(layoutTags)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default Timeline
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
-
import { useLayoutEffect } from 'react'
|
|
2
1
|
import { Dimensions } from 'react-native'
|
|
3
2
|
import { viewports } from '@telus-uds/system-constants'
|
|
4
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../utils/useSafeLayoutEffect'
|
|
5
|
+
|
|
5
6
|
// Use Dimensions instead of useWindowDimensions because useWindowDimensions forces context
|
|
6
7
|
// to update on every pixel change during window resize; but we only want rerenders to occur
|
|
7
8
|
// when a viewport threshold has been crossed.
|
|
8
9
|
const lookupViewport = () => viewports.select(Dimensions.get('window').width)
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* In SSR, React gets spooked if it sees `useLayoutEffect` and fires warnings assuming the
|
|
12
|
-
* developer doesn't realise the effect won't run: https://reactjs.org/link/uselayouteffect-ssr
|
|
13
|
-
*
|
|
14
|
-
* To avoid these warnings while still conforming to the rules of hooks, always use this
|
|
15
|
-
* explicitly no-op hook, instead of the useLayoutEffect that is implicitly no-op on SSR.
|
|
16
|
-
*/
|
|
17
|
-
const useViewportListenerSSR = () => {}
|
|
18
|
-
|
|
19
11
|
/**
|
|
20
12
|
* When client-side rendering, immediately set the viewport to the correct value as a layout effect so
|
|
21
13
|
* if the viewport isn't the smallest, any SSR-rendered components rerender correctly before anything
|
|
22
14
|
* is shown to the user. Then bind events to update the viewport if it changes.
|
|
23
15
|
*/
|
|
24
|
-
const
|
|
25
|
-
|
|
16
|
+
const useViewportListener = (setViewport) => {
|
|
17
|
+
useSafeLayoutEffect(() => {
|
|
26
18
|
setViewport(lookupViewport())
|
|
27
19
|
|
|
28
20
|
const onChange = ({ window }) => setViewport(viewports.select(window.width))
|
|
@@ -41,8 +33,4 @@ const useViewportListenerCSR = (setViewport) => {
|
|
|
41
33
|
}, [setViewport])
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
// Window is a defined global object in both Web and Native client-side, and undefined in SSR
|
|
45
|
-
const isSSR = typeof window === 'undefined'
|
|
46
|
-
const useViewportListener = isSSR ? useViewportListenerSSR : useViewportListenerCSR
|
|
47
|
-
|
|
48
36
|
export default useViewportListener
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { default as Modal } from './Modal'
|
|
|
24
24
|
export { default as Notification } from './Notification'
|
|
25
25
|
export { default as Pagination } from './Pagination'
|
|
26
26
|
export { default as Progress } from './Progress'
|
|
27
|
+
export { default as QuickLinks } from './QuickLinks'
|
|
27
28
|
export { default as Radio } from './Radio'
|
|
28
29
|
export * from './Radio'
|
|
29
30
|
export { default as RadioCard } from './RadioCard'
|
|
@@ -40,6 +41,7 @@ export { default as StepTracker } from './StepTracker'
|
|
|
40
41
|
export { default as Tabs } from './Tabs'
|
|
41
42
|
export { default as Tags } from './Tags'
|
|
42
43
|
export * from './TextInput'
|
|
44
|
+
export { default as Timeline } from './Timeline'
|
|
43
45
|
export * from './ToggleSwitch'
|
|
44
46
|
export { default as Tooltip } from './Tooltip'
|
|
45
47
|
export { default as TooltipButton } from './TooltipButton'
|
|
@@ -60,3 +62,4 @@ export {
|
|
|
60
62
|
} from './ThemeProvider'
|
|
61
63
|
|
|
62
64
|
export * from './utils'
|
|
65
|
+
export { Portal } from '@gorhom/portal'
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { Animated, Easing, Platform } from 'react-native'
|
|
3
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../useSafeLayoutEffect'
|
|
5
|
+
|
|
4
6
|
// TODO: systematise animations
|
|
5
7
|
// https://github.com/telus/universal-design-system/issues/487
|
|
6
8
|
function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
|
|
@@ -13,7 +15,7 @@ function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
|
|
|
13
15
|
|
|
14
16
|
const { expandDuration, collapseDuration } = tokens
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
useSafeLayoutEffect(() => {
|
|
17
19
|
if (expandStateChanged) {
|
|
18
20
|
setIsAnimating(true)
|
|
19
21
|
setWasExpanded(isExpanded)
|
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 useSafeLayoutEffect } from './useSafeLayoutEffect'
|
|
13
14
|
export { default as useScrollBlocking } from './useScrollBlocking'
|
|
14
15
|
export * from './useResponsiveProp'
|
|
15
16
|
export { default as useUniqueId } from './useUniqueId'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useLayoutEffect, useCallback } from 'react'
|
|
2
|
+
import { useHydrationContext } from '../BaseProvider/HydrationContext'
|
|
3
|
+
|
|
4
|
+
const isSSR = typeof window === 'undefined'
|
|
5
|
+
const noop = () => {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* useSafeLayoutEffect is a alternative to useLayoutEffect that avoids SSR hydration problems:
|
|
9
|
+
* - In a client-side render, it uses useLayoutEffect to avoid flashing the pre-render UI to the user.
|
|
10
|
+
* - During hydration from SSR, the provided function is skipped to avoid mismatches from server content.
|
|
11
|
+
* - In SSR, it is a no-op function to avoid warnings about using useLayoutEffect in SSR
|
|
12
|
+
*/
|
|
13
|
+
const useSafeLayoutEffect = isSSR
|
|
14
|
+
? noop // avoid React's fussy warnings by ensuring to never call useLayoutEffect on server
|
|
15
|
+
: (fn, deps = []) => {
|
|
16
|
+
const isHydrating = useHydrationContext()
|
|
17
|
+
|
|
18
|
+
// Callback updates and effect re-runs when deps array content changes, like useEffect.
|
|
19
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
20
|
+
const callback = useCallback(fn, deps)
|
|
21
|
+
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
// Do nothing before hydrating server-generated content, like useEffect. When hydration completes,
|
|
24
|
+
// useHydrationContext provides false, re-rendering this hook and re-running the effect.
|
|
25
|
+
if (isHydrating) return noop
|
|
26
|
+
// If there's no hydration in progress, behave like useLayoutEffect.
|
|
27
|
+
return callback()
|
|
28
|
+
}, [isHydrating, callback])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default useSafeLayoutEffect
|