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(Tab): add orientation support #1091

Merged
merged 16 commits into from
Aug 9, 2024
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
36 changes: 34 additions & 2 deletions packages/beeq/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { TStatusType } from "./components/status/bq-status.types";
import { TStepsSize, TStepsType } from "./components/steps/bq-steps.types";
import { TStepItemStatus } from "./components/step-item/bq-step-item.types";
import { TSwitchInnerLabel, TSwitchJustifyContent } from "./components/switch/bq-switch.types";
import { TTabSize } from "./components/tab/bq-tab.types";
import { TTabOrientation, TTabPlacement, TTabSize } from "./components/tab/bq-tab.types";
import { TTagBorderRadius, TTagColor, TTagSize, TTagVariant } from "./components/tag/bq-tag.types";
import { TTextareaAutoCapitalize, TTextareaWrap } from "./components/textarea/bq-textarea.types";
import { TToastBorderRadius, TToastPlacement, TToastType } from "./components/toast/bq-toast.types";
Expand Down Expand Up @@ -59,7 +59,7 @@ export { TStatusType } from "./components/status/bq-status.types";
export { TStepsSize, TStepsType } from "./components/steps/bq-steps.types";
export { TStepItemStatus } from "./components/step-item/bq-step-item.types";
export { TSwitchInnerLabel, TSwitchJustifyContent } from "./components/switch/bq-switch.types";
export { TTabSize } from "./components/tab/bq-tab.types";
export { TTabOrientation, TTabPlacement, TTabSize } from "./components/tab/bq-tab.types";
export { TTagBorderRadius, TTagColor, TTagSize, TTagVariant } from "./components/tag/bq-tag.types";
export { TTextareaAutoCapitalize, TTextareaWrap } from "./components/textarea/bq-textarea.types";
export { TToastBorderRadius, TToastPlacement, TToastType } from "./components/toast/bq-toast.types";
Expand Down Expand Up @@ -1233,6 +1233,14 @@ export namespace Components {
* Sets tabindex on the native `<button>` HTML element used under the hood. This method is used inside `<bq-tab-group>` to make tab focusable after the active one is focused
*/
"enableFocus": (value: boolean) => Promise<void>;
/**
* The direction that tab should be render
*/
"orientation"?: TTabOrientation;
/**
* The placement that tab should be render
*/
"placement"?: TTabPlacement;
/**
* The size of the tab
*/
Expand Down Expand Up @@ -1263,6 +1271,14 @@ export namespace Components {
* If true, the underline divider below the tabs won't be shown
*/
"disableDivider": boolean;
/**
* The direction that tab should be render
*/
"orientation"?: TTabOrientation;
/**
* The placement that tab should be render
*/
"placement"?: TTabPlacement;
/**
* The size of the tab
*/
Expand Down Expand Up @@ -3643,6 +3659,14 @@ declare namespace LocalJSX {
* Handler to be called when the tab key is pressed
*/
"onBqKeyDown"?: (event: BqTabCustomEvent<KeyboardEvent>) => void;
/**
* The direction that tab should be render
*/
"orientation"?: TTabOrientation;
/**
* The placement that tab should be render
*/
"placement"?: TTabPlacement;
/**
* The size of the tab
*/
Expand All @@ -3665,6 +3689,14 @@ declare namespace LocalJSX {
* Handler to be called when the tab value changes
*/
"onBqChange"?: (event: BqTabGroupCustomEvent<{ target: HTMLBqTabElement; value: string }>) => void;
/**
* The direction that tab should be render
*/
"orientation"?: TTabOrientation;
/**
* The placement that tab should be render
*/
"placement"?: TTabPlacement;
/**
* The size of the tab
*/
Expand Down
dgonzalezr marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Args, Meta, StoryObj } from '@storybook/web-components';
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit-html';

import mdx from './bq-tab-group.mdx';
import { TAB_SIZE } from '../../tab/bq-tab.types';
import { TAB_ORIENTATION, TAB_PLACEMENT, TAB_SIZE } from '../../tab/bq-tab.types';

const meta: Meta = {
title: 'Components/Tabs',
Expand All @@ -14,15 +15,32 @@ const meta: Meta = {
},
argTypes: {
size: { control: 'select', options: [...TAB_SIZE] },
orientation: { control: 'select', options: [...TAB_ORIENTATION] },
placement: { control: 'select', options: [...TAB_PLACEMENT] },
'disable-divider': { control: 'boolean' },
tabs: { control: 'text', table: { disable: true } },
icons: { control: 'text', table: { disable: true } },
// Event handlers
bqChange: { action: 'bqChange' },
bqFocus: { action: 'bqFocus', table: { disable: true } },
bqBlur: { action: 'bqBlur', table: { disable: true } },
},
args: {
size: 'medium',
orientation: 'horizontal',
placement: 'start',
'disable-divider': false,
// Not part of the public API, so we don't want to expose it in the docs
tabs: [
{ id: 1, label: 'Tab' },
{ id: 2, label: 'Tab' },
{ id: 3, label: 'Long Tab name' },
{ id: 4, label: 'Tab', disabled: true },
{ id: 5, label: 'Tab' },
{ id: 6, label: 'Tab' },
{ id: 7, label: 'Tab' },
{ id: 8, label: 'Tab' },
],
},
};
export default meta;
Expand All @@ -31,52 +49,64 @@ type Story = StoryObj;

const Template = (args: Args) => {
return html`
<bq-tab-group
value="5"
.size=${args.size}
?disable-divider=${args['disable-divider']}
@bqChange=${args.bqChange}
@bqFocus=${args.bqFocus}
@bqBlur=${args.bqBlur}
<main
class=${classMap({
flex: args.orientation === 'vertical',
'flex-row-reverse': args.placement === 'end',
'gap-xs2': true,
})}
>
<bq-tab tab-id="1">Tab</bq-tab>
<bq-tab tab-id="2">Tab</bq-tab>
<bq-tab tab-id="3">Long Tab name</bq-tab>
<bq-tab tab-id="4" disabled>Tab</bq-tab>
<bq-tab tab-id="5" active>Tab</bq-tab>
<bq-tab tab-id="6">Tab</bq-tab>
<bq-tab tab-id="7">Tab</bq-tab>
<bq-tab tab-id="8">Tab</bq-tab>
</bq-tab-group>
<bq-tab-group
value="5"
.size=${args.size}
.orientation=${args.orientation}
.placement=${args.placement}
?disable-divider=${args['disable-divider']}
@bqChange=${args.bqChange}
@bqFocus=${args.bqFocus}
@bqBlur=${args.bqBlur}
>
${args.tabs.map(
(tab, index) => html`
<bq-tab tab-id=${tab.id}>
${tab.label}
${args.icons
? html`<bq-icon name="${args.icons[index % args.icons.length]}" slot="icon"> </bq-icon> `
: null}
</bq-tab>
`,
)}
</bq-tab-group>
<div class="border h-80 w-full flex-1 border-dashed border-stroke-primary bg-[--bq-ui--alt]">
<h3 class="m-l">Tab content</h3>
</div>
</main>
`;
};

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

const IconTemplate = (args: Args) => {
return html`
<bq-tab-group
value="5"
.size=${args.size}
?disable-divider=${args['disable-divider']}
@bqChange=${args.bqChange}
@bqFocus=${args.bqFocus}
@bqBlur=${args.bqBlur}
>
<bq-tab tab-id="1"><bq-icon name="pulse" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="2"><bq-icon name="bell" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="3"><bq-icon name="airplane-in-flight" slot="icon"></bq-icon>Long Tab name</bq-tab>
<bq-tab tab-id="4" disabled><bq-icon name="airplane-tilt" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="5" active><bq-icon name="align-right-simple" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="6"><bq-icon name="anchor" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="7"><bq-icon name="anchor-simple" slot="icon"></bq-icon>Tab</bq-tab>
<bq-tab tab-id="8"><bq-icon name="android-logo" slot="icon"></bq-icon>Tab</bq-tab>
</bq-tab-group>
`;
export const Vertical: Story = {
render: Template,
args: {
orientation: 'vertical',
},
};

export const Icon: Story = {
render: IconTemplate,
render: Template,
args: {
icons: [
'pulse',
'bell',
'airplane-in-flight',
'airplane-tilt',
'align-right-simple',
'anchor',
'anchor-simple',
'android-logo',
],
},
};
71 changes: 54 additions & 17 deletions packages/beeq/src/components/tab-group/bq-tab-group.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Component, Element, Event, EventEmitter, h, Listen, Prop, Watch } from '@stencil/core';
import { Component, Element, Event, EventEmitter, h, Host, Listen, Prop, Watch } from '@stencil/core';

import { debounce, getNextElement, isHTMLElement, isNil, TDebounce, validatePropValue } from '../../shared/utils';
import { TAB_SIZE, TTabSize } from '../tab/bq-tab.types';
import {
TAB_ORIENTATION,
TAB_PLACEMENT,
TAB_SIZE,
TTabOrientation,
TTabPlacement,
TTabSize,
} from '../tab/bq-tab.types';

/**
* @part base - The HTML div wrapper inside the shadow DOM.
Expand Down Expand Up @@ -36,6 +43,12 @@ export class BqTabGroup {
/** The size of the tab */
@Prop({ reflect: true }) size: TTabSize = 'medium';

/** The direction that tab should be render */
@Prop({ reflect: true }) orientation?: TTabOrientation = 'horizontal';

/** The placement that tab should be render */
@Prop({ reflect: true }) placement?: TTabPlacement = 'start';

/** A number representing the delay value applied to bqChange event handler */
@Prop({ reflect: true, mutable: true }) debounceTime = 0;

Expand All @@ -62,11 +75,17 @@ export class BqTabGroup {

@Watch('size')
@Watch('value')
@Watch('orientation')
@Watch('placement')
checkPropValues() {
validatePropValue(TAB_SIZE, 'medium', this.el, 'size');
validatePropValue(TAB_ORIENTATION, 'horizontal', this.el, 'orientation');
validatePropValue(TAB_PLACEMENT, 'start', this.el, 'placement');

this.bqTabElements.forEach((bqTabElement) => {
bqTabElement.size = this.size;
bqTabElement.orientation = this.orientation;
bqTabElement.placement = this.placement;
bqTabElement.active = !isNil(this.value) ? bqTabElement.tabId === this.value : false;
});
}
Expand Down Expand Up @@ -114,19 +133,22 @@ export class BqTabGroup {
async onBqKeyDown(event: CustomEvent<KeyboardEvent>) {
const { target } = event;

// NOTE: ensures the target is an HTML element with the tag name 'bq-tab'
if (!isHTMLElement(target, 'bq-tab')) return;

switch (event.detail.key) {
case 'ArrowRight': {
await this.focusTabSibling(target, 'forward');
break;
}
case 'ArrowLeft': {
await this.focusTabSibling(target, 'backward');
break;
}
default:
}
const keyActions: { [key: string]: 'forward' | 'backward' } = {
ArrowDown: 'forward',
ArrowRight: 'forward',
ArrowUp: 'backward',
ArrowLeft: 'backward',
};

// NOTE: gets the direction based on key pressed
const direction = keyActions[event.detail.key];

if (!direction) return;

await this.focusTabSibling(target, direction);
}

@Listen('bqBlur', { capture: true, passive: true })
Expand Down Expand Up @@ -209,11 +231,26 @@ export class BqTabGroup {

render() {
return (
<div class={{ 'bq-tab-group flex w-full': true, 'no-divider': this.disableDivider }} part="base">
<div class="bq-tab-group--container flex overflow-x-auto" role="tablist" part="tabs">
<slot />
<Host class={{ 'inline-block': this.orientation === 'vertical' }}>
<div
class={{
[`bq-tab-group bq-tab-group--${this.orientation}-${this.placement} flex is-full`]: true,
'no-divider': this.disableDivider,
}}
part="base"
>
<div
class={{
'bq-tab-group--container flex overflow-x-auto': true,
'flex-col': this.orientation !== 'horizontal',
}}
role="tablist"
part="tabs"
>
<slot />
</div>
</div>
</div>
</Host>
);
}
}
14 changes: 8 additions & 6 deletions packages/beeq/src/components/tab-group/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

## Properties

| Property | Attribute | Description | Type | Default |
| ---------------- | ----------------- | ----------------------------------------------------------------------- | -------------------------------- | ----------- |
| `debounceTime` | `debounce-time` | A number representing the delay value applied to bqChange event handler | `number` | `0` |
| `disableDivider` | `disable-divider` | If true, the underline divider below the tabs won't be shown | `boolean` | `false` |
| `size` | `size` | The size of the tab | `"large" \| "medium" \| "small"` | `'medium'` |
| `value` | `value` | A string representing the id of the selected tab. | `string` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ---------------- | ----------------- | ----------------------------------------------------------------------- | -------------------------------- | -------------- |
| `debounceTime` | `debounce-time` | A number representing the delay value applied to bqChange event handler | `number` | `0` |
| `disableDivider` | `disable-divider` | If true, the underline divider below the tabs won't be shown | `boolean` | `false` |
| `orientation` | `orientation` | The direction that tab should be render | `"horizontal" \| "vertical"` | `'horizontal'` |
| `placement` | `placement` | The placement that tab should be render | `"end" \| "start"` | `'start'` |
| `size` | `size` | The size of the tab | `"large" \| "medium" \| "small"` | `'medium'` |
| `value` | `value` | A string representing the id of the selected tab. | `string` | `undefined` |


## Events
Expand Down
25 changes: 24 additions & 1 deletion packages/beeq/src/components/tab-group/scss/bq-tab-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,30 @@
@apply relative;

&::after {
@apply absolute bottom-0 flex w-full border-0 border-b-s border-solid border-bg-tertiary content-empty;
@apply absolute flex border-0 border-solid border-bg-tertiary content-empty;
}

&--horizontal-start,
&--horizontal-end {
&::after {
@apply is-full inset-be-0 [border-block-end-width:--bq-stroke-s];
}
}

&--horizontal-start {
@apply justify-start;
}

&--horizontal-end {
@apply justify-end;
}

&--vertical-start::after {
@apply end-0 bs-full is-0 [border-block-end-width:0px] [border-inline-end-width:--bq-stroke-s];
}

&--vertical-end::after {
@apply start-0 bs-full is-0 [border-block-end-width:0px] [border-inline-start-width:--bq-stroke-s];
}
}

Expand Down
Loading