Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 119 additions & 61 deletions components/header-bar/src/notification-icon.js
Original file line number Diff line number Diff line change
@@ -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, useState } from 'react'
import i18n from './locales/index.js'

const BADGE_REVEAL_DURATION_MS = 3000

function icon(kind) {
if (kind === 'message') {
return <IconMessages24 color={colors.white} />
Expand All @@ -19,67 +21,123 @@ export const NotificationIcon = ({
dataTestId,
title,
'aria-label': ariaLabel,
}) => (
<a
dir="ltr"
href={href}
className={kind}
data-test={dataTestId}
title={i18n.t(title)}
aria-label={i18n.t(ariaLabel)}
>
{icon(kind)}
}) => {
const [badgeExpanded, setBadgeExpanded] = useState(count > 0)

// Briefly reveal the full counter on mount, then collapse it to a dot
useEffect(() => {
if (count === 0) {
return
}
const timer = setTimeout(
() => setBadgeExpanded(false),
BADGE_REVEAL_DURATION_MS
)
return () => clearTimeout(timer)
// count is intentionally omitted: this should only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (
<a
dir="ltr"
href={href}
className={kind}
data-test={dataTestId}
title={i18n.t(title)}
aria-label={i18n.t(ariaLabel)}
>
{icon(kind)}

{count > 0 && (
<span
className={badgeExpanded ? 'is-expanded' : undefined}
data-test={`${dataTestId}-count`}
>
{count > 99 ? '99+' : count}
</span>
)}

{count > 0 && <span data-test={`${dataTestId}-count`}>{count}</span>}
<style jsx>{`
a {
position: relative;
margin: 0;
cursor: pointer;
padding: 0 ${spacers.dp12};
height: 100%;
display: flex;
align-items: center;
}
a:focus {
outline: 2px solid white;
outline-offset: -2px;
}
a:focus:not(:focus-visible) {
outline: none;
}
a:hover {
background: #1a557f;
}
a:active {
background: #104067;
}
span {
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
position: absolute;
top: 18px;
transform: translate(50%, -50%);
inset-inline-end: ${spacers.dp16};
background-color: ${colors.teal200};
font-size: 13px;
font-weight: 600;
line-height: 15px;
text-align: center;
cursor: inherit;
white-space: nowrap;
overflow: hidden;
border: 1px solid ${colors.white};

<style jsx>{`
a {
position: relative;
margin: 0;
cursor: pointer;
padding: 0 ${spacers.dp12};
height: 100%;
display: flex;
align-items: center;
}
a:focus {
outline: 2px solid white;
outline-offset: -2px;
}
a:focus:not(:focus-visible) {
outline: none;
}
a:hover {
background: #1a557f;
}
a:active {
background: #104067;
}
span {
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
position: absolute;
top: 3px;
inset-inline-end: 2px;
min-width: 18px;
min-height: 18px;
border-radius: ${spacers.dp12};
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
background-color: ${theme.secondary500};
color: ${colors.white};
font-size: 13px;
font-weight: 600;
line-height: 15px;
text-align: center;
cursor: inherit;
padding: 0 ${spacers.dp4};
}
`}</style>
</a>
)
/* Dot state (default) */
min-width: ${spacers.dp8};
max-width: ${spacers.dp8};
height: ${spacers.dp8};
border-radius: ${spacers.dp4};
padding: 0;
color: transparent;
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,
border-radius 150ms ease-in-out,
padding 150ms ease-in-out, color 150ms ease-in-out,
box-shadow 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.teal800};
box-shadow: ${elevations.e100};
}

@media (prefers-reduced-motion: reduce) {
span {
transition: none;
}
}
`}</style>
</a>
)
}

NotificationIcon.propTypes = {
'aria-label': PropTypes.string.isRequired,
Expand Down
Loading