Skip to content

Commit

Permalink
feat(cta): introduce button variant (#4779)
Browse files Browse the repository at this point in the history
Introudces button variant of the CTA, that inherits
`<dds-button-group-item>`.

`<dds-btn>`, the parent class of `<dds-button-group-item>`, is
written from scratch (instead of inheriting `<bx-btn>`) so it opens up
the APIs that CTA requires.

Refs #3556.
  • Loading branch information
asudoh authored Dec 28, 2020
1 parent 9e322f4 commit bbe4744
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
}

.#{$prefix}--buttongroup-item,
:host(#{$dds-prefix}-button-group-item) {
:host(#{$dds-prefix}-button-group-item),
:host(#{$dds-prefix}-button-cta) {
margin-top: $carbon--layout-01;
max-width: carbon--mini-units(40);
min-width: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

@import '@carbon/ibmdotcom-styles/scss/components/buttongroup/buttongroup';

:host(#{$dds-prefix}-button-group-item) {
:host(#{$dds-prefix}-button-group-item),
:host(#{$dds-prefix}-button-cta) {
outline: none;
}
198 changes: 194 additions & 4 deletions packages/web-components/src/components/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,212 @@
* LICENSE file in the root directory of this source tree.
*/

import { customElement } from 'lit-element';
import BXBtn from 'carbon-web-components/es/components/button/button.js';
import { classMap } from 'lit-html/directives/class-map';
import { html, property, internalProperty, customElement, LitElement } from 'lit-element';
import settings from 'carbon-components/es/globals/js/settings';
import ifNonNull from 'carbon-web-components/es/globals/directives/if-non-null.js';
import FocusMixin from 'carbon-web-components/es/globals/mixins/focus.js';
import { BUTTON_ICON_LAYOUT, BUTTON_KIND, BUTTON_SIZE } from 'carbon-web-components/es/components/button/button.js';
import ddsSettings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js';
import styles from './button.scss';

export { BUTTON_KIND, BUTTON_SIZE } from 'carbon-web-components/es/components/button/button.js';
export { BUTTON_KIND, BUTTON_SIZE };

const { prefix } = settings;
const { stablePrefix: ddsPrefix } = ddsSettings;

/**
* Expressive button.
*
* @element dds-btn
* @csspart button The button.
*/
@customElement(`${ddsPrefix}-btn`)
class DDSBtn extends BXBtn {
class DDSBtn extends FocusMixin(LitElement) {
/**
* `true` if there is an icon.
*/
@internalProperty()
protected _hasIcon = false;

/**
* `true` if there is a non-icon content.
*/
@internalProperty()
protected _hasMainContent = false;

/**
* The CSS class list for the button/link node.
*/
protected get _classes() {
const { disabled, kind, size, _hasIcon: hasIcon, _hasMainContent: hasMainContent } = this;
return classMap({
[`${prefix}--btn`]: true,
[`${prefix}--btn--${kind}`]: kind,
[`${prefix}--btn--disabled`]: disabled,
[`${prefix}--btn--icon-only`]: hasIcon && !hasMainContent,
[`${prefix}--btn--${size}`]: size,
[`${prefix}-ce--btn--has-icon`]: hasIcon,
});
}

/**
* Handles `slotchange` event.
*/
protected _handleSlotChange({ target }: Event) {
const { name } = target as HTMLSlotElement;
const hasContent = (target as HTMLSlotElement)
.assignedNodes()
.some(node => node.nodeType !== Node.TEXT_NODE || node!.textContent!.trim());
this[name === 'icon' ? '_hasIcon' : '_hasMainContent'] = hasContent;
this.requestUpdate();
}

/**
* @returns The disabled link content.
*/
protected _renderDisabledLink() {
const { _classes: classes } = this;
return html`
<p id="button" part="button" class="${classes}">
${this._renderInner()}
</p>
`;
}

/**
* @returns The inner content.
*/
protected _renderInner() {
const { _handleSlotChange: handleSlotChange } = this;
return html`
<slot @slotchange="${handleSlotChange}"></slot>
<slot name="icon" @slotchange="${handleSlotChange}"></slot>
`;
}

/**
* `true` if the button should have input focus when the page loads.
*/
@property({ type: Boolean, reflect: true })
autofocus = false;

/**
* `true` if the button should be disabled.
*/
@property({ type: Boolean, reflect: true })
disabled = false;

/**
* The default file name, used if this button is rendered as `<a>`.
*/
@property({ reflect: true })
download!: string;

/**
* Link `href`. If present, this button is rendered as `<a>`.
*/
@property({ reflect: true })
href!: string;

/**
* The language of what `href` points to, if this button is rendered as `<a>`.
*/
@property({ reflect: true })
hreflang!: string;

/**
* Button icon layout.
*/
@property({ reflect: true, attribute: 'icon-layout' })
iconLayout = BUTTON_ICON_LAYOUT.REGULAR;

/**
* Button kind.
*/
@property({ reflect: true })
kind = BUTTON_KIND.PRIMARY;

/**
* The a11y role for `<a>`.
*/
@property({ attribute: 'link-role' })
linkRole = 'button';

/**
* URLs to ping, if this button is rendered as `<a>`.
*/
@property({ reflect: true })
ping!: string;

/**
* The link type, if this button is rendered as `<a>`.
*/
@property({ reflect: true })
rel!: string;

/**
* Button size.
*/
@property({ reflect: true })
size = BUTTON_SIZE.REGULAR;

/**
* The link target, if this button is rendered as `<a>`.
*/
@property({ reflect: true })
target!: string;

/**
* The default behavior if the button is rendered as `<button>`. MIME type of the `target`if this button is rendered as `<a>`.
*/
@property({ reflect: true })
type!: string;

createRenderRoot() {
return this.attachShadow({
mode: 'open',
delegatesFocus: Number((/Safari\/(\d+)/.exec(navigator.userAgent) ?? ['', 0])[1]) <= 537,
});
}

render() {
const { autofocus, disabled, download, href, hreflang, linkRole, ping, rel, target, type, _classes: classes } = this;
if (href) {
return disabled
? this._renderDisabledLink()
: html`
<a
id="button"
part="button"
role="${ifNonNull(linkRole)}"
class="${classes}"
download="${ifNonNull(download)}"
href="${ifNonNull(href)}"
hreflang="${ifNonNull(hreflang)}"
ping="${ifNonNull(ping)}"
rel="${ifNonNull(rel)}"
target="${ifNonNull(target)}"
type="${ifNonNull(type)}"
>
${this._renderInner()}
</a>
`;
}
return html`
<button
id="button"
part="button"
class="${classes}"
?autofocus="${autofocus}"
?disabled="${disabled}"
type="${ifNonNull(type)}"
>
${this._renderInner()}
</button>
`;
}

static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { select } from '@storybook/addon-knobs';
import textNullable from '../../../../.storybook/knob-text-nullable';
import { CTA_TYPE } from '../defs';
import '../video-cta-container';
import '../button-cta';
import '../card-cta';
import '../card-cta-footer';
import '../feature-cta';
Expand Down Expand Up @@ -87,6 +88,39 @@ Text.story = {
},
};

export const Button = ({ parameters }) => {
const { copy, ctaType, download, href } = parameters?.props?.ButtonCTA ?? {};
return html`
<div>
<dds-button-group>
<dds-button-cta cta-type="${ifNonNull(ctaType)}" download="${ifNonNull(download)}" href="${href}">${copy}</dds-button-cta>
<dds-button-cta cta-type="${ifNonNull(ctaType)}" download="${ifNonNull(download)}" href="${href}">${copy}</dds-button-cta>
</dds-button-group>
</div>
`;
};

Button.story = {
parameters: {
knobs: {
ButtonCTA: ({ groupId }) => {
const ctaType = select('CTA type (cta-type)', types, null, groupId);
const copy = ctaType === CTA_TYPE.VIDEO ? undefined : textNullable('Copy text', 'Lorem ipsum dolor sit amet', groupId);
const download =
ctaType !== CTA_TYPE.DOWNLOAD
? undefined
: textNullable('Download target (download)', 'IBM_Annual_Report_2019.pdf', groupId);
return {
copy,
ctaType,
download,
href: textNullable(knobNamesForType[ctaType ?? CTA_TYPE.REGULAR], hrefsForType[ctaType ?? CTA_TYPE.REGULAR], groupId),
};
},
},
},
};

export const Card = ({ parameters }) => {
const { copy, ctaType, download, href } = parameters?.props?.CardCTA ?? {};
const { copy: footerCopy, download: footerDownload, href: footerHref } = parameters?.props?.CardCTAFooter ?? {};
Expand Down
114 changes: 114 additions & 0 deletions packages/web-components/src/components/cta/button-cta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @license
*
* Copyright IBM Corp. 2020
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { html, property, query, customElement } from 'lit-element';
import ddsSettings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js';
import {
formatVideoCaption,
formatVideoDuration,
} from '@carbon/ibmdotcom-utilities/es/utilities/formatVideoCaption/formatVideoCaption.js';
import DDSButtonGroupItem from '../button-group/button-group-item';
import CTAMixin from '../../component-mixins/cta/cta';
import VideoCTAMixin from '../../component-mixins/cta/video';
import { CTA_TYPE } from './defs';
import styles from './cta.scss';

export { CTA_TYPE };

const { stablePrefix: ddsPrefix } = ddsSettings;

/**
* Button CTA.
*
* @element dds-button-cta
*/
@customElement(`${ddsPrefix}-button-cta`)
class DDSButonCTA extends VideoCTAMixin(CTAMixin(DDSButtonGroupItem)) {
/**
* The button that may work as a link.
* @private
*/
@query('[part="button"]')
_linkNode!: HTMLElement;

/**
* @returns The main content.
*/
protected _renderContent() {
const { ctaType, _hasMainContent: hasMainContent } = this;
if (ctaType !== CTA_TYPE.VIDEO) {
return html`
<slot @slotchange="${this._handleSlotChange}"></slot>
`;
}
const {
videoDuration,
videoName,
formatVideoCaption: formatVideoCaptionInEffect,
formatVideoDuration: formatVideoDurationInEffect,
} = this;
const caption = hasMainContent
? undefined
: formatVideoCaptionInEffect({
duration: formatVideoDurationInEffect({ duration: !videoDuration ? videoDuration : videoDuration * 1000 }),
name: videoName,
});
return html`
<slot @slotchange="${this._handleSlotChange}"></slot>${caption}
`;
}

protected _renderInner() {
return html`
${this._renderContent()}${this._renderIcon()}
`;
}

/**
* The CTA type.
*/
@property({ reflect: true, attribute: 'cta-type' })
ctaType = CTA_TYPE.REGULAR;

/**
* The formatter for the video caption, composed with the video name and the video duration.
* Should be changed upon the locale the UI is rendered with.
*/
@property({ attribute: false })
formatVideoCaption = formatVideoCaption;

/**
* The formatter for the video duration.
* Should be changed upon the locale the UI is rendered with.
*/
@property({ attribute: false })
formatVideoDuration = formatVideoDuration;

/**
* The video duration.
*/
@property({ type: Number, attribute: 'video-duration' })
videoDuration?: number;

/**
* The video name.
*/
@property({ attribute: 'video-name' })
videoName?: string;

/**
* The video thumbnail URL.
* Button CTA does not support video thumbnail, and this property should never be set.
*/
videoThumbnailUrl?: never;

static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
}

export default DDSButonCTA;
Loading

0 comments on commit bbe4744

Please sign in to comment.