@dhis2-ui/header-bar 10.17.0 → 10.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@
8
8
  "Debug info": "Debug info",
9
9
  "Close": "Close",
10
10
  "Copy debug info": "Copy debug info",
11
+ "{{label}}, {{count}} unread": "{{label}}, {{count}} unread",
12
+ "{{label}}, {{count}} unread_plural": "{{label}}, {{count}} unread",
11
13
  "Interpretations": "Interpretations",
12
14
  "Messages": "Messages",
13
15
  "Online": "Online",
@@ -8,9 +8,11 @@ var _style = _interopRequireDefault(require("styled-jsx/style"));
8
8
  var _uiConstants = require("@dhis2/ui-constants");
9
9
  var _uiIcons = require("@dhis2/ui-icons");
10
10
  var _propTypes = _interopRequireDefault(require("prop-types"));
11
- var _react = _interopRequireDefault(require("react"));
11
+ var _react = _interopRequireWildcard(require("react"));
12
12
  var _index = _interopRequireDefault(require("./locales/index.js"));
13
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ const BADGE_REVEAL_DURATION_MS = 3000;
14
16
  function icon(kind) {
15
17
  if (kind === 'message') {
16
18
  return /*#__PURE__*/_react.default.createElement(_uiIcons.IconMessages24, {
@@ -29,20 +31,40 @@ const NotificationIcon = ({
29
31
  dataTestId,
30
32
  title,
31
33
  'aria-label': ariaLabel
32
- }) => /*#__PURE__*/_react.default.createElement("a", {
33
- dir: "ltr",
34
- href: href,
35
- "data-test": dataTestId,
36
- title: _index.default.t(title),
37
- "aria-label": _index.default.t(ariaLabel),
38
- className: _style.default.dynamic([["995028205", [_uiConstants.spacers.dp12, _uiConstants.spacers.dp12, _uiConstants.theme.secondary500, _uiConstants.colors.white, _uiConstants.spacers.dp4]]]) + " " + (kind || "")
39
- }, icon(kind), count > 0 && /*#__PURE__*/_react.default.createElement("span", {
40
- "data-test": `${dataTestId}-count`,
41
- className: _style.default.dynamic([["995028205", [_uiConstants.spacers.dp12, _uiConstants.spacers.dp12, _uiConstants.theme.secondary500, _uiConstants.colors.white, _uiConstants.spacers.dp4]]])
42
- }, count), /*#__PURE__*/_react.default.createElement(_style.default, {
43
- id: "995028205",
44
- dynamic: [_uiConstants.spacers.dp12, _uiConstants.spacers.dp12, _uiConstants.theme.secondary500, _uiConstants.colors.white, _uiConstants.spacers.dp4]
45
- }, [`a.__jsx-style-dynamic-selector{position:relative;margin:0;cursor:pointer;padding:0 ${_uiConstants.spacers.dp12};height:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}`, "a.__jsx-style-dynamic-selector:focus{outline:2px solid white;outline-offset:-2px;}", "a.__jsx-style-dynamic-selector:focus.__jsx-style-dynamic-selector:not(:focus-visible){outline:none;}", "a.__jsx-style-dynamic-selector:hover{background:#1a557f;}", "a.__jsx-style-dynamic-selector:active{background:#104067;}", `span.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;z-index:1;position:absolute;top:3px;inset-inline-end:2px;min-width:18px;min-height:18px;border-radius:${_uiConstants.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:${_uiConstants.theme.secondary500};color:${_uiConstants.colors.white};font-size:13px;font-weight:600;line-height:15px;text-align:center;cursor:inherit;padding:0 ${_uiConstants.spacers.dp4};}`]));
34
+ }) => {
35
+ const [badgeExpanded, setBadgeExpanded] = (0, _react.useState)(count > 0);
36
+ const label = _index.default.t(ariaLabel);
37
+ const displayCount = count > 99 ? '99+' : count;
38
+ const accessibleLabel = count > 0 ? _index.default.t('{{label}}, {{count}} unread', {
39
+ label,
40
+ count: displayCount
41
+ }) : label;
42
+
43
+ // Briefly reveal the full counter on mount, then collapse it to a dot
44
+ (0, _react.useEffect)(() => {
45
+ if (count === 0) {
46
+ return;
47
+ }
48
+ const timer = setTimeout(() => setBadgeExpanded(false), BADGE_REVEAL_DURATION_MS);
49
+ return () => clearTimeout(timer);
50
+ // count is intentionally omitted: this should only run once on mount
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, []);
53
+ return /*#__PURE__*/_react.default.createElement("a", {
54
+ dir: "ltr",
55
+ href: href,
56
+ "data-test": dataTestId,
57
+ title: _index.default.t(title),
58
+ "aria-label": accessibleLabel,
59
+ className: _style.default.dynamic([["471830497", [_uiConstants.spacers.dp12, _uiConstants.spacers.dp16, _uiConstants.colors.teal200, _uiConstants.colors.white, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp4, _uiConstants.spacers.dp12, _uiConstants.spacers.dp4, _uiConstants.colors.teal800, _uiConstants.elevations.e100]]]) + " " + (kind || "")
60
+ }, icon(kind), count > 0 && /*#__PURE__*/_react.default.createElement("span", {
61
+ "data-test": `${dataTestId}-count`,
62
+ className: _style.default.dynamic([["471830497", [_uiConstants.spacers.dp12, _uiConstants.spacers.dp16, _uiConstants.colors.teal200, _uiConstants.colors.white, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp4, _uiConstants.spacers.dp12, _uiConstants.spacers.dp4, _uiConstants.colors.teal800, _uiConstants.elevations.e100]]]) + " " + ((badgeExpanded ? 'is-expanded' : undefined) || "")
63
+ }, displayCount), /*#__PURE__*/_react.default.createElement(_style.default, {
64
+ id: "471830497",
65
+ dynamic: [_uiConstants.spacers.dp12, _uiConstants.spacers.dp16, _uiConstants.colors.teal200, _uiConstants.colors.white, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp8, _uiConstants.spacers.dp4, _uiConstants.spacers.dp12, _uiConstants.spacers.dp4, _uiConstants.colors.teal800, _uiConstants.elevations.e100]
66
+ }, [`a.__jsx-style-dynamic-selector{position:relative;margin:0;cursor:pointer;padding:0 ${_uiConstants.spacers.dp12};height:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}`, "a.__jsx-style-dynamic-selector:focus{outline:2px solid white;outline-offset:-2px;}", "a.__jsx-style-dynamic-selector:focus.__jsx-style-dynamic-selector:not(:focus-visible){outline:none;}", "a.__jsx-style-dynamic-selector:hover{background:#1a557f;}", "a.__jsx-style-dynamic-selector:active{background:#104067;}", `span.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;z-index:1;position:absolute;top:18px;-webkit-transform:translate(50%,-50%);-ms-transform:translate(50%,-50%);transform:translate(50%,-50%);inset-inline-end:${_uiConstants.spacers.dp16};background-color:${_uiConstants.colors.teal200};font-size:13px;font-weight:600;line-height:15px;text-align:center;cursor:inherit;white-space:nowrap;overflow:hidden;border:1px solid ${_uiConstants.colors.white};min-width:${_uiConstants.spacers.dp8};max-width:${_uiConstants.spacers.dp8};height:${_uiConstants.spacers.dp8};border-radius:${_uiConstants.spacers.dp4};padding:0;color:transparent;box-shadow:0 0 0 2px #2c6693;-webkit-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;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;}`, `span.is-expanded.__jsx-style-dynamic-selector,a.__jsx-style-dynamic-selector:hover span.__jsx-style-dynamic-selector,a.__jsx-style-dynamic-selector:focus-visible span.__jsx-style-dynamic-selector{min-width:18px;max-width:80px;height:18px;border-radius:${_uiConstants.spacers.dp12};padding:0 ${_uiConstants.spacers.dp4};color:${_uiConstants.colors.teal800};box-shadow:${_uiConstants.elevations.e100};}`, "@media (prefers-reduced-motion:reduce){span.__jsx-style-dynamic-selector{-webkit-transition:none;transition:none;}}"]));
67
+ };
46
68
  exports.NotificationIcon = NotificationIcon;
47
69
  NotificationIcon.propTypes = {
48
70
  'aria-label': _propTypes.default.string.isRequired,
@@ -8,6 +8,8 @@
8
8
  "Debug info": "Debug info",
9
9
  "Close": "Close",
10
10
  "Copy debug info": "Copy debug info",
11
+ "{{label}}, {{count}} unread": "{{label}}, {{count}} unread",
12
+ "{{label}}, {{count}} unread_plural": "{{label}}, {{count}} unread",
11
13
  "Interpretations": "Interpretations",
12
14
  "Messages": "Messages",
13
15
  "Online": "Online",
@@ -1,9 +1,10 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
- import { colors, theme, spacers } from '@dhis2/ui-constants';
2
+ import { colors, elevations, spacers } from '@dhis2/ui-constants';
3
3
  import { IconMessages24, IconMail24 } from '@dhis2/ui-icons';
4
4
  import PropTypes from 'prop-types';
5
- import React from 'react';
5
+ import React, { useEffect, useState } from 'react';
6
6
  import i18n from './locales/index.js';
7
+ const BADGE_REVEAL_DURATION_MS = 3000;
7
8
  function icon(kind) {
8
9
  if (kind === 'message') {
9
10
  return /*#__PURE__*/React.createElement(IconMessages24, {
@@ -22,20 +23,40 @@ export const NotificationIcon = ({
22
23
  dataTestId,
23
24
  title,
24
25
  'aria-label': ariaLabel
25
- }) => /*#__PURE__*/React.createElement("a", {
26
- dir: "ltr",
27
- href: href,
28
- "data-test": dataTestId,
29
- title: i18n.t(title),
30
- "aria-label": i18n.t(ariaLabel),
31
- className: _JSXStyle.dynamic([["995028205", [spacers.dp12, spacers.dp12, theme.secondary500, colors.white, spacers.dp4]]]) + " " + (kind || "")
32
- }, icon(kind), count > 0 && /*#__PURE__*/React.createElement("span", {
33
- "data-test": `${dataTestId}-count`,
34
- className: _JSXStyle.dynamic([["995028205", [spacers.dp12, spacers.dp12, theme.secondary500, colors.white, spacers.dp4]]])
35
- }, count), /*#__PURE__*/React.createElement(_JSXStyle, {
36
- id: "995028205",
37
- dynamic: [spacers.dp12, spacers.dp12, theme.secondary500, colors.white, spacers.dp4]
38
- }, [`a.__jsx-style-dynamic-selector{position:relative;margin:0;cursor:pointer;padding:0 ${spacers.dp12};height:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}`, "a.__jsx-style-dynamic-selector:focus{outline:2px solid white;outline-offset:-2px;}", "a.__jsx-style-dynamic-selector:focus.__jsx-style-dynamic-selector:not(:focus-visible){outline:none;}", "a.__jsx-style-dynamic-selector:hover{background:#1a557f;}", "a.__jsx-style-dynamic-selector:active{background:#104067;}", `span.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align: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};}`]));
26
+ }) => {
27
+ const [badgeExpanded, setBadgeExpanded] = useState(count > 0);
28
+ const label = i18n.t(ariaLabel);
29
+ const displayCount = count > 99 ? '99+' : count;
30
+ const accessibleLabel = count > 0 ? i18n.t('{{label}}, {{count}} unread', {
31
+ label,
32
+ count: displayCount
33
+ }) : label;
34
+
35
+ // Briefly reveal the full counter on mount, then collapse it to a dot
36
+ useEffect(() => {
37
+ if (count === 0) {
38
+ return;
39
+ }
40
+ const timer = setTimeout(() => setBadgeExpanded(false), BADGE_REVEAL_DURATION_MS);
41
+ return () => clearTimeout(timer);
42
+ // count is intentionally omitted: this should only run once on mount
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, []);
45
+ return /*#__PURE__*/React.createElement("a", {
46
+ dir: "ltr",
47
+ href: href,
48
+ "data-test": dataTestId,
49
+ title: i18n.t(title),
50
+ "aria-label": accessibleLabel,
51
+ className: _JSXStyle.dynamic([["471830497", [spacers.dp12, spacers.dp16, colors.teal200, colors.white, spacers.dp8, spacers.dp8, spacers.dp8, spacers.dp4, spacers.dp12, spacers.dp4, colors.teal800, elevations.e100]]]) + " " + (kind || "")
52
+ }, icon(kind), count > 0 && /*#__PURE__*/React.createElement("span", {
53
+ "data-test": `${dataTestId}-count`,
54
+ className: _JSXStyle.dynamic([["471830497", [spacers.dp12, spacers.dp16, colors.teal200, colors.white, spacers.dp8, spacers.dp8, spacers.dp8, spacers.dp4, spacers.dp12, spacers.dp4, colors.teal800, elevations.e100]]]) + " " + ((badgeExpanded ? 'is-expanded' : undefined) || "")
55
+ }, displayCount), /*#__PURE__*/React.createElement(_JSXStyle, {
56
+ id: "471830497",
57
+ dynamic: [spacers.dp12, spacers.dp16, colors.teal200, colors.white, spacers.dp8, spacers.dp8, spacers.dp8, spacers.dp4, spacers.dp12, spacers.dp4, colors.teal800, elevations.e100]
58
+ }, [`a.__jsx-style-dynamic-selector{position:relative;margin:0;cursor:pointer;padding:0 ${spacers.dp12};height:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}`, "a.__jsx-style-dynamic-selector:focus{outline:2px solid white;outline-offset:-2px;}", "a.__jsx-style-dynamic-selector:focus.__jsx-style-dynamic-selector:not(:focus-visible){outline:none;}", "a.__jsx-style-dynamic-selector:hover{background:#1a557f;}", "a.__jsx-style-dynamic-selector:active{background:#104067;}", `span.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;z-index:1;position:absolute;top:18px;-webkit-transform:translate(50%,-50%);-ms-transform:translate(50%,-50%);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};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;-webkit-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;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;}`, `span.is-expanded.__jsx-style-dynamic-selector,a.__jsx-style-dynamic-selector:hover span.__jsx-style-dynamic-selector,a.__jsx-style-dynamic-selector:focus-visible span.__jsx-style-dynamic-selector{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.__jsx-style-dynamic-selector{-webkit-transition:none;transition:none;}}"]));
59
+ };
39
60
  NotificationIcon.propTypes = {
40
61
  'aria-label': PropTypes.string.isRequired,
41
62
  href: PropTypes.string.isRequired,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/header-bar",
3
- "version": "10.17.0",
3
+ "version": "10.19.0",
4
4
  "description": "UI HeaderBar",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,21 +34,21 @@
34
34
  "styled-jsx": "^4"
35
35
  },
36
36
  "dependencies": {
37
- "@dhis2-ui/box": "10.17.0",
38
- "@dhis2-ui/button": "10.17.0",
39
- "@dhis2-ui/card": "10.17.0",
40
- "@dhis2-ui/center": "10.17.0",
41
- "@dhis2-ui/divider": "10.17.0",
42
- "@dhis2-ui/input": "10.17.0",
43
- "@dhis2-ui/layer": "10.17.0",
44
- "@dhis2-ui/loader": "10.17.0",
45
- "@dhis2-ui/logo": "10.17.0",
46
- "@dhis2-ui/menu": "10.17.0",
47
- "@dhis2-ui/modal": "10.17.0",
48
- "@dhis2-ui/user-avatar": "10.17.0",
37
+ "@dhis2-ui/box": "10.19.0",
38
+ "@dhis2-ui/button": "10.19.0",
39
+ "@dhis2-ui/card": "10.19.0",
40
+ "@dhis2-ui/center": "10.19.0",
41
+ "@dhis2-ui/divider": "10.19.0",
42
+ "@dhis2-ui/input": "10.19.0",
43
+ "@dhis2-ui/layer": "10.19.0",
44
+ "@dhis2-ui/loader": "10.19.0",
45
+ "@dhis2-ui/logo": "10.19.0",
46
+ "@dhis2-ui/menu": "10.19.0",
47
+ "@dhis2-ui/modal": "10.19.0",
48
+ "@dhis2-ui/user-avatar": "10.19.0",
49
49
  "@dhis2/prop-types": "^3.1.2",
50
- "@dhis2/ui-constants": "10.17.0",
51
- "@dhis2/ui-icons": "10.17.0",
50
+ "@dhis2/ui-constants": "10.19.0",
51
+ "@dhis2/ui-icons": "10.19.0",
52
52
  "classnames": "^2.3.1",
53
53
  "moment": "^2.29.1",
54
54
  "prop-types": "^15.7.2"
@@ -33,10 +33,10 @@ export const DebugInfoMenuItem = ({ hideProfileMenu, showDebugInfoModal }) => {
33
33
  appName: debugInfo.app_name,
34
34
  })
35
35
  : debugInfo.app_version
36
- ? i18n.t('App {{appVersion}}', {
37
- appVersion: debugInfo.app_version,
38
- })
39
- : i18n.t('App version unknown')}
36
+ ? i18n.t('App {{appVersion}}', {
37
+ appVersion: debugInfo.app_version,
38
+ })
39
+ : i18n.t('App version unknown')}
40
40
  {}
41
41
  </div>
42
42
  <style jsx>{`
@@ -8,6 +8,8 @@
8
8
  "Debug info": "Debug info",
9
9
  "Close": "Close",
10
10
  "Copy debug info": "Copy debug info",
11
+ "{{label}}, {{count}} unread": "{{label}}, {{count}} unread",
12
+ "{{label}}, {{count}} unread_plural": "{{label}}, {{count}} unread",
11
13
  "Interpretations": "Interpretations",
12
14
  "Messages": "Messages",
13
15
  "Online": "Online",
@@ -1,9 +1,11 @@
1
- import { colors, theme, spacers } from '@dhis2/ui-constants'
1
+ import { colors, elevations, spacers } from '@dhis2/ui-constants'
2
2
  import { IconMessages24, IconMail24 } from '@dhis2/ui-icons'
3
3
  import PropTypes from 'prop-types'
4
- import React from 'react'
4
+ import React, { useEffect, useState } from 'react'
5
5
  import i18n from './locales/index.js'
6
6
 
7
+ const BADGE_REVEAL_DURATION_MS = 3000
8
+
7
9
  function icon(kind) {
8
10
  if (kind === 'message') {
9
11
  return <IconMessages24 color={colors.white} />
@@ -19,67 +21,135 @@ export const NotificationIcon = ({
19
21
  dataTestId,
20
22
  title,
21
23
  'aria-label': ariaLabel,
22
- }) => (
23
- <a
24
- dir="ltr"
25
- href={href}
26
- className={kind}
27
- data-test={dataTestId}
28
- title={i18n.t(title)}
29
- aria-label={i18n.t(ariaLabel)}
30
- >
31
- {icon(kind)}
24
+ }) => {
25
+ const [badgeExpanded, setBadgeExpanded] = useState(count > 0)
26
+ const label = i18n.t(ariaLabel)
27
+ const displayCount = count > 99 ? '99+' : count
28
+ const accessibleLabel =
29
+ count > 0
30
+ ? i18n.t('{{label}}, {{count}} unread', {
31
+ label,
32
+ count: displayCount,
33
+ })
34
+ : label
35
+
36
+ // Briefly reveal the full counter on mount, then collapse it to a dot
37
+ useEffect(() => {
38
+ if (count === 0) {
39
+ return
40
+ }
41
+ const timer = setTimeout(
42
+ () => setBadgeExpanded(false),
43
+ BADGE_REVEAL_DURATION_MS
44
+ )
45
+ return () => clearTimeout(timer)
46
+ // count is intentionally omitted: this should only run once on mount
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [])
49
+
50
+ return (
51
+ <a
52
+ dir="ltr"
53
+ href={href}
54
+ className={kind}
55
+ data-test={dataTestId}
56
+ title={i18n.t(title)}
57
+ aria-label={accessibleLabel}
58
+ >
59
+ {icon(kind)}
60
+
61
+ {count > 0 && (
62
+ <span
63
+ className={badgeExpanded ? 'is-expanded' : undefined}
64
+ data-test={`${dataTestId}-count`}
65
+ >
66
+ {displayCount}
67
+ </span>
68
+ )}
32
69
 
33
- {count > 0 && <span data-test={`${dataTestId}-count`}>{count}</span>}
70
+ <style jsx>{`
71
+ a {
72
+ position: relative;
73
+ margin: 0;
74
+ cursor: pointer;
75
+ padding: 0 ${spacers.dp12};
76
+ height: 100%;
77
+ display: flex;
78
+ align-items: center;
79
+ }
80
+ a:focus {
81
+ outline: 2px solid white;
82
+ outline-offset: -2px;
83
+ }
84
+ a:focus:not(:focus-visible) {
85
+ outline: none;
86
+ }
87
+ a:hover {
88
+ background: #1a557f;
89
+ }
90
+ a:active {
91
+ background: #104067;
92
+ }
93
+ span {
94
+ display: flex;
95
+ justify-content: center;
96
+ align-items: center;
97
+ z-index: 1;
98
+ position: absolute;
99
+ top: 18px;
100
+ transform: translate(50%, -50%);
101
+ inset-inline-end: ${spacers.dp16};
102
+ background-color: ${colors.teal200};
103
+ font-size: 13px;
104
+ font-weight: 600;
105
+ line-height: 15px;
106
+ text-align: center;
107
+ cursor: inherit;
108
+ white-space: nowrap;
109
+ overflow: hidden;
110
+ border: 1px solid ${colors.white};
34
111
 
35
- <style jsx>{`
36
- a {
37
- position: relative;
38
- margin: 0;
39
- cursor: pointer;
40
- padding: 0 ${spacers.dp12};
41
- height: 100%;
42
- display: flex;
43
- align-items: center;
44
- }
45
- a:focus {
46
- outline: 2px solid white;
47
- outline-offset: -2px;
48
- }
49
- a:focus:not(:focus-visible) {
50
- outline: none;
51
- }
52
- a:hover {
53
- background: #1a557f;
54
- }
55
- a:active {
56
- background: #104067;
57
- }
58
- span {
59
- display: flex;
60
- justify-content: center;
61
- align-items: center;
62
- z-index: 1;
63
- position: absolute;
64
- top: 3px;
65
- inset-inline-end: 2px;
66
- min-width: 18px;
67
- min-height: 18px;
68
- border-radius: ${spacers.dp12};
69
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
70
- 0 1px 2px 0 rgba(0, 0, 0, 0.06);
71
- background-color: ${theme.secondary500};
72
- color: ${colors.white};
73
- font-size: 13px;
74
- font-weight: 600;
75
- line-height: 15px;
76
- text-align: center;
77
- cursor: inherit;
78
- padding: 0 ${spacers.dp4};
79
- }
80
- `}</style>
81
- </a>
82
- )
112
+ /* Dot state (default) */
113
+ min-width: ${spacers.dp8};
114
+ max-width: ${spacers.dp8};
115
+ height: ${spacers.dp8};
116
+ border-radius: ${spacers.dp4};
117
+ padding: 0;
118
+ color: transparent;
119
+ box-shadow: 0 0 0 2px #2c6693;
120
+
121
+ transition:
122
+ max-width 150ms ease-in-out,
123
+ min-width 150ms ease-in-out,
124
+ height 150ms ease-in-out,
125
+ border-radius 150ms ease-in-out,
126
+ padding 150ms ease-in-out,
127
+ color 150ms ease-in-out,
128
+ box-shadow 150ms ease-in-out;
129
+ }
130
+
131
+ /* Full counter badge */
132
+ span.is-expanded,
133
+ a:hover span,
134
+ a:focus-visible span {
135
+ min-width: 18px;
136
+ max-width: 80px;
137
+ height: 18px;
138
+ border-radius: ${spacers.dp12};
139
+ padding: 0 ${spacers.dp4};
140
+ color: ${colors.teal800};
141
+ box-shadow: ${elevations.e100};
142
+ }
143
+
144
+ @media (prefers-reduced-motion: reduce) {
145
+ span {
146
+ transition: none;
147
+ }
148
+ }
149
+ `}</style>
150
+ </a>
151
+ )
152
+ }
83
153
 
84
154
  NotificationIcon.propTypes = {
85
155
  'aria-label': PropTypes.string.isRequired,