Skip to content

Commit

Permalink
Merge pull request #2172 from AdaptiveConsulting/fix/4854-credit-sell…
Browse files Browse the repository at this point in the history
…side-keyboard-navigation

fix: sell-side keyboard navigation
  • Loading branch information
SHolleworth authored Apr 25, 2023
2 parents b216368 + 38bf6a2 commit 37c3202
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 33 deletions.
82 changes: 58 additions & 24 deletions src/client/e2e/credit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,36 @@ import { test } from "./fixtures"
import { OPENFIN_PROJECT_NAME } from "./utils"

test.describe("Credit", () => {
test.describe("New RFQ", () => {
test("When I select Googl instrument and click Send RFQ button then I should see a GOOGL RFQ created on the RFQ sections and I can accept any value @smoke", async ({
context,
creditPagesRec,
}, testInfo) => {
test.setTimeout(120000)
let newRfqPage: Page
let rfqsPage: Page
let rfqBlotterPage: Page

let newRfqPage: Page
let rfqsPage: Page
let rfqBlotterPage: Page
test.beforeAll(async ({ context, creditPagesRec }, testInfo) => {
if (testInfo.project.name === OPENFIN_PROJECT_NAME) {
const mainWindow = creditPagesRec["mainWindow"]

if (testInfo.project.name === OPENFIN_PROJECT_NAME) {
const mainWindow = creditPagesRec["mainWindow"]
await mainWindow.evaluate(async () => {
window.fin.Window.getCurrentSync().maximize()
})

await mainWindow.evaluate(async () => {
window.fin.Window.getCurrentSync().maximize()
})
newRfqPage = creditPagesRec["credit-new-rfq"]
rfqsPage = creditPagesRec["credit-rfqs"]
rfqBlotterPage = creditPagesRec["credit-blotter"]
} else {
const pages = context.pages()

newRfqPage = creditPagesRec["credit-new-rfq"]
rfqsPage = creditPagesRec["credit-rfqs"]
rfqBlotterPage = creditPagesRec["credit-blotter"]
} else {
const pages = context.pages()
newRfqPage = pages.length > 0 ? pages[0] : await context.newPage()

newRfqPage = pages.length > 0 ? pages[0] : await context.newPage()
await newRfqPage.goto(`${process.env.URL_PATH}/credit`)

await newRfqPage.goto(`${process.env.URL_PATH}/credit`)
rfqsPage = newRfqPage
rfqBlotterPage = newRfqPage
}
})

rfqsPage = newRfqPage
rfqBlotterPage = newRfqPage
}
test.describe("New RFQ", () => {
test("Create RFQ for GOOGL @smoke", async () => {
test.setTimeout(120000)

await newRfqPage.getByPlaceholder(/Enter a CUSIP/).click()
await newRfqPage
Expand Down Expand Up @@ -92,4 +91,39 @@ test.describe("Credit", () => {
expect(tradeId).toEqual(blotterId)
})
})

test.describe("Sell side", () => {
test("Sell side ticket", async ({ context }) => {
await newRfqPage.getByPlaceholder(/Enter a CUSIP/).click()
await newRfqPage
.locator("[data-testid='search-result-item']")
.nth(5)
.click()

const quantity = newRfqPage.locator("[data-testid='quantity']")
await quantity.type("2")

await newRfqPage
.locator("span")
.getByText(/Adaptive Bank/)
.click()

const pagePromise = context.waitForEvent("page")

await newRfqPage
.locator("button")
.getByText(/Send RFQ/)
.click()

const sellSidePage = await pagePromise

await sellSidePage.waitForSelector("text=New RFQ")

await sellSidePage.getByTestId("price-input").fill("100")

await sellSidePage.keyboard.press("Enter")

await expect(rfqsPage.getByTestId("quotes").first()).toContainText("$100")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const RfqGridInner = ({ caption }: RfqGridInner) => {
</TableHead>
<TableBody role="grid">
{rfqs.length ? (
rfqs.map((row: RfqRow) => (
rfqs.map((row) => (
<TableBodyRow
selected={row.id === selectedId}
highlight={row.id === highlightId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import {
} from "@/generated/TradingGateway"
import { createCreditQuote$, useCreditRfqDetails } from "@/services/credit"
import { ThemeName } from "@/theme"
import { customNumberFormatter, invertDirection } from "@/utils"
import {
customNumberFormatter,
invertDirection,
useClickElementOnEnter,
} from "@/utils"

import { CreditRfqTimer, isRfqTerminated } from "../../common"
import { useIsFocused } from "../utils/useIsFocused"
import { price$, usePrice } from "./SellSideTradeTicketParameters"

const FooterWrapper = styled.div<{ accepted: boolean; missed: boolean }>`
Expand Down Expand Up @@ -42,6 +47,10 @@ const FooterButton = styled.button`
border-radius: 3px;
height: 24px;
font-size: 11px;
&:focus {
border-radius: 3px;
border: 1px solid #4c76c4 !important;
}
`

const PassButton = styled(FooterButton)<{ disabled: boolean }>`
Expand Down Expand Up @@ -92,6 +101,7 @@ const TradeDetails = styled.div`

const [quoteRequest$, sendQuote] =
createSignal<{ rfqId: number; dealerId: number }>()

quoteRequest$
.pipe(
withLatestFrom(price$),
Expand Down Expand Up @@ -119,6 +129,10 @@ export const SellSideTradeTicketFooter = ({
}: SellSideTradeTicketTicketFooterProps) => {
const rfq = useCreditRfqDetails(rfqId)
const price = usePrice()
const isPriceFieldFocused = useIsFocused()

const clickElementRef =
useClickElementOnEnter<HTMLButtonElement>(isPriceFieldFocused)

if (!rfq) {
return <FooterWrapper accepted={false} missed={false} />
Expand All @@ -145,7 +159,8 @@ export const SellSideTradeTicketFooter = ({
{state === RfqState.Open && (
<>
<PassButton
disabled={!!quote}
// disabled={!!quote} //reinstate when pass feature is implemented
disabled={true}
onClick={() => {
console.log("Send message")
}}
Expand All @@ -162,6 +177,7 @@ export const SellSideTradeTicketFooter = ({
)}
</TimerWrapper>
<SendQuoteButton
ref={clickElementRef}
direction={direction}
onClick={() => sendQuote({ rfqId, dealerId })}
disabled={disableSend}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Loader } from "@/components/Loader"
import { useAdaptiveDealerId } from "@/services/credit"

import { useSelectedRfqId } from "../sellSideState"
import { focused$ } from "../utils/useIsFocused"
import { SellSideTradeTicketTicketCore } from "./SellSideTradeTicketCore"

const SellSideWrapper = styled.div`
Expand Down Expand Up @@ -74,7 +75,7 @@ const SellSideTradeTicketInner = () => {

export const SellSideTradeTicket = () => (
<SellSideWrapper>
<Subscribe fallback={<Loader />}>
<Subscribe fallback={<Loader />} source$={focused$}>
<SellSideTradeTicketInner />
</Subscribe>
</SellSideWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
selectedRfqId$,
SellSideQuoteState,
} from "../sellSideState"
import { setFocused } from "../utils/useIsFocused"

const ParametersWrapper = styled.div`
padding: 8px;
Expand Down Expand Up @@ -155,13 +156,19 @@ export const SellSideTradeTicketParameters = ({
)
) : (
<ParameterInput
tabIndex={1}
type="text"
data-testid="price-input"
ref={ref}
value={price.inputValue}
disabled={state !== RfqState.Open}
onChange={(event) => setPrice(event.currentTarget.value)}
onFocus={(event) => {
event.target.select()
setFocused(true)
}}
onBlur={() => {
setFocused(false)
}}
/>
)}
Expand Down
19 changes: 14 additions & 5 deletions src/client/src/App/Credit/SellSide/sellSideState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { bind } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import { combineLatest, merge } from "rxjs"
import { delay, map, startWith } from "rxjs/operators"
import { delay, filter, map, startWith } from "rxjs/operators"

import { HIGHLIGHT_ROW_FLASH_TIME } from "@/constants"
import { QuoteState, RfqState } from "@/generated/TradingGateway"
Expand Down Expand Up @@ -66,10 +66,6 @@ export const [useHighlightedRfqId] = bind(
null,
)

const [_selectedRfqId$, selectRfqId] = createSignal<number | null>()
export { selectRfqId }
export const [useSelectedRfqId, selectedRfqId$] = bind(_selectedRfqId$, null)

export enum SellSideQuotesTab {
All = "All RFQs",
Live = "Live RFQs",
Expand Down Expand Up @@ -148,3 +144,16 @@ const _sellSideRfqs$ = combineLatest([
)

export const [useSellSideRfqs, sellSideRfqs$] = bind(_sellSideRfqs$)

const [_selectedRfqId$, selectRfqId] = createSignal<number | null>()
export { selectRfqId }
export const [useSelectedRfqId, selectedRfqId$] = bind(
merge(
_selectedRfqId$,
sellSideRfqs$.pipe(
map((rfqs) => rfqs.at(0)?.id),
filter(Boolean),
),
),
null,
)
8 changes: 8 additions & 0 deletions src/client/src/App/Credit/SellSide/utils/useIsFocused.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { bind } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"

const [_focused$, setFocused] = createSignal<boolean>()

const [useIsFocused, focused$] = bind(_focused$, false)

export { focused$, setFocused, useIsFocused }
1 change: 1 addition & 0 deletions src/client/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./isMobile"
export * from "./isPwa"
export * from "./mapObject"
export * from "./showRfqInSellSide"
export * from "./useClickElementOnEnter"
export * from "./useLocalStorage"
export * from "./usePopUpMenu"
export * from "./windowCoordinates"
25 changes: 25 additions & 0 deletions src/client/src/utils/useClickElementOnEnter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useCallback, useEffect, useRef } from "react"

export const useClickElementOnEnter = <T extends HTMLElement>(
active = true,
) => {
const clickElementRef = useRef<T>(null)

const listener = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Enter" && active) {
clickElementRef.current?.click()
}
},
[active],
)

useEffect(() => {
addEventListener("keypress", listener)
return () => {
removeEventListener("keypress", listener)
}
}, [listener])

return clickElementRef
}

0 comments on commit 37c3202

Please sign in to comment.