diff --git a/src/components/ContentNode.vue b/src/components/ContentNode.vue
index 413949bcf..6b3f257ec 100644
--- a/src/components/ContentNode.vue
+++ b/src/components/ContentNode.vue
@@ -23,6 +23,8 @@ import Table from './ContentNode/Table.vue';
import StrikeThrough from './ContentNode/StrikeThrough.vue';
import Small from './ContentNode/Small.vue';
import BlockVideo from './ContentNode/BlockVideo.vue';
+import Column from './ContentNode/Column.vue';
+import Row from './ContentNode/Row.vue';
const BlockType = {
aside: 'aside',
@@ -37,6 +39,7 @@ const BlockType = {
dictionaryExample: 'dictionaryExample',
small: 'small',
video: 'video',
+ row: 'row',
};
const InlineType = {
@@ -299,6 +302,15 @@ function renderNode(createElement, references) {
})
) : null;
}
+ case BlockType.row: {
+ return createElement(
+ Row, { props: { columns: node.numberOfColumns } }, node.columns.map(col => (
+ createElement(
+ Column, { props: { span: col.size } }, renderChildren(col.content),
+ )
+ )),
+ );
+ }
case InlineType.codeVoice:
return createElement(CodeVoice, {}, (
node.code
diff --git a/src/components/ContentNode/Column.vue b/src/components/ContentNode/Column.vue
new file mode 100644
index 000000000..82d32a44a
--- /dev/null
+++ b/src/components/ContentNode/Column.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ContentNode/Row.vue b/src/components/ContentNode/Row.vue
new file mode 100644
index 000000000..e400fceeb
--- /dev/null
+++ b/src/components/ContentNode/Row.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/unit/components/ContentNode.spec.js b/tests/unit/components/ContentNode.spec.js
index 744953b0c..def82b972 100644
--- a/tests/unit/components/ContentNode.spec.js
+++ b/tests/unit/components/ContentNode.spec.js
@@ -24,6 +24,8 @@ import LinkableHeading from 'docc-render/components/ContentNode/LinkableHeading.
import StrikeThrough from 'docc-render/components/ContentNode/StrikeThrough.vue';
import Small from '@/components/ContentNode/Small.vue';
import BlockVideo from '@/components/ContentNode/BlockVideo.vue';
+import Row from '@/components/ContentNode/Row.vue';
+import Column from '@/components/ContentNode/Column.vue';
const { TableHeaderStyle } = ContentNode.constants;
@@ -342,6 +344,54 @@ describe('ContentNode', () => {
});
});
+ describe('with type="row"', () => {
+ it('renders a `` and ``', () => {
+ const wrapper = mountWithItem({
+ type: 'row',
+ numberOfColumns: 4,
+ columns: [
+ {
+ size: 2,
+ content: [
+ {
+ type: 'paragraph',
+ inlineContent: [
+ {
+ type: 'text',
+ text: 'foo',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ content: [
+ {
+ type: 'paragraph',
+ inlineContent: [
+ {
+ type: 'text',
+ text: 'bar',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ const grid = wrapper.find(Row);
+ expect(grid.props()).toEqual({
+ columns: 4,
+ });
+ const columns = grid.findAll(Column);
+ expect(columns).toHaveLength(2);
+ expect(columns.at(0).props()).toEqual({ span: 2 });
+ expect(columns.at(0).find('p').text()).toBe('foo');
+ expect(columns.at(1).props()).toEqual({ span: null });
+ expect(columns.at(1).find('p').text()).toBe('bar');
+ });
+ });
+
describe('with type="codeVoice"', () => {
it('renders a `CodeVoice`', () => {
const wrapper = mountWithItem({
@@ -1227,7 +1277,8 @@ describe('ContentNode', () => {
const content = wrapper.find(StrikeThrough);
// assert the `strong` tag is rendered
- expect(content.html()).toBe('2strong');
+ expect(content.html())
+ .toBe('2strong');
});
});
diff --git a/tests/unit/components/ContentNode/Column.spec.js b/tests/unit/components/ContentNode/Column.spec.js
new file mode 100644
index 000000000..547ef8995
--- /dev/null
+++ b/tests/unit/components/ContentNode/Column.spec.js
@@ -0,0 +1,32 @@
+/**
+ * This source file is part of the Swift.org open source project
+ *
+ * Copyright (c) 2022 Apple Inc. and the Swift project authors
+ * Licensed under Apache License v2.0 with Runtime Library Exception
+ *
+ * See https://swift.org/LICENSE.txt for license information
+ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors
+*/
+
+import Column from '@/components/ContentNode/Column.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createWrapper = props => shallowMount(Column, {
+ slots: {
+ default: 'Default Content',
+ },
+ ...props,
+});
+
+describe('Column', () => {
+ it('renders the Column', () => {
+ const wrapper = createWrapper();
+ expect(wrapper.classes()).toContain('column');
+ expect(wrapper.text()).toBe('Default Content');
+ expect(wrapper.vm.style).toHaveProperty('--col-span', null);
+ wrapper.setProps({
+ span: 5,
+ });
+ expect(wrapper.vm.style).toHaveProperty('--col-span', 5);
+ });
+});
diff --git a/tests/unit/components/ContentNode/Row.spec.js b/tests/unit/components/ContentNode/Row.spec.js
new file mode 100644
index 000000000..8ddcdb15f
--- /dev/null
+++ b/tests/unit/components/ContentNode/Row.spec.js
@@ -0,0 +1,47 @@
+/**
+ * This source file is part of the Swift.org open source project
+ *
+ * Copyright (c) 2022 Apple Inc. and the Swift project authors
+ * Licensed under Apache License v2.0 with Runtime Library Exception
+ *
+ * See https://swift.org/LICENSE.txt for license information
+ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors
+*/
+
+import Row from '@/components/ContentNode/Row.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createWrapper = props => shallowMount(Row, {
+ slots: {
+ default: 'Slot Content',
+ },
+ ...props,
+});
+
+describe('Row', () => {
+ it('renders the Row', () => {
+ const wrapper = createWrapper();
+ expect(wrapper.classes()).toContain('row');
+ expect(wrapper.classes()).not.toContain('with-columns');
+ expect(wrapper.text()).toContain('Slot Content');
+ });
+
+ it('renders with columns in mind', () => {
+ const wrapper = createWrapper({
+ propsData: {
+ columns: 4,
+ },
+ });
+ expect(wrapper.classes()).toContain('with-columns');
+ expect(wrapper.vm.style).toHaveProperty('--col-count', 4);
+ });
+
+ it('provides a --col-gap', () => {
+ const wrapper = createWrapper({
+ propsData: {
+ gap: 10,
+ },
+ });
+ expect(wrapper.vm.style).toHaveProperty('--col-gap', '10px');
+ });
+});