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 %}
+
+
+ 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`
+
+ `
+ }
+
+ `;
+ }
+};
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 %}
+ {{quote_tag}}>
+
{% if author %}
- {% block blockquote_footer %}
-
- {% 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 %}
-{{ tag }}>
+{% endspaceless %}{{ tag }}>
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);