use resizeObserver instead of window listeners

safer access to `localStorage` initial state
ie. legacy localStorage having `expanded` but not `multiline`
This commit is contained in:
David Tsay 2024-09-27 14:21:36 -07:00
parent 1e8e744b23
commit 89e8ed310e
3 changed files with 129 additions and 93 deletions

View File

@ -33,22 +33,17 @@
:class="{ :class="{
'l-shell__head--expanded': headExpanded, 'l-shell__head--expanded': headExpanded,
'l-shell__head--minify-indicators': !headExpanded, 'l-shell__head--minify-indicators': !headExpanded,
'l-shell__head--indicators-single-line': !indicatorsMultiline, 'l-shell__head--indicators-single-line': !indicatorsMultiline
'--indicators-overflowing': indicatorsOverflowing
}" }"
> >
<CreateButton class="l-shell__create-button" /> <CreateButton class="l-shell__create-button" />
<GrandSearch ref="grand-search" /> <GrandSearch ref="grand-search" />
<StatusIndicators <StatusIndicators ref="indicatorsComponent" />
:head-expanded="headExpanded"
:indicators-multiline="indicatorsMultiline"
@indicators-overflowing="indicatorsOverflowUpdate"
/>
<button <button
class="l-shell__head__button" class="l-shell__head__button"
:class="[indicatorsMultilineCssClass, indicatorsOverflowingCssClass]" :class="indicatorsMultilineCssClass"
:aria-label="`Display as ${indicatorsMultiline ? 'single line' : 'multiple lines'}`" :aria-label="indicatorsMultilineLabel"
:title="`Display as ${indicatorsMultiline ? 'single line' : 'multiple lines'}`" :title="indicatorsMultilineLabel"
@click="toggleIndicatorsMultiline" @click="toggleIndicatorsMultiline"
></button> ></button>
<button <button
@ -176,6 +171,8 @@
</template> </template>
<script> <script>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import ObjectView from '../components/ObjectView.vue'; import ObjectView from '../components/ObjectView.vue';
import Inspector from '../inspector/InspectorPanel.vue'; import Inspector from '../inspector/InspectorPanel.vue';
import Toolbar from '../toolbar/ToolbarContainer.vue'; import Toolbar from '../toolbar/ToolbarContainer.vue';
@ -190,6 +187,10 @@ import GrandSearch from './search/GrandSearch.vue';
import NotificationBanner from './status-bar/NotificationBanner.vue'; import NotificationBanner from './status-bar/NotificationBanner.vue';
import StatusIndicators from './status-bar/StatusIndicators.vue'; import StatusIndicators from './status-bar/StatusIndicators.vue';
const SHELL_HEAD_LOCAL_STORAGE_KEY = 'openmct-shell-head';
const DEFAULT_HEAD_EXPANDED = true;
const DEFAULT_INDICATORS_MULTILINE = true;
export default { export default {
components: { components: {
Inspector, Inspector,
@ -207,15 +208,120 @@ export default {
RecentObjectsList RecentObjectsList
}, },
inject: ['openmct'], inject: ['openmct'],
data: function () { setup() {
let storedHeadProps = window.localStorage.getItem('openmct-shell-head'); let resizeObserver;
let headExpanded = true; let element;
let indicatorsMultiline = true;
if (storedHeadProps) { const storedHeadProps = localStorage.getItem(SHELL_HEAD_LOCAL_STORAGE_KEY);
headExpanded = JSON.parse(storedHeadProps).expanded; const storedHeadPropsObject = JSON.parse(storedHeadProps);
indicatorsMultiline = JSON.parse(storedHeadProps).multiline; const storedHeadExpanded = storedHeadPropsObject?.expanded;
const storedIndicatorsMultiline = storedHeadPropsObject?.multiline;
// template ref of StatusIndicators component
const indicatorsComponent = ref(null);
const width = ref(null);
const scrollWidth = ref(null);
const headExpanded = ref(storedHeadExpanded ?? DEFAULT_HEAD_EXPANDED);
const indicatorsMultiline = ref(storedIndicatorsMultiline ?? DEFAULT_INDICATORS_MULTILINE);
const isOverflowing = computed(() => scrollWidth.value > width.value);
const indicatorsMultilineCssClass = computed(() => {
const multilineClass = indicatorsMultiline.value ? 'icon-singleline' : 'icon-multiline';
const overflowingClass =
isOverflowing.value && !indicatorsMultiline.value
? 'c-button c-button--major'
: 'c-icon-button';
return `${multilineClass} ${overflowingClass}`;
});
const indicatorsMultilineLabel = computed(() => {
return `Display as ${indicatorsMultiline.value ? 'single line' : 'multiple lines'}`;
});
const initialHeadProps = JSON.stringify({
expanded: headExpanded.value,
multiline: indicatorsMultiline.value
});
if (initialHeadProps !== storedHeadProps) {
localStorage.setItem(SHELL_HEAD_LOCAL_STORAGE_KEY, initialHeadProps);
} }
onMounted(() => {
resizeObserver = new ResizeObserver((entries) => {
width.value = entries[0].target.clientWidth;
scrollWidth.value = entries[0].target.scrollWidth;
});
// indicatorsContainer is a template ref inside of indicatorsComponent
element = indicatorsComponent.value.$refs.indicatorsContainer;
if (!indicatorsMultiline.value) {
observeIndicatorsOverflow();
}
});
onUnmounted(() => {
resizeObserver.disconnect();
});
function observeIndicatorsOverflow() {
resizeObserver.observe(element);
}
function unObserveIndicatorsOverflow() {
resizeObserver.unobserve(element);
}
function checkIndicatorsElementWidths() {
if (!indicatorsMultiline.value) {
width.value = element.clientWidth;
scrollWidth.value = element.scrollWidth;
}
}
async function toggleShellHead() {
headExpanded.value = !headExpanded.value;
setLocalStorageShellHead();
// nextTick is used because the element width on toggle is updated using css
await nextTick();
checkIndicatorsElementWidths();
}
function toggleIndicatorsMultiline() {
indicatorsMultiline.value = !indicatorsMultiline.value;
setLocalStorageShellHead();
if (indicatorsMultiline.value) {
unObserveIndicatorsOverflow();
} else {
observeIndicatorsOverflow();
}
}
function setLocalStorageShellHead() {
localStorage.setItem(
SHELL_HEAD_LOCAL_STORAGE_KEY,
JSON.stringify({
expanded: headExpanded.value,
multiline: indicatorsMultiline.value
})
);
}
return {
indicatorsComponent,
isOverflowing,
headExpanded,
indicatorsMultiline,
indicatorsMultilineCssClass,
indicatorsMultilineLabel,
toggleIndicatorsMultiline,
toggleShellHead
};
},
data() {
return { return {
fullScreen: false, fullScreen: false,
conductorComponent: undefined, conductorComponent: undefined,
@ -224,9 +330,6 @@ export default {
actionCollection: undefined, actionCollection: undefined,
triggerSync: false, triggerSync: false,
triggerReset: false, triggerReset: false,
headExpanded,
indicatorsMultiline,
indicatorsOverflowing: false,
isResizing: false, isResizing: false,
disableClearButton: false disableClearButton: false
}; };
@ -237,12 +340,6 @@ export default {
}, },
resizingClass() { resizingClass() {
return this.isResizing ? 'l-shell__resizing' : ''; return this.isResizing ? 'l-shell__resizing' : '';
},
indicatorsMultilineCssClass() {
return this.indicatorsMultiline ? 'icon-singleline' : 'icon-multiline';
},
indicatorsOverflowingCssClass() {
return this.indicatorsOverflowing ? 'c-button c-button--major' : 'c-icon-button';
} }
}, },
mounted() { mounted() {
@ -280,23 +377,6 @@ export default {
document.msExitFullscreen(); document.msExitFullscreen();
} }
}, },
toggleShellHead() {
this.headExpanded = !this.headExpanded;
this.setLocalStorageShellHead();
},
toggleIndicatorsMultiline() {
this.indicatorsMultiline = !this.indicatorsMultiline;
this.setLocalStorageShellHead();
},
setLocalStorageShellHead() {
window.localStorage.setItem(
'openmct-shell-head',
JSON.stringify({
expanded: this.headExpanded,
multiline: this.indicatorsMultiline
})
);
},
fullScreenToggle() { fullScreenToggle() {
if (this.fullScreen) { if (this.fullScreen) {
this.fullScreen = false; this.fullScreen = false;
@ -344,9 +424,6 @@ export default {
}, },
setClearButtonDisabled(isDisabled) { setClearButtonDisabled(isDisabled) {
this.disableClearButton = isDisabled; this.disableClearButton = isDisabled;
},
indicatorsOverflowUpdate(isOverflowing) {
this.indicatorsOverflowing = isOverflowing;
} }
} }
}; };

View File

@ -274,10 +274,6 @@
margin-left: auto; // Mimics justify-content: flex-end when in single line mode. margin-left: auto; // Mimics justify-content: flex-end when in single line mode.
} }
} }
&.--is-overflowing {
background: rgba(deeppink, 0.1);
}
} }
/******************************* MAIN AREA */ /******************************* MAIN AREA */

View File

@ -17,7 +17,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div ref="indicators" class="l-shell__head-section l-shell__indicators"> <div ref="indicatorsContainer" class="l-shell__head-section l-shell__indicators">
<component <component
:is="indicator.value.vueComponent" :is="indicator.value.vueComponent"
v-for="indicator in sortedIndicators" v-for="indicator in sortedIndicators"
@ -28,21 +28,15 @@
</template> </template>
<script> <script>
import { shallowRef } from 'vue'; import { defineExpose, ref, shallowRef } from 'vue';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
props: { setup() {
headExpanded: { const indicatorsContainer = ref(null);
type: Boolean,
required: true defineExpose({ indicatorsContainer });
}, },
indicatorsMultiline: {
type: Boolean,
required: true
}
},
emits: ['indicators-overflowing'],
data() { data() {
return { return {
indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef) indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef)
@ -57,30 +51,8 @@ export default {
return [...this.indicators].sort((a, b) => b.value.priority - a.value.priority); return [...this.indicators].sort((a, b) => b.value.priority - a.value.priority);
} }
}, },
watch: {
headExpanded() {
this.checkOverflowNextTick();
},
indicatorsMultiline() {
if (!this.indicatorsMultiline) {
window.addEventListener('resize', this.checkOverflow);
} else {
window.removeEventListener('resize', this.checkOverflow);
}
this.checkOverflowNextTick();
}
},
mounted() {
if (!this.indicatorsMultiline) {
// `load` listener is necessary because the width of the Indicators has to be eval'd after other components have loaded.
window.addEventListener('load', this.checkOverflow);
window.addEventListener('resize', this.checkOverflow);
}
},
beforeUnmount() { beforeUnmount() {
this.openmct.indicators.off('addIndicator', this.addIndicator); this.openmct.indicators.off('addIndicator', this.addIndicator);
window.removeEventListener('load', this.checkOverflow);
window.removeEventListener('resize', this.checkOverflow);
}, },
created() { created() {
this.openmct.indicators.on('addIndicator', this.addIndicator); this.openmct.indicators.on('addIndicator', this.addIndicator);
@ -88,15 +60,6 @@ export default {
methods: { methods: {
addIndicator(indicator) { addIndicator(indicator) {
this.indicators.push(shallowRef(indicator)); this.indicators.push(shallowRef(indicator));
},
checkOverflow() {
const element = this.$refs.indicators;
this.$emit('indicators-overflowing', element.scrollWidth > element.clientWidth);
},
checkOverflowNextTick() {
this.$nextTick(() => {
this.checkOverflow();
});
} }
} }
}; };