Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Exploration of hooks and context #2206

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
123 changes: 51 additions & 72 deletions lib/atom/commands.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, {useContext, useEffect} from 'react';
import {Disposable} from 'event-kit';
import PropTypes from 'prop-types';

import {DOMNodePropType, RefHolderPropType} from '../prop-types';
import {useAtomEnv} from '../context/atom';
import RefHolder from '../models/ref-holder';
import {RefHolderPropType, DOMNodePropType} from '../prop-types';

export default class Commands extends React.Component {
static propTypes = {
registry: PropTypes.object.isRequired,
target: PropTypes.oneOfType([
PropTypes.string,
DOMNodePropType,
RefHolderPropType,
]).isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,
}

render() {
const {registry, target} = this.props;
return (
<div>
{React.Children.map(this.props.children, child => {
return child ? React.cloneElement(child, {registry, target}) : null;
})}
</div>
);
}
}
const CommandsContext = React.createContext({registry: null, target: null});

export class Command extends React.Component {
static propTypes = {
registry: PropTypes.object,
target: PropTypes.oneOfType([
PropTypes.string,
DOMNodePropType,
RefHolderPropType,
]),
command: PropTypes.string.isRequired,
callback: PropTypes.func.isRequired,
}

constructor(props, context) {
super(props, context);
this.subTarget = new Disposable();
this.subCommand = new Disposable();
}

componentDidMount() {
this.observeTarget(this.props);
}
export function Commands({target, children}) {
const registry = useAtomEnv().commands;
const context = {registry, target};

componentWillReceiveProps(newProps) {
if (['registry', 'target', 'command', 'callback'].some(p => newProps[p] !== this.props[p])) {
this.observeTarget(newProps);
}
}

componentWillUnmount() {
this.subTarget.dispose();
this.subCommand.dispose();
}

observeTarget(props) {
this.subTarget.dispose();
this.subTarget = RefHolder.on(props.target).observe(t => this.registerCommand(t, props));
}

registerCommand(target, {registry, command, callback}) {
this.subCommand.dispose();
this.subCommand = registry.add(target, command, callback);
}
return (
<CommandsContext.Provider value={context}>
{children}
</CommandsContext.Provider>
);
}

render() {
return null;
}
Commands.propTypes = {
target: PropTypes.oneOf([
PropTypes.string,
DOMNodePropType,
RefHolderPropType,
]).isRequired,
children: PropTypes.node.isRequired,
};

export function Command({command, callback}) {
const {registry, target} = useContext(CommandsContext);

let subTarget = new Disposable();
let subCommand = new Disposable();

useEffect(() => {
subTarget.dispose();
subTarget = RefHolder.on(target).observe(t => {
subCommand.dispose();
subCommand = registry.add(t, command, callback);
});
return () => {
subTarget.dispose();
subCommand.dispose();
};
}, [registry, target]);

if (registry === null || target === null) {
throw new Error('Attempt to render Command outside of Commands');
}

return null;
}

Command.propTypes = {
command: PropTypes.string.isRequired,
callback: PropTypes.func.isRequired,
};
2 changes: 1 addition & 1 deletion lib/atom/status-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class StatusBar extends React.Component {
}

static defaultProps = {
onConsumeStatusBar: statusBar => {},
onConsumeStatusBar: () => {},
}

constructor(props) {
Expand Down
161 changes: 62 additions & 99 deletions lib/atom/tooltip.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import {useRef, useEffect} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {Disposable} from 'event-kit';

import {RefHolderPropType} from '../prop-types';
import {useAtomEnv} from '../context/atom';
import {createItem} from '../helpers';

const VERBATIM_OPTION_PROPS = [
Expand All @@ -12,125 +13,87 @@ const VERBATIM_OPTION_PROPS = [

const OPTION_PROPS = [
...VERBATIM_OPTION_PROPS,
'tooltips', 'className', 'showDelay', 'hideDelay',
'className', 'showDelay', 'hideDelay',
];

export default class Tooltip extends React.Component {
static propTypes = {
manager: PropTypes.object.isRequired,
target: RefHolderPropType.isRequired,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
]),
html: PropTypes.bool,
className: PropTypes.string,
placement: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
]),
trigger: PropTypes.oneOf(['hover', 'click', 'focus', 'manual']),
showDelay: PropTypes.number,
hideDelay: PropTypes.number,
keyBindingCommand: PropTypes.string,
keyBindingTarget: PropTypes.element,
children: PropTypes.element,
itemHolder: RefHolderPropType,
tooltipHolder: RefHolderPropType,
}

static defaultProps = {
getItemComponent: () => {},
}

constructor(props, context) {
super(props, context);

this.refSub = new Disposable();
this.tipSub = new Disposable();

this.domNode = null;
if (this.props.children !== undefined) {
this.domNode = document.createElement('div');
this.domNode.className = 'react-atom-tooltip';
}

this.lastTooltipProps = {};
}

componentDidMount() {
this.setupTooltip();
}

render() {
if (this.props.children !== undefined) {
return ReactDOM.createPortal(
this.props.children,
this.domNode,
);
} else {
return null;
}
}
export default function Tooltip(props) {
const atomEnv = useAtomEnv();

componentDidUpdate() {
if (this.shouldRecreateTooltip()) {
this.refSub.dispose();
this.tipSub.dispose();
this.setupTooltip();
}
}
const refSub = useRef(new Disposable());
const tipSub = useRef(new Disposable());
const domNode = useRef(null);

componentWillUnmount() {
this.refSub.dispose();
this.tipSub.dispose();
}

getTooltipProps() {
const p = {};
for (const key of OPTION_PROPS) {
p[key] = this.props[key];
useEffect(() => {
if (props.children !== undefined) {
domNode.current = document.createElement('div');
domNode.current.className = 'react-atom-tooltip';
}
return p;
}

shouldRecreateTooltip() {
return OPTION_PROPS.some(key => this.lastTooltipProps[key] !== this.props[key]);
}

setupTooltip() {
this.lastTooltipProps = this.getTooltipProps();
}, []);

useEffect(() => {
const options = {};
VERBATIM_OPTION_PROPS.forEach(key => {
if (this.props[key] !== undefined) {
options[key] = this.props[key];
if (props[key] !== undefined) {
options[key] = props[key];
}
});
if (this.props.className !== undefined) {
options.class = this.props.className;
if (props.className !== undefined) {
options.class = props.className;
}
if (this.props.showDelay !== undefined || this.props.hideDelay !== undefined) {
const delayDefaults = (this.props.trigger === 'hover' || this.props.trigger === undefined)
if (props.showDelay !== undefined || props.hideDelay !== undefined) {
const delayDefaults = (props.trigger === 'hover' || props.trigger === undefined)
&& {show: 1000, hide: 100}
|| {show: 0, hide: 0};

options.delay = {
show: this.props.showDelay !== undefined ? this.props.showDelay : delayDefaults.show,
hide: this.props.hideDelay !== undefined ? this.props.hideDelay : delayDefaults.hide,
show: props.showDelay !== undefined ? props.showDelay : delayDefaults.show,
hide: props.hideDelay !== undefined ? props.hideDelay : delayDefaults.hide,
};
}
if (this.props.children !== undefined) {
options.item = createItem(this.domNode, this.props.itemHolder);
if (props.children !== undefined) {
options.item = createItem(domNode.current, props.itemHolder);
}

this.refSub = this.props.target.observe(t => {
this.tipSub.dispose();
this.tipSub = this.props.manager.add(t, options);
const h = this.props.tooltipHolder;
refSub.current = props.target.observe(t => {
tipSub.current.dispose();
tipSub.current = atomEnv.tooltips.add(t, options);
const h = props.tooltipHolder;
if (h) {
h.setter(this.tipSub);
h.setter(tipSub.current);
}
});

return () => {
refSub.current.dispose();
tipSub.current.dispose();
};
}, OPTION_PROPS.map(name => props[name]));

if (props.children !== undefined) {
return ReactDOM.createPortal(props.children, domNode.current);
}

return null;
}

Tooltip.propTypes = {
target: RefHolderPropType.isRequired,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
]),
html: PropTypes.bool,
className: PropTypes.string,
placement: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
]),
trigger: PropTypes.oneOf(['hover', 'click', 'focus', 'manual']),
showDelay: PropTypes.number,
hideDelay: PropTypes.number,
keyBindingCommand: PropTypes.string,
keyBindingTarget: PropTypes.element,
children: PropTypes.element,
itemHolder: RefHolderPropType,
tooltipHolder: RefHolderPropType,
};
11 changes: 11 additions & 0 deletions lib/context/atom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React, {useContext} from 'react';

export const AtomContext = React.createContext(null);

export function useAtomEnv() {
const atomEnv = useContext(AtomContext);
if (atomEnv === null) {
throw new Error('AtomContext is required');
}
return atomEnv;
}
23 changes: 23 additions & 0 deletions lib/context/workdir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, {useContext} from 'react';

import WorkdirContext from '../models/workdir-context';
import WorkdirContextPool from '../models/workdir-context-pool';

export const WorkdirPoolContext = React.createContext(new WorkdirContextPool());

export const ActiveWorkdirContext = React.createContext(null);

export function useWorkdir() {
const maybeWorkdir = useContext(ActiveWorkdirContext);
return maybeWorkdir || WorkdirContext.absent();
}

export function useRepository() {
const workdir = useWorkdir();
return workdir.getRepository();
}

export function useResolutionProgress() {
const workdir = useWorkdir();
return workdir.getResolutionProgress();
}
Loading