Skip to content

Commit 3579ece

Browse files
committed
feat(QuestionStepper): update stepper layout to be closer to ODK Collect
1 parent ee5cd8e commit 3579ece

File tree

2 files changed

+68
-28
lines changed

2 files changed

+68
-28
lines changed

packages/web-forms/src/components/OdkWebForm.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ watchEffect(() => {
210210
<div class="form-questions">
211211
<div class="flex flex-column gap-2">
212212
<QuestionList v-if="!stepperLayout" :nodes="odkForm.currentState.children" />
213-
<QuestionStepper v-if="stepperLayout" :nodes="odkForm.currentState.children" @endOfForm="showSendButton=true" />
213+
<!-- Note that QuestionStepper has the 'Send' button integrated instead of using the button below -->
214+
<QuestionStepper v-if="stepperLayout" :nodes="odkForm.currentState.children" @sendFormFromStepper="handleSubmit()" />
214215
</div>
215216
</div>
216217
</template>
@@ -222,7 +223,7 @@ watchEffect(() => {
222223
</div>
223224
</div>
224225

225-
<div class="powered-by-wrapper">
226+
<div v-if="showSendButton" class="powered-by-wrapper">
226227
<a class="anchor" href="https://getodk.org" target="_blank">
227228
<span class="caption">Powered by</span>
228229
<img

packages/web-forms/src/components/QuestionStepper.vue

+65-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed, watch, provide } from 'vue';
2+
import { ref, computed, provide } from 'vue';
33
import type { ComponentPublicInstance } from 'vue';
44
import type {
55
AnyControlNode,
@@ -17,7 +17,7 @@ import ProgressBar from 'primevue/progressbar';
1717
import Button from 'primevue/button';
1818
1919
const props = defineProps<{ nodes: readonly GeneralChildNode[] }>();
20-
const emit = defineEmits(['endOfForm']);
20+
const emit = defineEmits(['sendFormFromStepper']);
2121
2222
const isGroupNode = (node: GeneralChildNode): node is GroupNode => {
2323
return node.nodeType === 'group';
@@ -59,44 +59,47 @@ const steps = computed(() =>
5959
);
6060
6161
// Handle stepper state
62-
const currentStep = ref(0);
63-
const isCurrentStepValidated = ref(true);
62+
const firstStep = 0;
63+
const finalStep = steps.value.length;
64+
const currentStep = ref(firstStep);
6465
const submitPressed = ref(false);
6566
provide('submitPressed', submitPressed);
6667
67-
const validateStep = () => {
68+
const allFieldsValid = () => {
6869
// Manually trigger submitPressed to display error messages
6970
submitPressed.value = true;
7071
7172
const currentNode = steps.value[currentStep.value];
73+
74+
// Check group error array
7275
if (isGroupNode(currentNode) && currentNode.validationState.violations.length > 0) {
73-
isCurrentStepValidated.value = false;
76+
return false;
77+
78+
// Check question single error
7479
} else if (currentNode.validationState.violation) {
75-
isCurrentStepValidated.value = false;
76-
} else {
77-
isCurrentStepValidated.value = true;
80+
return false;
7881
}
82+
83+
return true;
7984
}
8085
const nextStep = () => {
81-
validateStep();
86+
if (!allFieldsValid()) {
87+
// There was an error validating
88+
return false;
89+
}
8290
83-
if (isCurrentStepValidated.value && currentStep.value < steps.value.length - 1) {
91+
// Do not increment further if at end of form
92+
if (currentStep.value < steps.value.length - 1) {
8493
// Reset validation triggered later in the form
8594
submitPressed.value = false;
86-
// Also reset validation state of current node
87-
isCurrentStepValidated.value = true;
8895
currentStep.value++;
8996
}
9097
};
9198
const prevStep = () => {
92-
if (currentStep.value > 0) {
99+
if (currentStep.value > firstStep) {
93100
currentStep.value--;
94101
}
95102
};
96-
const isLastStep = computed(() => currentStep.value === steps.value.length - 1);
97-
watch(isLastStep, (newValue) => {
98-
emit('endOfForm', newValue);
99-
});
100103
101104
// // Calculate stepper progress
102105
// const totalNodes = computed(() =>
@@ -137,19 +140,55 @@ watch(isLastStep, (newValue) => {
137140
<ExpectModelNode v-else :node="step" />
138141
</template>
139142
</div>
140-
141-
<div class="navigation-buttons">
142-
<Button label="Previous" @click="prevStep" :disabled="currentStep === 0" />
143-
<Button label="Next" @click="nextStep" :disabled="isCurrentStepValidated && currentStep === steps.length - 1" />
144-
</div>
145143
</div>
144+
145+
<div class="navigation-button-group">
146+
<!-- If swapping to arrows: 🡨 🡪 -->
147+
<Button v-if="currentStep > firstStep" class="navigation-button" label="Back" @click="prevStep" rounded outlined />
148+
<Button v-if="currentStep === finalStep" class="navigation-button" label="Send" @click="allFieldsValid ? emit('sendFormFromStepper') : null" rounded />
149+
<!-- Note the button ordering is important here as we use a last-child selector for styling -->
150+
<Button v-if="currentStep < finalStep" class="navigation-button" label="Next" @click="nextStep" rounded outlined />
151+
</div>
146152
</template>
147153

148154
<style scoped lang="scss">
149-
.navigation-buttons {
155+
.stepper-container {
150156
display: flex;
151-
justify-content: space-between;
157+
flex-direction: column;
158+
flex-grow: 1;
159+
overflow-y: auto;
160+
padding-bottom: 3rem;
161+
162+
:deep(.p-panel) {
163+
box-shadow: none;
164+
}
165+
}
166+
167+
.navigation-button-group {
168+
display: flex;
169+
position: fixed;
170+
bottom: 0;
171+
left: 0;
152172
width: 100%;
153-
margin-top: 1rem;
173+
background: white;
174+
padding: 1rem;
175+
justify-content: space-between;
176+
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
177+
}
178+
179+
/* Ensure Next button is on the right when Back is hidden */
180+
.navigation-button-group .navigation-button:last-child {
181+
margin-left: auto;
182+
}
183+
184+
/* If only one button is visible, align it to the right */
185+
.navigation-button-group:has(.navigation-button:first-child:last-child) {
186+
justify-content: flex-end;
187+
}
188+
189+
.navigation-button {
190+
padding-left: 3rem;
191+
padding-right: 3rem;
192+
font-size: 1rem;
154193
}
155194
</style>

0 commit comments

Comments
 (0)