mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-16 21:16:35 +00:00
Do dropdown better
This commit is contained in:
parent
1c601f58e4
commit
af12e36606
@ -8,38 +8,19 @@ import { addressBookSelectors } from 'features/addressBook';
|
|||||||
import { Address, Identicon } from 'components/ui';
|
import { Address, Identicon } from 'components/ui';
|
||||||
import './AddressFieldDropdown.scss';
|
import './AddressFieldDropdown.scss';
|
||||||
|
|
||||||
/**
|
interface Props {
|
||||||
* @desc The `onChangeOverride` prop needs to work
|
addressInput: string;
|
||||||
* with actual events, but also needs a value to be directly passed in
|
|
||||||
* occasionally. This interface allows us to skip all of the other FormEvent
|
|
||||||
* properties and methods.
|
|
||||||
*/
|
|
||||||
interface FakeFormEvent {
|
|
||||||
currentTarget: {
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
value?: string;
|
|
||||||
dropdownThreshold?: number;
|
dropdownThreshold?: number;
|
||||||
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||||
currentTo: ReturnType<typeof transactionSelectors.getToRaw>;
|
onEntryClick(address: string): void;
|
||||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement> | FakeFormEvent): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
setCurrentTo: transactionActions.TSetCurrentTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps;
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
activeIndex: number | null;
|
activeIndex: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddressFieldDropdown extends React.Component<Props> {
|
class AddressFieldDropdownClass extends React.Component<Props, State> {
|
||||||
public state: State = {
|
public state = {
|
||||||
activeIndex: null
|
activeIndex: null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,67 +33,77 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { value, currentTo, dropdownThreshold = 3 } = this.props;
|
const { addressInput } = this.props;
|
||||||
const stringInQuestion = value != null ? value : currentTo;
|
|
||||||
const noMatchContent = stringInQuestion.startsWith('0x') ? null : (
|
|
||||||
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
|
|
||||||
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{stringInQuestion}".
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
return stringInQuestion.length >= dropdownThreshold ? (
|
return (
|
||||||
<ul className="AddressFieldDropdown" role="listbox">
|
this.getIsVisible() && (
|
||||||
{this.getFilteredLabels().length > 0 ? this.renderDropdownItems() : noMatchContent}
|
<ul className="AddressFieldDropdown" role="listbox">
|
||||||
</ul>
|
{this.getFilteredLabels().length > 0 ? (
|
||||||
) : null;
|
this.renderDropdownItems()
|
||||||
|
) : (
|
||||||
|
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
|
||||||
|
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{
|
||||||
|
addressInput
|
||||||
|
}".
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDropdownItems = () =>
|
private renderDropdownItems = () => {
|
||||||
this.getFilteredLabels().map((filteredLabel, index: number) => {
|
const { onEntryClick } = this.props;
|
||||||
const { onChangeOverride, setCurrentTo } = this.props;
|
const { activeIndex } = this.state;
|
||||||
const { activeIndex } = this.state;
|
|
||||||
const { address, label } = filteredLabel;
|
|
||||||
const isActive = activeIndex === index;
|
|
||||||
const className = `AddressFieldDropdown-dropdown-item ${
|
|
||||||
isActive ? 'AddressFieldDropdown-dropdown-item--active' : ''
|
|
||||||
}`;
|
|
||||||
|
|
||||||
return (
|
return this.getFilteredLabels().map(
|
||||||
<li
|
({ address, label }: { address: string; label: string }, index: number) => {
|
||||||
key={address}
|
const isActive = activeIndex === index;
|
||||||
className={className}
|
const className = `AddressFieldDropdown-dropdown-item ${
|
||||||
onClick={() =>
|
isActive ? 'AddressFieldDropdown-dropdown-item--active' : ''
|
||||||
onChangeOverride
|
}`;
|
||||||
? onChangeOverride({ currentTarget: { value: address } })
|
|
||||||
: setCurrentTo(address)
|
return (
|
||||||
}
|
<li
|
||||||
role="option"
|
key={address}
|
||||||
title={`${translateRaw('SEND_TO')}${label}`}
|
role="option"
|
||||||
>
|
className={className}
|
||||||
<div className="AddressFieldDropdown-dropdown-item-identicon">
|
onClick={() => onEntryClick(address)}
|
||||||
<Identicon address={address} />
|
title={`${translateRaw('SEND_TO')}${label}`}
|
||||||
</div>
|
>
|
||||||
<strong className="AddressFieldDropdown-dropdown-item-label">{label}</strong>
|
<div className="AddressFieldDropdown-dropdown-item-identicon">
|
||||||
<em className="AddressFieldDropdown-dropdown-item-address">
|
<Identicon address={address} />
|
||||||
<Address address={address} />
|
</div>
|
||||||
</em>
|
<strong className="AddressFieldDropdown-dropdown-item-label">{label}</strong>
|
||||||
</li>
|
<em className="AddressFieldDropdown-dropdown-item-address">
|
||||||
);
|
<Address address={address} />
|
||||||
});
|
</em>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private getFilteredLabels = () => {
|
private getFilteredLabels = () => {
|
||||||
const { value, currentTo } = this.props;
|
const { addressInput, labelAddresses } = this.props;
|
||||||
const includedString = value != null ? value : currentTo.toLowerCase();
|
|
||||||
|
|
||||||
return Object.keys(this.props.labelAddresses)
|
return Object.keys(labelAddresses)
|
||||||
.filter(label => label.toLowerCase().includes(includedString))
|
.filter(label => label.toLowerCase().includes(addressInput))
|
||||||
.map(label => ({ address: this.props.labelAddresses[label], label }))
|
.map(label => ({ address: labelAddresses[label], label }))
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
private getIsVisible = () =>
|
private getIsVisible = () => {
|
||||||
this.props.currentTo.length > 1 && this.getFilteredLabels().length > 0;
|
const { addressInput, dropdownThreshold = 3 } = this.props;
|
||||||
|
|
||||||
|
return addressInput.length >= dropdownThreshold && this.getFilteredLabels().length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
private setActiveIndex = (activeIndex: number | null) => this.setState({ activeIndex });
|
||||||
|
|
||||||
|
private clearActiveIndex = () => this.setActiveIndex(null);
|
||||||
|
|
||||||
|
//#region Keyboard Controls
|
||||||
private handleKeyDown = (e: KeyboardEvent) => {
|
private handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (this.getIsVisible()) {
|
if (this.getIsVisible()) {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
@ -132,6 +123,7 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private handleEnterKeyDown = () => {
|
private handleEnterKeyDown = () => {
|
||||||
|
const { onEntryClick } = this.props;
|
||||||
const { activeIndex } = this.state;
|
const { activeIndex } = this.state;
|
||||||
|
|
||||||
if (activeIndex !== null) {
|
if (activeIndex !== null) {
|
||||||
@ -139,7 +131,7 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||||||
|
|
||||||
filteredLabels.forEach(({ address }, index) => {
|
filteredLabels.forEach(({ address }, index) => {
|
||||||
if (activeIndex === index) {
|
if (activeIndex === index) {
|
||||||
this.props.setCurrentTo(address);
|
onEntryClick(address);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,16 +167,108 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||||||
|
|
||||||
this.setState({ activeIndex });
|
this.setState({ activeIndex });
|
||||||
};
|
};
|
||||||
|
//#endregion Keyboard Controls
|
||||||
private setActiveIndex = (activeIndex: number | null) => this.setState({ activeIndex });
|
|
||||||
|
|
||||||
private clearActiveIndex = () => this.setActiveIndex(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
//#region Uncontrolled
|
||||||
|
/**
|
||||||
|
* @desc The `onChangeOverride` prop needs to work
|
||||||
|
* with actual events, but also needs a value to be directly passed in
|
||||||
|
* occasionally. This interface allows us to skip all of the other FormEvent
|
||||||
|
* properties and methods.
|
||||||
|
*/
|
||||||
|
interface FakeFormEvent {
|
||||||
|
currentTarget: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UncontrolledAddressFieldDropdownProps {
|
||||||
|
value: string;
|
||||||
|
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||||
|
dropdownThreshold?: number;
|
||||||
|
onChangeOverride(ev: React.FormEvent<HTMLInputElement> | FakeFormEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc The uncontrolled dropdown changes the address input onClick,
|
||||||
|
* as well as calls the onChange override, but does not update the `currentTo`
|
||||||
|
* property in the Redux store.
|
||||||
|
*/
|
||||||
|
function RawUncontrolledAddressFieldDropdown({
|
||||||
|
value,
|
||||||
|
onChangeOverride,
|
||||||
|
labelAddresses,
|
||||||
|
dropdownThreshold
|
||||||
|
}: UncontrolledAddressFieldDropdownProps) {
|
||||||
|
const onEntryClick = (address: string) => onChangeOverride({ currentTarget: { value: address } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddressFieldDropdownClass
|
||||||
|
addressInput={value}
|
||||||
|
onEntryClick={onEntryClick}
|
||||||
|
labelAddresses={labelAddresses}
|
||||||
|
dropdownThreshold={dropdownThreshold}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UncontrolledAddressFieldDropdown = connect((state: AppState) => ({
|
||||||
|
labelAddresses: addressBookSelectors.getLabelAddresses(state)
|
||||||
|
}))(RawUncontrolledAddressFieldDropdown);
|
||||||
|
//#endregion Uncontrolled
|
||||||
|
|
||||||
|
//#region Controlled
|
||||||
|
interface ControlledAddressFieldDropdownProps {
|
||||||
|
currentTo: ReturnType<typeof transactionSelectors.getToRaw>;
|
||||||
|
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||||
|
setCurrentTo: transactionActions.TSetCurrentTo;
|
||||||
|
dropdownThreshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc The controlled dropdown connects directly to the Redux store,
|
||||||
|
* modifying the `currentTo` property onChange.
|
||||||
|
*/
|
||||||
|
function RawControlledAddressFieldDropdown({
|
||||||
|
currentTo,
|
||||||
|
labelAddresses,
|
||||||
|
setCurrentTo,
|
||||||
|
dropdownThreshold
|
||||||
|
}: ControlledAddressFieldDropdownProps) {
|
||||||
|
return (
|
||||||
|
<AddressFieldDropdownClass
|
||||||
|
addressInput={currentTo}
|
||||||
|
onEntryClick={setCurrentTo}
|
||||||
|
labelAddresses={labelAddresses}
|
||||||
|
dropdownThreshold={dropdownThreshold}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ControlledAddressFieldDropdown = connect(
|
||||||
(state: AppState) => ({
|
(state: AppState) => ({
|
||||||
labelAddresses: addressBookSelectors.getLabelAddresses(state),
|
currentTo: transactionSelectors.getToRaw(state),
|
||||||
currentTo: transactionSelectors.getToRaw(state)
|
labelAddresses: addressBookSelectors.getLabelAddresses(state)
|
||||||
}),
|
}),
|
||||||
{ setCurrentTo: transactionActions.setCurrentTo }
|
{
|
||||||
)(AddressFieldDropdown);
|
setCurrentTo: transactionActions.setCurrentTo
|
||||||
|
}
|
||||||
|
)(RawControlledAddressFieldDropdown);
|
||||||
|
//#endregion Controlled
|
||||||
|
|
||||||
|
interface AddressFieldDropdownProps {
|
||||||
|
controlled: boolean;
|
||||||
|
value?: string;
|
||||||
|
dropdownThreshold?: number;
|
||||||
|
onChangeOverride?(ev: React.FormEvent<HTMLInputElement> | FakeFormEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddressFieldDropdown({
|
||||||
|
controlled = true,
|
||||||
|
...props
|
||||||
|
}: AddressFieldDropdownProps) {
|
||||||
|
const Dropdown = controlled ? ControlledAddressFieldDropdown : UncontrolledAddressFieldDropdown;
|
||||||
|
|
||||||
|
return <Dropdown {...props} />;
|
||||||
|
}
|
||||||
|
@ -94,8 +94,14 @@ class AddressInputFactoryClass extends Component<Props> {
|
|||||||
addr = addHexPrefix(currentTo.value ? currentTo.value.toString('hex') : '0');
|
addr = addHexPrefix(currentTo.value ? currentTo.value.toString('hex') : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc If passed a value and an onChangeOverride function,
|
||||||
|
* infer that the dropdown should be uncontrolled.
|
||||||
|
*/
|
||||||
|
const controlled = value == null && !onChangeOverride;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`AddressInput form-group`}>
|
<div className={'AddressInput form-group'}>
|
||||||
<div className={inputClassName}>
|
<div className={inputClassName}>
|
||||||
<Query
|
<Query
|
||||||
params={['readOnly']}
|
params={['readOnly']}
|
||||||
@ -115,8 +121,9 @@ class AddressInputFactoryClass extends Component<Props> {
|
|||||||
{isFocused &&
|
{isFocused &&
|
||||||
!isENSAddress && (
|
!isENSAddress && (
|
||||||
<AddressFieldDropdown
|
<AddressFieldDropdown
|
||||||
onChangeOverride={onChangeOverride}
|
controlled={controlled}
|
||||||
value={value}
|
value={value}
|
||||||
|
onChangeOverride={onChangeOverride}
|
||||||
dropdownThreshold={dropdownThreshold}
|
dropdownThreshold={dropdownThreshold}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user