Skip to content
Permalink
Browse files

fix(b-form-textarea): handle initial auto-height when in modal, tabs,…

… or other component with transition or which uses `v-show` (fixes #3936, #3702) (#3937)
  • Loading branch information...
tmorehouse committed Aug 26, 2019
1 parent 7a3b350 commit be3ac62b81d43c434d03bd833db22e99bd1da3f4
@@ -172,11 +172,10 @@ disabled in auto-height mode.

Auto-height works by computing the resulting height via CSS queries, hence the input has to be in
document (DOM) and visible (not hidden via `display: none`). Initial height is computed on mount. If
the `b-form-text-area` is visually hidden on mount, the auto height cannot be computed.

In situations where the text area may initially be hidden visually (i.e. in non-lazy `b-tab`
components or non-lazy static `b-modal`), you may want to use `v-if` to delay mouting (lazy mount),
or delay setting the value of `b-form-textarea` until it's visually hidden parent is shown.
the browser client supports [`IntersectionObserver`](https://caniuse.com/#feat=intersectionobserver)
(either natively or via [a polyfill](/docs#js)), `<b-form-textarea>` will take advantage of this to
determine when the textarea becomes visible and will then compute the height. Refer to the
[Browser support](/docs#browser) section on the getting started page.

## Contextual states

@@ -1,19 +1,25 @@
import Vue from '../../utils/vue'
import { VBVisible } from '../../directives/visible'
import idMixin from '../../mixins/id'
import formMixin from '../../mixins/form'
import formSizeMixin from '../../mixins/form-size'
import formStateMixin from '../../mixins/form-state'
import formTextMixin from '../../mixins/form-text'
import formSelectionMixin from '../../mixins/form-selection'
import formValidityMixin from '../../mixins/form-validity'
import { getCS, isVisible } from '../../utils/dom'
import listenOnRootMixin from '../../mixins/listen-on-root'
import { getCS, isVisible, requestAF } from '../../utils/dom'
import { isNull } from '../../utils/inspect'

// @vue/component
export const BFormTextarea = /*#__PURE__*/ Vue.extend({
name: 'BFormTextarea',
directives: {
'b-visible': VBVisible
},
mixins: [
idMixin,
listenOnRootMixin,
formMixin,
formSizeMixin,
formStateMixin,
@@ -48,7 +54,6 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({
},
data() {
return {
dontResize: true,
heightInPx: null
}
},
@@ -60,64 +65,52 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({
resize: !this.computedRows || this.noResize ? 'none' : null
}
if (!this.computedRows) {
// Conditionaly set the computed CSS height when auto rows/height is enabled.
// We avoid setting the style to null, which can override user manual resize handle.
// Conditionally set the computed CSS height when auto rows/height is enabled
// We avoid setting the style to `null`, which can override user manual resize handle
styles.height = this.heightInPx
// We always add a vertical scrollbar to the textarea when auto-height is
// enabled so that the computed height calcaultion returns a stable value.
// enabled so that the computed height calculation returns a stable value
styles.overflowY = 'scroll'
}
return styles
},
computedMinRows() {
// Ensure rows is at least 2 and positive (2 is the native textarea value).
// A value of 1 can cause issues in some browsers, and most browsers only support
// 2 as the smallest value.
// Ensure rows is at least 2 and positive (2 is the native textarea value)
// A value of 1 can cause issues in some browsers, and most browsers
// only support 2 as the smallest value
return Math.max(parseInt(this.rows, 10) || 2, 2)
},
computedMaxRows() {
return Math.max(this.computedMinRows, parseInt(this.maxRows, 10) || 0)
},
computedRows() {
// This is used to set the attribute 'rows' on the textarea.
// If auto-height is enabled, then we return null as we use CSS to control height.
// This is used to set the attribute 'rows' on the textarea
// If auto-height is enabled, then we return `null` as we use CSS to control height
return this.computedMinRows === this.computedMaxRows ? this.computedMinRows : null
}
},
watch: {
dontResize(newVal, oldval) {
if (!newVal) {
this.setHeight()
}
},
localValue(newVal, oldVal) {
this.setHeight()
}
},
mounted() {
// Enable opt-in resizing once mounted
this.$nextTick(() => {
this.dontResize = false
})
},
activated() {
// If we are being re-activated in <keep-alive>, enable opt-in resizing
this.$nextTick(() => {
this.dontResize = false
})
},
deactivated() {
// If we are in a deactivated <keep-alive>, disable opt-in resizing
this.dontResize = true
},
beforeDestroy() {
/* istanbul ignore next */
this.dontResize = true
this.setHeight()
},
methods: {
// Called by intersection observer directive
visibleCallback(visible) /* istanbul ignore next */ {
if (visible) {
// We use a `$nextTick()` here just to make sure any
// transitions or portalling have completed
this.$nextTick(this.setHeight)
}
},
setHeight() {
this.$nextTick(() => {
this.heightInPx = this.computeHeight()
requestAF(() => {
this.heightInPx = this.computeHeight()
})
})
},
computeHeight() /* istanbul ignore next: can't test getComputedStyle in JSDOM */ {
@@ -127,7 +120,7 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({

const el = this.$el

// Element must be visible (not hidden) and in document.
// Element must be visible (not hidden) and in document
// Must be checked after above checks
if (!isVisible(el)) {
return null
@@ -153,18 +146,18 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({
// Probe scrollHeight by temporarily changing the height to `auto`
el.style.height = 'auto'
const scrollHeight = el.scrollHeight
// Place the original old height back on the element, just in case this computedProp
// returns the same value as before.
// Place the original old height back on the element, just in case `computedProp`
// returns the same value as before
el.style.height = oldHeight

// Calculate content height in "rows" (scrollHeight includes padding but not border)
// Calculate content height in 'rows' (scrollHeight includes padding but not border)
const contentRows = Math.max((scrollHeight - padding) / lineHeight, 2)
// Calculate number of rows to display (limited within min/max rows)
const rows = Math.min(Math.max(contentRows, this.computedMinRows), this.computedMaxRows)
// Calculate the required height of the textarea including border and padding (in pixels)
const height = Math.max(Math.ceil(rows * lineHeight + offset), minHeight)

// Computed height remains the larger of oldHeight and new height,
// Computed height remains the larger of `oldHeight` and new `height`,
// when height is in `sticky` mode (prop `no-auto-shrink` is true)
if (this.noAutoShrink && (parseFloat(oldHeight) || 0) > height) {
return oldHeight
@@ -184,9 +177,13 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({
directives: [
{
name: 'model',
rawName: 'v-model',
value: self.localValue,
expression: 'localValue'
value: self.localValue
},
{
name: 'b-visible',
value: this.visibleCallback,
// If textarea is within 640px of viewport, consider it visible
modifiers: { '640': true }
}
],
attrs: {
@@ -774,52 +774,6 @@ describe('form-textarea', () => {
input.destroy()
})

it('activate and deactivate hooks work (keepalive)', async () => {
const Keepalive = {
template:
'<div><keep-alive>' +
'<b-form-textarea ref="textarea" v-if="show" v-model="value"></b-form-textarea>' +
'<p v-else></p>' +
'</keep-alive></div>',
components: { BFormTextarea },
props: { show: true },
data() {
return { value: '' }
}
}

const keepalive = mount(Keepalive, {
attachToDocument: true,
propsData: {
show: true
}
})

expect(keepalive).toBeDefined()

const textarea = keepalive.find(BFormTextarea)
expect(textarea).toBeDefined()
expect(textarea.isVueInstance()).toBe(true)

// Check that the internal dontResize flag is now false
await keepalive.vm.$nextTick()
expect(textarea.vm.dontResize).toEqual(false)

// v-if the component out of document
keepalive.setProps({ show: false })
// Check that the internal dontResize flag is now true
await keepalive.vm.$nextTick()
expect(textarea.vm.dontResize).toEqual(true)

// v-if the component out of document
keepalive.setProps({ show: true })
// Check that the internal dontResize flag is now false
await keepalive.vm.$nextTick()
expect(textarea.vm.dontResize).toEqual(false)

keepalive.destroy()
})

it('trim modifier prop works', async () => {
const input = mount(BFormTextarea, {
attachToDocument: true,
@@ -22,7 +22,7 @@ For navigation based tabs (i.e. tabs that would change the URL), use the

**Tip:** You should supply each child `<b-tab>` component a unique `key` value if dynamically adding
or removing `<b-tab>` components (i.e. `v-if` or for loops). The `key` attribute is a special Vue
attribute, see https://vuejs.org/v2/api/#key).
attribute, see https://vuejs.org/v2/api/#key.

## Cards integration

@@ -479,7 +479,7 @@ order to use these methods.
</b-button>
</b-tab>

<!-- New Tab Button (Using tabs slot) -->
<!-- New Tab Button (Using tabs-end slot) -->
<template slot="tabs-end">
<b-nav-item @click.prevent="newTab" href="#"><b>+</b></b-nav-item>
</template>

0 comments on commit be3ac62

Please sign in to comment.
You can’t perform that action at this time.