Skip to content

Commit

Permalink
feat(side menu): new <bq-side-menu> and <bq-side-menu-item> compo…
Browse files Browse the repository at this point in the history
…nents (#289)
  • Loading branch information
dgonzalezr committed Jun 23, 2023
1 parent a74f0ad commit a44b2dd
Show file tree
Hide file tree
Showing 23 changed files with 1,268 additions and 2 deletions.
108 changes: 108 additions & 0 deletions packages/bee-q/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TDividerOrientation, TDividerStrokeLinecap, TDividerTitleAlignment } fr
import { TIconWeight } from "./components/icon/bq-icon.types";
import { TNotificationType } from "./components/notification/bq-notification.types";
import { TRadioGroupOrientation } from "./components/radio-group/bq-radio-group.types";
import { TSideMenuAppearance, TSideMenuSize } from "./components/side-menu/bq-side-menu.types";
import { TSliderType } from "./components/slider/bq-slider.types";
import { TSpinnerSize, TSpinnerTextPosition } from "./components/spinner/bq-spinner.types";
import { TStatusType } from "./components/status/bq-status.types";
Expand All @@ -25,6 +26,7 @@ export { TDividerOrientation, TDividerStrokeLinecap, TDividerTitleAlignment } fr
export { TIconWeight } from "./components/icon/bq-icon.types";
export { TNotificationType } from "./components/notification/bq-notification.types";
export { TRadioGroupOrientation } from "./components/radio-group/bq-radio-group.types";
export { TSideMenuAppearance, TSideMenuSize } from "./components/side-menu/bq-side-menu.types";
export { TSliderType } from "./components/slider/bq-slider.types";
export { TSpinnerSize, TSpinnerTextPosition } from "./components/spinner/bq-spinner.types";
export { TStatusType } from "./components/status/bq-status.types";
Expand Down Expand Up @@ -343,6 +345,38 @@ export namespace Components {
*/
"value"?: string;
}
interface BqSideMenu {
/**
* It sets a predefined appearance of the side menu
*/
"appearance": TSideMenuAppearance;
/**
* If true, the container will reduce its width
*/
"collapse": boolean;
/**
* It sets the size of the navigation menu items
*/
"size": TSideMenuSize;
/**
* Toggle the collapse state of the side menu
*/
"toggleCollapse": () => Promise<void>;
}
interface BqSideMenuItem {
/**
* If true, the menu item will be shown as active/selected.
*/
"active": boolean;
/**
* If true, the item label and suffix will be hidden and the with will be reduce according to its parent
*/
"collapse": boolean;
/**
* If true, the menu item will be disabled (no interaction allowed)
*/
"disabled": boolean;
}
interface BqSlider {
/**
* A number representing the delay value applied to bqChange event handler
Expand Down Expand Up @@ -574,6 +608,14 @@ export interface BqRadioGroupCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBqRadioGroupElement;
}
export interface BqSideMenuCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBqSideMenuElement;
}
export interface BqSideMenuItemCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBqSideMenuItemElement;
}
export interface BqSliderCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBqSliderElement;
Expand Down Expand Up @@ -654,6 +696,18 @@ declare global {
prototype: HTMLBqRadioGroupElement;
new (): HTMLBqRadioGroupElement;
};
interface HTMLBqSideMenuElement extends Components.BqSideMenu, HTMLStencilElement {
}
var HTMLBqSideMenuElement: {
prototype: HTMLBqSideMenuElement;
new (): HTMLBqSideMenuElement;
};
interface HTMLBqSideMenuItemElement extends Components.BqSideMenuItem, HTMLStencilElement {
}
var HTMLBqSideMenuItemElement: {
prototype: HTMLBqSideMenuItemElement;
new (): HTMLBqSideMenuItemElement;
};
interface HTMLBqSliderElement extends Components.BqSlider, HTMLStencilElement {
}
var HTMLBqSliderElement: {
Expand Down Expand Up @@ -713,6 +767,8 @@ declare global {
"bq-notification": HTMLBqNotificationElement;
"bq-radio": HTMLBqRadioElement;
"bq-radio-group": HTMLBqRadioGroupElement;
"bq-side-menu": HTMLBqSideMenuElement;
"bq-side-menu-item": HTMLBqSideMenuItemElement;
"bq-slider": HTMLBqSliderElement;
"bq-spinner": HTMLBqSpinnerElement;
"bq-status": HTMLBqStatusElement;
Expand Down Expand Up @@ -1054,6 +1110,54 @@ declare namespace LocalJSX {
*/
"value"?: string;
}
interface BqSideMenu {
/**
* It sets a predefined appearance of the side menu
*/
"appearance"?: TSideMenuAppearance;
/**
* If true, the container will reduce its width
*/
"collapse"?: boolean;
/**
* Callback handler to be called when the Side menu changes its width from expanded to collapse and vice versa
*/
"onBqCollapse"?: (event: BqSideMenuCustomEvent<{ collapse: boolean }>) => void;
/**
* Callback handler to be called when the active/selected menu item changes
*/
"onBqSelect"?: (event: BqSideMenuCustomEvent<HTMLBqSideMenuItemElement>) => void;
/**
* It sets the size of the navigation menu items
*/
"size"?: TSideMenuSize;
}
interface BqSideMenuItem {
/**
* If true, the menu item will be shown as active/selected.
*/
"active"?: boolean;
/**
* If true, the item label and suffix will be hidden and the with will be reduce according to its parent
*/
"collapse"?: boolean;
/**
* If true, the menu item will be disabled (no interaction allowed)
*/
"disabled"?: boolean;
/**
* Handler to be called when the button loses focus
*/
"onBqBlur"?: (event: BqSideMenuItemCustomEvent<HTMLBqSideMenuItemElement>) => void;
/**
* Handler to be called when button gets focus
*/
"onBqClick"?: (event: BqSideMenuItemCustomEvent<HTMLBqSideMenuItemElement>) => void;
/**
* Handler to be called when the button is clicked
*/
"onBqFocus"?: (event: BqSideMenuItemCustomEvent<HTMLBqSideMenuItemElement>) => void;
}
interface BqSlider {
/**
* A number representing the delay value applied to bqChange event handler
Expand Down Expand Up @@ -1278,6 +1382,8 @@ declare namespace LocalJSX {
"bq-notification": BqNotification;
"bq-radio": BqRadio;
"bq-radio-group": BqRadioGroup;
"bq-side-menu": BqSideMenu;
"bq-side-menu-item": BqSideMenuItem;
"bq-slider": BqSlider;
"bq-spinner": BqSpinner;
"bq-status": BqStatus;
Expand Down Expand Up @@ -1309,6 +1415,8 @@ declare module "@stencil/core" {
"bq-notification": LocalJSX.BqNotification & JSXBase.HTMLAttributes<HTMLBqNotificationElement>;
"bq-radio": LocalJSX.BqRadio & JSXBase.HTMLAttributes<HTMLBqRadioElement>;
"bq-radio-group": LocalJSX.BqRadioGroup & JSXBase.HTMLAttributes<HTMLBqRadioGroupElement>;
"bq-side-menu": LocalJSX.BqSideMenu & JSXBase.HTMLAttributes<HTMLBqSideMenuElement>;
"bq-side-menu-item": LocalJSX.BqSideMenuItem & JSXBase.HTMLAttributes<HTMLBqSideMenuItemElement>;
"bq-slider": LocalJSX.BqSlider & JSXBase.HTMLAttributes<HTMLBqSliderElement>;
/**
* Spinners are designed for users to display data loading.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { newE2EPage } from '@stencil/core/testing';

describe('bq-side-menu-item', () => {
it('should render', async () => {
const page = await newE2EPage();
await page.setContent('<bq-side-menu-item></bq-side-menu-item>');

const menuItemElem = await page.find('bq-side-menu-item');
expect(menuItemElem).toHaveClass('hydrated');
});

it('should have shadow root', async () => {
const page = await newE2EPage();
await page.setContent('<bq-side-menu-item></bq-side-menu-item>');

const menuItemElem = await page.find('bq-side-menu-item');
expect(menuItemElem.shadowRoot).not.toBeNull();
});

it('should display text', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item disabled="true">
Menu item label
</bq-side-menu-item>
`);

const menuItemElem = await page.find('bq-side-menu-item');
expect(menuItemElem).toEqualText('Menu item label');
});

it('should trigger click', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item>
Menu item label
</bq-side-menu-item>
`);

const bqFocus = await page.spyOnEvent('bqFocus');
const bqBlur = await page.spyOnEvent('bqBlur');
const bqClick = await page.spyOnEvent('bqClick');

const menuItemElem = await page.find('bq-side-menu-item');

await menuItemElem.click();

expect(bqFocus).toHaveReceivedEventTimes(1);
expect(bqBlur).toHaveReceivedEventTimes(0);
expect(bqClick).toHaveReceivedEventTimes(1);
});
});

it('should be keyboard accessible', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item>
<bq-icon size="18" name="user" slot="prefix"></bq-icon>
<span>Verified users</span>
</bq-side-menu-item>
`);

const bqFocus = await page.spyOnEvent('bqFocus');
const bqBlur = await page.spyOnEvent('bqBlur');
const bqClick = await page.spyOnEvent('bqClick');

await page.keyboard.press('Tab');

expect(bqFocus).toHaveReceivedEventTimes(1);
expect(bqClick).toHaveReceivedEventTimes(0);
expect(bqBlur).toHaveReceivedEventTimes(0);
});

it('should handle `active` property', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item active="true">
Menu item label
</bq-side-menu-item>
`);

const menuItemElem = await page.find('bq-side-menu-item >>> .bq-side-menu--item');
expect(menuItemElem).toHaveClass('active');
});

it('should handle `disabled` property', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item disabled="true">
Menu item label
</bq-side-menu-item>
`);

const bqFocus = await page.spyOnEvent('bqSideMenuItemFocus');
const bqBlur = await page.spyOnEvent('bqSideMenuItemBlur');
const bqClick = await page.spyOnEvent('bqSideMenuItemClick');

const menuItemElem = await page.find('bq-side-menu-item');

menuItemElem.click();

await page.waitForChanges();

expect(bqFocus).toHaveReceivedEventTimes(0);
expect(bqClick).toHaveReceivedEventTimes(0);
expect(bqBlur).toHaveReceivedEventTimes(0);
});

it('should render prefix element', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item>
<span slot="prefix">Prefix</span>
Dashboard
</bq-side-menu-item>
`);

const prefixText = await page.$eval('bq-side-menu-item', (element) => {
const slotElement = element.shadowRoot.querySelector('slot[name="prefix"]');
const assignedElements = (slotElement as HTMLSlotElement).assignedElements({ flatten: true })[0];

return assignedElements.textContent;
});

expect(prefixText).toBe('Prefix');
});

it('should render suffix element', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-side-menu-item>
<span slot="suffix">Suffix</span>
Dashboard
</bq-side-menu-item>
`);

const suffixText = await page.$eval('bq-side-menu-item', (element) => {
const slotElement = element.shadowRoot.querySelector('slot[name="suffix"]');
const assignedElements = (slotElement as HTMLSlotElement).assignedElements({ flatten: true })[0];

return assignedElements.textContent;
});

expect(suffixText).toEqualText('Suffix');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ArgTypes, Stories, Title, Subtitle } from '@storybook/addon-docs';

<Title>Side menu item</Title>

A side menu item is a navigation element typically placed on the side or left-hand side of a web page or application.
It serves as a hierarchical or categorical navigation option, allowing users to access different sections or pages within the application.

> The Side menu item component must be used inside the [Side menu component](?path=/docs/components-side-menu--overview) to function properly.
<Subtitle>Usage</Subtitle>

Use icons and labels: Users often rely on both visual cues and text labels to understand the purpose of each menu item. Use distinct icons or symbols to represent different sections or actions, and accompany them with clear text labels for better accessibility.

<Subtitle>👍 When to use</Subtitle>

Side menu items are commonly used in web and mobile applications that require a hierarchical or multi-level navigation structure. They are particularly useful when:

- Navigating different sections: If your application has multiple sections or modules that need to be accessed frequently, a side menu item can provide quick and easy navigation between these sections.
- Organizing content: Side menu items are beneficial when you have a large amount of content or options that need to be organized in a hierarchical manner. By displaying submenus or nested items, users can easily navigate through different levels of content.

<Title>Properties</Title>

<ArgTypes of="bq-side-menu-item" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Args, Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit-html';

import mdx from './bq-side-menu-item.mdx';

const meta: Meta = {
title: 'Components/Side menu/Side menu item',
component: 'bq-side-menu-item',
parameters: {
docs: {
page: mdx,
},
},
argTypes: {
active: { control: 'boolean' },
disabled: { control: 'boolean' },
collapse: { control: 'boolean' },
// Not part of the component
text: { control: 'text', table: { disable: true } },
// Event handlers
bqBlur: { action: 'bqBlur' },
bqFocus: { action: 'bqFocus' },
bqClick: { action: 'bqClick' },
},
args: {
active: false,
disabled: false,
collapse: false,
// Not part of the component
text: 'Menu item',
},
};
export default meta;

type Story = StoryObj;

const Template = (args: Args) => html`
<bq-side-menu-item
?active=${args.active}
?disabled=${args.disabled}
?collapse=${args.collapse}
@bqBlur=${args.bqBlur}
@bqClick=${args.bqClick}
@bqFocus=${args.bqFocus}
>
<bq-icon name="star-four" slot="prefix"></bq-icon>
${args.text}
<bq-badge class="ml-auto" slot="suffix"> 5 </bq-badge>
</bq-side-menu-item>
`;

export const Default: Story = {
render: Template,
args: {},
};
Loading

0 comments on commit a44b2dd

Please sign in to comment.