@telus-uds/components-web 3.1.0 → 3.2.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 CHANGED
@@ -1,12 +1,26 @@
1
1
  # Change Log - @telus-uds/components-web
2
2
 
3
- <!-- This log was last generated on Mon, 02 Dec 2024 20:23:48 GMT and should not be manually modified. -->
3
+ <!-- This log was last generated on Fri, 06 Dec 2024 02:03:26 GMT and should not be manually modified. -->
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 3.2.0
8
+
9
+ Fri, 06 Dec 2024 02:03:26 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `NavigationBar`: enable hash navigation (guillermo.peitzner@telus.com)
14
+ - `ChevronLink`, `Search` & `ResponsiveImage`: new `dataSet` prop added to the components to allow the capability to pass data (35577399+JoshHC@users.noreply.github.com)
15
+ - Bump @telus-uds/components-base to v2.2.0
16
+
17
+ ### Patches
18
+
19
+ - `QuantitySelector`: render hint when no label is provided (guillermo.peitzner@telus.com)
20
+
7
21
  ## 3.1.0
8
22
 
9
- Mon, 02 Dec 2024 20:23:48 GMT
23
+ Mon, 02 Dec 2024 20:29:26 GMT
10
24
 
11
25
  ### Minor changes
12
26
 
@@ -1,9 +1,9 @@
1
1
  var _withLinkRouter$propT, _withLinkRouter$propT2, _withLinkRouter$propT3, _withLinkRouter$propT4;
2
2
  import React from 'react';
3
3
  import PropTypes from 'prop-types';
4
- import { selectSystemProps, StackView, Typography, useResponsiveProp, withLinkRouter } from '@telus-uds/components-base';
4
+ import { selectSystemProps, StackView, Typography, useHash, useInputValue, useResponsiveProp, withLinkRouter } from '@telus-uds/components-base';
5
5
  import styled from 'styled-components';
6
- import { htmlAttrs } from '../utils';
6
+ import { htmlAttrs, scrollToAnchor } from '../utils';
7
7
  import NavigationItem from './NavigationItem';
8
8
  import NavigationSubMenu from './NavigationSubMenu';
9
9
  import collapseItems from './collapseItems';
@@ -33,18 +33,56 @@ const NavigationBar = /*#__PURE__*/React.forwardRef((_ref, ref) => {
33
33
  heading,
34
34
  headingLevel = 'h1',
35
35
  items,
36
- onChange = () => {},
36
+ onChange,
37
37
  selectedId,
38
+ value,
38
39
  LinkRouter,
39
40
  linkRouterProps,
40
41
  ...rest
41
42
  } = _ref;
43
+ const {
44
+ currentValue,
45
+ setValue
46
+ } = useInputValue({
47
+ value,
48
+ initialValue: selectedId,
49
+ onChange
50
+ });
51
+ useHash((hash, event) => {
52
+ let hashItem = hash && items.find(_ref2 => {
53
+ let {
54
+ href
55
+ } = _ref2;
56
+ return hash === href;
57
+ });
58
+ if (!hashItem) {
59
+ const parentItem = items.find(_ref3 => {
60
+ let {
61
+ items: parentItems
62
+ } = _ref3;
63
+ return parentItems === null || parentItems === void 0 ? void 0 : parentItems.some(_ref4 => {
64
+ let {
65
+ href
66
+ } = _ref4;
67
+ return hash === href;
68
+ });
69
+ });
70
+ hashItem = parentItem === null || parentItem === void 0 ? void 0 : parentItem.items.find(_ref5 => {
71
+ let {
72
+ href
73
+ } = _ref5;
74
+ return hash === href;
75
+ });
76
+ }
77
+ const hashId = hashItem && (hashItem.id || hashItem.label);
78
+ if (hashId) setValue(hashId, event);
79
+ }, [items, setValue]);
42
80
  const direction = useResponsiveProp({
43
81
  xs: 'column',
44
82
  sm: 'row'
45
83
  });
46
84
  const itemsForViewport = useResponsiveProp({
47
- xs: collapseItems(items, selectedId),
85
+ xs: collapseItems(items, currentValue),
48
86
  lg: items
49
87
  });
50
88
  const openOverlayRef = React.useRef(null);
@@ -118,7 +156,7 @@ const NavigationBar = /*#__PURE__*/React.forwardRef((_ref, ref) => {
118
156
  heading: headingLevel,
119
157
  children: heading
120
158
  })
121
- }), itemsForViewport === null || itemsForViewport === void 0 ? void 0 : itemsForViewport.map((_ref2, index) => {
159
+ }), itemsForViewport === null || itemsForViewport === void 0 ? void 0 : itemsForViewport.map((_ref6, index) => {
122
160
  let {
123
161
  href,
124
162
  label,
@@ -129,38 +167,50 @@ const NavigationBar = /*#__PURE__*/React.forwardRef((_ref, ref) => {
129
167
  linkRouterProps: itemLinkRouterProps,
130
168
  items: nestedItems,
131
169
  ...itemRest
132
- } = _ref2;
170
+ } = _ref6;
133
171
  const itemId = id ?? label;
134
172
  const handleClick = event => {
135
173
  if (nestedItems) {
136
174
  setOpenSubMenuId(openSubMenuId !== itemId ? itemId : null);
175
+ return;
176
+ }
177
+ if (href !== null && href !== void 0 && href.startsWith('#')) {
178
+ scrollToAnchor(href, event, () => setValue(itemId, event));
179
+ } else {
180
+ setValue(itemId, event);
137
181
  }
138
182
  onClick === null || onClick === void 0 || onClick(event);
139
- onChange === null || onChange === void 0 || onChange(itemId, event);
140
183
  };
141
184
  const ItemComponent = nestedItems ? NavigationSubMenu : NavigationItem;
142
185
  const isOpen = itemId === openSubMenuId;
186
+ const scrollableNestedItems = (nestedItems === null || nestedItems === void 0 ? void 0 : nestedItems.map(item => ({
187
+ ...item,
188
+ onPress: event => {
189
+ const nestedItemId = item.id ?? item.label;
190
+ scrollToAnchor(item.href, event, () => setValue(nestedItemId, event));
191
+ }
192
+ }))) ?? nestedItems;
143
193
  return /*#__PURE__*/_jsx(ItemComponent, {
144
194
  ref: itemRef,
145
195
  href: href,
146
196
  onClick: handleClick
147
197
  // TODO: refactor to pass selected ID via context
148
198
  ,
149
- selectedId: selectedId,
199
+ selectedId: currentValue,
150
200
  index: index,
151
201
  LinkRouter: ItemLinkRouter,
152
202
  linkRouterProps: {
153
203
  ...linkRouterProps,
154
204
  ...itemLinkRouterProps
155
205
  },
156
- items: nestedItems,
157
- selected: itemId === selectedId,
206
+ items: scrollableNestedItems,
207
+ selected: itemId === currentValue,
158
208
  itemsContainerRef: itemsRef,
159
209
  ...itemRest,
160
- ...(nestedItems && {
210
+ ...(scrollableNestedItems && {
161
211
  isOpen
162
212
  }),
163
- ...(nestedItems && isOpen && {
213
+ ...(scrollableNestedItems && isOpen && {
164
214
  openOverlayRef
165
215
  }),
166
216
  children: label
@@ -177,7 +227,7 @@ NavigationBar.propTypes = {
177
227
  *
178
228
  * Each `item` object must contain:
179
229
  * - `heading` - user-facing text in the tab link
180
- * - `href` - the URL of the page linked to. Do not use hash links, for content within a page, use `Tabs`.
230
+ * - `href` - the URL of the page linked to.
181
231
  * - `id` - a stable, unique identifier of the page within the set. Not written into the HTML.
182
232
  */
183
233
  items: PropTypes.arrayOf(PropTypes.shape({
@@ -208,13 +258,17 @@ NavigationBar.propTypes = {
208
258
  */
209
259
  headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
210
260
  /**
211
- * Matches the `id` property of the item in `items` corresponding to the current page
261
+ * Initial selected item ID
212
262
  */
213
- selectedId: PropTypes.string.isRequired,
263
+ selectedId: PropTypes.string,
214
264
  /**
215
265
  * Optional function to be called on pressing a link
216
266
  */
217
267
  onChange: PropTypes.func,
268
+ /**
269
+ * Controlled value for selected item ID
270
+ */
271
+ value: PropTypes.string,
218
272
  /**
219
273
  * Accesibility role for stackview
220
274
  */
@@ -78,7 +78,7 @@ const QuantitySelector = /*#__PURE__*/React.forwardRef((_ref, ref) => {
78
78
  setError(getCopy('errors').invalidCharacters);
79
79
  }
80
80
  };
81
- const renderLabel = () => label ? /*#__PURE__*/_jsx(InputLabel, {
81
+ const renderLabel = () => label || hint ? /*#__PURE__*/_jsx(InputLabel, {
82
82
  forId: id,
83
83
  label: label,
84
84
  hint: hint,
@@ -25,11 +25,13 @@ const ResponsiveImage = /*#__PURE__*/React.forwardRef((_ref, ref) => {
25
25
  fallbackSrc,
26
26
  alt,
27
27
  loading = 'eager',
28
+ dataSet,
28
29
  ...rest
29
30
  } = _ref;
30
31
  return /*#__PURE__*/_jsxs("picture", {
31
32
  ...selectProps(rest),
32
33
  ref: ref,
34
+ ...dataSet,
33
35
  children: [/*#__PURE__*/_jsx("source", {
34
36
  srcSet: xlSrc,
35
37
  media: `(min-width: ${viewports.map.get(viewports.xl)}px)`
@@ -89,6 +91,10 @@ ResponsiveImage.propTypes = {
89
91
  * @default 'eager'
90
92
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading
91
93
  */
92
- loading: PropTypes.oneOf(['eager', 'lazy'])
94
+ loading: PropTypes.oneOf(['eager', 'lazy']),
95
+ /**
96
+ * The dataSet prop allows to pass data-* attributes element to the component.
97
+ */
98
+ dataSet: PropTypes.object
93
99
  };
94
100
  export default ResponsiveImage;
@@ -7,4 +7,5 @@ import ssrStyles from './ssr';
7
7
  import isElementFocusable from './isElementFocusable';
8
8
  import renderStructuredContent from './renderStructuredContent';
9
9
  import useOverlaidPosition from './useOverlaidPosition';
10
- export { deprecate, htmlAttrs, contentfulProps, transformGradient, useTypographyTheme, warn, media, renderStructuredContent, ssrStyles, isElementFocusable, useOverlaidPosition };
10
+ import scrollToAnchor from './scrollToAnchor';
11
+ export { deprecate, htmlAttrs, contentfulProps, transformGradient, useTypographyTheme, warn, media, renderStructuredContent, ssrStyles, isElementFocusable, useOverlaidPosition, scrollToAnchor };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Scrolls smoothly to the anchor element specified by the href.
3
+ *
4
+ * @param {string} href - The href attribute value, expected to be an anchor link starting with '#'.
5
+ * @param {Event} event - The event object associated with the click or navigation action.
6
+ * @param {Function} onAfterScroll - A callback function to be executed after the scroll action is completed.
7
+ */
8
+ const scrollToAnchor = (href, event, onAfterScroll) => {
9
+ if (href !== null && href !== void 0 && href.startsWith('#')) {
10
+ event.preventDefault();
11
+ const target = document.getElementById(href.slice(1));
12
+ target === null || target === void 0 || target.scrollIntoView({
13
+ behavior: 'smooth'
14
+ });
15
+ window.location.hash = href;
16
+ onAfterScroll(event);
17
+ }
18
+ };
19
+ export default scrollToAnchor;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  ],
7
7
  "dependencies": {
8
8
  "@gorhom/portal": "^1.0.14",
9
- "@telus-uds/components-base": "^2.1.0",
9
+ "@telus-uds/components-base": "^2.2.0",
10
10
  "@telus-uds/system-constants": "^2.0.0",
11
11
  "fscreen": "^1.2.0",
12
12
  "lodash.omit": "^4.5.0",
@@ -81,5 +81,5 @@
81
81
  "skip": true
82
82
  },
83
83
  "types": "types/index.d.ts",
84
- "version": "3.1.0"
84
+ "version": "3.2.0"
85
85
  }
@@ -4,11 +4,13 @@ import {
4
4
  selectSystemProps,
5
5
  StackView,
6
6
  Typography,
7
+ useHash,
8
+ useInputValue,
7
9
  useResponsiveProp,
8
10
  withLinkRouter
9
11
  } from '@telus-uds/components-base'
10
12
  import styled from 'styled-components'
11
- import { htmlAttrs } from '../utils'
13
+ import { htmlAttrs, scrollToAnchor } from '../utils'
12
14
  import NavigationItem from './NavigationItem'
13
15
  import NavigationSubMenu from './NavigationSubMenu'
14
16
  import collapseItems from './collapseItems'
@@ -34,16 +36,37 @@ const NavigationBar = React.forwardRef(
34
36
  heading,
35
37
  headingLevel = 'h1',
36
38
  items,
37
- onChange = () => {},
39
+ onChange,
38
40
  selectedId,
41
+ value,
39
42
  LinkRouter,
40
43
  linkRouterProps,
41
44
  ...rest
42
45
  },
43
46
  ref
44
47
  ) => {
48
+ const { currentValue, setValue } = useInputValue({ value, initialValue: selectedId, onChange })
49
+
50
+ useHash(
51
+ (hash, event) => {
52
+ let hashItem = hash && items.find(({ href }) => hash === href)
53
+ if (!hashItem) {
54
+ const parentItem = items.find(({ items: parentItems }) =>
55
+ parentItems?.some(({ href }) => hash === href)
56
+ )
57
+ hashItem = parentItem?.items.find(({ href }) => hash === href)
58
+ }
59
+ const hashId = hashItem && (hashItem.id || hashItem.label)
60
+ if (hashId) setValue(hashId, event)
61
+ },
62
+ [items, setValue]
63
+ )
64
+
45
65
  const direction = useResponsiveProp({ xs: 'column', sm: 'row' })
46
- const itemsForViewport = useResponsiveProp({ xs: collapseItems(items, selectedId), lg: items })
66
+ const itemsForViewport = useResponsiveProp({
67
+ xs: collapseItems(items, currentValue),
68
+ lg: items
69
+ })
47
70
  const openOverlayRef = React.useRef(null)
48
71
  const [openSubMenuId, setOpenSubMenuId] = React.useState(null)
49
72
  const handleSubMenuClose = (event) => {
@@ -161,14 +184,28 @@ const NavigationBar = React.forwardRef(
161
184
  const handleClick = (event) => {
162
185
  if (nestedItems) {
163
186
  setOpenSubMenuId(openSubMenuId !== itemId ? itemId : null)
187
+ return
188
+ }
189
+ if (href?.startsWith('#')) {
190
+ scrollToAnchor(href, event, () => setValue(itemId, event))
191
+ } else {
192
+ setValue(itemId, event)
164
193
  }
165
194
  onClick?.(event)
166
- onChange?.(itemId, event)
167
195
  }
168
196
 
169
197
  const ItemComponent = nestedItems ? NavigationSubMenu : NavigationItem
170
198
  const isOpen = itemId === openSubMenuId
171
199
 
200
+ const scrollableNestedItems =
201
+ nestedItems?.map((item) => ({
202
+ ...item,
203
+ onPress: (event) => {
204
+ const nestedItemId = item.id ?? item.label
205
+ scrollToAnchor(item.href, event, () => setValue(nestedItemId, event))
206
+ }
207
+ })) ?? nestedItems
208
+
172
209
  return (
173
210
  <ItemComponent
174
211
  ref={itemRef}
@@ -176,16 +213,16 @@ const NavigationBar = React.forwardRef(
176
213
  href={href}
177
214
  onClick={handleClick}
178
215
  // TODO: refactor to pass selected ID via context
179
- selectedId={selectedId}
216
+ selectedId={currentValue}
180
217
  index={index}
181
218
  LinkRouter={ItemLinkRouter}
182
219
  linkRouterProps={{ ...linkRouterProps, ...itemLinkRouterProps }}
183
- items={nestedItems}
184
- selected={itemId === selectedId}
220
+ items={scrollableNestedItems}
221
+ selected={itemId === currentValue}
185
222
  itemsContainerRef={itemsRef}
186
223
  {...itemRest}
187
- {...(nestedItems && { isOpen })}
188
- {...(nestedItems && isOpen && { openOverlayRef })}
224
+ {...(scrollableNestedItems && { isOpen })}
225
+ {...(scrollableNestedItems && isOpen && { openOverlayRef })}
189
226
  >
190
227
  {label}
191
228
  </ItemComponent>
@@ -207,7 +244,7 @@ NavigationBar.propTypes = {
207
244
  *
208
245
  * Each `item` object must contain:
209
246
  * - `heading` - user-facing text in the tab link
210
- * - `href` - the URL of the page linked to. Do not use hash links, for content within a page, use `Tabs`.
247
+ * - `href` - the URL of the page linked to.
211
248
  * - `id` - a stable, unique identifier of the page within the set. Not written into the HTML.
212
249
  */
213
250
  items: PropTypes.arrayOf(
@@ -242,13 +279,17 @@ NavigationBar.propTypes = {
242
279
  */
243
280
  headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
244
281
  /**
245
- * Matches the `id` property of the item in `items` corresponding to the current page
282
+ * Initial selected item ID
246
283
  */
247
- selectedId: PropTypes.string.isRequired,
284
+ selectedId: PropTypes.string,
248
285
  /**
249
286
  * Optional function to be called on pressing a link
250
287
  */
251
288
  onChange: PropTypes.func,
289
+ /**
290
+ * Controlled value for selected item ID
291
+ */
292
+ value: PropTypes.string,
252
293
  /**
253
294
  * Accesibility role for stackview
254
295
  */
@@ -96,7 +96,7 @@ const QuantitySelector = React.forwardRef(
96
96
  }
97
97
 
98
98
  const renderLabel = () =>
99
- label ? (
99
+ label || hint ? (
100
100
  <InputLabel
101
101
  forId={id}
102
102
  label={label}
@@ -14,9 +14,12 @@ const staticStyles = {
14
14
  * Provide different image sources for different screen sizes.
15
15
  */
16
16
  const ResponsiveImage = React.forwardRef(
17
- ({ xsSrc, smSrc, mdSrc, lgSrc, xlSrc, fallbackSrc, alt, loading = 'eager', ...rest }, ref) => {
17
+ (
18
+ { xsSrc, smSrc, mdSrc, lgSrc, xlSrc, fallbackSrc, alt, loading = 'eager', dataSet, ...rest },
19
+ ref
20
+ ) => {
18
21
  return (
19
- <picture {...selectProps(rest)} ref={ref}>
22
+ <picture {...selectProps(rest)} ref={ref} {...dataSet}>
20
23
  <source srcSet={xlSrc} media={`(min-width: ${viewports.map.get(viewports.xl)}px)`} />
21
24
  <source srcSet={lgSrc} media={`(min-width: ${viewports.map.get(viewports.lg)}px)`} />
22
25
  <source srcSet={mdSrc} media={`(min-width: ${viewports.map.get(viewports.md)}px)`} />
@@ -65,7 +68,11 @@ ResponsiveImage.propTypes = {
65
68
  * @default 'eager'
66
69
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading
67
70
  */
68
- loading: PropTypes.oneOf(['eager', 'lazy'])
71
+ loading: PropTypes.oneOf(['eager', 'lazy']),
72
+ /**
73
+ * The dataSet prop allows to pass data-* attributes element to the component.
74
+ */
75
+ dataSet: PropTypes.object
69
76
  }
70
77
 
71
78
  export default ResponsiveImage
@@ -7,6 +7,7 @@ import ssrStyles from './ssr'
7
7
  import isElementFocusable from './isElementFocusable'
8
8
  import renderStructuredContent from './renderStructuredContent'
9
9
  import useOverlaidPosition from './useOverlaidPosition'
10
+ import scrollToAnchor from './scrollToAnchor'
10
11
 
11
12
  export {
12
13
  deprecate,
@@ -19,5 +20,6 @@ export {
19
20
  renderStructuredContent,
20
21
  ssrStyles,
21
22
  isElementFocusable,
22
- useOverlaidPosition
23
+ useOverlaidPosition,
24
+ scrollToAnchor
23
25
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Scrolls smoothly to the anchor element specified by the href.
3
+ *
4
+ * @param {string} href - The href attribute value, expected to be an anchor link starting with '#'.
5
+ * @param {Event} event - The event object associated with the click or navigation action.
6
+ * @param {Function} onAfterScroll - A callback function to be executed after the scroll action is completed.
7
+ */
8
+ const scrollToAnchor = (href, event, onAfterScroll) => {
9
+ if (href?.startsWith('#')) {
10
+ event.preventDefault()
11
+ const target = document.getElementById(href.slice(1))
12
+ target?.scrollIntoView({ behavior: 'smooth' })
13
+ window.location.hash = href
14
+ onAfterScroll(event)
15
+ }
16
+ }
17
+
18
+ export default scrollToAnchor