Skip to content

Commit e279d2c

Browse files
authored
Merge pull request #2651 from magento-borg/MAGETWO-91848-max-characters
[borg] MAGETWO-91848: Hint on product option with Maximum Characters should count down as more characters are entered
2 parents 2b7beca + 7c08f0d commit e279d2c

File tree

11 files changed

+287
-7
lines changed

11 files changed

+287
-7
lines changed

app/code/Magento/Catalog/i18n/en_US.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,9 @@ Groups,Groups
516516
"Maximum image width","Maximum image width"
517517
"Maximum image height","Maximum image height"
518518
"Maximum number of characters:","Maximum number of characters:"
519+
"Maximum %1 characters", "Maximum %1 characters"
520+
"too many", "too many"
521+
"remaining", "remaining"
519522
"start typing to search template","start typing to search template"
520523
"Product online","Product online"
521524
"Product offline","Product offline"

app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,23 @@ $class = ($_option->getIsRequire()) ? ' required' : '';
6161
cols="25"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea>
6262
<?php endif; ?>
6363
<?php if ($_option->getMaxCharacters()): ?>
64-
<p class="note"><?= /* @escapeNotVerified */ __('Maximum number of characters:') ?>
65-
<strong><?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?></strong></p>
64+
<p class="note note_<?= /* @escapeNotVerified */ $_option->getId() ?>">
65+
<?= /* @escapeNotVerified */ __('Maximum %1 characters', $_option->getMaxCharacters()) ?>
66+
<span class="character-counter no-display"></span>
67+
</p>
6668
<?php endif; ?>
6769
</div>
70+
<?php if ($_option->getMaxCharacters()): ?>
71+
<script type="text/x-magento-init">
72+
{
73+
"[data-selector='options[<?= /* @escapeNotVerified */ $_option->getId() ?>]']": {
74+
"Magento_Catalog/js/product/remaining-characters": {
75+
"maxLength": "<?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?>",
76+
"noteSelector": ".note_<?= /* @escapeNotVerified */ $_option->getId() ?>",
77+
"counterSelector": ".note_<?= /* @escapeNotVerified */ $_option->getId() ?> .character-counter"
78+
}
79+
}
80+
}
81+
</script>
82+
<?php endif; ?>
6883
</div>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright © Magento, Inc. All rights reserved.
3+
* See COPYING.txt for license details.
4+
*/
5+
6+
define([
7+
'jquery',
8+
'mage/translate',
9+
'jquery/ui'
10+
], function ($, $t) {
11+
'use strict';
12+
13+
$.widget('mage.remainingCharacters', {
14+
options: {
15+
remainingText: $t('remaining'),
16+
tooManyText: $t('too many'),
17+
errorClass: 'mage-error',
18+
noDisplayClass: 'no-display'
19+
},
20+
21+
/**
22+
* Initializes custom option component
23+
*
24+
* @private
25+
*/
26+
_create: function () {
27+
this.note = $(this.options.noteSelector);
28+
this.counter = $(this.options.counterSelector);
29+
30+
this.updateCharacterCount();
31+
this.element.on('change keyup paste', this.updateCharacterCount.bind(this));
32+
},
33+
34+
/**
35+
* Updates counter message
36+
*/
37+
updateCharacterCount: function () {
38+
var length = this.element.val().length,
39+
diff = this.options.maxLength - length;
40+
41+
this.counter.text(this._formatMessage(diff));
42+
this.counter.toggleClass(this.options.noDisplayClass, length === 0);
43+
this.note.toggleClass(this.options.errorClass, diff < 0);
44+
},
45+
46+
/**
47+
* Format remaining characters message
48+
*
49+
* @param {int} diff
50+
* @returns {String}
51+
* @private
52+
*/
53+
_formatMessage: function (diff) {
54+
var count = Math.abs(diff),
55+
qualifier = diff < 0 ? this.options.tooManyText : this.options.remainingText;
56+
57+
return '(' + count + ' ' + qualifier + ')';
58+
}
59+
});
60+
61+
return $.mage.remainingCharacters;
62+
});

app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@
336336
.lib-css(margin-top, @indent__xs);
337337
}
338338
}
339+
.field {
340+
.note.mage-error {
341+
color: @error__color;
342+
}
343+
}
339344
}
340345

341346
.product-options-bottom .price-box,
@@ -789,7 +794,7 @@
789794
clear: both;
790795
max-width: 100%;
791796
overflow-x: auto;
792-
position: relative; // Needed for Safari(iOS) to properly render "overflow-x" rule.
797+
position: relative; // Needed for Safari(iOS) to properly render 'overflow-x' rule.
793798

794799
.table-comparison > tbody > tr {
795800
> th,

dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/AdminProductActionGroup.xml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,40 @@
139139
<seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/>
140140
</actionGroup>
141141

142+
<!--Fill fields for simple product in a category in Admin, including text option with char limit-->
143+
<actionGroup name="AdminCreateSimpleProductWithTextOptionCharLimit">
144+
<arguments>
145+
<argument name="category"/>
146+
<argument name="simpleProduct"/>
147+
<argument name="charLimit"/>
148+
</arguments>
149+
<amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/>
150+
<click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/>
151+
<click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/>
152+
<fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/>
153+
<fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/>
154+
<fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/>
155+
<fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/>
156+
<searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/>
157+
<click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/>
158+
<fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/>
159+
160+
<click selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" stepKey="openCustomOptionsSection"/>
161+
<click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/>
162+
<fillField userInput="option1" selector="{{AdminProductCustomizableOptionsSection.optionTitleInput}}" stepKey="fillOptionTitle"/>
163+
<click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/>
164+
<click selector="{{AdminProductCustomizableOptionsSection.optionTypeTextField}}" stepKey="selectTypeTextField"/>
165+
<fillField userInput="20" selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput}}" stepKey="fillMaxChars"/>
166+
167+
<click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/>
168+
<seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/>
169+
<seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/>
170+
<seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/>
171+
<seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/>
172+
<click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/>
173+
<seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/>
174+
</actionGroup>
175+
142176
<!--Assert text in Related, Up-Sell or Cross-Sell section in Admin Product page-->
143177
<actionGroup name="AssertTextInAdminProductRelatedUpSellCrossSellSection">
144178
<arguments>

dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/ActionGroup/StorefrontProductPageActionGroup.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,21 @@
2121
<waitForPageLoad stepKey="waitForPageLoad"/>
2222
<see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/>
2323
</actionGroup>
24+
25+
<!--Verify text length validation hint with multiple inputs-->
26+
<actionGroup name="testDynamicValidationHint">
27+
<arguments>
28+
<argument name="charLimit"/>
29+
</arguments>
30+
<fillField userInput="abcde" selector="{{StorefrontProductPageSection.customTextOptionInput}}" stepKey="textInput1"/>
31+
<see selector="{{StorefrontProductPageSection.charCounter}}" userInput="(15 remaining)" stepKey="assertHint1"/>
32+
<fillField userInput="abcdefghjklansdmnbv" selector="{{StorefrontProductPageSection.customTextOptionInput}}" stepKey="textInput2"/>
33+
<see selector="{{StorefrontProductPageSection.charCounter}}" userInput="(1 remaining)" stepKey="assertHint2"/>
34+
<fillField userInput="abcdefghjklansdmnbvd" selector="{{StorefrontProductPageSection.customTextOptionInput}}" stepKey="textInput3"/>
35+
<see selector="{{StorefrontProductPageSection.charCounter}}" userInput="(0 remaining)" stepKey="assertHint3"/>
36+
<fillField userInput="abcdefghjklansdmnbvds" selector="{{StorefrontProductPageSection.customTextOptionInput}}" stepKey="textInput4"/>
37+
<see selector="{{StorefrontProductPageSection.charCounter}}" userInput="(1 too many)" stepKey="assertHint4"/>
38+
<fillField userInput="abcdefghjklansdmnbvdsasdfghjmn" selector="{{StorefrontProductPageSection.customTextOptionInput}}" stepKey="textInput5"/>
39+
<see selector="{{StorefrontProductPageSection.charCounter}}" userInput="(10 too many)" stepKey="assertHint5"/>
40+
</actionGroup>
2441
</actionGroups>

dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/AdminProductCustomizableOptionsSection.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
<element name="useDefaultOptionTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/>
1616
<element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/>
1717
<element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Title']/parent::label/parent::div//input[@class='admin__control-text']" parameterized="true"/>
18+
<element name="optionTitleInput" type="input" selector="input[name='product[options][0][title]']"/>
19+
<element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select"/>
20+
<element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li"/>
21+
<element name="maxCharactersInput" type="input" selector="input[name='product[options][0][max_characters]']"/>
22+
23+
1824
<element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div//div[@data-role='selected-option']" parameterized="true"/>
1925
<element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/>
2026
<element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/>

dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Catalog/Section/StorefrontProductPageSection.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
<element name="addToCartBtn" type="button" selector="button.action.tocart.primary"/>
1414
<element name="successMsg" type="button" selector="div.message-success"/>
1515
<element name="addToWishlist" type="button" selector="//a[@class='action towishlist']" timeout="30"/>
16+
<element name="customTextOptionInput" type="input" selector=".input-text.product-custom-option"/>
17+
<element name="charCounter" type="text" selector=".character-counter"/>
1618
</section>
1719
</sections>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd">
11+
<test name="ConfigurableOptionTextinputLengthValidationHintTest">
12+
<annotations>
13+
<features value="Product Customizable Option"/>
14+
<stories value="Customizable text option input-length validation hint changes dynamically"/>
15+
<title value="You should have a dynamic length validation hint when using text option has max char limit"/>
16+
<description value="You should have a dynamic length validation hint when using text option has max char limit"/>
17+
<severity value="MINOR"/>
18+
<testCaseId value="MAGETWO-92229"/>
19+
<group value="product"/>
20+
</annotations>
21+
<before>
22+
<createData entity="_defaultCategory" stepKey="createPreReqCategory"/>
23+
</before>
24+
<after>
25+
<amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/>
26+
<deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/>
27+
</after>
28+
29+
<actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/>
30+
<actionGroup ref="AdminCreateSimpleProductWithTextOptionCharLimit" stepKey="fillProductFieldsInAdmin">
31+
<argument name="category" value="$$createPreReqCategory$$"/>
32+
<argument name="simpleProduct" value="_defaultProduct"/>
33+
<argument name="charLimit" value="20"/>
34+
</actionGroup>
35+
<actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1">
36+
<argument name="category" value="$$createPreReqCategory$$"/>
37+
<argument name="product" value="_defaultProduct"/>
38+
</actionGroup>
39+
<actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2">
40+
<argument name="product" value="_defaultProduct"/>
41+
</actionGroup>
42+
<actionGroup ref="testDynamicValidationHint" stepKey="testDynamicValidationHint1">
43+
<argument name="charLimit" value="20"/>
44+
</actionGroup>
45+
</test>
46+
</tests>

dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class CustomOptions extends Form
5858
*
5959
* @var string
6060
*/
61-
protected $maxCharacters = './/div[@class="control"]/p[@class="note"]/strong';
61+
protected $maxCharacters = './/div[@class="control"]/p[contains(@class, "note")]';
6262

6363
/**
6464
* Selector for label of option value element
@@ -72,7 +72,7 @@ class CustomOptions extends Form
7272
*
7373
* @var string
7474
*/
75-
protected $noteByNumber = './/*[@class="note"][%d]/strong';
75+
protected $noteByNumber = './/*[contains(@class, "note")][%d]/strong';
7676

7777
/**
7878
* Selector for select element of option
@@ -220,13 +220,19 @@ public function isJsMessageVisible($customOptionTitle)
220220
protected function getFieldData(SimpleElement $option)
221221
{
222222
$price = $this->getOptionPriceNotice($option);
223-
$maxCharacters = $option->find($this->maxCharacters, Locator::SELECTOR_XPATH);
223+
$maxCharactersElement = $option->find($this->maxCharacters, Locator::SELECTOR_XPATH);
224+
225+
$maxCharacters = null;
226+
if ($maxCharactersElement->isVisible()) {
227+
preg_match('/\s([0-9]+)\s/', $maxCharactersElement->getText(), $match);
228+
$maxCharacters = isset($match[1]) ? $match[1] : $maxCharactersElement->getText();
229+
}
224230

225231
return [
226232
'options' => [
227233
[
228234
'price' => floatval($price),
229-
'max_characters' => $maxCharacters->isVisible() ? $maxCharacters->getText() : null,
235+
'max_characters' => $maxCharacters,
230236
],
231237
]
232238
];

0 commit comments

Comments
 (0)