Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPROVE] Increase decoupling between React components and Blaze templates #16642

Merged
merged 17 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions app/channel-settings/client/views/Multiselect.html

This file was deleted.

35 changes: 12 additions & 23 deletions app/channel-settings/client/views/Multiselect.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import './Multiselect.html';
import { Template } from 'meteor/templating';

import { MultiSelectSettingInput } from '../../../../client/components/admin/settings/inputs/MultiSelectSettingInput';


Template.Multiselect.onRendered(async function() {
const { MeteorProvider } = await import('../../../../client/providers/MeteorProvider');
const React = await import('react');
const ReactDOM = await import('react-dom');
this.container = this.firstNode;
this.autorun(() => {
ReactDOM.render(React.createElement(MeteorProvider, {
children: React.createElement(MultiSelectSettingInput, Template.currentData()),
}), this.container);
});
});


Template.Multiselect.onDestroyed(async function() {
const ReactDOM = await import('react-dom');
this.container && ReactDOM.unmountComponentAtNode(this.container);
});
import { HTML } from 'meteor/htmljs';

import { createTemplateForComponent } from '../../../../client/reactAdapters';

createTemplateForComponent(
'Multiselect',
() => import('../../../../client/components/admin/settings/inputs/MultiSelectSettingInput'),
{
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ class: 'rc-multiselect', style: 'display: flex;' }),
},
);
3 changes: 0 additions & 3 deletions app/ui-message/client/blocks/Blocks.html

This file was deleted.

48 changes: 0 additions & 48 deletions app/ui-message/client/blocks/Blocks.js

This file was deleted.

3 changes: 0 additions & 3 deletions app/ui-message/client/blocks/ButtonElement.html

This file was deleted.

196 changes: 31 additions & 165 deletions app/ui-message/client/blocks/MessageBlock.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
import { UiKitMessage as uiKitMessage, kitContext, UiKitModal as uiKitModal, messageParser, modalParser, UiKitComponent } from '@rocket.chat/fuselage-ui-kit';
import { uiKitText } from '@rocket.chat/ui-kit';
import { Modal, AnimatedVisibility, ButtonGroup, Button, Box } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer';
import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket.chat/fuselage-ui-kit';
import React, { useRef, useEffect } from 'react';

import { renderMessageBody } from '../../../ui-utils/client';
import { getURL } from '../../../utils/lib/getURL';
import { useReactiveValue } from '../../../../client/hooks/useReactiveValue';

const focusableElementsString = 'a[href]:not([tabindex="-1"]), area[href]:not([tabindex="-1"]), input:not([disabled]):not([tabindex="-1"]), select:not([disabled]):not([tabindex="-1"]), textarea:not([disabled]):not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable]';

const focusableElementsStringInvalid = 'a[href]:not([tabindex="-1"]):invalid, area[href]:not([tabindex="-1"]):invalid, input:not([disabled]):not([tabindex="-1"]):invalid, select:not([disabled]):not([tabindex="-1"]):invalid, textarea:not([disabled]):not([tabindex="-1"]):invalid, button:not([disabled]):not([tabindex="-1"]):invalid, iframe:invalid, object:invalid, embed:invalid, [tabindex]:not([tabindex="-1"]):invalid, [contenteditable]:invalid';
import * as ActionManager from '../ActionManager';

// TODO: move this to fuselage-ui-kit itself
messageParser.text = ({ text, type } = {}) => {
if (type !== 'mrkdwn') {
return text;
Expand All @@ -20,165 +14,37 @@ messageParser.text = ({ text, type } = {}) => {
return <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />;
};

modalParser.text = messageParser.text;

const contextDefault = {
action: console.log,
state: (data) => {
console.log('state', data);
},
};
export const messageBlockWithContext = (context) => (props) => {
const data = useReactiveValue(props.data);
return (
<kitContext.Provider value={context}>
{uiKitMessage(data.blocks)}
</kitContext.Provider>
);
};

const textParser = uiKitText(new class {
plain_text({ text }) {
return text;
}

text({ text }) {
return text;
}
}());
export function MessageBlock({ mid: _mid, rid, blocks, appId }) {
const context = {
action: ({ actionId, value, blockId, mid = _mid }) => {
ActionManager.triggerBlockAction({
blockId,
actionId,
value,
mid,
rid,
appId: blocks[0].appId,
container: {
type: UIKitIncomingInteractionContainerType.MESSAGE,
id: mid,
},
});
},
appId,
rid,
};

// https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html

export const modalBlockWithContext = ({
onSubmit,
onClose,
onCancel,
...context
}) => (props) => {
const id = `modal_id_${ useUniqueId() }`;

const { view, ...data } = useReactiveValue(props.data);
const values = useReactiveValue(props.values);
const ref = useRef();

// Auto focus
useEffect(() => {
if (!ref.current) {
return;
}

if (data.errors && Object.keys(data.errors).length) {
const element = ref.current.querySelector(focusableElementsStringInvalid);
element && element.focus();
} else {
const element = ref.current.querySelector(focusableElementsString);
element && element.focus();
}
}, [ref.current, data.errors]);
// save focus to restore after close
const previousFocus = useMemo(() => document.activeElement, []);
// restore the focus after the component unmount
useEffect(() => () => previousFocus && previousFocus.focus(), []);
// Handle Tab, Shift + Tab, Enter and Escape
const handleKeyDown = useCallback((event) => {
if (event.keyCode === 13) { // ENTER
return onSubmit(event);
}

if (event.keyCode === 27) { // ESC
event.stopPropagation();
event.preventDefault();
onClose();
return false;
}

if (event.keyCode === 9) { // TAB
const elements = Array.from(ref.current.querySelectorAll(focusableElementsString));
const [first] = elements;
const last = elements.pop();

if (!ref.current.contains(document.activeElement)) {
return first.focus();
}

if (event.shiftKey) {
if (!first || first === document.activeElement) {
last.focus();
event.stopPropagation();
event.preventDefault();
}
return;
}

if (!last || last === document.activeElement) {
first.focus();
event.stopPropagation();
event.preventDefault();
}
}
}, [onSubmit]);
// Clean the events
useEffect(() => {
const element = document.querySelector('.rc-modal-wrapper');
const container = element.querySelector('.rcx-modal__content');
const close = (e) => {
if (e.target !== element) {
return;
}
e.preventDefault();
e.stopPropagation();
onClose();
return false;
};

const ignoreIfnotContains = (e) => {
if (!container.contains(e.target)) {
return;
}
return handleKeyDown(e);
};

document.addEventListener('keydown', ignoreIfnotContains);
element.addEventListener('click', close);
return () => {
document.removeEventListener('keydown', ignoreIfnotContains);
element.removeEventListener('click', close);
};
}, handleKeyDown);
ref.current.dispatchEvent(new Event('rendered'));
}, []);

return (
<kitContext.Provider value={{ ...context, ...data, values }}>
<AnimatedVisibility visibility={AnimatedVisibility.UNHIDING}>
<Modal open id={id} ref={ref}>
<Modal.Header>
<Modal.Thumb url={getURL(`/api/apps/${ data.appId }/icon`)} />
<Modal.Title>{textParser([view.title])}</Modal.Title>
<Modal.Close tabIndex={-1} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Box
is='form'
method='post'
action='#'
onSubmit={onSubmit}
>
<UiKitComponent render={uiKitModal} blocks={view.blocks} />
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
{ view.close && <Button onClick={onCancel}>{textParser([view.close.text])}</Button>}
{ view.submit && <Button primary onClick={onSubmit}>{textParser([view.submit.text])}</Button>}
</ButtonGroup>
</Modal.Footer>
</Modal>
</AnimatedVisibility>
<kitContext.Provider value={context}>
<div className='js-block-wrapper' ref={ref} />
<UiKitComponent render={UiKitMessage} blocks={blocks} />
</kitContext.Provider>
);
};
}

export const MessageBlock = ({ blocks }, context = contextDefault) => (
<kitContext.Provider value={context}>
{uiKitMessage(blocks)}
</kitContext.Provider>
);
export default MessageBlock;
3 changes: 0 additions & 3 deletions app/ui-message/client/blocks/ModalBlock.html

This file was deleted.

Loading