diff --git a/docs/components/forms.md b/docs/components/forms.md index a4e0addc40da3c7899206eeaa29545f07750c617..5612afb688e4b531cd673e603a4720132b9be3b7 100644 --- a/docs/components/forms.md +++ b/docs/components/forms.md @@ -543,7 +543,7 @@ Block help text—for below inputs or for longer lines of help text—can be eas <label for="inputPassword5">Password</label> <input type="password" id="inputPassword5" class="form-control" aria-describedby="passwordHelpBlock"> <p id="passwordHelpBlock" class="text-muted"> - Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special characters or emoji. + Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special characters or emoji. </p> {% endexample %} @@ -607,33 +607,33 @@ Each checkbox and radio is wrapped in a `<label>` for three reasons: - It provides a helpful and semantic wrapper to help us replace the default `<input>`s. - It triggers the state of the `<input>` automatically, meaning no JavaScript is required. -We hide the default `<input>` with `opacity` and use the `.c-indicator` to build a new custom form control. We can't build a custom one from just the `<input>` because CSS's `content` doesn't work on that element. +We hide the default `<input>` with `opacity` and use the `.custom-control-indicator` to build a new custom form indicator in its place. Unfortunately we can't build a custom one from just the `<input>` because CSS's `content` doesn't work on that element. -With the sibling selector (`~`), we use the `:checked` state to trigger a makeshift checked state on the custom control. +We use the sibling selector (`~`) for all our `<input>` states—like `:checked`—to properly style our custom form indicator. When combined with the `.custom-control-description` class, we can also style the text for each item based on the `<input>`'s state. In the checked states, we use **base64 embedded SVG icons** from [Open Iconic](https://useiconic.com/open). This provides us the best control for styling and positioning across browsers and devices. #### Checkboxes {% example html %} -<label class="c-input c-checkbox"> - <input type="checkbox"> - <span class="c-indicator"></span> - Check this custom checkbox +<label class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Check this custom checkbox</span> </label> {% endexample %} Custom checkboxes can also utilize the `:indeterminate` pseudo class when manually set via JavaScript (there is no available HTML attribute for specifying it). <div class="bd-example bd-example-indeterminate"> - <label class="c-input c-checkbox"> - <input type="checkbox"> - <span class="c-indicator"></span> - Check this custom checkbox + <label class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Check this custom checkbox</span> </label> </div> - If you're using jQuery, something like this should suffice: +If you're using jQuery, something like this should suffice: {% highlight js %} $('.your-checkbox').prop('indeterminate', true) @@ -642,43 +642,62 @@ $('.your-checkbox').prop('indeterminate', true) #### Radios {% example html %} -<label class="c-input c-radio"> - <input id="radio1" name="radio" type="radio"> - <span class="c-indicator"></span> - Toggle this custom radio +<label class="custom-control custom-radio"> + <input id="radio1" name="radio" type="radio" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Toggle this custom radio</span> </label> -<label class="c-input c-radio"> - <input id="radio2" name="radio" type="radio"> - <span class="c-indicator"></span> - Or toggle this other custom radio +<label class="custom-control custom-radio"> + <input id="radio2" name="radio" type="radio" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Or toggle this other custom radio</span> </label> {% endexample %} +#### Disabled + +Custom checkboxes and radios can also be disabled. Add the `disabled` boolean attribute to the `<input>` and the custom indicator and label description will be automatically styled. + +{% example html %} +<label class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" disabled> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Check this custom checkbox</span> +</label> + +<label class="custom-control custom-radio"> + <input id="radio3" name="radioDisabled" type="radio" class="custom-control-input" disabled> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Toggle this custom radio</span> +</label> +{% endexample %} + + #### Stacked -Custom checkboxes and radios are inline to start. Add a parent with class `.c-inputs-stacked` to ensure each form control is on separate lines. +Custom checkboxes and radios are inline to start. Add a parent with class `.custom-controls-stacked` to ensure each form control is on separate lines. {% example html %} -<div class="c-inputs-stacked"> - <label class="c-input c-radio"> - <input id="radioStacked1" name="radio-stacked" type="radio"> - <span class="c-indicator"></span> - Toggle this custom radio +<div class="custom-controls-stacked"> + <label class="custom-control custom-radio"> + <input id="radioStacked1" name="radio-stacked" type="radio" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Toggle this custom radio</span> </label> - <label class="c-input c-radio"> - <input id="radioStacked2" name="radio-stacked" type="radio"> - <span class="c-indicator"></span> - Or toggle this other custom radio + <label class="custom-control custom-radio"> + <input id="radioStacked2" name="radio-stacked" type="radio" class="custom-control-input"> + <span class="custom-control-indicator"></span> + <span class="custom-control-description">Or toggle this other custom radio</span> </label> </div> {% endexample %} ### Select menu -Custom `<select>` menus need only a custom class, `.c-select` to trigger the custom styles. +Custom `<select>` menus need only a custom class, `.custom-select` to trigger the custom styles. {% example html %} -<select class="c-select"> +<select class="custom-select"> <option selected>Open this select menu</option> <option value="1">One</option> <option value="2">Two</option> @@ -690,14 +709,16 @@ Custom selects degrade nicely in IE9, receiving only a handful of overrides to r ### File browser +The file input is the most gnarly of the bunch and require additional JavaScript if you'd like to hook them up with functional *Choose file...* and selected file name text. + {% example html %} -<label class="file"> - <input type="file" id="file"> - <span class="file-custom"></span> +<label class="custom-file"> + <input type="file" id="file" class="custom-file-input"> + <span class="custom-file-control"></span> </label> {% endexample %} -The file input is the most gnarly of the bunch. Here's how it works: +Here's how it works: - We wrap the `<input>` in a `<label>` so the custom control properly triggers the file browser. - We hide the default file `<input>` via `opacity`. @@ -706,5 +727,3 @@ The file input is the most gnarly of the bunch. Here's how it works: - We declare a `height` on the `<input>` for proper spacing for surrounding content. In other words, it's an entirely custom element, all generated via CSS. - -**Heads up!** The custom file input is currently unable to update the *Choose file...* text with the filename. Without JavaScript, this might not be possible to change, but I'm open to ideas. diff --git a/scss/_custom-forms.scss b/scss/_custom-forms.scss index 4ffe79a6c395d2bb6e3b30101e805ab1e451c62a..974f76408813cba676f01d675fc09745e62213a1 100644 --- a/scss/_custom-forms.scss +++ b/scss/_custom-forms.scss @@ -1,3 +1,5 @@ +// scss-lint:disable PropertyCount + // Embedded icons from Open Iconic. // Released under MIT and copyright 2014 Waybury. // http://useiconic.com/open @@ -7,38 +9,49 @@ // // Base class takes care of all the key behavioral aspects. -.c-input { +.custom-control { position: relative; display: inline; padding-left: 1.5rem; color: #555; - cursor: pointer; - > input { - position: absolute; - z-index: -1; // Put the input behind the label so it doesn't overlay text - opacity: 0; + + .custom-control { + margin-left: 1rem; + } +} - &:checked ~ .c-indicator { - color: #fff; - background-color: #0074d9; - @include box-shadow(none); - } +.custom-control-input { + position: absolute; + z-index: -1; // Put the input behind the label so it doesn't overlay text + opacity: 0; - &:focus ~ .c-indicator { - // the mixin is not used here to make sure there is feedback - box-shadow: 0 0 0 .075rem #fff, 0 0 0 .2rem #0074d9; - } + &:checked ~ .custom-control-indicator { + color: #fff; + background-color: #0074d9; + @include box-shadow(none); + } - &:active ~ .c-indicator { - color: #fff; - background-color: #84c6ff; - @include box-shadow(none); - } + &:focus ~ .custom-control-indicator { + // the mixin is not used here to make sure there is feedback + box-shadow: 0 0 0 .075rem #fff, 0 0 0 .2rem #0074d9; } - + .c-input { - margin-left: 1rem; + &:active ~ .custom-control-indicator { + color: #fff; + background-color: #84c6ff; + @include box-shadow(none); + } + + &:disabled { + ~ .custom-control-indicator { + cursor: not-allowed; + background-color: $custom-form-bg-color-disabled; + } + + ~ .custom-control-description { + color: $custom-form-description-color-disabled; + cursor: not-allowed; + } } } @@ -46,9 +59,9 @@ // // Generates a shadow element to create our makeshift checkbox/radio background. -.c-indicator { +.custom-control-indicator { position: absolute; - top: 0; + top: .0625rem; left: 0; display: block; width: 1rem; @@ -58,27 +71,27 @@ color: #eee; text-align: center; user-select: none; - background-color: #eee; + background-color: $custom-form-bg-color; background-repeat: no-repeat; background-position: center center; background-size: 50% 50%; - @include box-shadow(inset 0 .125rem .125rem rgba(0,0,0,.1)); + @include box-shadow(inset 0 .25rem .25rem rgba(0,0,0,.1)); } // Checkboxes // // Tweak just a few things for checkboxes. -.c-checkbox { - .c-indicator { +.custom-checkbox { + .custom-control-indicator { border-radius: .25rem; } - input:checked ~ .c-indicator { + .custom-control-input:checked ~ .custom-control-indicator { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTYuNCwxTDUuNywxLjdMMi45LDQuNUwyLjEsMy43TDEuNCwzTDAsNC40bDAuNywwLjdsMS41LDEuNWwwLjcsMC43bDAuNy0wLjdsMy41LTMuNWwwLjctMC43TDYuNCwxTDYuNCwxeiINCgkvPg0KPC9zdmc+DQo=); } - input:indeterminate ~ .c-indicator { + .custom-control-input:indeterminate ~ .custom-control-indicator { background-color: #0074d9; background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iOHB4IiBoZWlnaHQ9IjhweCIgdmlld0JveD0iMCAwIDggOCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOCA4IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0wLDN2Mmg4VjNIMHoiLz4NCjwvc3ZnPg0K); @include box-shadow(none); @@ -89,12 +102,12 @@ // // Tweak just a few things for radios. -.c-radio { - .c-indicator { +.custom-radio { + .custom-control-indicator { border-radius: 50%; } - input:checked ~ .c-indicator { + .custom-control-input:checked ~ .custom-control-indicator { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQsMUMyLjMsMSwxLDIuMywxLDRzMS4zLDMsMywzczMtMS4zLDMtM1M1LjcsMSw0LDF6Ii8+DQo8L3N2Zz4NCg==); } } @@ -105,8 +118,8 @@ // By default radios and checkboxes are `inline-block` with no additional spacing // set. Use these optional classes to tweak the layout. -.c-inputs-stacked { - .c-input { +.custom-controls-stacked { + .custom-control { display: inline; &::after { @@ -115,7 +128,7 @@ content: ""; } - + .c-input { + + .custom-control { margin-left: 0; } } @@ -129,7 +142,7 @@ // // Includes IE9-specific hacks (noted by ` \9`). -.c-select { +.custom-select { display: inline-block; max-width: 100%; padding: .375rem 1.75rem .375rem .75rem; @@ -140,6 +153,7 @@ background-image: none \9; background-size: 8px 10px; border: $input-btn-border-width solid $input-border-color; + @include border-radius($border-radius); // Use vendor prefixes as `appearance` isn't part of the CSS spec. -moz-appearance: none; -webkit-appearance: none; @@ -156,7 +170,7 @@ } } -.c-select-sm { +.custom-select-sm { padding-top: 3px; padding-bottom: 3px; font-size: 12px; @@ -172,21 +186,27 @@ // // Custom file input. -.file { +.custom-file { position: relative; display: inline-block; max-width: 100%; height: 2.5rem; cursor: pointer; } -.file input { + +.custom-file-input { min-width: 14rem; max-width: 100%; margin: 0; filter: alpha(opacity = 0); opacity: 0; + + &:focus ~ .custom-file-control { + @include box-shadow(0 0 0 .075rem #fff, 0 0 0 .2rem #0074d9); + } } -.file-custom { + +.custom-file-control { position: absolute; top: 0; right: 0; @@ -201,28 +221,25 @@ border: $input-btn-border-width solid #ddd; border-radius: .25rem; @include box-shadow(inset 0 .2rem .4rem rgba(0,0,0,.05)); -} -.file-custom::after { - content: "Choose file..."; -} -.file-custom::before { - position: absolute; - top: -.075rem; - right: -.075rem; - bottom: -.075rem; - z-index: 6; - display: block; - height: 2.5rem; - padding: .5rem 1rem; - line-height: 1.5; - color: #555; - content: "Browse"; - background-color: #eee; - border: $input-btn-border-width solid #ddd; - border-radius: 0 .25rem .25rem 0; -} -// Focus state -.file input:focus ~ .file-custom { - @include box-shadow(0 0 0 .075rem #fff, 0 0 0 .2rem #0074d9); + &::after { + content: "Choose file..."; + } + + &::before { + position: absolute; + top: -.075rem; + right: -.075rem; + bottom: -.075rem; + z-index: 6; + display: block; + height: 2.5rem; + padding: .5rem 1rem; + line-height: 1.5; + color: #555; + content: "Browse"; + background-color: #eee; + border: $input-btn-border-width solid #ddd; + border-radius: 0 .25rem .25rem 0; + } } diff --git a/scss/_variables.scss b/scss/_variables.scss index 41bc7dbb4e00e241b3c08d372a0f8d098a7eff47..ae9955ce6eb3f9c06e194f4e78f95637306dd26c 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -322,6 +322,11 @@ $input-group-addon-border-color: $input-border-color !default; $cursor-disabled: not-allowed !default; +$custom-form-bg-color: #ddd !default; +$custom-form-bg-color-disabled: #eee !default; +$custom-form-description-color-disabled: #767676 !default; + + // Form validation icons $form-icon-success: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg==" !default; $form-icon-warning: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+" !default;