Toast improvements
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
8923502d02
commit
cae33447ed
@@ -26,7 +26,7 @@ export function StatusVariants() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -99,7 +99,7 @@ export function WithoutDescription() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -125,71 +125,11 @@ export function WithoutDescription() {
|
||||
);
|
||||
}
|
||||
|
||||
export function Positions() {
|
||||
const [currentPosition, setCurrentPosition] = useState<
|
||||
'top' | 'bottom' | null
|
||||
>(null);
|
||||
const [currentPlacement, setCurrentPlacement] = useState<
|
||||
'start' | 'center' | 'end' | null
|
||||
>(null);
|
||||
|
||||
const showToast = (
|
||||
position: 'top' | 'bottom',
|
||||
placement: 'start' | 'center' | 'end',
|
||||
) => {
|
||||
setCurrentPosition(position);
|
||||
setCurrentPlacement(placement);
|
||||
toastQueue.add({
|
||||
title: `${position} - ${placement}`,
|
||||
description: `Toast positioned at ${position} ${placement}`,
|
||||
status: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPosition && currentPlacement && (
|
||||
<ToastRegion
|
||||
queue={toastQueue}
|
||||
position={currentPosition}
|
||||
placement={currentPlacement}
|
||||
/>
|
||||
)}
|
||||
<Flex direction="column" gap="4">
|
||||
<div>
|
||||
<strong>Top Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('top', 'start')}>Top Start</Button>
|
||||
<Button onPress={() => showToast('top', 'center')}>
|
||||
Top Center
|
||||
</Button>
|
||||
<Button onPress={() => showToast('top', 'end')}>Top End</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bottom Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('bottom', 'start')}>
|
||||
Bottom Start
|
||||
</Button>
|
||||
<Button onPress={() => showToast('bottom', 'center')}>
|
||||
Bottom Center
|
||||
</Button>
|
||||
<Button onPress={() => showToast('bottom', 'end')}>
|
||||
Bottom End
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoDismiss() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add(
|
||||
@@ -258,7 +198,7 @@ export function QueueManagement() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
toastQueue.add({
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
statusVariantsSnippet,
|
||||
withDescriptionSnippet,
|
||||
withoutDescriptionSnippet,
|
||||
positionsSnippet,
|
||||
autoDismissSnippet,
|
||||
programmaticControlSnippet,
|
||||
queueManagementSnippet,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
StatusVariants,
|
||||
WithDescription,
|
||||
WithoutDescription,
|
||||
Positions,
|
||||
AutoDismiss,
|
||||
ProgrammaticControl,
|
||||
QueueManagement,
|
||||
@@ -30,7 +28,7 @@ import { ToastDefinition, ToastRegionDefinition } from '../../../utils/definitio
|
||||
|
||||
<PageTitle
|
||||
title="Toast"
|
||||
description="A Toast displays a brief, temporary notification of actions, errors, or other events in an application."
|
||||
description="A Toast displays a brief, temporary notification of actions, errors, or other events in the bottom-right corner of the application."
|
||||
/>
|
||||
|
||||
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
|
||||
@@ -95,18 +93,6 @@ Toasts can display a title only for simpler notifications.
|
||||
code={withoutDescriptionSnippet}
|
||||
/>
|
||||
|
||||
### Positions and Placements
|
||||
|
||||
Control where toasts appear on the screen with `position` (top/bottom) and `placement` (start/center/end).
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<Positions />}
|
||||
code={positionsSnippet}
|
||||
/>
|
||||
|
||||
### Auto Dismiss
|
||||
|
||||
Toasts can automatically dismiss after a timeout. For accessibility, a minimum of 5 seconds is recommended. Timers automatically pause when users hover or focus on a toast.
|
||||
|
||||
@@ -35,17 +35,5 @@ export const toastRegionPropDefs: Record<string, PropDef> = {
|
||||
values: ['ToastQueue<ToastContent>'],
|
||||
responsive: false,
|
||||
},
|
||||
position: {
|
||||
type: 'enum',
|
||||
values: ['top', 'bottom'],
|
||||
responsive: true,
|
||||
default: 'bottom',
|
||||
},
|
||||
placement: {
|
||||
type: 'enum',
|
||||
values: ['start', 'center', 'end'],
|
||||
responsive: true,
|
||||
default: 'end',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -116,7 +116,7 @@ export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
@@ -142,64 +142,13 @@ export function Example() {
|
||||
);
|
||||
}`;
|
||||
|
||||
export const positionsSnippet = `import { useState } from 'react';
|
||||
import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
const [currentPosition, setCurrentPosition] = useState<'top' | 'bottom' | null>(null);
|
||||
const [currentPlacement, setCurrentPlacement] = useState<'start' | 'center' | 'end' | null>(null);
|
||||
|
||||
const showToast = (
|
||||
position: 'top' | 'bottom',
|
||||
placement: 'start' | 'center' | 'end'
|
||||
) => {
|
||||
setCurrentPosition(position);
|
||||
setCurrentPlacement(placement);
|
||||
toastQueue.add({
|
||||
title: \`\${position} - \${placement}\`,
|
||||
description: \`Toast positioned at \${position} \${placement}\`,
|
||||
status: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPosition && currentPlacement && (
|
||||
<ToastRegion
|
||||
queue={toastQueue}
|
||||
position={currentPosition}
|
||||
placement={currentPlacement}
|
||||
/>
|
||||
)}
|
||||
<Flex direction="column" gap="4">
|
||||
<div>
|
||||
<strong>Top Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('top', 'start')}>Top Start</Button>
|
||||
<Button onPress={() => showToast('top', 'center')}>Top Center</Button>
|
||||
<Button onPress={() => showToast('top', 'end')}>Top End</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bottom Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('bottom', 'start')}>Bottom Start</Button>
|
||||
<Button onPress={() => showToast('bottom', 'center')}>Bottom Center</Button>
|
||||
<Button onPress={() => showToast('bottom', 'end')}>Bottom End</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const autoDismissSnippet = `import { ToastRegion, toastQueue, Button, Flex } from '@backstage/ui';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add(
|
||||
@@ -273,7 +222,7 @@ export function Example() {
|
||||
return (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} />
|
||||
<Flex gap="3" wrap="wrap">
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() => {
|
||||
toastQueue.add({
|
||||
|
||||
@@ -17,22 +17,23 @@
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
/* Toast Region - Container for all toasts */
|
||||
/* Toast Region - Container for all toasts (bottom-right only) */
|
||||
.bui-ToastRegion {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 100050;
|
||||
width: 300px;
|
||||
/* inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
z-index: 100050;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
outline: none; */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Position: Bottom */
|
||||
/* bottom: 0;
|
||||
flex-direction: column-reverse; */
|
||||
@media (min-width: 500px) {
|
||||
.bui-ToastRegion {
|
||||
width: 350px;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Individual Toast */
|
||||
@@ -40,17 +41,18 @@
|
||||
--toast-bg: var(--bui-bg-surface-1);
|
||||
--toast-fg: var(--bui-fg-primary);
|
||||
--toast-gap: 0.75rem;
|
||||
--toast-peek: 1.25rem;
|
||||
--toast-peek: 0.75rem;
|
||||
--toast-scale: calc(max(0, 1 - (var(--toast-index) * 0.05)));
|
||||
--toast-shrink: calc(1 - var(--toast-scale));
|
||||
|
||||
position: relative;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--bui-space-3);
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
margin: var(--bui-space-2);
|
||||
width: 100%;
|
||||
padding: var(--bui-space-3);
|
||||
border-radius: var(--bui-radius-3);
|
||||
background-color: var(--toast-bg);
|
||||
@@ -60,10 +62,12 @@
|
||||
line-height: 1.5;
|
||||
box-shadow: var(--bui-shadow);
|
||||
outline: none;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
z-index: calc(1000 - var(--toast-index));
|
||||
pointer-events: auto;
|
||||
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s;
|
||||
opacity: calc(1 - (var(--toast-index) * 0.15));
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 0.5s;
|
||||
|
||||
/* Focus ring */
|
||||
&:focus-visible {
|
||||
@@ -72,50 +76,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Bottom position stacking */
|
||||
.bui-ToastRegion .bui-Toast:not([data-entering]) {
|
||||
transform: translateY(
|
||||
calc(
|
||||
(var(--toast-index) * var(--toast-peek) * -1) -
|
||||
(var(--toast-shrink) * 100%)
|
||||
)
|
||||
)
|
||||
scale(var(--toast-scale));
|
||||
/* Stacking from bottom */
|
||||
.bui-Toast {
|
||||
bottom: 0;
|
||||
transform-origin: bottom center;
|
||||
transform: translateY(calc(var(--toast-index) * var(--toast-peek) * -1))
|
||||
scale(var(--toast-scale));
|
||||
opacity: calc(1 - (var(--toast-index) * 0.15));
|
||||
}
|
||||
|
||||
/* Only first toast is interactive */
|
||||
.bui-ToastRegion li:not(:first-child) .bui-Toast {
|
||||
.bui-Toast[style*='--toast-index: 0'] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bui-Toast:not([style*='--toast-index: 0']) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* List styling for toast container */
|
||||
/* .bui-ToastRegion ol {
|
||||
display: inherit;
|
||||
flex-direction: inherit;
|
||||
align-items: inherit;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
} */
|
||||
|
||||
.bui-ToastRegion li {
|
||||
display: flex !important;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Position list items based on toast region position */
|
||||
.bui-ToastRegion[data-position='bottom'] li {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.bui-ToastRegion[data-position='top'] li {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Status variants */
|
||||
.bui-Toast[data-status='info'] {
|
||||
--toast-bg: var(--bui-bg-info);
|
||||
@@ -215,52 +193,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes bui-toast-slide-in-bottom {
|
||||
from {
|
||||
transform: translateY(calc(100% + 2rem)) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
/* Entering and exiting states */
|
||||
.bui-Toast[data-entering] {
|
||||
transform: translateY(calc(100% + 2rem)) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes bui-toast-slide-in-top {
|
||||
from {
|
||||
transform: translateY(calc(-100% - 2rem)) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bui-toast-slide-out-bottom {
|
||||
to {
|
||||
transform: translateY(calc(100% + 2rem));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bui-toast-slide-out-top {
|
||||
to {
|
||||
transform: translateY(calc(-100% - 2rem));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply animations based on position */
|
||||
.bui-ToastRegion[data-position='bottom'] .bui-Toast[data-entering] {
|
||||
animation: bui-toast-slide-in-bottom 0.4s cubic-bezier(0.22, 1, 0.36, 1)
|
||||
forwards;
|
||||
}
|
||||
|
||||
.bui-ToastRegion[data-position='bottom'] .bui-Toast[data-exiting] {
|
||||
animation: bui-toast-slide-out-bottom 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
.bui-ToastRegion[data-position='top'] .bui-Toast[data-entering] {
|
||||
animation: bui-toast-slide-in-top 0.4s cubic-bezier(0.22, 1, 0.36, 1)
|
||||
forwards;
|
||||
}
|
||||
|
||||
.bui-ToastRegion[data-position='top'] .bui-Toast[data-exiting] {
|
||||
animation: bui-toast-slide-out-top 0.3s ease-in forwards;
|
||||
.bui-Toast[data-exiting] {
|
||||
transform: translateY(calc(100% + 2rem)) scale(var(--toast-scale));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@@ -268,10 +209,5 @@
|
||||
.bui-Toast {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.bui-Toast[data-entering],
|
||||
.bui-Toast[data-exiting] {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,6 @@ import { Button } from '../Button';
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Toast',
|
||||
component: ToastRegion,
|
||||
argTypes: {
|
||||
position: {
|
||||
control: 'select',
|
||||
options: ['top', 'bottom'],
|
||||
},
|
||||
placement: {
|
||||
control: 'select',
|
||||
options: ['start', 'center', 'end'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
@@ -168,89 +158,6 @@ export const WithoutIcons = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const Positions = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
<ToastRegion queue={toastQueue} position="top" placement="center" />
|
||||
<Flex gap="3">
|
||||
<Button
|
||||
onPress={() =>
|
||||
toastQueue.add({
|
||||
title: 'Toast from top center',
|
||||
description: 'This appears at the top center.',
|
||||
status: 'info',
|
||||
})
|
||||
}
|
||||
>
|
||||
Top Center
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const AllPositions = meta.story({
|
||||
render: () => {
|
||||
const [currentPosition, setCurrentPosition] = useState<'top' | 'bottom'>(
|
||||
'bottom',
|
||||
);
|
||||
const [currentPlacement, setCurrentPlacement] = useState<
|
||||
'start' | 'center' | 'end'
|
||||
>('end');
|
||||
|
||||
const showToast = (
|
||||
position: 'top' | 'bottom',
|
||||
placement: 'start' | 'center' | 'end',
|
||||
) => {
|
||||
setCurrentPosition(position);
|
||||
setCurrentPlacement(placement);
|
||||
toastQueue.add({
|
||||
title: `${position} - ${placement}`,
|
||||
description: `Toast positioned at ${position} ${placement}`,
|
||||
status: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastRegion
|
||||
queue={toastQueue}
|
||||
position={currentPosition}
|
||||
placement={currentPlacement}
|
||||
/>
|
||||
<Flex direction="column" gap="4">
|
||||
<div>
|
||||
<strong>Top Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('top', 'start')}>
|
||||
Top Start
|
||||
</Button>
|
||||
<Button onPress={() => showToast('top', 'center')}>
|
||||
Top Center
|
||||
</Button>
|
||||
<Button onPress={() => showToast('top', 'end')}>Top End</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bottom Positions:</strong>
|
||||
<Flex gap="2" style={{ marginTop: '8px' }}>
|
||||
<Button onPress={() => showToast('bottom', 'start')}>
|
||||
Bottom Start
|
||||
</Button>
|
||||
<Button onPress={() => showToast('bottom', 'center')}>
|
||||
Bottom Center
|
||||
</Button>
|
||||
<Button onPress={() => showToast('bottom', 'end')}>
|
||||
Bottom End
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const AutoDismiss = meta.story({
|
||||
render: () => (
|
||||
<>
|
||||
|
||||
@@ -22,11 +22,12 @@ import { ToastRegionDefinition } from './definition';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
/**
|
||||
* A ToastRegion displays one or more toast notifications.
|
||||
* A ToastRegion displays one or more toast notifications in the bottom-right corner.
|
||||
*
|
||||
* @remarks
|
||||
* The ToastRegion component should typically be placed once at the root of your application.
|
||||
* It manages the display and positioning of all toast notifications added to its queue.
|
||||
* It manages the display and stacking of all toast notifications added to its queue.
|
||||
* Toasts appear in the bottom-right corner with deep stacking when multiple are visible.
|
||||
* Toast regions are ARIA landmark regions that can be navigated using F6 (forward) and
|
||||
* Shift+F6 (backward) for keyboard accessibility.
|
||||
*
|
||||
@@ -35,28 +36,18 @@ import { Toast } from './Toast';
|
||||
* @example
|
||||
* Basic setup in app root:
|
||||
* ```tsx
|
||||
* import { ToastRegion, queue } from '@backstage/ui';
|
||||
* import { ToastRegion, toastQueue } from '@backstage/ui';
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <>
|
||||
* <ToastRegion queue={queue} />
|
||||
* <ToastRegion queue={toastQueue} />
|
||||
* <YourAppContent />
|
||||
* </>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Custom positioning:
|
||||
* ```tsx
|
||||
* <ToastRegion
|
||||
* queue={queue}
|
||||
* position="top"
|
||||
* placement="center"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ToastRegion = forwardRef(
|
||||
|
||||
@@ -52,8 +52,6 @@ export const ToastRegionDefinition = defineComponent<ToastRegionOwnProps>()({
|
||||
},
|
||||
propDefs: {
|
||||
queue: {},
|
||||
position: { dataAttribute: true, default: 'bottom' },
|
||||
placement: { dataAttribute: true, default: 'end' },
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,10 +64,6 @@ export interface ToastProps extends ToastOwnProps {}
|
||||
export type ToastRegionOwnProps = {
|
||||
/** Toast queue instance */
|
||||
queue: RAToastQueue<ToastContent>;
|
||||
/** Position of the toast region (top or bottom) */
|
||||
position?: Responsive<'top' | 'bottom'>;
|
||||
/** Horizontal placement of toasts */
|
||||
placement?: Responsive<'start' | 'center' | 'end'>;
|
||||
/** Custom class name */
|
||||
className?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user