Просмотр исходного кода

Fix WizardStepper positioning drift and modal overflow

- Center the active step using offsetLeft/offsetWidth instead of getBoundingClientRect,
  so repeated or mid-transition centering calls use untransformed layout positions and
  cannot accumulate drift.
- Constrain the stepper to width: 100% with overflow: hidden so its max-content track
  can no longer extend past the modal body into the header/footer area.
- Replace the window-size watcher with a ResizeObserver on the stepper's viewport so
  it re-centers when the modal body resizes for any reason.
master
Perplexity 2 часов назад
Родитель
Сommit
a81e28c1ab
2 измененных файлов: 33 добавлений и 20 удалений
  1. +32
    -19
      src/components/wizard/WizardStepper.vue
  2. +1
    -1
      tsconfig.app.tsbuildinfo

+ 32
- 19
src/components/wizard/WizardStepper.vue Просмотреть файл

@@ -28,8 +28,7 @@
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useWindowSize } from "@vueuse/core";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import type { WizardStep } from "../../composables/useProposalWizard";

const props = defineProps<{
@@ -50,6 +49,8 @@ const translateX = ref(0);
/** Suppresses the transition until the first centering, so the initial position snaps into place. */
const animated = ref(false);

let resizeObserver: ResizeObserver | null = null;

const setStepRef = (el: Element | any, index: number) => {
if (el instanceof HTMLElement) {
stepRefs.set(index, el);
@@ -58,19 +59,19 @@ const setStepRef = (el: Element | any, index: number) => {
}
};

/** Centers the current step in the viewport; overflow at the extremities is masked by the fade. */
/**
* Centers the current step in the viewport using untransformed layout math, so repeated or
* repeat-triggered calls stay correct while a transition is running.
*/
const centerCurrentStep = () => {
const viewport = viewportRef.value;
const track = trackRef.value;
const step = stepRefs.get(props.current);
if (!viewport || !track || !step) return;

const stepRect = step.getBoundingClientRect();
const trackRect = track.getBoundingClientRect();
if (!viewport || !step) return;

// Rects already include the current transform, so adjust from where the step is now.
const stepCenter = stepRect.left + stepRect.width / 2 - trackRect.left;
translateX.value += viewport.clientWidth / 2 - stepCenter;
// offsetLeft is relative to the track (offsetParent), independent of the current transform,
// so repeated calls stay correct even while a transition is running
const stepCenter = step.offsetLeft + step.offsetWidth / 2;
translateX.value = Math.round(viewport.clientWidth / 2 - stepCenter);
};

const recenter = async (withAnimation: boolean) => {
@@ -90,13 +91,22 @@ const trackStyle = computed(() => ({
: "none",
}));

const { width: windowWidth } = useWindowSize();

watch(() => props.current, () => recenter(true));
watch(() => props.steps, () => recenter(false), { deep: true });
watch(windowWidth, () => recenter(false));

onMounted(() => recenter(false));
onMounted(async () => {
await recenter(false);

// Re-center when the modal body resizes (e.g. content loading, window changes)
if (viewportRef.value) {
resizeObserver = new ResizeObserver(() => recenter(false));
resizeObserver.observe(viewportRef.value);
}
});

onBeforeUnmount(() => {
resizeObserver?.disconnect();
});

const stepClasses = (index: number) => {
if (index === props.current) return "text-slate-900 opacity-100";
@@ -113,12 +123,14 @@ const badgeClasses = (index: number) => {

<style scoped>
.wizard-stepper {
/* Single line; steps that overflow are faded out by the mask, so the extremities
show nothing rather than a hard clip or a scrollbar */
/* Single line that never pushes out of the modal body; overflowing steps are faded
by the mask, so the extremities show nothing rather than a hard clip or a scrollbar */
overflow: hidden;
display: block;
width: 100%;
white-space: nowrap;
mask-image: linear-gradient(to right, transparent 0, black 18%, black 82%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent 0, black 18%, black 82%, transparent 100%);
mask-image: linear-gradient(to right, transparent 0, black 20%, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent 0, black 20%, black 80%, transparent 100%);
}

.wizard-stepper-track {
@@ -132,6 +144,7 @@ const badgeClasses = (index: number) => {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
border-radius: 0.5rem;
padding: 0.375rem 0.5rem;
transition: opacity 300ms ease, color 300ms ease, background-color 300ms ease;


+ 1
- 1
tsconfig.app.tsbuildinfo
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


Загрузка…
Отмена
Сохранить