diff --git a/services/web/frontend/js/shared/components/notification-scrolled-to.tsx b/services/web/frontend/js/shared/components/notification-scrolled-to.tsx new file mode 100644 index 0000000000..9d6e77f200 --- /dev/null +++ b/services/web/frontend/js/shared/components/notification-scrolled-to.tsx @@ -0,0 +1,46 @@ +import Notification, { + NotificationProps, +} from '@/shared/components/notification' +import { useEffect } from 'react' + +function elementIsInView(el: HTMLElement) { + const scroll = window.scrollY + const boundsTop = el.getBoundingClientRect().top + scroll + + const viewport = { + top: scroll, + bottom: scroll + window.innerHeight, + } + + const bounds = { + top: boundsTop, + bottom: boundsTop + el.clientHeight, + } + + return ( + (bounds.bottom >= viewport.top && bounds.bottom <= viewport.bottom) || + (bounds.top <= viewport.bottom && bounds.top >= viewport.top) + ) +} + +function NotificationScrolledTo({ ...props }: NotificationProps) { + useEffect(() => { + if (props.id) { + const alert = document.getElementById(props.id) + if (alert && !elementIsInView(alert)) { + alert.scrollIntoView({ behavior: 'smooth' }) + } + } + }, [props]) + + const notificationProps = { ...props } + + if (!notificationProps.className) { + notificationProps.className = '' + } + + notificationProps.className = `${notificationProps.className} notification-with-scroll-margin` + + return +} +export default NotificationScrolledTo diff --git a/services/web/frontend/js/shared/components/notification.tsx b/services/web/frontend/js/shared/components/notification.tsx index c8f15be406..574a7c44d7 100644 --- a/services/web/frontend/js/shared/components/notification.tsx +++ b/services/web/frontend/js/shared/components/notification.tsx @@ -10,7 +10,7 @@ export type NotificationType = | 'error' | 'offer' -type NotificationProps = { +export type NotificationProps = { action?: React.ReactElement ariaLive?: 'polite' | 'off' | 'assertive' className?: string @@ -21,6 +21,7 @@ type NotificationProps = { onDismiss?: () => void title?: string type: NotificationType + id?: string } function NotificationIcon({ @@ -57,6 +58,7 @@ function Notification({ onDismiss, title, type, + id, }: NotificationProps) { type = type || 'info' const { t } = useTranslation() @@ -83,6 +85,7 @@ function Notification({ className={notificationClassName} aria-live={ariaLive || 'off'} role="alert" + id={id} > diff --git a/services/web/frontend/stylesheets/components/forms.less b/services/web/frontend/stylesheets/components/forms.less index 001f46a52a..5a04165714 100755 --- a/services/web/frontend/stylesheets/components/forms.less +++ b/services/web/frontend/stylesheets/components/forms.less @@ -192,15 +192,6 @@ input[type='date'] { margin-bottom: @form-group-margin-bottom; } -// Form alert -// -// adds top padding/mergin so that element has spacing when scrolled into -// view, but no vertical spacing is applied to layout -.form-group#form-alert { - padding-top: @form-group-margin-bottom; - margin-top: -@form-group-margin-bottom; -} - // Checkboxes and radios // // Indent the labels to position radios/checkboxes as hanging controls. diff --git a/services/web/frontend/stylesheets/components/notifications.less b/services/web/frontend/stylesheets/components/notifications.less index dd86eb061c..b46fa9d910 100644 --- a/services/web/frontend/stylesheets/components/notifications.less +++ b/services/web/frontend/stylesheets/components/notifications.less @@ -180,6 +180,10 @@ } } +.notification-with-scroll-margin { + scroll-margin: 16px; +} + .notification-list { .notification { margin-bottom: @margin-md;