From ef64066cccbb01c963baaff28948ef3491a8fc30 Mon Sep 17 00:00:00 2001 From: Florent Torregrosa <14238-florenttorregrosa@users.noreply.drupalcode.org> Date: Wed, 22 Mar 2023 19:19:47 +0100 Subject: [PATCH] Issue #3292515 by Grimreaper: Bootstrap 5 : Forms > Floating labels --- doc/Forms.md | 139 ++++++++++++++++++ src/Element/ElementProcessInputGroup.php | 15 +- src/HookHandler/ElementInfoAlter.php | 40 +++++ src/HookHandler/PreprocessFormElement.php | 33 ++++- src/HookHandler/PreprocessInput.php | 24 +++ src/HookHandler/PreprocessTextarea.php | 39 +++++ .../overrides/input/form-element.html.twig | 9 ++ ui_suite_bootstrap.theme | 11 ++ 8 files changed, 297 insertions(+), 13 deletions(-) create mode 100644 src/HookHandler/PreprocessTextarea.php diff --git a/doc/Forms.md b/doc/Forms.md index e7fa120a..217bf28a 100644 --- a/doc/Forms.md +++ b/doc/Forms.md @@ -401,3 +401,142 @@ $form['input_group_file'] = [ '#field_prefix' => $this->t('Upload'), ]; ``` + +## Floating labels + +https://getbootstrap.com/docs/5.2/forms/floating-labels. + +We handle a new value for `#title_display`: `floating`. + +UI Suite Bootstrap introduces a new property: `#floating_label`. + +When this property is set to `TRUE`, it has the same behavior as setting +`#title_display` to `floating`. + +This is useful for example in Webform UI, which let you set `#title_display` but +as there won't be the `floating` option, you can enter `#floating_label` in the +YAML of its advanced options. + +Also if no placeholder attributes is set. UI Suite Bootstrap will fallback to +the label itself. + +### Example + +https://getbootstrap.com/docs/5.2/forms/floating-labels/#example: + +```php +$form['floating_label_property'] = [ + '#type' => 'textfield', + '#title' => $this->t('With #floating_label property'), + '#floating_label' => TRUE, +]; + +$form['floating_label_email'] = [ + '#type' => 'email', + '#title' => $this->t('Email address'), + '#title_display' => 'floating', + '#attributes' => [ + 'placeholder' => $this->t('name@example.com'), + ], +]; + +$form['floating_label_password'] = [ + '#type' => 'password', + '#title' => $this->t('Password'), + '#title_display' => 'floating', + '#attributes' => [ + 'placeholder' => $this->t('Password'), + ], +]; + +$form['floating_label_value'] = [ + '#type' => 'email', + '#title' => $this->t('Input with value'), + '#title_display' => 'floating', + '#default_value' => 'test@example.com', + '#attributes' => [ + 'placeholder' => $this->t('name@example.com'), + ], +]; +``` + +### Textareas + +https://getbootstrap.com/docs/5.2/forms/floating-labels/#textareas: + +```php +$form['floating_label_textarea'] = [ + '#type' => 'textarea', + '#title' => $this->t('Comments'), + '#title_display' => 'floating', + '#attributes' => [ + 'placeholder' => $this->t('Leave a comment here'), + ], +]; +``` + +### Selects + +https://getbootstrap.com/docs/5.2/forms/floating-labels/#selects: + +```php +$form['floating_label_select'] = [ + '#type' => 'select', + '#title' => $this->t('Works with selects'), + '#title_display' => 'floating', + '#options' => [ + 'option_1' => $this->t('Option 1'), + 'option_2' => $this->t('Option 2'), + 'option_3' => $this->t('Option 3'), + ], +]; +``` + +### Readonly plaintext + +https://getbootstrap.com/docs/5.2/forms/floating-labels/#readonly-plaintext: + +```php +$form['floating_label_readonly'] = [ + '#type' => 'email', + '#title' => $this->t('Empty input'), + '#title_display' => 'floating', + '#attributes' => [ + 'placeholder' => $this->t('name@example.com'), + 'class' => [ + 'form-control-plaintext', + ], + 'readonly' => TRUE, + ], +]; + +$form['floating_label_readonly_value'] = [ + '#type' => 'email', + '#title' => $this->t('Input with value'), + '#title_display' => 'floating', + '#default_value' => 'name@example.com', + '#attributes' => [ + 'placeholder' => $this->t('name@example.com'), + 'class' => [ + 'form-control-plaintext', + ], + 'readonly' => TRUE, + ], +]; +``` + +### Input groups + +https://getbootstrap.com/docs/5.2/forms/floating-labels/#input-groups: + +```php +$form['floating_label_input_group'] = [ + '#type' => 'textfield', + '#title' => $this->t('Username'), + '#title_display' => 'floating', + '#attributes' => [ + 'placeholder' => $this->t('Username'), + ], + '#field_prefix' => '@', +]; +``` diff --git a/src/Element/ElementProcessInputGroup.php b/src/Element/ElementProcessInputGroup.php index 8af53cac..131cffe0 100644 --- a/src/Element/ElementProcessInputGroup.php +++ b/src/Element/ElementProcessInputGroup.php @@ -20,13 +20,13 @@ class ElementProcessInputGroup { public static function processInputGroup(array &$element, FormStateInterface $form_state, array &$complete_form): array { $element_object = Element::create($element); if ( - $element_object->getProperty('input') && - ( - $element_object->getProperty('field_prefix') || - $element_object->getProperty('field_suffix') || - $element_object->getProperty('input_group_after') || - $element_object->getProperty('input_group_before') || - $element_object->getProperty('input_group_button') + $element_object->getProperty('input') + && ( + $element_object->getProperty('field_prefix') + || $element_object->getProperty('field_suffix') + || $element_object->getProperty('input_group_after') + || $element_object->getProperty('input_group_before') + || $element_object->getProperty('input_group_button') ) ) { static::processAddon($element_object, 'field_prefix', 'input_group_before'); @@ -75,6 +75,7 @@ class ElementProcessInputGroup { if (empty($addons)) { return; } + $processed_addons = []; foreach ($addons as $addon) { // Allow to inject renderable array for advanced implementations. if (\is_array($addon)) { diff --git a/src/HookHandler/ElementInfoAlter.php b/src/HookHandler/ElementInfoAlter.php index 4400ca89..017db9cf 100644 --- a/src/HookHandler/ElementInfoAlter.php +++ b/src/HookHandler/ElementInfoAlter.php @@ -65,6 +65,35 @@ class ElementInfoAlter { 'weight', ]; + /** + * List of additional properties for floating label feature. + */ + public const FLOATING_LABEL_PROPERTIES = [ + 'floating_label' => FALSE, + ]; + + /** + * List of form elements supporting floating label. + */ + public const FLOATING_LABEL_ELEMENTS = [ + 'date', + 'email', + 'entity_autocomplete', + 'language_select', + 'machine_name', + 'number', + 'password', + 'password_confirm', + 'search', + 'select', + 'tel', + 'text_format', + 'textarea', + 'textfield', + 'url', + 'weight', + ]; + /** * Alter form element info. * @@ -116,6 +145,17 @@ class ElementInfoAlter { 'processInputGroup', ]; } + + // Floating label. + foreach (static::FLOATING_LABEL_ELEMENTS as $form_element_id) { + if (!isset($info[$form_element_id])) { + continue; + } + + foreach (static::FLOATING_LABEL_PROPERTIES as $property => $property_default_value) { + $info[$form_element_id]["#{$property}"] = $property_default_value; + } + } } } diff --git a/src/HookHandler/PreprocessFormElement.php b/src/HookHandler/PreprocessFormElement.php index 8d31ec74..cc42dce6 100644 --- a/src/HookHandler/PreprocessFormElement.php +++ b/src/HookHandler/PreprocessFormElement.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace Drupal\ui_suite_bootstrap\HookHandler; +use Drupal\Core\Template\Attribute; use Drupal\ui_suite_bootstrap\Utility\Element; use Drupal\ui_suite_bootstrap\Utility\Variables; @@ -22,9 +23,9 @@ class PreprocessFormElement { /** * An element object provided in the variables array, may not be set. * - * @var \Drupal\ui_suite_bootstrap\Utility\Element + * @var \Drupal\ui_suite_bootstrap\Utility\Element|false */ - protected Element $element; + protected $element; /** * Preprocess form element. @@ -39,6 +40,9 @@ class PreprocessFormElement { $this->variables = Variables::create($variables); $this->element = $this->variables->element; + if (!$this->element) { + return; + } $label = Element::create($variables['label']); // See https://getbootstrap.com/docs/5.2/forms/checks-radios @@ -65,9 +69,10 @@ class PreprocessFormElement { // Input group. // Create variables for input_group flags. - $this->variables->offsetSet('input_group', - $this->element->getProperty('input_group_after') || - $this->element->getProperty('input_group_before') + $this->variables->offsetSet( + 'input_group', + $this->element->getProperty('input_group_after') + || $this->element->getProperty('input_group_before') ); // Get input group attributes. // Cannot use map directly because of the attributes' management. @@ -79,7 +84,23 @@ class PreprocessFormElement { 'input_group_before' => 'input_group_before', ]); - + // Floating label. + // Override title_display if using #floating_label. + if ($this->element->hasProperty('floating_label') && $this->element->getProperty('floating_label')) { + $this->element->setProperty('title_display', 'floating'); + $this->variables->map([ + 'title_display' => 'title_display', + ]); + $this->variables->map([ + 'title_display' => 'label_display', + ]); + } + $this->variables->offsetSet('floating_label_attributes', new Attribute([ + 'class' => [ + 'form-floating', + ($this->variables->offsetGet('input_group') && $this->element->getProperty('errors')) ? 'is-invalid' : '', + ], + ])); } } diff --git a/src/HookHandler/PreprocessInput.php b/src/HookHandler/PreprocessInput.php index fbb6678d..8c2596a6 100644 --- a/src/HookHandler/PreprocessInput.php +++ b/src/HookHandler/PreprocessInput.php @@ -88,10 +88,34 @@ class PreprocessInput { $this->variables->offsetSet('label', $this->element->getProperty('value')); } + $this->floatingLabel(); + // Map the element properties. $this->variables->map([ 'attributes' => 'attributes', ]); } + /** + * Ensure the element has a placeholder. Otherwise, fallback to the label. + */ + protected function floatingLabel(): void { + if (!$this->element) { + return; + } + + if ( + ( + ( + $this->element->hasProperty('floating_label') + && $this->element->getProperty('floating_label') + ) + || $this->element->getProperty('title_display') == 'floating' + ) + && !$this->element->hasAttribute('placeholder') + ) { + $this->element->setAttribute('placeholder', $this->element->getProperty('title')); + } + } + } diff --git a/src/HookHandler/PreprocessTextarea.php b/src/HookHandler/PreprocessTextarea.php new file mode 100644 index 00000000..1e53be5b --- /dev/null +++ b/src/HookHandler/PreprocessTextarea.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\HookHandler; + +use Drupal\ui_suite_bootstrap\Utility\Variables; + +/** + * Pre-processes variables for the "textarea" theme hook. + */ +class PreprocessTextarea extends PreprocessInput { + + /** + * Prepare variables. + * + * @param array $variables + * The hook preprocess variables. + */ + public function preprocess(array &$variables): void { + if (!isset($variables['element'])) { + return; + } + + $this->variables = Variables::create($variables); + $this->element = $this->variables->element; + if (!$this->element) { + return; + } + + $this->floatingLabel(); + + // Map the element properties. + $this->variables->map([ + 'attributes' => 'attributes', + ]); + } + +} diff --git a/templates/overrides/input/form-element.html.twig b/templates/overrides/input/form-element.html.twig index c4df1a3f..edc97d1b 100644 --- a/templates/overrides/input/form-element.html.twig +++ b/templates/overrides/input/form-element.html.twig @@ -90,8 +90,17 @@ {{ input_group_before }} {% endif %} + {% if label_display == 'floating' %} + <div{{ floating_label_attributes }}> + {% endif %} + {{ children }} + {% if label_display == 'floating' %} + {{ label }} + </div> + {% endif %} + {% if input_group_after %} {{ input_group_after }} {% endif %} diff --git a/ui_suite_bootstrap.theme b/ui_suite_bootstrap.theme index 6b7a021e..85b513e3 100644 --- a/ui_suite_bootstrap.theme +++ b/ui_suite_bootstrap.theme @@ -26,6 +26,7 @@ use Drupal\ui_suite_bootstrap\HookHandler\PreprocessPatternNavbar; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessPatternNavbarNav; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessPatternNavItem; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessPatternPagination; +use Drupal\ui_suite_bootstrap\HookHandler\PreprocessTextarea; use Drupal\ui_suite_bootstrap\HookHandler\ThemeSuggestionsAlter; /** @@ -188,6 +189,16 @@ function ui_suite_bootstrap_preprocess_pattern_pagination(array &$variables): vo $instance->preprocess($variables); } +/** + * Implements hook_preprocess_HOOK() for 'textarea'. + */ +function ui_suite_bootstrap_preprocess_textarea(array &$variables): void { + /** @var \Drupal\ui_suite_bootstrap\HookHandler\PreprocessTextarea $instance */ + $instance = \Drupal::service('class_resolver') + ->getInstanceFromDefinition(PreprocessTextarea::class); + $instance->preprocess($variables); +} + /** * Implements hook_theme_suggestions_HOOK_alter() for 'input'. */ -- GitLab