Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cta): introduce button variant #4779

Merged
merged 1 commit into from
Dec 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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