From b01e81ed36493fc687250643395e2d5c55b07e28 Mon Sep 17 00:00:00 2001 From: Mark Otto <markdotto@gmail.com> Date: Sat, 23 Dec 2017 22:47:37 -0800 Subject: [PATCH] Rewrite custom file input - Changes the wrapping label to a div so we can style the label instead of another element while also supporting form validation. - Fixes form validation styles for custom file input (closes #24831). - Updates docs with validation styles (also adding example feedback text while I was there) and new how it works section. --- docs/4.0/components/forms.md | 50 +++++++++++++++++------------------- scss/_custom-forms.scss | 42 ++++++++++++++---------------- scss/_variables.scss | 7 +---- scss/mixins/_forms.scss | 17 ++++++++++-- 4 files changed, 59 insertions(+), 57 deletions(-) diff --git a/docs/4.0/components/forms.md b/docs/4.0/components/forms.md index c69bfe2ae2..e5a568b67b 100644 --- a/docs/4.0/components/forms.md +++ b/docs/4.0/components/forms.md @@ -899,31 +899,37 @@ Our example forms show native textual `<input>`s above, but form validation styl {% example html %} <form class="was-validated"> - <div class="custom-control custom-checkbox"> + <div class="custom-control custom-checkbox mb-3"> <input type="checkbox" class="custom-control-input" id="customControlValidation1" required> <label class="custom-control-label" for="customControlValidation1">Check this custom checkbox</label> + <div class="invalid-feedback">Example invalid feedback text</div> </div> <div class="custom-control custom-radio"> <input type="radio" class="custom-control-input" id="customControlValidation2" name="radio-stacked" required> <label class="custom-control-label" for="customControlValidation2">Toggle this custom radio</label> </div> - <div class="custom-control custom-radio"> + <div class="custom-control custom-radio mb-3"> <input type="radio" class="custom-control-input" id="customControlValidation3" name="radio-stacked" required> <label class="custom-control-label" for="customControlValidation3">Or toggle this other custom radio</label> + <div class="invalid-feedback">More example invalid feedback text</div> </div> - <select class="custom-select d-block my-3" required> - <option value="">Open this select menu</option> - <option value="1">One</option> - <option value="2">Two</option> - <option value="3">Three</option> - </select> + <div class="form-group"> + <select class="custom-select" required> + <option value="">Open this select menu</option> + <option value="1">One</option> + <option value="2">Two</option> + <option value="3">Three</option> + </select> + <div class="invalid-feedback">Example invalid custom select feedback</div> + </div> - <label class="custom-file"> - <input type="file" id="file" class="custom-file-input" required> - <span class="custom-file-control"></span> - </label> + <div class="custom-file"> + <input type="file" class="custom-file-input" id="validatedCustomFile" required> + <label class="custom-file-label" for="validatedCustomFile">Choose file...</label> + <div class="invalid-feedback">Example invalid custom file feedback</div> + </div> </form> {% endexample %} @@ -1062,24 +1068,16 @@ As is the `size` attribute: ### 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. +The file input is the most gnarly of the bunch and requires additional JavaScript if you'd like to hook them up with functional *Choose file...* and selected file name text. {% example html %} -<label class="custom-file"> - <input type="file" id="file2" class="custom-file-input"> - <span class="custom-file-control"></span> -</label> +<div class="custom-file"> + <input type="file" class="custom-file-input" id="customFile"> + <label class="custom-file-label" for="customFile">Choose file</label> +</div> {% endexample %} -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`. -- We use `::after` to generate a custom background and directive (*Choose file...*). -- We use `::before` to generate and position the *Browse* button. -- 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. +We hide the default file `<input>` via `opacity` and instead style the `<label>`. The button is generated and positioned with `::after`. Lastly, we declare a `width` and `height` on the `<input>` for proper spacing for surrounding content. #### Translating or customizing the strings diff --git a/scss/_custom-forms.scss b/scss/_custom-forms.scss index 56093bc484..d99a86dc7f 100644 --- a/scss/_custom-forms.scss +++ b/scss/_custom-forms.scss @@ -225,7 +225,9 @@ } .custom-file-input { - max-width: 100%; + position: relative; + z-index: 2; + width: 100%; height: $custom-file-height; margin: 0; opacity: 0; @@ -238,49 +240,43 @@ border-color: $custom-file-focus-border-color; } } + + @each $lang, $value in $custom-file-text { + &:lang(#{$lang}) ~ .custom-file-label::after { + content: $value; + } + } } -.custom-file-control { +.custom-file-label { position: absolute; top: 0; right: 0; left: 0; + z-index: 1; height: $custom-file-height; padding: $custom-file-padding-y $custom-file-padding-x; line-height: $custom-file-line-height; color: $custom-file-color; - pointer-events: none; - user-select: none; background-color: $custom-file-bg; border: $custom-file-border-width solid $custom-file-border-color; @include border-radius($custom-file-border-radius); @include box-shadow($custom-file-box-shadow); - @each $lang, $text in map-get($custom-file-text, placeholder) { - &:lang(#{$lang}):empty::after { - content: $text; - } - } - - &::before { + &::after { position: absolute; - top: -$custom-file-border-width; - right: -$custom-file-border-width; - bottom: -$custom-file-border-width; - z-index: 1; + top: 0; + right: 0; + bottom: 0; + z-index: 3; display: block; - height: $custom-file-height; + height: calc(#{$custom-file-height} - #{$custom-file-border-width} * 2); padding: $custom-file-padding-y $custom-file-padding-x; line-height: $custom-file-line-height; color: $custom-file-button-color; + content: "Browse"; @include gradient-bg($custom-file-button-bg); - border: $custom-file-border-width solid $custom-file-border-color; + border-left: $custom-file-border-width solid $custom-file-border-color; @include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0); } - - @each $lang, $text in map-get($custom-file-text, button-label) { - &:lang(#{$lang})::before { - content: $text; - } - } } diff --git a/scss/_variables.scss b/scss/_variables.scss index 8355bf5b8f..0f299a436f 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -509,12 +509,7 @@ $custom-file-box-shadow: $input-box-shadow !default; $custom-file-button-color: $custom-file-color !default; $custom-file-button-bg: $input-group-addon-bg !default; $custom-file-text: ( - placeholder: ( - en: "Choose file..." - ), - button-label: ( - en: "Browse" - ) + en: "Browse" ) !default; diff --git a/scss/mixins/_forms.scss b/scss/mixins/_forms.scss index ba1b16d6a1..d25df182df 100644 --- a/scss/mixins/_forms.scss +++ b/scss/mixins/_forms.scss @@ -88,11 +88,18 @@ background-color: lighten($color, 25%); } } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { + display: block; + } + &:checked { ~ .custom-control-label::before { @include gradient-bg(lighten($color, 10%)); } } + &:focus { ~ .custom-control-label::before { box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-focus-width rgba($color, .25); @@ -105,13 +112,19 @@ .custom-file-input { .was-validated &:#{$state}, &.is-#{$state} { - ~ .custom-file-control { + ~ .custom-file-label { border-color: $color; &::before { border-color: inherit; } } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { + display: block; + } + &:focus { - ~ .custom-file-control { + ~ .custom-file-label { box-shadow: 0 0 0 $input-focus-width rgba($color, .25); } } -- GitLab