@telus-uds/components-web 4.16.0 → 4.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 +34 -1
- package/lib/cjs/Card/Card.js +267 -107
- package/lib/cjs/NavigationBar/NavigationBar.js +59 -17
- package/lib/cjs/NavigationBar/NavigationSubMenu.js +51 -34
- package/lib/cjs/NavigationBar/collapseItems.js +1 -1
- package/lib/cjs/QuantitySelector/QuantitySelector.js +34 -14
- package/lib/cjs/shared/FullBleedContent/FullBleedContent.js +46 -8
- package/lib/cjs/utils/useOverlaidPosition.js +11 -1
- package/lib/esm/Card/Card.js +267 -107
- package/lib/esm/NavigationBar/NavigationBar.js +60 -18
- package/lib/esm/NavigationBar/NavigationSubMenu.js +49 -34
- package/lib/esm/NavigationBar/collapseItems.js +1 -1
- package/lib/esm/QuantitySelector/QuantitySelector.js +35 -15
- package/lib/esm/shared/FullBleedContent/FullBleedContent.js +48 -9
- package/lib/esm/utils/useOverlaidPosition.js +11 -1
- package/package.json +3 -3
- package/src/Card/Card.jsx +276 -110
- package/src/NavigationBar/NavigationBar.jsx +39 -4
- package/src/NavigationBar/NavigationSubMenu.jsx +38 -11
- package/src/NavigationBar/collapseItems.js +1 -1
- package/src/QuantitySelector/QuantitySelector.jsx +36 -18
- package/src/shared/FullBleedContent/FullBleedContent.jsx +34 -4
- package/src/utils/useOverlaidPosition.js +9 -1
package/src/Card/Card.jsx
CHANGED
|
@@ -30,6 +30,56 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
|
|
|
30
30
|
|
|
31
31
|
const GRID_COLUMNS = 12
|
|
32
32
|
|
|
33
|
+
const POSITION = {
|
|
34
|
+
LEFT: 'left',
|
|
35
|
+
RIGHT: 'right',
|
|
36
|
+
TOP: 'top',
|
|
37
|
+
BOTTOM: 'bottom',
|
|
38
|
+
NONE: 'none'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Helper function to get StackView tokens
|
|
43
|
+
* @param {Object} columnFlex - Column flex properties
|
|
44
|
+
* @param {string} contentStackAlign - Alignment value for content stack
|
|
45
|
+
* @returns {Object} StackView tokens object
|
|
46
|
+
*/
|
|
47
|
+
const getStackViewTokens = (columnFlex, contentStackAlign) => ({
|
|
48
|
+
...columnFlex,
|
|
49
|
+
alignItems: contentStackAlign
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper function to get CardContent tokens
|
|
54
|
+
* @param {Object} baseTokens - Base tokens to spread
|
|
55
|
+
* @param {Object} options - Options object
|
|
56
|
+
* @param {boolean} options.backgroundImage - Whether background image is present
|
|
57
|
+
* @param {string} options.fullBleedContentChildrenAlign - Alignment for full bleed content children
|
|
58
|
+
* @param {boolean} options.useTransparentBackground - Whether to use transparent background when no backgroundImage
|
|
59
|
+
* @returns {Object} CardContent tokens object
|
|
60
|
+
*/
|
|
61
|
+
const getCardContentTokens = (baseTokens, options = {}) => {
|
|
62
|
+
const { backgroundImage, fullBleedContentChildrenAlign, useTransparentBackground } = options
|
|
63
|
+
|
|
64
|
+
// Determine background color based on conditions
|
|
65
|
+
let backgroundColorOverride = {}
|
|
66
|
+
if (useTransparentBackground) {
|
|
67
|
+
if (!backgroundImage) {
|
|
68
|
+
backgroundColorOverride = { backgroundColor: 'transparent' }
|
|
69
|
+
}
|
|
70
|
+
} else if (backgroundImage) {
|
|
71
|
+
backgroundColorOverride = { backgroundColor: 'transparent' }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...baseTokens,
|
|
76
|
+
...backgroundColorOverride,
|
|
77
|
+
...(fullBleedContentChildrenAlign && {
|
|
78
|
+
alignSelf: fullBleedContentChildrenAlign
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
33
83
|
/**
|
|
34
84
|
* A basic card component, unstyled by default.
|
|
35
85
|
*
|
|
@@ -88,6 +138,40 @@ const DynamicWidthContainer = styled.div.withConfig({
|
|
|
88
138
|
})
|
|
89
139
|
)
|
|
90
140
|
|
|
141
|
+
const InteractiveCardWrapper = styled.div(() => ({
|
|
142
|
+
position: 'relative',
|
|
143
|
+
flex: 1,
|
|
144
|
+
display: 'flex',
|
|
145
|
+
flexDirection: 'column'
|
|
146
|
+
}))
|
|
147
|
+
|
|
148
|
+
const InteractiveOverlay = styled.div(({ overlayOpacity, borderRadius }) => ({
|
|
149
|
+
position: 'absolute',
|
|
150
|
+
top: 0,
|
|
151
|
+
left: 0,
|
|
152
|
+
right: 0,
|
|
153
|
+
bottom: 0,
|
|
154
|
+
backgroundColor: `rgba(0, 0, 0, ${overlayOpacity || 0})`,
|
|
155
|
+
borderRadius,
|
|
156
|
+
pointerEvents: 'none',
|
|
157
|
+
transition: 'background-color 0.2s ease',
|
|
158
|
+
zIndex: 1
|
|
159
|
+
}))
|
|
160
|
+
|
|
161
|
+
const FocusBorder = styled.div(({ borderWidth, borderColor, borderRadius }) => ({
|
|
162
|
+
position: 'absolute',
|
|
163
|
+
top: 0,
|
|
164
|
+
left: 0,
|
|
165
|
+
right: 0,
|
|
166
|
+
bottom: 0,
|
|
167
|
+
borderWidth,
|
|
168
|
+
borderColor,
|
|
169
|
+
borderRadius,
|
|
170
|
+
borderStyle: 'solid',
|
|
171
|
+
pointerEvents: 'none',
|
|
172
|
+
zIndex: 2
|
|
173
|
+
}))
|
|
174
|
+
|
|
91
175
|
const Card = React.forwardRef(
|
|
92
176
|
(
|
|
93
177
|
{
|
|
@@ -137,19 +221,61 @@ const Card = React.forwardRef(
|
|
|
137
221
|
const allThemeTokens = useThemeTokens('Card', tokens, variant)
|
|
138
222
|
const { borderRadius } = allThemeTokens
|
|
139
223
|
|
|
140
|
-
|
|
141
|
-
|
|
224
|
+
// Interactive cards: merge variants for CardBase (outer container)
|
|
225
|
+
// The outer variant takes priority over interactiveCard.variant for the style property
|
|
226
|
+
// This ensures the gradient is only applied to CardBase, not PressableCardBase and avoid duplication
|
|
227
|
+
const interactiveStyle = interactiveCard?.variant?.style
|
|
228
|
+
const outerStyle = variant?.style
|
|
229
|
+
const mergedVariant = {
|
|
230
|
+
...variant,
|
|
231
|
+
style: outerStyle || interactiveStyle
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Interactive cards: build configuration for PressableCardBase
|
|
235
|
+
// This determines which style to use for interactive states (hover, pressed, etc.)
|
|
236
|
+
// without causing gradient duplication
|
|
237
|
+
let interactiveCardConfig = {}
|
|
238
|
+
if (interactiveCard?.body) {
|
|
239
|
+
const styleToUse = interactiveCard?.variant?.style || variant?.style
|
|
240
|
+
const { style, ...otherVariantProps } = interactiveCard?.variant || {}
|
|
241
|
+
|
|
242
|
+
interactiveCardConfig = {
|
|
243
|
+
interactive: true,
|
|
244
|
+
...(styleToUse && { style: styleToUse }),
|
|
245
|
+
...otherVariantProps
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const getThemeTokensBase = useThemeTokensCallback(
|
|
142
250
|
'Card',
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
const marginOffset = `${-(cardBaseBorderWidth + pressableBorderWidth) / 2}px`
|
|
251
|
+
interactiveCard?.tokens,
|
|
252
|
+
interactiveCardConfig
|
|
253
|
+
)
|
|
147
254
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
255
|
+
// Wrap getThemeTokens to remove gradient from resolved tokens
|
|
256
|
+
// PressableCardBase calls this function with pressableState (hover, pressed, etc.)
|
|
257
|
+
// We intercept the resolved tokens and remove gradient properties to prevent duplication
|
|
258
|
+
// since the gradient should only appear on CardBase (outer container)
|
|
259
|
+
const getThemeTokens = React.useCallback(
|
|
260
|
+
(pressableState) => {
|
|
261
|
+
const resolvedTokens = getThemeTokensBase(pressableState)
|
|
262
|
+
const { gradient, backgroundGradient, ...tokensWithoutGradient } = resolvedTokens
|
|
263
|
+
return tokensWithoutGradient
|
|
264
|
+
},
|
|
265
|
+
[getThemeTokensBase]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const getFocusBorderTokens = useThemeTokensCallback(
|
|
269
|
+
'Card',
|
|
270
|
+
{},
|
|
271
|
+
{
|
|
272
|
+
...variant,
|
|
273
|
+
interactive: true
|
|
274
|
+
}
|
|
275
|
+
)
|
|
152
276
|
|
|
277
|
+
// Remove backgroundColor from tokens for full bleed interactive content
|
|
278
|
+
// to prevent background from covering the full bleed image
|
|
153
279
|
const { backgroundColor: _, ...tokensWithoutBg } = tokens
|
|
154
280
|
|
|
155
281
|
const getFullBleedInteractiveTokens = useThemeTokensCallback('Card', tokensWithoutBg, {
|
|
@@ -157,15 +283,17 @@ const Card = React.forwardRef(
|
|
|
157
283
|
interactive: true
|
|
158
284
|
})
|
|
159
285
|
|
|
160
|
-
const getFullBleedInteractiveCardTokens = (cardState) =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
286
|
+
const getFullBleedInteractiveCardTokens = (cardState) => {
|
|
287
|
+
return {
|
|
288
|
+
...getFullBleedInteractiveTokens(cardState),
|
|
289
|
+
paddingTop: 0,
|
|
290
|
+
paddingBottom: 0,
|
|
291
|
+
paddingLeft: 0,
|
|
292
|
+
paddingRight: 0,
|
|
293
|
+
borderWidth: 0,
|
|
294
|
+
...(interactiveCard?.body ? { gradient: undefined } : {})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
169
297
|
|
|
170
298
|
const hasFooter = Boolean(footer)
|
|
171
299
|
const fullBleedBorderRadius = getFullBleedBorderRadius(
|
|
@@ -179,15 +307,23 @@ const Card = React.forwardRef(
|
|
|
179
307
|
// pass as props to ConditionalWrapper
|
|
180
308
|
const imgColCurrentViewport = useResponsiveProp(imgCol)
|
|
181
309
|
const maxCol = GRID_COLUMNS
|
|
182
|
-
|
|
183
|
-
const
|
|
310
|
+
|
|
311
|
+
const hasValidImgCol =
|
|
312
|
+
imgCol && imgColCurrentViewport !== undefined && !Number.isNaN(imgColCurrentViewport)
|
|
313
|
+
const fullBleedImageWidth = hasValidImgCol
|
|
314
|
+
? `${(imgColCurrentViewport / maxCol) * 100}%`
|
|
315
|
+
: undefined
|
|
316
|
+
const adaptiveContentWidth = hasValidImgCol
|
|
317
|
+
? `${((maxCol - imgColCurrentViewport) / maxCol) * 100}%`
|
|
318
|
+
: undefined
|
|
184
319
|
|
|
185
320
|
const isImageWidthAdjustable =
|
|
186
|
-
|
|
321
|
+
hasValidImgCol &&
|
|
322
|
+
(fullBleedContentPosition === POSITION.LEFT || fullBleedContentPosition === POSITION.RIGHT)
|
|
187
323
|
|
|
188
324
|
const contentWrapperStyleProps = {
|
|
189
|
-
width: adaptiveContentWidth,
|
|
190
|
-
...(imgColCurrentViewport >= maxCol && { display: 'none' }),
|
|
325
|
+
...(hasValidImgCol && { width: adaptiveContentWidth }),
|
|
326
|
+
...(hasValidImgCol && imgColCurrentViewport >= maxCol && { display: 'none' }),
|
|
191
327
|
...(fullBleedContentChildrenAlign && {
|
|
192
328
|
alignSelf: fullBleedContentChildrenAlign
|
|
193
329
|
})
|
|
@@ -207,23 +343,29 @@ const Card = React.forwardRef(
|
|
|
207
343
|
'paddingBottom',
|
|
208
344
|
'paddingLeft',
|
|
209
345
|
'paddingRight',
|
|
210
|
-
...(backgroundImage ? ['backgroundColor'] : [])
|
|
346
|
+
...(backgroundImage && interactiveCard?.body ? ['backgroundColor'] : [])
|
|
211
347
|
].includes(key)
|
|
212
348
|
)
|
|
213
349
|
)
|
|
214
350
|
|
|
351
|
+
const isHorizontalFullBleed =
|
|
352
|
+
fullBleedContentPosition === POSITION.LEFT || fullBleedContentPosition === POSITION.RIGHT
|
|
353
|
+
const isVerticalFullBleed =
|
|
354
|
+
fullBleedContentPosition === POSITION.TOP || fullBleedContentPosition === POSITION.BOTTOM
|
|
355
|
+
|
|
215
356
|
const imageWrapperStyleProps = {
|
|
216
|
-
width: fullBleedImageWidth,
|
|
217
|
-
...(
|
|
218
|
-
|
|
357
|
+
...(isImageWidthAdjustable && { width: fullBleedImageWidth }),
|
|
358
|
+
...(isImageWidthAdjustable &&
|
|
359
|
+
imgColCurrentViewport >= maxCol && { borderRadius, overflow: 'hidden' }),
|
|
360
|
+
...(isImageWidthAdjustable && imgColCurrentViewport === 0 && { display: 'none' })
|
|
219
361
|
}
|
|
220
362
|
|
|
221
363
|
return (
|
|
222
364
|
<CardBase
|
|
223
365
|
ref={ref}
|
|
224
|
-
variant={{ ...variant, padding: 'custom' }}
|
|
366
|
+
variant={{ ...(interactiveCard?.body ? mergedVariant : variant), padding: 'custom' }}
|
|
225
367
|
tokens={cardBaseTokens}
|
|
226
|
-
backgroundImage={backgroundImage}
|
|
368
|
+
backgroundImage={!interactiveCard?.body && backgroundImage}
|
|
227
369
|
onPress={fullBleedInteractive ? undefined : onPress}
|
|
228
370
|
{...(interactiveCard?.selectionType && { interactiveCard, id: rest.id })}
|
|
229
371
|
{...selectProps(restProps)}
|
|
@@ -239,6 +381,7 @@ const Card = React.forwardRef(
|
|
|
239
381
|
ref={ref}
|
|
240
382
|
tokens={getThemeTokens}
|
|
241
383
|
dataSet={dataSet}
|
|
384
|
+
backgroundImage={backgroundImage}
|
|
242
385
|
onPress={onPress}
|
|
243
386
|
href={interactiveCard?.href}
|
|
244
387
|
hrefAttrs={interactiveCard?.hrefAttrs}
|
|
@@ -252,7 +395,7 @@ const Card = React.forwardRef(
|
|
|
252
395
|
</>
|
|
253
396
|
)}
|
|
254
397
|
</PressableCardBase>
|
|
255
|
-
{children && fullBleedContentPosition ===
|
|
398
|
+
{children && fullBleedContentPosition === POSITION.NONE && !fullBleedInteractive ? (
|
|
256
399
|
<CardContent tokens={tokens} variant={variant} withFooter={hasFooter}>
|
|
257
400
|
{children}
|
|
258
401
|
</CardContent>
|
|
@@ -260,92 +403,119 @@ const Card = React.forwardRef(
|
|
|
260
403
|
</>
|
|
261
404
|
) : null}
|
|
262
405
|
{fullBleedInteractive ? (
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
406
|
+
<PressableCardBase
|
|
407
|
+
ref={ref}
|
|
408
|
+
tokens={getFullBleedInteractiveCardTokens}
|
|
409
|
+
dataSet={dataSet}
|
|
410
|
+
onPress={effectiveFullBleedOnPress}
|
|
411
|
+
href={effectiveFullBleedHref}
|
|
412
|
+
hrefAttrs={effectiveFullBleedHrefAttrs}
|
|
413
|
+
{...selectProps(restProps)}
|
|
270
414
|
>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
415
|
+
{(cardState) => {
|
|
416
|
+
let overlayOpacity = 0
|
|
417
|
+
if (cardState.pressed) {
|
|
418
|
+
overlayOpacity = 0.2
|
|
419
|
+
} else if (cardState.hover) {
|
|
420
|
+
overlayOpacity = 0.1
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const focusTokens = getFocusBorderTokens(cardState)
|
|
424
|
+
const showFocusBorder = cardState.focus && focusTokens.borderWidth > 0
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<InteractiveCardWrapper>
|
|
428
|
+
<StackView
|
|
429
|
+
direction={contentStackDirection}
|
|
430
|
+
tokens={getStackViewTokens(columnFlex, contentStackAlign)}
|
|
431
|
+
space={0}
|
|
432
|
+
>
|
|
433
|
+
{children ? (
|
|
434
|
+
<ConditionalWrapper
|
|
435
|
+
WrapperComponent={DynamicWidthContainer}
|
|
436
|
+
wrapperProps={contentWrapperStyleProps}
|
|
437
|
+
condition={isImageWidthAdjustable}
|
|
438
|
+
>
|
|
439
|
+
<CardContent
|
|
440
|
+
tokens={{
|
|
441
|
+
...getCardContentTokens(tokensWithoutBg, {
|
|
442
|
+
backgroundImage,
|
|
443
|
+
fullBleedContentChildrenAlign,
|
|
444
|
+
useTransparentBackground: true
|
|
445
|
+
}),
|
|
446
|
+
paddingTop:
|
|
447
|
+
tokens.paddingTop !== undefined
|
|
448
|
+
? tokens.paddingTop
|
|
449
|
+
: allThemeTokens.paddingTop,
|
|
450
|
+
paddingBottom:
|
|
451
|
+
tokens.paddingBottom !== undefined
|
|
452
|
+
? tokens.paddingBottom
|
|
453
|
+
: allThemeTokens.paddingBottom,
|
|
454
|
+
paddingLeft:
|
|
455
|
+
tokens.paddingLeft !== undefined
|
|
456
|
+
? tokens.paddingLeft
|
|
457
|
+
: allThemeTokens.paddingLeft,
|
|
458
|
+
paddingRight:
|
|
459
|
+
tokens.paddingRight !== undefined
|
|
460
|
+
? tokens.paddingRight
|
|
461
|
+
: allThemeTokens.paddingRight
|
|
462
|
+
}}
|
|
463
|
+
variant={variant}
|
|
464
|
+
withFooter={hasFooter}
|
|
465
|
+
>
|
|
466
|
+
{children}
|
|
467
|
+
</CardContent>
|
|
468
|
+
</ConditionalWrapper>
|
|
469
|
+
) : null}
|
|
470
|
+
{fullBleedContentPosition !== POSITION.NONE && (
|
|
471
|
+
<ConditionalWrapper
|
|
472
|
+
WrapperComponent={DynamicWidthContainer}
|
|
473
|
+
wrapperProps={imageWrapperStyleProps}
|
|
474
|
+
condition={
|
|
475
|
+
isImageWidthAdjustable || isHorizontalFullBleed || isVerticalFullBleed
|
|
476
|
+
}
|
|
303
477
|
>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
</ConditionalWrapper>
|
|
478
|
+
<FullBleedContent
|
|
479
|
+
borderRadius={fullBleedBorderRadius}
|
|
480
|
+
{...fullBleedContentPropsClean}
|
|
481
|
+
position={fullBleedContentPosition}
|
|
482
|
+
cardState={undefined}
|
|
483
|
+
/>
|
|
484
|
+
</ConditionalWrapper>
|
|
485
|
+
)}
|
|
486
|
+
</StackView>
|
|
487
|
+
<InteractiveOverlay overlayOpacity={overlayOpacity} borderRadius={borderRadius} />
|
|
488
|
+
{showFocusBorder && (
|
|
489
|
+
<FocusBorder
|
|
490
|
+
borderWidth={`${focusTokens.borderWidth}px`}
|
|
491
|
+
borderColor={focusTokens.borderColor}
|
|
492
|
+
borderRadius={borderRadius}
|
|
493
|
+
/>
|
|
321
494
|
)}
|
|
322
|
-
</
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
</
|
|
495
|
+
</InteractiveCardWrapper>
|
|
496
|
+
)
|
|
497
|
+
}}
|
|
498
|
+
</PressableCardBase>
|
|
326
499
|
) : null}
|
|
327
500
|
{!fullBleedInteractive &&
|
|
328
501
|
!interactiveCard?.body &&
|
|
329
|
-
fullBleedContentPosition ===
|
|
502
|
+
fullBleedContentPosition === POSITION.NONE &&
|
|
330
503
|
children ? (
|
|
331
504
|
<CardContent
|
|
332
|
-
tokens={{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
alignSelf: fullBleedContentChildrenAlign
|
|
337
|
-
})
|
|
338
|
-
}}
|
|
505
|
+
tokens={getCardContentTokens(tokens, {
|
|
506
|
+
backgroundImage,
|
|
507
|
+
fullBleedContentChildrenAlign
|
|
508
|
+
})}
|
|
339
509
|
variant={variant}
|
|
340
510
|
withFooter={hasFooter}
|
|
341
511
|
>
|
|
342
512
|
{children}
|
|
343
513
|
</CardContent>
|
|
344
514
|
) : null}
|
|
345
|
-
{!fullBleedInteractive && fullBleedContentPosition !==
|
|
515
|
+
{!fullBleedInteractive && fullBleedContentPosition !== POSITION.NONE ? (
|
|
346
516
|
<StackView
|
|
347
517
|
direction={contentStackDirection}
|
|
348
|
-
tokens={
|
|
518
|
+
tokens={getStackViewTokens(columnFlex, contentStackAlign)}
|
|
349
519
|
space={0}
|
|
350
520
|
>
|
|
351
521
|
{children ? (
|
|
@@ -355,13 +525,9 @@ const Card = React.forwardRef(
|
|
|
355
525
|
condition={isImageWidthAdjustable}
|
|
356
526
|
>
|
|
357
527
|
<CardContent
|
|
358
|
-
tokens={{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
fullBleedContentChildrenAlign && {
|
|
362
|
-
alignSelf: fullBleedContentChildrenAlign
|
|
363
|
-
})
|
|
364
|
-
}}
|
|
528
|
+
tokens={getCardContentTokens(tokens, {
|
|
529
|
+
fullBleedContentChildrenAlign
|
|
530
|
+
})}
|
|
365
531
|
variant={variant}
|
|
366
532
|
withFooter={hasFooter}
|
|
367
533
|
>
|
|
@@ -372,7 +538,7 @@ const Card = React.forwardRef(
|
|
|
372
538
|
<ConditionalWrapper
|
|
373
539
|
WrapperComponent={DynamicWidthContainer}
|
|
374
540
|
wrapperProps={imageWrapperStyleProps}
|
|
375
|
-
condition={isImageWidthAdjustable}
|
|
541
|
+
condition={isImageWidthAdjustable || isHorizontalFullBleed || isVerticalFullBleed}
|
|
376
542
|
>
|
|
377
543
|
<FullBleedContent
|
|
378
544
|
borderRadius={fullBleedBorderRadius}
|
|
@@ -393,7 +559,7 @@ const Card = React.forwardRef(
|
|
|
393
559
|
}
|
|
394
560
|
)
|
|
395
561
|
|
|
396
|
-
const positionValues =
|
|
562
|
+
const positionValues = Object.values(POSITION)
|
|
397
563
|
const alignValues = ['start', 'end', 'center', 'stretch']
|
|
398
564
|
const PositionedFullBleedContentPropType = PropTypes.shape({
|
|
399
565
|
position: responsiveProps.getTypeOptionallyByViewport(PropTypes.oneOf(positionValues)).isRequired,
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
useInputValue,
|
|
9
9
|
useResponsiveProp,
|
|
10
10
|
withLinkRouter,
|
|
11
|
-
variantProp
|
|
11
|
+
variantProp,
|
|
12
|
+
resolveContentMaxWidth,
|
|
13
|
+
useTheme
|
|
12
14
|
} from '@telus-uds/components-base'
|
|
13
15
|
import styled from 'styled-components'
|
|
14
16
|
import { htmlAttrs, scrollToAnchor } from '../utils'
|
|
@@ -26,6 +28,15 @@ const Heading = styled.div({
|
|
|
26
28
|
'> *': { display: 'contents', letterSpacing: 0 }
|
|
27
29
|
})
|
|
28
30
|
|
|
31
|
+
const ContentWrapper = styled.div(({ maxWidth }) => ({
|
|
32
|
+
width: '100%',
|
|
33
|
+
...(maxWidth && {
|
|
34
|
+
maxWidth,
|
|
35
|
+
marginLeft: 'auto',
|
|
36
|
+
marginRight: 'auto'
|
|
37
|
+
})
|
|
38
|
+
}))
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
41
|
* NavigationBar can be used to allow customers to consistently navigate across
|
|
31
42
|
* key pages within a specific product line
|
|
@@ -44,11 +55,17 @@ const NavigationBar = React.forwardRef(
|
|
|
44
55
|
linkRouterProps,
|
|
45
56
|
tokens,
|
|
46
57
|
variant,
|
|
58
|
+
contentMaxWidth,
|
|
47
59
|
...rest
|
|
48
60
|
},
|
|
49
61
|
ref
|
|
50
62
|
) => {
|
|
51
63
|
const { currentValue, setValue } = useInputValue({ value, initialValue: selectedId, onChange })
|
|
64
|
+
const { themeOptions } = useTheme()
|
|
65
|
+
|
|
66
|
+
const contentWidthValue = useResponsiveProp(contentMaxWidth)
|
|
67
|
+
const responsiveWidth = useResponsiveProp(themeOptions?.contentMaxWidth)
|
|
68
|
+
const maxWidth = resolveContentMaxWidth(contentWidthValue, responsiveWidth)
|
|
52
69
|
|
|
53
70
|
useHash(
|
|
54
71
|
(hash, event) => {
|
|
@@ -149,7 +166,7 @@ const NavigationBar = React.forwardRef(
|
|
|
149
166
|
}
|
|
150
167
|
}, [openSubMenuId, handleMouseDown])
|
|
151
168
|
|
|
152
|
-
|
|
169
|
+
const stackView = (
|
|
153
170
|
<StackView
|
|
154
171
|
accessibilityRole={accessibilityRole}
|
|
155
172
|
direction={direction}
|
|
@@ -215,7 +232,6 @@ const NavigationBar = React.forwardRef(
|
|
|
215
232
|
key={itemId}
|
|
216
233
|
href={href}
|
|
217
234
|
onClick={handleClick}
|
|
218
|
-
// TODO: refactor to pass selected ID via context
|
|
219
235
|
selectedId={currentValue}
|
|
220
236
|
index={index}
|
|
221
237
|
LinkRouter={ItemLinkRouter}
|
|
@@ -236,6 +252,8 @@ const NavigationBar = React.forwardRef(
|
|
|
236
252
|
)}
|
|
237
253
|
</StackView>
|
|
238
254
|
)
|
|
255
|
+
|
|
256
|
+
return maxWidth ? <ContentWrapper maxWidth={maxWidth}>{stackView}</ContentWrapper> : stackView
|
|
239
257
|
}
|
|
240
258
|
)
|
|
241
259
|
|
|
@@ -302,7 +320,24 @@ NavigationBar.propTypes = {
|
|
|
302
320
|
/**
|
|
303
321
|
* Variant configuration
|
|
304
322
|
*/
|
|
305
|
-
variant: variantProp.propType
|
|
323
|
+
variant: variantProp.propType,
|
|
324
|
+
/**
|
|
325
|
+
* The maximum width of the content in the NavigationBar.
|
|
326
|
+
* This prop accepts responsive values for different viewports. If a number is provided,
|
|
327
|
+
* it will be the max content width for the desired viewport.
|
|
328
|
+
* - `xs`: 'max' | 'full' | <number>
|
|
329
|
+
* - `sm`: 'max' | 'full' | <number>
|
|
330
|
+
* - `md`: 'max' | 'full' | <number>
|
|
331
|
+
* - `lg`: 'max' | 'full' | <number>
|
|
332
|
+
* - `xl`: 'max' | 'full' | <number>
|
|
333
|
+
*/
|
|
334
|
+
contentMaxWidth: PropTypes.shape({
|
|
335
|
+
xl: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
|
|
336
|
+
lg: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
|
|
337
|
+
md: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
|
|
338
|
+
sm: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
|
|
339
|
+
xs: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number])
|
|
340
|
+
})
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
export default NavigationBar
|
|
@@ -11,6 +11,34 @@ import NavigationItem from './NavigationItem'
|
|
|
11
11
|
import useOverlaidPosition from '../utils/useOverlaidPosition'
|
|
12
12
|
import resolveItemSelection from './resolveItemSelection'
|
|
13
13
|
|
|
14
|
+
const MIN_WIDTH = 192
|
|
15
|
+
const MAX_WIDTH = 289
|
|
16
|
+
const DEFAULT_OFFSETS = { offsets: { vertical: 4 } }
|
|
17
|
+
const XS_ALIGN = { top: 'bottom', left: 'left' }
|
|
18
|
+
const SM_ALIGN = { top: 'bottom', right: 'right' }
|
|
19
|
+
const LG_ALIGN = { top: 'bottom', center: 'center' }
|
|
20
|
+
|
|
21
|
+
const getResponsiveBreakpoints = (sourceWidth) => ({
|
|
22
|
+
xs: {
|
|
23
|
+
...DEFAULT_OFFSETS,
|
|
24
|
+
align: XS_ALIGN,
|
|
25
|
+
minWidth: sourceWidth,
|
|
26
|
+
maxWidth: sourceWidth
|
|
27
|
+
},
|
|
28
|
+
sm: {
|
|
29
|
+
...DEFAULT_OFFSETS,
|
|
30
|
+
align: SM_ALIGN,
|
|
31
|
+
minWidth: sourceWidth,
|
|
32
|
+
maxWidth: sourceWidth
|
|
33
|
+
},
|
|
34
|
+
lg: {
|
|
35
|
+
...DEFAULT_OFFSETS,
|
|
36
|
+
align: LG_ALIGN,
|
|
37
|
+
minWidth: MIN_WIDTH,
|
|
38
|
+
maxWidth: MAX_WIDTH
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
14
42
|
/**
|
|
15
43
|
* A NavigationItem that opens or closes a Listbox of other NavigationItems.
|
|
16
44
|
*
|
|
@@ -36,18 +64,11 @@ const NavigationSubMenu = React.forwardRef(
|
|
|
36
64
|
ref
|
|
37
65
|
) => {
|
|
38
66
|
const focusTrapRef = React.useRef()
|
|
67
|
+
const [sourceWidth, setSourceWidth] = React.useState(0)
|
|
39
68
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
xs: { ...defaultOffsets, align: { top: 'bottom', left: 'left' }, minWidth: maxWidth },
|
|
44
|
-
sm: { ...defaultOffsets, align: { top: 'bottom', right: 'right' }, minWidth: maxWidth },
|
|
45
|
-
lg: {
|
|
46
|
-
...defaultOffsets,
|
|
47
|
-
align: { top: 'bottom', center: 'center' },
|
|
48
|
-
minWidth: 192
|
|
49
|
-
}
|
|
50
|
-
})
|
|
69
|
+
const { align, offsets, minWidth, maxWidth } = useResponsiveProp(
|
|
70
|
+
getResponsiveBreakpoints(sourceWidth)
|
|
71
|
+
)
|
|
51
72
|
|
|
52
73
|
const { overlaidPosition, sourceRef, targetRef, onTargetLayout, isReady } = useOverlaidPosition(
|
|
53
74
|
{
|
|
@@ -64,6 +85,12 @@ const NavigationSubMenu = React.forwardRef(
|
|
|
64
85
|
|
|
65
86
|
const { icoMenu } = useThemeTokens('NavigationBar', tokens, {}, { expanded: isOpen })
|
|
66
87
|
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
sourceRef.current?.measureInWindow((_, __, width) => {
|
|
90
|
+
setSourceWidth(width)
|
|
91
|
+
})
|
|
92
|
+
}, [isOpen, sourceRef])
|
|
93
|
+
|
|
67
94
|
return (
|
|
68
95
|
<>
|
|
69
96
|
<NavigationItem
|
|
@@ -7,7 +7,7 @@ const collapseItems = (items, selectedId) => {
|
|
|
7
7
|
// Give the root item the label of the current active link
|
|
8
8
|
// (or the first item if for some reason there's no match on the selectedId)
|
|
9
9
|
let rootLabel = items[0].label
|
|
10
|
-
const isSelected = ({ label, id }) => selectedId === id ?? label
|
|
10
|
+
const isSelected = ({ label, id }) => selectedId === (id ?? label)
|
|
11
11
|
|
|
12
12
|
// Linter doesn't like for loops, simulate loop that breaks
|
|
13
13
|
items.some((item) => {
|