Skip to content

Commit

Permalink
Merge pull request #57 from jameel-institute/jidea-83-param-help-ui
Browse files Browse the repository at this point in the history
Jidea-83 Parameter help UI
  • Loading branch information
EmmaLRussell authored Oct 25, 2024
2 parents eacb363 + dfefbf7 commit 156fded
Show file tree
Hide file tree
Showing 17 changed files with 1,283 additions and 5,707 deletions.
48 changes: 46 additions & 2 deletions assets/scss/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ body {
border-bottom-right-radius: 0 !important;
}

.btn-group > label.btn {
.btn-group label.btn {
border-radius: 0.75rem;
}

.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {
background-color: var(--cui-btn-color);
}

.btn-group-lg > label.btn {
.btn-group-lg label.btn {
border-radius: 1rem;
}

Expand Down Expand Up @@ -137,3 +137,47 @@ body {
.modal-open {
overflow-y: auto !important;
}

// Restore radio button group styling to allow for buttons no longer being direct children of button groups - which is
// required for tooltips to display
.btn-group > {
.radio-btn-container {
flex-grow: 1;
.btn {
width: 100%;
}
&:not(:last-child) > {
.btn {
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
border-right-width: 0 !important;
}
}
&:not(:first-child) > {
.btn {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
}
}
}

.btn-group-lg > {
.radio-btn-container > {
.btn {
--cui-btn-padding-y: .5rem;
--cui-btn-padding-x: 1rem;
--cui-btn-font-size: 1.25rem;
--cui-btn-border-radius: var(--cui-border-radius-lg);
}
}
}

// In vue-select parameter dropdowns, avoid highlighting both focused (used when navigating with keyboard) and
// hovered menu items, by forcing the unhighlighted background colour on any menu item which has a sibling which
// is hovered. This does not apply to the currently selected menu item (also highlighted, in a different colour).
.vue-select .menu-option:not(.selected):has(~ :hover), // select previous siblings to hovered, if not selected
.vue-select .menu-option:hover ~ .menu-option:not(.selected) // select subsequent siblings to hovered, if not selected
{
background-color: var(--vs-option-bg)!important;
}
30 changes: 16 additions & 14 deletions components/EditParameters.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<template>
<CModal
:visible="modalVisible"
aria-labelledby="modalTitle"
@close="() => { modalVisible = false }"
>
<CModalHeader>
<CModalTitle id="modalTitle">
Edit parameters
</CModalTitle>
</CModalHeader>
<CModalBody>
<ParameterForm :in-modal="true" />
</CModalBody>
</CModal>
<Teleport to="body">
<CModal
:visible="modalVisible"
aria-labelledby="modalTitle"
@close="() => { modalVisible = false }"
>
<CModalHeader>
<CModalTitle id="modalTitle">
Edit parameters
</CModalTitle>
</CModalHeader>
<CModalBody>
<ParameterForm :in-modal="true" />
</CModalBody>
</CModal>
</Teleport>
<CTooltip
content="Edit parameters"
placement="top"
Expand Down
163 changes: 123 additions & 40 deletions components/ParameterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
class="field-container"
>
<div v-if="renderAsRadios(parameter)" class="button-group-container">
<CRow>
<ParameterIcon :parameter="parameter" />
<CFormLabel :for="parameter.id">
{{ parameter.label }}
</CFormLabel>
<CRow class="pe-2">
<ParameterHeader :parameter="parameter" />
</CRow>
<CRow>
<CButtonGroup
Expand All @@ -28,48 +25,65 @@
:class="`${pulsingParameters.includes(parameter.id) ? 'pulse' : ''}`"
@change="handleChange(parameter)"
>
<CFormCheck
<CTooltip
v-for="(option) in parameter.options"
:id="option.id"
:key="option.id"
v-model="formData[parameter.id]"
type="radio"
:button="{ color: 'primary', variant: 'outline' }"
:name="parameter.id"
autocomplete="off"
:label="option.label"
:value="option.id"
/>
:content="option.description"
placement="top"
>
<template #toggler="{ togglerId, on }">
<div
class="radio-btn-container"
:aria-describedby="togglerId"
v-on="on"
>
<CFormCheck
:id="option.id"
v-model="formData[parameter.id]"
type="radio"
:button="{ color: 'primary', variant: 'outline' }"
:name="parameter.id"
autocomplete="off"
:label="option.label"
:value="option.id"
/>
</div>
</template>
</CTooltip>
</CButtonGroup>
</CRow>
</div>
<div v-else-if="renderAsSelect(parameter)">
<ParameterIcon :parameter="parameter" />
<CFormLabel :for="parameter.id">
{{ parameter.label }}
</CFormLabel>
<select
:id="parameter.id"
v-model="formData[parameter.id]"
:aria-label="parameter.label"
class="form-select" :class="[appStore.largeScreen ? 'form-select-lg' : '', pulsingParameters.includes(parameter.id) ? 'pulse' : '']"
@change="handleChange(parameter)"
>
<option
v-for="(option) in parameter.options"
:key="option.id"
:value="option.id"
:selected="option.id === formData[parameter.id]"
<div v-else-if="renderAsSelect(parameter)" class="select-container">
<CRow>
<ParameterHeader :parameter="parameter" />
<VueSelect
v-model="formData![parameter.id]"
:input-id="parameter.id"
:aria="{ labelledby: `${parameter.id}-label`, required: true }"
class="form-control"
:class="[pulsingParameters.includes(parameter.id) ? 'pulse' : '']"
:options="parameter.options.map((o) => ({ value: o.id, label: o.label, description: o.description }))"
:is-clearable="false"
@option-selected="handleChange(parameter)"
>
{{ option.label }}
</option>
</select>
<template #option="{ option }">
<div class="parameter-option">
<span>{{ option.label }}</span>
<div
v-if="option.description"
:class="option.value === formData[parameter.id] ? 'text-dark' : 'text-secondary'"
>
<small>{{ option.description }}</small>
</div>
</div>
</template>
</VueSelect>
</CRow>
</div>
<div v-else-if="parameter.parameterType === TypeOfParameter.Numeric">
<ParameterIcon :parameter="parameter" />
<CFormLabel :for="parameter.id">
{{ parameter.label }}
</CFormLabel>
<div class="d-flex numeric-header">
<ParameterHeader :parameter="parameter" />
</div>
<div class="d-flex flex-wrap">
<div class="flex-grow-1">
<CFormInput
Expand Down Expand Up @@ -141,6 +155,8 @@ import type { Parameter, ParameterSet, ValueData } from "@/types/parameterTypes"
import type { FetchError } from "ofetch";
import { TypeOfParameter } from "@/types/parameterTypes";
import { CIcon } from "@coreui/icons-vue";
import VueSelect from "vue3-select-component";
import ParameterHeader from "~/components/ParameterHeader.vue";
const props = defineProps<{
inModal: boolean
Expand Down Expand Up @@ -168,6 +184,14 @@ const formData = ref(
// or to the previous scenario's values if any.
appStore.currentScenario.parameters ? { ...appStore.currentScenario.parameters } : initialiseFormDataFromDefaults(),
);
// Making the vue select searchable means that it's possible to unset a parameter value (to undefined) if you clear the search
// input. In this case we just want to be able to revert to the previous value it had. However, this is tricky as we
// don't get the previous value in any watch of formData since it's watching deep changes in an object, and we can't
// use a computed setter for values in an object type. So here we keep a copy of the last full dictionary and reset in the watch
// if required.
const previousFullFormData = ref({ ...formData.value });
const pulsingParameters = ref([] as string[]);
const dependentParameters = computed((): Record<string, string[]> => {
const dependentParameters = {} as { [key: string]: Array<string> };
Expand Down Expand Up @@ -338,6 +362,17 @@ const submitForm = async () => {
};
};
watch(formData, (newVal) => {
if (newVal && paramMetadata.value && previousFullFormData.value) {
const invalid = paramMetadata.value.some(param => !newVal[param.id]);
if (invalid) {
formData.value = previousFullFormData.value;
} else {
previousFullFormData.value = { ...formData.value };
}
}
}, { deep: 1 });
onMounted(() => {
mounted.value = true; // Use in v-show, otherwise there are up to several seconds during which the form shows with out of date values.
Expand All @@ -350,7 +385,7 @@ onMounted(() => {
});
</script>

<style lang="scss">
<style scoped lang="scss">
.inputs {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -390,4 +425,52 @@ onMounted(() => {
box-shadow: 0 0 0 15px rgba(0, 0, 255, 0);
}
}
.select-container {
margin-left: 0.7rem;
margin-right: 0.55rem;
}
.numeric-header {
padding-right: 2.2rem;
}
.vue-select {
--vs-font-size: 1.25rem;
--vs-input-outline: transparent;
--vs-border-radius: 4px;
--vs-line-height: 0.9;
--vs-menu-height: 400px;
--vs-padding: 0;
--vs-option-font-size: var(--vs-font-size);
--vs-option-text-color: var(--vs-text-color);
--vs-option-hover-color: var(--cui-tertiary-bg);
--vs-option-focused-color: var(--vs-option-hover-color);
--vs-option-selected-color: var(--cui-primary-bg-subtle);
--vs-option-padding: 0 8px;
}
.vue-select {
border-radius: 1rem!important;
}
:deep(.vue-select .control) {
border-style: none;
}
:deep(.vue-select .menu) {
border-radius: 0.5rem!important;
}
// This prevents odd default styling where search text appears after width of current value
:deep(.vue-select .search-input) {
position: absolute;
left: 0;
width: 100%;
}
// This fixes an issue where the open select contracted in width because .single-value items had absolute position
:deep(.open .single-value) {
position: relative!important;
}
</style>
15 changes: 15 additions & 0 deletions components/ParameterHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<ParameterIcon :parameter="parameter" />
<CFormLabel :id="`${parameter.id}-label`" :for="parameter.id">
{{ parameter.label }}
</CFormLabel>
<TooltipHelp :help-text="parameter.description" :classes="['ms-auto', 'me-3', 'mt-1', 'smaller-icon']" />
</template>

<script setup lang="ts">
import type { Parameter } from "~/types/parameterTypes";
defineProps<{
parameter: Parameter
}>();
</script>
13 changes: 1 addition & 12 deletions components/TimeSeries.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@
<CAccordionHeader class="border-top" @click="handleAccordionToggle">
<span aria-describedby="labelDescriptor">{{ seriesMetadata?.label }}</span>
<span id="labelDescriptor" class="visually-hidden">{{ seriesMetadata?.description }}</span>
<CTooltip
v-if="seriesMetadata?.description"
:content="seriesMetadata.description"
placement="top"
>
<template #toggler="{ togglerId, on }">
<CIconSvg class="icon smaller-icon opacity-50 ms-2 mb-1">
<img src="~/assets/icons/circleQuestion.svg" :aria-describedby="togglerId" v-on="on">
</CIconSvg>
</template>
</CTooltip>
<TooltipHelp :help-text="seriesMetadata?.description" :classes="['ms-2', 'mb-1', 'smaller-icon']" />
</CAccordionHeader>
<CAccordionBody>
<div
Expand All @@ -39,7 +29,6 @@
</template>

<script lang="ts" setup>
import { CIconSvg } from "@coreui/icons-vue";
import * as Highcharts from "highcharts";
import accessibilityInitialize from "highcharts/modules/accessibility";
import exportDataInitialize from "highcharts/modules/export-data";
Expand Down
22 changes: 22 additions & 0 deletions components/TooltipHelp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<CTooltip
v-if="helpText"
:content="helpText"
placement="top"
>
<template #toggler="{ togglerId, on }">
<CIconSvg class="icon help-icon opacity-50 p-0" :class="classes">
<img src="~/assets/icons/circleQuestion.svg" :aria-describedby="togglerId" v-on="on">
</CIconSvg>
</template>
</CTooltip>
</template>

<script setup lang="ts">
import { CIconSvg } from "@coreui/icons-vue";
defineProps<{
helpText?: string
classes: string[]
}>();
</script>
Loading

0 comments on commit 156fded

Please sign in to comment.