Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… into feature/retrieve-colors
  • Loading branch information
interim17 committed Nov 23, 2024
2 parents 7ed1d8c + 5c070a9 commit ebca78d
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 46 deletions.
107 changes: 105 additions & 2 deletions src/components/CustomDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React, { ReactNode } from "react";
import React, {
KeyboardEventHandler,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { Dropdown, DropDownProps, MenuProps } from "antd";
import { ButtonClass } from "../../constants/interfaces";
import { ButtonClass, DropdownState } from "../../constants/interfaces";
import { DROPDOWN_HOVER_DELAY } from "../../constants";
import NavButton from "../NavButton";

import styles from "./style.css";
Expand All @@ -22,16 +29,112 @@ const CustomDropdown: React.FC<CustomDropdownProps> = ({
placement,
disabled,
}) => {
const [dropdownState, setDropdownState] = useState<DropdownState>(
DropdownState.CLOSED
);

const triggerRef = useRef<HTMLElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);

/**
* Prevents the menu wrapper from capturing focus,
* this prevents losing focus to the body
* when "Escape" is pressed.
*/
useEffect(() => {
const element = dropdownRef.current;
if (element) {
const menuElement = element.querySelector(
".ant-dropdown-menu"
) as HTMLElement;
if (menuElement) {
if (dropdownState === DropdownState.FORCED_OPEN) {
menuElement.setAttribute("tabIndex", "-1");
} else if (dropdownState === DropdownState.CLOSED) {
menuElement.setAttribute("tabIndex", "0");
}
}
}
}, [dropdownRef, dropdownState]);

/**
* Manually handling keydown and hover behavior because
* our focus management overrides the defaults of the antd components.
*/
const openTriggers = new Set(["Enter", " ", "ArrowDown"]);

const handleKeyDown: KeyboardEventHandler<any> = (event) => {
if (event.key === "Escape") {
event.preventDefault();
setDropdownState(DropdownState.CLOSED);
triggerRef.current?.focus();
}
if (
openTriggers.has(event.key) &&
dropdownState !== DropdownState.FORCED_OPEN
) {
event.preventDefault();
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
setDropdownState(DropdownState.FORCED_OPEN); // Opened by keyboard
}
};

const handleMouseEnter = () => {
if (dropdownState === DropdownState.CLOSED) {
setDropdownState(DropdownState.OPEN);
}
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
};

const handleMouseLeaveWithDelay = () => {
if (dropdownState !== DropdownState.FORCED_OPEN) {
closeTimeoutRef.current = setTimeout(() => {
setDropdownState(DropdownState.CLOSED);
}, DROPDOWN_HOVER_DELAY);
}
};

const buttonClickHandler = () => {
setDropdownState(
dropdownState === DropdownState.CLOSED
? DropdownState.OPEN
: DropdownState.CLOSED
);
};

return (
<Dropdown
menu={{ items, theme: "dark", className: styles.menu }}
placement={placement}
disabled={disabled}
trigger={["click"]}
open={dropdownState !== DropdownState.CLOSED}
dropdownRender={(menu) => (
<div
ref={dropdownRef}
tabIndex={-1}
onKeyDown={handleKeyDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeaveWithDelay}
>
{menu}
</div>
)}
>
<NavButton
ref={triggerRef as React.RefObject<HTMLButtonElement>}
titleText={titleText}
icon={icon}
buttonType={buttonType}
clickHandler={buttonClickHandler}
onKeyDown={handleKeyDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeaveWithDelay}
/>
</Dropdown>
);
Expand Down
62 changes: 34 additions & 28 deletions src/components/NavButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { ReactNode } from "react";
import React, { ReactNode, forwardRef } from "react";
import { Button, ButtonProps } from "antd";
import classNames from "classnames";

import styles from "./style.css";
import { ButtonClass } from "../../constants/interfaces";

Expand All @@ -13,32 +12,39 @@ export interface NavButtonProps extends ButtonProps {
isDisabled?: boolean;
}

const NavButton: React.FC<NavButtonProps> = ({
className,
titleText,
buttonType = ButtonClass.Action,
icon,
clickHandler,
isDisabled,
...props
}) => {
// NavButtons default to action button styling, provide secondary or primary to override
const buttonClassNames = classNames(
className,
styles.navButton,
styles[buttonType],
{ [styles.disabled]: isDisabled }
);
const NavButton = forwardRef<HTMLButtonElement, NavButtonProps>(
(
{
className,
titleText,
buttonType = ButtonClass.Action,
icon,
clickHandler,
isDisabled,
...props
},
ref
) => {
const buttonClassNames = classNames(
className,
styles.navButton,
styles[buttonType],
{ [styles.disabled]: isDisabled }
);

return (
<Button
{...props}
ref={ref}
className={buttonClassNames}
onClick={!isDisabled ? clickHandler : undefined}
>
{titleText} {icon}
</Button>
);
}
);

return (
<Button
{...props}
className={buttonClassNames}
onClick={!isDisabled ? clickHandler : undefined}
>
{titleText} {icon}
</Button>
);
};
NavButton.displayName = "NavButton";

export default NavButton;
10 changes: 7 additions & 3 deletions src/components/RecordMoviesComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ const RecordMovieComponent = (props: RecordMovieComponentProps) => {
* In this icon we are stacking glyphs to create multicolor icons via icomoon
*/
const startRecordingIcon = (
<span className={styles.iconContainer}>
<div className={styles.iconContainer}>
<span
className={classNames("icon-moon", "record-icon-circle")}
></span>
<span
className={classNames("icon-moon", "record-icon-ring")}
></span>
</span>
</div>
);

const activeRecordingIcon = (
Expand All @@ -81,7 +81,11 @@ const RecordMovieComponent = (props: RecordMovieComponentProps) => {
if (!isRecording) {
return startRecordingIcon;
} else if (isHovering) {
return "stop-record-icon";
return classNames(
styles.iconContainer,
"icon-moon",
"stop-record-icon"
);
} else return activeRecordingIcon;
};

Expand Down
26 changes: 14 additions & 12 deletions src/components/RecordMoviesComponent/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
}

.icon-container {
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
Expand All @@ -22,18 +24,18 @@
}

@keyframes pulse-red {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 #FF5252B3;
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px #FF525200;
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 #FF525200;
}
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 #ff5252b3;
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px #ff525200;
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 #ff525200;
}
}

.status-container {
Expand Down
1 change: 0 additions & 1 deletion src/components/SideBarContents/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}

.container label {
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ export const MAX_CONVERSION_FILE_SIZE = 2e8; // 200 MB
export const CONTROLS_MIN_WIDTH = 650;
export const CONTROLS_MIN_HEIGHT = 320;
export const SCALE_BAR_MIN_WIDTH = 550;
export const DROPDOWN_HOVER_DELAY = 300;
6 changes: 6 additions & 0 deletions src/constants/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ export interface ColorChange {
agent: SelectionEntry;
color: string;
}

export enum DropdownState {
OPEN = "open",
CLOSED = "closed",
FORCED_OPEN = "forced_open",
}
7 changes: 7 additions & 0 deletions src/state/compoundSelectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import {
} from "../selection/selectors";
import { ColorSetting } from "../selection/types";

/**
* compoundSelectors are selectors that consume state from multiple branches
* of state, so don't belong in a particular branch's selectors file,
* and are consumed by multiple containers, and so don't belong in a particular
* container's selectors file.
*/

export const getCurrentUIData = createSelector(
[getCurrentColorSetting, getSelectedUIDisplayData, getDefaultUIDisplayData],
(
Expand Down
8 changes: 8 additions & 0 deletions src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ export const roundToTimeStepPrecision = (
return Math.round(input * multiplier) / multiplier;
};

/**
Compare two instaces of UIDisplayData to see if they have the same agents
and display states.
This data structure is used to store different color settings.
We don't want to ever try and apply the color settings from one trajectory
to another, even if by chance they shared the same file name, or other
metadata.
*/
export const isSameAgentTree = (a: UIDisplayData, b: UIDisplayData) => {
if (a.length !== b.length) {
return false;
Expand Down

0 comments on commit ebca78d

Please sign in to comment.