Skip to content
Open
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
65 changes: 63 additions & 2 deletions apps/examples/ui-test-app/src/tests/CodeInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { jest } from "@jest/globals";
import { CodeInput } from "@lightsparkdev/ui/components/CodeInput/CodeInput";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render } from "./render";

describe("CodeInput", () => {
beforeEach(() => {
const mockClipboardReadWithoutNumbers = jest.fn(() =>
Promise.resolve("sdkjfnsd"),
);
Object.assign(navigator, {
clipboard: {
// `userEvent.setup()` may install `navigator.clipboard` as a getter-only prop.
// Use `defineProperty` so this mock is resilient regardless of that setup.
Object.defineProperty(navigator, "clipboard", {
value: {
readText: mockClipboardReadWithoutNumbers,
},
configurable: true,
});
});

Expand Down Expand Up @@ -134,4 +138,61 @@ describe("CodeInput", () => {
expect(inputFields[3]).toHaveValue(null);
expect(inputFields[2]).toHaveFocus();
});

it("redirects focus to first empty input when clicking on empty input in unified variant", async () => {
const user = userEvent.setup();
render(<CodeInput codeLength={6} variant="unified" />);
const inputFields = screen.getAllByRole("textbox");
expect(inputFields).toHaveLength(6);

// Enter some digits in the first two positions
fireEvent.keyDown(inputFields[0], { key: "1" });
fireEvent.keyDown(inputFields[1], { key: "2" });

// Now focus should be on the third input (index 2)
expect(inputFields[2]).toHaveFocus();

// Simulate clicking on the 5th input (index 4) - an empty position
// onMouseDown should redirect focus to the first empty input (index 2)
await user.click(inputFields[4]);
expect(inputFields[2]).toHaveFocus();
});

it("allows clicking on any filled input in unified variant", async () => {
const user = userEvent.setup();
render(<CodeInput codeLength={6} variant="unified" />);
const inputFields = screen.getAllByRole("textbox");

// Enter some digits
fireEvent.keyDown(inputFields[0], { key: "1" });
fireEvent.keyDown(inputFields[1], { key: "2" });
fireEvent.keyDown(inputFields[2], { key: "3" });

// Focus should be on the 4th input (index 3)
expect(inputFields[3]).toHaveFocus();

// Clicking on a filled input (index 1) should work normally - no redirect
await user.click(inputFields[1]);
expect(inputFields[1]).toHaveFocus();

// Can also click on index 0
await user.click(inputFields[0]);
expect(inputFields[0]).toHaveFocus();

// Can also click on index 2
await user.click(inputFields[2]);
expect(inputFields[2]).toHaveFocus();
});

it("focuses first input when all inputs are empty in unified variant", () => {
render(<CodeInput codeLength={6} variant="unified" />);
const inputFields = screen.getAllByRole("textbox");

// Blur the auto-focused first input
fireEvent.blur(inputFields[0]);

// Click on a middle input when all are empty
fireEvent.mouseDown(inputFields[3]);
expect(inputFields[0]).toHaveFocus();
});
});
41 changes: 41 additions & 0 deletions packages/core/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const CurrencyUnit = {
XAF: "XAF",
MWK: "MWK",
RWF: "RWF",
ZMW: "ZMW",
AED: "AED",
USDT: "USDT",
USDC: "USDC",

Expand All @@ -58,6 +60,7 @@ export const CurrencyUnit = {
Gbp: "GBP",
Inr: "INR",
Brl: "BRL",
Aed: "AED",
Usdt: "USDT",
Usdc: "USDC",
} as const;
Expand Down Expand Up @@ -110,6 +113,8 @@ const standardUnitConversionObj = {
[CurrencyUnit.XAF]: (v: number) => v,
[CurrencyUnit.MWK]: (v: number) => v,
[CurrencyUnit.RWF]: (v: number) => v,
[CurrencyUnit.ZMW]: (v: number) => v,
[CurrencyUnit.AED]: (v: number) => v,
[CurrencyUnit.USDT]: (v: number) => v,
[CurrencyUnit.USDC]: (v: number) => v,
};
Expand Down Expand Up @@ -161,6 +166,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toBitcoinConversion,
[CurrencyUnit.MWK]: toBitcoinConversion,
[CurrencyUnit.RWF]: toBitcoinConversion,
[CurrencyUnit.ZMW]: toBitcoinConversion,
[CurrencyUnit.AED]: toBitcoinConversion,
[CurrencyUnit.USDT]: toBitcoinConversion,
[CurrencyUnit.USDC]: toBitcoinConversion,
},
Expand Down Expand Up @@ -196,6 +203,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMicrobitcoinConversion,
[CurrencyUnit.MWK]: toMicrobitcoinConversion,
[CurrencyUnit.RWF]: toMicrobitcoinConversion,
[CurrencyUnit.ZMW]: toMicrobitcoinConversion,
[CurrencyUnit.AED]: toMicrobitcoinConversion,
[CurrencyUnit.USDT]: toMicrobitcoinConversion,
[CurrencyUnit.USDC]: toMicrobitcoinConversion,
},
Expand Down Expand Up @@ -231,6 +240,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMillibitcoinConversion,
[CurrencyUnit.MWK]: toMillibitcoinConversion,
[CurrencyUnit.RWF]: toMillibitcoinConversion,
[CurrencyUnit.ZMW]: toMillibitcoinConversion,
[CurrencyUnit.AED]: toMillibitcoinConversion,
[CurrencyUnit.USDT]: toMillibitcoinConversion,
[CurrencyUnit.USDC]: toMillibitcoinConversion,
},
Expand Down Expand Up @@ -266,6 +277,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMillisatoshiConversion,
[CurrencyUnit.MWK]: toMillisatoshiConversion,
[CurrencyUnit.RWF]: toMillisatoshiConversion,
[CurrencyUnit.ZMW]: toMillisatoshiConversion,
[CurrencyUnit.AED]: toMillisatoshiConversion,
[CurrencyUnit.USDT]: toMillisatoshiConversion,
[CurrencyUnit.USDC]: toMillisatoshiConversion,
},
Expand Down Expand Up @@ -301,6 +314,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toNanobitcoinConversion,
[CurrencyUnit.MWK]: toNanobitcoinConversion,
[CurrencyUnit.RWF]: toNanobitcoinConversion,
[CurrencyUnit.ZMW]: toNanobitcoinConversion,
[CurrencyUnit.AED]: toNanobitcoinConversion,
[CurrencyUnit.USDT]: toNanobitcoinConversion,
[CurrencyUnit.USDC]: toNanobitcoinConversion,
},
Expand Down Expand Up @@ -336,6 +351,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toSatoshiConversion,
[CurrencyUnit.MWK]: toSatoshiConversion,
[CurrencyUnit.RWF]: toSatoshiConversion,
[CurrencyUnit.ZMW]: toSatoshiConversion,
[CurrencyUnit.AED]: toSatoshiConversion,
[CurrencyUnit.USDT]: toSatoshiConversion,
[CurrencyUnit.USDC]: toSatoshiConversion,
},
Expand Down Expand Up @@ -364,6 +381,8 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: standardUnitConversionObj,
[CurrencyUnit.MWK]: standardUnitConversionObj,
[CurrencyUnit.RWF]: standardUnitConversionObj,
[CurrencyUnit.ZMW]: standardUnitConversionObj,
[CurrencyUnit.AED]: standardUnitConversionObj,
[CurrencyUnit.USDT]: standardUnitConversionObj,
[CurrencyUnit.USDC]: standardUnitConversionObj,
};
Expand Down Expand Up @@ -452,6 +471,8 @@ export type CurrencyMap = {
[CurrencyUnit.XAF]: number;
[CurrencyUnit.MWK]: number;
[CurrencyUnit.RWF]: number;
[CurrencyUnit.ZMW]: number;
[CurrencyUnit.AED]: number;
[CurrencyUnit.USDT]: number;
[CurrencyUnit.USDC]: number;
[CurrencyUnit.FUTURE_VALUE]: number;
Expand Down Expand Up @@ -490,6 +511,8 @@ export type CurrencyMap = {
[CurrencyUnit.XAF]: string;
[CurrencyUnit.MWK]: string;
[CurrencyUnit.RWF]: string;
[CurrencyUnit.ZMW]: string;
[CurrencyUnit.AED]: string;
[CurrencyUnit.USDT]: string;
[CurrencyUnit.USDC]: string;
[CurrencyUnit.FUTURE_VALUE]: string;
Expand Down Expand Up @@ -709,6 +732,8 @@ function convertCurrencyAmountValues(
xaf: CurrencyUnit.XAF,
mwk: CurrencyUnit.MWK,
rwf: CurrencyUnit.RWF,
zmw: CurrencyUnit.ZMW,
aed: CurrencyUnit.AED,
mibtc: CurrencyUnit.MICROBITCOIN,
mlbtc: CurrencyUnit.MILLIBITCOIN,
nbtc: CurrencyUnit.NANOBITCOIN,
Expand Down Expand Up @@ -792,6 +817,8 @@ export function mapCurrencyAmount(
xaf,
mwk,
rwf,
zmw,
aed,
usdt,
usdc,
} = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride);
Expand Down Expand Up @@ -825,6 +852,8 @@ export function mapCurrencyAmount(
[CurrencyUnit.XAF]: xaf,
[CurrencyUnit.MWK]: mwk,
[CurrencyUnit.RWF]: rwf,
[CurrencyUnit.ZMW]: zmw,
[CurrencyUnit.AED]: aed,
[CurrencyUnit.MICROBITCOIN]: mibtc,
[CurrencyUnit.MILLIBITCOIN]: mlbtc,
[CurrencyUnit.NANOBITCOIN]: nbtc,
Expand Down Expand Up @@ -956,6 +985,14 @@ export function mapCurrencyAmount(
value: rwf,
unit: CurrencyUnit.RWF,
}),
[CurrencyUnit.ZMW]: formatCurrencyStr({
value: zmw,
unit: CurrencyUnit.ZMW,
}),
[CurrencyUnit.AED]: formatCurrencyStr({
value: aed,
unit: CurrencyUnit.AED,
}),
[CurrencyUnit.USDT]: formatCurrencyStr({
value: usdt,
unit: CurrencyUnit.USDT,
Expand Down Expand Up @@ -1086,6 +1123,10 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => {
return "MWK";
case CurrencyUnit.RWF:
return "RWF";
case CurrencyUnit.ZMW:
return "ZMW";
case CurrencyUnit.AED:
return "AED";
}
return "Unsupported CurrencyUnit";
};
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/CardForm/CardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ const CardFormContentFull = styled.div<{ paddingBottom?: number | undefined }>`
flex-direction: column;
align-self: center;
height: 100%;
width: 100%;
padding-bottom: ${({ paddingBottom }) => paddingBottom ?? 0}px;
`;

Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/components/CardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Props = {
maxContentWidth?: number;
rightContent?: React.ReactNode;
preHeaderContent?: React.ReactNode;
headerRightContent?: React.ReactNode;
expandRight?: boolean;
id?: string;
};
Expand All @@ -43,6 +44,9 @@ export function CardPage(props: Props) {
<Heading type="h1" m0>
{props.title}
</Heading>
{props.headerRightContent && (
<CardPageHeaderRight>{props.headerRightContent}</CardPageHeaderRight>
)}
</CardPageHeader>
) : null;

Expand Down Expand Up @@ -350,6 +354,11 @@ const CardPageHeader = styled.div<{ headerMarginBottom?: number }>`
}
`;

const CardPageHeaderRight = styled.div`
display: flex;
align-items: center;
`;

export const CardPageContent = styled.div<CardPageContentProps>`
${({
maxContentWidth,
Expand Down
27 changes: 26 additions & 1 deletion packages/ui/src/components/CodeInput/CodeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,27 @@ export function CodeInput({

const inputsPerGroup = Math.ceil(codeLength / 2);

/**
* When clicking on the unified code input container, handle focus appropriately
* Uses onMouseDown instead of onClick because mousedown fires before focus,
* allowing us to prevent the default focus behavior and redirect to the correct input.
*/
const onContainerMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
const target = event.target as HTMLInputElement;
const isClickingFilledInput =
target.tagName === "INPUT" && inputState[target.id]?.value !== "";
if (!isClickingFilledInput) {
event.preventDefault();
const firstEmptyIndex = codeFromInputState(inputState).length;
const targetIndex =
firstEmptyIndex < codeLength ? firstEmptyIndex : codeLength - 1;
getRef(getInputId(targetIndex), inputRefs)?.focus();
}
},
[codeLength, getInputId, inputState, inputRefs],
);

const inputs = [];
for (let i = 0; i < codeLength; i += 1) {
const inputId = getInputId(i);
Expand Down Expand Up @@ -413,7 +434,11 @@ export function CodeInput({
}

if (variant === "unified") {
return <UnifiedCodeInputContainer>{inputs}</UnifiedCodeInputContainer>;
return (
<UnifiedCodeInputContainer onMouseDown={onContainerMouseDown}>
{inputs}
</UnifiedCodeInputContainer>
);
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from "@emotion/styled";
import { Spacing } from "../../styles/tokens/spacing.js";
import { ButtonSelector } from "../Button.js";

export const AppliedButtonsContainer = styled.div`
margin-top: ${Spacing.px.sm};
display: flex;
gap: ${Spacing.px.xs};
flex-wrap: wrap;

${ButtonSelector()} {
max-width: 100%;
}
`;
10 changes: 1 addition & 9 deletions packages/ui/src/components/DataManagerTable/EnumFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import styled from "@emotion/styled";
import { ensureArray } from "@lightsparkdev/core";
import { Spacing } from "../../styles/tokens/spacing.js";
import { z } from "../../styles/z-index.js";
import { Button } from "../Button.js";
import Select from "../Select.js";
import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js";
import { Filter, type FilterState } from "./Filter.js";
import { FilterType, type EnumFilterValue } from "./filters.js";

Expand Down Expand Up @@ -105,10 +104,3 @@ export const EnumFilter = ({
</>
);
};

const AppliedButtonsContainer = styled.div`
margin-top: ${Spacing.px.sm};
display: flex;
gap: ${Spacing.px.xs};
flex-wrap: wrap;
`;
10 changes: 1 addition & 9 deletions packages/ui/src/components/DataManagerTable/IdFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import styled from "@emotion/styled";
import { Spacing } from "../../styles/tokens/spacing.js";
import { Button } from "../Button.js";
import { TextInput } from "../TextInput.js";
import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js";
import { Filter, type FilterState } from "./Filter.js";
import { FilterType } from "./filters.js";

Expand Down Expand Up @@ -148,10 +147,3 @@ export const IdFilter = ({
</>
);
};

const AppliedButtonsContainer = styled.div`
margin-top: ${Spacing.px.sm};
display: flex;
gap: ${Spacing.px.xs};
flex-wrap: wrap;
`;
Loading