Skip to content

Commit

Permalink
feat(Modal): add RTL support
Browse files Browse the repository at this point in the history
  • Loading branch information
kyletsang committed Aug 11, 2021
1 parent 8f76539 commit 0cd40eb
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 26 deletions.
10 changes: 9 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@
"prettier/prettier": "warn",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off"
}
},
"overrides": [
{
"files": ["test/**/*"],
"rules": {
"@typescript-eslint/no-unused-expressions": "off"
}
}
]
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@babel/runtime": "^7.14.0",
"@restart/context": "^2.1.4",
"@restart/hooks": "^0.3.26",
"@restart/ui": "^0.2.0",
"@restart/ui": "^0.2.1",
"@types/invariant": "^2.2.33",
"@types/prop-types": "^15.7.3",
"@types/react": ">=16.14.8",
Expand All @@ -84,6 +84,10 @@
"@babel/register": "^7.14.5",
"@react-bootstrap/babel-preset": "^2.1.0",
"@react-bootstrap/eslint-config": "^2.0.0",
"@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0",
"@types/sinon": "^10.0.2",
"@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^4.28.5",
"@typescript-eslint/parser": "^4.28.5",
"babel-eslint": "^10.1.0",
Expand Down
27 changes: 18 additions & 9 deletions src/BootstrapModalManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import addClass from 'dom-helpers/addClass';
import css from 'dom-helpers/css';
import qsa from 'dom-helpers/querySelectorAll';
import removeClass from 'dom-helpers/removeClass';
import ModalManager, { ContainerState } from '@restart/ui/ModalManager';
import ModalManager, {
ContainerState,
ModalManagerOptions,
} from '@restart/ui/ModalManager';

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

if (!containerState.scrollBarWidth) return;

const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight';
const marginProp = this.isRTL ? 'marginLeft' : 'marginRight';

qsa(container, Selector.FIXED_CONTENT).forEach((el) =>
this.adjustAndStore('paddingRight', el, containerState.scrollBarWidth),
this.adjustAndStore(paddingProp, el, containerState.scrollBarWidth),
);
qsa(container, Selector.STICKY_CONTENT).forEach((el) =>
this.adjustAndStore('marginRight', el, -containerState.scrollBarWidth),
this.adjustAndStore(marginProp, el, -containerState.scrollBarWidth),
);
qsa(container, Selector.NAVBAR_TOGGLER).forEach((el) =>
this.adjustAndStore('marginRight', el, containerState.scrollBarWidth),
this.adjustAndStore(marginProp, el, containerState.scrollBarWidth),
);
}

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

const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight';
const marginProp = this.isRTL ? 'marginLeft' : 'marginRight';

qsa(container, Selector.FIXED_CONTENT).forEach((el) =>
this.restore('paddingRight', el),
this.restore(paddingProp, el),
);
qsa(container, Selector.STICKY_CONTENT).forEach((el) =>
this.restore('marginRight', el),
this.restore(marginProp, el),
);
qsa(container, Selector.NAVBAR_TOGGLER).forEach((el) =>
this.restore('marginRight', el),
this.restore(marginProp, el),
);
}
}

let sharedManager: BootstrapModalManager | undefined;
export function getSharedManager() {
if (!sharedManager) sharedManager = new BootstrapModalManager();
export function getSharedManager(options?: ModalManagerOptions) {
if (!sharedManager) sharedManager = new BootstrapModalManager(options);
return sharedManager;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import ModalFooter from './ModalFooter';
import ModalHeader from './ModalHeader';
import ModalTitle from './ModalTitle';
import { BsPrefixRefForwardingComponent } from './helpers';
import { useBootstrapPrefix } from './ThemeProvider';
import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';

export interface ModalProps
extends Omit<
Expand Down Expand Up @@ -287,6 +287,7 @@ const Modal: BsPrefixRefForwardingComponent<'div', ModalProps> =
const [modal, setModalRef] = useCallbackRef<ModalInstance>();
const mergedRef = useMergedRefs(ref, setModalRef);
const handleHide = useEventCallback(onHide);
const isRTL = useIsRTL();

bsPrefix = useBootstrapPrefix(bsPrefix, 'modal');

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

function getModalManager() {
if (propsManager) return propsManager;
return getSharedManager();
return getSharedManager({ isRTL });
}

function updateDialogStyle(node) {
Expand Down
140 changes: 140 additions & 0 deletions test/BootstrapModalManagerSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { expect } from 'chai';
import getScrollbarSize from 'dom-helpers/scrollbarSize';
import { injectCss } from './helpers';
import BootstrapModalManager, {
getSharedManager,
} from '../src/BootstrapModalManager';

const createModal = () => ({ dialog: null, backdrop: null });

describe('BootstrapModalManager', () => {
let container, manager;

beforeEach(() => {
manager?.reset();
manager = new BootstrapModalManager();
container = document.createElement('div');
container.setAttribute('id', 'container');

const fixedContent = document.createElement('div');
fixedContent.className = 'fixed-top';
container.appendChild(fixedContent);
const stickyContent = document.createElement('div');
stickyContent.className = 'sticky-top';
container.appendChild(stickyContent);
const navbarToggler = document.createElement('div');
navbarToggler.className = 'navbar-toggler';
container.appendChild(navbarToggler);

document.body.appendChild(container);
});

afterEach(() => {
manager?.reset();
document.body.removeChild(container);
container = null;
manager = null;
});

it('should add Modal', () => {
const modal = createModal();

manager.add(modal);

expect(manager.modals.length).to.equal(1);
expect(manager.modals[0]).to.equal(modal);

expect(manager.state).to.eql({
scrollBarWidth: 0,
style: {
overflow: '',
paddingRight: '',
},
});
});

it('should return a shared modal manager', () => {
const localManager = getSharedManager();
localManager.should.exist;
});

it('should return a same modal manager if called twice', () => {
let localManager = getSharedManager();
localManager.should.exist;

const modal = createModal();
localManager.add(modal as any);
localManager.modals.length.should.equal(1);

localManager = getSharedManager();
localManager.modals.length.should.equal(1);

localManager.remove(modal as any);
});

describe('container styles', () => {
beforeEach(() => {
injectCss(`
body {
padding-right: 20px;
padding-left: 20px;
overflow: scroll;
}
#container {
height: 4000px;
}
`);
});

afterEach(() => injectCss.reset());

it('should set padding to right side', () => {
const modal = createModal();
manager.add(modal);

expect(document.body.style.paddingRight).to.equal(
`${getScrollbarSize() + 20}px`,
);
});

it('should set padding to left side if RTL', () => {
const modal = createModal();

new BootstrapModalManager({ isRTL: true }).add(modal as any);

expect(document.body.style.paddingLeft).to.equal(
`${getScrollbarSize() + 20}px`,
);
});

it('should restore container overflow style', () => {
const modal = createModal();

document.body.style.overflow = 'scroll';

expect(document.body.style.overflow).to.equal('scroll');

manager.add(modal);
manager.remove(modal);

expect(document.body.style.overflow).to.equal('scroll');
document.body.style.overflow = '';
});

it('should restore container overflow style for RTL', () => {
const modal = createModal();

document.body.style.overflow = 'scroll';

expect(document.body.style.overflow).to.equal('scroll');

const localManager = new BootstrapModalManager({ isRTL: true });
localManager.add(modal as any);
localManager.remove(modal as any);

expect(document.body.style.overflow).to.equal('scroll');
document.body.style.overflow = '';
});
});
});
30 changes: 29 additions & 1 deletion test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
// eslint-disable-next-line import/prefer-default-export
export function shouldWarn(about) {
console.error.expected.push(about);
}

let style;
let seen = [];

export function injectCss(rules) {
if (seen.indexOf(rules) !== -1) {
return;
}

style =
style ||
(function iife() {
let _style = document.createElement('style');
_style.appendChild(document.createTextNode(''));
document.head.appendChild(_style);
return _style;
})();

seen.push(rules);
style.innerHTML += `\n${rules}`;
}

injectCss.reset = () => {
if (style) {
document.head.removeChild(style);
}
style = null;
seen = [];
};
9 changes: 9 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "..",
"compilerOptions": {
"jsx": "react-jsx",
"types": ["mocha", "chai", "sinon", "sinon-chai"],
"rootDir": "..",
},
"include": ["../src", "."]
}
16 changes: 10 additions & 6 deletions www/src/layouts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CodeBlock from '../components/CodeBlock';
import LinkedHeading from '../components/LinkedHeading';
import DocsAlert from '../components/DocsAlert';
import SEO from '../seo';
import ThemeProvider from '../../../src/ThemeProvider';

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

function DefaultLayout({ children, location, grayscale = true }) {
return (
<div className={grayscale ? styles.gray : undefined}>
<SEO pathname={location.pathname} />
<NavMain activePage={location.pathname} />
<DocsAlert />
<MDXProvider components={components}>{children}</MDXProvider>
</div>
/* Change dir to "rtl" for RTL dev */
<ThemeProvider dir="ltr">
<div className={grayscale ? styles.gray : undefined}>
<SEO pathname={location.pathname} />
<NavMain activePage={location.pathname} />
<DocsAlert />
<MDXProvider components={components}>{children}</MDXProvider>
</div>
</ThemeProvider>
);
}

Expand Down
6 changes: 5 additions & 1 deletion www/src/seo.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const SEO = ({ title, description, pathname, article }) => (

return (
<>
<Helmet title={seo.title} titleTemplate={titleTemplate}>
<Helmet
title={seo.title}
titleTemplate={titleTemplate}
// htmlAttributes={{ dir: 'rtl' }}
>
<meta name="description" content={seo.description} />
{seo.url && <meta property="og:url" content={seo.url} />}
{(article ? true : null) && (
Expand Down
2 changes: 2 additions & 0 deletions www/src/wrap-page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require('bootstrap/dist/css/bootstrap.min.css');
// require('bootstrap/dist/css/bootstrap.rtl.min.css');

const React = require('react');
const { MDXProvider } = require('@mdx-js/react');

Expand Down
Loading

0 comments on commit 0cd40eb

Please sign in to comment.