Horizon selects are now themable: Volumes

Horizon was using a standard select input.  Unfortunately, this
type of input is only customizable to a small extent.

I/We have created a new input type for forms to use that is a bootstrap
dropdown.  This will allow bootstrap themes to affect selects the same
way they do for check boxes and radio buttons and the like.

Material's inputs are a bit special, so custom css was added to keep
style parity with Bootswatch's Paper.

co-authored-by: brian.tully@hp.com
co-authored-by: hurgleburgler@gmail.com

ThemableSelect widget template has been modified so that shadow select
element does not contain any classes (which are usually used for
customizing appearance - since it's hidden, it doesn't need
that). Given that we could match legacy select element as 'div >
select.form-control' with no possibility that SelectFormFieldRegion
(legacy select widget region) will bind a hidden part of
ThemableSelect widget and intercept all commands send to
ThemableSelectFormFieldRegion. ThemableSelectFormFieldRegion was
modified to still use legacy widget wrapper for getting 'name' and
'value' properties, while doing all other things on its own, not using
Selenium wrapper for <select>.

Co-Authored-By: Timur Sufiev <tsufiev@mirantis.com>

Change-Id: Ie921b3adc2e1d3388d3c2aa1f76afe3af6ceb87b
Partially-implements: blueprint horizon-theme-css-reorg
This commit is contained in:
Diana Whitten 2016-03-07 17:02:53 -07:00
parent 9d7cff5d96
commit 20c17d28d3
14 changed files with 325 additions and 50 deletions

View File

@ -35,6 +35,9 @@ from horizon.forms.fields import MultiIPField # noqa
from horizon.forms.fields import SelectWidget # noqa from horizon.forms.fields import SelectWidget # noqa
from horizon.forms.fields import ThemableCheckboxInput # noqa from horizon.forms.fields import ThemableCheckboxInput # noqa
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
from horizon.forms.fields import ThemableChoiceField # noqa
from horizon.forms.fields import ThemableDynamicChoiceField # noqa
from horizon.forms.fields import ThemableSelectWidget # noqa
from horizon.forms.views import ModalFormMixin # noqa from horizon.forms.views import ModalFormMixin # noqa
from horizon.forms.views import ModalFormView # noqa from horizon.forms.views import ModalFormView # noqa
@ -49,6 +52,9 @@ __all__ = [
"DynamicChoiceField", "DynamicChoiceField",
"ThemableCheckboxInput", "ThemableCheckboxInput",
"ThemableCheckboxSelectMultiple", "ThemableCheckboxSelectMultiple",
"ThemableChoiceField",
"ThemableDynamicChoiceField",
"ThemableSelectWidget",
"IPField", "IPField",
"IPv4", "IPv4",
"IPv6", "IPv6",

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import itertools
import re import re
import netaddr import netaddr
@ -23,9 +24,12 @@ from django.core import urlresolvers
from django.forms import fields from django.forms import fields
from django.forms.utils import flatatt # noqa from django.forms.utils import flatatt # noqa
from django.forms import widgets from django.forms import widgets
from django.template import Context # noqa
from django.template.loader import get_template # noqa
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise # noqa from django.utils.functional import Promise # noqa
from django.utils import html from django.utils import html
from django.utils.safestring import mark_safe # noqa
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$') ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
@ -164,7 +168,7 @@ class SelectWidget(widgets.Select):
.... ....
.... ....
widget=forms.SelectWidget( attrs={'class': 'switchable', widget=forms.ThemableSelect( attrs={'class': 'switchable',
'data-slug': 'source'}, 'data-slug': 'source'},
transform_html_attrs=get_title ) transform_html_attrs=get_title )
@ -187,26 +191,84 @@ class SelectWidget(widgets.Select):
other_html = (u' selected="selected"' other_html = (u' selected="selected"'
if option_value in selected_choices else '') if option_value in selected_choices else '')
if callable(self.transform_html_attrs): other_html += self.transform_option_html_attrs(option_label)
html_attrs = self.transform_html_attrs(option_label)
other_html += flatatt(html_attrs)
data_attr_html = self.get_data_attrs(option_label)
if data_attr_html:
other_html += ' ' + data_attr_html
option_label = self.transform_option_label(option_label)
return u'<option value="%s"%s>%s</option>' % (
html.escape(option_value), other_html, option_label)
def get_data_attrs(self, option_label):
other_html = []
if not isinstance(option_label, (six.string_types, Promise)): if not isinstance(option_label, (six.string_types, Promise)):
for data_attr in self.data_attrs: for data_attr in self.data_attrs:
data_value = html.conditional_escape( data_value = html.conditional_escape(
force_text(getattr(option_label, force_text(getattr(option_label,
data_attr, ""))) data_attr, "")))
other_html += ' data-%s="%s"' % (data_attr, data_value) other_html.append('data-%s="%s"' % (data_attr, data_value))
return ' '.join(other_html)
if callable(self.transform): def transform_option_label(self, option_label):
if (not isinstance(option_label, (six.string_types, Promise)) and
callable(self.transform)):
option_label = self.transform(option_label) option_label = self.transform(option_label)
return html.conditional_escape(force_text(option_label))
return u'<option value="%s"%s>%s</option>' % ( def transform_option_html_attrs(self, option_label):
html.escape(option_value), other_html, if not callable(self.transform_html_attrs):
html.conditional_escape(force_text(option_label))) return ''
return flatatt(self.transform_html_attrs(option_label))
class DynamicSelectWidget(widgets.Select): class ThemableSelectWidget(SelectWidget):
"""Bootstrap base select field widget."""
def render(self, name, value, attrs=None, choices=()):
# NOTE(woodnt): Currently the "attrs" contents are being added to the
# select that's hidden. It's unclear whether this is the
# desired behavior. In some cases, the attribute should
# remain solely on the now-hidden select. But in others
# if it should live on the bootstrap button (visible)
# or both.
new_choices = []
for opt_value, opt_label in itertools.chain(self.choices, choices):
other_html = self.transform_option_html_attrs(opt_label)
data_attr_html = self.get_data_attrs(opt_label)
if data_attr_html:
other_html += ' ' + data_attr_html
opt_label = self.transform_option_label(opt_label)
if other_html:
new_choices.append((opt_value, opt_label, other_html))
else:
new_choices.append((opt_value, opt_label))
initial_value = value
if value is None and new_choices:
initial_value = new_choices[0][1]
attrs = self.build_attrs(attrs)
id = attrs.pop('id', 'id_%s' % name)
template = get_template('horizon/common/fields/_themable_select.html')
context = Context({
'name': name,
'options': new_choices,
'id': id,
'value': value,
'initial_value': initial_value,
'select_attrs': attrs,
})
return template.render(context)
class DynamicSelectWidget(SelectWidget):
"""A subclass of the ``Select`` widget which renders extra attributes for """A subclass of the ``Select`` widget which renders extra attributes for
use in callbacks to handle dynamic changes to the available choices. use in callbacks to handle dynamic changes to the available choices.
""" """
@ -231,6 +293,15 @@ class DynamicSelectWidget(widgets.Select):
return self.add_item_link return self.add_item_link
class ThemableDynamicSelectWidget(ThemableSelectWidget, DynamicSelectWidget):
pass
class ThemableChoiceField(fields.ChoiceField):
"""Bootstrap based select field."""
widget = ThemableSelectWidget
class DynamicChoiceField(fields.ChoiceField): class DynamicChoiceField(fields.ChoiceField):
"""A subclass of ``ChoiceField`` with additional properties that make """A subclass of ``ChoiceField`` with additional properties that make
dynamically updating its elements easier. dynamically updating its elements easier.
@ -251,6 +322,10 @@ class DynamicChoiceField(fields.ChoiceField):
self.widget.add_item_link_args = add_item_link_args self.widget.add_item_link_args = add_item_link_args
class ThemableDynamicChoiceField(DynamicChoiceField):
widget = ThemableDynamicSelectWidget
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField): class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``.""" """Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
pass pass

View File

@ -24,7 +24,7 @@
</div> </div>
{% elif filter.filter_type == 'server' %} {% elif filter.filter_type == 'server' %}
<div class="table_search"> <div class="table_search">
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options value=filter.filter_field %} {% with name=filter.get_param_name|add:'_field' options=filter.get_select_options stand_alone=1 value=filter.filter_field %}
{% include 'horizon/common/fields/_themable_select.html' %} {% include 'horizon/common/fields/_themable_select.html' %}
{% endwith %} {% endwith %}
<input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" /> <input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />

View File

@ -1,7 +1,7 @@
{% load horizon %} {% load horizon %}
{% minifyspace %} {% minifyspace %}
<div class="themable-select dropdown" xmlns="http://www.w3.org/1999/html"> <div class="themable-select dropdown {% if not stand_alone %} form-control{% endif %}" xmlns="http://www.w3.org/1999/html">
<button type="button" class="btn btn-default dropdown-toggle" <button type="button" class="btn btn-default dropdown-toggle"
data-toggle="dropdown"{% if value %} data-toggle="dropdown"{% if value %}
title="{{ value }}" {% endif %} title="{{ value }}" {% endif %}
@ -38,9 +38,7 @@
name="{{ name }}" name="{{ name }}"
{% endif %} {% endif %}
{% for k,v in select_attrs.items %} {% for k,v in select_attrs.items %}
{% if k == 'class' %} {% if k != 'class' %}
class="form-control {{ v }}"
{% else %}
{{ k|safe }}="{{ v }}" {{ k|safe }}="{{ v }}"
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@ -4,9 +4,7 @@
{% block modal-body %} {% block modal-body %}
{% if show_attach %} {% if show_attach %}
<h3>{% trans "Attach To Instance" %}</h3> <h3>{% trans "Attach To Instance" %}</h3>
<fieldset>
{% include "horizon/common/_form_fields.html" %} {% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -80,28 +80,29 @@ class CreateForm(forms.SelfHandlingForm):
description = forms.CharField(max_length=255, widget=forms.Textarea( description = forms.CharField(max_length=255, widget=forms.Textarea(
attrs={'rows': 4}), attrs={'rows': 4}),
label=_("Description"), required=False) label=_("Description"), required=False)
volume_source_type = forms.ChoiceField(label=_("Volume Source"), volume_source_type = forms.ChoiceField(
label=_("Volume Source"),
required=False, required=False,
widget=forms.Select(attrs={ widget=forms.ThemableSelectWidget(attrs={
'class': 'switchable', 'class': 'switchable',
'data-slug': 'source'})) 'data-slug': 'source'}))
snapshot_source = forms.ChoiceField( snapshot_source = forms.ChoiceField(
label=_("Use snapshot as a source"), label=_("Use snapshot as a source"),
widget=forms.SelectWidget( widget=forms.ThemableSelectWidget(
attrs={'class': 'snapshot-selector'}, attrs={'class': 'snapshot-selector'},
data_attrs=('size', 'name'), data_attrs=('size', 'name'),
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)), transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
required=False) required=False)
image_source = forms.ChoiceField( image_source = forms.ChoiceField(
label=_("Use image as a source"), label=_("Use image as a source"),
widget=forms.SelectWidget( widget=forms.ThemableSelectWidget(
attrs={'class': 'image-selector'}, attrs={'class': 'image-selector'},
data_attrs=('size', 'name', 'min_disk'), data_attrs=('size', 'name', 'min_disk'),
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))), transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
required=False) required=False)
volume_source = forms.ChoiceField( volume_source = forms.ChoiceField(
label=_("Use a volume as source"), label=_("Use a volume as source"),
widget=forms.SelectWidget( widget=forms.ThemableSelectWidget(
attrs={'class': 'image-selector'}, attrs={'class': 'image-selector'},
data_attrs=('size', 'name'), data_attrs=('size', 'name'),
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)), transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
@ -109,7 +110,7 @@ class CreateForm(forms.SelfHandlingForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_("Type"), label=_("Type"),
required=False, required=False,
widget=forms.Select( widget=forms.ThemableSelectWidget(
attrs={'class': 'switched', attrs={'class': 'switched',
'data-switch-on': 'source', 'data-switch-on': 'source',
'data-source-no_source_type': _('Type'), 'data-source-no_source_type': _('Type'),
@ -118,7 +119,7 @@ class CreateForm(forms.SelfHandlingForm):
availability_zone = forms.ChoiceField( availability_zone = forms.ChoiceField(
label=_("Availability Zone"), label=_("Availability Zone"),
required=False, required=False,
widget=forms.Select( widget=forms.ThemableSelectWidget(
attrs={'class': 'switched', attrs={'class': 'switched',
'data-switch-on': 'source', 'data-switch-on': 'source',
'data-source-no_source_type': _('Availability Zone'), 'data-source-no_source_type': _('Availability Zone'),
@ -419,7 +420,7 @@ class CreateForm(forms.SelfHandlingForm):
class AttachForm(forms.SelfHandlingForm): class AttachForm(forms.SelfHandlingForm):
instance = forms.ChoiceField(label=_("Attach to Instance"), instance = forms.ThemableChoiceField(label=_("Attach to Instance"),
help_text=_("Select an instance to " help_text=_("Select an instance to "
"attach to.")) "attach to."))
@ -645,7 +646,7 @@ class UploadToImageForm(forms.SelfHandlingForm):
attrs={'readonly': 'readonly'})) attrs={'readonly': 'readonly'}))
image_name = forms.CharField(max_length=255, label=_('Image Name')) image_name = forms.CharField(max_length=255, label=_('Image Name'))
disk_format = forms.ChoiceField(label=_('Disk Format'), disk_format = forms.ChoiceField(label=_('Disk Format'),
widget=forms.Select(), widget=forms.ThemableSelectWidget(),
required=False) required=False)
force = forms.BooleanField( force = forms.BooleanField(
label=pgettext_lazy("Force upload volume in in-use status to image", label=pgettext_lazy("Force upload volume in in-use status to image",
@ -751,11 +752,11 @@ class RetypeForm(forms.SelfHandlingForm):
name = forms.CharField(label=_('Volume Name'), name = forms.CharField(label=_('Volume Name'),
widget=forms.TextInput( widget=forms.TextInput(
attrs={'readonly': 'readonly'})) attrs={'readonly': 'readonly'}))
volume_type = forms.ChoiceField(label=_('Type')) volume_type = forms.ThemableChoiceField(label=_('Type'))
MIGRATION_POLICY_CHOICES = [('never', _('Never')), MIGRATION_POLICY_CHOICES = [('never', _('Never')),
('on-demand', _('On Demand'))] ('on-demand', _('On Demand'))]
migration_policy = forms.ChoiceField(label=_('Migration Policy'), migration_policy = forms.ChoiceField(label=_('Migration Policy'),
widget=forms.Select(), widget=forms.ThemableSelectWidget(),
choices=(MIGRATION_POLICY_CHOICES), choices=(MIGRATION_POLICY_CHOICES),
initial='never', initial='never',
required=False) required=False)

View File

@ -37,17 +37,19 @@
&.btn-primary { &.btn-primary {
color: $brand-primary; color: $brand-primary;
} }
&.btn-danger { &.btn-danger {
color: $brand-danger; color: $brand-danger;
} }
&.btn-warning { &.btn-warning {
color: $brand-warning; color: $brand-warning;
} }
&.btn-info { &.btn-info {
color: $brand-info; color: $brand-info;
} }
@include dropdown-button('default', $dropdown-link-color, $dropdown-link-hover-bg); @include dropdown-button('default', $dropdown-link-color, $dropdown-link-hover-bg);
@include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color); @include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color);
@include dropdown-button('info', $btn-info-bg, $btn-info-bg, $btn-info-color); @include dropdown-button('info', $btn-info-bg, $btn-info-bg, $btn-info-color);

View File

@ -3,6 +3,8 @@
// --------------------------------- // ---------------------------------
.themable-select { .themable-select {
position: relative;
.dropdown-title { .dropdown-title {
text-align: left; text-align: left;
@ -12,6 +14,61 @@
} }
select { select {
&,
&.form-control {
display: none; display: none;
} }
}
}
// The dropdowns within forms should look like all other
// form elements
.form-group .themable-select {
padding: 0;
&,
&.open {
.btn {
box-shadow: none;
border: none;
height: 100%;
}
.dropdown-menu {
background-color: $input-bg;
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
border-color: $input-border;
width: 100%;
li > a {
@include text-overflow();
color: $input-color;
&:hover {
color: $dropdown-link-hover-color;
cursor: pointer;
}
}
}
}
}
// For vertical forms, we'll want the button to take on the entire width.
// think old-school launch-instance.
form:not(.form-inline) .form-group .themable-select {
width: 100%;
.dropdown-toggle {
@extend .btn-block;
.fa-caret-down {
left: 0;
}
}
.dropdown-title {
@include text-overflow();
width: calc(100% - 1em);
float: left;
}
} }

View File

@ -228,7 +228,7 @@ class VolumesPage(basepage.BaseNavigationPage):
class VolumeAttachForm(forms.BaseFormRegion): class VolumeAttachForm(forms.BaseFormRegion):
_attach_to_instance_selector = (By.CSS_SELECTOR, 'select[name="instance"]') _attach_to_instance_selector = (By.CSS_SELECTOR, 'div > .themable-select')
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]') _attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button' _detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
@ -239,7 +239,9 @@ class VolumeAttachForm(forms.BaseFormRegion):
@property @property
def instance_selector(self): def instance_selector(self):
src_elem = self._get_element(*self._attach_to_instance_selector) src_elem = self._get_element(*self._attach_to_instance_selector)
return forms.SelectFormFieldRegion(self.driver, self.conf, src_elem) return forms.ThemableSelectFormFieldRegion(
self.driver, self.conf, src_elem=src_elem,
strict_options_match=False)
def detach(self, volume, instance): def detach(self, volume, instance):
detach_button = self.attachments_table.find_element( detach_button = self.attachments_table.find_element(
@ -248,9 +250,5 @@ class VolumeAttachForm(forms.BaseFormRegion):
return forms.BaseFormRegion(self.driver, self.conf) return forms.BaseFormRegion(self.driver, self.conf)
def attach_instance(self, instance_name): def attach_instance(self, instance_name):
instance = filter(lambda x: x.startswith(instance_name), self.instance_selector.text = instance_name
self.instance_selector.options.values())
if not instance:
raise AttributeError("Unable to select {0}".format(instance_name))
self.instance_selector.text = instance[0]
self.submit() self.submit()

View File

@ -36,7 +36,7 @@ class FieldFactory(baseregion.BaseRegion):
field_cls._element_locator_str_suffix)) field_cls._element_locator_str_suffix))
elements = super(FieldFactory, self)._get_elements(*locator) elements = super(FieldFactory, self)._get_elements(*locator)
for element in elements: for element in elements:
yield field_cls(self.driver, self.conf, element) yield field_cls(self.driver, self.conf, src_elem=element)
@classmethod @classmethod
def register_field_cls(cls, field_class, base_classes=None): def register_field_cls(cls, field_class, base_classes=None):
@ -174,7 +174,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
class SelectFormFieldRegion(BaseFormFieldRegion): class SelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field.""" """Select box field."""
_element_locator_str_suffix = 'div > select' _element_locator_str_suffix = 'div > select.form-control'
def is_displayed(self): def is_displayed(self):
return self.element._el.is_displayed() return self.element._el.is_displayed()
@ -218,6 +218,67 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
self.element.select_by_value(value) self.element.select_by_value(value)
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field."""
_element_locator_str_suffix = 'div > .themable-select'
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
def __init__(self, driver, conf, strict_options_match=True, **kwargs):
super(ThemableSelectFormFieldRegion, self).__init__(
driver, conf, **kwargs)
self.strict_options_match = strict_options_match
@property
def hidden_element(self):
elem = self._get_element(*self._raw_select_locator)
return SelectFormFieldRegion(self.driver, self.conf, src_elem=elem)
@property
def name(self):
return self.hidden_element.name
@property
def text(self):
return self._get_element(*self._selected_label_locator).text.strip()
@property
def value(self):
return self.hidden_element.value
@property
def options(self):
return self._get_elements(*self._dropdown_menu_locator)
@text.setter
def text(self, text):
if text != self.text:
self.src_elem.click()
for option in self.options:
if self.strict_options_match:
match = text == option.text.strip()
else:
match = option.text.startswith(text)
if match:
option.click()
return
raise ValueError('Widget "%s" does have an option with text "%s"'
% (self.name, text))
@value.setter
def value(self, value):
if value != self.value:
self.src_elem.click()
for option in self.options:
if value == option.get_attribute('data-select-value'):
option.click()
return
raise ValueError('Widget "%s" does have an option with value "%s"'
% (self.name, value))
class BaseFormRegion(baseregion.BaseRegion): class BaseFormRegion(baseregion.BaseRegion):
"""Base class for forms.""" """Base class for forms."""

View File

@ -6,6 +6,7 @@
@import "components/navbar"; @import "components/navbar";
@import "components/pie_charts"; @import "components/pie_charts";
@import "components/quota"; @import "components/quota";
@import "components/selects";
@import "components/sidebar"; @import "components/sidebar";
@import "components/tables"; @import "components/tables";
@import "components/table_actions"; @import "components/table_actions";

View File

@ -0,0 +1,3 @@
.form-group .themable-select .dropdown-menu {
@extend .dropdown-menu-right;
}

View File

@ -7,6 +7,7 @@
@import "components/magic_search"; @import "components/magic_search";
@import "components/messages"; @import "components/messages";
@import "components/navbar"; @import "components/navbar";
@import "components/selects";
@import "components/sidebar"; @import "components/sidebar";
.login .splash-logo { .login .splash-logo {

View File

@ -0,0 +1,74 @@
// The dropdowns within forms should look like all other
// form elements, so this mimics Bootswatch's Paper's
// bootswatch.scss form-group styles
.form-group .themable-select {
box-shadow: none;
border-radius: 0;
border: none;
&,
&.open {
.btn {
&,
&:hover,
&.active,
&:active,
&:focus {
background: transparent;
padding: 0;
border: none;
border-radius: 0;
-webkit-appearance: none;
@include box-shadow(inset 0 -1px 0 $table-border-color);
font-size: 16px;
&:focus {
@include box-shadow(inset 0 -2px 0 $brand-primary);
}
&[disabled],
&[readonly] {
@include box-shadow(none);
border-bottom: 1px dotted $table-border-color;
}
&.input {
&-sm {
font-size: $font-size-small;
}
&-lg {
font-size: $font-size-large;
}
}
}
}
}
&.open .dropdown-menu {
background-color: $dropdown-bg;
}
}
@mixin material-select-states($state, $color) {
.form-group.#{$state} .themable-select {
&,
&.open {
.btn {
&,
&:hover,
&.active,
&:active,
&:focus {
border-bottom: none;
@include box-shadow(inset 0 -2px 0 $color);
}
}
}
}
}
@include material-select-states('has-warning', $brand-warning);
@include material-select-states('has-error', $brand-danger);
@include material-select-states('has-success', $brand-success);