Skip to content

Commit e3a53c6

Browse files
authored
feat(cta): introduce card CTA (#3855)
### Related Ticket(s) Refs #3556. ### Description Introduces the following components: * `<dds-card-cta>`, the card variant of CTA * `<dds-card-cta-footer>`, the footer part of card CTA * `<dds-feature-cta>`, the feature card variant of CTA (Note: Doesn't have some UI in feature card block for now due to pending work) * `<dds-feature-cta-footer>`, the footer part of feature CTA * `<dds-cta-composite>`, non-API-integration portion of `<dds-cta-container>` * `<dds-markdown>`, a markdown renderer `<dds-cta-container>` is now wired to video player API portion of our Redux store to provide default video caption. Some other misc changes to make things work, etc.: * A change to `<dds-link-with-icon>` to allow inherited classes to override text/icon contents * Separted g11n formater for video duration from one for video caption. Also made the default formatter defined in `<dds-video-player>` reusable from other components * A change to `<dds-card>` to allow inherited classes to override the copy/images contents * Some changes to `<dds-card-footer>`: * One to allow inherited classes to override the copy content * One to utilize `bx--card__footer__icon-left` CSS class that was introduced recently * Introduced `hasGrid` story parameter. If it's set, the shared decorator of the stories provides better stying to accomodate grid * Some Storybook environment-specific grid classes (`dds-ce-demo-devenv--grid--card` and `dds-ce-demo-devenv--grid-row`) to make stories with grids look better * An API to allow CTA icon to be overriden * A fix to `<dds-video-player-container>` whose `mapStateToProps()` interface had redundant property ### Changelog **New** * `<dds-card-cta>`, the card variant of CTA * `<dds-card-cta-footer>`, the footer part of card CTA * `<dds-feature-cta>`, the feature card variant of CTA (Note: Doesn't have some UI in feature card block for now due to pending work) * `<dds-feature-cta-footer>`, the footer part of feature CTA * `<dds-cta-composite>`, non-API-integration portion of `<dds-cta-container>` * `<dds-markdown>`, a markdown renderer * An API to allow CTA icon to be overriden **Changed** * A change to `<dds-link-with-icon>` to allow inherited classes to override text/icon contents * Separted g11n formater for video duration from one for video caption. Also made the default formatter defined in `<dds-video-player>` reusable from other components * A change to `<dds-card>` to allow inherited classes to override the copy/images contents * Some changes to `<dds-card-footer>`: * One to allow inherited classes to override the copy content * One to utilize `bx--card__footer__icon-left` CSS class that was introduced recently * Introduced `hasGrid` story parameter. If it's set, the shared decorator of the stories provides better stying to accomodate grid * Some Storybook environment-specific grid classes (`dds-ce-demo-devenv--grid--card` and `dds-ce-demo-devenv--grid-row`) to make stories with grids look better * A fix to `<dds-video-player-container>` whose `mapStateToProps()` interface had redundant property
1 parent feab658 commit e3a53c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2356
-394
lines changed

packages/styles/scss/components/card/index.scss

+76-49
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
@mixin card {
1515
.#{$prefix}--card,
16-
:host(#{$dds-prefix}-card) {
16+
:host(#{$dds-prefix}-card),
17+
:host(#{$dds-prefix}-card-cta) {
1718
background-color: $ui-01;
1819
text-decoration: none;
1920
display: flex;
@@ -54,48 +55,6 @@
5455
margin-bottom: $carbon--spacing-03;
5556
color: $text-02;
5657
}
57-
::slotted(svg[slot='footer']),
58-
.#{$prefix}--card__cta,
59-
.#{$prefix}--card__cta a,
60-
.#{$prefix}--card__cta a:visited {
61-
color: $interactive-04;
62-
}
63-
64-
.#{$prefix}--card__cta__copy {
65-
margin-right: $carbon--spacing-03;
66-
color: $interactive-04;
67-
@include carbon--type-style('body-short-02');
68-
}
69-
70-
.#{$prefix}--card__footer,
71-
:host(#{$dds-prefix}-feature-card-footer) {
72-
display: flex;
73-
margin-top: auto;
74-
75-
.#{$prefix}--card__footer__copy {
76-
margin-bottom: -$carbon--spacing-01;
77-
}
78-
79-
svg {
80-
fill: currentColor;
81-
display: block;
82-
align-self: center;
83-
min-width: 20px;
84-
}
85-
}
86-
87-
.#{$prefix}--card__footer__icon-left {
88-
justify-content: flex-end;
89-
flex-direction: row-reverse;
90-
91-
.#{$prefix}--card__cta {
92-
margin-right: $carbon--spacing-03;
93-
}
94-
95-
.#{$prefix}--card__cta__copy {
96-
margin-right: 0;
97-
}
98-
}
9958

10059
.#{$prefix}--card:focus,
10160
.#{$prefix}--card:visited,
@@ -146,6 +105,68 @@
146105
}
147106
}
148107

108+
.#{$prefix}--card .#{$prefix}--card__cta,
109+
.#{$prefix}--card .#{$prefix}--card__cta a,
110+
.#{$prefix}--card .#{$prefix}--card__cta a:visited,
111+
.#{$prefix}--card ::slotted(svg[slot='footer']),
112+
:host(#{$dds-prefix}-card-footer)
113+
.#{$dds-prefix}-ce--card__footer
114+
::slotted(svg[slot='icon']),
115+
:host(#{$dds-prefix}-card-cta-footer)
116+
.#{$dds-prefix}-ce--card__footer
117+
::slotted(svg[slot='icon']) {
118+
color: $interactive-04;
119+
}
120+
121+
.#{$prefix}--card .#{$prefix}--card__footer,
122+
:host(#{$dds-prefix}-card-footer),
123+
:host(#{$dds-prefix}-card-cta-footer),
124+
:host(#{$dds-prefix}-feature-card-footer),
125+
:host(#{$dds-prefix}-feature-cta-footer) {
126+
margin-top: auto; /* Moves the footer down to the bottom in the card */
127+
128+
.#{$prefix}--card__cta__copy {
129+
margin-right: $carbon--spacing-03;
130+
color: $interactive-04;
131+
@include carbon--type-style('body-short-02');
132+
}
133+
134+
.#{$prefix}--card__footer__copy {
135+
margin-bottom: -$carbon--spacing-01;
136+
}
137+
138+
svg,
139+
::slotted(svg[slot='icon']) {
140+
display: block;
141+
min-width: 20px;
142+
}
143+
144+
.#{$prefix}--card__footer__icon-left {
145+
justify-content: flex-end;
146+
flex-direction: row-reverse;
147+
148+
svg.#{$prefix}--card__cta,
149+
::slotted(svg[slot='icon']) {
150+
margin-right: $carbon--spacing-03;
151+
}
152+
153+
.#{$prefix}--card__cta__copy {
154+
margin-right: 0;
155+
}
156+
}
157+
}
158+
159+
.#{$prefix}--card .#{$prefix}--card__footer,
160+
.#{$dds-prefix}-ce--card__footer {
161+
display: flex;
162+
}
163+
164+
.#{$prefix}--card .#{$prefix}--card__footer svg,
165+
.#{$dds-prefix}-ce--card__footer ::slotted(svg[slot='icon']) {
166+
fill: currentColor;
167+
align-self: center;
168+
}
169+
149170
.#{$prefix}--card--inverse,
150171
:host(#{$dds-prefix}-card)[color-scheme='inverse'] {
151172
background-color: $inverse-02;
@@ -162,13 +183,19 @@
162183
&:hover {
163184
background-color: $inverse-hover-ui;
164185
}
186+
}
165187

166-
.#{$prefix}--card__cta,
167-
.#{$prefix}--card__cta a,
168-
.#{$prefix}--card__cta a:visited,
169-
.#{$prefix}--card__cta__copy {
170-
color: $inverse-link;
171-
}
188+
.#{$prefix}--card--inverse .#{$prefix}--card__cta,
189+
.#{$prefix}--card--inverse .#{$prefix}--card__cta a,
190+
.#{$prefix}--card--inverse .#{$prefix}--card__cta a:visited,
191+
.#{$prefix}--card--inverse .#{$prefix}--card__cta__copy,
192+
:host(#{$dds-prefix}-feature-card-footer)
193+
.#{$dds-prefix}-ce--card__footer
194+
::slotted(svg[slot='icon']),
195+
:host(#{$dds-prefix}-feature-cta-footer)
196+
.#{$dds-prefix}-ce--card__footer
197+
::slotted(svg[slot='icon']) {
198+
color: $inverse-link;
172199
}
173200
}
174201

packages/styles/scss/components/feature-card/_feature-card.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
@mixin feature-card {
1515
.#{$prefix}--feature-card,
16-
:host(#{$dds-prefix}-feature-card) {
16+
:host(#{$dds-prefix}-feature-card),
17+
:host(#{$dds-prefix}-feature-cta) {
1718
padding-top: aspect-ratio(1, 1);
1819
position: relative;
1920
@include carbon--breakpoint('md') {

packages/styles/scss/components/link-with-icon/_link-with-icon.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
@mixin link-with-icon {
1414
.#{$prefix}--link-with-icon,
15-
:host(#{$dds-prefix}-link-with-icon) {
15+
:host(#{$dds-prefix}-link-with-icon),
16+
:host(#{$dds-prefix}-text-cta) {
1617
display: flex;
1718

1819
span {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright IBM Corp. 2020
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { formatVideoCaption, formatVideoDuration } from '../';
9+
10+
describe('g11n formatter for video caption', () => {
11+
describe('Formatting caption', () => {
12+
it('should support empty caption', async () => {
13+
expect(formatVideoCaption()).toBe('');
14+
});
15+
16+
it('should support name-only caption', async () => {
17+
expect(formatVideoCaption({ name: 'foo' })).toBe('foo');
18+
});
19+
20+
it('should support duration-only caption', async () => {
21+
expect(formatVideoCaption({ duration: '1:30' })).toBe('1:30');
22+
});
23+
24+
it('should support a caption with the name and the duration', async () => {
25+
expect(formatVideoCaption({ name: 'foo', duration: '1:30' })).toBe(
26+
'foo (1:30)'
27+
);
28+
});
29+
30+
it('should support a caption zero duration', async () => {
31+
expect(formatVideoCaption({ name: 'foo', duration: 0 })).toBe('foo (0)');
32+
});
33+
});
34+
35+
describe('Formatting duration', () => {
36+
it('should support undefined duration', async () => {
37+
expect(formatVideoDuration()).toBeUndefined();
38+
});
39+
40+
it('should support null duration', async () => {
41+
expect(formatVideoDuration({ duration: null })).toBeNull();
42+
});
43+
44+
it('should fill zero for minutes', async () => {
45+
expect(formatVideoDuration({ duration: 30000 })).toBe('0:30');
46+
});
47+
48+
it('should fill zero for seconds', async () => {
49+
expect(formatVideoDuration({ duration: 65000 })).toBe('1:05');
50+
});
51+
52+
it('should support more than 1 minute', async () => {
53+
expect(formatVideoDuration({ duration: 90000 })).toBe('1:30');
54+
});
55+
});
56+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright IBM Corp. 2020
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
/**
9+
* The default g11n formatter for video caption, combining video name and video duration.
10+
* Components using this function should have a mechanism to allow translators
11+
* to replace it with one accomodating the preferences of specific locale.
12+
*
13+
* @param {object} [options] The options, with a video name and a formatted video duration.
14+
* @param {string} [options.duration] The video duration.
15+
* @param {string} [options.name] The video name.
16+
* @returns {string} The formatted video caption.
17+
*/
18+
export function formatVideoCaption({ duration, name } = {}) {
19+
return !name || (duration !== 0 && !duration)
20+
? name || duration || ''
21+
: `${name} (${duration})`;
22+
}
23+
24+
/**
25+
* The default g11n formatter for video duration.
26+
* Components using this function should have a mechanism to allow translators
27+
* to replace it with one accomodating the preferences of specific locale,
28+
* or to replace it with general-purpose g11n formatting library.
29+
* (e.g. moment, though it's too big for us to make it a hard dependency)
30+
*
31+
* @param {object} [options] The options, with a video duration.
32+
* @param {number} [options.duration] The video duration, in seconds.
33+
* @returns {string} The formatted video duration.
34+
*/
35+
export function formatVideoDuration({ duration } = {}) {
36+
const minutes = Math.floor((duration ?? 0) / 60000);
37+
const seconds = Math.floor(((duration ?? 0) / 1000) % 60);
38+
const fillSeconds = Array.from({
39+
length: 2 - String(seconds).length + 1,
40+
}).join('0');
41+
return duration == null ? duration : `${minutes}:${fillSeconds}${seconds}`;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright IBM Corp. 2020
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export * from './formatVideoCaption';

packages/utilities/src/utilities/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './calculateTotalWidth';
1010
export * from './decodeString';
1111
export * from './escaperegexp';
1212
export * from './featureflag';
13+
export * from './formatVideoCaption';
1314
export * from './geolocation';
1415
export * from './ipcinfoCookie';
1516
export * from './markdownToHtml';

packages/web-components/.storybook/config.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import { html } from 'lit-html'; // eslint-disable-line import/first
11+
import { classMap } from 'lit-html/directives/class-map';
1112
import { configure, addDecorator, addParameters, setCustomElements } from '@storybook/web-components'; // eslint-disable-line import/first
1213
import { withKnobs } from '@storybook/addon-knobs';
1314
import customElements from '../custom-elements.json';
@@ -46,19 +47,19 @@ addParameters({
4647
// The TS configuration for `@storybook/web-components` does not seem to allow returning `TemplateResult` in decorators,
4748
// using `TemplateResult` in decorators seems to work with `@storybook/web-components` actually
4849
// @ts-ignore
49-
addDecorator(story => {
50+
addDecorator((story, { parameters }) => {
5051
const result = story();
5152
const { hasMainTag } = result as any;
53+
const { hasGrid } = parameters;
54+
const classes = classMap({
55+
'dds-ce-demo-devenv--container': true,
56+
'dds-ce-demo-devenv--container--has-grid': hasGrid,
57+
});
5258
return html`
5359
<style>
5460
${containerStyles}
5561
</style>
56-
<div
57-
name="main-content"
58-
data-floating-menu-container
59-
role="${hasMainTag ? 'none' : 'main'}"
60-
class="dds-ce-demo-devenv--container"
61-
>
62+
<div name="main-content" data-floating-menu-container role="${hasMainTag ? 'none' : 'main'}" class="${classes}">
6263
${result}
6364
</div>
6465
`;

packages/web-components/.storybook/container.scss

+14
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,17 @@ body {
4646
align-items: center;
4747
position: relative;
4848
}
49+
50+
.dds-ce-demo-devenv--container--has-grid {
51+
align-items: stretch;
52+
}
53+
54+
.dds-ce-demo-devenv--grid--card {
55+
margin-left: 0;
56+
margin-right: 0;
57+
}
58+
59+
.dds-ce-demo-devenv--grid-row {
60+
flex-direction: column;
61+
align-items: center;
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/build
2+
/node_modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!--
2+
@license
3+
4+
Copyright IBM Corp. 2020
5+
6+
This source code is licensed under the Apache-2.0 license found in the
7+
LICENSE file in the root directory of this source tree.
8+
-->
9+
10+
<html>
11+
<head>
12+
<title>@carbon/ibmdotcom-web-components example</title>
13+
<meta charset="UTF-8" />
14+
<script type="module">
15+
import '@carbon/ibmdotcom-web-components/es/components/cta/card-cta.js';
16+
</script>
17+
<style type="text/css">
18+
body {
19+
font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
20+
display: flex;
21+
flex-direction: column;
22+
align-items: center;
23+
}
24+
25+
body > * {
26+
width: 37.5%;
27+
}
28+
</style>
29+
</head>
30+
<body>
31+
<dds-card-cta href="https://www.ibm.com/" type="local">
32+
Copy text
33+
</dds-card-cta>
34+
</body>
35+
</html>

0 commit comments

Comments
 (0)