Skip to content

Commit a798d99

Browse files
authored
[CLX-3022][Horizon] Learn screen accessibility (#3290)
refs: CLX-3022 affects: Horizon release note:
1 parent f44a0d9 commit a798d99

File tree

9 files changed

+134
-16
lines changed

9 files changed

+134
-16
lines changed

libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import androidx.compose.ui.draw.clip
4141
import androidx.compose.ui.layout.ContentScale
4242
import androidx.compose.ui.platform.LocalContext
4343
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.semantics.semantics
4445
import androidx.compose.ui.text.LinkAnnotation
4546
import androidx.compose.ui.text.SpanStyle
4647
import androidx.compose.ui.text.TextLinkStyles
@@ -198,7 +199,11 @@ private fun ProgramsText(
198199
if (parts.size > 1) append(parts[1])
199200
}
200201

201-
Text(style = HorizonTypography.p1, text = fullText)
202+
Text(
203+
modifier = Modifier.semantics(mergeDescendants = true) {},
204+
style = HorizonTypography.p1,
205+
text = fullText
206+
)
202207
}
203208

204209
@Composable

libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import androidx.compose.ui.Alignment
3333
import androidx.compose.ui.Modifier
3434
import androidx.compose.ui.draw.clip
3535
import androidx.compose.ui.draw.scale
36+
import androidx.compose.ui.platform.LocalContext
3637
import androidx.compose.ui.res.stringResource
38+
import androidx.compose.ui.semantics.Role
39+
import androidx.compose.ui.semantics.role
40+
import androidx.compose.ui.semantics.semantics
3741
import androidx.compose.ui.unit.dp
3842
import androidx.navigation.NavHostController
3943
import com.instructure.horizon.features.learn.course.lti.CourseToolsScreen
@@ -45,6 +49,7 @@ import com.instructure.horizon.horizonui.foundation.HorizonColors
4549
import com.instructure.horizon.horizonui.foundation.HorizonTypography
4650
import com.instructure.horizon.horizonui.molecules.ProgressBar
4751
import com.instructure.horizon.horizonui.organisms.tabrow.TabRow
52+
import com.instructure.horizon.horizonui.selectable
4853
import kotlinx.coroutines.launch
4954

5055
@Composable
@@ -143,12 +148,17 @@ private fun Tab(tab: CourseDetailsTab, isSelected: Boolean, modifier: Modifier =
143148
modifier = modifier
144149
.padding(bottom = 2.dp)
145150
) {
151+
val context = LocalContext.current
146152
Text(
147153
stringResource(tab.titleRes),
148154
style = HorizonTypography.p1,
149155
color = color,
150156
modifier = Modifier
151157
.padding(top = 20.dp)
158+
.semantics {
159+
role = Role.Tab
160+
selectable(context, isSelected)
161+
}
152162
)
153163
}
154164
}

libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
*
1616
*/
17+
@file:OptIn(ExperimentalComposeUiApi::class)
18+
1719
package com.instructure.horizon.features.learn.course.score
1820

1921
import androidx.compose.animation.AnimatedContent
@@ -41,11 +43,15 @@ import androidx.compose.runtime.remember
4143
import androidx.compose.runtime.saveable.rememberSaveable
4244
import androidx.compose.runtime.setValue
4345
import androidx.compose.ui.Alignment
46+
import androidx.compose.ui.ExperimentalComposeUiApi
4447
import androidx.compose.ui.Modifier
4548
import androidx.compose.ui.draw.clip
4649
import androidx.compose.ui.platform.LocalContext
4750
import androidx.compose.ui.res.painterResource
4851
import androidx.compose.ui.res.stringResource
52+
import androidx.compose.ui.semantics.contentDescription
53+
import androidx.compose.ui.semantics.invisibleToUser
54+
import androidx.compose.ui.semantics.semantics
4955
import androidx.compose.ui.tooling.preview.Preview
5056
import androidx.compose.ui.unit.dp
5157
import androidx.hilt.navigation.compose.hiltViewModel
@@ -352,28 +358,36 @@ private fun GroupWeightsContent(assignmentGroups: List<AssignmentGroupScoreItem>
352358

353359
@Composable
354360
private fun GroupWeightItem(assignmentGroup: AssignmentGroupScoreItem) {
361+
val groupWeightText = stringResource(
362+
R.string.weightPercentageValue,
363+
assignmentGroup.groupWeight
364+
)
365+
val mergedContentDescription = "${assignmentGroup.name}, $groupWeightText"
366+
355367
Column(
356368
modifier = Modifier
357369
.padding(vertical = 16.dp)
370+
.semantics(mergeDescendants = true) {
371+
contentDescription = mergedContentDescription
372+
}
358373
) {
359374
Column (
360375
modifier = Modifier.padding(horizontal = 24.dp),
361376
) {
362377
Text(
363-
text = assignmentGroup.name.orEmpty(),
378+
text = assignmentGroup.name,
364379
style = HorizonTypography.p1,
365-
color = HorizonColors.Text.body()
380+
color = HorizonColors.Text.body(),
381+
modifier = Modifier.semantics { invisibleToUser() }
366382
)
367383

368384
HorizonSpace(SpaceSize.SPACE_8)
369385

370386
Text(
371-
text = stringResource(
372-
R.string.weightPercentageValue,
373-
assignmentGroup.groupWeight
374-
),
387+
text = groupWeightText,
375388
style = HorizonTypography.p1,
376-
color = HorizonColors.Text.body()
389+
color = HorizonColors.Text.body(),
390+
modifier = Modifier.semantics { invisibleToUser() }
377391
)
378392
}
379393
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.instructure.horizon.horizonui
17+
18+
import android.content.Context
19+
import androidx.compose.ui.semantics.LiveRegionMode
20+
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
21+
import androidx.compose.ui.semantics.liveRegion
22+
import androidx.compose.ui.semantics.onClick
23+
import androidx.compose.ui.semantics.stateDescription
24+
import com.instructure.horizon.R
25+
26+
fun SemanticsPropertyReceiver.expandable(context: Context, expanded: Boolean) {
27+
val expandedStateDesc = context.getString(R.string.a11y_expanded)
28+
val collapsedStateDesc = context.getString(R.string.a11y_collapsed)
29+
val expandActionLabel = context.getString(R.string.a11y_expand)
30+
val collapseActionLabel = context.getString(R.string.a11y_collapse)
31+
32+
stateDescription = if (expanded) expandedStateDesc else collapsedStateDesc
33+
liveRegion = LiveRegionMode.Assertive
34+
onClick(if (expanded) collapseActionLabel else expandActionLabel) { false }
35+
}
36+
37+
fun SemanticsPropertyReceiver.selectable(context: Context, selected: Boolean) {
38+
val selectedStateDesc = context.getString(R.string.a11y_selected)
39+
val unselectedStateDesc = context.getString(R.string.a11y_unselected)
40+
41+
stateDescription = if (selected) selectedStateDesc else unselectedStateDesc
42+
liveRegion = LiveRegionMode.Assertive
43+
}

libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,25 @@ import androidx.compose.material3.Text
3232
import androidx.compose.runtime.Composable
3333
import androidx.compose.runtime.getValue
3434
import androidx.compose.ui.Alignment
35+
import androidx.compose.ui.ExperimentalComposeUiApi
3536
import androidx.compose.ui.Modifier
3637
import androidx.compose.ui.draw.clip
3738
import androidx.compose.ui.draw.rotate
39+
import androidx.compose.ui.platform.LocalContext
3840
import androidx.compose.ui.res.painterResource
41+
import androidx.compose.ui.semantics.invisibleToUser
42+
import androidx.compose.ui.semantics.semantics
3943
import androidx.compose.ui.tooling.preview.Preview
4044
import androidx.compose.ui.unit.dp
4145
import com.instructure.horizon.R
46+
import com.instructure.horizon.horizonui.expandable
4247
import com.instructure.horizon.horizonui.foundation.HorizonColors
4348
import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius
4449
import com.instructure.horizon.horizonui.foundation.HorizonSpace
4550
import com.instructure.horizon.horizonui.foundation.HorizonTypography
4651
import com.instructure.horizon.horizonui.foundation.SpaceSize
4752

53+
@OptIn(ExperimentalComposeUiApi::class)
4854
@Composable
4955
fun CollapsableContentCard(
5056
title: String,
@@ -65,13 +71,18 @@ fun CollapsableContentCard(
6571
.padding(vertical = 16.dp)
6672
) {
6773
Column(
68-
modifier = Modifier.clickable { onExpandChanged(!expanded) }
74+
modifier = Modifier
75+
.clickable { onExpandChanged(!expanded) }
76+
.semantics {
77+
invisibleToUser()
78+
}
6979
){
7080
Text(
7181
title,
7282
style = HorizonTypography.h2,
7383
color = HorizonColors.Text.body(),
74-
modifier = Modifier.padding(horizontal = 24.dp)
84+
modifier = Modifier
85+
.padding(horizontal = 24.dp)
7586
)
7687

7788
HorizonSpace(SpaceSize.SPACE_16)
@@ -80,11 +91,15 @@ fun CollapsableContentCard(
8091
targetValue = if (expanded) 180f else 0f,
8192
label = "rotationAnimation"
8293
)
83-
94+
val context = LocalContext.current
8495
Row(
8596
modifier = Modifier
8697
.fillMaxWidth()
8798
.padding(horizontal = 24.dp)
99+
.semantics(mergeDescendants = true) {
100+
expandable(context, expanded)
101+
}
102+
88103
) {
89104
Icon(
90105
painter = painterResource(R.drawable.keyboard_arrow_down),

libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ import androidx.compose.ui.platform.LocalContext
4949
import androidx.compose.ui.res.painterResource
5050
import androidx.compose.ui.res.pluralStringResource
5151
import androidx.compose.ui.res.stringResource
52+
import androidx.compose.ui.semantics.semantics
5253
import androidx.compose.ui.text.style.TextOverflow
5354
import androidx.compose.ui.tooling.preview.Preview
5455
import androidx.compose.ui.unit.dp
5556
import com.instructure.canvasapi2.utils.ContextKeeper
5657
import com.instructure.horizon.R
58+
import com.instructure.horizon.horizonui.expandable
5759
import com.instructure.horizon.horizonui.foundation.HorizonColors
5860
import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius
5961
import com.instructure.horizon.horizonui.foundation.HorizonSpace
@@ -98,8 +100,13 @@ fun ModuleContainer(state: ModuleHeaderState, modifier: Modifier = Modifier, con
98100
Column {
99101
val onClick = state.onClick
100102
val clickModifier = if (onClick != null) Modifier.clickable { onClick() } else Modifier
103+
val context = LocalContext.current
101104

102-
Column(modifier = clickModifier.padding(16.dp)) {
105+
Column(modifier = clickModifier
106+
.semantics(mergeDescendants = true) {
107+
expandable(context, state.expanded)
108+
}
109+
.padding(16.dp)) {
103110
ModuleHeader(state = state)
104111
if (state.subtitle != null && state.expanded) {
105112
HorizonSpace(SpaceSize.SPACE_24)

libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo
118118
HorizonSpace(SpaceSize.SPACE_8)
119119
Icon(
120120
painterResource(R.drawable.lock),
121-
contentDescription = null,
121+
contentDescription = stringResource(R.string.a11y_locked),
122122
tint = HorizonColors.Surface.institution(),
123123
modifier = modifier
124124
)
@@ -128,7 +128,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo
128128
HorizonSpace(SpaceSize.SPACE_8)
129129
Icon(
130130
painterResource(R.drawable.check_circle_full),
131-
contentDescription = null,
131+
contentDescription = stringResource(R.string.a11y_completed),
132132
tint = HorizonColors.Surface.institution(),
133133
modifier = modifier
134134
)
@@ -138,7 +138,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo
138138
HorizonSpace(SpaceSize.SPACE_8)
139139
Icon(
140140
painterResource(R.drawable.circle),
141-
contentDescription = null,
141+
contentDescription = stringResource(R.string.a11y_not_completed),
142142
tint = HorizonColors.LineAndBorder.lineStroke(),
143143
modifier = modifier
144144
)

libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ import androidx.compose.ui.layout.onGloballyPositioned
3939
import androidx.compose.ui.platform.LocalContext
4040
import androidx.compose.ui.platform.LocalDensity
4141
import androidx.compose.ui.res.painterResource
42+
import androidx.compose.ui.res.stringResource
43+
import androidx.compose.ui.semantics.Role
44+
import androidx.compose.ui.semantics.clearAndSetSemantics
45+
import androidx.compose.ui.semantics.contentDescription
46+
import androidx.compose.ui.semantics.role
47+
import androidx.compose.ui.semantics.stateDescription
4248
import androidx.compose.ui.tooling.preview.Preview
4349
import androidx.compose.ui.unit.dp
4450
import com.instructure.canvasapi2.utils.ContextKeeper
@@ -55,7 +61,8 @@ fun SingleSelect(
5561
state: SingleSelectState,
5662
modifier: Modifier = Modifier
5763
) {
58-
64+
val expandedState = stringResource(R.string.a11y_expanded)
65+
val collapsedState = stringResource(R.string.a11y_collapsed)
5966
Input(
6067
label = state.label,
6168
helperText = state.helperText,
@@ -65,6 +72,15 @@ fun SingleSelect(
6572
.onFocusChanged {
6673
state.onFocusChanged(it.isFocused)
6774
}
75+
.clearAndSetSemantics {
76+
role = Role.DropdownList
77+
stateDescription = if (state.isMenuOpen) expandedState else collapsedState
78+
contentDescription = if (state.selectedOption != null) {
79+
"${state.label}, ${state.selectedOption}"
80+
} else {
81+
state.label ?: ""
82+
}
83+
}
6884
) {
6985
Column(
7086
modifier = Modifier

libs/horizon/src/main/res/values/strings.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,12 @@
370370
<string name="dashboardCompletedCourseDetails">Congrats! You’ve completed your course. View your progress and scores on the Learn page.</string>
371371
<string name="a11y_inboxComposeSelectCourse">Select Course</string>
372372
<string name="a11y_close">Close</string>
373+
<string name="a11y_expanded">Expanded</string>
374+
<string name="a11y_collapsed">Collapsed</string>
375+
<string name="a11y_expand">Expand</string>
376+
<string name="a11y_collapse">Collapse</string>
377+
<string name="a11y_completed">Completed</string>
378+
<string name="a11y_not_completed">Not completed</string>
379+
<string name="a11y_locked">Locked</string>
380+
<string name="a11y_unselected">Unselected</string>
373381
</resources>

0 commit comments

Comments
 (0)