Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Affix functionality for formfield #2545

Merged
merged 27 commits into from
Oct 15, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
879dbb0
added cookbook affix examples
thsp-jyskebank-dk Oct 12, 2022
8097178
added prefix / suffix slots to form-field.component.html
thsp-jyskebank-dk Oct 12, 2022
837c973
affix directive
thsp-jyskebank-dk Oct 12, 2022
a27a26f
position affix correctly. Input field padding still wrong
thsp-jyskebank-dk Oct 12, 2022
1167ae7
fix affix example to align with new syntax
thsp-jyskebank-dk Oct 12, 2022
0e819f7
moved some shared form field variables to a new file from form-field-…
thsp-jyskebank-dk Oct 13, 2022
df2079f
ResizeObserver for affix
thsp-jyskebank-dk Oct 13, 2022
58d9915
Refactor template to simplify calculations
RasmusKjeldgaard Oct 13, 2022
f41d5b5
Leftalign code snippet for better presentation
RasmusKjeldgaard Oct 13, 2022
2add694
Remove affix heading from example page
RasmusKjeldgaard Oct 13, 2022
f0c69df
Use designtoken instead of css prop for padding
RasmusKjeldgaard Oct 13, 2022
27d49d0
Move ResizeObserver init to AfterContentInit hook
RasmusKjeldgaard Oct 13, 2022
04144ba
Revert form-field-variables
RasmusKjeldgaard Oct 13, 2022
c6b5cc5
Simplify styling of affix and prefix/suffix container
RasmusKjeldgaard Oct 13, 2022
af07da7
🐛 Fix missing input element after content init
RasmusKjeldgaard Oct 13, 2022
19f97d7
Disable eslint for kirby-affix selector
RasmusKjeldgaard Oct 13, 2022
e1f9f35
📝 Add disclaimer with known limitations to docs
RasmusKjeldgaard Oct 14, 2022
cb1173e
✅ Adjust tests to fit new DOM structure
RasmusKjeldgaard Oct 14, 2022
d714917
affixElements should never be undefined after content init
RasmusKjeldgaard Oct 14, 2022
baa789b
Increase readability of input padding calculation
RasmusKjeldgaard Oct 14, 2022
643e0da
added some tests for affix on formfield spec
thsp-jyskebank-dk Oct 14, 2022
c5e8ed0
increase wait time for formfield affix test
thsp-jyskebank-dk Oct 14, 2022
89983cd
Merge branch 'develop' into feature/form-field-affix-functionality
tspringborg Oct 14, 2022
26bda4a
Enforce affix text color as semi-dark
RasmusKjeldgaard Oct 14, 2022
470dc5f
✅ Reuse existing test util and improve structure
RasmusKjeldgaard Oct 14, 2022
71a0c49
✅ Remove flaky tests
RasmusKjeldgaard Oct 15, 2022
5dcd094
Merge branch 'develop' into feature/form-field-affix-functionality
RasmusKjeldgaard Oct 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';

import { InputSize } from '@kirbydesign/designsystem';

const config = {
selector: 'cookbook-form-field-input-affix-example',
template: `<kirby-form-field label="With prefix">
<kirby-icon name="payment-card" kirby-affix="prefix"></kirby-icon>
<input kirby-input placeholder="Enter your card number" [size]="size" />
</kirby-form-field>

<kirby-form-field label="With suffix">
<input kirby-input kirby-decimal-mask [size]="size" type="number" placeholder="Monthly payments" [size]="size" />
<span kirby-affix="suffix">$/m</span>
</kirby-form-field>

<kirby-form-field label="With prefix and suffix">
<kirby-icon name="search" kirby-affix="prefix"></kirby-icon>
<input kirby-input decimal-mask placeholder="Search..." [size]="size" />
<kirby-spinner kirby-affix="suffix"></kirby-spinner>
</kirby-form-field>`,
};

@Component({
selector: config.selector,
template: config.template,
})
export class FormFieldInputAffixExampleComponent {
template: string = config.template;
@Input() size: InputSize;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ <h2>Input</h2>
<cookbook-form-field-input-label-message-example
[size]="size"
></cookbook-form-field-input-label-message-example>
<cookbook-form-field-input-affix-example [size]="size">
</cookbook-form-field-input-affix-example>
<cookbook-form-field-input-counter-example
[size]="size"
></cookbook-form-field-input-counter-example>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';

import { KirbyModule } from '@kirbydesign/designsystem';

import { FormFieldInputAffixExampleComponent } from './examples/input/affix';
import { FormFieldInputBorderlessExampleComponent } from './examples/input/borderless';
import { FormFieldInputCounterExampleComponent } from './examples/input/counter';
import { FormFieldInputDateExampleComponent } from './examples/input/date';
Expand All @@ -29,6 +30,7 @@ const COMPONENT_DECLARATIONS = [
FormFieldInputDecimalMaskExampleComponent,
FormFieldInputDateExampleComponent,
FormFieldInputDisabledExampleComponent,
FormFieldInputAffixExampleComponent,
FormFieldInputErrorExampleComponent,
FormFieldInputBorderlessExampleComponent,
FormFieldFocusExampleComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ <h3>Label & Message</h3>
></cookbook-form-field-input-label-message-example>
</cookbook-example-viewer>

<h3>Affix</h3>
<p>
<strong>Please note:</strong> Affix functionality is <em>only</em> intended for use with
kirby-input. Date mask input currently does not support prefixed content.
</p>
<cookbook-example-viewer [html]="affixExample.template">
<cookbook-form-field-input-affix-example
[size]="size"
#affixExample
></cookbook-form-field-input-affix-example>
</cookbook-example-viewer>

<h3>Character counter</h3>
<cookbook-example-viewer [html]="inputCounterExample.template">
<cookbook-form-field-input-counter-example
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Directive, ElementRef, Input } from '@angular/core';

@Directive({
// eslint-disable-next-line
selector: '[kirby-affix]',
})
export class AffixDirective {
@Input('kirby-affix') type: 'prefix' | 'suffix';
constructor(public el: ElementRef) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@
</div>

<ng-template #slottedInputTemplate>
<ng-content select="input[kirby-input]"></ng-content>
<ng-content select="textarea[kirby-textarea]"></ng-content>
<ng-content select="kirby-radio-group"></ng-content>
<div class="affix-container">
<div class="prefix vertical-align">
<ng-content select="[kirby-affix='prefix']"></ng-content>
</div>
<ng-content select="input[kirby-input]"></ng-content>
<ng-content select="textarea[kirby-textarea]"></ng-content>
<ng-content select="kirby-radio-group"></ng-content>
<div class="suffix vertical-align">
<ng-content select="[kirby-affix='suffix']"></ng-content>
</div>
</div>
</ng-template>

<ng-template #labelTextTemplate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,23 @@ label {
margin-bottom: utils.size('xxxs');
padding: 0 utils.size('s');
}

.affix-container {
position: relative;

.vertical-align {
display: flex;
align-items: center;
position: absolute;
top: 0;
bottom: 0;
}

.prefix {
left: utils.size('xs');
}

.suffix {
right: utils.size('xs');
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator';
import { byText, createHostFactory, SpectatorHost } from '@ngneat/spectator';

import { DesignTokenHelper } from '@kirbydesign/core';

Expand All @@ -14,6 +14,7 @@ import { FormFieldComponent } from './form-field.component';
import { InputCounterComponent } from './input-counter/input-counter.component';
import { InputComponent } from './input/input.component';
import { TextareaComponent } from './textarea/textarea.component';
import { AffixDirective } from './directives/affix/affix.directive';

const { size, fontSize, fontWeight, lineHeight, getElevation } = DesignTokenHelper;

Expand All @@ -24,6 +25,7 @@ describe('FormFieldComponent', () => {
component: FormFieldComponent,
declarations: [
FormFieldMessageComponent,
AffixDirective,
InputComponent,
TextareaComponent,
RadioGroupComponent,
Expand Down Expand Up @@ -226,10 +228,10 @@ describe('FormFieldComponent', () => {
expect(inputElement).toBeTruthy();
});

it('should render the input as a direct descendant', () => {
it('should render the input as a descendant', () => {
const inputElement = spectator.queryHost('input[kirby-input]');
expect(inputElement).toBeTruthy();
expect(inputElement.parentElement).toEqual(spectator.element);
expect(inputElement.closest('kirby-form-field')).toEqual(spectator.element);
});

it('should not render the input within a label', () => {
Expand Down Expand Up @@ -314,10 +316,10 @@ describe('FormFieldComponent', () => {
expect(textareaElement).toBeTruthy();
});

it('should render the textarea as a direct descendant', () => {
it('should render the textarea as a descendant', () => {
const textareaElement = spectator.queryHost('textarea[kirby-textarea]');
expect(textareaElement).toBeTruthy();
expect(textareaElement.parentElement).toEqual(spectator.element);
expect(textareaElement.closest('kirby-form-field')).toEqual(spectator.element);
});

it('should not render the textarea within a label', () => {
Expand Down Expand Up @@ -368,9 +370,9 @@ describe('FormFieldComponent', () => {
label = spectator.queryHost('label');
});

it('should render the radio-group as a direct descendant', () => {
it('should render the radio-group as a descendant', () => {
expect(radioGroupElement).toBeTruthy();
expect(radioGroupElement.parentElement).toEqual(spectator.element);
expect(radioGroupElement.closest('kirby-form-field')).toEqual(spectator.element);
});

it('should not render the radio-group within a label', () => {
Expand Down Expand Up @@ -518,4 +520,80 @@ describe('FormFieldComponent', () => {
expect(secondEvent.type).toBe('touchend');
});
});
describe('affix', () => {
const wait = (ms: number = 25): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
describe('with prefix', () => {
beforeEach(() => {
spectator = createHost(
`<kirby-form-field>
<span kirby-affix="prefix" style="width: 50px">foo</span>
<input kirby-input />
</kirby-form-field>`
);
});
it('should render prefix content', () => {
const affix = spectator.query(byText('foo'));
expect(affix).toBeTruthy();
});
it('should render prefix content in correct slot', () => {
const affix = spectator.query(byText('foo'));
expect(affix.parentElement.classList).toContain('prefix');
});
it('should add width of prefix content to padding-left of <input>', async () => {
await wait();
const input = spectator.queryHost('input');
const expectedPadding = 50 + parseInt(DesignTokenHelper.size('s'));
expect(input).toHaveComputedStyle({
'padding-left': `${expectedPadding}px`,
});
});
});
describe('with suffix', () => {
beforeEach(() => {
spectator = createHost(
`<kirby-form-field>
<span kirby-affix="suffix" style="width: 50px">foo</span>
<input kirby-input />
</kirby-form-field>`
);
});
it('should render suffix content', () => {
const affix = spectator.query(byText('foo'));
expect(affix).toBeTruthy();
});
it('should render suffix content in correct slot', () => {
const affix = spectator.query(byText('foo'));
expect(affix.parentElement.classList).toContain('suffix');
});
it('should add width of suffix content to padding-right of <input>', async () => {
await wait();
tspringborg marked this conversation as resolved.
Show resolved Hide resolved
const input = spectator.queryHost('input');
const expectedPadding = 50 + parseInt(DesignTokenHelper.size('s'));
expect(input).toHaveComputedStyle({
'padding-right': `${expectedPadding}px`,
});
});
});
describe('with suffix and prefix', () => {
beforeEach(() => {
spectator = createHost(
`<kirby-form-field>
<span kirby-affix="suffix">foo</span>
<span kirby-affix="prefix">bar</span>
<input kirby-input />
</kirby-form-field>`
);
});
it('should render both prefix and suffix content', () => {
const suffix = spectator.query(byText('foo'));
expect(suffix).toBeTruthy();
const prefix = spectator.query(byText('bar'));
expect(prefix).toBeTruthy();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import {
ChangeDetectionStrategy,
Component,
ContentChild,
ContentChildren,
ElementRef,
HostListener,
Input,
OnDestroy,
OnInit,
QueryList,
Renderer2,
} from '@angular/core';
import { DesignTokenHelper } from '../../helpers';

import { PlatformService } from '../../helpers/platform.service';
import { UniqueIdGenerator } from '../../helpers/unique-id-generator.helper';
import { WindowRef } from '../../types/window-ref';
import { RadioGroupComponent } from '../radio/radio-group/radio-group.component';
import { ResizeObserverService } from '../shared/resize-observer/resize-observer.service';
import { AffixDirective } from './directives/affix/affix.directive';

import { InputCounterComponent } from './input-counter/input-counter.component';
import { InputComponent } from './input/input.component';
Expand All @@ -39,6 +44,7 @@ export class FormFieldComponent
@Input() label: string;
@Input() message: string;

@ContentChildren(AffixDirective) affixElements: QueryList<AffixDirective>;
@ContentChild(InputCounterComponent, { static: false }) counter: InputCounterComponent;
@ContentChild(RadioGroupComponent) private radioGroupComponent: RadioGroupComponent;
@ContentChild(RadioGroupComponent, { read: ElementRef })
Expand All @@ -51,7 +57,8 @@ export class FormFieldComponent
elementRef: ElementRef<HTMLElement>,
private platform: PlatformService,
private renderer: Renderer2,
private windowRef: WindowRef
private windowRef: WindowRef,
private resizeObserverService: ResizeObserverService
) {
this.element = elementRef.nativeElement;
}
Expand Down Expand Up @@ -107,6 +114,25 @@ export class FormFieldComponent
this._labelId
);
}

// Measure the width of all slotted affix elements,
// and apply their width + standard padding to the input elements
// padding, so the start/end of the input is correctly indented.
if (this.input) {
this.affixElements.forEach((affix) => {
this.resizeObserverService.observe(affix.el, (entry) => {
const padding = affix.type === 'prefix' ? 'padding-left' : 'padding-right';
const affixWidth = entry.contentRect.width;
const existingPadding = parseInt(DesignTokenHelper.size('s'));

this.renderer.setStyle(
this.input.nativeElement,
`${padding}`,
`${affixWidth + existingPadding}px`
);
});
});
}
}

ngAfterContentChecked(): void {
Expand Down Expand Up @@ -137,5 +163,9 @@ export class FormFieldComponent
detail: this.element,
})
);

this.affixElements.forEach((affix) => {
this.resizeObserverService.unobserve(affix.el);
});
}
}
1 change: 1 addition & 0 deletions libs/designsystem/src/lib/components/form-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { InputCounterComponent } from './input-counter/input-counter.component';
export { TextareaComponent } from './textarea/textarea.component';
export { DateInputDirective } from './directives/date/date-input.directive';
export { DecimalMaskDirective } from './directives/decimal-mask/decimal-mask.directive';
export { AffixDirective } from './directives/affix/affix.directive';
2 changes: 2 additions & 0 deletions libs/designsystem/src/lib/kirby.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FabSheetComponent } from './components/fab-sheet/fab-sheet.component';
import { FlagComponent } from './components/flag/flag.component';
import { DateInputDirective } from './components/form-field/directives/date/date-input.directive';
import { DecimalMaskDirective } from './components/form-field/directives/decimal-mask/decimal-mask.directive';
import { AffixDirective } from './components/form-field/directives/affix/affix.directive';
import { FormFieldMessageComponent } from './components/form-field/form-field-message/form-field-message.component';
import { FormFieldComponent } from './components/form-field/form-field.component';
import { InputCounterComponent } from './components/form-field/input-counter/input-counter.component';
Expand Down Expand Up @@ -96,6 +97,7 @@ const exportedDeclarations = [
SlideButtonComponent,
ToggleComponent,
EmptyStateComponent,
AffixDirective,
FormFieldComponent,
InputComponent,
InputCounterComponent,
Expand Down