From 2b4d7b7be917cf943f3e7957aa35dd953dbf952c Mon Sep 17 00:00:00 2001 From: Manu MA Date: Sat, 19 Jan 2019 22:56:00 +0100 Subject: [PATCH 1/2] fix(angular): apply validation classes properly * fix(angular): add validation classes to ion-item * fix(inputs): focus handling fixes #17171 fixes #16052 fixes #15572 fixes #16452 fixes #17063 --- .../control-value-accessors/value-accessor.ts | 47 ++++++++---- .../test-app/src/app/app-routing.module.ts | 2 + angular/test/test-app/src/app/app.module.ts | 6 +- .../test-app/src/app/form/form.component.html | 76 +++++++++++++++++++ .../test-app/src/app/form/form.component.ts | 37 +++++++++ .../app/home-page/home-page.component.html | 5 ++ core/src/components/checkbox/checkbox.tsx | 9 +++ core/src/components/datetime/datetime.tsx | 1 + core/src/components/input/input.md.vars.scss | 4 +- core/src/components/item/item.scss | 12 ++- core/src/components/range/range.tsx | 66 ++++++++++------ core/src/components/select/select.tsx | 1 + core/src/components/toggle/toggle.tsx | 40 +++++----- core/src/utils/tap-click.ts | 9 --- 14 files changed, 241 insertions(+), 74 deletions(-) create mode 100644 angular/test/test-app/src/app/form/form.component.html create mode 100644 angular/test/test-app/src/app/form/form.component.ts diff --git a/angular/src/directives/control-value-accessors/value-accessor.ts b/angular/src/directives/control-value-accessors/value-accessor.ts index dcd19e3a218..bca308cd7f6 100644 --- a/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/angular/src/directives/control-value-accessors/value-accessor.ts @@ -43,26 +43,43 @@ export class ValueAccessor implements ControlValueAccessor { export function setIonicClasses(element: ElementRef) { requestAnimationFrame(() => { - const classList = (element.nativeElement as HTMLElement).classList; + const input = element.nativeElement as HTMLElement; + const classes = getClasses(input); + setClasses(input, classes); - classList.remove( - 'ion-valid', - 'ion-invalid', - 'ion-touched', - 'ion-untouched', - 'ion-dirty', - 'ion-pristine' - ); - - for (let i = 0; i < classList.length; i++) { - const item = classList.item(i); - if (item !== null && startsWith(item, 'ng-')) { - classList.add(`ion-${item.substr(3)}`); - } + const item = input.closest('ion-item'); + if (item) { + setClasses(item, classes); } }); } +function getClasses(element: HTMLElement) { + const classList = element.classList; + const classes = []; + for (let i = 0; i < classList.length; i++) { + const item = classList.item(i); + if (item !== null && startsWith(item, 'ng-')) { + classes.push(`ion-${item.substr(3)}`); + } + } + return classes; +} + +function setClasses(element: HTMLElement, classes: string[]) { + const classList = element.classList; + + classList.remove( + 'ion-valid', + 'ion-invalid', + 'ion-touched', + 'ion-untouched', + 'ion-dirty', + 'ion-pristine' + ); + classList.add(...classes); +} + function startsWith(input: string, search: string): boolean { return input.substr(0, search.length) === search; } diff --git a/angular/test/test-app/src/app/app-routing.module.ts b/angular/test/test-app/src/app/app-routing.module.ts index 3e52216f662..ea4839d5d0f 100644 --- a/angular/test/test-app/src/app/app-routing.module.ts +++ b/angular/test/test-app/src/app/app-routing.module.ts @@ -17,10 +17,12 @@ import { NestedOutletPage2Component } from './nested-outlet-page2/nested-outlet- import { ViewChildComponent } from './view-child/view-child.component'; import { ProvidersComponent } from './providers/providers.component'; import { SlidesComponent } from './slides/slides.component'; +import { FormComponent } from './form/form.component'; const routes: Routes = [ { path: '', component: HomePageComponent }, { path: 'inputs', component: InputsComponent }, + { path: 'form', component: FormComponent }, { path: 'modals', component: ModalComponent }, { path: 'view-child', component: ViewChildComponent }, { path: 'providers', component: ProvidersComponent }, diff --git a/angular/test/test-app/src/app/app.module.ts b/angular/test/test-app/src/app/app.module.ts index d831ecc8e68..38f4f1decd3 100644 --- a/angular/test/test-app/src/app/app.module.ts +++ b/angular/test/test-app/src/app/app.module.ts @@ -1,5 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -25,6 +26,7 @@ import { NavComponent } from './nav/nav.component'; import { ViewChildComponent } from './view-child/view-child.component'; import { ProvidersComponent } from './providers/providers.component'; import { SlidesComponent } from './slides/slides.component'; +import { FormComponent } from './form/form.component'; @NgModule({ declarations: [ @@ -48,12 +50,14 @@ import { SlidesComponent } from './slides/slides.component'; NavComponent, ViewChildComponent, ProvidersComponent, - SlidesComponent + SlidesComponent, + FormComponent ], imports: [ BrowserModule, AppRoutingModule, FormsModule, + ReactiveFormsModule, IonicModule.forRoot(), ], entryComponents: [ diff --git a/angular/test/test-app/src/app/form/form.component.html b/angular/test/test-app/src/app/form/form.component.html new file mode 100644 index 00000000000..fdc8c8765cb --- /dev/null +++ b/angular/test/test-app/src/app/form/form.component.html @@ -0,0 +1,76 @@ + + + + Forms test + + + + + +
+ + + + DateTime + + + + + Select + + No Game Console + NES + Nintendo64 + PlayStation + Sega Genesis + Sega Saturn + SNES + + + + + Toggle + + + + + Input (required) + + + + + Input + + + + + Checkbox + + + + + Range + + + + +

+ Form Status: {{ profileForm.status }} +

+

+ Form Status: {{ profileForm.value | json }} +

+ Submit + +
+ + + Outside form + + {{outsideToggle.value}} + + +

+ Set values +

+
diff --git a/angular/test/test-app/src/app/form/form.component.ts b/angular/test/test-app/src/app/form/form.component.ts new file mode 100644 index 00000000000..75f54f461b8 --- /dev/null +++ b/angular/test/test-app/src/app/form/form.component.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-form', + templateUrl: './form.component.html', +}) +export class FormComponent { + + profileForm: FormGroup; + outsideToggle = new FormControl(true); + + constructor(fb: FormBuilder) { + this.profileForm = fb.group({ + datetime: ['2010-08-20', Validators.required], + select: [undefined, Validators.required], + toggle: [false], + input: ['', Validators.required], + input2: ['Default Value'], + checkbox: [false], + range: [20, Validators.min(10)], + }, {updateOn: 'blur'}); + } + + setValues() { + this.profileForm.patchValue({ + datetime: '2010-08-20', + setValue: 'nes', + toggle: true, + input: 'Some value', + input2: 'Another values', + checkbox: true, + range: 50 + }); + } + +} diff --git a/angular/test/test-app/src/app/home-page/home-page.component.html b/angular/test/test-app/src/app/home-page/home-page.component.html index 9d228c8ea8b..4848df3cac7 100644 --- a/angular/test/test-app/src/app/home-page/home-page.component.html +++ b/angular/test/test-app/src/app/home-page/home-page.component.html @@ -12,6 +12,11 @@ Inputs test + + + Form test + + Modals test diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index ad13f19e5c7..6fa1b725b97 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -15,6 +15,7 @@ import { createColorClasses, hostContext } from '../../utils/theme'; export class Checkbox implements ComponentInterface { private inputId = `ion-cb-${checkboxIds++}`; + private buttonEl?: HTMLElement; @Element() el!: HTMLElement; @@ -98,9 +99,16 @@ export class Checkbox implements ComponentInterface { @Listen('click') onClick() { + this.setFocus(); this.checked = !this.checked; } + private setFocus() { + if (this.buttonEl) { + this.buttonEl.focus(); + } + } + private onFocus = () => { this.ionFocus.emit(); } @@ -146,6 +154,7 @@ export class Checkbox implements ComponentInterface { onFocus={this.onFocus} onBlur={this.onBlur} disabled={this.disabled} + ref={el => this.buttonEl = el} > ]; diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index aec4c3d4388..b75a192be01 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -246,6 +246,7 @@ export class Datetime implements ComponentInterface { @Listen('click') onClick() { + this.setFocus(); this.open(); } diff --git a/core/src/components/input/input.md.vars.scss b/core/src/components/input/input.md.vars.scss index 22ee3734202..375d2dd51e2 100644 --- a/core/src/components/input/input.md.vars.scss +++ b/core/src/components/input/input.md.vars.scss @@ -8,13 +8,13 @@ $input-md-font-size: inherit !default; /// @prop - Margin top of the input -$input-md-padding-top: $item-md-padding-top !default; +$input-md-padding-top: 10px !default; /// @prop - Margin end of the input $input-md-padding-end: 0 !default; /// @prop - Margin bottom of the input -$input-md-padding-bottom: $item-md-padding-bottom !default; +$input-md-padding-bottom: 10px !default; /// @prop - Margin start of the input $input-md-padding-start: ($item-md-padding-start / 2) !default; diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss index 06ef5caec0c..c55d7002c7d 100644 --- a/core/src/components/item/item.scss +++ b/core/src/components/item/item.scss @@ -284,9 +284,8 @@ button, a { // Item Input Focused // -------------------------------------------------- -:host(.item-interactive.item-has-focus) { - --highlight-background: var(--highlight-color-focused); - +:host(.item-interactive.item-has-focus), +:host(.item-interactive.ion-touched.ion-invalid) { // If the item has a full border and highlight is enabled, show the full item highlight --full-highlight-height: #{calc(var(--highlight-height) * var(--show-full-highlight))}; @@ -294,6 +293,12 @@ button, a { --inset-highlight-height: #{calc(var(--highlight-height) * var(--show-inset-highlight))}; } +// Item Input Focus +// -------------------------------------------------- + +:host(.item-interactive.item-has-focus) { + --highlight-background: var(--highlight-color-focused); +} // Item Input Valid // -------------------------------------------------- @@ -302,7 +307,6 @@ button, a { --highlight-background: var(--highlight-color-valid); } - // Item Input Invalid // -------------------------------------------------- diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index ff1c4467176..8e4deb60b31 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, QueueApi, State, Watch } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, QueueApi, State, Watch } from '@stencil/core'; import { Color, Gesture, GestureDetail, KnobName, Mode, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface'; import { clamp, debounceEvent } from '../../utils/helpers'; @@ -145,6 +145,24 @@ export class Range implements ComponentInterface { */ @Event() ionBlur!: EventEmitter; + @Listen('focusout') + onBlur() { + if (this.hasFocus) { + this.hasFocus = false; + this.ionBlur.emit(); + this.emitStyle(); + } + } + + @Listen('focusin') + onFocus() { + if (!this.hasFocus) { + this.hasFocus = true; + this.ionFocus.emit(); + this.emitStyle(); + } + } + componentWillLoad() { this.updateRatio(); this.debounceChanged(); @@ -165,7 +183,7 @@ export class Range implements ComponentInterface { this.gesture.setDisabled(this.disabled); } - private handleKeyboard = (knob: string, isIncrease: boolean) => { + private handleKeyboard = (knob: KnobName, isIncrease: boolean) => { let step = this.step; step = step > 0 ? step : 1; step = step / (this.max - this.min); @@ -200,29 +218,12 @@ export class Range implements ComponentInterface { private emitStyle() { this.ionStyle.emit({ + 'interactive': true, 'interactive-disabled': this.disabled }); } - private fireBlur() { - if (this.hasFocus) { - this.hasFocus = false; - this.ionBlur.emit(); - this.emitStyle(); - } - } - - private fireFocus() { - if (!this.hasFocus) { - this.hasFocus = true; - this.ionFocus.emit(); - this.emitStyle(); - } - } - private onStart(detail: GestureDetail) { - this.fireFocus(); - const rect = this.rect = this.rangeSlider!.getBoundingClientRect() as any; const currentX = detail.currentX; @@ -234,6 +235,8 @@ export class Range implements ComponentInterface { ? 'A' : 'B'; + this.setFocus(this.pressedKnob); + // update the active knob's position this.update(currentX); } @@ -245,7 +248,6 @@ export class Range implements ComponentInterface { private onEnd(detail: GestureDetail) { this.update(detail.currentX); this.pressedKnob = undefined; - this.fireBlur(); } private update(currentX: number) { @@ -255,8 +257,11 @@ export class Range implements ComponentInterface { let ratio = clamp(0, (currentX - rect.left) / rect.width, 1); if (this.snaps) { // snaps the ratio to the current value - const value = ratioToValue(ratio, this.min, this.max, this.step); - ratio = valueToRatio(value, this.min, this.max); + ratio = valueToRatio( + ratioToValue(ratio, this.min, this.max, this.step), + this.min, + this.max + ); } // update which knob is pressed @@ -317,6 +322,15 @@ export class Range implements ComponentInterface { this.noUpdate = false; } + private setFocus(knob: KnobName) { + if (this.el.shadowRoot) { + const knobEl = this.el.shadowRoot.querySelector(knob === 'A' ? '.range-knob-a' : '.range-knob-b') as HTMLElement | undefined; + if (knobEl) { + knobEl.focus(); + } + } + } + hostData() { return { class: { @@ -401,7 +415,7 @@ export class Range implements ComponentInterface { } interface RangeKnob { - knob: string; + knob: KnobName; value: number; ratio: number; min: number; @@ -410,7 +424,7 @@ interface RangeKnob { pressed: boolean; pin: boolean; - handleKeyboard: (name: string, isIncrease: boolean) => void; + handleKeyboard: (name: KnobName, isIncrease: boolean) => void; } function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard }: RangeKnob) { @@ -431,6 +445,8 @@ function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, hand }} class={{ 'range-knob-handle': true, + 'range-knob-a': knob === 'A', + 'range-knob-b': knob === 'B', 'range-knob-pressed': pressed, 'range-knob-min': value === min, 'range-knob-max': value === max diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 0ce45c7f736..7b9f4b704e9 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -140,6 +140,7 @@ export class Select implements ComponentInterface { @Listen('click') onClick(ev: UIEvent) { + this.setFocus(); this.open(ev); } diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index bdca2a1daa2..4ae8a2d4586 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -16,8 +16,9 @@ import { createColorClasses, hostContext } from '../../utils/theme'; export class Toggle implements ComponentInterface { private inputId = `ion-tg-${toggleIds++}`; - private pivotX = 0; private gesture?: Gesture; + private buttonEl?: HTMLElement; + private lastDrag = 0; @Element() el!: HTMLElement; @@ -108,8 +109,9 @@ export class Toggle implements ComponentInterface { queue: this.queue, gestureName: 'toggle', gesturePriority: 100, - threshold: 0, - onStart: ev => this.onStart(ev), + threshold: 5, + passive: false, + onStart: () => this.onStart(), onMove: ev => this.onMove(ev), onEnd: ev => this.onEnd(ev), }); @@ -118,7 +120,9 @@ export class Toggle implements ComponentInterface { @Listen('click') onClick() { - this.checked = !this.checked; + if (this.lastDrag + 300 < Date.now()) { + this.checked = !this.checked; + } } private emitStyle() { @@ -127,38 +131,37 @@ export class Toggle implements ComponentInterface { }); } - private onStart(detail: GestureDetail) { - this.pivotX = detail.currentX; + private onStart() { this.activated = true; // touch-action does not work in iOS - detail.event.preventDefault(); - return true; + this.setFocus(); } private onMove(detail: GestureDetail) { - const currentX = detail.currentX; - if (shouldToggle(this.checked, currentX - this.pivotX, -15)) { + if (shouldToggle(this.checked, detail.deltaX, -10)) { this.checked = !this.checked; - this.pivotX = currentX; hapticSelection(); } } - private onEnd(detail: GestureDetail) { - const delta = detail.currentX - this.pivotX; - if (shouldToggle(this.checked, delta, 4)) { - this.checked = !this.checked; - hapticSelection(); - } - + private onEnd(ev: GestureDetail) { this.activated = false; + this.lastDrag = Date.now(); + ev.event.preventDefault(); + ev.event.stopImmediatePropagation(); } private getValue() { return this.value || ''; } + private setFocus() { + if (this.buttonEl) { + this.buttonEl.focus(); + } + } + private onFocus = () => { this.ionFocus.emit(); } @@ -205,6 +208,7 @@ export class Toggle implements ComponentInterface { onFocus={this.onFocus} onBlur={this.onBlur} disabled={this.disabled} + ref={el => this.buttonEl = el} > ]; diff --git a/core/src/utils/tap-click.ts b/core/src/utils/tap-click.ts index 945172462b2..dde74196edd 100644 --- a/core/src/utils/tap-click.ts +++ b/core/src/utils/tap-click.ts @@ -5,7 +5,6 @@ import { now, pointerCoord } from './helpers'; export function startTapClick(doc: Document, config: Config) { let lastTouch = -MOUSE_WAIT * 10; let lastActivated = 0; - let cancelled = false; let scrollingEl: HTMLElement | undefined; let activatableEle: HTMLElement | undefined; @@ -51,11 +50,9 @@ export function startTapClick(doc: Document, config: Config) { removeActivated(false); activatableEle = undefined; } - cancelled = true; } function pointerDown(ev: any) { - cancelled = false; if (activatableEle || isScrolling()) { return; } @@ -64,13 +61,7 @@ export function startTapClick(doc: Document, config: Config) { } function pointerUp(ev: UIEvent) { - if (isScrolling()) { - return; - } setActivatedElement(undefined, ev); - if (cancelled && ev.cancelable) { - ev.preventDefault(); - } } function setActivatedElement(el: HTMLElement | undefined, ev: UIEvent) { From e7538f3ccd9490466d40b2df096702df709e64f3 Mon Sep 17 00:00:00 2001 From: Manu MA Date: Sat, 19 Jan 2019 23:27:23 +0100 Subject: [PATCH 2/2] fix(textarea): new-line in firefox (#17176) fixes #17155 --- core/src/components/textarea/textarea.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/components/textarea/textarea.scss b/core/src/components/textarea/textarea.scss index 96275e5b177..d97d8854d75 100644 --- a/core/src/components/textarea/textarea.scss +++ b/core/src/components/textarea/textarea.scss @@ -84,11 +84,12 @@ outline: none; background: transparent; - box-sizing: border-box; resize: none; appearance: none; + white-space: pre-wrap; + &::placeholder { color: var(--placeholder-color);