Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/heavy-teachers-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudfour/patterns': minor
---

Add initial version of Comment component
16,250 changes: 4,629 additions & 11,621 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@storybook/html": "6.2.9",
"@svgr/webpack": "5.5.0",
"@types/jest": "26.0.23",
"@types/lodash": "^4.14.171",
"@types/prismjs": "1.16.5",
"@whitespace/storybook-addon-html": "5.0.0",
"@wordpress/block-library": "3.2.9",
Expand All @@ -75,6 +76,7 @@
"gulp-sass": "4.1.1",
"gulp-svgmin": "3.0.0",
"html-to-react": "1.4.5",
"jabber": "^1.2.2",
"jest": "27.0.6",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
Expand Down
2 changes: 1 addition & 1 deletion src/base/_themes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
--theme-color-text-base: #{color.$text-light};
--theme-color-text-code: var(--theme-color-text-emphasis);
--theme-color-text-emphasis: #{color.$text-light-emphasis};
--theme-color-text-muted: inherit;
--theme-color-text-muted: #{color.$text-light};
--theme-opacity-del: 1;
}

Expand Down
10 changes: 5 additions & 5 deletions src/components/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
-webkit-appearance: none; /* stylelint-disable-line property-no-vendor-prefix */
appearance: none; /* 2 */
background-color: color.$text-light;
border: size.$edge-medium solid currentColor;
border: size.$edge-control solid currentColor;
border-radius: size.$border-radius-medium;
color: color.$text-dark;
cursor: pointer;
Expand Down Expand Up @@ -76,13 +76,13 @@
background-position: center;
background-repeat: no-repeat;
background-size: contain;
bottom: size.$edge-medium; /* 1 */
bottom: size.$edge-control; /* 1 */
content: '';
left: size.$edge-medium; /* 1 */
left: size.$edge-control; /* 1 */
opacity: 0; /* 2 */
position: absolute;
right: size.$edge-medium; /* 1 */
top: size.$edge-medium; /* 1 */
right: size.$edge-control; /* 1 */
top: size.$edge-control; /* 1 */
transform: scale(0); /* 2 */
transition-duration: transition.$quick;
transition-property: opacity, transform;
Expand Down
131 changes: 131 additions & 0 deletions src/components/comment/comment.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
@use 'sass:math';
@use '../../compiled/tokens/scss/color';
@use '../../compiled/tokens/scss/font-weight';
@use '../../compiled/tokens/scss/size';
@use '../../mixins/headings';
@use '../../mixins/theme';

.c-comment {
display: grid;
grid-column-gap: size.$spacing-gap-inline-medium;
grid-row-gap: size.$rhythm-condensed;
grid-template-areas:
'object header'
'thread-line content'
'thread-line footer';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is clever. 👍 CSS Grid is awesome

// We set the object column size directly (instead of relying on `auto`)
// because the container for child comments will be setting a negative margin
// based on this value, and we don't want the layout to break if the wrong
// avatar size is set in the HTML.
grid-template-columns: #{size.$square-avatar-small} 1fr;
grid-template-rows: minmax(0, auto) 1fr minmax(0, auto);
}

/// Display a vertical line if the comment contains a reply thread to connect
/// those comments visually with their parent.
.c-comment--thread::after {
border-radius: size.$border-radius-full;
content: '';
display: block;
grid-area: thread-line;
height: 100%;
margin-left: auto;
margin-right: auto;
width: size.$edge-medium;

@include theme.styles() {
background-color: color.$base-gray-light;
}

@include theme.styles(dark) {
background-color: color.$base-blue-darker;
}
}

/// Named object for consistency with the Media component.
.c-comment__object {
grid-area: object;
}

.c-comment__header {
// We generally want comment contents to align to the start/top, but the
// heading looks better center-aligned with the avatar.
align-self: center;
grid-area: header;
}

/// The heading level will change depending on comment depth, but we want it to
/// appear consistent. We lighten the weight a bit from usual headings to offset
/// it from the main article/page content.
.c-comment__heading {
@include headings.level($level: 3, $include-weight: false);
font-weight: font-weight.$semibold;
}

.c-comment__content {
grid-area: content;
// The fluid heading size changes depending on the viewport, but the content
// always looks just a *tad* close to the header. This offsets that.
margin-top: math.div(size.$rhythm-condensed, -4);
}

.c-comment__footer {
grid-area: footer;
}

.c-comment__meta {
align-items: center;
display: flex;
flex-wrap: wrap;
}

.c-comment__meta-detail {
color: var(--theme-color-text-muted);

&:not(:last-child) {
margin-right: size.$spacing-gap-inline-small;
}
}

/// We don't want to reduce the padding of a button, but we also don't want the
/// overall height of the meta elements changing between comments with a button
/// and comments without. This negates the margin on the button's container
/// to minimize this layout shift.
///
/// The horizontal value is less than the vertical value to account for the
/// possibility of icons being present (which rest closer to the edge than the
/// text label would on its own).
.c-comment__meta-control {
// If the edge and padding sizes for controls use the same units, just crunch
// the numbers in Sass. Otherwise, use `calc` to do it in the browser.
@if (math.compatible(size.$edge-control, size.$padding-control-vertical)) {
margin: ((size.$edge-control + size.$padding-control-vertical) * -1)
(math.div((size.$edge-control + size.$padding-control-horizontal), -2));
} @else {
margin: calc(
(#{size.$edge-control} + #{size.$padding-control-vertical}) * -1
)
calc((#{size.$edge-control} + #{size.$padding-control-horizontal}) / -2);
}
}

/// Normally we would avoid obscuring links, but in this case things like
/// permalinks and source links are quite tertiary in comparison to the main
/// content. I found several accessibility resources that also do this in their
/// comments (Deque being the most prominent example), so I feel okay about it!
.c-comment__meta-link {
&:not(:hover):not(:focus) {
color: inherit;
text-decoration: inherit;
}
}

.c-comment__replies {
// Visually centers the child avatars with the indentation of the parent
// comment. Without this, the replies feel nested too far to the right.
margin-left: math.div(size.$square-avatar-small, -2);
// Since the replies are likely wrapped in a Rhythm object intended to inherit
// the parent rhythm, we can use that custom property to inherit that same
// spacing between the meta and replies.
margin-top: var(--rhythm, #{size.$rhythm-default});
}
78 changes: 78 additions & 0 deletions src/components/comment/comment.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Story, Canvas, Meta } from '@storybook/addon-docs/blocks';
import { makeComment } from './demo/data.ts';
import template from './comment.twig';

<Meta title="Components/Comment" />

# Comment

Displays a single comment in a comment thread, optionally with replies. Multiple comments can be displayed within [a Rhythm layout object](/docs/objects-rhythm--default-story).

## Status

This component is still a work in progress. The following features are still in development:

- Indicating when a comment's author is a Cloud Four team member.
- Displaying a message when a comment is not yet approved.
- Integrating the comment reply form.
- Adding blocks to the template to allow for more customization.

## Single

At minimum, a single comment consists of:

- Author name
- Author avatar
- Comment content (HTML)
- Publication date
- Permalink to the comment

This information may be passed to the component as a `comment` object matching the structure of a [Timber comment](https://timber.github.io/docs/reference/timber-comment/).

<Canvas>
<Story name="Single">{template({ comment: makeComment() })}</Story>
</Canvas>

## With source

Additionally, a `source` object may be passed to the template consisting of a `url` and `name`. This is helpful if integrating [webmentions](https://indieweb.org/Webmention) into comment threads.

<Canvas>
<Story name="With source">
{template({
comment: makeComment(),
source: {
url: 'https://twitter.com/smashingmag/status/1371521325236416516',
name: 'twitter.com',
},
})}
</Story>
</Canvas>

## With reply button

This feature is still a work in progress.

<Canvas>
<Story name="With reply button">
{template({
comment: makeComment(),
demo_control: true,
})}
</Story>
</Canvas>

## With reply thread

If a `comment` contains an array of `children`, they will be display in the footer of the original comment. A vertical line is used to visually associate the replies with the original comment. If the parent comment is contained within [a Rhythm layout object](/docs/objects-rhythm--default-story),the child comments will inherit that spacing.

While it is theoretically possible to infinitely nest `children`, it's recommended to limit the depth to a single level. This is for two reasons:

- Increasingly narrow comments will become difficult to read on narrow screens.
- The heading levels for comment threads increment up to a maximum of six (since HTML only provides six heading levels). This means that levels beyond that will be harder to traverse for user agents navigating via the document outline.

<Canvas>
<Story name="With reply thread">
{template({ comment: makeComment({ replies: 2 }) })}
</Story>
</Canvas>
90 changes: 90 additions & 0 deletions src/components/comment/comment.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{% set _heading_depth = min(heading_depth|default(3), 6) %}

<article class="c-comment{% if comment.children is not empty %} c-comment--thread{% endif %}" id="comment-{{comment.ID}}">
<header class="c-comment__header">
<h{{_heading_depth}} class="c-comment__heading">
{{comment.author.name}}
<span class="u-hidden-visually">
{% if comment.is_child %}replied{% else %}said{% endif %}:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much enjoyed this audible experience, thank you! ❤️

</span>
</h{{_heading_depth}}>
</header>
<div class="c-comment__object">
{% include '@cloudfour/components/avatar/avatar.twig' with {
src: comment.avatar,
size: 'full'
} only %}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we confident there will always be avatars when using this pattern? (e.g. will we use Wordpress's fallbacks or something else if there's no avatar?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and if the avatar is omitted, you'll just see a placeholder circle with the current pattern, so it's pretty fault-tolerant.

{% embed '@cloudfour/objects/rhythm/rhythm.twig' with {
class: 'c-comment__content o-rhythm--condensed'
} %}
{% block content %}
{{comment.comment_content|raw}}
{% endblock %}
{% endembed %}
<footer class="c-comment__footer">
<div class="c-comment__meta">
<div class="c-comment__meta-detail">
<a class="c-comment__meta-link"
href="{{comment.link}}">
<span class="c-comment__meta-icon">
{% include '@cloudfour/components/icon/icon.twig' with {
name: 'link',
inline: true,
aria_hidden: 'true',
} only %}
</span>
<span class="u-hidden-visually">
Permalink to {{comment.author.name}}’s
</span>
<time datetime="{{comment.date|date('Y-m-d')}}">
{{comment.date|date('M j, Y')}}
</time>
<span class="u-hidden-visually">
{% if comment.is_child %}reply{% else %}comment{% endif %}
</span>
</a>
</div>
{% if source %}
<div class="c-comment__meta-detail">
via <a class="c-comment__meta-link" href="{{source.url}}">{{source.name}}</a>
</div>
{% endif %}
{#
TODO: Replace `demo_control` with a more meaningful block or properties
once we have a better idea of how we want to implement the reply
functionality. For now, this property exists to allow us to test the
presentation of the control.
#}
{% if demo_control %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we add a comment explaining this var name? It confused me until I went back to the docs page

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Paul-Hebert Good idea, I've added a TODO comment with more context.

<div class="c-comment__meta-control">
{% include '@cloudfour/components/button/button.twig' with {
type: 'button',
class: 'c-button--tertiary',
content_start_icon: 'speech-balloon',
label: 'Reply'
} only %}
</div>
{% endif %}
</div>
{% if comment.children is not empty %}
{% set _section_heading_depth = min(_heading_depth + 1, 6) %}
{% set _child_heading_depth = min(_section_heading_depth + 1, 6) %}
<h{{_section_heading_depth}} class="u-hidden-visually">
Replies to {{comment.author.name}}
</h{{_section_heading_depth}}>
{% embed '@cloudfour/objects/rhythm/rhythm.twig' with {
class: 'c-comment__replies'
} %}
{% block content %}
{% for child in comment.children %}
{% include '@cloudfour/components/comment/comment.twig' with {
comment: child,
heading_depth: _child_heading_depth
} only %}
{% endfor %}
{% endblock %}
{% endembed %}
{% endif %}
</footer>
</article>
Loading