Skip to content

Commit

Permalink
fix: position the material text-area bottom bar (#3359)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki committed Feb 1, 2022
1 parent dea35ea commit a4ba585
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 4 deletions.
3 changes: 2 additions & 1 deletion packages/text-area/src/vaadin-text-area.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { InputFieldMixin } from '@vaadin/field-base/src/input-field-mixin.js';
import { PatternMixin } from '@vaadin/field-base/src/pattern-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
Expand Down Expand Up @@ -76,7 +77,7 @@ export interface TextAreaEventMap extends HTMLElementEventMap, TextAreaCustomEve
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
*/
declare class TextArea extends PatternMixin(InputFieldMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
declare class TextArea extends ResizeMixin(PatternMixin(InputFieldMixin(ThemableMixin(ElementMixin(HTMLElement))))) {
/**
* Maximum number of characters (in Unicode code points) that the user can enter.
*/
Expand Down
30 changes: 29 additions & 1 deletion packages/text-area/src/vaadin-text-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import '@vaadin/input-container/src/vaadin-input-container.js';
import { html, PolymerElement } from '@polymer/polymer';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { InputFieldMixin } from '@vaadin/field-base/src/input-field-mixin.js';
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
import { PatternMixin } from '@vaadin/field-base/src/pattern-mixin.js';
Expand Down Expand Up @@ -59,8 +60,9 @@ registerStyles('vaadin-text-area', inputFieldShared, { moduleId: 'vaadin-text-ar
* @mixes ElementMixin
* @mixes PatternMixin
* @mixes ThemableMixin
* @mixes ResizeMixin
*/
export class TextArea extends PatternMixin(InputFieldMixin(ThemableMixin(ElementMixin(PolymerElement)))) {
export class TextArea extends ResizeMixin(PatternMixin(InputFieldMixin(ThemableMixin(ElementMixin(PolymerElement))))) {
static get is() {
return 'vaadin-text-area';
}
Expand Down Expand Up @@ -140,6 +142,7 @@ export class TextArea extends PatternMixin(InputFieldMixin(ThemableMixin(Element
disabled="[[disabled]]"
invalid="[[invalid]]"
theme$="[[theme]]"
on-scroll="__scrollPositionUpdated"
>
<slot name="prefix" slot="prefix"></slot>
<slot name="textarea"></slot>
Expand Down Expand Up @@ -197,7 +200,26 @@ export class TextArea extends PatternMixin(InputFieldMixin(ThemableMixin(Element
super.connectedCallback();

this._inputField = this.shadowRoot.querySelector('[part=input-field]');

// Wheel scrolling results in async scroll events. Preventing the wheel
// event, scrolling manually and then synchronously updating the scroll position CSS variable
// allows us to avoid some jumpy behavior that would occur on wheel otherwise.
this._inputField.addEventListener('wheel', (e) => {
e.preventDefault();
this._inputField.scrollTop += e.deltaY;
this.__scrollPositionUpdated();
});

this._updateHeight();
this.__scrollPositionUpdated();
}

/**
* @protected
* @override
*/
_onResize() {
this.__scrollPositionUpdated();
}

/** @protected */
Expand All @@ -216,6 +238,12 @@ export class TextArea extends PatternMixin(InputFieldMixin(ThemableMixin(Element
this.addEventListener('animationend', this._onAnimationEnd);
}

/** @private */
__scrollPositionUpdated() {
this._inputField.style.setProperty('--_text-area-vertical-scroll-position', '0px');
this._inputField.style.setProperty('--_text-area-vertical-scroll-position', this._inputField.scrollTop + 'px');
}

/** @private */
_onAnimationEnd(e) {
if (e.animationName.indexOf('vaadin-text-area-appear') === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ snapshots["vaadin-text-area shadow default"] =
>
</span>
</div>
<vaadin-input-container part="input-field">
<vaadin-input-container
part="input-field"
style="--_text-area-vertical-scroll-position:0px;"
>
<slot
name="prefix"
slot="prefix"
Expand Down Expand Up @@ -58,6 +61,7 @@ snapshots["vaadin-text-area shadow disabled"] =
<vaadin-input-container
disabled=""
part="input-field"
style="--_text-area-vertical-scroll-position:0px;"
>
<slot
name="prefix"
Expand Down Expand Up @@ -104,6 +108,7 @@ snapshots["vaadin-text-area shadow readonly"] =
<vaadin-input-container
part="input-field"
readonly=""
style="--_text-area-vertical-scroll-position:0px;"
>
<slot
name="prefix"
Expand Down Expand Up @@ -150,6 +155,7 @@ snapshots["vaadin-text-area shadow invalid"] =
<vaadin-input-container
invalid=""
part="input-field"
style="--_text-area-vertical-scroll-position:0px;"
>
<slot
name="prefix"
Expand Down Expand Up @@ -195,6 +201,7 @@ snapshots["vaadin-text-area shadow theme"] =
</div>
<vaadin-input-container
part="input-field"
style="--_text-area-vertical-scroll-position:0px;"
theme="align-right"
>
<slot
Expand Down
56 changes: 55 additions & 1 deletion packages/text-area/test/text-area.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync, oneEvent } from '@vaadin/testing-helpers';
import { fixtureSync, nextFrame, oneEvent } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../src/vaadin-text-area.js';

Expand Down Expand Up @@ -340,6 +340,60 @@ describe('text-area', () => {
)
);
});

describe('--_text-area-vertical-scroll-position CSS variable', () => {
function wheel({ element = inputField, deltaY = 0 }) {
const e = new CustomEvent('wheel', { bubbles: true, cancelable: true });
e.deltaY = deltaY;
e.deltaX = 0;
element.dispatchEvent(e);
return e;
}

function getVerticalScrollPosition() {
return textArea.shadowRoot
.querySelector('[part="input-field"]')
.style.getPropertyValue('--_text-area-vertical-scroll-position');
}

beforeEach(() => {
textArea.style.height = '100px';
textArea.value = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz';
});

it('should be 0 initially', () => {
expect(getVerticalScrollPosition()).to.equal('0px');
});

it('should update value on scroll', async () => {
inputField.scrollTop = 10;
await nextFrame();
expect(getVerticalScrollPosition()).to.equal('10px');
});

it('should update value on wheel', async () => {
wheel({ deltaY: 10 });
expect(getVerticalScrollPosition()).to.equal('10px');
});

it('should scroll on wheel', async () => {
wheel({ deltaY: 10 });
expect(inputField.scrollTop).to.equal(10);
});

it('should cancel wheel event', () => {
const e = wheel({ deltaY: 10 });
expect(e.defaultPrevented).to.be.true;
});

it('should update value on resize', async () => {
inputField.scrollTop = 10;
await nextFrame();
textArea.style.height = `${inputField.scrollHeight}px`;
await nextFrame();
expect(getVerticalScrollPosition()).to.equal('0px');
});
});
});

describe('pattern', () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/text-area/test/visual/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

/* hide caret */
registerStyles(
'vaadin-text-area',
css`
::slotted(textarea) {
caret-color: transparent;
}
`
);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/text-area/test/visual/lumo/text-area.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fixtureSync } from '@vaadin/testing-helpers/dist/fixture.js';
import { visualDiff } from '@web/test-runner-visual-regression';
import '../common.js';
import '../../../theme/lumo/vaadin-text-area.js';

describe('text-area', () => {
Expand Down Expand Up @@ -58,6 +59,13 @@ describe('text-area', () => {
await visualDiff(div, 'invalid');
});

it('scrolled', async () => {
element.style.height = '70px';
element.value = 'a\nb\nc\nd\ne';
element.focus();
await visualDiff(div, 'scrolled');
});

it('error message', async () => {
element.label = 'Label';
element.errorMessage = 'This field is required';
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/text-area/test/visual/material/text-area.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fixtureSync } from '@vaadin/testing-helpers/dist/fixture.js';
import { visualDiff } from '@web/test-runner-visual-regression';
import '../common.js';
import '../../../theme/material/vaadin-text-area.js';

describe('text-area', () => {
Expand Down Expand Up @@ -58,6 +59,13 @@ describe('text-area', () => {
await visualDiff(div, 'invalid');
});

it('scrolled', async () => {
element.style.height = '70px';
element.value = 'a\nb\nc\nd\ne';
element.focus();
await visualDiff(div, 'scrolled');
});

it('error message', async () => {
element.label = 'Label';
element.errorMessage = 'This field is required';
Expand Down
5 changes: 5 additions & 0 deletions packages/text-area/theme/material/vaadin-text-area-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const textArea = css`
white-space: pre-wrap; /* override "nowrap" from <vaadin-text-field> */
align-self: stretch; /* override "baseline" from <vaadin-text-field> */
}
[part='input-field']::before,
[part='input-field']::after {
bottom: calc(var(--_text-area-vertical-scroll-position) * -1);
}
`;

registerStyles('vaadin-text-area', [inputFieldShared, textArea], { moduleId: 'material-text-area' });

0 comments on commit a4ba585

Please sign in to comment.