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

[MIRROR] Fixes tgui alert buttons #2950

Merged
merged 1 commit into from
Apr 19, 2024
Merged
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
30 changes: 21 additions & 9 deletions tgui/packages/tgui/components/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,50 @@ import {
} from './Flex';

type Props = Partial<{
vertical: boolean;
/** Fills available space. */
fill: boolean;
/** Reverses the stack. */
reverse: boolean;
/** Flex column */
vertical: boolean;
/** Adds zebra striping to the stack. */
zebra: boolean;
}> &
FlexProps;

export const Stack = (props: Props) => {
const { className, vertical, fill, zebra, ...rest } = props;
export function Stack(props: Props) {
const { className, vertical, fill, reverse, zebra, ...rest } = props;

const directionPrefix = vertical ? 'column' : 'row';
const directionSuffix = reverse ? '-reverse' : '';

return (
<div
className={classes([
'Stack',
fill && 'Stack--fill',
vertical ? 'Stack--vertical' : 'Stack--horizontal',
zebra && 'Stack--zebra',
reverse && `Stack--reverse${vertical ? '--vertical' : ''}`,
className,
computeFlexClassName(props),
])}
{...computeFlexProps({
direction: vertical ? 'column' : 'row',
direction: `${directionPrefix}${directionSuffix}`,
...rest,
})}
/>
);
};
}

type StackItemProps = FlexItemProps &
Partial<{
innerRef: RefObject<HTMLDivElement>;
}>;

const StackItem = (props: StackItemProps) => {
function StackItem(props: StackItemProps) {
const { className, innerRef, ...rest } = props;

return (
<div
className={classes([
Expand All @@ -61,7 +72,7 @@ const StackItem = (props: StackItemProps) => {
{...computeFlexItemProps(rest)}
/>
);
};
}

Stack.Item = StackItem;

Expand All @@ -70,8 +81,9 @@ type StackDividerProps = FlexItemProps &
hidden: boolean;
}>;

const StackDivider = (props: StackDividerProps) => {
function StackDivider(props: StackDividerProps) {
const { className, hidden, ...rest } = props;

return (
<div
className={classes([
Expand All @@ -84,6 +96,6 @@ const StackDivider = (props: StackDividerProps) => {
{...computeFlexItemProps(rest)}
/>
);
};
}

Stack.Divider = StackDivider;
224 changes: 120 additions & 104 deletions tgui/packages/tgui/interfaces/AlertModal.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import { useState } from 'react';

import {
KEY_ENTER,
KEY_ESCAPE,
KEY_LEFT,
KEY_RIGHT,
KEY_SPACE,
KEY_TAB,
} from '../../common/keycodes';
import { KEY } from 'common/keys';
import { BooleanLike } from 'common/react';
import { KeyboardEvent, useState } from 'react';

import { useBackend } from '../backend';
import { Autofocus, Box, Button, Flex, Section, Stack } from '../components';
import { Autofocus, Box, Button, Section, Stack } from '../components';
import { Window } from '../layouts';
import { Loader } from './common/Loader';

type AlertModalData = {
autofocus: boolean;
type Data = {
autofocus: BooleanLike;
buttons: string[];
large_buttons: boolean;
large_buttons: BooleanLike;
message: string;
swapped_buttons: boolean;
swapped_buttons: BooleanLike;
timeout: number;
title: string;
};

const KEY_DECREMENT = -1;
const KEY_INCREMENT = 1;
enum DIRECTION {
Increment = 1,
Decrement = -1,
}

export const AlertModal = (props) => {
const { act, data } = useBackend<AlertModalData>();
export function AlertModal(props) {
const { act, data } = useBackend<Data>();
const {
autofocus,
buttons = [],
Expand All @@ -36,128 +32,148 @@ export const AlertModal = (props) => {
timeout,
title,
} = data;

const [selected, setSelected] = useState(0);

// At least one of the buttons has a long text message
const isVerbose = buttons.some((button) => button.length > 10);
const largeSpacing = isVerbose && large_buttons ? 20 : 15;

// Dynamically sets window dimensions
const windowHeight =
115 +
120 +
(isVerbose ? largeSpacing * buttons.length : 0) +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
(message.length && large_buttons ? 5 : 0);
const windowWidth = 325 + (buttons.length > 2 ? 55 : 0);
const onKey = (direction: number) => {
if (selected === 0 && direction === KEY_DECREMENT) {
setSelected(buttons.length - 1);
} else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) {
setSelected(0);
} else {
setSelected(selected + direction);

const windowWidth = 345 + (buttons.length > 2 ? 55 : 0);

/** Changes button selection, etc */
function keyDownHandler(event: KeyboardEvent<HTMLDivElement>) {
switch (event.key) {
case KEY.Space:
case KEY.Enter:
act('choose', { choice: buttons[selected] });
return;
case KEY.Escape:
act('cancel');
return;
case KEY.Left:
event.preventDefault();
onKey(DIRECTION.Decrement);
return;
case KEY.Tab:
case KEY.Right:
event.preventDefault();
onKey(DIRECTION.Increment);
return;
}
};
}

/** Manages iterating through the buttons */
function onKey(direction: DIRECTION) {
const newIndex = (selected + direction + buttons.length) % buttons.length;
setSelected(newIndex);
}

return (
<Window height={windowHeight} title={title} width={windowWidth}>
{!!timeout && <Loader value={timeout} />}
<Window.Content
onKeyDown={(e) => {
const keyCode = window.event ? e.which : e.keyCode;
/**
* Simulate a click when pressing space or enter,
* allow keyboard navigation, override tab behavior
*/
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
act('choose', { choice: buttons[selected] });
} else if (keyCode === KEY_ESCAPE) {
act('cancel');
} else if (keyCode === KEY_LEFT) {
e.preventDefault();
onKey(KEY_DECREMENT);
} else if (keyCode === KEY_TAB || keyCode === KEY_RIGHT) {
e.preventDefault();
onKey(KEY_INCREMENT);
}
}}
>
<Window.Content onKeyDown={keyDownHandler}>
<Section fill>
<Stack fill vertical>
<Stack.Item grow m={1}>
<Stack.Item m={1}>
<Box color="label" overflow="hidden">
{message}
</Box>
</Stack.Item>
<Stack.Item>
<Stack.Item grow>
{!!autofocus && <Autofocus />}
<ButtonDisplay selected={selected} />
{isVerbose ? (
<VerticalButtons selected={selected} />
) : (
<HorizontalButtons selected={selected} />
)}
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
}

type ButtonDisplayProps = {
selected: number;
};

/**
* Displays a list of buttons ordered by user prefs.
* Technically this handles more than 2 buttons, but you
* should just be using a list input in that case.
*/
const ButtonDisplay = (props) => {
const { data } = useBackend<AlertModalData>();
function HorizontalButtons(props: ButtonDisplayProps) {
const { act, data } = useBackend<Data>();
const { buttons = [], large_buttons, swapped_buttons } = data;
const { selected } = props;

return (
<Flex
align="center"
direction={!swapped_buttons ? 'row-reverse' : 'row'}
fill
justify="space-around"
wrap
>
{buttons?.map((button, index) =>
!!large_buttons && buttons.length < 3 ? (
<Flex.Item grow key={index}>
<AlertButton
button={button}
id={index.toString()}
selected={selected === index}
/>
</Flex.Item>
) : (
<Flex.Item key={index}>
<AlertButton
button={button}
id={index.toString()}
selected={selected === index}
/>
</Flex.Item>
),
)}
</Flex>
<Stack fill justify="space-around" reverse={!swapped_buttons}>
{buttons.map((button, index) => (
<Stack.Item grow={large_buttons ? 1 : undefined} key={index}>
<Button
fluid={!!large_buttons}
minWidth={5}
onClick={() => act('choose', { choice: button })}
overflowX="hidden"
px={2}
py={large_buttons ? 0.5 : 0}
selected={selected === index}
textAlign="center"
>
{!large_buttons ? button : button.toUpperCase()}
</Button>
</Stack.Item>
))}
</Stack>
);
};
}

/**
* Displays a button with variable sizing.
* Technically the parent handles more than 2 buttons, but you
* should just be using a list input in that case.
*/
const AlertButton = (props) => {
const { act, data } = useBackend<AlertModalData>();
const { large_buttons } = data;
const { button, selected } = props;
const buttonWidth = button.length > 7 ? button.length : 7;
function VerticalButtons(props: ButtonDisplayProps) {
const { act, data } = useBackend<Data>();
const { buttons = [], large_buttons, swapped_buttons } = data;
const { selected } = props;

return (
<Button
fluid={!!large_buttons}
height={!!large_buttons && 2}
onClick={() => act('choose', { choice: button })}
m={0.5}
pl={2}
pr={2}
pt={large_buttons ? 0.33 : 0}
selected={selected}
textAlign="center"
width={!large_buttons && buttonWidth}
<Stack
align="center"
fill
justify="space-around"
reverse={!swapped_buttons}
vertical
>
{!large_buttons ? button : button.toUpperCase()}
</Button>
{buttons.map((button, index) => (
<Stack.Item
grow
width={large_buttons ? '100%' : undefined}
key={index}
m={0}
>
<Button
fluid
minWidth={20}
onClick={() => act('choose', { choice: button })}
overflowX="hidden"
px={2}
py={large_buttons ? 0.5 : 0}
selected={selected === index}
textAlign="center"
>
{!large_buttons ? button : button.toUpperCase()}
</Button>
</Stack.Item>
))}
</Stack>
);
};
}
18 changes: 18 additions & 0 deletions tgui/packages/tgui/styles/components/Stack.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ $zebra-background-color: base.$color-bg-section !default;
}
}

.Stack--reverse > .Stack__item {
margin-left: 0;
margin-right: 0.5em;

&:first-child {
margin-right: 0;
}
}

.Stack--reverse--vertical > .Stack__item {
margin-top: 0;
margin-bottom: 0.5em;

&:first-child {
margin-bottom: 0;
}
}

.Stack--zebra > .Stack__item:nth-child(even) {
background-color: $zebra-background-color;
}
Expand Down
Loading