diff --git a/packages/styles/scss/components/buttongroup/_buttongroup.scss b/packages/styles/scss/components/buttongroup/_buttongroup.scss index fb8d576e9be..d6ffb8839ae 100644 --- a/packages/styles/scss/components/buttongroup/_buttongroup.scss +++ b/packages/styles/scss/components/buttongroup/_buttongroup.scss @@ -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; diff --git a/packages/web-components/src/components/button-group/button-group.scss b/packages/web-components/src/components/button-group/button-group.scss index 07a883c5ab6..2b4f2aafb31 100644 --- a/packages/web-components/src/components/button-group/button-group.scss +++ b/packages/web-components/src/components/button-group/button-group.scss @@ -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; } diff --git a/packages/web-components/src/components/button/button.ts b/packages/web-components/src/components/button/button.ts index 6a7d3128eea..348ef58dad3 100644 --- a/packages/web-components/src/components/button/button.ts +++ b/packages/web-components/src/components/button/button.ts @@ -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` +

+ ${this._renderInner()} +

+ `; + } + + /** + * @returns The inner content. + */ + protected _renderInner() { + const { _handleSlotChange: handleSlotChange } = this; + return html` + + + `; + } + + /** + * `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 ``. + */ + @property({ reflect: true }) + download!: string; + + /** + * Link `href`. If present, this button is rendered as ``. + */ + @property({ reflect: true }) + href!: string; + + /** + * The language of what `href` points to, if this button is rendered as ``. + */ + @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 ``. + */ + @property({ attribute: 'link-role' }) + linkRole = 'button'; + + /** + * URLs to ping, if this button is rendered as ``. + */ + @property({ reflect: true }) + ping!: string; + + /** + * The link type, if this button is rendered as ``. + */ + @property({ reflect: true }) + rel!: string; + + /** + * Button size. + */ + @property({ reflect: true }) + size = BUTTON_SIZE.REGULAR; + + /** + * The link target, if this button is rendered as ``. + */ + @property({ reflect: true }) + target!: string; + + /** + * The default behavior if the button is rendered as ` + `; + } + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } diff --git a/packages/web-components/src/components/cta/__stories__/cta.stories.ts b/packages/web-components/src/components/cta/__stories__/cta.stories.ts index b69db3c6a32..418721e31bb 100644 --- a/packages/web-components/src/components/cta/__stories__/cta.stories.ts +++ b/packages/web-components/src/components/cta/__stories__/cta.stories.ts @@ -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'; @@ -87,6 +88,39 @@ Text.story = { }, }; +export const Button = ({ parameters }) => { + const { copy, ctaType, download, href } = parameters?.props?.ButtonCTA ?? {}; + return html` +
+ + ${copy} + ${copy} + +
+ `; +}; + +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 ?? {}; diff --git a/packages/web-components/src/components/cta/button-cta.ts b/packages/web-components/src/components/cta/button-cta.ts new file mode 100644 index 00000000000..ed9a77a6daf --- /dev/null +++ b/packages/web-components/src/components/cta/button-cta.ts @@ -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` + + `; + } + const { + videoDuration, + videoName, + formatVideoCaption: formatVideoCaptionInEffect, + formatVideoDuration: formatVideoDurationInEffect, + } = this; + const caption = hasMainContent + ? undefined + : formatVideoCaptionInEffect({ + duration: formatVideoDurationInEffect({ duration: !videoDuration ? videoDuration : videoDuration * 1000 }), + name: videoName, + }); + return html` + ${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; diff --git a/packages/web-components/src/components/cta/cta.scss b/packages/web-components/src/components/cta/cta.scss index b41bb5455f3..44ff1cf44bf 100644 --- a/packages/web-components/src/components/cta/cta.scss +++ b/packages/web-components/src/components/cta/cta.scss @@ -5,9 +5,17 @@ // LICENSE file in the root directory of this source tree. // +@import '../button-group/button-group'; @import '../link-with-icon/link-with-icon'; @import '../feature-card/feature-card'; /* Covers regular card style, too */ .#{$dds-prefix}-ce--cta__icon { flex-shrink: 0; } + +:host(#{$dds-prefix}-button-cta) .#{$dds-prefix}-ce--cta__icon { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); +}