From 1eac2e5526baefb5a03e4ab77101b7721121045c Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Wed, 10 Jun 2026 11:00:36 +0300 Subject: [PATCH 01/13] Added support for `labelIcons` and `secondaryLabelIcons` --- Card/Card.js | 73 +++++++++++++++++++++++-- Card/Card.module.less | 30 ++++++++-- samples/sampler/stories/default/Card.js | 23 +++++++- styles/variables.less | 2 + 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index cfc0a19f2..7ec957c67 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -17,11 +17,13 @@ import {forProp, forward, handle, not} from '@enact/core/handle'; import kind from '@enact/core/kind'; +import {mapAndFilterChildren} from '@enact/core/util'; import Spottable from '@enact/spotlight/Spottable'; import {Card as UiCard} from '@enact/ui/Card'; -import {Cell, Row} from '@enact/ui/Layout'; +import {Cell, Column, Row} from '@enact/ui/Layout'; import Touchable from '@enact/ui/Touchable'; import ri from '@enact/ui/resolution'; +import {cloneElement} from 'react'; import PropTypes from 'prop-types'; import compose from 'ramda/src/compose'; @@ -196,6 +198,19 @@ const CardBase = kind({ */ label: PropTypes.string, + /** + * Icons to be included with the secondary caption. + * + * Typically, up to 3 icons are used. + * + * @type {Element|Element[]} + * @public + */ + labelIcons: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + /** * The layout orientation of the component. * @@ -262,6 +277,19 @@ const CardBase = kind({ */ secondaryLabel: PropTypes.string, + /** + * Icons to be included with the ternary caption. + * + * Typically, up to 3 icons are used. + * + * @type {Element|Element[]} + * @public + */ + secondaryLabelIcons: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + /** * Applies a selected visual effect to the image. * @@ -323,9 +351,16 @@ const CardBase = kind({ return ariaLabel || `${children || ''}${label ? ` ${label}` : ''}${secondaryLabel ? ` ${secondaryLabel}` : ''}${selected ? ' ' + $L('Selected') : ''}`; }, captionOverlay: ({captionOverlay, captionOverlayOnFocus}) => captionOverlay || captionOverlayOnFocus, - children: ({captionOverlay, captionOverlayOnFocus, centered, children, css, 'data-index': index, imageIconSrc, label, orientation, progress, secondaryLabel, showProgressBar, splitCaption, withoutMarquee}) => { + children: ({captionOverlay, captionOverlayOnFocus, centered, children, css, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, secondaryLabel, secondaryLabelIcons, showProgressBar, splitCaption, withoutMarquee}) => { const hasImageIcon = imageIconSrc && orientation === 'vertical'; const alignment = centered && !imageIconSrc ? {alignment: 'center'} : null; + const getLabelIcons = (icons, key) => { + return mapAndFilterChildren(icons, (labelIcon, idx) => ( + + {cloneElement(labelIcon, {className: css.labelIcon})} + + )) || null; + }; const captions = ( @@ -340,15 +375,39 @@ const CardBase = kind({ {withoutMarquee ? (
{children}
- {typeof label !== 'undefined' ?
{label}
: null} - {typeof secondaryLabel !== 'undefined' ?
{secondaryLabel}
: null} + + {typeof label !== 'undefined' ? ( + + {getLabelIcons(labelIcons, 'labelIcons')} +
{label}
+
+ ) : null} + {typeof secondaryLabel !== 'undefined' ? ( + + {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons')} +
{secondaryLabel}
+
+ ) : null} +
{showProgressBar ? : null}
) : ( {children} - {typeof label !== 'undefined' ? {label} : null} - {typeof secondaryLabel !== 'undefined' ? {secondaryLabel} : null} + + {typeof label !== 'undefined' ? ( + + {getLabelIcons(labelIcons, 'labelIcons')} + {label} + + ) : null} + {typeof secondaryLabel !== 'undefined' ? ( + + {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons')} + {secondaryLabel} + + ) : null} + {showProgressBar ? : null} )} @@ -404,8 +463,10 @@ const CardBase = kind({ delete rest.captionOverlayOnFocus; delete rest.centered; delete rest.label; + delete rest.labelIcons; delete rest.progress; delete rest.secondaryLabel; + delete rest.secondaryLabelIcons; delete rest.showProgressBar; delete rest.imageIconSrc; delete rest.hasContainer; diff --git a/Card/Card.module.less b/Card/Card.module.less index a6270519a..dc6e1a4b7 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -94,10 +94,29 @@ font-weight: @lime-card-caption-font-weight; } - .label { - font-size: @lime-card-label-font-size; - line-height: @lime-card-label-line-height; - font-weight: @lime-card-label-font-weight; + .labels { + gap: @lime-card-labels-spacing; + + .labelContainer { + align-items: center; + gap: @lime-card-label-container-spacing; + + .label { + font-size: @lime-card-label-font-size; + line-height: @lime-card-label-line-height; + font-weight: @lime-card-label-font-weight; + } + + &:has(.labelIcon) { + .label { + margin-top: '~calc(@{lime-icon-small-size} - @{lime-card-label-line-height})'; + } + + .labelIcon { + margin: 0; + } + } + } } &.selected { @@ -430,7 +449,8 @@ color: @lime-card-label-main-focused-color; } - .label { + .label, + .labelIcon { color: @lime-card-label-sub-focused-color; } } diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index 9382c2145..76a2f6dc1 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -1,5 +1,6 @@ import {Card, CardBase} from '@enact/limestone/Card'; import icons from '@enact/limestone/Icon/IconList'; +import {Icon} from '@enact/limestone/Icon'; import {mergeComponentMetadata} from '@enact/storybook-utils'; import {action} from '@enact/storybook-utils/addons/actions'; import {boolean, number, object, select, text} from '@enact/storybook-utils/addons/controls'; @@ -19,9 +20,25 @@ const generateImageSrc = (color) => { }; const iconsList = Object.keys(icons).sort(); +const randomIcon = iconsList[Math.floor(Math.random() * iconsList.length)]; const prop = { - orientation: ['horizontal', 'vertical'] + orientation: ['horizontal', 'vertical'], + icons: { + 'no icons': null, + '1 icon': [ + {randomIcon} + ], + '2 icons': [ + {randomIcon}, + {randomIcon} + ], + '3 icons': [ + {randomIcon}, + {randomIcon}, + {randomIcon} + ] + } }; export default { @@ -38,6 +55,7 @@ export const _Card = (args) => ( disabled={args['disabled']} fitImage={args['fitImage']} icon={args['icon']} + labelIcons={prop.icons[args['labelIcons']]} imageIconSrc={args['imageIconSrc']} imageSize={args['imageSize']} hasContainer={args['hasContainer']} @@ -51,6 +69,7 @@ export const _Card = (args) => ( secondaryBadgeSrc={args['secondaryBadgeSrc']} // eslint-disable-next-line no-undefined secondaryLabel={args['secondaryLabel'] ? args['secondaryLabel'] : undefined} + secondaryLabelIcons={prop.icons[args['secondaryLabelIcons']]} selected={args['selected']} showProgressBar={args['showProgressBar']} splitCaption={args['splitCaption']} @@ -73,12 +92,14 @@ object('imageIconSrc', _Card, Config, generateImageSrc('0084ff')); object('imageSize', _Card, Config); boolean('hasContainer', _Card, Config); text('label', _Card, Config, 'Card label'); +select('labelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); select('orientation', _Card, prop.orientation, Config); object('primaryBadgeSrc', _Card, Config, generateImageSrc('ff6d78')); number('progress', _Card, Config, 0.5); boolean('roundedImage', _Card, Config); object('secondaryBadgeSrc', _Card, Config, generateImageSrc('ffc600')); text('secondaryLabel', _Card, Config, 'Card secondary label'); +select('secondaryLabelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); boolean('selected', _Card, Config); boolean('showProgressBar', _Card, Config); boolean('splitCaption', _Card, Config); diff --git a/styles/variables.less b/styles/variables.less index 51d800d3a..c3636e8e1 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -471,8 +471,10 @@ @lime-card-badge-right: var(--primitive-spacing-24); @lime-card-caption-line-height: 84px; @lime-card-caption-line-height-large: 100px; +@lime-card-label-container-spacing: var(--primitive-spacing-12); @lime-card-label-line-height: 60px; @lime-card-label-line-height-large: 70px; +@lime-card-labels-spacing: var(--primitive-spacing-6); @lime-card-split-caption-label-padding: 0 var(--primitive-spacing-48); @lime-card-split-caption-lower-label-padding: var(--primitive-spacing-18); @lime-card-vertical-captions-padding: var(--primitive-spacing-36) 0; From ceedf183593d57702f70da2e59ee62818401afd9 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Thu, 11 Jun 2026 10:28:57 +0300 Subject: [PATCH 02/13] Added support for `centeredTitle` --- Card/Card.js | 28 ++++++++++++++------ Card/Card.module.less | 34 ++++++++++++++++++++++++- samples/sampler/stories/default/Card.js | 16 +++++++----- styles/variables.less | 2 +- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index 7ec957c67..aba44eb6f 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -104,6 +104,15 @@ const CardBase = kind({ */ centered: PropTypes.bool, + /** + * Centers the title and `imageIconSrc` horizontally and vertically. + * It only applies when `captionOverlay` or `captionOverlayOnFocus` is `true`. + * + * @type {Boolean} + * @public + */ + centeredTitle: PropTypes.bool, + /** * The primary caption displayed with the image. * @@ -351,9 +360,11 @@ const CardBase = kind({ return ariaLabel || `${children || ''}${label ? ` ${label}` : ''}${secondaryLabel ? ` ${secondaryLabel}` : ''}${selected ? ' ' + $L('Selected') : ''}`; }, captionOverlay: ({captionOverlay, captionOverlayOnFocus}) => captionOverlay || captionOverlayOnFocus, - children: ({captionOverlay, captionOverlayOnFocus, centered, children, css, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, secondaryLabel, secondaryLabelIcons, showProgressBar, splitCaption, withoutMarquee}) => { + children: ({captionOverlay, captionOverlayOnFocus, centered, centeredTitle, children, css, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, secondaryLabel, secondaryLabelIcons, showProgressBar, splitCaption, withoutMarquee}) => { + const isCenteredTitle = (captionOverlay || captionOverlayOnFocus) && orientation === 'vertical' && centeredTitle; const hasImageIcon = imageIconSrc && orientation === 'vertical'; - const alignment = centered && !imageIconSrc ? {alignment: 'center'} : null; + const alignment = (centered && !imageIconSrc) || isCenteredTitle ? {alignment: 'center'} : null; + const CaptionsComponent = isCenteredTitle ? Column : Row; const getLabelIcons = (icons, key) => { return mapAndFilterChildren(icons, (labelIcon, idx) => ( @@ -363,7 +374,7 @@ const CardBase = kind({ }; const captions = ( - + {hasImageIcon ? ( ) : null} {withoutMarquee ? ( - +
{children}
{typeof label !== 'undefined' ? ( @@ -392,7 +403,7 @@ const CardBase = kind({ {showProgressBar ? : null}
) : ( - + {children} {typeof label !== 'undefined' ? ( @@ -408,10 +419,10 @@ const CardBase = kind({
) : null} - {showProgressBar ? : null} + {(showProgressBar && !isCenteredTitle) ? : null}
)} -
+ ); const splitCaptions = ( @@ -447,9 +458,10 @@ const CardBase = kind({ selectedCaptions ); }, - className: ({captionOverlay, captionOverlayOnFocus, icon, label, pressed, roundedImage, hasContainer, orientation, secondaryLabel, styler}) => styler.append({ + className: ({captionOverlay, captionOverlayOnFocus, centeredTitle, icon, label, pressed, roundedImage, hasContainer, orientation, secondaryLabel, styler}) => styler.append({ captionOverlay: captionOverlay && orientation === 'vertical', captionOverlayOnFocus: !captionOverlay && captionOverlayOnFocus && orientation === 'vertical', + centeredTitle, pressed, roundedImage, hasContainer: (orientation === 'horizontal') || (hasContainer && !captionOverlay && !captionOverlayOnFocus), diff --git a/Card/Card.module.less b/Card/Card.module.less index dc6e1a4b7..c20925269 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -96,6 +96,7 @@ .labels { gap: @lime-card-labels-spacing; + height: fit-content; .labelContainer { align-items: center; @@ -108,8 +109,11 @@ } &:has(.labelIcon) { + height: @lime-icon-small-size; + will-change: transform; + .label { - margin-top: '~calc(@{lime-icon-small-size} - @{lime-card-label-line-height})'; + margin-top: ~"calc(@{lime-icon-small-size} - @{lime-card-label-line-height})"; } .labelIcon { @@ -196,6 +200,30 @@ padding: @lime-card-vertical-caption-overlay-captions-padding; } + &.captionOverlay.centeredTitle { + .captions { + gap: 24px; + justify-content: center; + } + + .captionCell { + max-height: fit-content; + } + + .children { + height: 100%; + } + + .labels { + display: none; + } + + .imageIcon { + width: @lime-card-selection-icon-size; + height: @lime-card-selection-icon-size; + } + } + &.captionOverlay { &.splitCaption { .caption { @@ -395,6 +423,10 @@ background: @lime-card-captions-gradient-color; } + &.centeredTitle .captions { + background: transparent; + } + &.roundedImage .captions { border-bottom-left-radius: @lime-card-border-radius; border-bottom-right-radius: @lime-card-border-radius; diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index 76a2f6dc1..4ba8b31ac 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -20,23 +20,23 @@ const generateImageSrc = (color) => { }; const iconsList = Object.keys(icons).sort(); -const randomIcon = iconsList[Math.floor(Math.random() * iconsList.length)]; +const randomIcon = () => iconsList[Math.floor(Math.random() * iconsList.length)]; const prop = { orientation: ['horizontal', 'vertical'], icons: { 'no icons': null, '1 icon': [ - {randomIcon} + {randomIcon()} ], '2 icons': [ - {randomIcon}, - {randomIcon} + {randomIcon()}, + {randomIcon()} ], '3 icons': [ - {randomIcon}, - {randomIcon}, - {randomIcon} + {randomIcon()}, + {randomIcon()}, + {randomIcon()} ] } }; @@ -52,6 +52,7 @@ export const _Card = (args) => ( captionOverlay={args['captionOverlay']} captionOverlayOnFocus={args['captionOverlayOnFocus']} centered={args['centered']} + centeredTitle={args['centeredTitle']} disabled={args['disabled']} fitImage={args['fitImage']} icon={args['icon']} @@ -84,6 +85,7 @@ text('aria-label', _Card, Config); boolean('captionOverlay', _Card, Config); boolean('captionOverlayOnFocus', _Card, Config); boolean('centered', _Card, Config); +boolean('centeredTitle', _Card, Config); text('children', _Card, Config, 'Card Caption'); boolean('disabled', _Card, Config); boolean('fitImage', _Card, Config); diff --git a/styles/variables.less b/styles/variables.less index 5117261e4..5a6d282fd 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -484,7 +484,7 @@ @lime-card-vertical-image-icon-margin: 0; @lime-card-vertical-caption-overlay-captions-padding: var(--primitive-spacing-36) var(--primitive-spacing-48); @lime-card-vertical-has-container-captions-padding: var(--primitive-spacing-36) var(--primitive-spacing-48); -@lime-card-horizontal-width: 1320px; +@lime-card-horizontal-width: 1192px; @lime-card-horizontal-height: 336px; @lime-card-horizontal-captions-padding: var(--primitive-spacing-48); From 2b9d10adc8567f92f5ee20a03f91b71af75ed346 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Fri, 12 Jun 2026 15:28:25 +0300 Subject: [PATCH 03/13] Added support for `media duration`, `duration overlay`, `progress bar overlay` and `captions images icons` --- Card/Card.js | 173 +++++++++++++++++++++--- Card/Card.module.less | 95 +++++++++++-- samples/sampler/stories/default/Card.js | 16 +++ styles/variables.less | 14 ++ 4 files changed, 274 insertions(+), 24 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index aba44eb6f..1b19fb531 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -46,6 +46,44 @@ const getDefaultImageSize = (orientation) => { return sizes[orientation]; }; +const getLabelIcons = (icons, key) => { + return mapAndFilterChildren(icons, (labelIcon, idx) => ( + + {cloneElement(labelIcon, {className: componentCss.labelIcon})} + + )) || null; +}; + +const getCaptionImageIcons = (imageSrc, key) => { + return imageSrc.map((src, idx) => ( + + )) || null; +}; + +const formatDuration = (duration) => { + if (duration < 0) return "00:00"; + + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = duration % 60; + + const mm = String(minutes).padStart(2, '0'); + const ss = String(seconds).padStart(2, '0'); + + if (hours > 0) { + const hh = String(hours).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; + } + + return `${mm}:${ss}`; +}; + /** * A Limestone styled base component for {@link limestone/Card.Card|Card}. * @@ -78,6 +116,51 @@ const CardBase = kind({ */ 'aria-label': PropTypes.string, + /** + * Source for the image icon. + * + * String value or Object of values used to determine which image will appear on + * a specific screenSize. This prop is only used when `orientation` is `'vertical'`. + * + * @type {String|Object} + * @public + */ + captionImageIconsSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * The size of the caption images. + * + * The following properties should be provided: + * * `height` - The height of the image + * * `width` - The width of the image + + * @type {Object} + * @default {height: 432, width: 768} + * @public + */ + captionImageSize: PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number + }), + + /** + * Determines whether the caption will overflow the card container or not. + * It only applies when `orientation` is `'vertical'` and `captionOverlay` and `captionOverlayOnFocus` is `false`. + * + * @type {Boolean} + * @public + */ + captionOverflow: PropTypes.bool, + + /** + * Determines whether the caption will overflow the card container and show only on card focus. + * It only applies when `orientation` is `'vertical'` and `captionOverlay` and `captionOverlayOnFocus` is `false`. + * + * @type {Boolean} + * @public + */ + captionOverflowOnFocus: PropTypes.bool, + /** * Determines whether the caption will be placed over the image or not. * It only applies when `orientation` is `'vertical'`. @@ -146,6 +229,22 @@ const CardBase = kind({ */ disabled: PropTypes.bool, + /** + * Media's entire duration in seconds. + * + * @type {Number} + * @public + */ + duration: PropTypes.number, + + /** + * Determines whether the `Duration` will be placed over the image or not. + * + * @type {Boolean} + * @public + */ + durationOverlay: PropTypes.bool, + /** * Fits the image to its height and width and positions it on the center of the Card. * @@ -262,6 +361,14 @@ const CardBase = kind({ */ progress: PropTypes.number, + /** + * Determines whether the `ProgressBar` will be placed over the image or not. + * + * @type {Boolean} + * @public + */ + progressBarOverlay: PropTypes.bool, + /** * Set to `true` to display the image with rounded corners. * @@ -307,6 +414,14 @@ const CardBase = kind({ */ selected: PropTypes.bool, + /** + * Activates the 'Duration'. + * + * @type {Boolean} + * @public + */ + showDuration: PropTypes.bool, + /** * Activates the 'ProgressBar'. * @@ -360,18 +475,12 @@ const CardBase = kind({ return ariaLabel || `${children || ''}${label ? ` ${label}` : ''}${secondaryLabel ? ` ${secondaryLabel}` : ''}${selected ? ' ' + $L('Selected') : ''}`; }, captionOverlay: ({captionOverlay, captionOverlayOnFocus}) => captionOverlay || captionOverlayOnFocus, - children: ({captionOverlay, captionOverlayOnFocus, centered, centeredTitle, children, css, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, secondaryLabel, secondaryLabelIcons, showProgressBar, splitCaption, withoutMarquee}) => { + children: ({captionImageIconsSrc, captionOverlay, captionOverlayOnFocus, centered, centeredTitle, children, css, duration, durationOverlay, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, progressBarOverlay, secondaryLabel, secondaryLabelIcons, showDuration, showProgressBar, splitCaption, withoutMarquee}) => { const isCenteredTitle = (captionOverlay || captionOverlayOnFocus) && orientation === 'vertical' && centeredTitle; const hasImageIcon = imageIconSrc && orientation === 'vertical'; + const hasCaptionImageIcons = captionImageIconsSrc && orientation === 'vertical'; const alignment = (centered && !imageIconSrc) || isCenteredTitle ? {alignment: 'center'} : null; const CaptionsComponent = isCenteredTitle ? Column : Row; - const getLabelIcons = (icons, key) => { - return mapAndFilterChildren(icons, (labelIcon, idx) => ( - - {cloneElement(labelIcon, {className: css.labelIcon})} - - )) || null; - }; const captions = ( @@ -399,8 +508,16 @@ const CardBase = kind({
{secondaryLabel}
) : null} + {hasCaptionImageIcons ? ( + + {getCaptionImageIcons(captionImageIconsSrc, 'captionImageIcons')} + + ) : null} - {showProgressBar ? : null} + {(showProgressBar && !progressBarOverlay && !isCenteredTitle) ? : null} + {(showDuration && !durationOverlay && !showProgressBar) ? ( +
{formatDuration(duration)}
+ ) : null}
) : ( @@ -418,8 +535,16 @@ const CardBase = kind({ {secondaryLabel} ) : null} + {hasCaptionImageIcons ? ( + + {getCaptionImageIcons(captionImageIconsSrc, 'captionImageIcons')} + + ) : null} - {(showProgressBar && !isCenteredTitle) ? : null} + {(showProgressBar && !progressBarOverlay && !isCenteredTitle) ? : null} + {(showDuration && !durationOverlay && !showProgressBar) ? ( +
{formatDuration(duration)}
+ ) : null}
)} @@ -458,28 +583,36 @@ const CardBase = kind({ selectedCaptions ); }, - className: ({captionOverlay, captionOverlayOnFocus, centeredTitle, icon, label, pressed, roundedImage, hasContainer, orientation, secondaryLabel, styler}) => styler.append({ + className: ({captionOverflow, captionOverflowOnFocus, captionOverlay, captionOverlayOnFocus, centeredTitle, durationOverlay, icon, label, pressed, progressBarOverlay, roundedImage, hasContainer, orientation, secondaryLabel, styler}) => styler.append({ + captionOverflow: captionOverflow && orientation === 'vertical' && !captionOverlay && !captionOverlayOnFocus, + captionOverflowOnFocus: !captionOverflow && captionOverflowOnFocus && orientation === 'vertical' && !captionOverlay && !captionOverlayOnFocus, captionOverlay: captionOverlay && orientation === 'vertical', captionOverlayOnFocus: !captionOverlay && captionOverlayOnFocus && orientation === 'vertical', centeredTitle, + durationOverlay, pressed, roundedImage, hasContainer: (orientation === 'horizontal') || (hasContainer && !captionOverlay && !captionOverlayOnFocus), hasLabel: (orientation === 'vertical') && (label && secondaryLabel), - isCheckIcon: icon === 'check' + isCheckIcon: icon === 'check', + progressBarOverlay }), + showDuration: ({showDuration, showProgressBar, durationOverlay}) => showDuration && durationOverlay && !showProgressBar, + showProgressBar: ({showProgressBar, progressBarOverlay}) => showProgressBar && progressBarOverlay, splitCaption: ({captionOverlay, captionOverlayOnFocus, splitCaption}) => (captionOverlay || captionOverlayOnFocus) && splitCaption }, - render: ({css, disabled, icon, imageSize, primaryBadgeSrc, secondaryBadgeSrc, style, ...rest}) => { + render: ({captionImageSize, css, disabled, icon, imageSize, primaryBadgeSrc, secondaryBadgeSrc, showDuration, duration, progress, showProgressBar, style, ...rest}) => { + delete rest.captionImageIconsSrc; + delete rest.captionOverflow; + delete rest.captionOverflowOnFocus; delete rest.captionOverlayOnFocus; delete rest.centered; + delete rest.centeredTitle; delete rest.label; delete rest.labelIcons; - delete rest.progress; delete rest.secondaryLabel; delete rest.secondaryLabelIcons; - delete rest.showProgressBar; delete rest.imageIconSrc; delete rest.hasContainer; delete rest.pressed; @@ -506,12 +639,20 @@ const CardBase = kind({
{icon}
+ {showDuration ? ( +
{formatDuration(duration)}
+ ) : null} + {showProgressBar ? ( + + ) : null} } style={{ ...style, '--card-image-height': ri.scaleToRem(imageSize?.height ?? defaultImageSize.height), - '--card-image-width': ri.scaleToRem(imageSize?.width ?? defaultImageSize.width) + '--card-image-width': ri.scaleToRem(imageSize?.width ?? defaultImageSize.width), + ...(captionImageSize?.height && {'--caption-image-height': ri.scaleToRem(captionImageSize.height)}), + ...(captionImageSize?.width && {'--caption-image-width': ri.scaleToRem(captionImageSize.width)}) }} /> ); diff --git a/Card/Card.module.less b/Card/Card.module.less index c20925269..d7d7ca46d 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -178,6 +178,17 @@ height: auto; .lime-focus-out-motion(); + .captionImageIconsContainer { + margin-top: @lime-card-caption-image-icon-margin; + gap: @lime-card-caption-image-icon-spacing; + + .captionImageIcon { + height: var(--caption-image-height, @lime-card-caption-image-icon-size); + width: var(--caption-image-width, @lime-card-caption-image-icon-size); + margin: 0; + } + } + .captions { padding: @lime-card-vertical-captions-padding; display: flex; @@ -196,13 +207,15 @@ margin: @lime-card-vertical-image-icon-margin; } - &.captionOverlay .captions { - padding: @lime-card-vertical-caption-overlay-captions-padding; + &.captionOverlay { + .captions { + padding: @lime-card-vertical-caption-overlay-captions-padding; + } } &.captionOverlay.centeredTitle { .captions { - gap: 24px; + gap: @lime-card-vertical-captions-centered-title-gap; justify-content: center; } @@ -250,8 +263,20 @@ padding: @lime-card-vertical-has-container-captions-padding; } - &:not(.hasContainer) .image { - z-index: 1; + &:not(.hasContainer) { + .image { + z-index: 1; + } + + &.captionOverflow, &.captionOverflowOnFocus { + .captions { + width: @lime-card-vertical-captions-overflow-width; + } + } + + &.captionOverflowOnFocus .captions { + display: none; + } } &.selected:not(.hasContainer) .selectionContainer { @@ -268,8 +293,46 @@ padding: @lime-card-horizontal-captions-padding; } + .duration { + transform: none; + transition: none; + } + .image { width: var(--card-image-width); + + .duration { + transform: none; + transition: none; + } + } + } + + &.vertical, + &.horizontal { + .duration { + font-size: @lime-card-duration-font-size; + line-height: @lime-card-duration-line-height; + font-weight: @lime-card-duration-font-weight; + padding: @lime-card-duration-padding; + border-radius: @lime-card-duration-border-radius; + width: fit-content; + + .margin-start-end(auto, 0); + } + + &.durationOverlay .duration { + position: absolute; + bottom: @lime-card-vertical-duration-spacing; + right: @lime-card-vertical-duration-spacing; + } + + &.progressBarOverlay .progress { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: ~"calc(100% - @{lime-card-vertical-progress-spacing} * 2)"; } } @@ -287,8 +350,14 @@ visibility: visible; } - &:not(.hasContainer) .selectionContainer { - z-index: 5; + &:not(.hasContainer) { + .selectionContainer { + z-index: 5; + } + + &.captionOverflowOnFocus .captions { + display: flex; + } } } }); @@ -347,6 +416,11 @@ line-height: @lime-card-caption-line-height-large; } + .duration { + font-size: @lime-card-duration-font-size-large; + line-height: @lime-card-duration-line-height-large; + } + .label { font-size: @lime-card-label-font-size-large; line-height: @lime-card-label-line-height-large; @@ -373,15 +447,20 @@ &.vertical { &:not(.captionOverlay):not(.captionOverlayOnFocus):is(.hasLabel) .image { - height:~"calc(var(--card-image-height) - 36px)"; + height: ~"calc(var(--card-image-height) - 36px)"; } } }); // Skin colors .applySkins({ + .duration { + background-color: ~"color(from" var(--semantic-color-surface-overlay-default) ~"srgb r g b / 0.5)"; + } + .selectionContainer { border-color: transparent; + .selectionIcon { color: transparent; } diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index 4ba8b31ac..bbe092186 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -49,11 +49,17 @@ export default { export const _Card = (args) => ( ( orientation={args['orientation']} primaryBadgeSrc={args['primaryBadgeSrc']} progress={args['progress']} + progressBarOverlay={args['progressBarOverlay']} roundedImage={args['roundedImage']} secondaryBadgeSrc={args['secondaryBadgeSrc']} // eslint-disable-next-line no-undefined secondaryLabel={args['secondaryLabel'] ? args['secondaryLabel'] : undefined} secondaryLabelIcons={prop.icons[args['secondaryLabelIcons']]} selected={args['selected']} + showDuration={args['showDuration']} showProgressBar={args['showProgressBar']} splitCaption={args['splitCaption']} src={args['src']} @@ -82,12 +90,18 @@ export const _Card = (args) => ( ); text('aria-label', _Card, Config); +object('captionImageIconsSrc', _Card, Config, generateImageSrc('0084ff')); +object('captionImageSize', _Card, Config); +boolean('captionOverflow', _Card, Config); +boolean('captionOverflowOnFocus', _Card, Config); boolean('captionOverlay', _Card, Config); boolean('captionOverlayOnFocus', _Card, Config); boolean('centered', _Card, Config); boolean('centeredTitle', _Card, Config); text('children', _Card, Config, 'Card Caption'); boolean('disabled', _Card, Config); +number('duration', _Card, Config, 234); +boolean('durationOverlay', _Card, Config); boolean('fitImage', _Card, Config); select('icon', _Card, iconsList, Config); object('imageIconSrc', _Card, Config, generateImageSrc('0084ff')); @@ -98,11 +112,13 @@ select('labelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config select('orientation', _Card, prop.orientation, Config); object('primaryBadgeSrc', _Card, Config, generateImageSrc('ff6d78')); number('progress', _Card, Config, 0.5); +boolean('progressBarOverlay', _Card, Config); boolean('roundedImage', _Card, Config); object('secondaryBadgeSrc', _Card, Config, generateImageSrc('ffc600')); text('secondaryLabel', _Card, Config, 'Card secondary label'); select('secondaryLabelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); boolean('selected', _Card, Config); +boolean('showDuration', _Card, Config); boolean('showProgressBar', _Card, Config); boolean('splitCaption', _Card, Config); object('src', _Card, Config, generateImageSrc('93d371')); diff --git a/styles/variables.less b/styles/variables.less index 5a6d282fd..b2021247a 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -134,6 +134,8 @@ @lime-button-small-font-size: var(--primitive-font-size-54); @lime-card-caption-font-size: var(--primitive-font-size-60); @lime-card-caption-font-size-large: var(--primitive-font-size-72); +@lime-card-duration-font-size: var(--primitive-font-size-48); +@lime-card-duration-font-size-large: var(--primitive-font-size-58); @lime-card-label-font-size: var(--primitive-font-size-48); @lime-card-label-font-size-large: var(--primitive-font-size-58); @lime-card-split-caption-label-font-size: var(--primitive-font-size-60); @@ -281,6 +283,7 @@ // Card @lime-card-caption-font-weight: var(--primitive-font-weight-semibold); +@lime-card-duration-font-weight: var(--primitive-font-weight-semibold); @lime-card-label-font-weight: var(--primitive-font-weight-regular); @lime-card-split-caption-label-font-weight: var(--primitive-font-weight-semibold); @@ -451,6 +454,9 @@ // Card // --------------------------------------- +@lime-card-caption-image-icon-size: 96px; +@lime-card-caption-image-icon-margin: var(--primitive-spacing-24); +@lime-card-caption-image-icon-spacing: var(--primitive-spacing-24); @lime-card-margin: @lime-item-margin; @lime-card-border-radius: var(--semantic-radius-container); @lime-card-image-border-width: 6px; @@ -472,6 +478,10 @@ @lime-card-badge-right: var(--primitive-spacing-24); @lime-card-caption-line-height: 84px; @lime-card-caption-line-height-large: 100px; +@lime-card-duration-border-radius: var(--primitive-radius-12); +@lime-card-duration-line-height: 60px; +@lime-card-duration-line-height-large: 70px; +@lime-card-duration-padding: var(--primitive-spacing-6) var(--primitive-spacing-12); @lime-card-label-container-spacing: var(--primitive-spacing-12); @lime-card-label-line-height: 60px; @lime-card-label-line-height-large: 70px; @@ -479,7 +489,11 @@ @lime-card-split-caption-label-padding: 0 var(--primitive-spacing-48); @lime-card-split-caption-lower-label-padding: var(--primitive-spacing-18); @lime-card-vertical-captions-padding: var(--primitive-spacing-36) 0; +@lime-card-vertical-captions-overflow-width: 1140px; @lime-card-vertical-captions-gap: var(--primitive-spacing-48); +@lime-card-vertical-captions-centered-title-gap: var(--primitive-spacing-24); +@lime-card-vertical-duration-spacing: var(--primitive-spacing-24); +@lime-card-vertical-progress-spacing: var(--primitive-spacing-48); @lime-card-vertical-image-icon-size: 180px; @lime-card-vertical-image-icon-margin: 0; @lime-card-vertical-caption-overlay-captions-padding: var(--primitive-spacing-36) var(--primitive-spacing-48); From 7d9ff40ccfcac8b5947e8ef9761a091bddebc29e Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Mon, 15 Jun 2026 14:21:05 +0300 Subject: [PATCH 04/13] Fixed `spacing` --- Card/Card.js | 2 +- Card/Card.module.less | 30 +++++++++++++----------------- styles/variables.less | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index 1b19fb531..f8f2929b5 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -478,7 +478,7 @@ const CardBase = kind({ children: ({captionImageIconsSrc, captionOverlay, captionOverlayOnFocus, centered, centeredTitle, children, css, duration, durationOverlay, 'data-index': index, imageIconSrc, label, labelIcons, orientation, progress, progressBarOverlay, secondaryLabel, secondaryLabelIcons, showDuration, showProgressBar, splitCaption, withoutMarquee}) => { const isCenteredTitle = (captionOverlay || captionOverlayOnFocus) && orientation === 'vertical' && centeredTitle; const hasImageIcon = imageIconSrc && orientation === 'vertical'; - const hasCaptionImageIcons = captionImageIconsSrc && orientation === 'vertical'; + const hasCaptionImageIcons = captionImageIconsSrc && (captionImageIconsSrc.filter(Boolean).length && orientation === 'vertical'); const alignment = (centered && !imageIconSrc) || isCenteredTitle ? {alignment: 'center'} : null; const CaptionsComponent = isCenteredTitle ? Column : Row; diff --git a/Card/Card.module.less b/Card/Card.module.less index d7d7ca46d..2b3777bce 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -94,22 +94,22 @@ font-weight: @lime-card-caption-font-weight; } + .label { + font-size: @lime-card-label-font-size; + line-height: @lime-card-label-line-height; + font-weight: @lime-card-label-font-weight; + } + .labels { - gap: @lime-card-labels-spacing; height: fit-content; .labelContainer { align-items: center; gap: @lime-card-label-container-spacing; - .label { - font-size: @lime-card-label-font-size; - line-height: @lime-card-label-line-height; - font-weight: @lime-card-label-font-weight; - } - &:has(.labelIcon) { height: @lime-icon-small-size; + margin-bottom: @lime-card-label-icon-margin; will-change: transform; .label { @@ -119,6 +119,12 @@ .labelIcon { margin: 0; } + + // Removes the margin-bottom if the label is directly followed by icons (.captionImageIconsContainer) + // or if it's the last element in the list, to prevent excessive spacing. + &:has(+ .captionImageIconsContainer), &:is(:last-child) { + margin-bottom: 0; + } } } } @@ -293,18 +299,8 @@ padding: @lime-card-horizontal-captions-padding; } - .duration { - transform: none; - transition: none; - } - .image { width: var(--card-image-width); - - .duration { - transform: none; - transition: none; - } } } diff --git a/styles/variables.less b/styles/variables.less index b2021247a..32d133c26 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -483,9 +483,9 @@ @lime-card-duration-line-height-large: 70px; @lime-card-duration-padding: var(--primitive-spacing-6) var(--primitive-spacing-12); @lime-card-label-container-spacing: var(--primitive-spacing-12); +@lime-card-label-icon-margin: var(--primitive-spacing-6); @lime-card-label-line-height: 60px; @lime-card-label-line-height-large: 70px; -@lime-card-labels-spacing: var(--primitive-spacing-6); @lime-card-split-caption-label-padding: 0 var(--primitive-spacing-48); @lime-card-split-caption-lower-label-padding: var(--primitive-spacing-18); @lime-card-vertical-captions-padding: var(--primitive-spacing-36) 0; From 4f4adb8246a093304e7731b8961601d7f2f9f4a1 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Mon, 15 Jun 2026 14:22:34 +0300 Subject: [PATCH 05/13] Added `CHANGELOG` --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff1da160..88cf2131a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The following is a curated list of changes in the Enact limestone module, newest ### Changed - `limestone/Button` styling to match the latest GUI +- `limestone/Card` styling to match the latest GUI - `limestone/ImageItem` styling to match the latest GUI - `limestone/ProgressBar` styling to match the latest GUI - `limestone/Slider` styling to match the latest GUI From f107e86224bc6b2b1d802c94eb8fa99816703ce9 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 14:12:39 +0300 Subject: [PATCH 06/13] Fixed `centered` and overflow of the `captions` without marquee --- Card/Card.js | 162 ++++++++++++++++-------- Card/Card.module.less | 41 +++--- samples/sampler/stories/default/Card.js | 18 ++- 3 files changed, 145 insertions(+), 76 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index f8f2929b5..8391a213b 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -23,7 +23,7 @@ import {Card as UiCard} from '@enact/ui/Card'; import {Cell, Column, Row} from '@enact/ui/Layout'; import Touchable from '@enact/ui/Touchable'; import ri from '@enact/ui/resolution'; -import {cloneElement} from 'react'; +import {cloneElement, isValidElement} from 'react'; import PropTypes from 'prop-types'; import compose from 'ramda/src/compose'; @@ -37,21 +37,38 @@ import Skinnable from '../Skinnable'; import componentCss from './Card.module.less'; -const getDefaultImageSize = (orientation) => { - const sizes = { - vertical: {width: 768, height: 432}, - horizontal: {width: 596, height: 336} - }; +const formatDuration = (duration) => { + if (duration < 0) return "00:00"; - return sizes[orientation]; + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = duration % 60; + + const mm = String(minutes).padStart(2, '0'); + const ss = String(seconds).padStart(2, '0'); + + if (hours > 0) { + const hh = String(hours).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; + } + + return `${mm}:${ss}`; }; -const getLabelIcons = (icons, key) => { - return mapAndFilterChildren(icons, (labelIcon, idx) => ( - - {cloneElement(labelIcon, {className: componentCss.labelIcon})} - - )) || null; +const getBadge = (badge, size, className) => { + let element =
{badge}
; + let elementSize = {}; + + if (isValidElement(badge)) element = badge; + if (size) { + elementSize = typeof size === 'object' ? { + width: ri.scaleToRem(size.width), height: ri.scaleToRem(size.height) + } : { + fontSize: ri.scaleToRem(size) + }; + } + + return cloneElement(element, {className: className, style: {...elementSize}}); }; const getCaptionImageIcons = (imageSrc, key) => { @@ -66,22 +83,25 @@ const getCaptionImageIcons = (imageSrc, key) => { )) || null; }; -const formatDuration = (duration) => { - if (duration < 0) return "00:00"; - - const hours = Math.floor(duration / 3600); - const minutes = Math.floor((duration % 3600) / 60); - const seconds = duration % 60; +const getDefaultImageSize = (orientation) => { + const sizes = { + vertical: {width: 768, height: 432}, + horizontal: {width: 596, height: 336} + }; - const mm = String(minutes).padStart(2, '0'); - const ss = String(seconds).padStart(2, '0'); + return sizes[orientation]; +}; - if (hours > 0) { - const hh = String(hours).padStart(2, '0'); - return `${hh}:${mm}:${ss}`; - } +const getLabelIcons = (icons, key, className) => { + if (!icons) return null; - return `${mm}:${ss}`; + return icons.map((labelIcon, index) => { + return ( + + {cloneElement(labelIcon, {className})} + + ) + }) || null; }; /** @@ -117,7 +137,7 @@ const CardBase = kind({ 'aria-label': PropTypes.string, /** - * Source for the image icon. + * Sources for the image icon. * * String value or Object of values used to determine which image will appear on * a specific screenSize. This prop is only used when `orientation` is `'vertical'`. @@ -125,7 +145,9 @@ const CardBase = kind({ * @type {String|Object} * @public */ - captionImageIconsSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + captionImageIconsSrc: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.object]) + ), /** * The size of the caption images. @@ -346,12 +368,30 @@ const CardBase = kind({ pressed: PropTypes.bool, /** - * The primary badge image source. + * The primary badge. * - * @type {String|Object} + * @type {Element|String} * @public */ - primaryBadgeSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + primaryBadge: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + + /** + * The size of the primary badge. Can be a number or an object with specific dimensions. + * The following properties should be provided for the object: + * * `height` - The height of the badge + * * `width` - The width of the badge + * + * @type {Object|Number} + * @default {width: 108, height: 108} + * @public + */ + primaryBadgeSize: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number + }) + ]), /** * The progress displayed inside the ProgressBar @@ -378,12 +418,30 @@ const CardBase = kind({ roundedImage: PropTypes.bool, /** - * The secondary badge image source. + * The secondary badge. * - * @type {String|Object} + * @type {Element|String} * @public */ - secondaryBadgeSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + secondaryBadge: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + + /** + * The size of the secondary badge. Can be a number or an object with specific dimensions. + * The following properties should be provided for the object: + * * `height` - The height of the badge + * * `width` - The width of the badge + * + * @type {Object|Number} + * @default {width: 108, height: 108} + * @public + */ + secondaryBadgeSize: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number + }) + ]), /** * A ternary caption displayed with the image. @@ -498,14 +556,14 @@ const CardBase = kind({ {typeof label !== 'undefined' ? ( - {getLabelIcons(labelIcons, 'labelIcons')} -
{label}
+ {getLabelIcons(labelIcons, 'labelIcons', css.labelIcon)} +
{label}
) : null} {typeof secondaryLabel !== 'undefined' ? ( - {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons')} -
{secondaryLabel}
+ {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons', css.labelIcon)} +
{secondaryLabel}
) : null} {hasCaptionImageIcons ? ( @@ -525,14 +583,14 @@ const CardBase = kind({ {typeof label !== 'undefined' ? ( - {getLabelIcons(labelIcons, 'labelIcons')} - {label} + {getLabelIcons(labelIcons, 'labelIcons', css.labelIcon)} + {label} ) : null} {typeof secondaryLabel !== 'undefined' ? ( - {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons')} - {secondaryLabel} + {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons', css.labelIcon)} + {secondaryLabel} ) : null} {hasCaptionImageIcons ? ( @@ -602,21 +660,23 @@ const CardBase = kind({ splitCaption: ({captionOverlay, captionOverlayOnFocus, splitCaption}) => (captionOverlay || captionOverlayOnFocus) && splitCaption }, - render: ({captionImageSize, css, disabled, icon, imageSize, primaryBadgeSrc, secondaryBadgeSrc, showDuration, duration, progress, showProgressBar, style, ...rest}) => { + render: ({captionImageSize, css, disabled, icon, imageSize, primaryBadge, primaryBadgeSize, secondaryBadge, secondaryBadgeSize, showDuration, duration, progress, showProgressBar, style, ...rest}) => { delete rest.captionImageIconsSrc; delete rest.captionOverflow; delete rest.captionOverflowOnFocus; delete rest.captionOverlayOnFocus; delete rest.centered; delete rest.centeredTitle; + delete rest.durationOverlay; + delete rest.hasContainer; + delete rest.imageIconSrc; delete rest.label; delete rest.labelIcons; - delete rest.secondaryLabel; - delete rest.secondaryLabelIcons; - delete rest.imageIconSrc; - delete rest.hasContainer; delete rest.pressed; + delete rest.progressBarOverlay; delete rest.roundedImage; + delete rest.secondaryLabel; + delete rest.secondaryLabelIcons; delete rest.withoutMarquee; const defaultImageSize = getDefaultImageSize(rest.orientation); @@ -630,11 +690,11 @@ const CardBase = kind({ disabled={disabled} imageComponent={ - {primaryBadgeSrc ? ( - + {primaryBadge ? ( + getBadge(primaryBadge, primaryBadgeSize, css.primaryBadge) ) : null} - {secondaryBadgeSrc ? ( - + {secondaryBadge ? ( + getBadge(secondaryBadge, secondaryBadgeSize, css.secondaryBadge) ) : null}
{icon} diff --git a/Card/Card.module.less b/Card/Card.module.less index 2b3777bce..06b2626e2 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -71,6 +71,10 @@ } .primaryBadge, .secondaryBadge { + align-items: center; + display: flex; + justify-content: center; + text-align: center; position: absolute; width: @lime-card-badge-size; height: @lime-card-badge-size; @@ -100,31 +104,26 @@ font-weight: @lime-card-label-font-weight; } - .labels { - height: fit-content; - - .labelContainer { - align-items: center; - gap: @lime-card-label-container-spacing; + .labelContainer { + align-items: center; + gap: @lime-card-label-container-spacing; - &:has(.labelIcon) { - height: @lime-icon-small-size; - margin-bottom: @lime-card-label-icon-margin; - will-change: transform; + &:has(.labelIcon) { + margin-bottom: @lime-card-label-icon-margin; + will-change: transform; - .label { - margin-top: ~"calc(@{lime-icon-small-size} - @{lime-card-label-line-height})"; - } + .label { + margin-top: ~"calc(@{lime-icon-small-size} - @{lime-card-label-line-height})"; + } - .labelIcon { - margin: 0; - } + .labelIcon { + margin: 0; + } - // Removes the margin-bottom if the label is directly followed by icons (.captionImageIconsContainer) - // or if it's the last element in the list, to prevent excessive spacing. - &:has(+ .captionImageIconsContainer), &:is(:last-child) { - margin-bottom: 0; - } + // Removes the margin-bottom if the label is directly followed by icons (.captionImageIconsContainer) + // or if it's the last element in the list, to prevent excessive spacing. + &:has(+ .captionImageIconsContainer), &:is(:last-child) { + margin-bottom: 0; } } } diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index bbe092186..f608b60a0 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -1,6 +1,7 @@ import {Card, CardBase} from '@enact/limestone/Card'; import icons from '@enact/limestone/Icon/IconList'; import {Icon} from '@enact/limestone/Icon'; +import {Image} from '@enact/limestone/Image'; import {mergeComponentMetadata} from '@enact/storybook-utils'; import {action} from '@enact/storybook-utils/addons/actions'; import {boolean, number, object, select, text} from '@enact/storybook-utils/addons/controls'; @@ -23,6 +24,11 @@ const iconsList = Object.keys(icons).sort(); const randomIcon = () => iconsList[Math.floor(Math.random() * iconsList.length)]; const prop = { + badges: { + 'image': , + 'icon': {randomIcon()}, + 'text': 'Text' + }, orientation: ['horizontal', 'vertical'], icons: { 'no icons': null, @@ -70,11 +76,13 @@ export const _Card = (args) => ( label={args['label'] ? args['label'] : undefined} onClick={action('onClick')} orientation={args['orientation']} - primaryBadgeSrc={args['primaryBadgeSrc']} + primaryBadge={prop.badges[args['primaryBadge']]} + primaryBadgeSize={args['primaryBadgeSize']} progress={args['progress']} progressBarOverlay={args['progressBarOverlay']} roundedImage={args['roundedImage']} - secondaryBadgeSrc={args['secondaryBadgeSrc']} + secondaryBadge={prop.badges[args['secondaryBadge']]} + secondaryBadgeSize={args['secondaryBadgeSize']} // eslint-disable-next-line no-undefined secondaryLabel={args['secondaryLabel'] ? args['secondaryLabel'] : undefined} secondaryLabelIcons={prop.icons[args['secondaryLabelIcons']]} @@ -110,11 +118,13 @@ boolean('hasContainer', _Card, Config); text('label', _Card, Config, 'Card label'); select('labelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); select('orientation', _Card, prop.orientation, Config); -object('primaryBadgeSrc', _Card, Config, generateImageSrc('ff6d78')); +select('primaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); +object('primaryBadgeSize', _Card, Config, {width: 108, height: 108}); number('progress', _Card, Config, 0.5); boolean('progressBarOverlay', _Card, Config); boolean('roundedImage', _Card, Config); -object('secondaryBadgeSrc', _Card, Config, generateImageSrc('ffc600')); +select('secondaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); +object('secondaryBadgeSize', _Card, Config, {width: 108, height: 108}); text('secondaryLabel', _Card, Config, 'Card secondary label'); select('secondaryLabelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); boolean('selected', _Card, Config); From 49f570c8c6c3d8f54d5b8ec4c2752c58744bd0aa Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 14:16:05 +0300 Subject: [PATCH 07/13] Fixed lint warnings --- Card/Card.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index 8391a213b..1a0b7a21f 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -17,7 +17,6 @@ import {forProp, forward, handle, not} from '@enact/core/handle'; import kind from '@enact/core/kind'; -import {mapAndFilterChildren} from '@enact/core/util'; import Spottable from '@enact/spotlight/Spottable'; import {Card as UiCard} from '@enact/ui/Card'; import {Cell, Column, Row} from '@enact/ui/Layout'; @@ -100,7 +99,7 @@ const getLabelIcons = (icons, key, className) => { {cloneElement(labelIcon, {className})} - ) + ); }) || null; }; From 71bcac785f86a95e2638f3b83ce396044c70c1fb Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 14:42:14 +0300 Subject: [PATCH 08/13] Fixed `labels height` --- Card/Card.module.less | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Card/Card.module.less b/Card/Card.module.less index 06b2626e2..c9cc59783 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -104,26 +104,30 @@ font-weight: @lime-card-label-font-weight; } - .labelContainer { - align-items: center; - gap: @lime-card-label-container-spacing; + .labels { + height: fit-content; - &:has(.labelIcon) { - margin-bottom: @lime-card-label-icon-margin; - will-change: transform; + .labelContainer { + align-items: center; + gap: @lime-card-label-container-spacing; - .label { - margin-top: ~"calc(@{lime-icon-small-size} - @{lime-card-label-line-height})"; - } + &:has(.labelIcon) { + margin-bottom: @lime-card-label-icon-margin; + will-change: transform; - .labelIcon { - margin: 0; - } + .label { + margin-top: ~"calc(@{lime-icon-small-size} - @{lime-card-label-line-height})"; + } - // Removes the margin-bottom if the label is directly followed by icons (.captionImageIconsContainer) - // or if it's the last element in the list, to prevent excessive spacing. - &:has(+ .captionImageIconsContainer), &:is(:last-child) { - margin-bottom: 0; + .labelIcon { + margin: 0; + } + + // Removes the margin-bottom if the label is directly followed by icons (.captionImageIconsContainer) + // or if it's the last element in the list, to prevent excessive spacing. + &:has(+ .captionImageIconsContainer), &:is(:last-child) { + margin-bottom: 0; + } } } } From bfdab5efd735d8fe57440609048c03ededa9f981 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 16:43:30 +0300 Subject: [PATCH 09/13] Added `screenshot tests` --- Card/Card.js | 24 ++++++++++++++---------- Card/Card.module.less | 4 ++++ samples/sampler/stories/default/Card.js | 4 ++-- tests/screenshot/apps/components/Card.js | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index 1a0b7a21f..549865f52 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -295,13 +295,13 @@ const CardBase = kind({ /** * Source for the image icon. * - * String value or Object of values used to determine which image will appear on - * a specific screenSize. This prop is only used when `orientation` is `'vertical'`. + * String value or element or Object of values used to determine which image will appear on + * a specific screenSize. This prop is only used when `orientation` is `'vertical'` or `centeredTitle` is `true`. * - * @type {String|Object} + * @type {String|Object|Element} * @public */ - imageIconSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + imageIconSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]), /** * The size of the image. @@ -542,12 +542,16 @@ const CardBase = kind({ const captions = ( {hasImageIcon ? ( - + isValidElement(imageIconSrc) ? ( + cloneElement(imageIconSrc, {className: css.imageIcon}) + ) : ( + + ) ) : null} {withoutMarquee ? ( diff --git a/Card/Card.module.less b/Card/Card.module.less index c9cc59783..db8746c13 100644 --- a/Card/Card.module.less +++ b/Card/Card.module.less @@ -211,6 +211,8 @@ } .imageIcon { + font-size: @lime-card-vertical-image-icon-size; + line-height: @lime-card-vertical-image-icon-size; height: @lime-card-vertical-image-icon-size; width: @lime-card-vertical-image-icon-size; margin: @lime-card-vertical-image-icon-margin; @@ -241,6 +243,8 @@ } .imageIcon { + font-size: @lime-card-selection-icon-size; + line-height: @lime-card-selection-icon-size; width: @lime-card-selection-icon-size; height: @lime-card-selection-icon-size; } diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index f608b60a0..5aadbbb2d 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -119,12 +119,12 @@ text('label', _Card, Config, 'Card label'); select('labelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); select('orientation', _Card, prop.orientation, Config); select('primaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); -object('primaryBadgeSize', _Card, Config, {width: 108, height: 108}); +object('primaryBadgeSize', _Card, Config); number('progress', _Card, Config, 0.5); boolean('progressBarOverlay', _Card, Config); boolean('roundedImage', _Card, Config); select('secondaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); -object('secondaryBadgeSize', _Card, Config, {width: 108, height: 108}); +object('secondaryBadgeSize', _Card, Config); text('secondaryLabel', _Card, Config, 'Card secondary label'); select('secondaryLabelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); boolean('selected', _Card, Config); diff --git a/tests/screenshot/apps/components/Card.js b/tests/screenshot/apps/components/Card.js index e8e45df7d..aaa86017d 100644 --- a/tests/screenshot/apps/components/Card.js +++ b/tests/screenshot/apps/components/Card.js @@ -1,9 +1,15 @@ import Card from '../../../../Card'; +import Icon from '../../../../Icon'; +import Image from '../../../../Image'; import {withConfig, withProps} from './utils'; import img from '../../images/600x600.png'; +const iconBadge = ai; +const imageBadge = ; +const labelIcons = [ai, ai]; + const defaultCardTests = [ // Vertical Short, @@ -55,9 +61,26 @@ const defaultCardTests = [ Short ]; +const newTypeCardTests = [ + // Vertical + Title, + Title, + Title Title Title Title Title, + Title Title Title Title Title, + Title, + Title, + Title, + Title, + + // Horizontal + Title +] + const CardTests = [ ...defaultCardTests, + ...newTypeCardTests, + // Disabled ...withProps({disabled: true}, defaultCardTests), @@ -75,6 +98,7 @@ const CardTests = [ // Focused ...withConfig({focus: true, wrapper: {padded: true}}, defaultCardTests), + ...withConfig({focus: true, wrapper: {padded: true}}, newTypeCardTests), // FocusRing ...withConfig({focusRing: true, focus: true, wrapper: {padded: true}}, withProps({label: 'focusRing'}, defaultCardTests)), From 3a0f7f5836c4e57371c9055b25a3d0799638ef34 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 20:17:52 +0300 Subject: [PATCH 10/13] Fixed lint warnings --- Card/Card.js | 52 +++++++++++++----------- tests/screenshot/apps/components/Card.js | 5 ++- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index 549865f52..a3aac1543 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -70,16 +70,31 @@ const getBadge = (badge, size, className) => { return cloneElement(element, {className: className, style: {...elementSize}}); }; -const getCaptionImageIcons = (imageSrc, key) => { - return imageSrc.map((src, idx) => ( - - )) || null; +const getImageIcons = (images, key, className) => { + if (!images) return null; + + const getCellElement = (src) => { + return ( + + ); + }; + + if (Array.isArray(images)) { + return images.map((src, idx) => ( + cloneElement(getCellElement(src), {key: `${key}${idx}`}) + )) || null; + } + + if (isValidElement(images)) { + return cloneElement(images, {className}); + } + + return getCellElement(images); }; const getDefaultImageSize = (orientation) => { @@ -541,18 +556,7 @@ const CardBase = kind({ const captions = ( - {hasImageIcon ? ( - isValidElement(imageIconSrc) ? ( - cloneElement(imageIconSrc, {className: css.imageIcon}) - ) : ( - - ) - ) : null} + {hasImageIcon ? getImageIcons(imageIconSrc, null, css.imageIcon) : null} {withoutMarquee ? (
{children}
@@ -571,7 +575,7 @@ const CardBase = kind({ ) : null} {hasCaptionImageIcons ? ( - {getCaptionImageIcons(captionImageIconsSrc, 'captionImageIcons')} + {getImageIcons(captionImageIconsSrc, 'captionImageIcons', css.captionImageIcon)} ) : null} @@ -598,7 +602,7 @@ const CardBase = kind({ ) : null} {hasCaptionImageIcons ? ( - {getCaptionImageIcons(captionImageIconsSrc, 'captionImageIcons')} + {getImageIcons(captionImageIconsSrc, 'captionImageIcons', css.captionImageIcon)} ) : null} diff --git a/tests/screenshot/apps/components/Card.js b/tests/screenshot/apps/components/Card.js index aaa86017d..2d0bb5ca4 100644 --- a/tests/screenshot/apps/components/Card.js +++ b/tests/screenshot/apps/components/Card.js @@ -74,7 +74,7 @@ const newTypeCardTests = [ // Horizontal Title -] +]; const CardTests = [ ...defaultCardTests, @@ -104,7 +104,8 @@ const CardTests = [ ...withConfig({focusRing: true, focus: true, wrapper: {padded: true}}, withProps({label: 'focusRing'}, defaultCardTests)), // Large text - ...withConfig({skinVariants: ['largeText']}, defaultCardTests) + ...withConfig({skinVariants: ['largeText']}, defaultCardTests), + ...withConfig({skinVariants: ['largeText']}, newTypeCardTests) ]; export default CardTests; From 08d857f2f05f06a5024fb1ae06a2d08386b823ce Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Tue, 16 Jun 2026 21:11:52 +0300 Subject: [PATCH 11/13] Adjusted `screenshot tests` --- tests/screenshot/apps/components/Card.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/screenshot/apps/components/Card.js b/tests/screenshot/apps/components/Card.js index 2d0bb5ca4..15cb60ffd 100644 --- a/tests/screenshot/apps/components/Card.js +++ b/tests/screenshot/apps/components/Card.js @@ -68,8 +68,8 @@ const newTypeCardTests = [ Title Title Title Title Title, Title Title Title Title Title, Title, - Title, - Title, + Title, + Title, Title, // Horizontal From d442785bd7ef87ab47f8f6c0858f13142509a494 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Wed, 17 Jun 2026 13:31:49 +0300 Subject: [PATCH 12/13] Added `unit tests` --- Card/tests/Card-specs.js | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/Card/tests/Card-specs.js b/Card/tests/Card-specs.js index f80f0d012..9148c9bda 100644 --- a/Card/tests/Card-specs.js +++ b/Card/tests/Card-specs.js @@ -103,4 +103,95 @@ describe('Card', () => { fireEvent.mouseUp(card); expect(card).not.toHaveClass('pressed'); }); + + test('should format a negative duration as "00:00" in the image overlay', () => { + render(); + + expect(screen.getByText('00:00')).toBeInTheDocument(); + }); + + test('should format a duration of one hour or more as HH:MM:SS in the captions', () => { + render(); + + expect(screen.getByText('01:01:01')).toBeInTheDocument(); + }); + + test('should render an array of `captionImageIconsSrc` as Images in the captions', () => { + render( + + ); + + expect(screen.queryAllByRole('img')).toHaveLength(6); + }); + + test('should render a React element passed as `imageIconSrc`', () => { + render( + } + /> + ); + + expect(screen.getByTestId('custom-image-icon')).toBeInTheDocument(); + }); + + test('should render `label` without icons when `labelIcons` is not provided', () => { + render(); + + expect(screen.getByText('Label text')).toBeInTheDocument(); + }); + + test('should render `labelIcons` alongside the `label`', () => { + render( + , +
+ + ]} + secondaryLabel="Secondary label" + secondaryLabelIcons={[
]} + /> + ); + + expect(screen.getByTestId('label-icon-1')).toBeInTheDocument(); + expect(screen.getByTestId('label-icon-2')).toBeInTheDocument(); + expect(screen.getByTestId('secondary-label-icon')).toBeInTheDocument(); + }); + + test('should render `labelIcons` alongside the `label` in `withoutMarquee` mode', () => { + render( + ]} + /> + ); + + expect(screen.getByTestId('label-icon')).toBeInTheDocument(); + }); + + test('should render `primaryBadge` string in the image overlay', () => { + render(); + + expect(screen.getByText('Primary Badge')).toBeInTheDocument(); + }); + + test('should render `secondaryBadge` string in the image overlay', () => { + render(); + + expect(screen.getByText('Secondary Badge')).toBeInTheDocument(); + }); + + test('should render `ProgressBar` in the image overlay when `progressBarOverlay` is true', () => { + render(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); }); From 6650125c57f22ee64c5d76136a43ac49a30bebd2 Mon Sep 17 00:00:00 2001 From: ion-andrusciac-lgp Date: Fri, 19 Jun 2026 11:58:15 +0300 Subject: [PATCH 13/13] Review fixes --- Card/Card.js | 93 +++++++++---------------- Card/tests/Card-specs.js | 10 ++- samples/sampler/stories/default/Card.js | 13 +++- 3 files changed, 51 insertions(+), 65 deletions(-) diff --git a/Card/Card.js b/Card/Card.js index a3aac1543..d13996e5b 100644 --- a/Card/Card.js +++ b/Card/Card.js @@ -37,7 +37,7 @@ import Skinnable from '../Skinnable'; import componentCss from './Card.module.less'; const formatDuration = (duration) => { - if (duration < 0) return "00:00"; + if (!duration || duration < 0) return '00:00'; const hours = Math.floor(duration / 3600); const minutes = Math.floor((duration % 3600) / 60); @@ -87,7 +87,7 @@ const getImageIcons = (images, key, className) => { if (Array.isArray(images)) { return images.map((src, idx) => ( cloneElement(getCellElement(src), {key: `${key}${idx}`}) - )) || null; + )); } if (isValidElement(images)) { @@ -153,10 +153,10 @@ const CardBase = kind({ /** * Sources for the image icon. * - * String value or Object of values used to determine which image will appear on + * An array of String values or Objects of values used to determine which image will appear on * a specific screenSize. This prop is only used when `orientation` is `'vertical'`. * - * @type {String|Object} + * @type {String[]|Object[]} * @public */ captionImageIconsSrc: PropTypes.arrayOf( @@ -169,9 +169,9 @@ const CardBase = kind({ * The following properties should be provided: * * `height` - The height of the image * * `width` - The width of the image - + * * @type {Object} - * @default {height: 432, width: 768} + * @default {height: 96, width: 96} * @public */ captionImageSize: PropTypes.shape({ @@ -324,7 +324,7 @@ const CardBase = kind({ * The following properties should be provided: * * `height` - The height of the image * * `width` - The width of the image - + * * @type {Object} * @default {height: 432, width: 768} * @public @@ -552,66 +552,39 @@ const CardBase = kind({ const hasImageIcon = imageIconSrc && orientation === 'vertical'; const hasCaptionImageIcons = captionImageIconsSrc && (captionImageIconsSrc.filter(Boolean).length && orientation === 'vertical'); const alignment = (centered && !imageIconSrc) || isCenteredTitle ? {alignment: 'center'} : null; + const labelsProps = withoutMarquee ? {style: {textAlign: alignment?.alignment}} : {...alignment}; const CaptionsComponent = isCenteredTitle ? Column : Row; + const LabelsComponent = withoutMarquee ? 'div' : Marquee; const captions = ( {hasImageIcon ? getImageIcons(imageIconSrc, null, css.imageIcon) : null} - {withoutMarquee ? ( - -
{children}
- - {typeof label !== 'undefined' ? ( - - {getLabelIcons(labelIcons, 'labelIcons', css.labelIcon)} -
{label}
-
- ) : null} - {typeof secondaryLabel !== 'undefined' ? ( - - {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons', css.labelIcon)} -
{secondaryLabel}
-
- ) : null} - {hasCaptionImageIcons ? ( - - {getImageIcons(captionImageIconsSrc, 'captionImageIcons', css.captionImageIcon)} - - ) : null} -
- {(showProgressBar && !progressBarOverlay && !isCenteredTitle) ? : null} - {(showDuration && !durationOverlay && !showProgressBar) ? ( -
{formatDuration(duration)}
+ + {children} + + {typeof label !== 'undefined' ? ( + + {getLabelIcons(labelIcons, 'labelIcons', css.labelIcon)} + {label} + + ) : null} + {typeof secondaryLabel !== 'undefined' ? ( + + {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons', css.labelIcon)} + {secondaryLabel} + ) : null} - - ) : ( - - {children} - - {typeof label !== 'undefined' ? ( - - {getLabelIcons(labelIcons, 'labelIcons', css.labelIcon)} - {label} - - ) : null} - {typeof secondaryLabel !== 'undefined' ? ( - - {getLabelIcons(secondaryLabelIcons, 'secondaryLabelIcons', css.labelIcon)} - {secondaryLabel} - - ) : null} - {hasCaptionImageIcons ? ( - - {getImageIcons(captionImageIconsSrc, 'captionImageIcons', css.captionImageIcon)} - - ) : null} - - {(showProgressBar && !progressBarOverlay && !isCenteredTitle) ? : null} - {(showDuration && !durationOverlay && !showProgressBar) ? ( -
{formatDuration(duration)}
+ {hasCaptionImageIcons ? ( + + {getImageIcons(captionImageIconsSrc, 'captionImageIcons', css.captionImageIcon)} + ) : null} -
- )} + + {(showProgressBar && !progressBarOverlay && !isCenteredTitle) ? : null} + {(showDuration && !durationOverlay && !showProgressBar) ? ( +
{formatDuration(duration)}
+ ) : null} +
); diff --git a/Card/tests/Card-specs.js b/Card/tests/Card-specs.js index 9148c9bda..c88020b59 100644 --- a/Card/tests/Card-specs.js +++ b/Card/tests/Card-specs.js @@ -110,6 +110,12 @@ describe('Card', () => { expect(screen.getByText('00:00')).toBeInTheDocument(); }); + test('should format a duration of one hour or more as MM:SS in the captions', () => { + render(); + + expect(screen.getByText('03:20')).toBeInTheDocument(); + }); + test('should format a duration of one hour or more as HH:MM:SS in the captions', () => { render(); @@ -120,11 +126,11 @@ describe('Card', () => { render( ); - expect(screen.queryAllByRole('img')).toHaveLength(6); + expect(screen.queryAllByRole('img')).toHaveLength(2); }); test('should render a React element passed as `imageIconSrc`', () => { diff --git a/samples/sampler/stories/default/Card.js b/samples/sampler/stories/default/Card.js index 5aadbbb2d..719524a06 100644 --- a/samples/sampler/stories/default/Card.js +++ b/samples/sampler/stories/default/Card.js @@ -20,6 +20,13 @@ const generateImageSrc = (color) => { }; }; +const generateImageSize = (width, height) => { + return { + width: width, + height: height + }; +}; + const iconsList = Object.keys(icons).sort(); const randomIcon = () => iconsList[Math.floor(Math.random() * iconsList.length)]; @@ -99,7 +106,7 @@ export const _Card = (args) => ( text('aria-label', _Card, Config); object('captionImageIconsSrc', _Card, Config, generateImageSrc('0084ff')); -object('captionImageSize', _Card, Config); +object('captionImageSize', _Card, Config, generateImageSize(96, 96)); boolean('captionOverflow', _Card, Config); boolean('captionOverflowOnFocus', _Card, Config); boolean('captionOverlay', _Card, Config); @@ -119,12 +126,12 @@ text('label', _Card, Config, 'Card label'); select('labelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); select('orientation', _Card, prop.orientation, Config); select('primaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); -object('primaryBadgeSize', _Card, Config); +object('primaryBadgeSize', _Card, Config, generateImageSize(108, 108)); number('progress', _Card, Config, 0.5); boolean('progressBarOverlay', _Card, Config); boolean('roundedImage', _Card, Config); select('secondaryBadge', _Card, ['image', 'icon', 'text'], Config, 'image'); -object('secondaryBadgeSize', _Card, Config); +object('secondaryBadgeSize', _Card, Config, generateImageSize(108, 108)); text('secondaryLabel', _Card, Config, 'Card secondary label'); select('secondaryLabelIcons', _Card, ['no icons', '1 icon', '2 icons', '3 icons'], Config, 'no icons'); boolean('selected', _Card, Config);