diff --git a/docs-site/src/pages/pattern-lab/_patterns/02-components/blockquote/35-blockquote-web-component.twig b/docs-site/src/pages/pattern-lab/_patterns/02-components/blockquote/35-blockquote-web-component.twig new file mode 100644 index 0000000000..d4982aef3c --- /dev/null +++ b/docs-site/src/pages/pattern-lab/_patterns/02-components/blockquote/35-blockquote-web-component.twig @@ -0,0 +1,87 @@ +{% macro code_example(code, copy) %} + {% spaceless %} + {{ code | replace({ + '<': '<', + '>': '>', + }) | trim | raw }} + {% endspaceless %} +{% endmacro %} + +{% import _self as blockquote_demo %} + +{% set blockquote_demo_simple %} + +

The greater danger for most of us lies not in setting our aim too high and falling short; but in setting our aim too low, and achieving our mark.

+
+{% endset %} + +{% set blockquote_demo_attributes %} + +

The greater danger for most of us lies not in setting our aim too high and falling short; but in setting our aim too low, and achieving our mark.

+
+{% endset %} + +{% set blockquote_demo_logo %} + + PayPal Logo +

The greater danger for most of us lies not in setting our aim too high and falling short...

+

In fact, the greater danger is setting our aim too low and achieving our mark.

+
+{% endset %} + +
+ + Web Component Usage + + + Bolt Link is a web component that renders a semantic blockquote with Bolt styles. For a simple blockquote, wrap your quote content in the <bolt-blockquote> custom element. Note: you must wrap your quote text in <p> tags for the appropriate quotation marks to appear. Add attribution to the quote by adding the author-name, author-title, and author-image attributes to <bolt-blockquote>. + +
+ {% grid "o-bolt-grid--flex o-bolt-grid--matrix o-bolt-grid--center" %} + {% cell "u-bolt-width-12/12" %} + {{ blockquote_demo_simple }} + {% endcell %} + {% endgrid %} +
+
+ {% include blockquote_demo.code_example(blockquote_demo_simple, true) %} +
+
+ +
+ + Additional Options + + + Apply additional configuration options via attributes on the <bolt-blockquote> element. Attribute names and values match the Twig schema but use "kebab-case" instead of "camelCase". For example, alignItems becomes align-items. + +
+ {% grid "o-bolt-grid--flex o-bolt-grid--matrix o-bolt-grid--center" %} + {% cell "u-bolt-width-12/12" %} + {{ blockquote_demo_attributes }} + {% endcell %} + {% endgrid %} +
+
+ {% include blockquote_demo.code_example(blockquote_demo_attributes, true) %} +
+
+ +
+ + Advanced Usage + + + To add a logo to <bolt-blockquote> place logo content (for example: <bolt-logo> or <img>) next to blockquote text, and add the attribute slot="logo" to the logo's outermost container. + +
+ {% grid "o-bolt-grid--flex o-bolt-grid--matrix o-bolt-grid--center" %} + {% cell "u-bolt-width-12/12" %} + {{ blockquote_demo_logo }} + {% endcell %} + {% endgrid %} +
+
+ {% include blockquote_demo.code_example(blockquote_demo_logo, true) %} +
+
\ No newline at end of file diff --git a/docs-site/src/templates/_site-head.twig b/docs-site/src/templates/_site-head.twig index 5023b8db81..b5362c40b6 100644 --- a/docs-site/src/templates/_site-head.twig +++ b/docs-site/src/templates/_site-head.twig @@ -20,7 +20,7 @@ {% set cacheBuster = bolt.data.config.prod ? "?v=" ~ bolt.data.fullManifest.version : "" %} - + @@ -83,5 +83,4 @@ {% endif %} #} - - + \ No newline at end of file diff --git a/packages/components/bolt-blockquote/TESTING.md b/packages/components/bolt-blockquote/TESTING.md new file mode 100644 index 0000000000..40b0f41067 --- /dev/null +++ b/packages/components/bolt-blockquote/TESTING.md @@ -0,0 +1,34 @@ +# Testing Steps + +## Simple Use Case + +As a user, I view a simple Blockquote. [View example »](https://feature-convert-blockquote-to-web-component.boltdesignsystem.com/pattern-lab/patterns/02-components-blockquote-05-blockquote/02-components-blockquote-05-blockquote.html) + +### Quotation + +I verify that: + +- The blockquote contains a quotation. +- The quotation has opening and closing quotation marks. +- If the quotation contains more than one paragraph, the closing quotation mark comes only at the end of the second paragraph. + +### Author information + +Blockquotes may include the author's photo, name, and title after the quotation. + +If there is information about the author, I verify that: + +- The author's photo (optional) is below the quotation, after a small space. +- The author's name and/or title is smaller in size than the quotation. +- The author's name is bold and the author's title is normal font weight. + +### Decoration + +Blockquotes may have decorative borders. + +If there is a decorative border, I verify that: + +- The border color is green. +- The border appears to the left of the quotation content. +- The border spans from the top of the quotation to the last piece of author information. +- There is a small space between the border and the quotation and author content. diff --git a/packages/components/bolt-blockquote/blockquote.schema.yml b/packages/components/bolt-blockquote/blockquote.schema.yml index 3dbff9a13d..08cede6119 100644 --- a/packages/components/bolt-blockquote/blockquote.schema.yml +++ b/packages/components/bolt-blockquote/blockquote.schema.yml @@ -1,4 +1,3 @@ -$schema: http://json-schema.org/draft-04/schema# title: Blockquote type: object required: @@ -8,7 +7,7 @@ properties: type: object description: A Drupal-style attributes object with extra attributes to append to this component. content: - description: Text to appear in blockquote. + description: Text to appear in blockquote (Twig only). May be plain text or text wrapped in

tags. type: string size: description: Text size. @@ -43,19 +42,19 @@ properties: default: false type: boolean logo: - description: Add a logo component. - type: object - ref: '@bolt-components-logo/logo.schema.yml' + description: Add a logo component. + type: object + ref: '@bolt-components-logo/logo.schema.yml' author: - description: Author of the quote. - type: object - properties: - name: - type: string - description: Author's name. - title: - type: string - description: Author's title. - image: - type: object - ref: '@bolt-components-image/image.schema.yml' + description: Author of the quote. + type: object + properties: + name: + type: string + description: Author's name. + title: + type: string + description: Author's title. + image: + type: object + ref: '@bolt-components-image/image.schema.yml' diff --git a/packages/components/bolt-blockquote/index.js b/packages/components/bolt-blockquote/index.js new file mode 100644 index 0000000000..dd654fe05a --- /dev/null +++ b/packages/components/bolt-blockquote/index.js @@ -0,0 +1,5 @@ +import { polyfillLoader } from '@bolt/core/polyfills'; + +polyfillLoader.then(res => { + import(/* webpackMode: 'eager', webpackChunkName: 'bolt-blockquote' */ './src/blockquote'); +}); diff --git a/packages/components/bolt-blockquote/index.scss b/packages/components/bolt-blockquote/index.scss new file mode 100644 index 0000000000..eb5fee97de --- /dev/null +++ b/packages/components/bolt-blockquote/index.scss @@ -0,0 +1 @@ +@import 'src/blockquote.scss'; diff --git a/packages/components/bolt-blockquote/package.json b/packages/components/bolt-blockquote/package.json index f7b904dc36..ec636cdb1a 100755 --- a/packages/components/bolt-blockquote/package.json +++ b/packages/components/bolt-blockquote/package.json @@ -26,13 +26,17 @@ "license": "MIT", "repository": "https://github.com/bolt-design-system/bolt/tree/master/packages/components/bolt-blockquote", "bugs": "https://github.com/bolt-design-system/bolt/issues", - "style": "src/blockquote.scss", "publishConfig": { "access": "public" }, "dependencies": { - "@bolt/core": "^2.3.0-rc.0" + "@bolt/core": "^2.3.0-rc.0", + "@bolt/components-text": "^2.3.0-rc.0", + "@bolt/components-image": "^2.3.0-rc.0" }, + "style": "index.scss", + "main": "index.js", + "twig": "src/blockquote.twig", "schema": "blockquote.schema.yml", "gitHead": "b47538629e315eeecbdfcb8d0c22e787b3bcc089" } diff --git a/packages/components/bolt-blockquote/src/Author/AuthorImage.js b/packages/components/bolt-blockquote/src/Author/AuthorImage.js new file mode 100644 index 0000000000..1515d63cbb --- /dev/null +++ b/packages/components/bolt-blockquote/src/Author/AuthorImage.js @@ -0,0 +1,28 @@ +import { html } from '@bolt/core/renderers/renderer-lit-html'; +import { ifDefined } from 'lit-html/directives/if-defined'; +import classNames from 'classnames/bind'; +import styles from '../blockquote.scss'; + +const cx = classNames.bind(styles); + +export const AuthorImage = elem => { + const { props, slots } = elem; + if (slots['author-image'] || props.authorImage) { + return html` +

+ ${ + slots['author-image'] + ? html` + ${elem.slot('author-image')} + ` + : html` + ${ifDefined(props.authorTitle)} + ` + } +
+ `; + } +}; diff --git a/packages/components/bolt-blockquote/src/Author/AuthorName.js b/packages/components/bolt-blockquote/src/Author/AuthorName.js new file mode 100644 index 0000000000..ed01a49e31 --- /dev/null +++ b/packages/components/bolt-blockquote/src/Author/AuthorName.js @@ -0,0 +1,21 @@ +import { html } from '@bolt/core/renderers/renderer-lit-html'; + +export const AuthorName = elem => { + const { props, slots } = elem; + if (slots['author-name'] || props.authorName) { + return html` + + ${ + elem.slots['author-name'] + ? elem.slot('author-name') + : props.authorName + } + + `; + } +}; diff --git a/packages/components/bolt-blockquote/src/Author/AuthorTitle.js b/packages/components/bolt-blockquote/src/Author/AuthorTitle.js new file mode 100644 index 0000000000..39a064361f --- /dev/null +++ b/packages/components/bolt-blockquote/src/Author/AuthorTitle.js @@ -0,0 +1,16 @@ +import { html } from '@bolt/core/renderers/renderer-lit-html'; + +export const AuthorTitle = elem => { + const { props, slots } = elem; + if (slots['author-title'] || props.authorTitle) { + return html` + + ${ + elem.slots['author-title'] + ? elem.slot('author-title') + : props.authorTitle + } + + `; + } +}; diff --git a/packages/components/bolt-blockquote/src/Author/index.js b/packages/components/bolt-blockquote/src/Author/index.js new file mode 100644 index 0000000000..a36b9d7e94 --- /dev/null +++ b/packages/components/bolt-blockquote/src/Author/index.js @@ -0,0 +1,3 @@ +export { AuthorImage } from './AuthorImage'; +export { AuthorName } from './AuthorName'; +export { AuthorTitle } from './AuthorTitle'; diff --git a/packages/components/bolt-blockquote/src/blockquote.js b/packages/components/bolt-blockquote/src/blockquote.js new file mode 100644 index 0000000000..a87da57c8d --- /dev/null +++ b/packages/components/bolt-blockquote/src/blockquote.js @@ -0,0 +1,211 @@ +import { props, define, hasNativeShadowDomSupport } from '@bolt/core/utils'; +import { withLitHtml, html } from '@bolt/core/renderers/renderer-lit-html'; + +import { convertInitialTags } from '@bolt/core/decorators'; +import classNames from 'classnames/bind'; +import styles from './blockquote.scss'; +import schema from '../blockquote.schema.yml'; +import { AuthorImage, AuthorName, AuthorTitle } from './Author'; + +let cx = classNames.bind(styles); + +@define +@convertInitialTags('blockquote') // The first matching tag will have its attributes converted to component props +class BoltBlockquote extends withLitHtml() { + static is = 'bolt-blockquote'; + + static props = { + size: props.string, + alignItems: props.string, + border: props.string, + indent: props.boolean, + fullBleed: props.boolean, + authorName: props.string, + authorTitle: props.string, + authorImage: props.string, + }; + + // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context + constructor(self) { + self = super(self); + self.useShadow = hasNativeShadowDomSupport; + self.schema = this.getModifiedSchema(schema); + return self; + } + + rendered() { + super.rendered && super.rendered(); + const self = this; + + if (window.MutationObserver) { + // Re-generate slots + re-render when mutations are observed + const mutationCallback = function(mutationsList, observer) { + self.slots = self._checkSlots(); + self.triggerUpdate(); + }; + + // Create an observer instance linked to the callback function + self.observer = new MutationObserver(mutationCallback); + + // Start observing the target node for configured mutations + self.observer.observe(this, { + attributes: false, + childList: true, + subtree: true, + }); + } + } + + disconnected() { + super.disconnected && super.disconnected(); + + // remove MutationObserver if supported + exists + if (window.MutationObserver && this.observer) { + this.observer.disconnect(); + } + } + + getModifiedSchema(schema) { + var modifiedSchema = schema; + + // Remove "content" from schema, does not apply to web component. + for (let property in modifiedSchema.properties) { + if (property === 'content') { + delete modifiedSchema.properties[property]; + } + } + + const index = modifiedSchema.required.indexOf('content'); + modifiedSchema.required.splice(index, 1); + + return modifiedSchema; + } + + getAlignItemsOption(prop) { + switch (prop) { + case 'right': + return 'end'; + case 'center': + return 'center'; + default: + // left => start + return 'start'; + } + } + + getBorderOption(prop) { + switch (prop) { + case 'none': + return 'borderless'; + case 'horizontal': + return 'bordered-horizontal'; + default: + // vertical => bordered-vertical + return 'bordered-vertical'; + } + } + + // automatically adds classes for the first and last slotted item (in the default slot) to help with tricky ::slotted selectors + addClassesToSlottedChildren() { + if (this.slots) { + if (this.slots.default) { + const defaultSlot = []; + + this.slots.default.forEach(item => { + if (item.tagName) { + item.classList.remove('is-first-child'); + item.classList.remove('is-last-child'); // clean up existing classes + defaultSlot.push(item); + } + }); + + if (defaultSlot[0]) { + defaultSlot[0].classList.add('is-first-child'); + + if (defaultSlot.length === 1) { + defaultSlot[0].classList.add('is-last-child'); + } + } + + if (defaultSlot[defaultSlot.length - 1]) { + defaultSlot[defaultSlot.length - 1].classList.add('is-last-child'); + } + } + } + } + + render() { + // validate the original prop data passed along -- returns back the validated data w/ added default values + const { + size, + alignItems, + border, + indent, + fullBleed, + authorName, + authorTitle, + authorImage, + } = this.validateProps(this.props); + + const classes = cx('c-bolt-blockquote', { + [`c-bolt-blockquote--${size}`]: size, + [`c-bolt-blockquote--align-items-${this.getAlignItemsOption( + alignItems, + )}`]: this.getAlignItemsOption(alignItems), + [`c-bolt-blockquote--${this.getBorderOption( + border, + )}`]: this.getBorderOption(border), + [`c-bolt-blockquote--indented`]: indent, + [`c-bolt-blockquote--full`]: fullBleed, + }); + + let footerItems = []; + footerItems.push(AuthorImage(this), AuthorName(this), AuthorTitle(this)); + + this.addClassesToSlottedChildren(); + + return html` + ${this.addStyles([styles])} +
+ ${ + this.slots.logo + ? html` +
+ ${this.slot('logo')} +
+ ` + : '' + } +
+ + ${this.slot('default')} + +
+ ${ + footerItems.length > 0 + ? html` + + ` + : '' + } +
+ `; + } +} + +export { BoltBlockquote }; diff --git a/packages/components/bolt-blockquote/src/blockquote.scss b/packages/components/bolt-blockquote/src/blockquote.scss index 1c4b1a7b37..bcb7d74625 100644 --- a/packages/components/bolt-blockquote/src/blockquote.scss +++ b/packages/components/bolt-blockquote/src/blockquote.scss @@ -1,46 +1,3 @@ -/* ------------------------------------ *\ - Blockquote -\* ------------------------------------ */ - -// Sample Usage -// -//
-// -//
-//
-// -//

This is the quote.

-//
-//
-//
-// -//
-//
- @import '@bolt/core'; // Local Variables @@ -53,11 +10,9 @@ $bolt-blockquote-image-border-style: $bolt-border-style; $bolt-blockquote-image-border-color: rgba(bolt-color(gray), 0.2); $bolt-blockquote-image-size: 4rem; - // Register Custom Block Element @include bolt-custom-element('bolt-blockquote', block, medium); - // Blockquote container .c-bolt-blockquote { @include bolt-margin(0); @@ -81,14 +36,12 @@ $bolt-blockquote-image-size: 4rem; } } - // Logo .c-bolt-blockquote__logo { @include bolt-margin-bottom(small); display: block; } - // Quotation .c-bolt-blockquote__quote { @include bolt-margin-bottom(medium); @@ -97,31 +50,61 @@ $bolt-blockquote-image-size: 4rem; max-width: 44rem; color: bolt-theme(headline); - p:first-child:before, - p:last-child:after { + p:not([slot]):first-child:before, + p:not([slot]):last-child:after { font-family: 'Georgia', serif; // TODO: Replace with Noto Serif when it is added. } - p:first-child:before { + p:not([slot]):first-child:before { content: '\201C'; } - p:last-child:after { + p:not([slot]):last-child:after { content: '\201D'; } -} + ::slotted(p:first-child), + ::slotted(p.is-first-child), + ::slotted(p:last-child), + ::slotted(p.is-last-child) { + &:before, + &:after { + font-family: 'Georgia', serif; // TODO: Replace with Noto Serif when it is added. + } + } + + ::slotted(p:first-child), + ::slotted(p.is-first-child) { + &:before { + content: '\201C'; + } + } + + ::slotted(p:last-child), + ::slotted(p.is-last-child) { + &:after { + content: '\201D'; + } + } +} // Attribution .c-bolt-blockquote__image { + @include bolt-margin-bottom(small); display: inline-block; + box-sizing: border-box; width: $bolt-blockquote-image-size; height: $bolt-blockquote-image-size; overflow: hidden; + vertical-align: middle; border-radius: 50%; border-width: $bolt-blockquote-image-border-width; border-style: $bolt-blockquote-image-border-style; border-color: $bolt-blockquote-image-border-color; + + > * { + max-width: 100%; + } } .c-bolt-blockquote__footer { @@ -130,7 +113,6 @@ $bolt-blockquote-image-size: 4rem; } .c-bolt-blockquote__footer-item { - @include bolt-margin-bottom(small); display: block; &:last-child { @@ -138,7 +120,6 @@ $bolt-blockquote-image-size: 4rem; } } - // Horizontal alignment of items inside .c-bolt-blockquote--align-items-start { text-align: left; @@ -158,6 +139,11 @@ $bolt-blockquote-image-size: 4rem; @include bolt-margin-right(auto); @include bolt-margin-left(auto); } + + .c-bolt-blockquote__logo > *::slotted(*) { + @include bolt-margin-right(auto); + @include bolt-margin-left(auto); + } } .c-bolt-blockquote--align-items-end { @@ -168,17 +154,25 @@ $bolt-blockquote-image-size: 4rem; @include bolt-margin-right(0); @include bolt-margin-left(auto); } -} + .c-bolt-blockquote__logo > *::slotted(*) { + @include bolt-margin-right(0); + @include bolt-margin-left(auto); + } +} // Border Options .c-bolt-blockquote--bordered-vertical { @include bolt-padding(0 medium); border-style: $bolt-blockquote-border-style; border-color: $bolt-blockquote-border-color; - border-color: var(--bolt-theme-blockquote-border, $bolt-blockquote-border-color); + border-color: var( + --bolt-theme-blockquote-border, + $bolt-blockquote-border-color + ); - &:before, &:after { + &:before, + &:after { display: none; } @@ -205,19 +199,20 @@ $bolt-blockquote-image-size: 4rem; } .c-bolt-blockquote--bordered-horizontal { - &:before, &:after { + &:before, + &:after { display: inline-block; display: inline-flex; } } .c-bolt-blockquote--borderless { - &:before, &:after { + &:before, + &:after { display: none; } } - // Full bleed. Text takes up full width of screen instead of hitting a max width .c-bolt-blockquote--full { .c-bolt-blockquote__quote { @@ -225,7 +220,6 @@ $bolt-blockquote-image-size: 4rem; } } - // Indent options .c-bolt-blockquote--indented { @include bolt-margin(0 medium); @@ -239,7 +233,6 @@ $bolt-blockquote-image-size: 4rem; } } - // Perfecting the hanging quotation mark's position in all browsers. .c-bolt-blockquote--align-items-start { .c-bolt-blockquote__quote { @@ -247,6 +240,14 @@ $bolt-blockquote-image-size: 4rem; position: absolute; transform: translate3d(-110%, 0, 0); } + + ::slotted(p:first-child), + ::slotted(p.is-first-child) { + &:before { + position: absolute; + transform: translate3d(-110%, 0, 0); + } + } } } @@ -255,6 +256,13 @@ $bolt-blockquote-image-size: 4rem; p:first-child:before { @include bolt-padding(0 2px); } + + ::slotted(p:first-child), + ::slotted(p.is-first-child) { + &:before { + @include bolt-padding(0 2px); + } + } } } @@ -264,9 +272,24 @@ $bolt-blockquote-image-size: 4rem; @include bolt-padding(0 2px); } + ::slotted(p:first-child), + ::slotted(p.is-first-child) { + &:before { + @include bolt-padding(0 2px); + } + } + p:last-child:after { position: absolute; - transform: translate3d(10%, 0 ,0); + transform: translate3d(10%, 0, 0); + } + + ::slotted(p:last-child), + ::slotted(p.is-last-child) { + &:after { + position: absolute; + transform: translate3d(10%, 0, 0); + } } } } diff --git a/packages/components/bolt-blockquote/src/blockquote.twig b/packages/components/bolt-blockquote/src/blockquote.twig index 9011fcacc8..c67bb1dc8f 100644 --- a/packages/components/bolt-blockquote/src/blockquote.twig +++ b/packages/components/bolt-blockquote/src/blockquote.twig @@ -1,125 +1,125 @@ -{# Sample Usage - {% include "@bolt/twig" with { - // Default is large. [large, xlarge, xxlarge] - "size": "large", - - // Default is left. [left, center, right] - "alignItems": "left", - - // Default is vertical. [vertical, horizontal, none] - "border": "vertical", - - // Default is false. [true, false] - "fullBleed": false, - - // Logo is optional. - "logo": { - "src": "/images/sample/PayPal-logo.svg" - }, - - // Content is required. - "content": "

The greater danger for most of us lies not in setting our aim too high and falling short; but in setting our aim too low, and achieving our mark.

", - - // Author is optional. - "author": { - "image": { - "src": "/images/placeholders/500x500.jpg" - }, - "name": "Michelangelo di Lodovico Buonarroti Simoni", - "title": "Renaissance Artist" - } - } only %} -#} +{% set schema = bolt.data.components["@bolt-components-blockquote"].schema %} {% if enable_json_schema_validation %} - {{ validate_data_schema(bolt.data.components['@bolt-components-blockquote'].schema, _self) | raw }} + {{ validate_data_schema(schema, _self) | raw }} {% endif %} -{% set prefix = "c-bolt-" %} - -{% set sizeOptions = [ - "large", - "xlarge", - "xxlarge" -] %} - -{% set alignItemsOptions = { +{# Variables #} +{% set base_class = "c-bolt-blockquote" %} +{% set props = create_attribute({ + "size": size, + "align-items": alignItems, + "border": border, + "indent": indent, + "full-bleed": fullBleed, + "author-name": author.name, + "author-title": author.title, + "author-title": author.title, + "author-image": author.image +}) %} +{% set attributes = merge_attributes(create_attribute(attributes|default({})), props) %} +{% set inner_attributes = create_attribute({}) %} + +{# Required by Blockquote to map prop values to strings used in classname #} +{% set align_items_options = { "left": "start", "center": "center", "right": "end" } %} -{% set borderOptions = { - "none": "borderless", - "vertical": "bordered-vertical", - "horizontal": "bordered-horizontal" -} %} - -{% set attributes = create_attribute(attributes|default({})) %} - - -{% set componentName = "blockquote" %} -{% set baseClass = prefix ~ componentName %} -{% set size = size == false and size is null ? "xlarge" : size | default("xlarge") %} -{% set alignItems = alignItems == false and alignItems is null ? "left" : alignItems | default("left") %} -{% set border = border == false and border is null ? "vertical" : border | default("vertical") %} -{% set fullBleed = fullBleed == false and fullBleed is null ? "false" : "true" %} +{# Blockquote content is not required to be wrapped in a

tag, but if it is, update variables accordingly #} +{% set quote_tag = "

" in content ? "replace-with-grandchildren" : "replace-with-children" %} +{% set text_tag = "

" in content ? "div" : "p" %} +{# Check that the component's current prop values are valid. if not, default to the schema default #} +{% set size = size in schema.properties.size.enum ? size : schema.properties.size.default %} +{% set align_items = alignItems in schema.properties.alignItems.enum ? alignItems : schema.properties.alignItems.default %} +{% set border = border in schema.properties.border.enum ? border : schema.properties.border.default %} +{# Array of classes based on the defined + default props #} {% set classes = [ - baseClass, - size in sizeOptions ? baseClass ~ "--" ~ size : "", - alignItems in alignItemsOptions|keys ? baseClass ~ "--align-items-" ~ alignItemsOptions[alignItems], - border in borderOptions|keys ? baseClass ~ "--" ~ borderOptions[border], - indent ? baseClass ~ "--indented" : "", - fullBleed == "true" ? baseClass ~ "--full" : "" + base_class, + size in schema.properties.size.enum ? base_class ~ "--" ~ size : "", + align_items in align_items_options|keys ? base_class ~ "--align-items-" ~ align_items_options[align_items], + border == 'none' ? base_class ~ "--borderless" : base_class ~ "--bordered-" ~ border, + indent ? base_class ~ "--indented" : "", + fullBleed ? base_class ~ "--full" : "" ] %} - -

+{# + Sort classes passed in via attributes into two groups: + 1. Those that should be applied to the inner tag (namely, "is-" and "has-" classes) + 2. Those that should be applied to the outer custom element (everything else EXCEPT c-bolt-* classes, which should never be passed in via attributes) +#} +{% set outer_classes = [] %} +{% set inner_classes = classes %} + +{% for class in attributes["class"] %} + {% if class starts with "is-" or class starts with "has-" %} + {% set inner_classes = inner_classes|merge([class]) %} + {% elseif class starts with "c-bolt-" == false %} + {% set outer_classes = outer_classes|merge([class]) %} + {% endif %} +{% endfor %} + + +
{% if logo %} - {% block blockquote_logo %} -
- {% include "@bolt/logo.twig" with logo only %} -
- {% endblock %} + + {% include "@bolt/logo.twig" with logo|merge({ + "lazyload": false, + slot: "logo", + }) only %} + {% endif %} - {% block blockquote_quote %} -
- {% include "@bolt-components-headline/text.twig" with { - text: content, - tag: "div", - size: size, - weight: "semibold" - } only %} -
- {% endblock %} + + <{{quote_tag}} class="{{ "#{base_class}__quote" }}"> + {% include "@bolt-components-headline/text.twig" with { + text: content, + tag: text_tag, + size: size, + weight: "semibold" + } only %} + + {% if author %} - {% block blockquote_footer %} -
- {% if author.image %} -
-
- {% include "@bolt/image.twig" with author.image only %} -
-
- {% endif %} -
- {% include "@bolt-components-headline/text.twig" with { - text: author.name, - tag: "cite", - size: "xsmall", - weight: "bold" - } only %} + +
+ {% if author.image %} + +
+ {% include "@bolt/image.twig" with author.image|merge({"lazyload": false, slot:"author-image"}) only %} +
+
+ {% endif %} + + {% if author.name %} + + {% include "@bolt-components-headline/text.twig" with { + text: author.name, + tag: "cite", + size: "xsmall", + weight: "bold", + slot: "author-name" + } %} + + {% endif %} + {% if author.title %} - {% include "@bolt-components-headline/text.twig" with { - text: author.title, - size: "xsmall" - } only %} + + {% include "@bolt-components-headline/text.twig" with { + text: author.title, + tag: "cite", + size: "xsmall", + slot: "author-title" + } only %} + {% endif %} -
- {% endblock %} + {% endif %}
- +
diff --git a/packages/components/bolt-headline/src/_typography.twig b/packages/components/bolt-headline/src/_typography.twig index f382744163..b5f78d30e3 100644 --- a/packages/components/bolt-headline/src/_typography.twig +++ b/packages/components/bolt-headline/src/_typography.twig @@ -64,6 +64,10 @@ {% set longTitle = true %} {% endif %} +{% if slot %} + {% set attributes = attributes.setAttribute("slot", slot) %} +{% endif %} + {% set classes = [ baseClass, @@ -77,7 +81,7 @@ iconPosition ? baseClass ~ "--icon-position-" ~ iconPosition : "" ] %} -<{{ tag }} {{ attributes.addClass(classes) }}> +<{{ tag }} {{ attributes.addClass(classes) }}>{% spaceless %} {% if icon and not url and iconPosition == "before" %} {% include "@bolt/icon.twig" with icon only %} @@ -105,4 +109,4 @@ {% include "@bolt/icon.twig" with icon only %} {% endif %} - +{% endspaceless %} diff --git a/packages/components/bolt-image/src/image.twig b/packages/components/bolt-image/src/image.twig index 7ab7b93447..648fec49f9 100644 --- a/packages/components/bolt-image/src/image.twig +++ b/packages/components/bolt-image/src/image.twig @@ -83,7 +83,7 @@ }} /> {% endset %} - + {% block image_content %} {% if width > 0 and height > 0 and useAspectRatio == true %} {% include "@bolt-components-ratio/ratio.twig" with { diff --git a/packages/components/bolt-logo/src/logo.twig b/packages/components/bolt-logo/src/logo.twig index 20f1e019de..5a50b900ec 100644 --- a/packages/components/bolt-logo/src/logo.twig +++ b/packages/components/bolt-logo/src/logo.twig @@ -2,6 +2,6 @@ {{ validate_data_schema(bolt.data.components['@bolt-components-logo'].schema, _self) | raw }} {% endif %} - + {{ include('@bolt-components-image/image.twig', with_context = true) }} diff --git a/packages/components/bolt-text/src/text.js b/packages/components/bolt-text/src/text.js index 1a85eb8248..1b1f5734b8 100644 --- a/packages/components/bolt-text/src/text.js +++ b/packages/components/bolt-text/src/text.js @@ -1,5 +1,5 @@ import { polyfillLoader } from '@bolt/core/polyfills'; polyfillLoader.then(res => { - import('./text.standalone.js'); + import(/* webpackMode: 'eager', webpackChunkName: 'bolt-text' */ './text.standalone'); }); diff --git a/packages/components/bolt-text/src/text.standalone.js b/packages/components/bolt-text/src/text.standalone.js index 221aa3b893..ea4e09148c 100644 --- a/packages/components/bolt-text/src/text.standalone.js +++ b/packages/components/bolt-text/src/text.standalone.js @@ -292,6 +292,10 @@ class BoltText extends withLitHtml() { return html` ${innerHTML} `; + case 'cite': + return html` + ${innerHTML} + `; default: return html`

${innerHTML}

diff --git a/packages/components/bolt-text/src/text.twig b/packages/components/bolt-text/src/text.twig index 78d04ebaa6..717f3f26e5 100644 --- a/packages/components/bolt-text/src/text.twig +++ b/packages/components/bolt-text/src/text.twig @@ -78,6 +78,10 @@ {% set attributes = attributes.setAttribute("eyebrow", "") %} {% endif %} +{% if slot %} + {% set attributes = attributes.setAttribute("slot", slot) %} +{% endif %} + {# Icon specific attributes #} {# this is for when someone is using an eyebrow, headline, or subheadline with url but doesn't want the chevron right #} diff --git a/packages/components/bolt-text/text.schema.yml b/packages/components/bolt-text/text.schema.yml index 16bcf21035..21e3de6a6d 100644 --- a/packages/components/bolt-text/text.schema.yml +++ b/packages/components/bolt-text/text.schema.yml @@ -24,6 +24,7 @@ properties: - p - div - span + - cite display: type: string description: Inline text or a block of text. diff --git a/packages/core-php/src/TwigExtensions/BoltExtras.php b/packages/core-php/src/TwigExtensions/BoltExtras.php index 2c6dc34432..6bf732dd93 100644 --- a/packages/core-php/src/TwigExtensions/BoltExtras.php +++ b/packages/core-php/src/TwigExtensions/BoltExtras.php @@ -20,7 +20,8 @@ public function getFunctions() { Bolt\TwigFunctions::link(), Bolt\TwigFunctions::getSpacingScaleSequence(), Bolt\TwigFunctions::github_url(), - Bolt\TwigFunctions::inlineFile() + Bolt\TwigFunctions::inlineFile(), + Bolt\TwigFunctions::merge_attributes() ]; } diff --git a/packages/core-php/src/TwigFunctions.php b/packages/core-php/src/TwigFunctions.php index e121a0888d..e7755a6e09 100644 --- a/packages/core-php/src/TwigFunctions.php +++ b/packages/core-php/src/TwigFunctions.php @@ -265,6 +265,23 @@ public static function create_attribute() { }); } + // Custom function for merging Drupal Attribute objects + // Gives $source preference, unless a key is set in both arrays and $source value is empty or null + public static function merge_attributes() { + return new Twig_SimpleFunction('merge_attributes', function($target, $source) { + // For each key in $source... + foreach ($source as $key => $value) { + // If $key is not in $target, or if $key is in $target and $value in $source is empty, add/overwrite $key in $target + // NOTE: empty() and is_null() do not work in the second half of this statement. Why is that? + if (empty($target[$key]) || (!empty($target[$key]) && $value != "")) { + $target[$key] = $value; + } + } + + return $target; + }); + } + public static function github_url() { return new Twig_SimpleFunction('github_url', function(\Twig_Environment $env, $twigPath) { $filePath = TwigTools\Utils::resolveTwigPath($env, $twigPath); diff --git a/packages/core/decorators/convert-initial-tags.js b/packages/core/decorators/convert-initial-tags.js index 98e5bf2144..73c05b4585 100644 --- a/packages/core/decorators/convert-initial-tags.js +++ b/packages/core/decorators/convert-initial-tags.js @@ -4,12 +4,13 @@ * Example: `` will convert attributes on an `` into component props. * * @param {(string|string[])} tags - A tag name or a list of tag names. - * @returns {Object} - The original Class with extended `connecting()` method + * @param {boolean} moveChildrenToRoot - If true, moves children of the root element to the custom element root. + * @returns {Object} - The original Class with extended `connecting()` method. */ import { getComponentRootElement } from '@bolt/core/utils'; -export function convertInitialTags(tags) { +export function convertInitialTags(tags, moveChildrenToRoot = true) { return target => { return class extends target { connecting() { @@ -23,9 +24,11 @@ export function convertInitialTags(tags) { if (rootElement) { this.rootElement = document.createDocumentFragment(); - // Take any child elements and move them to the root of the custom element - while (rootElement.firstChild) { - this.appendChild(rootElement.firstChild); + if (moveChildrenToRoot) { + // Take any child elements and move them to the root of the custom element + while (rootElement.firstChild) { + this.appendChild(rootElement.firstChild); + } } this.rootElement.appendChild(rootElement);