feat(ui-toolkit): form element design changes (#4525)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Bartłomiej Głownia 2023-08-22 18:39:52 +02:00 committed by GitHub
parent e0a91b3850
commit 78414b4429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1288 additions and 240 deletions

View File

@ -2,7 +2,7 @@ import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import {
Input,
TradingInput,
TinyScroll,
VegaIcon,
VegaIconNames,
@ -57,7 +57,7 @@ export const MarketSelector = ({
/>
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="flex-1">
<Input
<TradingInput
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}

View File

@ -1,4 +1,4 @@
import { Checkbox } from '@vegaprotocol/ui-toolkit';
import { TradingCheckbox } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
@ -7,7 +7,7 @@ export const TelemetryApproval = ({ helpText }: { helpText: string }) => {
return (
<div className="flex flex-col py-3">
<div className="mr-4" role="form">
<Checkbox
<TradingCheckbox
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
checked={isApproved}
name="telemetry-approval"

View File

@ -9,13 +9,13 @@ import {
import { t } from '@vegaprotocol/i18n';
import {
Button,
FormGroup,
Input,
InputError,
RichSelect,
Select,
TradingFormGroup,
TradingInput,
TradingInputError,
TradingRichSelect,
TradingSelect,
Tooltip,
Checkbox,
TradingCheckbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@ -130,12 +130,16 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<FormGroup label="Vega key" labelFor="to-address">
<TradingFormGroup label="Vega key" labelFor="to-address">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
select={
<Select {...register('toAddress')} id="to-address" defaultValue="">
<TradingSelect
{...register('toAddress')}
id="to-address"
defaultValue=""
>
<option value="" disabled={true}>
{t('Please select')}
</option>
@ -147,10 +151,10 @@ export const TransferForm = ({
{pk}
</option>
))}
</Select>
</TradingSelect>
}
input={
<Input
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
@ -171,12 +175,12 @@ export const TransferForm = ({
}
/>
{errors.toAddress?.message && (
<InputError forInput="to-address">
<TradingInputError forInput="to-address">
{errors.toAddress.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label="Asset" labelFor="asset">
</TradingFormGroup>
<TradingFormGroup label="Asset" labelFor="asset">
<Controller
control={control}
name="asset"
@ -186,7 +190,7 @@ export const TransferForm = ({
},
}}
render={({ field }) => (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
@ -208,15 +212,17 @@ export const TransferForm = ({
}
/>
))}
</RichSelect>
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<InputError forInput="asset">{errors.asset.message}</InputError>
<TradingInputError forInput="asset">
{errors.asset.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup label="Amount" labelFor="amount">
<Input
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
autoComplete="off"
appendElement={
@ -239,11 +245,13 @@ export const TransferForm = ({
})}
/>
{errors.amount?.message && (
<InputError forInput="amount">{errors.amount.message}</InputError>
<TradingInputError forInput="amount">
{errors.amount.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
<div className="mb-4">
<Checkbox
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount}
label={

View File

@ -1,4 +1,4 @@
import { Option } from '@vegaprotocol/ui-toolkit';
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@ -28,7 +28,7 @@ export const Balance = ({
export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
return (
<Option key={asset.id} value={asset.id}>
<TradingOption key={asset.id} value={asset.id}>
<div className="flex flex-col items-start">
<div className="flex flex-row align-baseline gap-2">
<span>{asset.name}</span>{' '}
@ -49,6 +49,6 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
</span>
</div>
</div>
</Option>
</TradingOption>
);
};

View File

@ -15,7 +15,7 @@ import {
} from 'date-fns';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { TradingInputError } from '@vegaprotocol/ui-toolkit';
const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams {
@ -195,7 +195,7 @@ export const DateRangeFilter = forwardRef(
}, [value, props]);
const notification = useMemo(() => {
const not = error ? <InputError>{error}</InputError> : null;
const not = error ? <TradingInputError>{error}</TradingInputError> : null;
return (
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
);

View File

@ -1,4 +1,8 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { DealTicketAmountProps } from './deal-ticket-amount';
@ -22,17 +26,17 @@ export const DealTicketLimitAmount = ({
const renderError = () => {
if (sizeError) {
return (
<InputError testId="deal-ticket-error-message-size-limit">
<TradingInputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</InputError>
</TradingInputError>
);
}
if (priceError) {
return (
<InputError testId="deal-ticket-error-message-price-limit">
<TradingInputError testId="deal-ticket-error-message-price-limit">
{priceError}
</InputError>
</TradingInputError>
);
}
@ -43,7 +47,7 @@ export const DealTicketLimitAmount = ({
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={t('Size')}
labelFor="input-order-size-limit"
className="!mb-0"
@ -59,8 +63,8 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-limit"
className="w-full"
type="number"
@ -68,15 +72,16 @@ export const DealTicketLimitAmount = ({
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
<div className="pt-7 leading-10">@</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
@ -93,19 +98,20 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
</div>
{renderError()}

View File

@ -4,7 +4,11 @@ import {
validateAmount,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import {
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '@vegaprotocol/markets';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
@ -33,7 +37,7 @@ export const DealTicketMarketAmount = ({
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<div className="mb-2 text-sm">{t('Size')}</div>
<div className="mb-2 text-xs">{t('Size')}</div>
<Controller
name="size"
control={control}
@ -45,8 +49,8 @@ export const DealTicketMarketAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-market"
className="w-full"
type="number"
@ -54,12 +58,13 @@ export const DealTicketMarketAmount = ({
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</div>
<div className="pt-7 leading-10">@</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1 text-sm text-right">
{inAuction && (
<Tooltip
@ -72,7 +77,7 @@ export const DealTicketMarketAmount = ({
)}
<div
data-testid="last-price"
className={classNames('leading-10', { 'pt-7': !inAuction })}
className={classNames('leading-10', { 'pt-5': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
@ -85,12 +90,12 @@ export const DealTicketMarketAmount = ({
</div>
</div>
{sizeError && (
<InputError
<TradingInputError
intent="danger"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</InputError>
</TradingInputError>
)}
</div>
);

View File

@ -4,9 +4,9 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
@ -32,9 +32,9 @@ export const DealTicketSizeIceberg = ({
const renderPeakSizeError = () => {
if (peakSizeError) {
return (
<InputError testId="deal-ticket-peak-error-message-size-limit">
<TradingInputError testId="deal-ticket-peak-error-message-size-limit">
{peakSizeError}
</InputError>
</TradingInputError>
);
}
@ -44,9 +44,9 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
<InputError testId="deal-ticket-minimum-error-message-size-limit">
<TradingInputError testId="deal-ticket-minimum-error-message-size-limit">
{minimumVisibleSizeError}
</InputError>
</TradingInputError>
);
}
@ -57,7 +57,7 @@ export const DealTicketSizeIceberg = ({
<div className="mb-2">
<div className="flex items-center gap-4">
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={
<Tooltip
description={
@ -93,7 +93,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'peakSize'),
}}
render={({ field }) => (
<Input
<TradingInput
id="input-order-peak-size"
className="w-full"
type="number"
@ -106,14 +106,14 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex"></div>
<div className="flex"></div>
</div>
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={
<Tooltip
description={
@ -151,7 +151,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
}}
render={({ field }) => (
<Input
<TradingInput
id="input-order-minimum-size"
className="w-full"
type="number"
@ -164,7 +164,7 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
</div>
{renderPeakSizeError()}

View File

@ -10,13 +10,13 @@ import {
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
Radio,
RadioGroup,
Input,
Checkbox,
FormGroup,
InputError,
Select,
TradingRadio,
TradingRadioGroup,
TradingInput,
TradingCheckbox,
TradingFormGroup,
TradingInputError,
TradingSelect,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
@ -187,9 +187,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.type && (
<InputError testId="stop-order-error-message-type">
<TradingInputError testId="stop-order-error-message-type">
{errors.type.message}
</InputError>
</TradingInputError>
)}
<Controller
@ -199,21 +199,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<FormGroup label={t('Trigger')} compact={true} labelFor="">
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
<TradingRadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
@ -221,7 +221,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<Radio
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
@ -229,7 +229,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
@ -246,16 +246,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
validate: validateAmount(priceStep, 'Price'),
}}
control={control}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
<TradingInput
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
@ -263,9 +264,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.triggerPrice && (
<InputError testId="stop-order-error-message-trigger-price">
<TradingInputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</InputError>
</TradingInputError>
)}
</div>
)}
@ -294,16 +295,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
'Trailing percentage offset'
),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
<TradingInput
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
@ -311,9 +313,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.triggerTrailingPercentOffset && (
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
<TradingInputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</InputError>
</TradingInputError>
)}
</div>
)}
@ -324,25 +326,29 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
<TradingRadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio value="price" id="triggerType-price" label={'Price'} />
<Radio
<TradingRadio
value="price"
id="triggerType-price"
label={'Price'}
/>
<TradingRadio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
</FormGroup>
</TradingFormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Size`)}
className="!mb-0 flex-1"
@ -358,10 +364,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<Input
<TradingInput
id="order-size"
className="w-full"
type="number"
@ -370,16 +376,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</FormGroup>
<div className="pt-7 leading-10">@</div>
</TradingFormGroup>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
@ -397,10 +404,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<Input
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
@ -408,15 +415,16 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</FormGroup>
</TradingFormGroup>
) : (
<div
className="text-sm text-right pt-7 leading-10"
className="text-sm text-right pt-5 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
@ -427,21 +435,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</div>
</div>
{errors.size && (
<InputError testId="stop-order-error-message-size">
<TradingInputError testId="stop-order-error-message-size">
{errors.size.message}
</InputError>
</TradingInputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<InputError testId="stop-order-error-message-price">
<TradingInputError testId="stop-order-error-message-price">
{errors.price.message}
</InputError>
</TradingInputError>
)}
</div>
<div className="mb-2">
<FormGroup
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
@ -449,11 +457,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<Select
render={({ field, fieldState }) => (
<TradingSelect
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...field}
>
<option
@ -468,14 +477,14 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</Select>
</TradingSelect>
)}
/>
</FormGroup>
</TradingFormGroup>
{errors.timeInForce && (
<InputError testId="stop-error-message-tif">
<TradingInputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</InputError>
</TradingInputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-between">
@ -485,29 +494,29 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
render={({ field }) => {
const { onChange: onCheckedChange, value } = field;
return (
<Checkbox
<TradingCheckbox
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={<span className="text-xs">{t('Expire')}</span>}
label={t('Expire')}
/>
);
}}
/>
<Checkbox
<TradingCheckbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
</div>
{expire && (
<>
<FormGroup
<TradingFormGroup
label={t('Strategy')}
labelFor="expiryStrategy"
compact={true}
@ -517,26 +526,26 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
render={({ field }) => {
return (
<RadioGroup orientation="horizontal" {...field}>
<Radio
<TradingRadioGroup orientation="horizontal" {...field}>
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
id="expiryStrategy-submit"
label={'Submit'}
/>
<Radio
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
}
id="expiryStrategy-cancel"
label={'Cancel'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
</FormGroup>
</TradingFormGroup>
<div className="mb-2">
<Controller
name="expiresAt"

View File

@ -17,8 +17,8 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
Checkbox,
InputError,
TradingCheckbox,
TradingInputError,
Intent,
Notification,
Tooltip,
@ -417,7 +417,7 @@ export const DealTicket = ({
name="postOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="post-only"
checked={!disablePostOnlyCheckbox && field.value}
disabled={disablePostOnlyCheckbox}
@ -449,7 +449,7 @@ export const DealTicket = ({
name="reduceOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="reduce-only"
checked={!disableReduceOnlyCheckbox && field.value}
disabled={disableReduceOnlyCheckbox}
@ -483,7 +483,7 @@ export const DealTicket = ({
name="iceberg"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
@ -572,11 +572,11 @@ export const NoWalletWarning = ({
if (isReadOnly) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
</InputError>
</TradingInputError>
</div>
);
}
@ -613,9 +613,9 @@ const SummaryMessage = memo(
if (error?.message) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{error?.message}
</InputError>
</TradingInputError>
</div>
);
}

View File

@ -1,4 +1,8 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useRef } from 'react';
@ -19,24 +23,25 @@ export const ExpirySelector = ({
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (
<FormGroup
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<Input
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={dateFormatted}
onChange={(e) => onSelect(e.target.value)}
min={minDate}
hasError={!!errorMessage}
/>
{errorMessage && (
<InputError testId="deal-ticket-error-message-expiry">
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</InputError>
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
);
};

View File

@ -1,7 +1,7 @@
import {
FormGroup,
InputError,
Select,
TradingFormGroup,
TradingInputError,
TradingSelect,
Tooltip,
SimpleGrid,
} from '@vegaprotocol/ui-toolkit';
@ -90,12 +90,12 @@ export const TimeInForceSelector = ({
};
return (
<FormGroup
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Select
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
@ -103,18 +103,19 @@ export const TimeInForceSelector = ({
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</Select>
</TradingSelect>
{errorMessage && (
<InputError testId="deal-ticket-error-message-tif">
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
);
};

View File

@ -1,5 +1,5 @@
import {
InputError,
TradingInputError,
SimpleGrid,
Tooltip,
TradingDropdown,
@ -178,9 +178,9 @@ export const TypeSelector = ({
value={value}
/>
{errorMessage && (
<InputError testId="deal-ticket-error-message-type">
<TradingInputError testId="deal-ticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</InputError>
</TradingInputError>
)}
</>
);

View File

@ -14,14 +14,14 @@ import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
FormGroup,
Input,
InputError,
RichSelect,
TradingFormGroup,
TradingInput,
TradingInputError,
TradingRichSelect,
Notification,
Intent,
ButtonLink,
Select,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@ -151,7 +151,7 @@ export const DepositForm = ({
noValidate={true}
data-testid="deposit-form"
>
<FormGroup
<TradingFormGroup
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
>
@ -197,15 +197,17 @@ export const DepositForm = ({
}}
/>
{errors.from?.message && (
<InputError intent="danger">{errors.from.message}</InputError>
<TradingInputError intent="danger">
{errors.from.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
</TradingFormGroup>
<TradingFormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<TradingSelect {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
@ -215,10 +217,10 @@ export const DepositForm = ({
{pk}
</option>
))}
</Select>
</TradingSelect>
}
input={
<Input
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
@ -233,12 +235,12 @@ export const DepositForm = ({
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
<TradingInputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset">
</TradingFormGroup>
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@ -248,7 +250,7 @@ export const DepositForm = ({
},
}}
render={({ field }) => (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
@ -271,13 +273,13 @@ export const DepositForm = ({
}
/>
))}
</RichSelect>
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<InputError intent="danger" forInput="asset">
<TradingInputError intent="danger" forInput="asset">
{errors.asset.message}
</InputError>
</TradingInputError>
)}
{isActive && isFaucetable && selectedAsset && (
<UseButton onClick={submitFaucet}>
@ -296,7 +298,7 @@ export const DepositForm = ({
{t('View asset details')}
</button>
)}
</FormGroup>
</TradingFormGroup>
<FaucetNotification
isActive={isActive}
selectedAsset={selectedAsset}
@ -308,8 +310,8 @@ export const DepositForm = ({
</div>
)}
{approved && (
<FormGroup label={t('Amount')} labelFor="amount">
<Input
<TradingFormGroup label={t('Amount')} labelFor="amount">
<TradingInput
type="number"
autoComplete="off"
id="amount"
@ -374,9 +376,9 @@ export const DepositForm = ({
})}
/>
{errors.amount?.message && (
<InputError intent="danger" forInput="amount">
<TradingInputError intent="danger" forInput="amount">
{errors.amount.message}
</InputError>
</TradingInputError>
)}
{selectedAsset && balances && (
<UseButton
@ -390,7 +392,7 @@ export const DepositForm = ({
{t('Use maximum')}
</UseButton>
)}
</FormGroup>
</TradingFormGroup>
)}
<ApproveNotification
isActive={isActive}

View File

@ -4,10 +4,10 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
ButtonLink,
Input,
TradingInput,
Loader,
Radio,
RadioGroup,
TradingRadio,
TradingRadioGroup,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '../../hooks';
import { CUSTOM_NODE_KEY } from '../../types';
@ -76,7 +76,7 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
`This app will only work on ${VEGA_ENV}. Select a node to connect to.`
)}
</p>
<RadioGroup
<TradingRadioGroup
value={nodeRadio}
onChange={(value) => setNodeRadio(value)}
>
@ -112,7 +112,7 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
/>
</div>
</div>
</RadioGroup>
</TradingRadioGroup>
<div className="mt-4">
<Button
fill={true}
@ -161,7 +161,7 @@ const CustomRowWrapper = ({
<LayoutRow dataTestId="custom-row">
<div className="flex w-full mb-2">
{nodes.length > 0 && (
<Radio
<TradingRadio
id="node-url-custom"
value={CUSTOM_NODE_KEY}
label={nodeRadio === CUSTOM_NODE_KEY ? '' : t('Other')}
@ -172,7 +172,7 @@ const CustomRowWrapper = ({
data-testid="custom-node"
className="flex items-center w-full gap-2"
>
<Input
<TradingInput
placeholder="https://"
value={inputText}
hasError={Boolean(error)}

View File

@ -2,7 +2,7 @@ import type { ApolloError } from '@apollo/client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { isValidUrl } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Radio } from '@vegaprotocol/ui-toolkit';
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { CUSTOM_NODE_KEY } from '../../types';
import {
@ -138,7 +138,7 @@ export const RowData = ({
<>
{id !== CUSTOM_NODE_KEY && (
<div className="break-all" data-testid="node">
<Radio id={`node-url-${id}`} value={url} label={url} />
<TradingRadio id={`node-url-${id}`} value={url} label={url} />
</div>
)}
<LayoutCell

View File

@ -9,9 +9,9 @@ import { t } from '@vegaprotocol/i18n';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Button,
Dialog,
Icon,
@ -102,8 +102,12 @@ export const OrderEditDialog = ({
noValidate
>
<div className="flex flex-col md:flex-row gap-4">
<FormGroup label={t('Price')} labelFor="limitPrice" className="grow">
<Input
<TradingFormGroup
label={t('Price')}
labelFor="limitPrice"
className="grow"
>
<TradingInput
type="number"
step={step}
{...register('limitPrice', {
@ -119,13 +123,13 @@ export const OrderEditDialog = ({
id="limitPrice"
/>
{errors.limitPrice?.message && (
<InputError intent="danger">
<TradingInputError intent="danger">
{errors.limitPrice.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('Size')} labelFor="size" className="grow">
<Input
</TradingFormGroup>
<TradingFormGroup label={t('Size')} labelFor="size" className="grow">
<TradingInput
type="number"
step={stepSize}
{...register('size', {
@ -139,9 +143,11 @@ export const OrderEditDialog = ({
id="size"
/>
{errors.size?.message && (
<InputError intent="danger">{errors.size.message}</InputError>
<TradingInputError intent="danger">
{errors.size.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
</div>
<Button variant="primary" size="md" type="submit">
{t('Update')}

View File

@ -172,7 +172,7 @@ module.exports = {
900: '#F9FAFA',
},
},
danger: '#FF077F',
danger: '#EC003C',
warning: '#FF8700',
success: '#00F780',
},

View File

@ -16,8 +16,8 @@ export * from './form-group';
export * from './healthbar';
export * from './icon';
export * from './indicator';
export * from './input-error';
export * from './input';
export * from './input-error';
export * from './key-value-table';
export * from './link';
export * from './loader';
@ -48,10 +48,18 @@ export * from './tiny-scroll';
export * from './toast';
export * from './toggle';
export * from './tooltip';
export * from './trading-button';
export * from './trading-dropdown';
export * from './traffic-light';
export * from './vega-icons';
export * from './vega-logo';
export * from './viewing-as-user';
export * from './pill';
// Trading specific components
export * from './trading-button';
export * from './trading-checkbox';
export * from './trading-dropdown';
export * from './trading-form-group';
export * from './trading-input-error';
export * from './trading-input';
export * from './trading-radio-group';
export * from './trading-select';

View File

@ -0,0 +1,40 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { TradingCheckbox } from './checkbox';
describe('Checkbox', () => {
it('should render checkbox with label successfully', () => {
render(<TradingCheckbox label="test" />);
expect(screen.getByText('test')).toBeInTheDocument();
});
it('should render a checked checkbox if specified in state', () => {
render(<TradingCheckbox label="label" checked={true} />);
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
it('should render an unchecked checkbox if specified in state', () => {
render(<TradingCheckbox label="unchecked" checked={false} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('should render an indeterminate checkbox if specified in state', () => {
render(<TradingCheckbox label="indeterminate" checked="indeterminate" />);
expect(screen.getByTestId('indeterminate-icon')).toBeInTheDocument();
});
it('fires callback on change if provided', () => {
const callback = jest.fn();
render(
<TradingCheckbox
name="test"
label="onchange"
onCheckedChange={callback}
/>
);
const checkbox = screen.getByText('onchange');
fireEvent.click(checkbox);
expect(callback).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,38 @@
import type { Meta, StoryFn } from '@storybook/react';
import type { TradingCheckboxProps } from './checkbox';
import { TradingCheckbox } from './checkbox';
export default {
component: TradingCheckbox,
title: 'Checkbox',
} as Meta<typeof TradingCheckbox>;
const Template: StoryFn<TradingCheckboxProps> = (args) => (
<TradingCheckbox {...args} />
);
export const Default = Template.bind({});
Default.args = {
name: 'default',
label: 'Regular checkbox',
};
export const Overflow = Template.bind({});
Overflow.args = {
name: 'overflow',
label:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
label: 'Disabled',
};
export const Indeterminate = Template.bind({});
Indeterminate.args = {
name: 'default',
checked: 'indeterminate',
label: 'Indeterminate checkbox',
};

View File

@ -0,0 +1,63 @@
import { VegaIcon, VegaIconNames } from '../icon';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import classNames from 'classnames';
import type { ReactNode } from 'react';
type CheckedState = boolean | 'indeterminate';
export interface TradingCheckboxProps {
checked?: CheckedState;
label?: ReactNode;
name?: string;
onCheckedChange?: (checked: CheckedState) => void;
disabled?: boolean;
}
export const TradingCheckbox = ({
checked,
label,
name,
onCheckedChange,
disabled = false,
}: TradingCheckboxProps) => {
const rootClasses = classNames(
'relative flex justify-center items-center w-3 h-3',
'border rounded-sm overflow-hidden',
'border-vega-clight-500 dark:border-vega-cdark-500',
'aria-checked:border-vega-clight-400 dark:aria-checked:border-vega-cdark-400',
'disabled:border-vega-clight-600 dark:disabled:border-vega-cdark-600',
'bg-vega-clight-700 dark:bg-vega-cdark-700'
);
return (
<div className="flex gap-1.5 items-center">
<CheckboxPrimitive.Root
name={name}
id={name}
className={rootClasses}
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
data-testid={name}
>
<CheckboxPrimitive.CheckboxIndicator className="flex justify-center items-center w-3 h-3">
{checked === 'indeterminate' ? (
<span
data-testid="indeterminate-icon"
className="absolute w-[8px] h-[2px] bg-vega-clight-50 dark:bg-vega-cdark-50"
/>
) : (
<VegaIcon name={VegaIconNames.TICK} size={10} />
)}
</CheckboxPrimitive.CheckboxIndicator>
</CheckboxPrimitive.Root>
<label
htmlFor={name}
className={classNames('text-xs flex-1', {
'text-vega-clight-200 dark:text-vega-cdark-200': disabled,
})}
>
{label}
</label>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './checkbox';

View File

@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react';
import { TradingFormGroup } from './form-group';
describe('FormGroup', () => {
it('should render label if given a label', () => {
render(
<TradingFormGroup label="label" labelFor="test">
<input id="test"></input>
</TradingFormGroup>
);
expect(screen.getByLabelText('label')).toBeInTheDocument();
});
it('should render children', () => {
render(
<TradingFormGroup label="label" labelFor="test">
<input data-testid="foo" id="test"></input>
</TradingFormGroup>
);
expect(screen.getByTestId('foo')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,53 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
export interface TradingFormGroupProps {
children: ReactNode;
className?: string;
label: string | ReactNode; // For accessibility reasons this must always be set for screen readers. If you want it to not show, then use the hideLabel prop"
labelFor: string; // Same as above
hideLabel?: boolean;
disabled?: boolean;
labelDescription?: string;
labelAlign?: 'left' | 'right';
compact?: boolean;
}
export const TradingFormGroup = ({
children,
className,
label,
labelFor,
labelDescription,
labelAlign = 'left',
hideLabel = false,
compact = false,
disabled = false,
}: TradingFormGroupProps) => {
const wrapperClasses = classNames(
'relative',
{
'mb-2': compact,
'mb-4': !compact,
},
className
);
const labelClasses = classNames('block mb-2 text-xs', {
'text-right': labelAlign === 'right',
'sr-only': hideLabel,
'text-muted': disabled,
});
return (
<div data-testid="form-group" className={wrapperClasses}>
{label && (
<label htmlFor={labelFor} className={labelClasses}>
{label}
{labelDescription && (
<div className="font-light mt-1">{labelDescription}</div>
)}
</label>
)}
{children}
</div>
);
};

View File

@ -0,0 +1,47 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInput } from '../trading-input';
import type { TradingFormGroupProps } from './form-group';
import { TradingFormGroup } from './form-group';
export default {
component: TradingFormGroup,
title: 'FormGroup',
argTypes: {
label: {
type: 'string',
},
labelFor: {
type: 'string',
},
labelDescription: {
type: 'string',
},
className: {
type: 'string',
},
hasError: {
type: 'boolean',
},
disabled: {
type: 'boolean',
},
},
} as Meta;
const Template: StoryFn<TradingFormGroupProps> = (args) => (
<TradingFormGroup {...args}>
<TradingInput id="labelFor" />
</TradingFormGroup>
);
export const Default = Template.bind({});
Default.args = {
label: 'Label',
labelFor: 'labelFor',
};
export const WithLabelDescription = Template.bind({});
WithLabelDescription.args = {
label: 'Label',
labelFor: 'labelFor',
labelDescription: 'Description text',
};

View File

@ -0,0 +1 @@
export * from './form-group';

View File

@ -0,0 +1 @@
export * from './input-error';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { TradingInputError } from './input-error';
describe('InputError', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingInputError />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInputError } from './input-error';
export default {
component: TradingInputError,
title: 'InputError',
} as Meta;
const Template: StoryFn = (args) => <TradingInputError {...args} />;
export const Danger = Template.bind({});
Danger.args = {
children: 'An error that might have happened',
};
export const Warning = Template.bind({});
Warning.args = {
intent: 'warning',
children: 'Something that might be an issue',
};

View File

@ -0,0 +1,42 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
interface TradingInputErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
intent?: 'danger' | 'warning';
forInput?: string;
testId?: string;
}
export const TradingInputError = ({
intent = 'danger',
children,
forInput,
testId,
className,
...props
}: TradingInputErrorProps) => {
const effectiveClassName = classNames(
'text-xs flex items-center first-letter:uppercase',
'mt-2',
{
'border-danger': intent === 'danger',
'border-warning': intent === 'warning',
},
{
'text-warning': intent === 'warning',
'text-danger': intent === 'danger',
}
);
return (
<div
data-testid={testId || 'input-error-text'}
aria-describedby={forInput}
className={classNames(effectiveClassName, className)}
{...props}
role="alert"
>
{children}
</div>
);
};

View File

@ -0,0 +1 @@
export * from './input';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { TradingInput } from './input';
describe('Input', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingInput />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,83 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInput } from './input';
import { FormGroup } from '../form-group';
export default {
component: TradingInput,
title: 'Input',
} as Meta;
const Template: StoryFn = (args) => (
<FormGroup label="Hello" labelFor={args.id}>
<TradingInput value="I type words" {...args} />
</FormGroup>
);
const customElementPlaceholder = (
<span
style={{
fontFamily: 'monospace',
backgroundColor: 'grey',
padding: '4px',
}}
>
Ω
</span>
);
export const Default = Template.bind({});
Default.args = {
id: 'input-default',
};
export const WithError = Template.bind({});
WithError.args = {
hasError: true,
id: 'input-has-error',
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
id: 'input-disabled',
};
export const TypeDate = Template.bind({});
TypeDate.args = {
type: 'date',
id: 'input-date',
};
export const TypeDateTime = Template.bind({});
TypeDateTime.args = {
type: 'datetime-local',
id: 'input-datetime-local',
min: '2022-09-05T11:29:17',
max: '2023-09-05T10:29:49',
};
export const IconPrepend = Template.bind({});
IconPrepend.args = {
prependIconName: 'search',
id: 'input-icon-prepend',
};
export const IconAppend = Template.bind({});
IconAppend.args = {
value: 'I type words and even more words',
appendIconName: 'search',
id: 'input-icon-append',
};
export const ElementPrepend = Template.bind({});
ElementPrepend.args = {
value: '<- custom element',
prependElement: customElementPlaceholder,
id: 'input-element-prepend',
};
export const ElementAppend = Template.bind({});
ElementAppend.args = {
value: 'custom element ->',
appendElement: customElementPlaceholder,
id: 'input-element-append',
};

View File

@ -0,0 +1,174 @@
import type { InputHTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import type { IconName } from '../icon';
import { Icon } from '../icon';
import { defaultFormElement } from '../../utils/shared';
type InputRootProps = InputHTMLAttributes<HTMLInputElement> & {
hasError?: boolean;
disabled?: boolean;
className?: string;
};
type NoPrepend = {
prependIconName?: never;
prependIconDescription?: string;
prependElement?: never;
};
type NoAppend = {
appendIconName?: never;
appendIconDescription?: string;
appendElement?: never;
};
type InputPrepend = NoAppend &
(
| NoPrepend
| {
prependIconName: IconName;
prependIconDescription?: string;
prependElement?: never;
}
| {
prependIconName?: never;
prependIconDescription?: never;
prependElement: ReactNode;
}
);
type InputAppend = NoPrepend &
(
| NoAppend
| {
appendIconName: IconName;
appendIconDescription?: string;
appendElement?: never;
}
| {
appendIconName?: never;
appendIconDescription?: never;
appendElement: ReactNode;
}
);
type AffixProps = InputPrepend | InputAppend;
export type TradingInputProps = InputRootProps & AffixProps;
export const tradingInputStyle = ({
style,
disabled,
}: {
style?: React.CSSProperties;
disabled?: boolean;
}) =>
disabled
? {
...style,
backgroundImage:
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAAXNSR0IArs4c6QAAACNJREFUGFdjtLS0/M8ABcePH2eEsRlJl4BpBdHIuuFmEi0BABqjEQVjx/LTAAAAAElFTkSuQmCC)',
}
: style;
const getAffixElement = ({
prependElement,
prependIconName,
prependIconDescription,
appendElement,
appendIconName,
appendIconDescription,
}: Pick<TradingInputProps, keyof AffixProps>) => {
const position = prependIconName || prependElement ? 'pre' : 'post';
const className = classNames(
['fill-black dark:fill-white', 'absolute', 'z-10'],
{
'left-3': position === 'pre',
'right-3': position === 'post',
}
);
const element = prependElement || appendElement;
const iconName = prependIconName || appendIconName;
const iconDescription = prependIconDescription || appendIconDescription;
if (element) {
return <div className={className}>{element}</div>;
}
if (iconName) {
return (
<Icon
name={iconName}
className={className}
aria-label={iconDescription}
aria-hidden={!iconDescription}
/>
);
}
return null;
};
export const TradingInput = forwardRef<HTMLInputElement, TradingInputProps>(
(
{
prependIconName,
prependIconDescription,
appendIconName,
appendIconDescription,
prependElement,
appendElement,
className,
hasError,
...props
},
ref
) => {
const hasPrepended = !!(prependIconName || prependElement);
const hasAppended = !!(appendIconName || appendElement);
const inputClassName = classNames(
'appearance-none dark:color-scheme-dark px-3 h-8',
className,
{
'pl-9': hasPrepended,
'pr-9': hasAppended,
}
);
const input = (
<input
{...props}
ref={ref}
className={classNames(
defaultFormElement(hasError, props.disabled),
inputClassName
)}
/>
);
const element = getAffixElement({
prependIconName,
prependIconDescription,
appendIconName,
appendIconDescription,
prependElement,
appendElement,
});
if (element) {
return (
<div className="flex items-center relative">
{hasPrepended && element}
{input}
{hasAppended && element}
</div>
);
}
return input;
}
);

View File

@ -0,0 +1 @@
export * from './radio-group';

View File

@ -0,0 +1,22 @@
import type { StoryFn, Meta } from '@storybook/react';
import type { TradingRadioGroupProps } from './radio-group';
import { TradingRadioGroup, TradingRadio } from './radio-group';
export default {
component: TradingRadioGroup,
title: 'RadioGroup',
} as Meta;
const Template: StoryFn<TradingRadioGroupProps> = (args) => (
<TradingRadioGroup {...args}>
<TradingRadio id="item-1" value="1" label="Item 1" />
<TradingRadio id="item-2" value="2" label="Item 2" />
<TradingRadio id="item-3" value="3" label="Disabled item" disabled={true} />
</TradingRadioGroup>
);
export const Vertical = Template.bind({});
export const Horizontal = Template.bind({});
Horizontal.args = {
orientation: 'horizontal',
};

View File

@ -0,0 +1,99 @@
import { forwardRef } from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import type { ReactNode } from 'react';
export interface TradingRadioGroupProps {
name?: string;
children: ReactNode;
defaultValue?: string;
value?: string;
orientation?: 'horizontal' | 'vertical';
onChange?: (value: string) => void;
className?: string;
}
export const TradingRadioGroup = forwardRef<
HTMLDivElement,
TradingRadioGroupProps
>(
(
{
children,
name,
value,
orientation = 'vertical',
onChange,
className,
}: TradingRadioGroupProps,
ref
) => {
const groupClasses = classNames(
'flex text-sm',
{
'flex-col gap-2': orientation === 'vertical',
'flex-row gap-4': orientation === 'horizontal',
},
className
);
return (
<RadioGroupPrimitive.Root
ref={ref}
name={name}
value={value}
onValueChange={onChange}
orientation={orientation}
className={groupClasses}
>
{children}
</RadioGroupPrimitive.Root>
);
}
);
interface RadioProps {
id: string;
value: string;
label: string;
disabled?: boolean;
}
export const TradingRadio = ({ id, value, label, disabled }: RadioProps) => {
const wrapperClasses = classNames('flex items-center gap-1.5 text-xs');
const itemClasses = classNames(
'flex justify-center items-center',
'w-3 h-3 rounded-full border',
'border-vega-clight-500 dark:border-vega-cdark-500',
'aria-checked:border-vega-clight-400 dark:aria-checked:border-vega-cdark-400',
'disabled:border-vega-clight-600 dark:disabled:border-vega-cdark-600',
'bg-vega-clight-700 dark:bg-vega-cdark-700'
);
const indicatorClasses = classNames(
'block w-2.5 h-2.5 border-2 rounded-full',
'bg-vega-clight-50 dark:bg-vega-cdark-50',
'border-vega-clight-700 dark:border-vega-cdark-700'
);
return (
<div className={wrapperClasses}>
<RadioGroupPrimitive.Item
value={value}
className={itemClasses}
id={id}
data-testid={id}
disabled={disabled}
>
<RadioGroupPrimitive.Indicator className={indicatorClasses} />
</RadioGroupPrimitive.Item>
<label
htmlFor={id}
className={
disabled
? 'text-vega-clight-200 dark:text-vega-cdark-200'
: 'cursor-pointer'
}
>
{label}
</label>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './select';

View File

@ -0,0 +1,37 @@
import { render } from '@testing-library/react';
import { TradingRichSelect, TradingSelect, TradingOption } from './select';
describe('Select', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingSelect />);
expect(baseElement).toBeTruthy();
});
});
describe('RichSelect', () => {
it('should render select element with placeholder when no value is pre-selected', async () => {
const { findByTestId } = render(
<TradingRichSelect placeholder={'Select'}>
<TradingOption value={'1'}>1</TradingOption>
<TradingOption value={'2'}>2</TradingOption>
</TradingRichSelect>
);
const btn = (await findByTestId(
'rich-select-trigger'
)) as HTMLButtonElement;
expect(btn.textContent).toEqual('Select');
});
it('should render select element with pre-selected value', async () => {
const { findByTestId } = render(
<TradingRichSelect placeholder={'Select'} value={'1'}>
<TradingOption value={'1'}>1</TradingOption>
<TradingOption value={'2'}>2</TradingOption>
</TradingRichSelect>
);
const btn = (await findByTestId(
'rich-select-trigger'
)) as HTMLButtonElement;
expect(btn.textContent).toEqual('1');
});
});

View File

@ -0,0 +1,92 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingOption, TradingSelect, TradingRichSelect } from './select';
import { FormGroup } from '../form-group';
export default {
component: TradingSelect,
title: 'Select',
} as Meta;
const Template: StoryFn = (args) => (
<FormGroup label="Select an option" labelFor={args.id}>
<TradingSelect {...args}>
<option value="Option 1">Option 1</option>
<option value="Option 2">Option 2</option>
<option value="Option 3">Option 3</option>
</TradingSelect>
</FormGroup>
);
const RichSelectTemplate: StoryFn = ({ placeholder, ...props }) => (
<FormGroup label="Select an option" labelFor={props.id}>
<TradingRichSelect placeholder={placeholder} {...props} />
</FormGroup>
);
export const Default = Template.bind({});
Default.args = {
id: 'select-default',
};
export const WithError = Template.bind({});
WithError.args = {
id: 'select-has-error',
hasError: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
id: 'select-disabled',
disabled: true,
};
export const RichDefaultSelect = RichSelectTemplate.bind({});
RichDefaultSelect.args = {
id: 'rich',
name: 'rich',
placeholder: 'Select an option',
onValueChange: (v: string) => {
// eslint-disable-next-line no-console
console.log(v);
},
children: (
<>
<TradingOption value="1">
<div className="flex flex-col justify-start items-start">
<span>Option One</span>
<span className="text-xs">First option</span>
</div>
</TradingOption>
<TradingOption value="2">
<div className="flex flex-col justify-start items-start">
<span>Option Two</span>
<span className="text-xs">Second option</span>
</div>
</TradingOption>
<TradingOption value="3">
<div className="flex flex-col justify-start items-start">
<span>Option Three</span>
<span className="text-xs">Third option</span>
</div>
</TradingOption>
<TradingOption value="4">
<div className="flex flex-col justify-start items-start">
<span>Option Four</span>
<span className="text-xs">Fourth option</span>
</div>
</TradingOption>
<TradingOption value="5">
<div className="flex flex-col justify-start items-start">
<span>Option Five</span>
<span className="text-xs">Fifth option</span>
</div>
</TradingOption>
<TradingOption value="6">
<div className="flex flex-col justify-start items-start">
<span>Option Six</span>
<span className="text-xs">Sixth option</span>
</div>
</TradingOption>
</>
),
};

View File

@ -0,0 +1,129 @@
import type { Ref, SelectHTMLAttributes } from 'react';
import { useRef } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import { Icon } from '..';
import { defaultSelectElement } from '../../utils/shared';
import * as SelectPrimitive from '@radix-ui/react-select';
export interface TradingSelectProps
extends SelectHTMLAttributes<HTMLSelectElement> {
hasError?: boolean;
className?: string;
value?: string | number;
children?: React.ReactNode;
}
export const TradingSelect = forwardRef<HTMLSelectElement, TradingSelectProps>(
({ className, hasError, ...props }, ref) => (
<div className="flex items-center relative">
<select
ref={ref}
{...props}
className={classNames(
defaultSelectElement(hasError, props.disabled),
className,
'appearance-none rounded-md'
)}
/>
<Icon
name="chevron-down"
className="absolute right-4 z-10 pointer-events-none"
/>
</div>
)
);
export type TradingRichSelectProps = React.ComponentProps<
typeof SelectPrimitive.Root
> & {
placeholder: string;
hasError?: boolean;
id?: string;
'data-testid'?: string;
};
export const TradingRichSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
TradingRichSelectProps
>(({ id, children, placeholder, hasError, ...props }, forwardedRef) => {
const containerRef = useRef<HTMLDivElement>();
const contentRef = useRef<HTMLDivElement>();
return (
<div
ref={containerRef as Ref<HTMLDivElement>}
className="flex items-center relative"
>
<SelectPrimitive.Root {...props} defaultOpen={false}>
<SelectPrimitive.Trigger
data-testid={props['data-testid'] || 'rich-select-trigger'}
className={classNames(
defaultSelectElement(hasError, props.disabled),
'rounded-md pl-2 pr-11',
'max-w-full overflow-hidden break-all'
)}
id={id}
ref={forwardedRef}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className={classNames('absolute right-4')}>
<Icon name="chevron-down" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal container={containerRef.current}>
<SelectPrimitive.Content
ref={contentRef as Ref<HTMLDivElement>}
className={classNames(
'relative',
'z-20',
'bg-white dark:bg-black',
'border border-neutral-500 focus:border-black dark:focus:border-white rounded',
'overflow-hidden',
'shadow-lg'
)}
position={'item-aligned'}
side={'bottom'}
align={'center'}
>
<SelectPrimitive.ScrollUpButton className="flex items-center justify-center py-1 absolute w-full h-6 z-20 bg-gradient-to-t from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-up" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton className="flex items-center justify-center py-1 absolute bottom-0 w-full h-6 z-20 bg-gradient-to-b from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-down" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
</div>
);
});
export const TradingOption = forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentProps<typeof SelectPrimitive.Item>
>(({ children, className, ...props }, forwardedRef) => (
<SelectPrimitive.Item
data-testid="rich-select-option"
className={classNames(
'relative',
'text-black dark:text-white',
'cursor-pointer outline-none',
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
'focus:bg-neutral-100 dark:focus:bg-neutral-800',
'pl-2 py-2',
'pr-12',
'w-full',
'text-sm',
'data-selected:bg-vega-yellow dark:data-selected:text-black dark:data-selected:bg-vega-yellow',
className
)}
{...props}
ref={forwardedRef}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator className="absolute right-4 top-[50%] translate-y-[-50%]">
<Icon name="tick" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
));

View File

@ -1,18 +1,20 @@
import classnames from 'classnames';
export const defaultSelectElement = (hasError?: boolean) =>
classnames(defaultFormElement(hasError), 'pr-10 dark:bg-black');
export const defaultSelectElement = (hasError?: boolean, disabled?: boolean) =>
classnames(defaultFormElement(hasError, disabled), 'pr-10 min-h-8 py-1');
export const defaultFormElement = (hasError?: boolean) =>
export const defaultFormElement = (hasError?: boolean, disabled?: boolean) =>
classnames(
'flex items-center w-full text-sm',
'p-2 rounded whitespace-nowrap text-ellipsis overflow-hidden',
'bg-transparent',
'border',
'focus:border-vega-light-300 dark:focus:border-vega-dark-300',
'disabled:opacity-60',
'focus:border-vega-clight-400 dark:focus:border-vega-cdark-400',
{
'border-vega-pink text-vega-pink': hasError,
'border-vega-light-200 dark:border-vega-dark-200': !hasError,
'bg-vega-clight-700 dark:bg-vega-cdark-700': !disabled && !hasError,
'bg-transparent': disabled || hasError,
'border-vega-clight-600 dark:border-vega-cdark-600': disabled,
'border-vega-red-500': !disabled && hasError,
'border-vega-clight-500 dark:border-vega-cdark-500':
!disabled && !hasError,
}
);

View File

@ -2,11 +2,11 @@ import classNames from 'classnames';
import { create } from 'zustand';
import {
Dialog,
FormGroup,
Input,
Intent,
Pill,
TradingButton,
TradingFormGroup,
TradingInput,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@ -421,17 +421,17 @@ const CustomUrlInput = ({
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
</button>
</div>
<FormGroup
<TradingFormGroup
labelFor="wallet-url"
label={t('Custom wallet location')}
hideLabel
>
<Input
<TradingInput
value={walletUrl}
onChange={(e) => setWalletUrl(e.target.value)}
name="wallet-url"
/>
</FormGroup>
</TradingFormGroup>
<ConnectionOption
disabled={!isDesktopWalletRunning}
type="jsonRpc"

View File

@ -1,8 +1,8 @@
import { t } from '@vegaprotocol/i18n';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Intent,
TradingButton,
VegaIcon,
@ -60,8 +60,8 @@ export function ViewConnectorForm({
'Browse from the perspective of another Vega user in read-only mode.'
)}
</p>
<FormGroup label={t('Vega Pubkey')} labelFor="address">
<Input
<TradingFormGroup label={t('Vega Pubkey')} labelFor="address">
<TradingInput
{...register('address', {
required: t('Required'),
validate: validatePubkey,
@ -71,9 +71,11 @@ export function ViewConnectorForm({
type="text"
/>
{errors.address?.message && (
<InputError intent="danger">{errors.address.message}</InputError>
<TradingInputError intent="danger">
{errors.address.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
<TradingButton
data-testid="connect"
intent={Intent.Info}

View File

@ -12,11 +12,11 @@ import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Notification,
RichSelect,
TradingRichSelect,
ExternalLink,
Intent,
} from '@vegaprotocol/ui-toolkit';
@ -150,7 +150,7 @@ export const WithdrawForm = ({
field: ControllerRenderProps<FormFields, 'asset'>;
}) => {
return (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id="asset"
name="asset"
@ -170,7 +170,7 @@ export const WithdrawForm = ({
balance={<AssetBalance asset={a} />}
/>
))}
</RichSelect>
</TradingRichSelect>
);
};
@ -193,7 +193,7 @@ export const WithdrawForm = ({
noValidate={true}
data-testid="withdraw-form"
>
<FormGroup label={t('Asset')} labelFor="asset">
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@ -205,10 +205,12 @@ export const WithdrawForm = ({
render={renderAssetsSelector}
/>
{errors.asset?.message && (
<InputError intent="danger">{errors.asset.message}</InputError>
<TradingInputError intent="danger">
{errors.asset.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup
</TradingFormGroup>
<TradingFormGroup
label={t('To (Ethereum address)')}
labelFor="ethereum-address"
>
@ -218,15 +220,17 @@ export const WithdrawForm = ({
clearErrors('to');
}}
/>
<Input
<TradingInput
id="ethereum-address"
data-testid="eth-address-input"
{...register('to', { validate: { required, ethereumAddress } })}
/>
{errors.to?.message && (
<InputError intent="danger">{errors.to.message}</InputError>
<TradingInputError intent="danger">
{errors.to.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
{selectedAsset && threshold && (
<div className="mb-4">
<WithdrawLimits
@ -238,8 +242,8 @@ export const WithdrawForm = ({
/>
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount">
<Input
<TradingFormGroup label={t('Amount')} labelFor="amount">
<TradingInput
data-testid="amount-input"
type="number"
autoComplete="off"
@ -259,7 +263,9 @@ export const WithdrawForm = ({
})}
/>
{errors.amount?.message && (
<InputError intent="danger">{errors.amount.message}</InputError>
<TradingInputError intent="danger">
{errors.amount.message}
</TradingInputError>
)}
{selectedAsset && (
<UseButton
@ -282,7 +288,7 @@ export const WithdrawForm = ({
/>
</div>
)}
</FormGroup>
</TradingFormGroup>
<Button
data-testid="submit-withdrawal"
type="submit"