From ffc9ac57ac68544696b5255b5a84697dd8822515 Mon Sep 17 00:00:00 2001 From: Arina Date: Tue, 7 Jul 2026 14:32:59 +0200 Subject: [PATCH 1/5] feat(header-bar): add expandable notification dot indicator --- .../header-bar/src/notification-icon.js | 172 +++++++++++------- 1 file changed, 111 insertions(+), 61 deletions(-) diff --git a/components/header-bar/src/notification-icon.js b/components/header-bar/src/notification-icon.js index 6a2010030..17ba77d07 100755 --- a/components/header-bar/src/notification-icon.js +++ b/components/header-bar/src/notification-icon.js @@ -1,9 +1,11 @@ -import { colors, theme, spacers } from '@dhis2/ui-constants' +import { colors, elevations, spacers } from '@dhis2/ui-constants' import { IconMessages24, IconMail24 } from '@dhis2/ui-icons' import PropTypes from 'prop-types' -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' import i18n from './locales/index.js' +const BADGE_REVEAL_DURATION_MS = 3000 + function icon(kind) { if (kind === 'message') { return @@ -19,67 +21,115 @@ export const NotificationIcon = ({ dataTestId, title, 'aria-label': ariaLabel, -}) => ( - - {icon(kind)} +}) => { + const [badgeExpanded, setBadgeExpanded] = useState(count > 0) + + const hadUnreadAtMountRef = useRef(count > 0) + useEffect(() => { + if (!hadUnreadAtMountRef.current) { + return + } + const timer = setTimeout( + () => setBadgeExpanded(false), + BADGE_REVEAL_DURATION_MS + ) + return () => clearTimeout(timer) + }, []) // Only on mount — simulates the post-login reveal + + return ( + + {icon(kind)} + + {count > 0 && ( + + {count > 99 ? '99+' : count} + + )} - {count > 0 && {count}} + - -) + /* Dot state (default) */ + min-width: 8px; + max-width: 8px; + height: 8px; + border-radius: 4px; + padding: 0; + color: transparent; + box-shadow: 0 0 0 2px ${colors.blue700}; + + transition: max-width 150ms ease-in-out, + min-width 150ms ease-in-out, height 150ms ease-in-out, + border-radius 150ms ease-in-out, + padding 150ms ease-in-out, color 150ms ease-in-out, + box-shadow 150ms ease-in-out, + transform 150ms ease-in-out; + } + + /* Full counter badge */ + span.is-expanded, + a:hover span, + a:focus-visible span { + min-width: 18px; + max-width: 80px; + height: 18px; + border-radius: ${spacers.dp12}; + padding: 0 ${spacers.dp4}; + color: ${colors.white}; + box-shadow: ${elevations.e100}; + } + `} + + ) +} NotificationIcon.propTypes = { 'aria-label': PropTypes.string.isRequired, From 86ed313a01a1a1a6234e62219f6e8d8779047dcf Mon Sep 17 00:00:00 2001 From: Arina Date: Tue, 7 Jul 2026 15:13:34 +0200 Subject: [PATCH 2/5] fix(notification-icon): update box-shadow color to match the header bar background --- components/header-bar/src/notification-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/header-bar/src/notification-icon.js b/components/header-bar/src/notification-icon.js index 17ba77d07..2145cdfc0 100755 --- a/components/header-bar/src/notification-icon.js +++ b/components/header-bar/src/notification-icon.js @@ -104,7 +104,7 @@ export const NotificationIcon = ({ border-radius: 4px; padding: 0; color: transparent; - box-shadow: 0 0 0 2px ${colors.blue700}; + box-shadow: 0 0 0 2px #2c6693; transition: max-width 150ms ease-in-out, min-width 150ms ease-in-out, height 150ms ease-in-out, From 660784a2fdfd0cc8c723a2397931b95d346aeb48 Mon Sep 17 00:00:00 2001 From: Arina Date: Mon, 13 Jul 2026 15:25:56 +0200 Subject: [PATCH 3/5] fix(notification-icon): change background color to red500 for improved contrast --- components/header-bar/src/notification-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/header-bar/src/notification-icon.js b/components/header-bar/src/notification-icon.js index 2145cdfc0..b17c0e3a8 100755 --- a/components/header-bar/src/notification-icon.js +++ b/components/header-bar/src/notification-icon.js @@ -88,7 +88,7 @@ export const NotificationIcon = ({ top: 18px; transform: translate(50%, -50%); inset-inline-end: ${spacers.dp16}; - background-color: ${colors.red300}; + background-color: ${colors.red500}; font-size: 13px; font-weight: 600; line-height: 15px; From 4b9e1b30cf27f0fa881abee3a4004fbc8458e5a7 Mon Sep 17 00:00:00 2001 From: Arina Date: Thu, 23 Jul 2026 09:12:58 +0200 Subject: [PATCH 4/5] refactor(notification-icon): simplify badge reveal logic --- .../header-bar/src/notification-icon.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/components/header-bar/src/notification-icon.js b/components/header-bar/src/notification-icon.js index b17c0e3a8..24edb09ef 100755 --- a/components/header-bar/src/notification-icon.js +++ b/components/header-bar/src/notification-icon.js @@ -1,7 +1,7 @@ import { colors, elevations, spacers } from '@dhis2/ui-constants' import { IconMessages24, IconMail24 } from '@dhis2/ui-icons' import PropTypes from 'prop-types' -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import i18n from './locales/index.js' const BADGE_REVEAL_DURATION_MS = 3000 @@ -24,9 +24,9 @@ export const NotificationIcon = ({ }) => { const [badgeExpanded, setBadgeExpanded] = useState(count > 0) - const hadUnreadAtMountRef = useRef(count > 0) + // Briefly reveal the full counter on mount, then collapse it to a dot useEffect(() => { - if (!hadUnreadAtMountRef.current) { + if (count === 0) { return } const timer = setTimeout( @@ -34,7 +34,9 @@ export const NotificationIcon = ({ BADGE_REVEAL_DURATION_MS ) return () => clearTimeout(timer) - }, []) // Only on mount — simulates the post-login reveal + // count is intentionally omitted: this should only run once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) return ( ) From e73cd57317fd1e9a97a844dfd56f3571f2bcac3d Mon Sep 17 00:00:00 2001 From: Arina Date: Tue, 4 Aug 2026 08:52:38 +0200 Subject: [PATCH 5/5] fix(notification-icon): align badge text color with global shell --- components/header-bar/src/notification-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/header-bar/src/notification-icon.js b/components/header-bar/src/notification-icon.js index 24edb09ef..285a84749 100755 --- a/components/header-bar/src/notification-icon.js +++ b/components/header-bar/src/notification-icon.js @@ -125,7 +125,7 @@ export const NotificationIcon = ({ height: 18px; border-radius: ${spacers.dp12}; padding: 0 ${spacers.dp4}; - color: ${colors.teal700}; + color: ${colors.teal800}; box-shadow: ${elevations.e100}; }