Skip to content

Commit 11763b2

Browse files
authored
feat(v5): Add ToastContainer (react-bootstrap#5566)
* feat(v5): Add ToastContainer * Clean up * Cleanup
1 parent 15a5e34 commit 11763b2

File tree

7 files changed

+193
-42
lines changed

7 files changed

+193
-42
lines changed

src/ToastContainer.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import classNames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import * as React from 'react';
4+
import { useBootstrapPrefix } from './ThemeProvider';
5+
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
6+
7+
export type ToastPosition =
8+
| 'top-start'
9+
| 'top-center'
10+
| 'top-end'
11+
| 'middle-start'
12+
| 'middle-center'
13+
| 'middle-end'
14+
| 'bottom-start'
15+
| 'bottom-center'
16+
| 'bottom-end';
17+
18+
export interface ToastContainerProps
19+
extends BsPrefixProps,
20+
React.HTMLAttributes<HTMLElement> {
21+
position?: ToastPosition;
22+
}
23+
24+
const propTypes = {
25+
/**
26+
* @default 'toast-container'
27+
*/
28+
bsPrefix: PropTypes.string,
29+
30+
/**
31+
* Where the toasts will be placed within the container.
32+
*/
33+
position: PropTypes.oneOf<ToastPosition>([
34+
'top-start',
35+
'top-center',
36+
'top-end',
37+
'middle-start',
38+
'middle-center',
39+
'middle-end',
40+
'bottom-start',
41+
'bottom-center',
42+
'bottom-end',
43+
]),
44+
};
45+
46+
const positionClasses = {
47+
'top-start': 'top-0 start-0',
48+
'top-center': 'top-0 start-50 translate-middle-x',
49+
'top-end': 'top-0 end-0',
50+
'middle-start': 'top-50 start-0 translate-middle-y',
51+
'middle-center': 'top-50 start-50 translate-middle',
52+
'middle-end': 'top-50 end-0 translate-middle-y',
53+
'bottom-start': 'bottom-0 start-0',
54+
'bottom-center': 'bottom-0 start-50 translate-middle-x',
55+
'bottom-end': 'bottom-0 end-0',
56+
};
57+
58+
const ToastContainer: BsPrefixRefForwardingComponent<
59+
'div',
60+
ToastContainerProps
61+
> = React.forwardRef<HTMLDivElement, ToastContainerProps>(
62+
(
63+
{
64+
bsPrefix,
65+
position,
66+
className,
67+
// Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
68+
as: Component = 'div',
69+
...props
70+
},
71+
ref,
72+
) => {
73+
bsPrefix = useBootstrapPrefix(bsPrefix, 'toast-container');
74+
75+
return (
76+
<Component
77+
ref={ref}
78+
{...props}
79+
className={classNames(
80+
bsPrefix,
81+
position && `position-absolute ${positionClasses[position]}`,
82+
className,
83+
)}
84+
/>
85+
);
86+
},
87+
);
88+
89+
ToastContainer.displayName = 'ToastContainer';
90+
ToastContainer.propTypes = propTypes;
91+
92+
export default ToastContainer;

src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ export { default as ToastBody } from './ToastBody';
189189
export { default as ToastHeader } from './ToastHeader';
190190
export type { ToastHeaderProps } from './ToastHeader';
191191

192+
export { default as ToastContainer } from './ToastContainer';
193+
export type { ToastContainerProps } from './ToastContainer';
194+
192195
export { default as ToggleButton } from './ToggleButton';
193196
export type { ToggleButtonProps } from './ToggleButton';
194197

test/ToastContainerSpec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { mount } from 'enzyme';
2+
3+
import ToastContainer from '../src/ToastContainer';
4+
5+
const expectedClasses = {
6+
'top-start': '.position-absolute.top-0.start-0',
7+
'top-center': '.position-absolute.top-0.start-50.translate-middle-x',
8+
'top-end': '.position-absolute.top-0.end-0',
9+
'middle-start': '.position-absolute.top-50.start-0.translate-middle-y',
10+
'middle-center': '.position-absolute.top-50.start-50.translate-middle',
11+
'middle-end': '.position-absolute.top-50.end-0.translate-middle-y',
12+
'bottom-start': '.position-absolute.bottom-0.start-0',
13+
'bottom-center': '.position-absolute.bottom-0.start-50.translate-middle-x',
14+
'bottom-end': '.position-absolute.bottom-0.end-0',
15+
};
16+
17+
describe('ToastContainer', () => {
18+
it('should render a basic toast container', () => {
19+
mount(<ToastContainer />).assertSingle('.toast-container');
20+
});
21+
22+
Object.keys(expectedClasses).forEach((position) => {
23+
it(`should render position=${position}`, () => {
24+
mount(<ToastContainer position={position} />).assertSingle(
25+
expectedClasses[position],
26+
);
27+
});
28+
});
29+
});

www/src/examples/Toast/Placement.js

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,54 @@
1-
<div
2-
aria-live="polite"
3-
aria-atomic="true"
4-
style={{
5-
position: 'relative',
6-
minHeight: '100px',
7-
}}
8-
>
9-
<Toast
10-
style={{
11-
position: 'absolute',
12-
top: 0,
13-
right: 0,
14-
}}
15-
>
16-
<Toast.Header>
17-
<img src="holder.js/20x20?text=%20" className="rounded me-2" alt="" />
18-
<strong className="me-auto">Bootstrap</strong>
19-
<small>just now</small>
20-
</Toast.Header>
21-
<Toast.Body>See? Just like this.</Toast.Body>
22-
</Toast>
23-
</div>;
1+
function Example() {
2+
const [position, setPosition] = useState('top-start');
3+
4+
return (
5+
<>
6+
<div className="mb-3">
7+
<label htmlFor="selectToastPlacement">Toast position</label>
8+
<Form.Select
9+
id="selectToastPlacement"
10+
className="mt-2"
11+
onChange={(e) => setPosition(e.currentTarget.value)}
12+
>
13+
{[
14+
'top-start',
15+
'top-center',
16+
'top-end',
17+
'middle-start',
18+
'middle-center',
19+
'middle-end',
20+
'bottom-start',
21+
'bottom-center',
22+
'bottom-end',
23+
].map((p) => (
24+
<option key={p} value={p}>
25+
{p}
26+
</option>
27+
))}
28+
</Form.Select>
29+
</div>
30+
31+
<div
32+
aria-live="polite"
33+
aria-atomic="true"
34+
className="bg-dark position-relative"
35+
style={{ minHeight: '240px' }}
36+
>
37+
<ToastContainer className="p-3" position={position}>
38+
<Toast>
39+
<Toast.Header closeButton={false}>
40+
<img
41+
src="holder.js/20x20?text=%20"
42+
className="rounded me-2"
43+
alt=""
44+
/>
45+
<strong className="me-auto">Bootstrap</strong>
46+
<small>11 mins ago</small>
47+
</Toast.Header>
48+
<Toast.Body>Hello, world! This is a toast message.</Toast.Body>
49+
</Toast>
50+
</ToastContainer>
51+
</div>
52+
</>
53+
);
54+
}
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
11
<div
22
aria-live="polite"
33
aria-atomic="true"
4-
style={{
5-
position: 'relative',
6-
minHeight: '200px',
7-
}}
4+
className="bg-dark position-relative"
5+
style={{ minHeight: '240px' }}
86
>
9-
<div
10-
style={{
11-
position: 'absolute',
12-
top: 0,
13-
right: 0,
14-
}}
15-
>
7+
<ToastContainer position="top-end" className="p-3">
168
<Toast>
179
<Toast.Header>
1810
<img src="holder.js/20x20?text=%20" className="rounded me-2" alt="" />
1911
<strong className="me-auto">Bootstrap</strong>
20-
<small>just now</small>
12+
<small className="text-muted">just now</small>
2113
</Toast.Header>
2214
<Toast.Body>See? Just like this.</Toast.Body>
2315
</Toast>
2416
<Toast>
2517
<Toast.Header>
2618
<img src="holder.js/20x20?text=%20" className="rounded me-2" alt="" />
2719
<strong className="me-auto">Bootstrap</strong>
28-
<small>2 seconds ago</small>
20+
<small className="text-muted">2 seconds ago</small>
2921
</Toast.Header>
3022
<Toast.Body>Heads up, toasts will stack automatically</Toast.Body>
3123
</Toast>
32-
</div>
24+
</ToastContainer>
3325
</div>;

www/src/examples/Toast/Stacking.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
<>
1+
<ToastContainer>
22
<Toast>
33
<Toast.Header>
44
<img src="holder.js/20x20?text=%20" className="rounded me-2" alt="" />
55
<strong className="me-auto">Bootstrap</strong>
6-
<small>just now</small>
6+
<small className="text-muted">just now</small>
77
</Toast.Header>
88
<Toast.Body>See? Just like this.</Toast.Body>
99
</Toast>
1010
<Toast>
1111
<Toast.Header>
1212
<img src="holder.js/20x20?text=%20" className="rounded me-2" alt="" />
1313
<strong className="me-auto">Bootstrap</strong>
14-
<small>2 seconds ago</small>
14+
<small className="text-muted">2 seconds ago</small>
1515
</Toast.Header>
1616
<Toast.Body>Heads up, toasts will stack automatically</Toast.Body>
1717
</Toast>
18-
</>;
18+
</ToastContainer>;

www/src/pages/components/toasts.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ When you have multiple toasts, we default to vertically stacking them in a reada
4141

4242
### Placement
4343

44-
Place toasts with custom CSS as you need them. The top right is often used for notifications, as is the top middle.
44+
Place toasts by setting a `position` in a `ToastContainer`. The top right is often used for notifications, as is the top middle.
4545

4646
<ReactPlayground codeText={ToastPlacement} />
4747

@@ -60,6 +60,7 @@ A Toast can also automatically hide after X milliseconds. For that, use the `aut
6060
<ComponentApi metadata={props.data.Toast} />
6161
<ComponentApi metadata={props.data.ToastHeader}/>
6262
<ComponentApi metadata={props.data.ToastBody}/>
63+
<ComponentApi metadata={props.data.ToastContainer} />
6364

6465
export const query = graphql`
6566
query ToastQuery {
@@ -72,5 +73,8 @@ export const query = graphql`
7273
ToastBody: componentMetadata(displayName: { eq: "ToastBody" }) {
7374
...ComponentApi_metadata
7475
}
76+
ToastContainer: componentMetadata(displayName: { eq: "ToastContainer" }) {
77+
...ComponentApi_metadata
78+
}
7579
}
7680
`;

0 commit comments

Comments
 (0)