NoticeBubble
TIP
Each example uses its own instance of NoticeBubbleManager, which is why notices from different examples can overlay each other.
Basic notice
import Button from '@semcore/ui/button';
import Link from '@semcore/ui/link';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type BaseNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: BaseNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: (
<>
Link was moved to
{' '}
<Link href='#'>Cats from outer space group</Link>
</>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show basic notice
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: BaseNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
NoticeBubble not in portal
If NoticeBubbleContainer has disablePortal it will add position: sticky to Bubbles.
Parent should have position: relative and overflow with scroll.
import Button from '@semcore/ui/button';
import Link from '@semcore/ui/link';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type NotInPortalNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: NotInPortalNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: (
<>
Link was moved to
{' '}
<Link href='#'>Cats from outer space group</Link>
</>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<div
style={{ border: '1px dashed #eee', height: '180px', position: 'relative', overflow: 'auto' }}
>
<div style={{ height: '800px' }}>
<NoticeBubbleContainer manager={manager} disablePortal={true} />
<Button onClick={handleClick} m={5} ref={openButtonRef}>
Show basic notice
</Button>
</div>
</div>
);
};
export const defaultProps: NotInPortalNoticeBubbleProps = {
initialAnimation: true,
duration: 300000,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Undo action
import Button from '@semcore/ui/button';
import Link from '@semcore/ui/link';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type UndoActionNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: UndoActionNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: (
<>
Link was moved to
{' '}
<Link href='#'>Cats from outer space group</Link>
</>
),
action: <Button theme='invert'>Undo</Button>,
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show notice with undo action
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: UndoActionNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Reload action
import Button from '@semcore/ui/button';
import ReloadM from '@semcore/ui/icon/Reload/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type ReloadActionNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: ReloadActionNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: 'Data for 5 new profiles is ready. Please reload the page to view it.',
action: (
<Button theme='invert' addonLeft={ReloadM}>
Reload the page
</Button>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show notice with reload action
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: ReloadActionNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Completion state
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import CheckM from '@semcore/ui/icon/Check/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type CompletionNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: CompletionNoticeBubbleProps) => {
const handleClick = () => {
manager.add({
children: (
<Flex justifyContent='center' alignItems='center' gap={1}>
<CheckM color='--intergalactic-icon-primary-success' />
Undone
</Flex>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
});
};
return (
<>
<Button onClick={handleClick}>Show notice with completion state</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: CompletionNoticeBubbleProps = {
initialAnimation: true,
duration: 4000,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Success notice
import Button from '@semcore/ui/button';
import CheckM from '@semcore/ui/icon/Check/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type SuccessNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: SuccessNoticeBubbleProps) => {
const handleClick = () => {
manager.add({
icon: <CheckM color='--intergalactic-icon-primary-success' />,
children: 'Keyword was successfully moved to Keyword Analyzer!',
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
});
};
return (
<>
<Button onClick={handleClick}>Show success notice</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: SuccessNoticeBubbleProps = {
initialAnimation: true,
duration: 5000,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Failure notice
import Button from '@semcore/ui/button';
import ReloadM from '@semcore/ui/icon/Reload/m';
import WarningM from '@semcore/ui/icon/Warning/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type FailtureNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: FailtureNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: 'Unfortunately, your recent changes were not saved. Try again later.',
icon: <WarningM color='--intergalactic-icon-primary-warning' />,
action: (
<Button theme='invert' addonLeft={ReloadM}>
Reload the page
</Button>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show failure notice
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: FailtureNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Loading state
Activate the Try again button in the notice to see the loading state.
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import ReloadM from '@semcore/ui/icon/Reload/m';
import WarningM from '@semcore/ui/icon/Warning/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import Spin from '@semcore/ui/spin';
import React from 'react';
type DynamicNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
let notice: any = null;
const Demo = (props: DynamicNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const tryAgain = async () => {
if (!notice) return;
notice.update({
icon: null,
children: (
<Flex justifyContent='center' gap={1}>
<Spin size='xs' theme='invert' />
Loading...
</Flex>
),
action: null,
});
await new Promise((resolve) => setTimeout(resolve, 1500));
notice.update({
children: 'Unfortunately, your recent changes were not saved. Try again later.',
icon: <WarningM color='--intergalactic-icon-primary-warning' />,
action: (
<Button theme='invert' onClick={tryAgain} addonLeft={ReloadM}>
Try again
</Button>
),
});
};
const handleClick = async () => {
if (notice) {
notice.remove();
await new Promise((resolve) => setTimeout(resolve, 500));
}
notice = manager.add({
children: 'Unfortunately, your recent changes were not saved. Try again later.',
icon: <WarningM color='--intergalactic-icon-primary-warning' />,
action: (
<Button theme='invert' onClick={tryAgain} addonLeft={ReloadM}>
Try again
</Button>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show dynamic notice
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: DynamicNoticeBubbleProps = {
initialAnimation: true,
duration: 20000,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Special event notice
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import MailSent from '@semcore/ui/illustration/MailSent';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type SpecialEventsNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: SpecialEventsNoticeBubbleProps) => {
const handleClick = () => {
manager.add({
children: (
<Flex gap={4}>
<MailSent style={{ flexShrink: 0 }} />
Your post is on its way, and we will take great care of it!
</Flex>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
});
};
return (
<>
<Button onClick={handleClick}>Show special event notice</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: SpecialEventsNoticeBubbleProps = {
initialAnimation: true,
duration: 10000,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
No connection
Use type="warning" for this case.
import Button from '@semcore/ui/button';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import Spin from '@semcore/ui/spin';
import React from 'react';
type NoConnectionNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: NoConnectionNoticeBubbleProps) => {
const handleClick = () => {
manager.add({
icon: <Spin size='xs' theme='invert' />,
children: 'Server connection lost. Reconnecting...',
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
});
};
return (
<>
<Button onClick={handleClick}>Show no connection notice</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: NoConnectionNoticeBubbleProps = {
initialAnimation: true,
duration: 10000,
type: 'warning',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
No connection with action
Use type="warning" for this case.
import Button from '@semcore/ui/button';
import ReloadM from '@semcore/ui/icon/Reload/m';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type NoConnectionActionNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const Demo = (props: NoConnectionActionNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: 'Server connection lost. Check your internet connection and reload the page.',
action: (
<Button theme='invert' addonLeft={ReloadM}>
Reload the page
</Button>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show no connection notice with action
</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: NoConnectionActionNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'warning',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Replace previous notice
Press the button several times to replace the previous notice.
WARNING
Use this API only if there's enough time between events, so that all notices have enough time to appear and be read by the user, or if missing some notices isn't critical.
import Button from '@semcore/ui/button';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type ReplaceLastNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
let counter = 0;
const manager = new NoticeBubbleManager();
const Demo = (props: ReplaceLastNoticeBubbleProps) => {
const handleClick = () => {
counter++;
manager.replaceLast({
children: `Link ${counter} was moved to "Cats from outer space"`,
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
});
};
return (
<>
<Button onClick={handleClick}>Show basic notice</Button>
<NoticeBubbleContainer manager={manager} />
</>
);
};
export const defaultProps: ReplaceLastNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;
Use in micro-frontends
When using notifications on a page that contains multiple front-end applications, you should use a common container for notifications so that they do not overlap each other.
import Button from '@semcore/ui/button';
import CheckM from '@semcore/ui/icon/Check/m';
import Link from '@semcore/ui/link';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
type BaseNoticeBubbleProps = { initialAnimation: boolean; duration: number; type: 'info' | 'warning'; focusLock: boolean };
const manager = new NoticeBubbleManager();
const manager2 = new NoticeBubbleManager();
const Demo = (props: BaseNoticeBubbleProps) => {
const openButtonRef = React.useRef<HTMLButtonElement>(null);
const openButtonRef2 = React.useRef<HTMLButtonElement>(null);
const replaceButtonRef = React.useRef<HTMLButtonElement>(null);
const handleClick = () => {
manager.add({
children: (
<>
Link was moved to
{' '}
<Link href='#'>Cats from outer space group</Link>
</>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef.current?.focus();
}, 300);
},
});
};
const handleClickSuccess = () => {
manager2.add({
icon: <CheckM color='--intergalactic-icon-primary-success' />,
children: 'Keyword was successfully moved to Keyword Analyzer!',
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef2.current?.focus();
}, 300);
},
});
};
const handleClickReplace = () => {
manager2.replaceLast({
children: (
<>
This is notice about replace!
</>
),
initialAnimation: props.initialAnimation,
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
onClose: () => {
setTimeout(() => {
openButtonRef2.current?.focus();
}, 300);
},
});
};
// @ts-ignore - we don't have sm2 in our storybook environment
const containerNode = window.sm2.getNoticeBubbleContainer();
return (
<>
<Button onClick={handleClick} ref={openButtonRef}>
Show basic notice
</Button>
<br />
<br />
<Button onClick={handleClickSuccess} ref={openButtonRef2}>
Show success notice
</Button>
{' '}
<Button onClick={handleClickReplace} ref={replaceButtonRef}>
Replace last success
</Button>
<NoticeBubbleContainer manager={manager} containerNode={containerNode} />
<NoticeBubbleContainer manager={manager2} containerNode={containerNode} />
</>
);
};
/** =============== This is a container from sm2, you shouldn't add it by yourself =============== */
if (typeof window !== 'undefined' && window.document) {
const noticeContainer = document.createElement('div');
noticeContainer.id = 'notice-bubble-container';
noticeContainer.style.setProperty('position', 'fixed');
noticeContainer.style.setProperty('right', 'var(--intergalactic-spacing-3x, 12px)');
noticeContainer.style.setProperty('top', 'var(--intergalactic-spacing-3x, 12px)');
noticeContainer.style.setProperty('width', '300px');
noticeContainer.style.setProperty('z-index', '50');
document.body.appendChild(noticeContainer);
// @ts-ignore
window.sm2 = {
getNoticeBubbleContainer: () => {
return document.getElementById('notice-bubble-container');
},
};
}
/** =============== This is a container from sm2, you shouldn't add it by yourself =============== */
export const defaultProps: BaseNoticeBubbleProps = {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
};
Demo.defaultProps = defaultProps;
export default Demo;