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
18 changes: 9 additions & 9 deletions playwright/cps-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,15 @@ const components: ComponentEntry[] = [
// },
{ route: '/radio-group', name: 'Radio', selector: 'cps-radio-group' },
// { route: '/scheduler', name: 'Scheduler', selector: 'cps-scheduler' },
// {
// route: '/select',
// name: 'Select',
// selector: ['cps-select', '.cps-select-options-menu'],
// setup: async (page) => {
// await page.waitForSelector('cps-select');
// await page.locator('cps-select').first().click();
// }
// },
{
route: '/select',
name: 'Select',
selector: ['cps-select', '.cps-select-options-menu'],
setup: async (page) => {
await page.waitForSelector('cps-select');
await page.locator('cps-select').first().click();
}
},
{
route: '/sidebar-menu',
name: 'Sidebar menu',
Expand Down
10 changes: 9 additions & 1 deletion projects/composition/src/app/api-data/cps-select.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
"default": "",
"description": "Label of the select component."
},
{
"name": "ariaLabel",
"optional": false,
"readonly": false,
"type": "string",
"default": "",
"description": "Aria label for the select component, used for accessibility, it takes precedence over label."
},
{
"name": "placeholder",
"optional": false,
Expand Down Expand Up @@ -186,7 +194,7 @@
"optional": false,
"readonly": false,
"type": "iconSizeType",
"default": "18px",
"default": "1.125rem",
"description": "Size of icon before input value."
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
optionValue="val"
optionInfo="ticker"
placeholder="Enter a company"
hint="This autocomplete has a fixed width of 500px"
hint="This autocomplete has a fixed width of 31.25rem"
[clearable]="true"
[multiple]="true"
[closableChips]="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,258 +12,258 @@
{ name: 'Berlin', data: { code: 'BER' } }
];`;

export const autocompleteExamples: Record<
string,
{ html: string; ts?: string }
> = {
required: {
html: `
<cps-autocomplete
label="Required single autocomplete with a tooltip"
[options]="options"
optionLabel="name"
optionInfo="info"
infoTooltip="Provide any information here"
placeholder="Enter a city"
[clearable]="true"
formControlName="requiredAutocomplete">
</cps-autocomplete>`,
ts: `
${citiesOptionsTs.trim()}

form = this.fb.group({
requiredAutocomplete: [this.options[1], [Validators.required]]
});`
},

singleAsync: {
html: `
<cps-autocomplete
label="Single search autocomplete with fetched options list"
hint="This autocomplete fetches matching options list from the server side based on user input"
[options]="singleOptionsObservable$ | async"
optionLabel="name"
optionInfo="info"
placeholder="Search a city"
[clearable]="true"
[loading]="isSingleLoading"
loadingMessage="Loading cities..."
emptyMessage="No cities found"
(inputChanged)="onSingleInputChanged($event)">
</cps-autocomplete>`,
ts: `
isSingleLoading = false;
singleOptionsObservable$: Observable<City[]>;

onSingleInputChanged(val: string): void {
if (val) this.filterSubject$.next(val);
}`
},

multipleAsync: {
html: `
<cps-autocomplete
label="Multiple search autocomplete with fetched options list"
hint="This autocomplete fetches matching options list from the server side based on user input"
[options]="multiOptionsObservable$ | async"
optionLabel="name"
optionInfo="info"
placeholder="Search cities"
[clearable]="true"
[multiple]="true"
[loading]="isMultiLoading"
loadingMessage="Loading cities..."
emptyMessage="No cities found"
(inputChanged)="onMultiInputChanged($event)">
</cps-autocomplete>`,
ts: `
isMultiLoading = false;
multiOptionsObservable$: Observable<City[]>;

onMultiInputChanged(val: string): void {
if (val) this.filterSubject$.next(val);
}`
},

disabled: {
html: `
<cps-autocomplete
label="Disabled autocomplete"
[disabled]="true"
hint="This autocomplete is disabled">
</cps-autocomplete>`
},

multipleNotClearable: {
html: `
<cps-autocomplete
label="Multiple autocomplete"
[options]="options"
optionLabel="name"
optionValue="data"
optionInfo="info"
placeholder="Enter a city"
[clearable]="false"
[multiple]="true"
[chips]="false"
[returnObject]="false"
[value]="[{ code: 'CPT' }]"
hint="This autocomplete is not clearable">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

virtualScroll: {
html: `
<cps-autocomplete
label="Multiple autocomplete with virtual scroll, chips and persistent clear icon"
[options]="options"
hint="This autocomplete doesn't have Select All option"
[virtualScroll]="true"
[selectAll]="false"
optionLabel="name"
optionInfo="info"
placeholder="Enter a city"
[clearable]="true"
[multiple]="true"
[persistentClear]="true"
[value]="[options[0], options[4]]">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

nonClosableChips: {
html: `
<cps-autocomplete
label="Multiple autocomplete with non-closable chips"
[returnObject]="false"
[options]="options"
optionLabel="name"
optionValue="data"
optionInfo="info"
placeholder="Enter a city"
hint="This autocomplete doesn't have a chevron icon"
[showChevron]="false"
[clearable]="true"
[multiple]="true"
[closableChips]="false"
[value]="[options[0].data, options[4].data]">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

prefixIcon: {
html: `
<cps-autocomplete
label="Multiple autocomplete with prefix icon"
[options]="options"
optionLabel="name"
optionInfo="info"
placeholder="Enter a city"
[clearable]="true"
[multiple]="true"
[chips]="false"
prefixIcon="search"
[value]="[options[5]]">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

twoWayBinding: {
html: `
<cps-autocomplete
width="31.25rem"
label="Multiple autocomplete with two-way binding and keeping initial items order"
[returnObject]="false"
[options]="syncOptions"
[keepInitialOrder]="true"
optionLabel="title"
optionValue="val"
optionInfo="ticker"
placeholder="Enter a company"
hint="This autocomplete has a fixed width of 500px"
hint="This autocomplete has a fixed width of 31.25rem"
[clearable]="true"
[multiple]="true"
[closableChips]="false"
[(ngModel)]="syncVal"
[ngModelOptions]="{ standalone: true }">
</cps-autocomplete>
<span>{{ syncVal }}</span>`,
ts: `
syncOptions = [
{ title: 'Amazon', val: 'AMZN', ticker: 'AMZN' },
{ title: 'Apple', val: 'AAPL', ticker: 'AAPL' },
{ title: 'Google', val: 'GOOGL', ticker: 'GOOGL' },
{ title: 'Meta', val: 'META', ticker: 'META' },
{ title: 'Microsoft', val: 'MSFT', ticker: 'MSFT' },
{ title: 'Netflix', val: 'NFLX', ticker: 'NFLX' },
{ title: 'Tesla', val: 'TSLA', ticker: 'TSLA' }
];
syncVal: string[] = [];`
},

underlined: {
html: `
<cps-autocomplete
label="Underlined autocomplete"
[options]="options"
optionLabel="name"
optionInfo="info"
placeholder="Enter a city"
[clearable]="true"
[multiple]="true"
[chips]="false"
prefixIcon="search"
appearance="underlined">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

borderless: {
html: `
<cps-autocomplete
label="Borderless autocomplete"
[options]="options"
optionLabel="name"
optionInfo="info"
placeholder="Enter a city"
[clearable]="true"
[multiple]="true"
[chips]="false"
prefixIcon="search"
appearance="borderless">
</cps-autocomplete>`,
ts: citiesOptionsTs
},

asyncValidation: {
html: `
<cps-autocomplete
label="Autocomplete with async validation"
[options]="options"
optionLabel="name"
optionInfo="info"
placeholder="Select a city"
[clearable]="true"
[(ngModel)]="selectedOption"
[ngModelOptions]="{ standalone: true }"
[validating]="validating"
(valueChanged)="onOptionSelected($event)"
[externalError]="externalError"
hint="This autocomplete simulates async validation upon selection.">
</cps-autocomplete>`,
ts: `
validating = false;
externalError = '';
selectedOption: Option | null = null;

onOptionSelected(option: Option): void {
this.validating = true;
this.externalError = '';
of(option).pipe(delay(3000)).subscribe({
next: () => (this.validating = false),
error: () => (this.externalError = 'Validation failed')
});
}`
}
};

Check warning on line 269 in projects/composition/src/app/pages/autocomplete-page/autocomplete-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
Expand Up @@ -52,27 +52,31 @@
<div class="inline-radio-form">
<span>On the</span>
<cps-select
ariaLabel="Select day"
[options]="dayOptions"
optionLabel="name"
[hideDetails]="true"
[value]="dayOptions[0]">
</cps-select>
<span>of every</span>
<cps-select
ariaLabel="Select month"
[options]="monthOptions"
optionLabel="name"
[hideDetails]="true"
[value]="monthOptions[0]">
</cps-select>
<span>month(s) at</span>
<cps-select
ariaLabel="Select hour"
[options]="hourOptions"
optionLabel="name"
[hideDetails]="true"
[value]="hourOptions[0]">
</cps-select>
<span>:</span>
<cps-select
ariaLabel="Select minute"
[options]="minuteOptions"
optionLabel="name"
[hideDetails]="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
</cps-select>
<div class="sync-val-example">
<cps-select
width="500"
width="31.25rem"
label="Multiple select with two-way binding and keeping initial items order"
[returnObject]="false"
[options]="syncOptions"
Expand All @@ -94,7 +94,7 @@
optionValue="val"
optionInfo="ticker"
placeholder="Select a company"
hint="This select has a fixed width of 500px"
hint="This select has a fixed width of 31.25rem"
[clearable]="true"
[multiple]="true"
[closableChips]="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.selects-group {
gap: 24px;
gap: 1.5rem;
display: flex;
flex-direction: column;
}
Expand All @@ -8,6 +8,6 @@
display: flex;
align-items: center;
.sync-val {
margin-left: 24px;
margin-left: 1.5rem;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@
<div class="cps-autocomplete-box-area">
@if (prefixIcon) {
<cps-icon
aria-hidden="true"
[icon]="prefixIcon"
[style.color]="disabled ? 'var(--cps-text-muted)' : null"
[size]="prefixIconSize"
class="prefix-icon">
</cps-icon>
}
@if (
(!multiple && !isEmptyValue()) || (value?.length > 0 && multiple)
) {
@if (hasSelectedValue()) {
<div class="cps-autocomplete-box-items">
@if (!multiple) {
<span class="single-item">
Expand Down Expand Up @@ -159,11 +158,12 @@
class="cps-autocomplete-box-clear-icon"
[ngClass]="{
'cps-autocomplete-box-clear-icon-hidden':
!persistentClear &&
(!multiple || !value?.length) &&
(multiple || isEmptyValue())
!persistentClear && !hasSelectedValue()
}">
<cps-icon icon="delete" size="small"></cps-icon>
<cps-icon
icon="delete"
size="small"
aria-hidden="true"></cps-icon>
</span>
}
@if (showChevron && options.length) {
Expand All @@ -178,6 +178,7 @@
"
[tabindex]="disabled ? -1 : 0">
<cps-icon
aria-hidden="true"
icon="chevron-down"
size="small"
[color]="disabled ? 'text-light' : 'text-dark'"></cps-icon>
Expand Down Expand Up @@ -240,7 +241,9 @@
[attr.aria-posinset]="1"
[attr.aria-selected]="value?.length === options.length"
[class.allselected]="value?.length === options.length"
[class.highlighten]="optionHighlightedIndex === 0"
[class.highlighten]="
isArrowNavigating && optionHighlightedIndex === 0
"
(mousedown)="$event.preventDefault()"
(click)="toggleAll()">
<span class="cps-autocomplete-options-option-left">
Expand Down Expand Up @@ -331,11 +334,7 @@
[attr.aria-required]="isRequired || null"
[ngClass]="inputClass"
[ngStyle]="inputStyle"
[placeholder]="
(!multiple && isEmptyValue()) || (value?.length < 1 && multiple)
? placeholder
: ''
"
[placeholder]="!hasSelectedValue() ? placeholder : ''"
(input)="filterOptions($event)"
(keydown)="onInputKeyDown($event)"
[(ngModel)]="inputText"
Expand All @@ -355,6 +354,7 @@
(mousedown)="$event.preventDefault()"
(click)="onOptionClick(item)"
[class.highlighten]="
isArrowNavigating &&
itemIndex === optionHighlightedIndex - (isSelectAllVisible ? 1 : 0)
"
[attr.aria-selected]="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ $hover-transition-duration: 0.2s;
outline: none;
font-family: inherit;
&::placeholder {
user-select: none;
color: $autocomplete-placeholder-color;
font-style: italic;
opacity: 1; /* Firefox */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,24 +195,24 @@ describe('CpsAutocompleteComponent', () => {

it('should allow options menu to close with ESCAPE key when validating', () => {
fixture.componentRef.setInput('validating', true);
const onBlurStub = jest.spyOn(component, 'onBlur');
jest.spyOn(component, 'clearInput');
fixture.detectChanges();
const result = component.onBeforeOptionsHidden(
CpsMenuHideReason.KEYDOWN_ESCAPE
);
expect(result).toBe(undefined);
expect(onBlurStub).toHaveBeenCalledTimes(1);
expect(component.clearInput).toHaveBeenCalled();
});

it('should allow options menu to close with TAB key when validating', () => {
fixture.componentRef.setInput('validating', true);
const onBlurStub = jest.spyOn(component, 'onBlur');
jest.spyOn(component, 'clearInput');
fixture.detectChanges();
const result = component.onBeforeOptionsHidden(
CpsMenuHideReason.KEYDOWN_TAB
);
expect(result).toBe(undefined);
expect(onBlurStub).toHaveBeenCalledTimes(1);
expect(component.clearInput).toHaveBeenCalled();
});

it('should display loading indicator when validating', () => {
Expand Down
Loading
Loading