Skip to content

Commit eb050c7

Browse files
authored
feat(#295): adds overlay for rank initial state (#354)
- Adds an overlay with a button when the user hasn't interacted with the question type (required or not required question). - Updates the button's disabled shades to match geopoint and image upload question types - Prevents icons from shrinking when text is too long
1 parent e7287de commit eb050c7

File tree

3 files changed

+134
-64
lines changed

3 files changed

+134
-64
lines changed

.changeset/itchy-glasses-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@getodk/web-forms': minor
3+
---
4+
5+
Added an empty state with an overlay for rank questions, requiring user interaction and treating non-interaction as a missed question.

packages/web-forms/src/components/controls/RankControl.vue

Lines changed: 119 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ const values = computed<string[]>({
4343
return currentValues.slice();
4444
}
4545
46-
return props.question.currentState.valueOptions.map((option) => option.value);
46+
return getRankItems();
4747
},
4848
set: (orderedValues) => {
4949
touched.value = true;
5050
props.question.setValues(orderedValues);
5151
},
5252
});
5353
54+
const getRankItems = () => props.question.currentState.valueOptions.map((option) => option.value);
55+
56+
const selectDefaultOrder = () => {
57+
values.value = getRankItems();
58+
};
59+
5460
const setHighlight = (index: number | null) => {
5561
highlight.index.value = index;
5662
@@ -101,58 +107,70 @@ const swapItems = (index: number, newPosition: number) => {
101107
<template>
102108
<ControlText :question="question" />
103109

104-
<VueDraggable
105-
:id="question.nodeId"
106-
v-model="values"
107-
:delay="HOLD_DELAY"
108-
:delay-on-touch-only="true"
109-
:disabled="disabled"
110-
ghost-class="fade-moving"
111-
class="rank-control"
112-
:class="{ 'disabled': disabled }"
113-
>
114-
<div
115-
v-for="(value, index) in values"
116-
:id="value"
117-
:key="value"
118-
class="rank-option"
119-
:class="{ 'moving': highlight.index.value === index }"
120-
tabindex="0"
121-
@keydown.up.prevent="moveUp(index)"
122-
@keydown.down.prevent="moveDown(index)"
123-
>
124-
<div class="rank-label">
125-
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 768 768">
126-
<path d="M480 511.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM480 319.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM480 256.5q-25.5 0-45-19.5t-19.5-45 19.5-45 45-19.5 45 19.5 19.5 45-19.5 45-45 19.5zM288 127.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM288 319.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM352.5 576q0 25.5-19.5 45t-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5 45 19.5 19.5 45z" />
110+
<div class="range-control-container">
111+
<div v-if="!touched" class="rank-overlay">
112+
<button :disabled="disabled" @click="selectDefaultOrder">
113+
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="15" viewBox="0 0 8 15" fill="none">
114+
<path d="M3.91263 2.57495L5.96263 4.52245C6.28763 4.8312 6.81263 4.8312 7.13763 4.52245C7.46263 4.2137 7.46263 3.71495 7.13763 3.4062L4.49596 0.888697C4.17096 0.579948 3.64596 0.579948 3.32096 0.888697L0.679297 3.4062C0.354297 3.71495 0.354297 4.2137 0.679297 4.52245C1.0043 4.8312 1.5293 4.8312 1.8543 4.52245L3.91263 2.57495ZM3.91263 12.3441L1.86263 10.3966C1.53763 10.0879 1.01263 10.0879 0.68763 10.3966C0.36263 10.7054 0.36263 11.2041 0.68763 11.5129L3.3293 14.0304C3.6543 14.3391 4.1793 14.3391 4.5043 14.0304L7.14596 11.5208C7.47096 11.212 7.47096 10.7133 7.14596 10.4045C6.82096 10.0958 6.29596 10.0958 5.97096 10.4045L3.91263 12.3441Z" fill="#323232" />
127115
</svg>
128-
<span>{{ props.question.getValueLabel(value)?.asString }}</span>
129-
</div>
116+
<!-- TODO: translations -->
117+
<span>Rank items</span>
118+
</button>
119+
</div>
130120

131-
<div class="rank-buttons">
132-
<button
133-
v-if="values.length > 1"
134-
:disabled="disabled || (index === 0)"
135-
@click="moveUp(index)"
136-
@mousedown="setHighlight(index)"
137-
>
138-
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 768 768">
139-
<path d="M384 256.5l192 192-45 45-147-147-147 147-45-45z" />
140-
</svg>
141-
</button>
142-
143-
<button
144-
v-if="values.length > 1"
145-
:disabled="disabled || (index === values.length - 1)"
146-
@click="moveDown(index)"
147-
@mousedown="setHighlight(index)"
148-
>
149-
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 768 768">
150-
<path d="M531 274.5l45 45-192 192-192-192 45-45 147 147z" />
121+
<VueDraggable
122+
:id="question.nodeId"
123+
v-model="values"
124+
:delay="HOLD_DELAY"
125+
:delay-on-touch-only="true"
126+
:disabled="disabled"
127+
ghost-class="fade-moving"
128+
class="rank-control"
129+
:class="{ disabled: disabled }"
130+
>
131+
<div
132+
v-for="(value, index) in values"
133+
:id="value"
134+
:key="value"
135+
class="rank-option"
136+
:class="{ moving: highlight.index.value === index }"
137+
tabindex="0"
138+
@keydown.up.prevent="moveUp(index)"
139+
@keydown.down.prevent="moveDown(index)"
140+
>
141+
<div class="rank-label">
142+
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 768 768">
143+
<path d="M480 511.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM480 319.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM480 256.5q-25.5 0-45-19.5t-19.5-45 19.5-45 45-19.5 45 19.5 19.5 45-19.5 45-45 19.5zM288 127.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM288 319.5q25.5 0 45 19.5t19.5 45-19.5 45-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5zM352.5 576q0 25.5-19.5 45t-45 19.5-45-19.5-19.5-45 19.5-45 45-19.5 45 19.5 19.5 45z" />
151144
</svg>
152-
</button>
145+
<span>{{ props.question.getValueLabel(value)?.asString }}</span>
146+
</div>
147+
148+
<div class="rank-buttons">
149+
<button
150+
v-if="values.length > 1"
151+
:disabled="disabled || index === 0"
152+
@click="moveUp(index)"
153+
@mousedown="setHighlight(index)"
154+
>
155+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 768 768">
156+
<path d="M384 256.5l192 192-45 45-147-147-147 147-45-45z" />
157+
</svg>
158+
</button>
159+
160+
<button
161+
v-if="values.length > 1"
162+
:disabled="disabled || index === values.length - 1"
163+
@click="moveDown(index)"
164+
@mousedown="setHighlight(index)"
165+
>
166+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 768 768">
167+
<path d="M531 274.5l45 45-192 192-192-192 45-45 147 147z" />
168+
</svg>
169+
</button>
170+
</div>
153171
</div>
154-
</div>
155-
</VueDraggable>
172+
</VueDraggable>
173+
</div>
156174

157175
<ValidationMessage
158176
:message="question.validationState.violation?.message.asString"
@@ -164,18 +182,25 @@ const swapItems = (index: number, newPosition: number) => {
164182
@import 'primeflex/core/_variables.scss';
165183
166184
// Variable definition to root element
167-
.rank-control {
185+
.range-control-container {
168186
--rankSpacing: 7px;
169187
--rankBorder: 1px solid var(--surface-300);
170188
--rankBorderRadius: 10px;
171189
--rankHighlightBackground: var(--primary-50);
172190
--rankHighlightBorder: var(--primary-500);
191+
--rankBaseBackground: var(--surface-0);
192+
--rankDisabledBackground: var(--surface-300);
193+
--rankDisabledText: var(--surface-500);
173194
}
174195
175196
// Overriding VueDraggable's sortable-chosen class
176197
.sortable-chosen {
177198
opacity: 0.9;
178-
background-color: var(--surface-0);
199+
background-color: var(--rankBaseBackground);
200+
}
201+
202+
.range-control-container {
203+
position: relative;
179204
}
180205
181206
.rank-control {
@@ -193,6 +218,7 @@ const swapItems = (index: number, newPosition: number) => {
193218
justify-content: space-between;
194219
width: 100%;
195220
padding: 8px;
221+
background: var(--rankBaseBackground);
196222
border: var(--rankBorder);
197223
border-radius: var(--rankBorderRadius);
198224
font-size: 1rem;
@@ -205,6 +231,10 @@ const swapItems = (index: number, newPosition: number) => {
205231
align-items: center;
206232
gap: var(--rankSpacing);
207233
}
234+
235+
.rank-label svg {
236+
flex-shrink: 0;
237+
}
208238
}
209239
210240
.moving,
@@ -217,28 +247,29 @@ const swapItems = (index: number, newPosition: number) => {
217247
opacity: 0.5;
218248
}
219249
220-
.rank-buttons {
250+
.rank-overlay button,
251+
.rank-buttons button {
221252
display: flex;
253+
align-items: center;
222254
gap: var(--rankSpacing);
255+
border: var(--rankBorder);
256+
border-radius: var(--rankBorderRadius);
257+
background: var(--rankBaseBackground);
258+
padding: var(--rankSpacing);
259+
line-height: 0;
223260
224-
button {
225-
border: var(--rankBorder);
226-
border-radius: var(--rankBorderRadius);
227-
background: var(--surface-0);
228-
padding: var(--rankSpacing);
229-
line-height: 0;
230-
}
231-
232-
button:hover:not(:disabled) {
261+
&:hover:not(:disabled) {
233262
background: var(--rankHighlightBackground);
234263
border: 1px solid var(--rankHighlightBorder);
235264
}
236265
237-
button:disabled {
238-
background: var(--surface-100);
266+
&:disabled {
267+
background: var(--rankDisabledBackground);
268+
color: var(--rankDisabledText);
239269
border: none;
270+
240271
svg path {
241-
fill: var(--surface-300);
272+
fill: var(--rankDisabledText);
242273
}
243274
}
244275
}
@@ -248,6 +279,30 @@ const swapItems = (index: number, newPosition: number) => {
248279
cursor: not-allowed;
249280
}
250281
282+
.rank-buttons {
283+
display: flex;
284+
gap: var(--rankSpacing);
285+
}
286+
287+
.rank-overlay {
288+
position: absolute;
289+
width: 100%;
290+
height: 100%;
291+
display: flex;
292+
justify-content: center;
293+
align-items: center;
294+
background-color: rgba(244, 243, 242, 0.9);
295+
border-radius: var(--rankBorderRadius);
296+
297+
button {
298+
padding: var(--rankSpacing) 20px;
299+
}
300+
}
301+
302+
.highlight .rank-overlay {
303+
background-color: rgba(157, 157, 157, 0.9);
304+
}
305+
251306
@media screen and (max-width: #{$sm}) {
252307
.rank-buttons {
253308
display: none;

packages/web-forms/tests/components/RankControl.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ describe('RankControl', () => {
1212
return rankControl.findAll('.rank-label').map((element) => element.text());
1313
};
1414

15+
const clickOnRankItemsButton = async (rankControl: VueWrapper) => {
16+
const button = rankControl.find('.rank-overlay button');
17+
expect(button.exists()).toBe(true);
18+
await button.trigger('click');
19+
};
20+
1521
const getRankControlWithRandomize = async () => {
1622
const xform = await getReactiveForm('1-rank.xml');
1723

@@ -80,6 +86,7 @@ describe('RankControl', () => {
8086
const rankControl = await getRankControlWithRandomize();
8187
expect(rankControl.exists()).toBe(true);
8288

89+
await clickOnRankItemsButton(rankControl);
8390
const allOptions = getAllOptions(rankControl);
8491
expect(allOptions.length).toEqual(10);
8592
expect(allOptions).have.all.members(expectedOptions);
@@ -89,6 +96,7 @@ describe('RankControl', () => {
8996
const rankControl = await getRankControlWithRandomize();
9097
expect(rankControl.exists()).toBe(true);
9198

99+
await clickOnRankItemsButton(rankControl);
92100
const expectedOptions = getAllOptions(rankControl);
93101
swapItems(expectedOptions, 4, 3);
94102
swapItems(expectedOptions, 6, 7);
@@ -104,6 +112,7 @@ describe('RankControl', () => {
104112
const rankControl = await getRankControlWithRandomize();
105113
expect(rankControl.exists()).toBe(true);
106114

115+
await clickOnRankItemsButton(rankControl);
107116
const expectedOptions = getAllOptions(rankControl);
108117
swapItems(expectedOptions, 3, 2);
109118

@@ -133,6 +142,7 @@ describe('RankControl', () => {
133142
const rankControl = refreshedFormQuestions[1].findComponent(RankControl) as VueWrapper;
134143
expect(rankControl.exists()).toBe(true);
135144

145+
await clickOnRankItemsButton(rankControl);
136146
const rankOptions = getAllOptions(rankControl);
137147
expect(rankOptions).length(2);
138148
expect(rankOptions).toEqual(['Environmental Sustainability', 'Creativity and Innovation']);

0 commit comments

Comments
 (0)