Skip to content

Commit

Permalink
feat: control profiles (#173)
Browse files Browse the repository at this point in the history
* feat: controls profile form

- wip control profile form form poc

* feat: wip design

- not sure this is the best application of this style of button list

* feat: show behavior

* feat: edit name

- not sure I like this iteration, but a decent idea

* feat: tentative edit impl

* feat: editable profile name

* feat: remove debug info, fix form submits

* feat: hoist shared icon

* feat: independent profiles with same name

* fix: zooming behavior

* feat: tests
  • Loading branch information
thenick775 authored Sep 21, 2024
1 parent e207feb commit cce3bfa
Show file tree
Hide file tree
Showing 13 changed files with 509 additions and 44 deletions.
46 changes: 41 additions & 5 deletions gbajs3/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gbajs3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@mui/x-tree-view": "^7.0.0",
"@uidotdev/usehooks": "^2.4.1",
"jwt-decode": "^4.0.0",
"nanoid": "^5.0.7",
"react": "^18.2.0",
"react-animate-height": "^3.2.2",
"react-dom": "^18.2.0",
Expand Down
1 change: 1 addition & 0 deletions gbajs3/src/components/controls/consts.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const saveStateSlotLocalStorageKey = 'currentSaveStateSlot';
export const virtualControlsLocalStorageKey = 'areVirtualControlsEnabled';
export const virtualControlProfilesLocalStorageKey = 'virtualControlProfiles';
1 change: 1 addition & 0 deletions gbajs3/src/components/controls/control-panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ describe('<ControlPanel />', () => {
const testLayout = {
clearLayouts: vi.fn(),
setLayout: setLayoutSpy,
setLayouts: vi.fn(),
hasSetLayout: true,
layouts: { screen: { initialBounds: new DOMRect() } }
};
Expand Down
7 changes: 1 addition & 6 deletions gbajs3/src/components/modals/cheats.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Button, IconButton, TextField, useMediaQuery } from '@mui/material';
import { useCallback, useId, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { BiPlus } from 'react-icons/bi';
import { CiSquareRemove } from 'react-icons/ci';
import { styled, useTheme } from 'styled-components';

Expand All @@ -15,6 +14,7 @@ import {
} from '../product-tour/embedded-product-tour.tsx';
import { CircleCheckButton } from '../shared/circle-check-button.tsx';
import { ManagedCheckbox } from '../shared/managed-checkbox.tsx';
import { StyledBiPlus } from '../shared/styled.tsx';

type OptionallyHiddenProps = {
$shouldHide: boolean;
Expand Down Expand Up @@ -54,11 +54,6 @@ const StyledCiSquareRemove = styled(CiSquareRemove)`
min-width: 40px;
`;

const StyledBiPlus = styled(BiPlus)`
width: 25px;
height: 25px;
`;

const CheatsFormSeparator = styled.div<CheatsFormSeparatorProps>`
display: flex;
flex-direction: column;
Expand Down
34 changes: 31 additions & 3 deletions gbajs3/src/components/modals/controls.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('<ControlsModal />', () => {
expect(
screen.getByRole('tab', { name: 'Virtual Controls', selected: true })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Profiles', selected: false })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Key Bindings', selected: false })
).toBeVisible();
Expand All @@ -27,12 +30,34 @@ describe('<ControlsModal />', () => {
screen.getByRole('button', { name: 'Save Changes' }).getAttribute('form')
);

// select control profiles
await userEvent.click(screen.getByRole('tab', { name: 'Profiles' }));

expect(
screen.getByRole('tab', { name: 'Profiles', selected: true })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Key Bindings', selected: false })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Virtual Controls', selected: false })
).toBeVisible();

expect(screen.getByRole('list', { name: 'Profiles List' })).toBeVisible();
// note: save changes button is not shown on control profiles tab
expect(
screen.queryByRole('button', { name: 'Save Changes' })
).not.toBeInTheDocument();

// select key bindings form
await userEvent.click(screen.getByRole('tab', { name: 'Key Bindings' }));

expect(
screen.getByRole('tab', { name: 'Key Bindings', selected: true })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Profiles', selected: false })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Virtual Controls', selected: false })
).toBeVisible();
Expand All @@ -53,15 +78,18 @@ describe('<ControlsModal />', () => {
expect(
screen.getByRole('tab', { name: 'Virtual Controls', selected: true })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Profiles', selected: false })
).toBeVisible();
expect(
screen.getByRole('tab', { name: 'Key Bindings', selected: false })
).toBeVisible();

const virtualControlsFormRenavigate = screen.getByRole('form', {
const virtualControlsFormReNavigate = screen.getByRole('form', {
name: 'Virtual Controls Form'
});
expect(virtualControlsFormRenavigate).toBeVisible();
expect(virtualControlsFormRenavigate.id).toEqual(
expect(virtualControlsFormReNavigate).toBeVisible();
expect(virtualControlsFormReNavigate.id).toEqual(
screen.getByRole('button', { name: 'Save Changes' }).getAttribute('form')
);
});
Expand Down
51 changes: 40 additions & 11 deletions gbajs3/src/components/modals/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type TourSteps
} from '../product-tour/embedded-product-tour.tsx';
import { CircleCheckButton } from '../shared/circle-check-button.tsx';
import { ControlProfiles } from './controls/control-profiles.tsx';

type TabPanelProps = {
children: ReactNode;
Expand All @@ -23,6 +24,7 @@ type TabPanelProps = {
type ControlTabsProps = {
setFormId: Dispatch<React.SetStateAction<string>>;
virtualControlsFormId: string;
controlProfilesFormId: string;
keyBindingsFormId: string;
resetPositionsButtonId: string;
setIsSuccessfulSubmit: (successfulSubmit: boolean) => void;
Expand All @@ -31,6 +33,10 @@ type ControlTabsProps = {
const TabsWithBorder = styled(Tabs)`
border-bottom: 1px solid;
border-color: rgba(0, 0, 0, 0.12);
& .MuiTabs-scrollButtons {
width: fit-content;
}
`;

const TabWrapper = styled.div`
Expand Down Expand Up @@ -60,16 +66,30 @@ const TabPanel = ({ children, index, value }: TabPanelProps) => {
const ControlTabs = ({
setFormId,
virtualControlsFormId,
controlProfilesFormId,
keyBindingsFormId,
resetPositionsButtonId,
setIsSuccessfulSubmit
}: ControlTabsProps) => {
const { clearLayouts } = useLayoutContext();
const [value, setValue] = useState(0);

const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
setFormId(newValue === 0 ? virtualControlsFormId : keyBindingsFormId);
const tabIndexToFormId = (tabIndex: number) => {
switch (tabIndex) {
case 0:
return virtualControlsFormId;
case 1:
return controlProfilesFormId;
case 2:
return keyBindingsFormId;
default:
return virtualControlsFormId;
}
};

const handleTabChange = (_: React.SyntheticEvent, tabIndex: number) => {
setValue(tabIndex);
setFormId(tabIndexToFormId(tabIndex));
setIsSuccessfulSubmit(false);
};

Expand All @@ -78,12 +98,15 @@ const ControlTabs = ({
return (
<>
<TabsWithBorder
variant="scrollable"
value={value}
onChange={handleTabChange}
aria-label="Control tabs"
allowScrollButtonsMobile
>
<Tab label="Virtual Controls" {...a11yProps(0)} />
<Tab label="Key Bindings" {...a11yProps(1)} />
<Tab label="Profiles" {...a11yProps(1)} />
<Tab label="Key Bindings" {...a11yProps(2)} />
</TabsWithBorder>
<TabPanel value={value} index={0}>
<VirtualControlsForm
Expand All @@ -99,6 +122,9 @@ const ControlTabs = ({
</Button>
</TabPanel>
<TabPanel value={value} index={1}>
<ControlProfiles id={controlProfilesFormId} />
</TabPanel>
<TabPanel value={value} index={2}>
<KeyBindingsForm id={keyBindingsFormId} onAfterSubmit={onAfterSubmit} />
</TabPanel>
</>
Expand Down Expand Up @@ -175,19 +201,22 @@ export const ControlsModal = () => {
<ControlTabs
setFormId={setFormId}
virtualControlsFormId={`${baseId}--virtual-controls-form`}
controlProfilesFormId={`${baseId}--control-profiles`}
keyBindingsFormId={`${baseId}--key-bindings-form`}
resetPositionsButtonId={`${baseId}--reset-positions-button`}
setIsSuccessfulSubmit={setIsSuccessfulSubmit}
/>
</ModalBody>
<ModalFooter>
<CircleCheckButton
copy="Save Changes"
form={formId}
id={`${baseId}--save-changes-button`}
type="submit"
showSuccess={isSuccessfulSubmit}
/>
{formId !== `${baseId}--control-profiles` && (
<CircleCheckButton
copy="Save Changes"
form={formId}
id={`${baseId}--save-changes-button`}
type="submit"
showSuccess={isSuccessfulSubmit}
/>
)}
<Button variant="outlined" onClick={() => setIsModalOpen(false)}>
Close
</Button>
Expand Down
Loading

0 comments on commit cce3bfa

Please sign in to comment.