@telus-uds/components-base 1.33.0 → 1.34.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.
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _react = require("react");
9
+
10
+ var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Dimensions"));
11
+
12
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
+
14
+ const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
15
+ const offset = Math.max(0, initialOffset);
16
+ const otherEdgeOverflow = Math.max(0, offset + sourceWidth - windowWidth);
17
+ const tooWideBy = Math.max(0, otherEdgeOverflow - offset);
18
+ const adjusted = {
19
+ offset: Math.max(0, offset - otherEdgeOverflow)
20
+ };
21
+ if (tooWideBy) adjusted.width = Math.max(0, sourceWidth - tooWideBy);
22
+ return adjusted;
23
+ };
24
+
25
+ const getPosition = _ref => {
26
+ let {
27
+ edge,
28
+ fromEdge,
29
+ sourceSize
30
+ } = _ref;
31
+
32
+ switch (edge) {
33
+ case 'near':
34
+ return fromEdge;
35
+
36
+ case 'mid':
37
+ return fromEdge + sourceSize / 2;
38
+
39
+ case 'far':
40
+ return fromEdge + sourceSize;
41
+
42
+ default:
43
+ return 0;
44
+ }
45
+ };
46
+
47
+ const getEdgeType = (align, alignSide) => {
48
+ const alignTo = align[alignSide];
49
+ const edge = ['center', 'middle'].includes(alignTo) && 'mid' || (alignSide === alignTo ? 'near' : 'far');
50
+ return edge;
51
+ };
52
+ /**
53
+ * Based on UDS's private getTooltipPosition but generalised.
54
+ *
55
+ * Used for absolute positioning of the tooltip. Since the tooltip is always centered relatively
56
+ * to the source (button) and we have a limited set of positions, an easy and consistent way
57
+ * of positioning it is to check all of the possible positions and pick one that will be rendered
58
+ * within the window bounds. This way we can also rely on the tooltip being actually rendered
59
+ * before it is shown, which makes it account for the width being limiting in styles, custom font
60
+ * rendering, etc.
61
+ */
62
+
63
+
64
+ function getOverlaidPosition(_ref2) {
65
+ let {
66
+ sourceLayout,
67
+ targetDimensions,
68
+ windowDimensions,
69
+ offsets = {},
70
+ align
71
+ } = _ref2;
72
+ // Web-only: this will be difficult to mimic on native because there's no global scroll position.
73
+ // TODO: wire something in e.g. a scroll ref accessible from a provider included in Allium provider
74
+ // that can be passed to the appropriate ScrollView?
75
+ const {
76
+ scrollX = 0,
77
+ scrollY = 0
78
+ } = typeof window === 'object' ? window : {}; // Will have top, bottom, left and/or right offsets depending on `align`
79
+
80
+ const positioning = {};
81
+ if (align.top) positioning.top = getPosition({
82
+ edge: getEdgeType(align, 'top'),
83
+ fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0),
84
+ sourceSize: sourceLayout.height
85
+ });
86
+ if (align.middle) positioning.top = getPosition({
87
+ edge: getEdgeType(align, 'middle'),
88
+ fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0) - targetDimensions.height / 2,
89
+ sourceSize: sourceLayout.height
90
+ });
91
+ if (align.bottom) positioning.bottom = getPosition({
92
+ edge: getEdgeType(align, 'bottom'),
93
+ fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - (offsets.vertical ?? 0)),
94
+ sourceSize: sourceLayout.height
95
+ });
96
+ if (align.left) positioning.left = getPosition({
97
+ edge: getEdgeType(align, 'left'),
98
+ fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0),
99
+ sourceSize: sourceLayout.width
100
+ });
101
+ if (align.center) positioning.left = getPosition({
102
+ edge: getEdgeType(align, 'center'),
103
+ fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0) - targetDimensions.width / 2,
104
+ sourceSize: sourceLayout.width
105
+ });
106
+ if (align.right) positioning.right = getPosition({
107
+ edge: getEdgeType(align, 'right'),
108
+ fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - (offsets.horizontal ?? 0)),
109
+ sourceSize: sourceLayout.width
110
+ });
111
+
112
+ if (!(align.left && align.right)) {
113
+ // Check if the position and/or width need adjusting to fit on the screen
114
+ const side = align.right ? 'right' : 'left';
115
+ const adjusted = adjustHorizontalToFit(positioning[side], windowDimensions.width, sourceLayout.width);
116
+ if (typeof adjusted.width === 'number') positioning.width = adjusted.width;
117
+
118
+ if (typeof adjusted.offset === 'number') {
119
+ positioning[side] = adjusted.offset;
120
+ }
121
+ }
122
+
123
+ return positioning;
124
+ }
125
+ /**
126
+ * Positions an element in a modal or portal so that it appears tooltip-like below the
127
+ * target element.
128
+ *
129
+ * @TODO - add support for positioning other than 'below' like UDS's tooltip (this is not
130
+ * a small task because UDS's tooltip logic only really works for short text - it might be
131
+ * better to use a third-party library).
132
+ */
133
+
134
+
135
+ const useOverlaidPosition = _ref3 => {
136
+ let {
137
+ isShown = false,
138
+ offsets,
139
+ // By default, align the overlaid target's `top` to the bottom of the source, and center horizontally.
140
+ align = {
141
+ center: 'center',
142
+ top: 'bottom'
143
+ }
144
+ } = _ref3;
145
+ // Element in main document flow that the targetRef element is positioned around
146
+ const sourceRef = (0, _react.useRef)(null);
147
+ const [sourceLayout, setSourceLayout] = (0, _react.useState)(null); // Element in a modal or portal overlay positioned to appear adjacent to sourceRef
148
+
149
+ const targetRef = (0, _react.useRef)(null);
150
+ const [targetDimensions, setTargetDimensions] = (0, _react.useState)(null);
151
+ const [windowDimensions, setWindowDimensions] = (0, _react.useState)(null);
152
+ const onTargetLayout = (0, _react.useCallback)(_ref4 => {
153
+ let {
154
+ nativeEvent: {
155
+ layout: {
156
+ width,
157
+ height
158
+ }
159
+ }
160
+ } = _ref4;
161
+ // NOTE: UDS's Tooltip logic injects some additional width to allow for antialiasing etc of text,
162
+ // avoiding adding unnecessary line breaks to text that is slightly wider than it thinks it is.
163
+ // That is probably something specific to text tooltips that doesn't belong in a generic hook.
164
+ setTargetDimensions(previousDimensions => {
165
+ // Re-render on first non-zero width / height: avoid infinite loops on changes, or mispositioning
166
+ // if user scrolls while a slidedown animation is changing the height and recalculating position.
167
+ if (!previousDimensions && width && height) {
168
+ return {
169
+ width,
170
+ height
171
+ };
172
+ }
173
+
174
+ return previousDimensions;
175
+ });
176
+ }, []);
177
+ const readyToShow = Boolean(isShown && sourceRef.current);
178
+ (0, _react.useEffect)(() => {
179
+ const handleDimensionsChange = _ref5 => {
180
+ var _sourceRef$current;
181
+
182
+ let {
183
+ window
184
+ } = _ref5;
185
+ (_sourceRef$current = sourceRef.current) === null || _sourceRef$current === void 0 ? void 0 : _sourceRef$current.measureInWindow((x, y, width, height) => {
186
+ // Could add a debouncer here if there's too many rerenders during gradual resizes
187
+ setWindowDimensions(window);
188
+ setSourceLayout({
189
+ x,
190
+ y,
191
+ width,
192
+ height
193
+ });
194
+ });
195
+ };
196
+
197
+ let subscription;
198
+
199
+ const unsubscribe = () => {
200
+ var _subscription;
201
+
202
+ if (typeof ((_subscription = subscription) === null || _subscription === void 0 ? void 0 : _subscription.remove) === 'function') {
203
+ // React Native >=0.65.0
204
+ subscription.remove();
205
+ } else if (typeof _Dimensions.default.removeEventListener === 'function') {
206
+ // React Native <0.65.0
207
+ _Dimensions.default.removeEventListener('change', handleDimensionsChange);
208
+ }
209
+
210
+ setSourceLayout(null);
211
+ setTargetDimensions(null);
212
+ };
213
+
214
+ if (readyToShow) {
215
+ subscription = _Dimensions.default.addEventListener('change', handleDimensionsChange);
216
+ handleDimensionsChange({
217
+ window: _Dimensions.default.get('window')
218
+ });
219
+ } else {
220
+ unsubscribe();
221
+ }
222
+
223
+ return unsubscribe;
224
+ }, [readyToShow]);
225
+ const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions);
226
+ const overlaidPosition = isReady ? getOverlaidPosition({
227
+ sourceLayout,
228
+ targetDimensions,
229
+ windowDimensions,
230
+ offsets,
231
+ align
232
+ }) : {};
233
+ return {
234
+ overlaidPosition,
235
+ sourceRef,
236
+ targetRef,
237
+ onTargetLayout,
238
+ isReady
239
+ };
240
+ };
241
+
242
+ var _default = useOverlaidPosition;
243
+ exports.default = _default;
@@ -213,8 +213,9 @@ const staticStyles = StyleSheet.create({
213
213
 
214
214
  },
215
215
  modal: {
216
- maxHeight: '100%' // so that the modal can expand vertically up to the sizing container's height (exclusive of its vertical padding)
217
-
216
+ maxHeight: '100%',
217
+ // so that the modal can expand vertically up to the sizing container's height (exclusive of its vertical padding)
218
+ overflow: 'auto'
218
219
  },
219
220
  closeButtonContainer: {
220
221
  position: 'absolute',
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
5
  import { Portal } from '@gorhom/portal';
6
- import { copyPropTypes, getTokensPropType, selectTokens, useCopy, variantProp } from '../utils';
6
+ import { selectTokens, useCopy, copyPropTypes, getTokensPropType, variantProp } from '../utils';
7
7
  import { useViewport } from '../ViewportProvider';
8
8
  import { useThemeTokens } from '../ThemeProvider';
9
9
  import dictionary from './dictionary';
@@ -11,14 +11,12 @@ import Card from '../Card';
11
11
  import IconButton from '../IconButton';
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { jsxs as _jsxs } from "react/jsx-runtime";
14
- import { Fragment as _Fragment } from "react/jsx-runtime";
15
14
  const staticStyles = StyleSheet.create({
16
15
  positioner: {
17
16
  flex: 1,
18
17
  // Grow to maxWidth when possible, shrink when not possible
19
18
  position: 'absolute',
20
19
  height: 330,
21
- paddingTop: 5,
22
20
  zIndex: 10000 // Position on top of all the other overlays, including backdrops and modals
23
21
 
24
22
  },
@@ -27,6 +25,11 @@ const staticStyles = StyleSheet.create({
27
25
  top: 0,
28
26
  right: 0,
29
27
  zIndex: 1
28
+ },
29
+ hidden: {
30
+ // Use opacity not visibility to hide the dropdown during positioning
31
+ // so on web, children may be focused from the first render
32
+ opacity: 0
30
33
  }
31
34
  });
32
35
 
@@ -58,8 +61,11 @@ const selectPaddingContainerStyles = _ref2 => {
58
61
  const ModalOverlay = /*#__PURE__*/forwardRef((_ref3, ref) => {
59
62
  let {
60
63
  children,
61
- tokens,
64
+ isReady = false,
65
+ overlaidPosition,
66
+ onLayout,
62
67
  variant,
68
+ tokens,
63
69
  copy,
64
70
  onClose
65
71
  } = _ref3;
@@ -77,26 +83,25 @@ const ModalOverlay = /*#__PURE__*/forwardRef((_ref3, ref) => {
77
83
  copy
78
84
  });
79
85
  const closeLabel = getCopy('closeButton');
80
- return /*#__PURE__*/_jsx(_Fragment, {
81
- children: /*#__PURE__*/_jsx(Portal, {
86
+ return /*#__PURE__*/_jsx(Portal, {
87
+ children: /*#__PURE__*/_jsx(View, {
82
88
  ref: ref,
83
- children: /*#__PURE__*/_jsx(View, {
84
- style: [{
85
- minWidth: maxWidth
86
- }, staticStyles.positioner],
87
- children: /*#__PURE__*/_jsxs(Card, {
88
- tokens: selectPaddingContainerStyles(themeTokens),
89
- children: [/*#__PURE__*/_jsx(View, {
90
- style: [staticStyles.closeButtonContainer, selectCloseButtonContainerStyles(themeTokens)],
91
- children: /*#__PURE__*/_jsx(IconButton, {
92
- onPress: onClose,
93
- icon: CloseIconComponent,
94
- accessibilityRole: "button",
95
- accessibilityLabel: closeLabel,
96
- tokens: selectTokens('IconButton', themeTokens, 'close')
97
- })
98
- }), children]
99
- })
89
+ onLayout: onLayout,
90
+ style: [overlaidPosition, {
91
+ minWidth: maxWidth
92
+ }, staticStyles.positioner, !isReady && staticStyles.hidden],
93
+ children: /*#__PURE__*/_jsxs(Card, {
94
+ tokens: selectPaddingContainerStyles(themeTokens),
95
+ children: [/*#__PURE__*/_jsx(View, {
96
+ style: [staticStyles.closeButtonContainer, selectCloseButtonContainerStyles(themeTokens)],
97
+ children: /*#__PURE__*/_jsx(IconButton, {
98
+ onPress: onClose,
99
+ icon: CloseIconComponent,
100
+ accessibilityRole: "button",
101
+ accessibilityLabel: closeLabel,
102
+ tokens: selectTokens('IconButton', themeTokens, 'close')
103
+ })
104
+ }), children]
100
105
  })
101
106
  })
102
107
  });
@@ -104,6 +109,13 @@ const ModalOverlay = /*#__PURE__*/forwardRef((_ref3, ref) => {
104
109
  ModalOverlay.displayName = 'ModalOverlay';
105
110
  ModalOverlay.propTypes = {
106
111
  children: PropTypes.node.isRequired,
112
+ isReady: PropTypes.bool,
113
+ overlaidPosition: PropTypes.shape({
114
+ top: PropTypes.number,
115
+ left: PropTypes.number,
116
+ width: PropTypes.number
117
+ }),
118
+ onLayout: PropTypes.func,
107
119
  variant: variantProp.propType,
108
120
  tokens: getTokensPropType('Modal'),
109
121
  copy: copyPropTypes,
@@ -1,7 +1,7 @@
1
- import React, { forwardRef, useState } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider';
4
- import { containUniqueFields, getTokensPropType, getPressHandlersWithArgs, selectTokens, useCopy, useMultipleInputValues, variantProp } from '../utils';
4
+ import { containUniqueFields, getTokensPropType, getPressHandlersWithArgs, selectTokens, useOverlaidPosition, useCopy, useMultipleInputValues, useResponsiveProp, variantProp } from '../utils';
5
5
  import dictionary from './dictionary';
6
6
  import Box from '../Box';
7
7
  import { Button, ButtonDropdown } from '../Button';
@@ -45,7 +45,7 @@ const selectDividerToknes = _ref2 => {
45
45
  };
46
46
  };
47
47
 
48
- const MultiSelectFilter = /*#__PURE__*/forwardRef((_ref3, ref) => {
48
+ const MultiSelectFilter = _ref3 => {
49
49
  let {
50
50
  label,
51
51
  subtitle,
@@ -109,9 +109,41 @@ const MultiSelectFilter = /*#__PURE__*/forwardRef((_ref3, ref) => {
109
109
  setIsOpen(false);
110
110
  };
111
111
 
112
+ const {
113
+ align,
114
+ offsets
115
+ } = useResponsiveProp({
116
+ xs: {
117
+ align: {
118
+ top: 'top',
119
+ left: 'left',
120
+ bottom: 'bottom',
121
+ right: 'right'
122
+ }
123
+ },
124
+ sm: {
125
+ align: {
126
+ top: 'bottom',
127
+ left: 'left'
128
+ },
129
+ offsets: {
130
+ vertical: 4
131
+ }
132
+ }
133
+ });
134
+ const {
135
+ overlaidPosition,
136
+ onTargetLayout,
137
+ isReady,
138
+ sourceRef
139
+ } = useOverlaidPosition({
140
+ isShown: isOpen,
141
+ offsets,
142
+ align
143
+ });
112
144
  return /*#__PURE__*/_jsxs(_Fragment, {
113
145
  children: [/*#__PURE__*/_jsx(ButtonDropdown, {
114
- ref: ref,
146
+ ref: sourceRef,
115
147
  ...pressHandlers,
116
148
  value: isOpen,
117
149
  selected: isSelected,
@@ -120,10 +152,15 @@ const MultiSelectFilter = /*#__PURE__*/forwardRef((_ref3, ref) => {
120
152
  tokens: getButtonTokens,
121
153
  inactive: inactive
122
154
  }, id), isOpen && /*#__PURE__*/_jsxs(ModalOverlay, {
155
+ overlaidPosition: overlaidPosition,
123
156
  variant: {
124
157
  width: colSize > 1 ? 'size576' : 's'
125
158
  },
126
159
  onClose: () => setIsOpen(false),
160
+ tokens: tokens,
161
+ copy: copy,
162
+ isReady: isReady,
163
+ onLayout: onTargetLayout,
127
164
  children: [/*#__PURE__*/_jsx(Row, {
128
165
  children: /*#__PURE__*/_jsx(Typography, {
129
166
  variant: {
@@ -189,7 +226,8 @@ const MultiSelectFilter = /*#__PURE__*/forwardRef((_ref3, ref) => {
189
226
  })]
190
227
  })]
191
228
  });
192
- });
229
+ };
230
+
193
231
  MultiSelectFilter.displayName = 'MultiSelectFilter';
194
232
  MultiSelectFilter.propTypes = {
195
233
  /**
@@ -43,10 +43,12 @@ const selectDescriptionStyles = _ref2 => {
43
43
  descriptionLineHeight,
44
44
  descriptionMarginLeft,
45
45
  inputSize,
46
+ fontColor,
46
47
  labelMarginLeft = 0
47
48
  } = _ref2;
48
49
  return {
49
50
  marginLeft: descriptionMarginLeft ?? containerPaddingLeft + inputSize + labelMarginLeft,
51
+ color: fontColor,
50
52
  ...applyTextStyles({
51
53
  fontSize: descriptionFontSize,
52
54
  lineHeight: descriptionLineHeight
@@ -205,7 +207,9 @@ const Radio = /*#__PURE__*/forwardRef((_ref4, ref) => {
205
207
  })
206
208
  })]
207
209
  }), Boolean(description) && /*#__PURE__*/_jsx(Text, {
208
- style: selectDescriptionStyles(getTokens()),
210
+ style: selectDescriptionStyles({ ...getTokens(),
211
+ fontColor: labelStyles.color
212
+ }),
209
213
  children: description
210
214
  })]
211
215
  });
@@ -9,6 +9,7 @@ export { default as useCopy } from './useCopy';
9
9
  export { default as useHash } from './useHash';
10
10
  export { default as useSpacingScale } from './useSpacingScale';
11
11
  export { default as useResponsiveProp } from './useResponsiveProp';
12
+ export { default as useOverlaidPosition } from './useOverlaidPosition';
12
13
  export { default as useSafeLayoutEffect } from './useSafeLayoutEffect';
13
14
  export { default as useScrollBlocking } from './useScrollBlocking';
14
15
  export * from './useResponsiveProp';