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

feat(components): create post-listbox and post-listbox-item #4187

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions .changeset/shaggy-icons-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@swisspost/design-system-documentation': minor
'@swisspost/design-system-components': minor
---

Implemented listbox raw component.
Binary file not shown.
121 changes: 121 additions & 0 deletions packages/components/cypress/e2e/listbox.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
describe('PostListbox', { baseUrl: null, includeShadowDom: false }, () => {
beforeEach(() => {
// Visit the page where the component is rendered
cy.visit('./cypress/fixtures/post-listbox.test.html');
});

it('should render the post-listbox component', () => {
// Check if the post-listbox component is rendered
cy.get('post-listbox').should('exist');
});

it('should have an id for the first div in post-listbox', () => {
// Ensure the first div inside post-listbox has an id attribute
cy.get('post-listbox')
.find('div')
.first()
.should('have.attr', 'id')
.and('not.be.empty')
.then($div => {
const id = $div['id'];
cy.log(`First div ID: ${id}`);
});
});

it('should throw an error if the label is missing', () => {
// Check for the mandatory label accessibility error if no label is provided
cy.on('uncaught:exception', err => {
expect(err.message).to.include(
'Please provide a label to the listbox component. Label is mandatory for accessibility purposes.',
);
return false;
});
cy.get('post-listbox').within(() => {
cy.get('[slot="post-listbox-item"]').first().invoke('remove');
});
});

it('should hide the label when label-hidden is set', () => {
// Set the `label-hidden` property and check if the label is hidden
cy.get('post-listbox').invoke('attr', 'label-hidden', true);
cy.get('post-listbox div').first().should('have.class', 'visually-hidden');
});

it('should ensure post-listbox-item components have the correct slot and role', () => {
// Verify that the `post-listbox-item` components have the correct slot and role attributes
cy.get('post-listbox').within(() => {
cy.get('post-listbox-item').each($el => {
cy.wrap($el)
.should('have.attr', 'slot', 'post-listbox-item')
.and('have.attr', 'role', 'option');
});
});
});

it('should have correct ARIA attributes on the listbox', () => {
cy.get('post-listbox [role="listbox"]')
.should('have.attr', 'aria-labelledby')
.and('not.be.empty');
cy.get('post-listbox [role="listbox"]').should('have.attr', 'aria-activedescendant');
});

it('should allow navigation with keyboard for single-select', () => {
cy.get('post-listbox [role="listbox"]').focus().type('{downarrow}');
cy.get('post-listbox [role="listbox"]')
.invoke('attr', 'aria-activedescendant')
.then(id => {
cy.get(`#${id}`).should('exist').and('have.focus');
});
});

it('should allow selection with Space key for multi-select', () => {
cy.get('post-listbox').invoke('attr', 'multiselect', true);
cy.get('post-listbox [role="listbox"]').focus().type(' ');
cy.get('post-listbox-item[selected]').should('exist');
});

it('should highlight search term in listbox items', () => {
cy.get('post-listbox').invoke('attr', 'search-term', 'abc');
cy.get('post-listbox-item mark').each($el => {
cy.wrap($el)
.invoke('text')
.should(text => {
expect(text.toLowerCase()).to.contain('abc'.toLowerCase());
});
});
});

it('should set active and selected item on click', () => {
cy.get('post-listbox [slot="post-listbox-item"]').first().click();
cy.get('post-listbox [role="listbox"]')
.invoke('attr', 'aria-activedescendant')
.should('not.be.empty');
cy.get('post-listbox-item[selected]').should('exist');
});

it('should initialize pre-selected items correctly', () => {
cy.get('post-listbox-item[selected]').each($el => {
cy.wrap($el).should('have.attr', 'selected', 'selected');
});
});

it('should retain state after losing focus', () => {
cy.get('post-listbox [role="listbox"]').focus().type('{downarrow}').type('{downarrow}');
cy.get('post-listbox [role="listbox"]')
.invoke('attr', 'aria-activedescendant')
.then(lastFocusedId => {
cy.get(`#${lastFocusedId}`).blur();
cy.get('post-listbox [role="listbox"]').focus();
cy.get('post-listbox [role="listbox"]')
.invoke('attr', 'aria-activedescendant')
.should('equal', lastFocusedId);
});
});
});

describe('Accessibility', () => {
it('Has no detectable a11y violations on load for all variants', () => {
cy.getSnapshots('post-listbox');
cy.checkA11y('#root-inner');
});
});
29 changes: 29 additions & 0 deletions packages/components/cypress/fixtures/post-listbox.test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/post-components/post-components.esm.js" type="module"></script>
</head>
<body>
<post-listbox search-term="abc" label-hidden multiselect="true">
Label
<post-listbox-item><button>Option 1</button></post-listbox-item>
<post-listbox-item><a href="#">Option ABC</a></post-listbox-item>
<post-listbox-item><post-icon name="3000"></post-icon>Option 3</post-listbox-item>
<post-listbox-item>Option 4</post-listbox-item>
<post-listbox-item><button>Option 5</button></post-listbox-item>
<post-listbox-item><a href="#">Option 6</a></post-listbox-item>
<post-listbox-item><post-icon name="3000"></post-icon>Option ABC</post-listbox-item>
<post-listbox-item>Option 8</post-listbox-item>
<post-listbox-item>Option 9</post-listbox-item>
<post-listbox-item><a href="#">Option 10</a></post-listbox-item>
<post-listbox-item><post-icon name="3000"></post-icon>ABC Option 11</post-listbox-item>
<post-listbox-item>Option 12</post-listbox-item>
<post-listbox-item>Option 13</post-listbox-item>
<post-listbox-item>Option 14</post-listbox-item>
<post-listbox-item selected>Option 15</post-listbox-item>
</post-listbox>
</body>
</html>
66 changes: 66 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,30 @@ export namespace Components {
}
interface PostListItem {
}
interface PostListbox {
/**
* If `true`, the listbox title will be hidden. Otherwise, it will be displayed
*/
"labelHidden": boolean;
/**
* The description of the listbox
*/
"listboxDescription"?: string;
/**
* If `true`, the listbox is multiselectable
*/
"multiselect": boolean;
/**
* A string to be highlighted to indicate a search term
*/
"searchTerm"?: string;
}
interface PostListboxItem {
/**
* Indicates if the item is currently active. This will be set dynamically by the parent `listbox`.
*/
"selected": boolean;
}
interface PostLogo {
/**
* The URL to which the user is redirected upon clicking the logo.
Expand Down Expand Up @@ -682,6 +706,18 @@ declare global {
prototype: HTMLPostListItemElement;
new (): HTMLPostListItemElement;
};
interface HTMLPostListboxElement extends Components.PostListbox, HTMLStencilElement {
}
var HTMLPostListboxElement: {
prototype: HTMLPostListboxElement;
new (): HTMLPostListboxElement;
};
interface HTMLPostListboxItemElement extends Components.PostListboxItem, HTMLStencilElement {
}
var HTMLPostListboxItemElement: {
prototype: HTMLPostListboxItemElement;
new (): HTMLPostListboxItemElement;
};
interface HTMLPostLogoElement extends Components.PostLogo, HTMLStencilElement {
}
var HTMLPostLogoElement: {
Expand Down Expand Up @@ -864,6 +900,8 @@ declare global {
"post-language-switch": HTMLPostLanguageSwitchElement;
"post-list": HTMLPostListElement;
"post-list-item": HTMLPostListItemElement;
"post-listbox": HTMLPostListboxElement;
"post-listbox-item": HTMLPostListboxItemElement;
"post-logo": HTMLPostLogoElement;
"post-mainnavigation": HTMLPostMainnavigationElement;
"post-megadropdown": HTMLPostMegadropdownElement;
Expand Down Expand Up @@ -1126,6 +1164,30 @@ declare namespace LocalJSX {
}
interface PostListItem {
}
interface PostListbox {
/**
* If `true`, the listbox title will be hidden. Otherwise, it will be displayed
*/
"labelHidden"?: boolean;
/**
* The description of the listbox
*/
"listboxDescription"?: string;
/**
* If `true`, the listbox is multiselectable
*/
"multiselect"?: boolean;
/**
* A string to be highlighted to indicate a search term
*/
"searchTerm"?: string;
}
interface PostListboxItem {
/**
* Indicates if the item is currently active. This will be set dynamically by the parent `listbox`.
*/
"selected"?: boolean;
}
interface PostLogo {
/**
* The URL to which the user is redirected upon clicking the logo.
Expand Down Expand Up @@ -1301,6 +1363,8 @@ declare namespace LocalJSX {
"post-language-switch": PostLanguageSwitch;
"post-list": PostList;
"post-list-item": PostListItem;
"post-listbox": PostListbox;
"post-listbox-item": PostListboxItem;
"post-logo": PostLogo;
"post-mainnavigation": PostMainnavigation;
"post-megadropdown": PostMegadropdown;
Expand Down Expand Up @@ -1347,6 +1411,8 @@ declare module "@stencil/core" {
"post-language-switch": LocalJSX.PostLanguageSwitch & JSXBase.HTMLAttributes<HTMLPostLanguageSwitchElement>;
"post-list": LocalJSX.PostList & JSXBase.HTMLAttributes<HTMLPostListElement>;
"post-list-item": LocalJSX.PostListItem & JSXBase.HTMLAttributes<HTMLPostListItemElement>;
"post-listbox": LocalJSX.PostListbox & JSXBase.HTMLAttributes<HTMLPostListboxElement>;
"post-listbox-item": LocalJSX.PostListboxItem & JSXBase.HTMLAttributes<HTMLPostListboxItemElement>;
"post-logo": LocalJSX.PostLogo & JSXBase.HTMLAttributes<HTMLPostLogoElement>;
"post-mainnavigation": LocalJSX.PostMainnavigation & JSXBase.HTMLAttributes<HTMLPostMainnavigationElement>;
"post-megadropdown": LocalJSX.PostMegadropdown & JSXBase.HTMLAttributes<HTMLPostMegadropdownElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component, Prop, Element, Host, h } from '@stencil/core';
/*
* @slot default- Slot for placing the content of the list item.
*/

@Component({
tag: 'post-listbox-item',
shadow: true,
})
export class PostListboxItem {
@Element() host: HTMLPostListboxItemElement;

/**
* Indicates if the item is currently active.
* This will be set dynamically by the parent `listbox`.
*/
@Prop() selected: boolean = false;

connectedCallback() {
this.host.setAttribute('slot', 'post-listbox-item');
}

render() {
return (
<Host role="option" tabindex="-1">
<slot></slot>
</Host>
);
}
}
17 changes: 17 additions & 0 deletions packages/components/src/components/post-listbox-item/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# post-listbox-item



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ------------------------------------------------------------------------------------------------ | --------- | ------- |
| `selected` | `selected` | Indicates if the item is currently active. This will be set dynamically by the parent `listbox`. | `boolean` | `false` |


----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use '@swisspost/design-system-styles/core' as post;

post-listbox {
display: flex;
flex-direction: column;
gap: var(--post-listbox-heading-gap);
width: auto;
max-height: 100%;
overflow-y: auto;

> div[role='listbox'] {
display: flex;
flex-direction: column;
gap: var(--post-listbox-item-gap);
post-listbox-item {
mark {
background-color: var(--post-listbox-highlight-color, yellow);
}
}
}
}
Loading
Loading