Skip to content

Commit c25283c

Browse files
Merge branch 'public-ui:develop' into 7572-fix-cursor-styling-input-file
2 parents 40a1565 + ba12c49 commit c25283c

35 files changed

+321
-81
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,71 @@
11
import { test } from '@stencil/playwright';
22
import { testInputCallbacksAndEvents, testInputValueReflection } from '../../e2e';
3+
import { expect } from '@playwright/test';
34

45
const COMPONENT_NAME = 'kol-combobox';
56
const TEST_VALUE = 'Hello World';
7+
const OPTIONS = ['North', 'South', 'West', 'East'];
68

79
test.describe(COMPONENT_NAME, () => {
810
testInputValueReflection<HTMLKolComboboxElement>(COMPONENT_NAME, TEST_VALUE);
911
testInputCallbacksAndEvents<HTMLKolComboboxElement>(COMPONENT_NAME, TEST_VALUE);
12+
13+
test('should fire input and change events', async ({ page }) => {
14+
await page.setContent(`<kol-combobox _label="Input" _suggestions=${JSON.stringify(OPTIONS)}></kol-combobox>`);
15+
const input = page.locator('input.kol-combobox__input');
16+
17+
await input.fill(TEST_VALUE);
18+
await expect(input).toHaveValue(TEST_VALUE);
19+
});
20+
21+
test('should open listbox when button is clicked', async ({ page }) => {
22+
await page.setContent(`<kol-combobox _label="Input" _suggestions=${JSON.stringify(OPTIONS)}></kol-combobox>`);
23+
await page.getByRole('button').click();
24+
const listbox = page.locator('ul[role="listbox"]');
25+
await expect(listbox).toBeVisible();
26+
});
27+
28+
test('should close listbox when pressing Escape', async ({ page }) => {
29+
await page.setContent(`<kol-combobox _label="Input" _suggestions=${JSON.stringify(OPTIONS)}></kol-combobox>`);
30+
await page.getByRole('button').click();
31+
const input = page.locator('input.kol-combobox__input');
32+
await input.press('Escape');
33+
const listbox = page.locator('ul[role="listbox"]');
34+
await expect(listbox).toHaveCount(0);
35+
});
36+
37+
test('should select option with Enter key', async ({ page }) => {
38+
await page.setContent(`<kol-combobox _label="Input" _suggestions=${JSON.stringify(OPTIONS)}></kol-combobox>`);
39+
await page.getByRole('button').click();
40+
const input = page.locator('input.kol-combobox__input');
41+
await input.focus();
42+
await page.keyboard.press('ArrowDown');
43+
await page.keyboard.press('Enter');
44+
45+
await expect(input).toHaveValue('North');
46+
});
47+
48+
test('should filter suggestions based on input', async ({ page }) => {
49+
await page.setContent(`<kol-combobox _label="Input"></kol-combobox>`);
50+
await page.evaluate(() => {
51+
const combobox = document.querySelector('kol-combobox');
52+
if (combobox) combobox._suggestions = ['North', 'South', 'West', 'East'];
53+
});
54+
const input = page.locator('input.kol-combobox__input');
55+
await input.focus();
56+
await input.fill('SOU');
57+
58+
await page.waitForChanges();
59+
await page.waitForTimeout(300);
60+
const suggestions = page.locator('ul[role="listbox"] li');
61+
await expect(suggestions).toHaveCount(1);
62+
await expect(suggestions.first()).toHaveText('South');
63+
});
64+
65+
test('should disable interaction when _disabled is true', async ({ page }) => {
66+
await page.setContent(`<kol-combobox _label="Input" _disabled _suggestions=${JSON.stringify(OPTIONS)}></kol-combobox>`);
67+
await page.getByRole('button').click({ force: true });
68+
const listbox = page.locator('ul[role="listbox"]');
69+
await expect(listbox).toHaveCount(0);
70+
});
1071
});

packages/components/src/components/combobox/shadow.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ export class KolCombobox implements ComboboxAPI {
6565
if (this.state._disabled === true) {
6666
this._isOpen = false;
6767
} else {
68-
this._isOpen = !this._isOpen;
6968
this.refInput?.focus();
70-
if (this._isOpen && Array.isArray(this._filteredSuggestions) && this._filteredSuggestions.length > 0) {
69+
if (!this._hasOpened && Array.isArray(this._filteredSuggestions) && this._filteredSuggestions.length > 0) {
70+
this._isOpen = true;
71+
this._hasOpened = true;
7172
const selectedIndex = this._filteredSuggestions.findIndex((option) => option === this.state._value);
72-
this._focusedOptionIndex = selectedIndex >= 0 ? selectedIndex : 0;
73+
this._focusedOptionIndex = selectedIndex >= 0 ? selectedIndex : -1;
7374
this.focusOption(this._focusedOptionIndex);
7475
}
7576
}
@@ -293,6 +294,8 @@ export class KolCombobox implements ComboboxAPI {
293294
break;
294295
case 'Esc':
295296
case 'Escape': {
297+
this._hasOpened = false;
298+
this._isOpen = false;
296299
handleEvent(false);
297300
this.refInput?.focus();
298301
break;
@@ -340,6 +343,8 @@ export class KolCombobox implements ComboboxAPI {
340343
private _isOpen: boolean = false;
341344
@State()
342345
private _filteredSuggestions?: SuggestionsPropType;
346+
@State()
347+
private _hasOpened = false;
343348

344349
@Listen('click', { target: 'window' })
345350
handleWindowClick(event: MouseEvent) {
@@ -590,6 +595,7 @@ export class KolCombobox implements ComboboxAPI {
590595
}
591596

592597
private onBlur() {
598+
this._hasOpened = false;
593599
if (this._isOpen) {
594600
this._isOpen = !this._isOpen;
595601
this.refInput?.focus();

packages/components/src/components/single-select/shadow.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ export class KolSingleSelect implements SingleSelectAPI {
7373
if (this.state._disabled) {
7474
return;
7575
} else {
76-
this._isOpen = !this._isOpen;
77-
if (this._isOpen) {
76+
if (!this._hasOpened) {
77+
this._isOpen = true;
78+
this._hasOpened = true;
7879
this.refInput?.focus();
7980
const selectedIndex = Array.isArray(this._filteredOptions) ? this._filteredOptions.findIndex((option) => option.label === this._inputValue) : -1;
80-
this._focusedOptionIndex = selectedIndex >= 0 ? selectedIndex : 0;
81+
this._focusedOptionIndex = selectedIndex >= 0 ? selectedIndex : -1;
8182
this.focusOption(this._focusedOptionIndex);
8283
}
8384
}
@@ -89,6 +90,7 @@ export class KolSingleSelect implements SingleSelectAPI {
8990
this._filteredOptions = [...this.state._options];
9091
}
9192
this._isOpen = false;
93+
this._hasOpened = false;
9294
}
9395

9496
private clearSelection() {
@@ -252,6 +254,7 @@ export class KolSingleSelect implements SingleSelectAPI {
252254
{this._inputValue && !this.state._hideClearButton && (
253255
<KolIconTag
254256
_icons="codicon codicon-close"
257+
data-testid="single-select-delete"
255258
_label={translate('kol-delete-selection')}
256259
onClick={() => {
257260
this.clearSelection();
@@ -359,6 +362,8 @@ export class KolSingleSelect implements SingleSelectAPI {
359362
break;
360363
case 'Esc':
361364
case 'Escape': {
365+
this._hasOpened = false;
366+
this._isOpen = false;
362367
handleEvent(false);
363368
break;
364369
}
@@ -422,6 +427,9 @@ export class KolSingleSelect implements SingleSelectAPI {
422427
private _inputValue: string = '';
423428
@State()
424429
private blockSuggestionMouseOver: boolean = false;
430+
@State()
431+
private _hasOpened = false;
432+
425433
/**
426434
* Defines which key combination can be used to trigger or focus the interactive element of the component.
427435
*/

packages/components/src/components/single-select/single-select.e2e.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test } from '@stencil/playwright';
22
import { testInputCallbacksAndEvents, testInputValueReflection } from '../../e2e';
33
import type { FillAction } from '../../e2e/utils/FillAction';
4+
import { expect } from '@playwright/test';
45

56
const COMPONENT_NAME = 'kol-single-select';
67
const TEST_VALUE = 'E';
@@ -20,4 +21,108 @@ const fillAction: FillAction = async (page) => {
2021
test.describe(COMPONENT_NAME, () => {
2122
testInputValueReflection<HTMLKolSingleSelectElement>(COMPONENT_NAME, TEST_VALUE, fillAction, OPTIONS_ATTRIBUTE);
2223
testInputCallbacksAndEvents<HTMLKolSingleSelectElement>(COMPONENT_NAME, TEST_VALUE, fillAction, undefined, OPTIONS_ATTRIBUTE);
24+
25+
test.describe('kol-single-select additional interactions', () => {
26+
test('should open listbox on button click and close on ESC', async ({ page }) => {
27+
await page.setContent(`<kol-single-select _label="Input" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
28+
29+
await page.getByRole('button').click();
30+
31+
await expect(page.getByRole('listbox')).toBeVisible();
32+
33+
await page.keyboard.press('Escape');
34+
35+
await expect(page.getByRole('listbox')).toHaveCount(0);
36+
});
37+
38+
test('should move focus with arrow keys and select with Enter', async ({ page }) => {
39+
await page.setContent(`<kol-single-select _label="Input" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
40+
41+
await page.getByRole('button').click();
42+
43+
await page.keyboard.press('ArrowDown');
44+
await page.keyboard.press('ArrowDown');
45+
await page.keyboard.press('Enter');
46+
47+
const value = await page.locator('kol-single-select').evaluate((el: HTMLKolInputDateElement) => el._value);
48+
expect(value).toBe('S');
49+
});
50+
51+
test('should filter options when typing and select the filtered one', async ({ page }) => {
52+
await page.setContent(`<kol-single-select _label="Input" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
53+
await page.getByRole('button').click();
54+
55+
await page.locator('input.kol-single-select__input').focus();
56+
await page.locator('input.kol-single-select__input').fill('We');
57+
58+
await expect(page.getByText('West')).toBeVisible();
59+
await expect(page.getByText('North')).toHaveCount(0);
60+
61+
await page.keyboard.press('ArrowDown');
62+
await page.keyboard.press('Enter');
63+
64+
const value = await page.locator('kol-single-select').evaluate((el: HTMLKolInputDateElement) => el._value);
65+
expect(value).toBe('W');
66+
});
67+
68+
test('should clear the selection when clear button is clicked', async ({ page }) => {
69+
await page.setContent(`<kol-single-select _label="Input" _options='${JSON.stringify(OPTIONS)}' ></kol-single-select>`);
70+
await page.getByRole('button').click();
71+
72+
const input = page.locator('input.kol-single-select__input');
73+
74+
await page.getByRole('listbox').getByText(TEST_LABEL).click({ force: true });
75+
76+
await expect(input).toHaveValue(TEST_LABEL);
77+
await page.waitForChanges();
78+
await page.waitForTimeout(500);
79+
80+
const clearButton = page.getByTestId('single-select-delete');
81+
await expect(clearButton).toHaveCount(1);
82+
await clearButton.click({ force: true });
83+
84+
await expect(input).toHaveValue('');
85+
});
86+
87+
test('should not render clear button when _hideClearButton is true', async ({ page }) => {
88+
await page.setContent(`<kol-single-select _label="Input" _hideClearButton="true" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
89+
90+
await page.getByRole('button').click();
91+
await page.getByRole('listbox').getByText(TEST_LABEL).click({ force: true });
92+
93+
if (page.locator('input.kol-single-select__input')) await expect(page.locator('input.kol-single-select__input')).toHaveValue(TEST_LABEL);
94+
const clearButton = page.getByTestId('single-select-delete');
95+
await expect(clearButton).not.toBeVisible();
96+
});
97+
98+
test('should select option with SPACE key', async ({ page }) => {
99+
await page.setContent(`<kol-single-select _label="Input" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
100+
await page.getByRole('button').click();
101+
102+
const input = page.locator('input.kol-single-select__input');
103+
104+
await input.click();
105+
await input.press('ArrowDown');
106+
await input.press('Space');
107+
108+
await expect(page.locator('kol-single-select')).toHaveJSProperty('_value', 'N');
109+
});
110+
111+
test('should disable interaction when _disabled is true', async ({ page }) => {
112+
await page.setContent(`<kol-single-select _label="Input" _disabled="true" _options='${JSON.stringify(OPTIONS)}'></kol-single-select>`);
113+
114+
await expect(page.locator('input.kol-single-select__input')).toBeDisabled();
115+
116+
const listbox = page.locator('ul[role="listbox"]');
117+
await expect(listbox).toHaveCount(0);
118+
});
119+
120+
test('should display no results message when input does not match', async ({ page }) => {
121+
await page.setContent(`<kol-single-select _label="Test" _options='[{"label":"North","value":"N"}]'></kol-single-select>`);
122+
const input = page.locator('input.kol-single-select__input');
123+
await input.fill('Something');
124+
const noResult = page.getByText('Keine Ergebnisse gefunden.');
125+
await expect(noResult).toBeVisible();
126+
});
127+
});
23128
});

packages/components/src/components/table-stateful/shadow.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,12 @@ export class KolTableStateful implements TableAPI {
419419
*
420420
* @returns {JSX.Element} The rendered pagination controls including page range and navigation.
421421
*/
422-
private renderPagination(): JSX.Element {
422+
private renderPagination(position: 'top' | 'bottom'): JSX.Element {
423+
const label = translate('kol-table-pagination-label', {
424+
placeholders: {
425+
label: `${this.state._label} (${translate(`kol-pagination-position-${position}`)})`,
426+
},
427+
});
423428
return (
424429
<div class={`kol-table-stateful__pagination kol-table-stateful__pagination--${this.state._paginationPosition}`}>
425430
<span>
@@ -448,7 +453,7 @@ export class KolTableStateful implements TableAPI {
448453
_siblingCount={this.state._pagination._siblingCount}
449454
_tooltipAlign="bottom"
450455
_max={this.state._pagination._max || this.state._pagination._max || this.state._data.length}
451-
_label={translate('kol-table-pagination-label', { placeholders: { label: this.state._label } })}
456+
_label={label}
452457
></KolPaginationTag>
453458
</div>
454459
</div>
@@ -520,8 +525,8 @@ export class KolTableStateful implements TableAPI {
520525
this.showPagination ? (this.state._pagination?._pageSize ?? 10) : this.state._sortedData.length,
521526
this.state._pagination._page || 1,
522527
);
523-
const paginationTop = this._paginationPosition === 'top' || this._paginationPosition === 'both' ? this.renderPagination() : null;
524-
const paginationBottom = this._paginationPosition === 'bottom' || this._paginationPosition === 'both' ? this.renderPagination() : null;
528+
const paginationTop = this._paginationPosition === 'top' || this._paginationPosition === 'both' ? this.renderPagination('top') : null;
529+
const paginationBottom = this._paginationPosition === 'bottom' || this._paginationPosition === 'both' ? this.renderPagination('bottom') : null;
525530

526531
const headerCells: TableHeaderCells = {
527532
horizontal: this.state._headers.horizontal?.map((row) => row.map((cell) => ({ ...cell, sortDirection: this.getHeaderCellSortState(cell) }))),

packages/components/src/locales/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ export default {
5050
'delete-selection': 'Auswahl entfernen',
5151
'filename-text': 'Datei auswählen oder hier ablegen...',
5252
'data-browse-text': 'Datei auswählen',
53+
'pagination-position-top': 'oben',
54+
'pagination-position-bottom': 'unten',
5355
};

packages/components/src/locales/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ export default {
5050
'delete-selection': 'Delete selection',
5151
'filename-text': 'Choose a file or drop it here...',
5252
'data-browse-text': 'Browse',
53+
'pagination-position-top': 'top',
54+
'pagination-position-bottom': 'bottom',
5355
};

packages/samples/react/src/components/SampleDescription.tsx

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,48 @@ import { PUBLIC_CODE_COMPONENT_URL, PUBLIC_DOC_COMPONENT_URL } from '../shares/c
66
import { KolLink } from '@public-ui/react';
77

88
import { HideMenusContext } from '../shares/HideMenusContext';
9-
10-
const getLocationPaths = () => {
11-
return location.hash.split('/').slice(1);
12-
};
9+
import { useLocation } from 'react-router';
1310

1411
export const SampleDescription: FC<PropsWithChildren> = (props) => {
1512
const hideMenus = useContext(HideMenusContext);
13+
const location = useLocation();
14+
const paths = location.pathname.split('/').slice(1);
1615

1716
const docLink = useMemo(() => {
18-
const paths = getLocationPaths();
1917
return paths[0] === 'scenarios'
2018
? null // Scenarios are not a component and hence have no documentation.
2119
: `${PUBLIC_DOC_COMPONENT_URL}/${paths[0]}`;
2220
}, [location.hash]);
2321

2422
const codeLink = useMemo(() => {
25-
const paths = getLocationPaths();
2623
return paths[0] === 'scenarios'
2724
? null // Scenarios are not a component and hence have no documentation.
2825
: `${PUBLIC_CODE_COMPONENT_URL}/${paths[0]}/${paths[1]}.tsx`;
2926
}, [location.hash]);
3027

31-
return hideMenus ? null : (
32-
<div className="flex justify-between mb-sm">
33-
<div className="indented-text">{props.children}</div>
34-
<div className="flex flex-wrap gap-2 shrink-0 ml">
35-
{codeLink && <KolLink _href={codeLink} _label="Code" _target="_blank" />}
36-
{docLink && <KolLink _href={docLink} _label="Documentation" _target="_blank" />}
37-
<KolLink _href={`${location.href}?hideMenus`} _label="Standalone example" _target="_blank" />
38-
</div>
39-
</div>
28+
return (
29+
<>
30+
<h1 className="visually-hidden">{location.pathname.replace(/\//g, ' ')}</h1>
31+
{hideMenus ? null : (
32+
<div className="grid sm:flex gap-4 justify-between pb-sm border-b-1 border-b-solid border-gray mb-2">
33+
<div className="indented-text">{props.children}</div>
34+
<ul className="flex flex-wrap gap-2 list-none m-0 p-0">
35+
{codeLink && (
36+
<li>
37+
<KolLink _href={codeLink} _label="Code" _target="_blank" />
38+
</li>
39+
)}
40+
{docLink && (
41+
<li>
42+
<KolLink _href={docLink} _label="Documentation" _target="_blank" />
43+
</li>
44+
)}
45+
<li>
46+
<KolLink _href={`#${location.pathname}?hideMenus`} _label="Standalone example" _target="_blank" />
47+
</li>
48+
</ul>
49+
</div>
50+
)}
51+
</>
4052
);
4153
};

0 commit comments

Comments
 (0)