From 3c920553bdfd886ab909feeced53470db44a3aff Mon Sep 17 00:00:00 2001 From: PosikBoy Date: Thu, 23 Apr 2026 20:23:51 +0300 Subject: [PATCH 1/2] fix(a11y): fixed accessibility in notifications --- src/components/Notification/Notification.scss | 43 ++++- src/components/Notification/Notification.tsx | 147 ++++++++++-------- .../Notification/NotificationAction.tsx | 1 + src/components/Notification/i18n/en.json | 3 + src/components/Notification/i18n/index.ts | 8 + src/components/Notification/i18n/ru.json | 3 + .../Notifications/Notifications.scss | 17 +- .../Notifications/Notifications.tsx | 8 +- .../Notifications/NotificationsList.tsx | 16 +- 9 files changed, 167 insertions(+), 79 deletions(-) create mode 100644 src/components/Notification/i18n/en.json create mode 100644 src/components/Notification/i18n/index.ts create mode 100644 src/components/Notification/i18n/ru.json diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss index c2985d02..88709ef7 100644 --- a/src/components/Notification/Notification.scss +++ b/src/components/Notification/Notification.scss @@ -15,6 +15,11 @@ $notificationSourceIconSize: 36px; color: inherit; text-decoration: none; + &__layout { + position: relative; + width: 100%; + } + &_active:hover { background: var(--g-color-base-simple-hover); cursor: pointer; @@ -24,10 +29,19 @@ $notificationSourceIconSize: 36px; flex: 1; } + &__main-content { + min-width: 0; + } + &__title-wrapper { + display: flex; + align-items: center; + flex: 1; min-width: 0; overflow-x: hidden; + + min-height: var(--g-spacing-7); } &__source-text, @@ -47,7 +61,9 @@ $notificationSourceIconSize: 36px; &__title { font-weight: 500; - color: var(--g-color-text-primary); + + margin: 0; + padding: 0; } &__title-with-source { @@ -55,10 +71,15 @@ $notificationSourceIconSize: 36px; } &__content { + display: flex; + align-items: center; + font-size: 13px; line-height: 18px; color: var(--g-color-text-complementary); + + min-height: var(--g-spacing-7); } &_unread { @@ -80,13 +101,19 @@ $notificationSourceIconSize: 36px; } &__actions_side-actions { + position: absolute; + inset-block-start: 12px; + inset-inline-end: 12px; height: 28px; opacity: 0; } - &:hover &__actions_side-actions, + + &__layout:hover &__actions_side-actions, + &__layout:focus-within &__actions_side-actions, &__actions_side-actions:focus-within { opacity: 1; } + &_mobile &__actions_side-actions { opacity: 1; } @@ -193,4 +220,16 @@ $notificationSourceIconSize: 36px; width: 36px; height: 36px; } + + &__visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } } diff --git a/src/components/Notification/Notification.tsx b/src/components/Notification/Notification.tsx index 1be47050..92241e48 100644 --- a/src/components/Notification/Notification.tsx +++ b/src/components/Notification/Notification.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; -import {Flex, Icon, Link, useMobile, useUniqId} from '@gravity-ui/uikit'; +import {Flex, Icon, Link, Text, useMobile, useUniqId} from '@gravity-ui/uikit'; import {CnMods, block} from '../utils/cn'; import {NotificationProps, NotificationSourceProps} from './definitions'; +import {i18n} from './i18n'; import './Notification.scss'; @@ -13,6 +14,7 @@ const b = block('notification'); type Props = {notification: NotificationProps}; export const Notification = React.memo(function Notification(props: Props) { + const {t} = i18n.useTranslation(); const mobile = useMobile(); const {notification} = props; const { @@ -37,13 +39,15 @@ export const Notification = React.memo(function Notification(props: Props) { const renderedTitle = title ? (
-
{title}
+ + {title} +
) : null; - const renderedSideActions = ( + const renderedSideActions = props.notification.sideActions ? (
{props.notification.sideActions}
- ); + ) : null; const renderedBottomActions = props.notification.bottomActions ? (
@@ -64,7 +68,7 @@ export const Notification = React.memo(function Notification(props: Props) { }) : null} {source?.title && formattedDate ? : null} - {formattedDate ?
{formattedDate}
: null} + {formattedDate ? : null} ) : null; @@ -72,83 +76,98 @@ export const Notification = React.memo(function Notification(props: Props) { const hasSourceOnBottom = renderedSourceText && sourcePlacement === 'bottom'; const topPart = renderedTitle || hasSourceOnTop - ? withSideActions( - renderTitleAndSource(renderedTitle, hasSourceOnTop ? renderedSourceText : null), - renderedSideActions, - ) + ? renderTitleAndSource(renderedTitle, hasSourceOnTop ? renderedSourceText : null) : null; - const notificationContent = ( + const notificationMainContent = ( - {sourceIcon ?
{sourceIcon}
: null} - - - - {topPart} - - {withSideActions( - renderedContent, - !renderedTitle && !hasSourceOnTop ? renderedSideActions : null, - )} + {topPart} + {renderedContent} + {hasSourceOnBottom ? ( +
{renderedSourceText}
+ ) : null} +
+ ); - {hasSourceOnBottom ? ( -
{renderedSourceText}
- ) : null} + const notificationInnerContent = ( + + {sourceIcon ?
{sourceIcon}
: null} - {renderedBottomActions} + + + {notificationMainContent} + + {renderedBottomActions}
); - if (notification.href) { - const handleLinkClick: React.MouseEventHandler = (event) => { - if (event.target instanceof Element && event.target.closest('button')) { - event.preventDefault(); - return; - } + const hiddenUnreadLabel = unread ? ( + {t('unread-label')} + ) : null; - notification.onClick?.(event); - }; - - return ( - - {notificationContent} - - ); - } + const commonProps = { + className: b(modifiers, notification.className), + onMouseEnter: notification.onMouseEnter, + onMouseLeave: notification.onMouseLeave, + }; - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + const handleClick = (event: React.MouseEvent) => { + if (event.target instanceof Element && event.target.closest('button')) { + event.preventDefault(); + event.stopPropagation(); + return; + } + + notification.onClick?.(event); + }; + + const interactiveContent = notification.href ? ( + } + href={notification.href} + target={notification.target ?? '_blank'} + rel="noreferrer" + > + {hiddenUnreadLabel} + {notificationInnerContent} + + ) : (
} + onKeyDown={ + notification.onClick + ? (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleClick(event as unknown as React.MouseEvent); + } + } + : undefined + } > - {notificationContent} + {hiddenUnreadLabel} + {notificationInnerContent}
); -}); -function withSideActions(content: React.ReactNode, sideActions: React.ReactNode) { - return sideActions ? ( - - {content} - {sideActions} - - ) : ( - content + return ( +
+ {interactiveContent} + {renderedSideActions} +
); -} +}); function renderTitleAndSource(title: React.ReactNode, source: React.ReactNode) { return title && source ? ( diff --git a/src/components/Notification/NotificationAction.tsx b/src/components/Notification/NotificationAction.tsx index 9594970d..625a121e 100644 --- a/src/components/Notification/NotificationAction.tsx +++ b/src/components/Notification/NotificationAction.tsx @@ -23,6 +23,7 @@ export const NotificationAction = React.memo(function NotificationAction({action href={action.href as any} target={action.target} onClick={action.onClick} + aria-label={action.text} > {content} diff --git a/src/components/Notification/i18n/en.json b/src/components/Notification/i18n/en.json new file mode 100644 index 00000000..66233aac --- /dev/null +++ b/src/components/Notification/i18n/en.json @@ -0,0 +1,3 @@ +{ + "unread-label": "Unread notification" +} diff --git a/src/components/Notification/i18n/index.ts b/src/components/Notification/i18n/index.ts new file mode 100644 index 00000000..9c996a24 --- /dev/null +++ b/src/components/Notification/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '@gravity-ui/uikit/i18n'; + +import {NAMESPACE} from '../../utils/cn'; + +import en from './en.json'; +import ru from './ru.json'; + +export const i18n = addComponentKeysets({en, ru}, `${NAMESPACE}notification`); diff --git a/src/components/Notification/i18n/ru.json b/src/components/Notification/i18n/ru.json new file mode 100644 index 00000000..ffefa6bc --- /dev/null +++ b/src/components/Notification/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "unread-label": "Непрочитанное уведомление" +} diff --git a/src/components/Notifications/Notifications.scss b/src/components/Notifications/Notifications.scss index ed9b8aa0..a4d146a5 100644 --- a/src/components/Notifications/Notifications.scss +++ b/src/components/Notifications/Notifications.scss @@ -24,6 +24,7 @@ $block: '.#{variables.$ns}notifications'; line-height: 24px; color: var(--g-color-text-primary); + margin: 0; } &__body { @@ -32,6 +33,12 @@ $block: '.#{variables.$ns}notifications'; overflow-y: auto; } + &__list { + list-style: none; + padding: 0; + margin: 0; + } + &__empty { height: 100%; gap: 16px; @@ -64,7 +71,7 @@ $block: '.#{variables.$ns}notifications'; height: 28px; } - &__notification-wrapper:not(:first-child)::before { + &__item:not(:first-child) &__notification-wrapper::before { content: ''; display: block; border-block-start: 1px solid var(--g-color-line-generic); @@ -72,11 +79,11 @@ $block: '.#{variables.$ns}notifications'; } // :hover - &__notification-wrapper_active:hover:not(:first-child)::before, - &__notification-wrapper_active:hover + &__notification-wrapper::before, + &__item:not(:first-child):has(&__notification-wrapper_active:hover) &__notification-wrapper::before, + &__item:has(&__notification-wrapper_active:hover) + &__item &__notification-wrapper::before, // .unread - &__notification-wrapper_unread:not(:first-child)::before, - &__notification-wrapper_unread + &__notification-wrapper::before { + &__item:not(:first-child):has(&__notification-wrapper_unread) &__notification-wrapper::before, + &__item:has(&__notification-wrapper_unread) + &__item &__notification-wrapper::before { content: ''; display: block; border-block-start: 1px solid transparent; diff --git a/src/components/Notifications/Notifications.tsx b/src/components/Notifications/Notifications.tsx index 1486d221..1f8b2d20 100644 --- a/src/components/Notifications/Notifications.tsx +++ b/src/components/Notifications/Notifications.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import {Text} from '@gravity-ui/uikit'; + import {InfiniteScroll} from '../InfiniteScroll'; import {block} from '../utils/cn'; @@ -55,7 +57,11 @@ export const Notifications = React.memo(function Notifications(props: Notificati ); } - const title =
{props.title || t('title')}
; + const title = ( + + {props.title || t('title')} + + ); return (
diff --git a/src/components/Notifications/NotificationsList.tsx b/src/components/Notifications/NotificationsList.tsx index aa0013d4..aa416011 100644 --- a/src/components/Notifications/NotificationsList.tsx +++ b/src/components/Notifications/NotificationsList.tsx @@ -16,14 +16,16 @@ type Props = { export const NotificationsList = React.memo(function NotificationsList(props: Props) { return ( -
+
    {props.notifications.map((notification) => ( - +
  • + +
  • ))} -
+ ); }); From c905c366aee8738c47fc3848f6e4f1871e379e37 Mon Sep 17 00:00:00 2001 From: PosikBoy Date: Fri, 8 May 2026 18:18:49 +0300 Subject: [PATCH 2/2] fix(a11y): fixed a11y markup component --- src/components/Notification/Notification.scss | 274 +++++++++++++----- src/components/Notification/Notification.tsx | 193 ++++++------ 2 files changed, 281 insertions(+), 186 deletions(-) diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss index 88709ef7..9b8f53c9 100644 --- a/src/components/Notification/Notification.scss +++ b/src/components/Notification/Notification.scss @@ -3,140 +3,277 @@ $block: '.#{variables.$ns}notification'; $notificationSourceIconSize: 36px; +$notificationSourceIconGap: var(--g-spacing-3); +$notificationLeftOffset: $notificationSourceIconSize + 12px; +$notificationSourceLineHeight: 18px; +$notificationSourceGap: var(--g-spacing-1); +$notificationSourceBlockSize: $notificationSourceLineHeight + 4px; +$notificationSideActionsWidth: 36px; +$notificationSideActionsGap: var(--g-spacing-2); +$notificationSideActionsOffset: $notificationSideActionsWidth + 8px; #{$block} { - display: flex; - padding: 12px; - gap: 12px; - border-radius: 4px; + position: relative; + display: block; + border-radius: var(--g-spacing-1); box-sizing: border-box; width: 100%; font: inherit; color: inherit; - text-decoration: none; - &__layout { - position: relative; + background-color: var(--g-color-base-background); + + &__clickable { + display: block; width: 100%; + padding: var(--g-spacing-3); + box-sizing: border-box; + color: inherit; + text-decoration: none; + border-radius: var(--g-spacing-1); + + appearance: none; + background: transparent; + border: 0; + margin: 0; + font: inherit; + text-align: inherit; + outline-offset: var(--g-spacing-half); + } + + &__clickable_active { + cursor: pointer; } &_active:hover { background: var(--g-color-base-simple-hover); - cursor: pointer; } - &__right { - flex: 1; + &__clickable_has-left { + padding-inline-start: calc(var(--g-spacing-3) + #{$notificationLeftOffset}); + } + + &__clickable_has-side-actions { + padding-inline-end: calc(var(--g-spacing-3) + #{$notificationSideActionsOffset}); + } + + &__clickable_has-source-top { + padding-block: calc(var(--g-spacing-2) + #{$notificationSourceBlockSize}) var(--g-spacing-4); + } + + &__clickable_has-source-bottom { + padding-block-end: calc(var(--g-spacing-3) + #{$notificationSourceBlockSize}); + } + + &__clickable_has-bottom-actions { + padding-block-end: calc(var(--g-spacing-3) + 36px + var(--g-spacing-2)); + } + + &__clickable_has-source-bottom#{&}__clickable_has-bottom-actions { + padding-block-end: calc( + var(--g-spacing-3) + #{$notificationSourceBlockSize} + 28px + var(--g-spacing-2) + ); + } + + &__clickable_has-source-top#{&}__clickable_has-bottom-actions { + padding-block-end: calc(var(--g-spacing-6) + #{$notificationSourceBlockSize}); } &__main-content { + display: flex; + flex-direction: column; min-width: 0; } &__title-wrapper { display: flex; align-items: center; - - flex: 1; min-width: 0; - overflow-x: hidden; - - min-height: var(--g-spacing-7); - } - - &__source-text, - &__title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + height: var(--g-spacing-7); } - &__source-text { - color: var(--g-color-text-secondary); - } + &_has-source-top &__title-wrapper { + height: auto; - &__bottom-source { - margin-block-start: 4px; + margin: var(--g-spacing-1) 0 var(--g-spacing-1) 0; } &__title { font-weight: 500; - margin: 0; padding: 0; - } - &__title-with-source { - margin-block-end: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - &__content { + &__content-wrapper { + min-height: var(--g-spacing-7); + display: flex; align-items: center; + min-width: 0; + } + &__content { font-size: 13px; line-height: 18px; - color: var(--g-color-text-complementary); + word-break: break-word; + overflow-wrap: anywhere; + } + + &__left { + position: absolute; + inset-block-start: var(--g-spacing-3); + inset-inline-start: var(--g-spacing-3); + display: flex; + align-items: center; min-height: var(--g-spacing-7); + z-index: 1; } - &_unread { - background: var(--g-color-base-selection); - &:hover { - background: var(--g-color-base-selection-hover); - } + &__top-source, + &__bottom-source { + position: absolute; + inset-inline: var(--g-spacing-3) var(--g-spacing-3); + z-index: 1; + width: fit-content; } - &__actions { - display: flex; - align-items: center; + &__top-source { + inset-block-start: var(--g-spacing-3); } - &__actions_bottom-actions { - margin-block-start: 8px; - gap: 8px; - flex-wrap: wrap; + &__bottom-source { + inset-block-end: var(--g-spacing-3); + } + + &__source-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--g-color-text-secondary); + + display: inline-flex; + gap: var(--g-spacing-1); + } + + &__right-source-title { + pointer-events: auto; + position: relative; } &__actions_side-actions { position: absolute; - inset-block-start: 12px; - inset-inline-end: 12px; - height: 28px; + inset-block-start: var(--g-spacing-2); + inset-inline-end: var(--g-spacing-3); + z-index: 1; + + display: flex; + flex-direction: column; + align-items: center; + gap: var(--g-spacing-1); + opacity: 0; + transition: opacity 0.1s ease-in-out; } - &__layout:hover &__actions_side-actions, - &__layout:focus-within &__actions_side-actions, + #{$block}:hover &__actions_side-actions, + #{$block}:focus-within &__actions_side-actions, &__actions_side-actions:focus-within { opacity: 1; } + &__actions_side-actions, + &__actions_bottom-actions { + background: transparent; + } + &_mobile &__actions_side-actions { opacity: 1; } - &__action_icon { - color: var(--g-color-text-secondary); + &__actions_bottom-actions { + position: absolute; + inset-inline: var(--g-spacing-3) var(--g-spacing-3); + z-index: 1; + + display: flex; + align-items: center; + gap: var(--g-spacing-2); + flex-wrap: wrap; + } + + &_has-left &__top-source, + &_has-left &__bottom-source, + &_has-left &__actions_bottom-actions { + inset-inline-start: calc(var(--g-spacing-3) + #{$notificationLeftOffset}); + } + + &_has-source-top &__actions_bottom-actions { + inset-block-end: var(--g-spacing-3); + } + + &_has-side-actions &__top-source { + inset-inline-end: calc(var(--g-spacing-3) + #{$notificationSideActionsOffset}); + } + + &_has-bottom-actions &__bottom-source { + inset-block-end: calc(var(--g-spacing-2) + var(--g-spacing-10)); + } + + &_has-source-bottom &__actions_bottom-actions { + inset-block-end: var(--g-spacing-3); + } + + &_unread { + background: var(--g-color-base-selection); + } + + &_unread#{&}_active:hover { + background: var(--g-color-base-selection-hover); } &_theme_success { - border-inline-start: 4px solid var(--g-color-line-positive); + border-inline-start: var(--g-spacing-1) solid var(--g-color-line-positive); } &_theme_info { - border-inline-start: 4px solid var(--g-color-line-info); + border-inline-start: var(--g-spacing-1) solid var(--g-color-line-info); } &_theme_warning { - border-inline-start: 4px solid var(--g-color-line-warning); + border-inline-start: var(--g-spacing-1) solid var(--g-color-line-warning); } &_theme_danger { - border-inline-start: 4px solid var(--g-color-line-danger); + border-inline-start: var(--g-spacing-1) solid var(--g-color-line-danger); } - &_active { - cursor: pointer; + &__action_icon { + color: var(--g-color-text-secondary); + } + + &__actions { + display: flex; + align-items: center; + } + + &__source-icon { + width: 36px; + height: 36px; + } + + &__visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } &__swipe-wrap { @@ -169,7 +306,7 @@ $notificationSourceIconSize: 36px; &__swipe-action { display: flex; - gap: 8px; + gap: var(--g-spacing-2); align-items: center; justify-content: center; height: 100%; @@ -207,7 +344,7 @@ $notificationSourceIconSize: 36px; } &__swipe-action-icon { - padding: 8px; + padding: var(--g-spacing-2); border-radius: 100%; color: var(--g-color-base-background); } @@ -215,21 +352,4 @@ $notificationSourceIconSize: 36px; &__swipe-action-text { font-size: 16px; } - - &__source-icon { - width: 36px; - height: 36px; - } - - &__visually-hidden { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; - } } diff --git a/src/components/Notification/Notification.tsx b/src/components/Notification/Notification.tsx index 92241e48..51c7bfa3 100644 --- a/src/components/Notification/Notification.tsx +++ b/src/components/Notification/Notification.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {Flex, Icon, Link, Text, useMobile, useUniqId} from '@gravity-ui/uikit'; +import {Icon, Link, Text, useMobile, useUniqId} from '@gravity-ui/uikit'; import {CnMods, block} from '../utils/cn'; @@ -27,12 +27,7 @@ export const Notification = React.memo(function Notification(props: Props) { sourcePlacement = 'bottom', } = notification; - const modifiers: CnMods = { - unread, - theme, - mobile, - active: Boolean(notification.onClick || notification.href), - }; + const isInteractive = Boolean(notification.onClick || notification.href); const titleId = useUniqId(); const sourceIcon = source && renderSourceIcon(source, titleId); @@ -45,21 +40,27 @@ export const Notification = React.memo(function Notification(props: Props) {
) : null; - const renderedSideActions = props.notification.sideActions ? ( -
{props.notification.sideActions}
+ const hasSideActions = Boolean(notification.sideActions); + const hasBottomActions = Boolean(notification.bottomActions); + const hasLeft = Boolean(sourceIcon); + + const renderedSideActions = notification.sideActions ? ( +
{notification.sideActions}
) : null; - const renderedBottomActions = props.notification.bottomActions ? ( -
- {props.notification.bottomActions} -
+ const renderedBottomActions = notification.bottomActions ? ( +
{notification.bottomActions}
) : null; - const renderedContent =
{content}
; + const renderedContent = ( +
+
{content}
+
+ ); const renderedSourceText = source?.title || formattedDate ? ( - + {source?.title ? renderSourceTitle({ title: source.title, @@ -69,117 +70,91 @@ export const Notification = React.memo(function Notification(props: Props) { : null} {source?.title && formattedDate ? : null} {formattedDate ? : null} - + ) : null; - const hasSourceOnTop = renderedSourceText && sourcePlacement === 'top'; - const hasSourceOnBottom = renderedSourceText && sourcePlacement === 'bottom'; - const topPart = - renderedTitle || hasSourceOnTop - ? renderTitleAndSource(renderedTitle, hasSourceOnTop ? renderedSourceText : null) - : null; + const hasSourceOnTop = Boolean(renderedSourceText && sourcePlacement === 'top'); + const hasSourceOnBottom = Boolean(renderedSourceText && sourcePlacement === 'bottom'); - const notificationMainContent = ( - - {topPart} - {renderedContent} - {hasSourceOnBottom ? ( -
{renderedSourceText}
- ) : null} -
- ); + const layoutModifiers: CnMods = { + unread, + theme, + mobile, + active: isInteractive, + 'has-left': hasLeft, + 'has-side-actions': hasSideActions, + 'has-bottom-actions': hasBottomActions, + 'has-source-top': hasSourceOnTop, + 'has-source-bottom': hasSourceOnBottom, + }; - const notificationInnerContent = ( + const clickableContent = ( - {sourceIcon ?
{sourceIcon}
: null} - - - - {notificationMainContent} - - - {renderedBottomActions} - + {unread ? {t('unread-label')} : null} +
+ {renderedTitle} + {renderedContent} +
); - const hiddenUnreadLabel = unread ? ( - {t('unread-label')} - ) : null; - - const commonProps = { - className: b(modifiers, notification.className), - onMouseEnter: notification.onMouseEnter, - onMouseLeave: notification.onMouseLeave, - }; - - const handleClick = (event: React.MouseEvent) => { - if (event.target instanceof Element && event.target.closest('button')) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - notification.onClick?.(event); - }; + const clickableClassName = b('clickable', { + active: isInteractive, + 'has-left': hasLeft, + 'has-side-actions': hasSideActions, + 'has-bottom-actions': hasBottomActions, + 'has-source-top': hasSourceOnTop, + 'has-source-bottom': hasSourceOnBottom, + }); + + let clickableElement: React.ReactNode; + if (notification.href) { + clickableElement = ( + } + href={notification.href} + target={notification.target ?? '_blank'} + rel="noreferrer" + > + {clickableContent} + + ); + } else if (notification.onClick) { + clickableElement = ( + + ); + } else { + clickableElement =
{clickableContent}
; + } - const interactiveContent = notification.href ? ( - } - href={notification.href} - target={notification.target ?? '_blank'} - rel="noreferrer" - > - {hiddenUnreadLabel} - {notificationInnerContent} - - ) : ( + return (
} - onKeyDown={ - notification.onClick - ? (event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleClick(event as unknown as React.MouseEvent); - } - } - : undefined - } + className={b(layoutModifiers, notification.className)} + onMouseEnter={notification.onMouseEnter} + onMouseLeave={notification.onMouseLeave} > - {hiddenUnreadLabel} - {notificationInnerContent} -
- ); + {clickableElement} + + {sourceIcon ?
{sourceIcon}
: null} + + {hasSourceOnTop ?
{renderedSourceText}
: null} + {hasSourceOnBottom ? ( + {renderedSourceText} + ) : null} - return ( -
- {interactiveContent} {renderedSideActions} + {renderedBottomActions}
); }); -function renderTitleAndSource(title: React.ReactNode, source: React.ReactNode) { - return title && source ? ( - - {source} - {title} - - ) : ( - (title ?? source) - ); -} - interface RenderSourceTitleOptions { title: string; href?: string;