Skip to content

Commit d6ad577

Browse files
Swoyamjeetcodespre-commit-ci-lite[bot]akolson
authored
[Remove Vuetify from Studio] Layout and input on the Collection > Select channels page (#5812)
* migrate Select channels list layout/input from Vuetify to KDS (#5774) * [pre-commit.ci lite] apply automatic fixes * replace Vuetify grey--text with KDS annotation token * implements pr review feedback --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Samson Akol <akolson2010@gmail.com>
1 parent be5ba48 commit d6ad577

2 files changed

Lines changed: 100 additions & 45 deletions

File tree

contentcuration/contentcuration/frontend/channelList/views/ChannelSet/ChannelSelectionList.vue

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
<template>
22

3-
<VContainer
4-
fluid
5-
class="pa-0 pb-5"
6-
>
7-
<LoadingText
8-
v-if="loading"
9-
class="pt-4"
3+
<div class="selection-list">
4+
<StudioLargeLoader
5+
v-if="show(loaderKey, loading, 400)"
6+
class="selection-loader"
107
/>
118
<template v-else>
12-
<VTextField
9+
<KTextbox
1310
v-model="search"
14-
style="max-width: 350px"
15-
class="mt-4"
16-
box
11+
class="search-input"
1712
:label="$tr('searchText')"
1813
/>
1914
<p
2015
v-if="!listChannels.length"
21-
class="grey--text mb-0 mt-4"
16+
class="no-channels-found"
2217
>
2318
{{ $tr('noChannelsFound') }}
2419
</p>
@@ -28,41 +23,39 @@
2823
:key="channel.id"
2924
flat
3025
hover
31-
class="list-card-hover px-3"
26+
class="list-card-hover selection-card"
3227
>
33-
<VLayout
34-
align-center
35-
row
36-
>
37-
<Checkbox
38-
v-model="selectedChannels"
39-
color="primary"
28+
<div class="selection-row">
29+
<KCheckbox
30+
:checked="selectedChannels.includes(channel.id)"
31+
:label="channel.name"
32+
:showLabel="false"
4033
:data-testid="`checkbox-${channel.id}`"
41-
:value="channel.id"
42-
class="channel ma-0"
34+
class="channel-checkbox"
35+
@change="handleSelectChannel(channel.id)"
4336
/>
4437
<ChannelItem
4538
:channelId="channel.id"
4639
:data-testid="`channel-item-${channel.id}`"
4740
@click="handleSelectChannel"
4841
/>
49-
</VLayout>
42+
</div>
5043
</VCard>
5144
</template>
5245
</template>
53-
</VContainer>
46+
</div>
5447

5548
</template>
5649

5750

5851
<script>
5952
6053
import sortBy from 'lodash/sortBy';
54+
import useKShow from 'kolibri-design-system/lib/composables/useKShow';
6155
import { mapGetters, mapActions } from 'vuex';
6256
import ChannelItem from './ChannelItem';
6357
import { ChannelListTypes } from 'shared/constants';
64-
import Checkbox from 'shared/views/form/Checkbox';
65-
import LoadingText from 'shared/views/LoadingText';
58+
import StudioLargeLoader from 'shared/views/StudioLargeLoader';
6659
6760
function listTypeValidator(value) {
6861
// The value must match one of the ListTypes
@@ -72,9 +65,12 @@
7265
export default {
7366
name: 'ChannelSelectionList',
7467
components: {
75-
Checkbox,
7668
ChannelItem,
77-
LoadingText,
69+
StudioLargeLoader,
70+
},
71+
setup() {
72+
const { show } = useKShow();
73+
return { show };
7874
},
7975
props: {
8076
value: {
@@ -117,6 +113,9 @@
117113
'name',
118114
);
119115
},
116+
loaderKey() {
117+
return `channel-selection-list-${this.listType}`;
118+
},
120119
},
121120
mounted() {
122121
this.loading = true;
@@ -146,12 +145,37 @@
146145

147146
<style lang="scss" scoped>
148147
149-
.add-channel-button {
150-
margin: 0;
148+
.selection-list {
149+
padding-bottom: 20px;
150+
}
151+
152+
.selection-loader {
153+
padding-top: 16px;
154+
}
155+
156+
.search-input {
157+
max-width: 350px;
158+
margin-top: 16px;
151159
}
152160
153-
.channel /deep/ .k-checkbox {
154-
vertical-align: middle;
161+
.no-channels-found {
162+
margin-top: 16px;
163+
margin-bottom: 0;
164+
color: v-bind('$themeTokens.annotation');
165+
}
166+
167+
.selection-card {
168+
padding-inline: 12px;
169+
}
170+
171+
.selection-row {
172+
display: flex;
173+
align-items: center;
174+
}
175+
176+
.channel-checkbox {
177+
padding-inline-end: 4px;
178+
margin: 0;
155179
}
156180
157181
.list-card-hover {

contentcuration/contentcuration/frontend/channelList/views/ChannelSet/__tests__/channelSelectionList.spec.js

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,27 @@ import { Store } from 'vuex';
55
import ChannelSelectionList from '../ChannelSelectionList';
66
import { ChannelListTypes } from 'shared/constants';
77

8+
jest.mock('kolibri-design-system/lib/composables/useKShow', () => ({
9+
__esModule: true,
10+
default: () => ({
11+
show: (_id, loading) => loading,
12+
}),
13+
}));
14+
815
const searchWord = 'search test';
916

1017
const editChannel = {
1118
id: 'editchannel',
1219
name: searchWord,
13-
description: '',
20+
description: 'A curated collection of math resources',
1421
edit: true,
1522
published: true,
1623
};
1724

1825
const editChannel2 = {
1926
id: 'editchannel2',
2027
name: 'Another Channel',
21-
description: '',
28+
description: 'Science and nature topics for all ages',
2229
edit: true,
2330
published: true,
2431
};
@@ -79,33 +86,48 @@ describe('ChannelSelectionList', () => {
7986
// Specific wait avoids wrapping the whole block in waitFor
8087
expect(await screen.findByLabelText('Search for a channel')).toBeInTheDocument();
8188

82-
expect(screen.getByText(editChannel.name)).toBeInTheDocument();
83-
expect(screen.getByText(editChannel2.name)).toBeInTheDocument();
84-
expect(screen.queryByText(publicChannel.name)).not.toBeInTheDocument();
89+
expect(screen.getByRole('heading', { name: editChannel.name })).toBeInTheDocument();
90+
expect(screen.getByRole('heading', { name: editChannel2.name })).toBeInTheDocument();
91+
expect(screen.queryByRole('heading', { name: publicChannel.name })).not.toBeInTheDocument();
92+
});
93+
94+
it('shows loader while the channel list is loading', async () => {
95+
let resolveLoad;
96+
const loadingPromise = new Promise(resolve => {
97+
resolveLoad = resolve;
98+
});
99+
mockActions.loadChannelList.mockReturnValueOnce(loadingPromise);
100+
101+
await renderComponent();
102+
103+
expect(screen.getByTestId('loader')).toBeInTheDocument();
104+
105+
resolveLoad();
106+
expect(await screen.findByLabelText('Search for a channel')).toBeInTheDocument();
85107
});
86108

87109
it('filters the channel list when the user types in the search box', async () => {
88110
const user = userEvent.setup();
89111
await renderComponent();
90112

91113
// Wait for data load
92-
expect(await screen.findByText(editChannel.name)).toBeInTheDocument();
93-
expect(screen.getByText(editChannel2.name)).toBeInTheDocument();
114+
expect(await screen.findByRole('heading', { name: editChannel.name })).toBeInTheDocument();
115+
expect(screen.getByRole('heading', { name: editChannel2.name })).toBeInTheDocument();
94116

95117
const searchInput = screen.getByLabelText('Search for a channel');
96118
await user.clear(searchInput);
97119
await user.type(searchInput, editChannel.name);
98120

99121
// Verify filter happened
100-
expect(await screen.findByText(editChannel.name)).toBeInTheDocument();
101-
expect(screen.queryByText(editChannel2.name)).not.toBeInTheDocument();
122+
expect(await screen.findByRole('heading', { name: editChannel.name })).toBeInTheDocument();
123+
expect(screen.queryByRole('heading', { name: editChannel2.name })).not.toBeInTheDocument();
102124
});
103125

104126
it('selects a channel when the user clicks the checkbox', async () => {
105127
const user = userEvent.setup();
106128
const { emitted } = await renderComponent();
107129

108-
await screen.findByText(editChannel.name);
130+
await screen.findByRole('heading', { name: editChannel.name });
109131

110132
// Using getByTestId because the component doesn't expose unique
111133
// accessible roles for individual channel checkboxes
@@ -127,7 +149,7 @@ describe('ChannelSelectionList', () => {
127149
// Initialize with the channel already selected
128150
const { emitted } = await renderComponent({ value: [editChannel.id] });
129151

130-
await screen.findByText(editChannel.name);
152+
await screen.findByRole('heading', { name: editChannel.name });
131153

132154
// Using getByTestId because the component doesn't expose unique
133155
// accessible roles for individual channel checkboxes
@@ -146,7 +168,7 @@ describe('ChannelSelectionList', () => {
146168
const user = userEvent.setup();
147169
const { emitted } = await renderComponent();
148170

149-
await screen.findByText(editChannel.name);
171+
await screen.findByRole('heading', { name: editChannel.name });
150172

151173
// Using getByTestId because the component doesn't expose accessible
152174
// roles for channel cards
@@ -164,7 +186,7 @@ describe('ChannelSelectionList', () => {
164186
// Initialize with the channel already selected
165187
const { emitted } = await renderComponent({ value: [editChannel.id] });
166188

167-
await screen.findByText(editChannel.name);
189+
await screen.findByRole('heading', { name: editChannel.name });
168190

169191
// Using getByTestId because the component doesn't expose accessible
170192
// roles for channel cards
@@ -175,4 +197,13 @@ describe('ChannelSelectionList', () => {
175197
expect(emitted().input).toHaveLength(1);
176198
expect(emitted().input[0][0]).toEqual([]);
177199
});
200+
201+
it('each checkbox has an accessible name matching its channel name', async () => {
202+
await renderComponent();
203+
204+
// KCheckbox renders a visually-hidden <label for="id"> associated with the input,
205+
// so getByRole resolves the accessible name correctly for screen readers
206+
expect(await screen.findByRole('checkbox', { name: editChannel.name })).toBeInTheDocument();
207+
expect(screen.getByRole('checkbox', { name: editChannel2.name })).toBeInTheDocument();
208+
});
178209
});

0 commit comments

Comments
 (0)