Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions projects/composition/src/app/api-data/cps-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
"default": "solid",
"description": "Type of the button in terms of appearance, it can be 'solid' or 'outlined' or 'borderless'."
},
{
"name": "nativeType",
"optional": false,
"readonly": false,
"type": "'button' | 'submit' | 'reset'",
"default": "button",
"description": "Native HTML button type attribute, it can be 'button', 'submit' or 'reset'."
},
{
"name": "label",
"optional": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,38 @@
</div>
</app-code-example>

<app-code-example
label="Native types"
[htmlCode]="examples.nativeTypes.html"
[tsCode]="examples.nativeTypes.ts">
<form
class="native-types-form"
[formGroup]="nativeForm"
(ngSubmit)="onNativeSubmit($event)"
(reset)="onNativeReset()">
<cps-input label="Name" formControlName="name"></cps-input>
<div class="buttons-group-row">
<cps-button
label="Plain button"
nativeType="button"
type="outlined"
(clicked)="onNativePlainClick()"></cps-button>
<cps-button
label="Submit"
nativeType="submit"
color="luxury"></cps-button>
<cps-button
label="Reset"
nativeType="reset"
type="borderless"
color="surprise"></cps-button>
</div>
<div class="native-types-form__message" aria-live="polite">
{{ nativeSubmitMessage }}
</div>
</form>
</app-code-example>

<app-code-example
label="Miscellaneous"
[htmlCode]="examples.misc.html"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@
padding-left: 1.125rem;
padding-right: 1.125rem;
}

.native-types-form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 25rem;

&__message {
color: var(--cps-color-depth);
font-style: italic;
min-height: 1.5em;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { CpsButtonComponent } from 'cps-ui-kit';
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { CpsButtonComponent, CpsInputComponent } from 'cps-ui-kit';
import { ComponentDocsViewerComponent } from '../../components/component-docs-viewer/component-docs-viewer.component';
import { CodeExampleComponent } from '../../components/code-example/code-example.component';

Expand All @@ -10,21 +11,49 @@ import { buttonExamples } from './button-page.examples';
imports: [
CpsButtonComponent,
ComponentDocsViewerComponent,
CodeExampleComponent
CodeExampleComponent,
CpsInputComponent,
ReactiveFormsModule
],
selector: 'app-button-page',
templateUrl: './button-page.component.html',
styleUrls: ['./button-page.component.scss'],
host: { class: 'composition-page' }
})
export class ButtonPageComponent {
private readonly fb = inject(FormBuilder);

componentData = ComponentData;
isLoading = false;

nativeForm = this.fb.nonNullable.group({
name: ['', Validators.required]
});

nativeSubmitMessage = '';

onClickForLoading() {
this.isLoading = true;
setTimeout(() => (this.isLoading = false), 2000);
}

onNativePlainClick() {
this.nativeSubmitMessage = 'Plain button clicked (no form action).';
}

onNativeSubmit(event: Event) {
event.preventDefault();
if (this.nativeForm.invalid) {
this.nativeSubmitMessage = 'Form is invalid.';
return;
}
this.nativeSubmitMessage = `Form submitted with name: "${this.nativeForm.value.name}"`;
}

onNativeReset() {
this.nativeForm.reset();
this.nativeSubmitMessage = '';
}

readonly examples = buttonExamples;
}
Original file line number Diff line number Diff line change
@@ -1,123 +1,182 @@
export const buttonExamples: Record<string, { html: string; ts?: string }> = {
solid: {
html: `
<!-- large -->
<cps-button label="Large button" size="large" color="luxury"></cps-button>
<cps-button icon="add" label="Large button" size="large"></cps-button>
<cps-button icon="add" label="Large button" color="surprise" iconPosition="after" size="large"></cps-button>
<cps-button icon="add" label="Large button" color="luxury" size="large" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="large" ariaLabel="Loading button"></cps-button>

<!-- normal -->
<cps-button label="Normal button" color="luxury"></cps-button>
<cps-button icon="add" label="Normal button"></cps-button>
<cps-button icon="add" label="Normal button" color="surprise" iconPosition="after"></cps-button>
<cps-button icon="add" label="Normal button" color="luxury" [disabled]="true"></cps-button>
<cps-button [loading]="true" ariaLabel="Loading button"></cps-button>

<!-- small -->
<cps-button label="Small button" size="small" color="luxury"></cps-button>
<cps-button icon="add" label="Small button" size="small"></cps-button>
<cps-button icon="add" label="Small button" color="surprise" iconPosition="after" size="small"></cps-button>
<cps-button icon="add" label="Small button" color="luxury" size="small" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="small" ariaLabel="Loading button"></cps-button>

<!-- xsmall -->
<cps-button label="XSmall button" size="xsmall" color="luxury"></cps-button>
<cps-button icon="add" label="XSmall button" size="xsmall"></cps-button>
<cps-button icon="add" label="XSmall button" color="surprise" iconPosition="after" size="xsmall"></cps-button>
<cps-button icon="add" label="XSmall button" color="luxury" size="xsmall" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="xsmall" ariaLabel="Loading button"></cps-button>`
},

outlined: {
html: `
<!-- large -->
<cps-button label="Large button" size="large" type="outlined" color="luxury"></cps-button>
<cps-button icon="add" label="Large button" size="large" type="outlined"></cps-button>
<cps-button icon="add" label="Large button" color="surprise" iconPosition="after" size="large" type="outlined"></cps-button>
<cps-button icon="add" label="Large button" color="luxury" size="large" type="outlined" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="large" type="outlined" ariaLabel="Loading button"></cps-button>

<!-- normal -->
<cps-button label="Normal button" type="outlined" color="luxury"></cps-button>
<cps-button icon="add" label="Normal button" type="outlined"></cps-button>
<cps-button icon="add" label="Normal button" color="surprise" iconPosition="after" type="outlined"></cps-button>
<cps-button icon="add" label="Normal button" color="luxury" type="outlined" [disabled]="true"></cps-button>
<cps-button [loading]="true" type="outlined" ariaLabel="Loading button"></cps-button>

<!-- small -->
<cps-button label="Small button" size="small" type="outlined" color="luxury"></cps-button>
<cps-button icon="add" label="Small button" size="small" type="outlined"></cps-button>
<cps-button icon="add" label="Small button" color="surprise" iconPosition="after" size="small" type="outlined"></cps-button>
<cps-button icon="add" label="Small button" color="luxury" size="small" type="outlined" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="small" type="outlined" ariaLabel="Loading button"></cps-button>

<!-- xsmall -->
<cps-button label="XSmall button" size="xsmall" type="outlined" color="luxury"></cps-button>
<cps-button icon="add" label="XSmall button" size="xsmall" type="outlined"></cps-button>
<cps-button icon="add" label="XSmall button" color="surprise" iconPosition="after" size="xsmall" type="outlined"></cps-button>
<cps-button icon="add" label="XSmall button" color="luxury" size="xsmall" type="outlined" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="xsmall" type="outlined" ariaLabel="Loading button"></cps-button>`
},

borderless: {
html: `
<!-- large -->
<cps-button label="Large button" size="large" type="borderless" color="luxury"></cps-button>
<cps-button icon="add" label="Large button" size="large" type="borderless"></cps-button>
<cps-button icon="add" label="Large button" color="surprise" iconPosition="after" size="large" type="borderless"></cps-button>
<cps-button icon="add" label="Large button" color="luxury" size="large" type="borderless" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="large" type="borderless" ariaLabel="Loading button"></cps-button>

<!-- normal -->
<cps-button label="Normal button" type="borderless" color="luxury"></cps-button>
<cps-button icon="add" label="Normal button" type="borderless"></cps-button>
<cps-button icon="add" label="Normal button" color="surprise" iconPosition="after" type="borderless"></cps-button>
<cps-button icon="add" label="Normal button" color="luxury" type="borderless" [disabled]="true"></cps-button>
<cps-button [loading]="true" type="borderless" ariaLabel="Loading button"></cps-button>

<!-- small -->
<cps-button label="Small button" size="small" type="borderless" color="luxury"></cps-button>
<cps-button icon="add" label="Small button" size="small" type="borderless"></cps-button>
<cps-button icon="add" label="Small button" color="surprise" iconPosition="after" size="small" type="borderless"></cps-button>
<cps-button icon="add" label="Small button" color="luxury" size="small" type="borderless" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="small" type="borderless" ariaLabel="Loading button"></cps-button>

<!-- xsmall -->
<cps-button label="XSmall button" size="xsmall" type="borderless" color="luxury"></cps-button>
<cps-button icon="add" label="XSmall button" size="xsmall" type="borderless"></cps-button>
<cps-button icon="add" label="XSmall button" color="surprise" iconPosition="after" size="xsmall" type="borderless"></cps-button>
<cps-button icon="add" label="XSmall button" color="luxury" size="xsmall" type="borderless" [disabled]="true"></cps-button>
<cps-button [loading]="true" size="xsmall" type="borderless" ariaLabel="Loading button"></cps-button>`
},

nativeTypes: {
html: `
<form
class="native-types-form"
[formGroup]="nativeForm"
(ngSubmit)="onNativeSubmit($event)"
(reset)="onNativeReset()">
<cps-input label="Name" formControlName="name"></cps-input>
<div class="buttons-group-row">
<cps-button
label="Plain button"
nativeType="button"
type="outlined"
(clicked)="onNativePlainClick()"></cps-button>
<cps-button
label="Submit"
nativeType="submit"
color="luxury"></cps-button>
<cps-button
label="Reset"
nativeType="reset"
type="borderless"
color="surprise"></cps-button>
</div>
@if (nativeSubmitMessage) {
<div class="native-types-form__message">{{ nativeSubmitMessage }}</div>
}
</form>`,
ts: `
private readonly fb = inject(FormBuilder);

componentData = ComponentData;
isLoading = false;

nativeForm = this.fb.nonNullable.group({
name: ['', Validators.required]
});

nativeSubmitMessage = '';

onNativePlainClick() {
this.nativeSubmitMessage = 'Plain button clicked (no form action).';
}

onNativeSubmit(event: Event) {
event.preventDefault();
if (this.nativeForm.invalid) {
this.nativeSubmitMessage = 'Form is invalid.';
return;
}
this.nativeSubmitMessage = "Form submitted with name: " + this.nativeForm.value.name;
}

onNativeReset() {
this.nativeForm.reset();
this.nativeSubmitMessage = '';
}`
},

misc: {
html: `
<!-- Interactive loading state -->
<cps-button
label="Click to load"
type="outlined"
color="white"
[loading]="isLoading"
(clicked)="onClickForLoading()">
</cps-button>

<!-- Icon-only -->
<cps-button color="white" type="outlined" icon="like" ariaLabel="Like" size="large"></cps-button>
<cps-button color="graphite" icon="eye" size="large" ariaLabel="View"></cps-button>

<!-- Custom size -->
<cps-button label="Custom size" borderRadius="2rem" width="300" height="60" color="white" type="outlined" icon="avatar-top-menu"></cps-button>

<!-- Block / full-width -->
<cps-button label="Block large button" borderRadius="0" width="100%" size="large" color="depth"></cps-button>`,
ts: `
isLoading = false;

onClickForLoading(): void {
this.isLoading = true;
setTimeout(() => (this.isLoading = false), 2000);
}`
}
};

Check warning on line 182 in projects/composition/src/app/pages/button-page/button-page.examples.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div>
<button
type="button"
[attr.type]="nativeType"
Comment thread
lukasmatta marked this conversation as resolved.
[ngClass]="classesList"
[disabled]="disabled"
(click)="onClick($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('CpsButtonComponent', () => {
expect(component.contentColor).toBe('white');
expect(component.borderRadius).toBe('0.25rem');
expect(component.type).toBe('solid');
expect(component.nativeType).toBe('button');
expect(component.label).toBe('');
expect(component.icon).toBe('');
expect(component.iconPosition).toBe('before');
Expand Down Expand Up @@ -251,6 +252,73 @@ describe('CpsButtonComponent', () => {
expect(component.enterActive).toBe(false);
});

describe('nativeType', () => {
it('should default native type attribute to "button"', () => {
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('button');
});

it('should set native type attribute to "submit"', () => {
fixture.componentRef.setInput('nativeType', 'submit');
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('submit');
});

it('should set native type attribute to "reset"', () => {
fixture.componentRef.setInput('nativeType', 'reset');
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('reset');
});

it('should not affect styling type when nativeType changes', () => {
fixture.componentRef.setInput('type', 'outlined');
fixture.componentRef.setInput('nativeType', 'submit');
fixture.detectChanges();
expect(component.classesList).toContain('cps-button--outlined');
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('submit');
});
Comment thread
lukasmatta marked this conversation as resolved.

it('should fall back to "button" native type if nativeType set to null', () => {
fixture.componentRef.setInput('nativeType', null);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('button');
});

it('should fall back to "button" native type if nativeType set to undefined', () => {
fixture.componentRef.setInput('nativeType', undefined);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('button');
});

it('should fall back to "button" and warn if nativeType is an invalid string', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
fixture.componentRef.setInput('nativeType', 'invalid-type');
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
expect(button.getAttribute('type')).toBe('button');
expect(component.nativeType).toBe('button');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid nativeType value')
);
warnSpy.mockRestore();
});

it('should not warn for valid nativeType values', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
fixture.componentRef.setInput('nativeType', 'submit');
fixture.detectChanges();
fixture.componentRef.setInput('nativeType', 'reset');
fixture.detectChanges();
expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});

describe('aria-label', () => {
it('should set aria-label from ariaLabel input', () => {
fixture.componentRef.setInput('ariaLabel', 'Save');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
Input,
OnChanges,
OnInit,
Output
Output,
SimpleChanges
} from '@angular/core';
import { getCSSColor } from '../../utils/colors-utils';
import { CpsIconComponent, IconType } from '../cps-icon/cps-icon.component';
Expand Down Expand Up @@ -50,6 +51,12 @@ export class CpsButtonComponent implements OnInit, OnChanges {
*/
@Input() type: 'solid' | 'outlined' | 'borderless' = 'solid';

/**
* Native HTML button type attribute, it can be 'button', 'submit' or 'reset'.
* @group Props
*/
@Input() nativeType: 'button' | 'submit' | 'reset' = 'button';

/**
* Label or text on the button.
* @group Props
Expand Down Expand Up @@ -131,7 +138,7 @@ export class CpsButtonComponent implements OnInit, OnChanges {
logMissingAriaLabelError('CpsButtonComponent', this.label, this.ariaLabel);
}

ngOnChanges(): void {
ngOnChanges(changes: SimpleChanges): void {
this.buttonColor = getCSSColor(this.color, this.document);
this.borderRadius = convertSize(this.borderRadius);
this.textColor =
Expand All @@ -143,6 +150,16 @@ export class CpsButtonComponent implements OnInit, OnChanges {
}
this.setClasses();
logMissingAriaLabelError('CpsButtonComponent', this.label, this.ariaLabel);

if (
changes.nativeType &&
!['button', 'submit', 'reset'].includes(this.nativeType)
) {
console.warn(
`Invalid nativeType value: ${this.nativeType}. Expected 'button', 'submit', or 'reset'. Defaulting to 'button'.`
);
this.nativeType = 'button';
}
}

setClasses() {
Expand Down
Loading