Skip to content

Commit a71033c

Browse files
render task list items with a checkbox (#433)
rdar://100145156 Co-authored-by: Marcus Ortiz <[email protected]>
1 parent 5108cdf commit a71033c

File tree

4 files changed

+206
-5
lines changed

4 files changed

+206
-5
lines changed

src/components/ContentNode.vue

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import BlockVideo from './ContentNode/BlockVideo.vue';
2626
import Column from './ContentNode/Column.vue';
2727
import Row from './ContentNode/Row.vue';
2828
import TabNavigator from './ContentNode/TabNavigator.vue';
29+
import TaskList from './ContentNode/TaskList.vue';
2930
3031
const BlockType = {
3132
aside: 'aside',
@@ -279,10 +280,23 @@ function renderNode(createElement, references) {
279280
renderChildren(definition.content)
280281
)),
281282
]));
282-
case BlockType.unorderedList:
283-
return createElement('ul', {}, (
284-
renderListItems(node.items)
285-
));
283+
case BlockType.unorderedList: {
284+
const isTaskList = list => TaskList.props.tasks.validator(list.items);
285+
return isTaskList(node) ? (
286+
createElement(TaskList, {
287+
props: {
288+
tasks: node.items,
289+
},
290+
scopedSlots: {
291+
task: slotProps => renderChildren(slotProps.task.content),
292+
},
293+
})
294+
) : (
295+
createElement('ul', {}, (
296+
renderListItems(node.items)
297+
))
298+
);
299+
}
286300
case BlockType.dictionaryExample: {
287301
const props = {
288302
example: node.example,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!--
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
-->
10+
11+
<template>
12+
<ul class="tasklist">
13+
<li v-for="(task, i) in tasks" :key="i">
14+
<input v-if="showCheckbox(task)" type="checkbox" disabled :checked="task.checked" />
15+
<slot name="task" :task="task" />
16+
</li>
17+
</ul>
18+
</template>
19+
20+
<script>
21+
const CHECKED_PROP = 'checked';
22+
23+
const hasCheckedProp = obj => Object.hasOwnProperty.call(obj, CHECKED_PROP);
24+
25+
export default {
26+
name: 'TaskList',
27+
props: {
28+
tasks: {
29+
required: true,
30+
type: Array,
31+
// A task list is an unordered list that has at least one item with a
32+
// boolean `checked` property
33+
validator: list => list.some(hasCheckedProp),
34+
},
35+
},
36+
methods: {
37+
showCheckbox: hasCheckedProp,
38+
},
39+
};
40+
</script>
41+
42+
<style scoped lang="scss">
43+
.tasklist {
44+
--checkbox-width: 1rem;
45+
--indent-width: calc(var(--checkbox-width) / 2);
46+
--content-margin: var(--indent-width);
47+
48+
list-style-type: none;
49+
margin-left: var(--indent-width);
50+
}
51+
52+
p {
53+
margin-left: var(--content-margin);
54+
55+
&:only-child {
56+
--content-margin: calc(var(--checkbox-width) + var(--indent-width));
57+
}
58+
59+
input[type="checkbox"] + & {
60+
display: inline-block;
61+
}
62+
}
63+
</style>

tests/unit/components/ContentNode.spec.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
import { shallowMount } from '@vue/test-utils';
11+
import { shallowMount, mount } from '@vue/test-utils';
1212
import Aside from 'docc-render/components/ContentNode/Aside.vue';
1313
import CodeListing from 'docc-render/components/ContentNode/CodeListing.vue';
1414
import CodeVoice from 'docc-render/components/ContentNode/CodeVoice.vue';
@@ -27,6 +27,7 @@ import BlockVideo from '@/components/ContentNode/BlockVideo.vue';
2727
import Row from '@/components/ContentNode/Row.vue';
2828
import Column from '@/components/ContentNode/Column.vue';
2929
import TabNavigator from '@/components/ContentNode/TabNavigator.vue';
30+
import TaskList from 'docc-render/components/ContentNode/TaskList.vue';
3031

3132
const { TableHeaderStyle } = ContentNode.constants;
3233

@@ -325,6 +326,58 @@ describe('ContentNode', () => {
325326
expect(items.at(0).find('p').text()).toBe('foo');
326327
expect(items.at(1).find('p').text()).toBe('bar');
327328
});
329+
330+
it('renders a `TaskList` for checked items', () => {
331+
const items = [
332+
{
333+
checked: true,
334+
content: [
335+
{
336+
type: 'paragraph',
337+
inlineContent: [
338+
{
339+
type: 'text',
340+
text: 'foo',
341+
},
342+
],
343+
},
344+
],
345+
},
346+
{
347+
checked: false,
348+
content: [
349+
{
350+
type: 'paragraph',
351+
inlineContent: [
352+
{
353+
type: 'text',
354+
text: 'bar',
355+
},
356+
],
357+
},
358+
],
359+
},
360+
];
361+
const wrapper = mount(ContentNode, {
362+
propsData: {
363+
content: [
364+
{
365+
type: 'unorderedList',
366+
items,
367+
},
368+
],
369+
},
370+
});
371+
372+
const list = wrapper.find(TaskList);
373+
expect(list.exists()).toBe(true);
374+
expect(list.props('tasks')).toEqual(items);
375+
376+
const paragraphs = list.findAll('li p');
377+
expect(paragraphs.length).toBe(items.length);
378+
expect(paragraphs.at(0).text()).toBe('foo');
379+
expect(paragraphs.at(1).text()).toBe('bar');
380+
});
328381
});
329382

330383
describe('with type="small"', () => {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import { shallowMount } from '@vue/test-utils';
12+
import TaskList from 'docc-render/components/ContentNode/TaskList.vue';
13+
14+
describe('ContentNode', () => {
15+
const createWrapper = options => shallowMount(TaskList, options);
16+
17+
it('renders a list with a disabled checkbox for each list item', () => {
18+
const tasks = [
19+
{ checked: false },
20+
{ checked: true },
21+
];
22+
23+
const wrapper = createWrapper({
24+
propsData: { tasks },
25+
});
26+
expect(wrapper.is('ul.tasklist')).toBe(true);
27+
28+
const checkboxes = wrapper.findAll('li input[disabled][type="checkbox"]');
29+
expect(checkboxes.length).toBe(tasks.length);
30+
expect(checkboxes.at(0).element.checked).toBe(tasks[0].checked);
31+
expect(checkboxes.at(1).element.checked).toBe(tasks[1].checked);
32+
});
33+
34+
it('renders slot content for tasks', () => {
35+
const tasks = [
36+
{ checked: false, text: 'foo' },
37+
{ checked: true, text: 'bar' },
38+
];
39+
40+
const wrapper = createWrapper({
41+
propsData: { tasks },
42+
scopedSlots: {
43+
task: function render(slotProps) {
44+
return this.$createElement('p', slotProps.task.text);
45+
},
46+
},
47+
});
48+
49+
const paragraphs = wrapper.findAll('li p');
50+
expect(paragraphs.length).toBe(tasks.length);
51+
expect(paragraphs.at(0).text()).toBe(tasks[0].text);
52+
expect(paragraphs.at(1).text()).toBe(tasks[1].text);
53+
});
54+
55+
it('does not render checkbox for items without `checked` prop', () => {
56+
const tasks = [
57+
{ checked: false, content: 'foo' },
58+
{ content: 'bar' },
59+
{ checked: true, content: 'baz' },
60+
];
61+
62+
const wrapper = createWrapper({
63+
propsData: { tasks },
64+
});
65+
const items = wrapper.findAll('li');
66+
expect(items.length).toBe(tasks.length);
67+
expect(items.at(0).find('input[type="checkbox"]').exists()).toBe(true);
68+
expect(items.at(1).find('input[type="checkbox"]').exists()).toBe(false);
69+
expect(items.at(2).find('input[type="checkbox"]').exists()).toBe(true);
70+
});
71+
});

0 commit comments

Comments
 (0)