From 48d8846e29d8cd66188066a8f91bd0030b231c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Fri, 5 Jul 2024 15:42:42 +0200 Subject: [PATCH] chore: factor out and create new `SendRecipientInput` component - some minor visual fixes (padding & clear button color) - make it always paste plain text, eventhough the base component has to stay RichText - use it in SendModal->RecipientView - add a storybook page - added QML tests Fixes #15252 --- storybook/pages/SendRecipientInputPage.qml | 76 ++++++++ .../qmlTests/tests/tst_SendRecipientInput.qml | 183 ++++++++++++++++++ .../shared/stores/send/TransactionStore.qml | 5 +- ui/StatusQ/include/StatusQ/QClipboardProxy.h | 1 + ui/StatusQ/src/QClipboardProxy.cpp | 5 + .../StatusQ/Controls/StatusClearButton.qml | 2 +- .../src/StatusQ/Controls/StatusInput.qml | 2 +- .../send/controls/SendRecipientInput.qml | 76 ++++++++ ui/imports/shared/popups/send/controls/qmldir | 1 + .../popups/send/views/RecipientView.qml | 50 +---- 10 files changed, 353 insertions(+), 48 deletions(-) create mode 100644 storybook/pages/SendRecipientInputPage.qml create mode 100644 storybook/qmlTests/tests/tst_SendRecipientInput.qml create mode 100644 ui/imports/shared/popups/send/controls/SendRecipientInput.qml diff --git a/storybook/pages/SendRecipientInputPage.qml b/storybook/pages/SendRecipientInputPage.qml new file mode 100644 index 0000000000..215f7900d0 --- /dev/null +++ b/storybook/pages/SendRecipientInputPage.qml @@ -0,0 +1,76 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 + +import Storybook 1.0 + +import utils 1.0 + +import shared.popups.send.controls 1.0 + +SplitView { + id: root + + Logs { id: logs } + + SplitView { + orientation: Qt.Vertical + SplitView.fillWidth: true + + Rectangle { + SplitView.fillHeight: true + SplitView.fillWidth: true + color: Theme.palette.baseColor2 + + SendRecipientInput { + anchors.centerIn: parent + interactive: ctrlInteractive.checked + checkMarkVisible: ctrlCheckmark.checked + Component.onCompleted: forceActiveFocus() + + onClearClicked: logs.logEvent("SendRecipientInput::clearClicked", [], arguments) + onValidateInputRequested: logs.logEvent("SendRecipientInput::validateInputRequested", [], arguments) + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 200 + + logsView.logText: logs.logText + + ColumnLayout { + TextEdit { + readOnly: true + selectByMouse: true + text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4" + } + + Switch { + id: ctrlInteractive + text: "Interactive" + checked: true + } + + Switch { + id: ctrlCheckmark + text: "Checkmark visible" + checked: false + } + } + } + } +} + +// category: Controls + +// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9707-106469&t=MeyLezc91kfFYcm9-0 +// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=10259-120493&t=MeyLezc91kfFYcm9-0 +// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9019-88679&t=MeyLezc91kfFYcm9-0 +// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9707-105782&t=MeyLezc91kfFYcm9-0 + diff --git a/storybook/qmlTests/tests/tst_SendRecipientInput.qml b/storybook/qmlTests/tests/tst_SendRecipientInput.qml new file mode 100644 index 0000000000..2c6548b093 --- /dev/null +++ b/storybook/qmlTests/tests/tst_SendRecipientInput.qml @@ -0,0 +1,183 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils + +import shared.popups.send.controls 1.0 + +Item { + id: root + width: 600 + height: 400 + + Component { + id: componentUnderTest + SendRecipientInput { + anchors.centerIn: parent + focus: true + } + } + + SignalSpy { + id: signalSpyClearClicked + target: controlUnderTest + signalName: "clearClicked" + } + + SignalSpy { + id: signalSpyValidateInputRequested + target: controlUnderTest + signalName: "validateInputRequested" + } + + property SendRecipientInput controlUnderTest: null + + TestCase { + name: "SendRecipientInput" + when: windowShown + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + signalSpyClearClicked.clear() + signalSpyValidateInputRequested.clear() + QClipboardProxy.clear() + } + + function test_basicGeometry() { + verify(!!controlUnderTest) + verify(controlUnderTest.width > 0) + verify(controlUnderTest.height > 0) + } + + function test_textInput() { + verify(!!controlUnderTest) + + verify(controlUnderTest.input.edit.focus) + verify(controlUnderTest.interactive) + compare(controlUnderTest.placeholderText, qsTr("Enter an ENS name or address")) + + keyClick(Qt.Key_0) + keyClick(Qt.Key_X) + keyClick(Qt.Key_D) + keyClick(Qt.Key_E) + keyClick(Qt.Key_A) + keyClick(Qt.Key_D) + keyClick(Qt.Key_B) + keyClick(Qt.Key_E) + keyClick(Qt.Key_E) + keyClick(Qt.Key_F) + + const plainText = StatusQUtils.StringUtils.plainText(controlUnderTest.text) + + // input's text should be what we typed, + compare(plainText, "0xdeadbeef") + // ... and for each letter pressed the signal `validateInputRequested` should be emitted + compare(signalSpyValidateInputRequested.count, plainText.length) + } + + function test_interactive() { + verify(!!controlUnderTest) + verify(controlUnderTest.input.edit.focus) + verify(controlUnderTest.interactive) + + controlUnderTest.interactive = false + + keyClick(Qt.Key_A) + keyClick(Qt.Key_B) + keyClick(Qt.Key_C) + + const plainText = StatusQUtils.StringUtils.plainText(controlUnderTest.text) + + // when non-interactive, any text input should be ignored + compare(plainText, "") + } + + function test_pasteButton() { + verify(!!controlUnderTest) + const pasteButton = findChild(controlUnderTest, "pasteButton") + verify(!!pasteButton) + compare(pasteButton.visible, false) + + // copy sth to clipboard + controlUnderTest.text = "0xdeadbeef" + controlUnderTest.input.edit.selectAll() + controlUnderTest.input.edit.copy() + + // paste button should be visible if input is empty and clipboard is not + compare(pasteButton.visible, false) + controlUnderTest.input.edit.clear() + compare(pasteButton.visible, true) + } + + function test_pasteUsingButton() { + verify(!!controlUnderTest) + const pasteButton = findChild(controlUnderTest, "pasteButton") + verify(!!pasteButton) + tryCompare(pasteButton, "visible", false) + + const richTextToTest = "this is bold text" + + // copy rich text to clipboard + controlUnderTest.text = richTextToTest + controlUnderTest.input.edit.selectAll() + controlUnderTest.input.edit.copy() + controlUnderTest.input.edit.clear() + compare(controlUnderTest.input.edit.length , 0) + + compare(pasteButton.visible, true) + mouseClick(pasteButton) + verify(!controlUnderTest.text.includes("this is bold text")) + } + + function test_pasteUsingKbdShortcut() { + verify(!!controlUnderTest) + const pasteButton = findChild(controlUnderTest, "pasteButton") + verify(!!pasteButton) + tryCompare(pasteButton, "visible", false) + + const richTextToTest = "this is bold text" + + // copy rich text to clipboard + controlUnderTest.text = richTextToTest + controlUnderTest.input.edit.selectAll() + controlUnderTest.input.edit.copy() + controlUnderTest.input.edit.clear() + compare(controlUnderTest.input.edit.length , 0) + + compare(pasteButton.visible, true) + keySequence(StandardKey.Paste) + verify(!controlUnderTest.text.includes("this is bold text")) + } + + function test_clearButton() { + verify(!!controlUnderTest) + verify(controlUnderTest.input.edit.focus) + verify(controlUnderTest.interactive) + + const clearButton = findChild(controlUnderTest, "clearButton") + compare(clearButton.visible, false) + + controlUnderTest.text = "0xdeadbeef" + + // clear button should be visible with some text + verify(clearButton.visible) + mouseClick(clearButton) + compare(StatusQUtils.StringUtils.plainText(controlUnderTest.text), "") + compare(signalSpyClearClicked.count, 1) + + // and when interactive + controlUnderTest.interactive = false + compare(clearButton.visible, false) + } + + function test_checkmark() { + verify(!!controlUnderTest) + compare(controlUnderTest.checkMarkVisible, false) + controlUnderTest.checkMarkVisible = true + const checkmarkIcon = findChild(controlUnderTest, "checkmarkIcon") + verify(!!checkmarkIcon) + verify(checkmarkIcon.visible) + } + } +} diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml index a0d9b11536..70fdfe0e76 100644 --- a/storybook/stubs/shared/stores/send/TransactionStore.qml +++ b/storybook/stubs/shared/stores/send/TransactionStore.qml @@ -71,8 +71,9 @@ QtObject { return textAddrss } - function resolveENS(value) { - return root.mainModuleInst.resolvedENS("", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", "") // return some valid address + function resolveENS(value: string) { + if (!!value && value.endsWith(".eth")) + root.mainModuleInst.resolvedENS("", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", "") // return some valid address } function getAsset(assetsList, symbol) { diff --git a/ui/StatusQ/include/StatusQ/QClipboardProxy.h b/ui/StatusQ/include/StatusQ/QClipboardProxy.h index cb530308b8..f95592dc8c 100644 --- a/ui/StatusQ/include/StatusQ/QClipboardProxy.h +++ b/ui/StatusQ/include/StatusQ/QClipboardProxy.h @@ -55,6 +55,7 @@ public: Q_INVOKABLE bool isValidImageUrl(const QUrl &url, const QStringList &acceptedExtensions) const; Q_INVOKABLE qint64 getFileSize(const QUrl &url) const; Q_INVOKABLE void copyTextToClipboard(const QString& text); + Q_INVOKABLE void clear(); signals: void contentChanged(); diff --git a/ui/StatusQ/src/QClipboardProxy.cpp b/ui/StatusQ/src/QClipboardProxy.cpp index fa4212b572..ab95751265 100644 --- a/ui/StatusQ/src/QClipboardProxy.cpp +++ b/ui/StatusQ/src/QClipboardProxy.cpp @@ -92,3 +92,8 @@ void QClipboardProxy::copyTextToClipboard(const QString &text) { m_clipboard->setText(text); } + +void QClipboardProxy::clear() +{ + m_clipboard->clear(); +} diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml index 15442dd2a1..8179c0b5f1 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml @@ -10,6 +10,6 @@ StatusFlatRoundButton { icon.height: 16 implicitWidth: 24 implicitHeight: 24 - icon.color: Theme.palette.baseColor1 + icon.color: Theme.palette.directColor9 backgroundHoverColor: "transparent" } diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml index 52a2127c47..5cbfe440f5 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml @@ -455,7 +455,7 @@ Item { onKeyPressed: { root.keyPressed(event); } - onEditChanged: { + onEditClicked: { root.editClicked(); } onEditingFinished: { diff --git a/ui/imports/shared/popups/send/controls/SendRecipientInput.qml b/ui/imports/shared/popups/send/controls/SendRecipientInput.qml new file mode 100644 index 0000000000..b7e0225a78 --- /dev/null +++ b/ui/imports/shared/popups/send/controls/SendRecipientInput.qml @@ -0,0 +1,76 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +StatusInput { + id: root + + property bool interactive: true + property bool checkMarkVisible + + signal clearClicked() + signal validateInputRequested() + + placeholderText: qsTr("Enter an ENS name or address") + input.background.color: Theme.palette.indirectColor1 + input.background.border.width: 0 + input.implicitHeight: 56 + rightPadding: 12 + input.clearable: false // custom button below + input.edit.readOnly: !root.interactive + multiline: false + input.edit.textFormat: TextEdit.RichText + + input.rightComponent: RowLayout { + StatusButton { + objectName: "pasteButton" + font.weight: Font.Normal + borderColor: Theme.palette.primaryColor1 + borderWidth: 1 + size: StatusBaseButton.Size.Tiny + text: qsTr("Paste") + visible: root.input.edit.length === 0 && root.input.edit.canPaste + focusPolicy: Qt.NoFocus + onClicked: { + root.input.edit.forceActiveFocus() + root.text = QClipboardProxy.text // paste plain text + root.input.edit.cursorPosition = root.input.edit.length + root.validateInputRequested() + } + } + StatusIcon { + objectName: "checkmarkIcon" + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + icon: "tiny/checkmark" + color: Theme.palette.primaryColor1 + visible: root.checkMarkVisible + } + StatusClearButton { + objectName: "clearButton" + visible: root.input.edit.length !== 0 && root.interactive + onClicked: { + root.input.edit.clear() + root.clearClicked() + } + } + } + + Connections { + target: root.input + function onKeyPressed(event) { + if (event.matches(StandardKey.Paste)) { + event.accepted = true + root.text = QClipboardProxy.text // paste plain text + } + } + } + + Keys.onTabPressed: event.accepted = true + Keys.onReleased: root.validateInputRequested() +} diff --git a/ui/imports/shared/popups/send/controls/qmldir b/ui/imports/shared/popups/send/controls/qmldir index 669d7bd88a..48d32ecbbf 100644 --- a/ui/imports/shared/popups/send/controls/qmldir +++ b/ui/imports/shared/popups/send/controls/qmldir @@ -10,3 +10,4 @@ BalanceExceeded 1.0 BalanceExceeded.qml CollectibleBackButtonWithInfo 1.0 CollectibleBackButtonWithInfo.qml CollectibleNestedDelegate 1.0 CollectibleNestedDelegate.qml HeaderTitleText 1.0 HeaderTitleText.qml +SendRecipientInput 1.0 SendRecipientInput.qml diff --git a/ui/imports/shared/popups/send/views/RecipientView.qml b/ui/imports/shared/popups/send/views/RecipientView.qml index ba9e7a6f79..1d03d038c8 100644 --- a/ui/imports/shared/popups/send/views/RecipientView.qml +++ b/ui/imports/shared/popups/send/views/RecipientView.qml @@ -175,24 +175,14 @@ Loader { Component { id: addressRecipient - StatusInput { - id: recipientInput + SendRecipientInput { width: parent.width height: visible ? implicitHeight: 0 visible: !root.isBridgeTx && !!root.selectedAsset - - placeholderText: qsTr("Enter an ENS name or address") - input.background.color: Theme.palette.indirectColor1 - input.background.border.width: 0 - input.implicitHeight: 56 - input.clearable: false // custom button below - input.edit.readOnly: !root.interactive - multiline: false - input.edit.textFormat: TextEdit.RichText text: root.addressText function validateInput() { - const plainText = store.plainText(recipientInput.text) + const plainText = store.plainText(text) root.isLoading() if (Utils.isValidEns(plainText)) { d.isPending = true @@ -202,38 +192,10 @@ Loader { } } - input.rightComponent: RowLayout { - StatusButton { - font.weight: Font.Normal - borderColor: Theme.palette.primaryColor1 - size: StatusBaseButton.Size.Tiny - text: qsTr("Paste") - visible: recipientInput.input.edit.length === 0 && recipientInput.input.edit.canPaste - focusPolicy: Qt.NoFocus - onClicked: { - recipientInput.input.edit.forceActiveFocus() - recipientInput.input.edit.paste() - recipientInput.input.edit.cursorPosition = recipientInput.input.edit.length - recipientInput.validateInput() - } - } - StatusIcon { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - icon: "tiny/checkmark" - color: Theme.palette.primaryColor1 - visible: root.ready - } - StatusClearButton { - visible: recipientInput.input.edit.length !== 0 && root.interactive - onClicked: { - recipientInput.input.edit.clear() - d.clearValues() - } - } - } - Keys.onTabPressed: event.accepted = true - Keys.onReleased: recipientInput.validateInput() + interactive: root.interactive + checkMarkVisible: root.ready + onClearClicked: d.clearValues() + onValidateInputRequested: validateInput() } }