diff --git a/.changeset/serious-chefs-provide.md b/.changeset/serious-chefs-provide.md new file mode 100644 index 0000000000..fd84577330 --- /dev/null +++ b/.changeset/serious-chefs-provide.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +--- + +Avoid Layout Shift for DismissableBanner when using a storageApi with latency (e.g. user-settings-backend) + +Properly handle the `unknown` state of the storageApi. There's a trade-off: this may lead to some Layout Shift if the banner has not been dismissed, but once it has been dismissed, you won't have any. diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 8c18a1feee..e3e423d166 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -102,11 +102,14 @@ export const DismissableBanner = (props: Props) => { const classes = useStyles(); const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); - const rawDismissedBanners = - notificationsStore.snapshot('dismissedBanners').value ?? []; + const dismissedBannersSnapshot = + notificationsStore.snapshot('dismissedBanners'); const [dismissedBanners, setDismissedBanners] = useState( - new Set(rawDismissedBanners), + new Set(dismissedBannersSnapshot.value ?? []), + ); + const [loadingSettings, setLoadingSettings] = useState( + dismissedBannersSnapshot.presence === 'unknown', ); const observedItems = useObservable( @@ -114,11 +117,21 @@ export const DismissableBanner = (props: Props) => { ); useEffect(() => { + if (observedItems?.presence === 'unknown' || observedItems === undefined) { + setLoadingSettings(true); + } + if (observedItems?.value) { const currentValue = observedItems?.value ?? []; setDismissedBanners(new Set(currentValue)); } - }, [observedItems?.value]); + if ( + observedItems?.presence === 'absent' || + observedItems?.presence === 'present' + ) { + setLoadingSettings(false); + } + }, [observedItems]); const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); @@ -131,7 +144,7 @@ export const DismissableBanner = (props: Props) => { ? { vertical: 'bottom', horizontal: 'center' } : { vertical: 'top', horizontal: 'center' } } - open={!dismissedBanners.has(id)} + open={!loadingSettings && !dismissedBanners.has(id)} classes={{ root: classNames(classes.root, !fixed && classes.topPosition), }}