Skip to content

Commit 0bb7b42

Browse files
authored
feat: add NavbarOffcanvas component (react-bootstrap#5999)
1 parent 86e5503 commit 0bb7b42

File tree

7 files changed

+157
-40
lines changed

7 files changed

+157
-40
lines changed

src/Navbar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import createWithBsPrefix from './createWithBsPrefix';
1010
import NavbarBrand from './NavbarBrand';
1111
import NavbarCollapse from './NavbarCollapse';
1212
import NavbarToggle from './NavbarToggle';
13+
import NavbarOffcanvas from './NavbarOffcanvas';
1314
import { useBootstrapPrefix } from './ThemeProvider';
1415
import NavbarContext, { NavbarContextType } from './NavbarContext';
1516
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
@@ -49,7 +50,8 @@ const propTypes = {
4950
* The breakpoint, below which, the Navbar will collapse.
5051
* When `true` the Navbar will always be expanded regardless of screen size.
5152
*/
52-
expand: PropTypes.oneOf([true, 'sm', 'md', 'lg', 'xl', 'xxl']).isRequired,
53+
expand: PropTypes.oneOf([false, true, 'sm', 'md', 'lg', 'xl', 'xxl'])
54+
.isRequired,
5355

5456
/**
5557
* A convenience prop for adding `bg-*` utility classes since they are so commonly used here.
@@ -220,7 +222,8 @@ Navbar.displayName = 'Navbar';
220222

221223
export default Object.assign(Navbar, {
222224
Brand: NavbarBrand,
223-
Toggle: NavbarToggle,
224225
Collapse: NavbarCollapse,
226+
Offcanvas: NavbarOffcanvas,
225227
Text: NavbarText,
228+
Toggle: NavbarToggle,
226229
});

src/NavbarOffcanvas.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
import { useContext } from 'react';
3+
import Offcanvas, { OffcanvasProps } from './Offcanvas';
4+
import NavbarContext from './NavbarContext';
5+
6+
const NavbarOffcanvas = React.forwardRef<HTMLDivElement, OffcanvasProps>(
7+
(props, ref) => {
8+
const context = useContext(NavbarContext);
9+
10+
return <Offcanvas ref={ref} show={!!context?.expanded} {...props} />;
11+
},
12+
);
13+
14+
NavbarOffcanvas.displayName = 'NavbarOffcanvas';
15+
16+
export default NavbarOffcanvas;

src/Offcanvas.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import classNames from 'classnames';
22
import useEventCallback from '@restart/hooks/useEventCallback';
33
import PropTypes from 'prop-types';
44
import * as React from 'react';
5-
import { useCallback, useMemo, useRef } from 'react';
5+
import { useCallback, useContext, useMemo, useRef } from 'react';
66
import BaseModal, {
77
ModalProps as BaseModalProps,
88
ModalHandle,
@@ -11,6 +11,7 @@ import Fade from './Fade';
1111
import OffcanvasBody from './OffcanvasBody';
1212
import OffcanvasToggling from './OffcanvasToggling';
1313
import ModalContext from './ModalContext';
14+
import NavbarContext from './NavbarContext';
1415
import OffcanvasHeader from './OffcanvasHeader';
1516
import OffcanvasTitle from './OffcanvasTitle';
1617
import { BsPrefixRefForwardingComponent } from './helpers';
@@ -218,9 +219,13 @@ const Offcanvas: BsPrefixRefForwardingComponent<'div', OffcanvasProps> =
218219
ref,
219220
) => {
220221
const modalManager = useRef<BootstrapModalManager>();
221-
const handleHide = useEventCallback(onHide);
222-
223222
bsPrefix = useBootstrapPrefix(bsPrefix, 'offcanvas');
223+
const { onToggle } = useContext(NavbarContext) || {};
224+
225+
const handleHide = useEventCallback(() => {
226+
onToggle?.();
227+
onHide?.();
228+
});
224229

225230
const modalContext = useMemo(
226231
() => ({
@@ -258,10 +263,7 @@ const Offcanvas: BsPrefixRefForwardingComponent<'div', OffcanvasProps> =
258263
(backdropProps) => (
259264
<div
260265
{...backdropProps}
261-
className={classNames(
262-
`${bsPrefix}-backdrop`,
263-
backdropClassName,
264-
)}
266+
className={classNames(`${bsPrefix}-backdrop`, backdropClassName)}
265267
/>
266268
),
267269
[backdropClassName, bsPrefix],
@@ -297,7 +299,7 @@ const Offcanvas: BsPrefixRefForwardingComponent<'div', OffcanvasProps> =
297299
restoreFocusOptions={restoreFocusOptions}
298300
onEscapeKeyDown={onEscapeKeyDown}
299301
onShow={onShow}
300-
onHide={onHide}
302+
onHide={handleHide}
301303
onEnter={handleEnter}
302304
onEntering={onEntering}
303305
onEntered={onEntered}

test/NavbarOffcanvasSpec.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { fireEvent, render } from '@testing-library/react';
2+
import sinon from 'sinon';
3+
4+
import Navbar from '../src/Navbar';
5+
import Offcanvas from '../src/Offcanvas';
6+
7+
describe('<NavbarOffcanvas>', () => {
8+
it('should should open the offcanvas', () => {
9+
const { getByTestId } = render(
10+
<Navbar>
11+
<Navbar.Toggle data-testid="toggle" />
12+
<Navbar.Offcanvas data-testid="offcanvas">hello</Navbar.Offcanvas>
13+
</Navbar>,
14+
);
15+
16+
fireEvent.click(getByTestId('toggle'));
17+
getByTestId('offcanvas').classList.contains('show').should.be.true;
18+
});
19+
20+
it('should close the offcanvas on header close button click', () => {
21+
const onToggleSpy = sinon.spy();
22+
const { getByLabelText } = render(
23+
<Navbar onToggle={onToggleSpy} expanded>
24+
<Navbar.Toggle data-testid="toggle" />
25+
<Navbar.Offcanvas data-testid="offcanvas">
26+
<Offcanvas.Header closeButton>header</Offcanvas.Header>
27+
</Navbar.Offcanvas>
28+
</Navbar>,
29+
);
30+
31+
fireEvent.click(getByLabelText('Close'));
32+
onToggleSpy.should.have.been.calledWith(false);
33+
});
34+
});

www/src/examples/Navbar/NavScroll.js

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
<Navbar bg="light" expand="lg">
2-
<Navbar.Brand href="#">Navbar scroll</Navbar.Brand>
3-
<Navbar.Toggle aria-controls="navbarScroll" />
4-
<Navbar.Collapse id="navbarScroll">
5-
<Nav
6-
className="mr-auto my-2 my-lg-0"
7-
style={{ maxHeight: '100px' }}
8-
navbarScroll
9-
>
10-
<Nav.Link href="#action1">Home</Nav.Link>
11-
<Nav.Link href="#action2">Link</Nav.Link>
12-
<NavDropdown title="Link" id="navbarScrollingDropdown">
13-
<NavDropdown.Item href="#action3">Action</NavDropdown.Item>
14-
<NavDropdown.Item href="#action4">Another action</NavDropdown.Item>
15-
<NavDropdown.Divider />
16-
<NavDropdown.Item href="#action5">Something else here</NavDropdown.Item>
17-
</NavDropdown>
18-
<Nav.Link href="#" disabled>
19-
Link
20-
</Nav.Link>
21-
</Nav>
22-
<Form className="d-flex">
23-
<FormControl
24-
type="search"
25-
placeholder="Search"
26-
className="mr-2"
27-
aria-label="Search"
28-
/>
29-
<Button variant="outline-success">Search</Button>
30-
</Form>
31-
</Navbar.Collapse>
2+
<Container fluid>
3+
<Navbar.Brand href="#">Navbar scroll</Navbar.Brand>
4+
<Navbar.Toggle aria-controls="navbarScroll" />
5+
<Navbar.Collapse id="navbarScroll">
6+
<Nav
7+
className="me-auto my-2 my-lg-0"
8+
style={{ maxHeight: '100px' }}
9+
navbarScroll
10+
>
11+
<Nav.Link href="#action1">Home</Nav.Link>
12+
<Nav.Link href="#action2">Link</Nav.Link>
13+
<NavDropdown title="Link" id="navbarScrollingDropdown">
14+
<NavDropdown.Item href="#action3">Action</NavDropdown.Item>
15+
<NavDropdown.Item href="#action4">Another action</NavDropdown.Item>
16+
<NavDropdown.Divider />
17+
<NavDropdown.Item href="#action5">
18+
Something else here
19+
</NavDropdown.Item>
20+
</NavDropdown>
21+
<Nav.Link href="#" disabled>
22+
Link
23+
</Nav.Link>
24+
</Nav>
25+
<Form className="d-flex">
26+
<FormControl
27+
type="search"
28+
placeholder="Search"
29+
className="me-2"
30+
aria-label="Search"
31+
/>
32+
<Button variant="outline-success">Search</Button>
33+
</Form>
34+
</Navbar.Collapse>
35+
</Container>
3236
</Navbar>;

www/src/examples/Navbar/Offcanvas.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Navbar bg="light" expand={false}>
2+
<Container fluid>
3+
<Navbar.Brand href="#">Navbar Offcanvas</Navbar.Brand>
4+
<Navbar.Toggle aria-controls="offcanvasNavbar" />
5+
<Navbar.Offcanvas
6+
id="offcanvasNavbar"
7+
aria-labelledby="offcanvasNavbarLabel"
8+
placement="end"
9+
>
10+
<Offcanvas.Header closeButton>
11+
<Offcanvas.Title id="offcanvasNavbarLabel">Offcanvas</Offcanvas.Title>
12+
</Offcanvas.Header>
13+
<Offcanvas.Body>
14+
<Nav className="justify-content-end flex-grow-1 pe-3">
15+
<Nav.Link href="#action1">Home</Nav.Link>
16+
<Nav.Link href="#action2">Link</Nav.Link>
17+
<NavDropdown title="Dropdown" id="offcanvasNavbarDropdown">
18+
<NavDropdown.Item href="#action3">Action</NavDropdown.Item>
19+
<NavDropdown.Item href="#action4">Another action</NavDropdown.Item>
20+
<NavDropdown.Divider />
21+
<NavDropdown.Item href="#action5">
22+
Something else here
23+
</NavDropdown.Item>
24+
</NavDropdown>
25+
</Nav>
26+
<Form className="d-flex">
27+
<FormControl
28+
type="search"
29+
placeholder="Search"
30+
className="me-2"
31+
aria-label="Search"
32+
/>
33+
<Button variant="outline-success">Search</Button>
34+
</Form>
35+
</Offcanvas.Body>
36+
</Navbar.Offcanvas>
37+
</Container>
38+
</Navbar>;

www/src/pages/components/navbar.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import NavbarBrand from '../../examples/Navbar/Brand';
1111
import NavbarCollapsible from '../../examples/Navbar/Collapsible';
1212
import NavbarColorSchemes from '../../examples/Navbar/ColorSchemes';
1313
import NavScroll from '../../examples/Navbar/NavScroll';
14+
import NavbarOffcanvas from '../../examples/Navbar/Offcanvas';
1415
import NavbarTextLink from '../../examples/Navbar/TextLink';
1516
import ContainerOutside from '../../examples/Navbar/ContainerOutside';
1617
import ContainerInside from '../../examples/Navbar/ContainerInside';
@@ -178,6 +179,25 @@ export default withLayout(function NaπvbarSection({ data }) {
178179
</Callout>
179180
<ReactPlayground codeText={NavbarCollapsible} />
180181

182+
<LinkedHeading h="3" id="navbar-offcanvas">
183+
Offcanvas
184+
</LinkedHeading>
185+
186+
<p>
187+
Transform your expanding and collapsing navbar into an offcanvas drawer
188+
with the offcanvas component. We extend both the offcanvas default
189+
styles and use the <code>expand</code> prop to create a dynamic and
190+
flexible navigation sidebar.
191+
</p>
192+
193+
<p>
194+
In the example below, to create an offcanvas navbar that is always
195+
collapsed across all breakpoints, set the <code>expand</code> prop to{' '}
196+
<code>false</code>.
197+
</p>
198+
199+
<ReactPlayground codeText={NavbarOffcanvas} />
200+
181201
<LinkedHeading h="2" id="navbar-api">
182202
API
183203
</LinkedHeading>

0 commit comments

Comments
 (0)