import * as React from 'react';
import {
Fragment,
useCallback,
useContext,
useLayoutEffect,
useReducer,
useRef,
useState,
} from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import ElementBadges from './ElementBadges';
import {OwnersListContext, useChangeOwnerAction} from './OwnersListContext';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {useIsOverflowing} from '../hooks';
import {StoreContext} from '../context';
import Tooltip from '../Components/reach-ui/tooltip';
import {
Menu,
MenuList,
MenuButton,
MenuItem,
} from '../Components/reach-ui/menu-button';
import type {SerializedElement} from 'react-devtools-shared/src/frontend/types';
import styles from './OwnersStack.css';
type SelectOwner = (owner: SerializedElement | null) => void;
type ACTION_UPDATE_OWNER_ID = {
type: 'UPDATE_OWNER_ID',
ownerID: number | null,
owners: Array<SerializedElement>,
};
type ACTION_UPDATE_SELECTED_INDEX = {
type: 'UPDATE_SELECTED_INDEX',
selectedIndex: number,
};
type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
type State = {
ownerID: number | null,
owners: Array<SerializedElement>,
selectedIndex: number,
};
function dialogReducer(state: State, action: Action) {
switch (action.type) {
case 'UPDATE_OWNER_ID':
const selectedIndex = action.owners.findIndex(
owner => owner.id === action.ownerID,
);
return {
ownerID: action.ownerID,
owners: action.owners,
selectedIndex,
};
case 'UPDATE_SELECTED_INDEX':
return {
...state,
selectedIndex: action.selectedIndex,
};
default:
throw new Error(`Invalid action "${action.type}"`);
}
}
type OwnerStackFlatListProps = {
owners: Array<SerializedElement>,
selectedIndex: number,
selectOwner: SelectOwner,
setElementsTotalWidth: (width: number) => void,
};
function OwnerStackFlatList({
owners,
selectedIndex,
selectOwner,
setElementsTotalWidth,
}: OwnerStackFlatListProps): React.Node {
const containerRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const container = containerRef.current;
if (container === null) {
return;
}
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
setElementsTotalWidth(entry.contentRect.width);
});
observer.observe(container);
return observer.disconnect.bind(observer);
}, []);
return (
<div className={styles.OwnerStackFlatListContainer} ref={containerRef}>
{owners.map((owner, index) => (
<Fragment key={index}>
<ElementView
owner={owner}
isSelected={index === selectedIndex}
selectOwner={selectOwner}
/>
{index < owners.length - 1 && (
<span className={styles.OwnerStackFlatListSeparator}>ยป</span>
)}
</Fragment>
))}
</div>
);
}
export default function OwnerStack(): React.Node {
const read = useContext(OwnersListContext);
const {ownerID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
const changeOwnerAction = useChangeOwnerAction();
const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
ownerID: null,
owners: [],
selectedIndex: 0,
});
if (ownerID === null) {
dispatch({
type: 'UPDATE_OWNER_ID',
ownerID: null,
owners: [],
});
} else if (ownerID !== state.ownerID) {
const isInStore =
state.owners.findIndex(owner => owner.id === ownerID) >= 0;
dispatch({
type: 'UPDATE_OWNER_ID',
ownerID,
owners: isInStore ? state.owners : read(ownerID) || [],
});
}
const {owners, selectedIndex} = state;
const selectOwner = useCallback<SelectOwner>(
(owner: SerializedElement | null) => {
if (owner !== null) {
const index = owners.indexOf(owner);
dispatch({
type: 'UPDATE_SELECTED_INDEX',
selectedIndex: index >= 0 ? index : 0,
});
changeOwnerAction(owner.id);
} else {
dispatch({
type: 'UPDATE_SELECTED_INDEX',
selectedIndex: 0,
});
treeDispatch({type: 'RESET_OWNER_STACK'});
}
},
[owners, treeDispatch],
);
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
const elementsBarRef = useRef<HTMLDivElement | null>(null);
const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
const selectedOwner = owners[selectedIndex];
return (
<div className={styles.OwnerStack}>
<div className={styles.Bar} ref={elementsBarRef}>
{isOverflowing ? (
<Fragment>
<ElementsDropdown
owners={owners}
selectedIndex={selectedIndex}
selectOwner={selectOwner}
/>
<BackToOwnerButton
owners={owners}
selectedIndex={selectedIndex}
selectOwner={selectOwner}
/>
{selectedOwner != null && (
<ElementView
owner={selectedOwner}
isSelected={true}
selectOwner={selectOwner}
/>
)}
</Fragment>
) : (
<OwnerStackFlatList
owners={owners}
selectedIndex={selectedIndex}
selectOwner={selectOwner}
setElementsTotalWidth={setElementsTotalWidth}
/>
)}
</div>
<div className={styles.VRule} />
<Button onClick={() => selectOwner(null)} title="Back to tree view">
<ButtonIcon type="close" />
</Button>
</div>
);
}
type ElementsDropdownProps = {
owners: Array<SerializedElement>,
selectedIndex: number,
selectOwner: SelectOwner,
};
function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
const store = useContext(StoreContext);
const menuItems = [];
for (let index = owners.length - 1; index >= 0; index--) {
const owner = owners[index];
const isInStore = store.containsElement(owner.id);
menuItems.push(
<MenuItem
key={owner.id}
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
onSelect={() => (isInStore ? selectOwner(owner) : null)}>
{owner.displayName}
<ElementBadges
hocDisplayNames={owner.hocDisplayNames}
environmentName={owner.env}
compiledWithForget={owner.compiledWithForget}
className={styles.BadgesBlock}
/>
</MenuItem>,
);
}
return (
<Menu>
<MenuButton className={styles.MenuButton}>
<Tooltip label="Open elements dropdown">
<span className={styles.MenuButtonContent} tabIndex={-1}>
<ButtonIcon type="more" />
</span>
</Tooltip>
</MenuButton>
<MenuList className={styles.Modal}>{menuItems}</MenuList>
</Menu>
);
}
type ElementViewProps = {
isSelected: boolean,
owner: SerializedElement,
selectOwner: SelectOwner,
...
};
function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
const store = useContext(StoreContext);
const {displayName, hocDisplayNames, compiledWithForget} = owner;
const isInStore = store.containsElement(owner.id);
const handleChange = useCallback(() => {
if (isInStore) {
selectOwner(owner);
}
}, [isInStore, selectOwner, owner]);
return (
<Toggle
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
isChecked={isSelected}
onChange={handleChange}>
{displayName}
<ElementBadges
hocDisplayNames={hocDisplayNames}
environmentName={owner.env}
compiledWithForget={compiledWithForget}
className={styles.BadgesBlock}
/>
</Toggle>
);
}
type BackToOwnerButtonProps = {
owners: Array<SerializedElement>,
selectedIndex: number,
selectOwner: SelectOwner,
};
function BackToOwnerButton({
owners,
selectedIndex,
selectOwner,
}: BackToOwnerButtonProps) {
const store = useContext(StoreContext);
if (selectedIndex <= 0) {
return null;
}
const owner = owners[selectedIndex - 1];
const isInStore = store.containsElement(owner.id);
return (
<Button
className={isInStore ? undefined : styles.NotInStore}
onClick={() => (isInStore ? selectOwner(owner) : null)}
title={`Up to ${owner.displayName || 'owner'}`}>
<ButtonIcon type="previous" />
</Button>
);
}