Skip to content

Commit 0cd40eb

Browse files
committed
feat(Modal): add RTL support
1 parent 8f76539 commit 0cd40eb

File tree

11 files changed

+260
-26
lines changed

11 files changed

+260
-26
lines changed

.eslintrc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,13 @@
66
"prettier/prettier": "warn",
77
"react/jsx-uses-react": "off",
88
"react/react-in-jsx-scope": "off"
9-
}
9+
},
10+
"overrides": [
11+
{
12+
"files": ["test/**/*"],
13+
"rules": {
14+
"@typescript-eslint/no-unused-expressions": "off"
15+
}
16+
}
17+
]
1018
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@babel/runtime": "^7.14.0",
6161
"@restart/context": "^2.1.4",
6262
"@restart/hooks": "^0.3.26",
63-
"@restart/ui": "^0.2.0",
63+
"@restart/ui": "^0.2.1",
6464
"@types/invariant": "^2.2.33",
6565
"@types/prop-types": "^15.7.3",
6666
"@types/react": ">=16.14.8",
@@ -84,6 +84,10 @@
8484
"@babel/register": "^7.14.5",
8585
"@react-bootstrap/babel-preset": "^2.1.0",
8686
"@react-bootstrap/eslint-config": "^2.0.0",
87+
"@types/chai": "^4.2.21",
88+
"@types/mocha": "^9.0.0",
89+
"@types/sinon": "^10.0.2",
90+
"@types/sinon-chai": "^3.2.5",
8791
"@typescript-eslint/eslint-plugin": "^4.28.5",
8892
"@typescript-eslint/parser": "^4.28.5",
8993
"babel-eslint": "^10.1.0",

src/BootstrapModalManager.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import addClass from 'dom-helpers/addClass';
22
import css from 'dom-helpers/css';
33
import qsa from 'dom-helpers/querySelectorAll';
44
import removeClass from 'dom-helpers/removeClass';
5-
import ModalManager, { ContainerState } from '@restart/ui/ModalManager';
5+
import ModalManager, {
6+
ContainerState,
7+
ModalManagerOptions,
8+
} from '@restart/ui/ModalManager';
69

710
const Selector = {
811
FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
@@ -44,14 +47,17 @@ class BootstrapModalManager extends ModalManager {
4447

4548
if (!containerState.scrollBarWidth) return;
4649

50+
const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight';
51+
const marginProp = this.isRTL ? 'marginLeft' : 'marginRight';
52+
4753
qsa(container, Selector.FIXED_CONTENT).forEach((el) =>
48-
this.adjustAndStore('paddingRight', el, containerState.scrollBarWidth),
54+
this.adjustAndStore(paddingProp, el, containerState.scrollBarWidth),
4955
);
5056
qsa(container, Selector.STICKY_CONTENT).forEach((el) =>
51-
this.adjustAndStore('marginRight', el, -containerState.scrollBarWidth),
57+
this.adjustAndStore(marginProp, el, -containerState.scrollBarWidth),
5258
);
5359
qsa(container, Selector.NAVBAR_TOGGLER).forEach((el) =>
54-
this.adjustAndStore('marginRight', el, containerState.scrollBarWidth),
60+
this.adjustAndStore(marginProp, el, containerState.scrollBarWidth),
5561
);
5662
}
5763

@@ -61,21 +67,24 @@ class BootstrapModalManager extends ModalManager {
6167
const container = this.getElement();
6268
removeClass(container, 'modal-open');
6369

70+
const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight';
71+
const marginProp = this.isRTL ? 'marginLeft' : 'marginRight';
72+
6473
qsa(container, Selector.FIXED_CONTENT).forEach((el) =>
65-
this.restore('paddingRight', el),
74+
this.restore(paddingProp, el),
6675
);
6776
qsa(container, Selector.STICKY_CONTENT).forEach((el) =>
68-
this.restore('marginRight', el),
77+
this.restore(marginProp, el),
6978
);
7079
qsa(container, Selector.NAVBAR_TOGGLER).forEach((el) =>
71-
this.restore('marginRight', el),
80+
this.restore(marginProp, el),
7281
);
7382
}
7483
}
7584

7685
let sharedManager: BootstrapModalManager | undefined;
77-
export function getSharedManager() {
78-
if (!sharedManager) sharedManager = new BootstrapModalManager();
86+
export function getSharedManager(options?: ModalManagerOptions) {
87+
if (!sharedManager) sharedManager = new BootstrapModalManager(options);
7988
return sharedManager;
8089
}
8190

src/Modal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import ModalFooter from './ModalFooter';
2323
import ModalHeader from './ModalHeader';
2424
import ModalTitle from './ModalTitle';
2525
import { BsPrefixRefForwardingComponent } from './helpers';
26-
import { useBootstrapPrefix } from './ThemeProvider';
26+
import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';
2727

2828
export interface ModalProps
2929
extends Omit<
@@ -287,6 +287,7 @@ const Modal: BsPrefixRefForwardingComponent<'div', ModalProps> =
287287
const [modal, setModalRef] = useCallbackRef<ModalInstance>();
288288
const mergedRef = useMergedRefs(ref, setModalRef);
289289
const handleHide = useEventCallback(onHide);
290+
const isRTL = useIsRTL();
290291

291292
bsPrefix = useBootstrapPrefix(bsPrefix, 'modal');
292293

@@ -299,7 +300,7 @@ const Modal: BsPrefixRefForwardingComponent<'div', ModalProps> =
299300

300301
function getModalManager() {
301302
if (propsManager) return propsManager;
302-
return getSharedManager();
303+
return getSharedManager({ isRTL });
303304
}
304305

305306
function updateDialogStyle(node) {

test/BootstrapModalManagerSpec.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { expect } from 'chai';
2+
import getScrollbarSize from 'dom-helpers/scrollbarSize';
3+
import { injectCss } from './helpers';
4+
import BootstrapModalManager, {
5+
getSharedManager,
6+
} from '../src/BootstrapModalManager';
7+
8+
const createModal = () => ({ dialog: null, backdrop: null });
9+
10+
describe('BootstrapModalManager', () => {
11+
let container, manager;
12+
13+
beforeEach(() => {
14+
manager?.reset();
15+
manager = new BootstrapModalManager();
16+
container = document.createElement('div');
17+
container.setAttribute('id', 'container');
18+
19+
const fixedContent = document.createElement('div');
20+
fixedContent.className = 'fixed-top';
21+
container.appendChild(fixedContent);
22+
const stickyContent = document.createElement('div');
23+
stickyContent.className = 'sticky-top';
24+
container.appendChild(stickyContent);
25+
const navbarToggler = document.createElement('div');
26+
navbarToggler.className = 'navbar-toggler';
27+
container.appendChild(navbarToggler);
28+
29+
document.body.appendChild(container);
30+
});
31+
32+
afterEach(() => {
33+
manager?.reset();
34+
document.body.removeChild(container);
35+
container = null;
36+
manager = null;
37+
});
38+
39+
it('should add Modal', () => {
40+
const modal = createModal();
41+
42+
manager.add(modal);
43+
44+
expect(manager.modals.length).to.equal(1);
45+
expect(manager.modals[0]).to.equal(modal);
46+
47+
expect(manager.state).to.eql({
48+
scrollBarWidth: 0,
49+
style: {
50+
overflow: '',
51+
paddingRight: '',
52+
},
53+
});
54+
});
55+
56+
it('should return a shared modal manager', () => {
57+
const localManager = getSharedManager();
58+
localManager.should.exist;
59+
});
60+
61+
it('should return a same modal manager if called twice', () => {
62+
let localManager = getSharedManager();
63+
localManager.should.exist;
64+
65+
const modal = createModal();
66+
localManager.add(modal as any);
67+
localManager.modals.length.should.equal(1);
68+
69+
localManager = getSharedManager();
70+
localManager.modals.length.should.equal(1);
71+
72+
localManager.remove(modal as any);
73+
});
74+
75+
describe('container styles', () => {
76+
beforeEach(() => {
77+
injectCss(`
78+
body {
79+
padding-right: 20px;
80+
padding-left: 20px;
81+
overflow: scroll;
82+
}
83+
84+
#container {
85+
height: 4000px;
86+
}
87+
`);
88+
});
89+
90+
afterEach(() => injectCss.reset());
91+
92+
it('should set padding to right side', () => {
93+
const modal = createModal();
94+
manager.add(modal);
95+
96+
expect(document.body.style.paddingRight).to.equal(
97+
`${getScrollbarSize() + 20}px`,
98+
);
99+
});
100+
101+
it('should set padding to left side if RTL', () => {
102+
const modal = createModal();
103+
104+
new BootstrapModalManager({ isRTL: true }).add(modal as any);
105+
106+
expect(document.body.style.paddingLeft).to.equal(
107+
`${getScrollbarSize() + 20}px`,
108+
);
109+
});
110+
111+
it('should restore container overflow style', () => {
112+
const modal = createModal();
113+
114+
document.body.style.overflow = 'scroll';
115+
116+
expect(document.body.style.overflow).to.equal('scroll');
117+
118+
manager.add(modal);
119+
manager.remove(modal);
120+
121+
expect(document.body.style.overflow).to.equal('scroll');
122+
document.body.style.overflow = '';
123+
});
124+
125+
it('should restore container overflow style for RTL', () => {
126+
const modal = createModal();
127+
128+
document.body.style.overflow = 'scroll';
129+
130+
expect(document.body.style.overflow).to.equal('scroll');
131+
132+
const localManager = new BootstrapModalManager({ isRTL: true });
133+
localManager.add(modal as any);
134+
localManager.remove(modal as any);
135+
136+
expect(document.body.style.overflow).to.equal('scroll');
137+
document.body.style.overflow = '';
138+
});
139+
});
140+
});

test/helpers.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1-
// eslint-disable-next-line import/prefer-default-export
21
export function shouldWarn(about) {
32
console.error.expected.push(about);
43
}
4+
5+
let style;
6+
let seen = [];
7+
8+
export function injectCss(rules) {
9+
if (seen.indexOf(rules) !== -1) {
10+
return;
11+
}
12+
13+
style =
14+
style ||
15+
(function iife() {
16+
let _style = document.createElement('style');
17+
_style.appendChild(document.createTextNode(''));
18+
document.head.appendChild(_style);
19+
return _style;
20+
})();
21+
22+
seen.push(rules);
23+
style.innerHTML += `\n${rules}`;
24+
}
25+
26+
injectCss.reset = () => {
27+
if (style) {
28+
document.head.removeChild(style);
29+
}
30+
style = null;
31+
seen = [];
32+
};

test/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "..",
3+
"compilerOptions": {
4+
"jsx": "react-jsx",
5+
"types": ["mocha", "chai", "sinon", "sinon-chai"],
6+
"rootDir": "..",
7+
},
8+
"include": ["../src", "."]
9+
}

www/src/layouts/index.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CodeBlock from '../components/CodeBlock';
99
import LinkedHeading from '../components/LinkedHeading';
1010
import DocsAlert from '../components/DocsAlert';
1111
import SEO from '../seo';
12+
import ThemeProvider from '../../../src/ThemeProvider';
1213

1314
const styles = css`
1415
.gray > :not(:first-child) {
@@ -46,12 +47,15 @@ const propTypes = {
4647

4748
function DefaultLayout({ children, location, grayscale = true }) {
4849
return (
49-
<div className={grayscale ? styles.gray : undefined}>
50-
<SEO pathname={location.pathname} />
51-
<NavMain activePage={location.pathname} />
52-
<DocsAlert />
53-
<MDXProvider components={components}>{children}</MDXProvider>
54-
</div>
50+
/* Change dir to "rtl" for RTL dev */
51+
<ThemeProvider dir="ltr">
52+
<div className={grayscale ? styles.gray : undefined}>
53+
<SEO pathname={location.pathname} />
54+
<NavMain activePage={location.pathname} />
55+
<DocsAlert />
56+
<MDXProvider components={components}>{children}</MDXProvider>
57+
</div>
58+
</ThemeProvider>
5559
);
5660
}
5761

www/src/seo.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ const SEO = ({ title, description, pathname, article }) => (
3636

3737
return (
3838
<>
39-
<Helmet title={seo.title} titleTemplate={titleTemplate}>
39+
<Helmet
40+
title={seo.title}
41+
titleTemplate={titleTemplate}
42+
// htmlAttributes={{ dir: 'rtl' }}
43+
>
4044
<meta name="description" content={seo.description} />
4145
{seo.url && <meta property="og:url" content={seo.url} />}
4246
{(article ? true : null) && (

www/src/wrap-page.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
require('bootstrap/dist/css/bootstrap.min.css');
2+
// require('bootstrap/dist/css/bootstrap.rtl.min.css');
3+
24
const React = require('react');
35
const { MDXProvider } = require('@mdx-js/react');
46

0 commit comments

Comments
 (0)