diff options
9 files changed, 80 insertions, 36 deletions
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index f3ffcd157..5f1b9b688 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -36,6 +36,11 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode prev[d].push(cur) return prev }, {} as Record<string, typeof transactions>) + /** + * FIXME: create an abstraction of a table with accessible feature + * multi column multi header and subheaders + * Also responsiveness + */ return ( <div class="px-4 mt-4"> <div class="sm:flex sm:items-center"> @@ -44,28 +49,41 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </div> </div> <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white"> - <table class="min-w-full divide-y divide-gray-300"> + <table class="min-w-full divide-y divide-gray-300" summary={i18n.str`List of all transfer received and sent related to this account sorted by date with the latest on top.`}> <thead> <tr> - <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th> - <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th> - <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th> - <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th> + <th id="transfer-date" scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "> + {i18n.str`Date`} + </th> + <th id="transfer-counterpart" scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "> + {i18n.str`Counterpart`} + </th> + <th id="transfer-subject" scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "> + {i18n.str`Subject`} + </th> + <th id="transfer-amount" scope="col" class="hidden sm:table-cell px-2 py-3.5 text-left text-sm font-semibold text-gray-900 "> + {i18n.str`Amount`} + </th> </tr> </thead> <tbody> - {Object.entries(txByDate).map(([date, txs], idx) => { + {Object.entries(txByDate).map(([date, txs], idxGr) => { return <Fragment> <tr class="border-t border-gray-200"> - <th colSpan={4} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"> + <th colSpan={4} scope="colgroup" + class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3" + id={`transfer-date-${idxGr}`} + headers={`transfer-date`} + > {date} </th> </tr> - {txs.map(item => { + {txs.map((item, idxTx) => { const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss", { locale: dateLocale }) - return (<tr key={idx} class="border-b border-gray-200 last:border-none"> - <td class="relative py-2 pl-2 pr-2 text-sm "> - <div class="font-medium text-gray-900">{time}</div> + return (<tr key={idxTx} class="border-b border-gray-200 last:border-none"> + <td class="relative py-2 pl-2 pr-2 text-sm sm:bg-gray-50 w-28" + id={`transfer-time-${idxGr}-${idxTx}`} + headers={`transfer-date-${idxGr} transfer-date`}> <dl class="font-normal sm:hidden"> <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt> <dd class="mt-1 truncate text-gray-700"> @@ -90,19 +108,25 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </dd> </dl> </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500" + headers={`transfer-date-${idxGr} transfer-time-${idxGr}-${idxTx} transfer-counterpart`} + > + <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900"> + {item.counterpart} + </a> + </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all " + headers={`transfer-date-${idxGr} transfer-time-${idxGr}-${idxTx} transfer-subject`} + >{item.subject}</td> <td data-negative={item.negative ? "true" : "false"} - class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 "> + class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 min-w-md" + headers={`transfer-date-${idxGr} transfer-time-${idxGr}-${idxTx} transfer-amount`} + > {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor spec={config.currency_specification} /> ) : ( <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> )} </td> - <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500"> - <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900"> - {item.counterpart} - </a> - </td> - <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td> </tr>) })} </Fragment> diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index 05d53bb05..c43efb933 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -317,7 +317,7 @@ export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> { onClose() }} > - Cancel + <i18n.Translate>Cancel</i18n.Translate> </button> </div> diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 2a7374cab..80a7a620f 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -35,6 +35,7 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq const [bankState] = useBankState(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); + const [changeUsingMouse, setChangeUsingMouse] = useState(false) return ( <div class="mt-4"> @@ -45,11 +46,12 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq </legend> <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4"> - {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */} - <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}> - <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => { - setTab("charge-wallet") - }} + <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus-within:ring focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-gray-600"}> + <input type="radio" name="transfer-type" value="char-wallet" class="sr-only" + onClick={(e) => { + setChangeUsingMouse(e.clientX > 0 && e.clientY > 0) + setTab("charge-wallet") + }} /> <div class="flex flex-col"> <span class="flex"> @@ -76,13 +78,16 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq </label> - <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}> - <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => { + <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus-visible:ring-red-400" + (tab === "wire-transfer" ? " ring-2 ring-indigo-600" : "border-gray-300")}> + <input type="radio" name="transfer-type" value="wire-transfer" class="sr-only" onClick={(e) => { + setChangeUsingMouse(e.clientX > 0 && e.clientY > 0) setTab("wire-transfer") }} /> <div class="flex flex-col"> <span class="flex"> - <div class="text-4xl mr-4 my-auto">↔</div> + <div class="text-4xl mr-4 my-auto"> + <span aria-hidden="true">↔</span> + </div> <span class="grow self-center text-lg font-medium text-gray-900 align-middle text-center"> <i18n.Translate>to another bank account</i18n.Translate> </span> @@ -98,7 +103,7 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq </div> {tab === "charge-wallet" && ( <WalletWithdrawForm - focus + focus={changeUsingMouse} limit={limit} onAuthorizationRequired={onAuthorizationRequired} goToConfirmOperation={goToConfirmOperation} @@ -109,7 +114,7 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq )} {tab === "wire-transfer" && ( <PaytoWireTransferForm - focus + focus={changeUsingMouse} title={i18n.str`Transfer details`} limit={limit} onAuthorizationRequired={onAuthorizationRequired} diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx index b85230a1b..bb87d5415 100644 --- a/packages/web-util/src/components/Attention.tsx +++ b/packages/web-util/src/components/Attention.tsx @@ -1,5 +1,6 @@ import { TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useTranslationContext } from "../index.browser.js"; interface Props { type?: "info" | "success" | "warning" | "danger", @@ -8,6 +9,7 @@ interface Props { children?: ComponentChildren, } export function Attention({ type = "info", title, children, onClose }: Props): VNode { + const { i18n } = useTranslationContext(); return <div class={`group attention-${type} mt-2 shadow-lg`}> <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> <div class="flex"> @@ -39,7 +41,7 @@ export function Attention({ type = "info", title, children, onClose }: Props): V </div> {onClose && <div> - <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" + <button type="button" aria-label={i18n.str`Close banner`} class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" onClick={(e) => { e.preventDefault(); onClose(); diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx index e76447291..bb08b99ef 100644 --- a/packages/web-util/src/components/CopyButton.tsx +++ b/packages/web-util/src/components/CopyButton.tsx @@ -1,5 +1,7 @@ import { h, VNode } from "preact"; +import { useTransition } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; +import { useTranslationContext } from "../index.browser.js"; export function CopyIcon(): VNode { return ( @@ -19,6 +21,7 @@ export function CopiedIcon(): VNode { export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode { const [copied, setCopied] = useState(false); + const {i18n} = useTranslationContext() function copyText(): void { if (!navigator.clipboard && !window.isSecureContext) { alert('clipboard is not available on insecure context (http)') @@ -38,7 +41,7 @@ export function CopyButton({ class: clazz, getContent }: { class: string, getCon if (!copied) { return ( - <button class={clazz} onClick={copyText} > + <button class={clazz} onClick={copyText} aria-label={i18n.str`Copy`} > <CopyIcon /> </button> ); diff --git a/packages/web-util/src/components/LangSelector.tsx b/packages/web-util/src/components/LangSelector.tsx index a8d910129..ebcd1c696 100644 --- a/packages/web-util/src/components/LangSelector.tsx +++ b/packages/web-util/src/components/LangSelector.tsx @@ -65,7 +65,7 @@ export function LangSelector({ supportedLangs }: { supportedLangs: string[] }): return ( <div> <div class="relative mt-2"> - <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" + <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" onClick={() => { setHidden((h) => !h); }}> @@ -81,7 +81,7 @@ export function LangSelector({ supportedLangs }: { supportedLangs: string[] }): </button> {!hidden && - <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> + <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-activedescendant="listbox-option-3"> {supportedLangs .filter((l) => l !== lang) .map((lang) => ( diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx index 7d9a1b378..a8a30b32a 100644 --- a/packages/web-util/src/forms/InputArray.tsx +++ b/packages/web-util/src/forms/InputArray.tsx @@ -5,6 +5,7 @@ import { FormProvider, UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; import { useField } from "./useField.js"; +import { useTranslationContext } from "../index.browser.js"; function Option({ label, @@ -77,6 +78,7 @@ export function InputArray<T extends object, K extends keyof T>( labelField: string; } & UIFormProps<T, K>, ): VNode { + const { i18n } = useTranslationContext(); const { fields, labelField, name, label, required, tooltip } = props; const { value, onChange, state } = useField<T, K>(name); const list = (value ?? []) as Array<Record<string, string | undefined>>; @@ -175,7 +177,7 @@ export function InputArray<T extends object, K extends keyof T>( }} class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " > - Remove + <i18n.Translate>Remove</i18n.Translate> </button> )} </div> diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx index a67eb23b7..d1e31c15c 100644 --- a/packages/web-util/src/forms/InputSelectMultiple.tsx +++ b/packages/web-util/src/forms/InputSelectMultiple.tsx @@ -4,6 +4,7 @@ import { UIFormProps } from "./FormProvider.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { useTranslationContext } from "../index.browser.js"; export function InputSelectMultiple<T extends object, K extends keyof T>( props: { @@ -22,6 +23,8 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( return { ...prev, [curr.value as string]: curr.label }; }, {} as Record<string, string>); + const { i18n } = useTranslationContext(); + const list = (value ?? []) as string[]; const filteredChoices = filter === undefined @@ -51,7 +54,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" > - <span class="sr-only">Remove</span> + <span class="sr-only"><i18n.Translate>Remove</i18n.Translate></span> <svg viewBox="0 0 14 14" class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" @@ -81,6 +84,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( <button type="button" disabled={state.disabled} + aria-label={i18n.str`Clear filter`} onClick={() => { setFilter(filter === undefined ? "" : undefined); }} diff --git a/packages/web-util/src/forms/InputSelectOne.tsx b/packages/web-util/src/forms/InputSelectOne.tsx index d100b079d..93cb2c2f2 100644 --- a/packages/web-util/src/forms/InputSelectOne.tsx +++ b/packages/web-util/src/forms/InputSelectOne.tsx @@ -4,12 +4,14 @@ import { UIFormProps } from "./FormProvider.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { useTranslationContext } from "../index.browser.js"; export function InputSelectOne<T extends object, K extends keyof T>( props: { choices: ChoiceS<T[K]>[]; } & UIFormProps<T, K>, ): VNode { + const { i18n } = useTranslationContext(); const { name, label, choices, placeholder, tooltip, required } = props; const { value, onChange } = useField<T, K>(name); @@ -25,6 +27,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( : choices.filter((v) => { return regex.test(v.label); }); + return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -42,7 +45,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" > - <span class="sr-only">Remove</span> + <span class="sr-only"><i18n.Translate>Remove</i18n.Translate></span> <svg viewBox="0 0 14 14" class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" @@ -69,6 +72,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( /> <button type="button" + aria-label={i18n.str`Clear filter`} onClick={() => { setFilter(filter === undefined ? "" : undefined); }} |