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
26 changes: 26 additions & 0 deletions examples/web-components/toolbar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
yarn.lock
.yarn

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
6 changes: 6 additions & 0 deletions examples/web-components/toolbar/.sassrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"includePaths": [
"node_modules",
"../../node_modules"
]
}
4 changes: 4 additions & 0 deletions examples/web-components/toolbar/gallery.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Toolbar",
"template": "node"
}
24 changes: 24 additions & 0 deletions examples/web-components/toolbar/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!--
@license

Copyright IBM Corp. 2025

This source code is licensed under the Apache-2.0 license found in the
LICENSE file in the root directory of this source tree.
-->

<html>
<head>
<title>carbon-labs-web-components toolbar pattern examples</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="src/toolbar.scss" />
<script type="module" src="src/toolbar.ts"></script>
<script type="module" src="src/toolbar-vertical.ts"></script>
</head>
<body>
<div class="example-container">
<clabs-toolbar orientation="horizontal"></clabs-toolbar>
<clabs-toolbar-vertical orientation="vertical"></clabs-toolbar-vertical>
</div>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/web-components/toolbar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "toolbar-example",
"version": "0.1.0",
"private": true,
"description": "Sample project for getting started with the Web Components from Carbon for IBM Products.",
"license": "Apache-2",
"main": "index.html",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"clean": "rimraf node_modules dist .cache"
},
"dependencies": {
"@carbon/ibm-products-styles": "^2.62.0-rc.0",
"@carbon/styles": "1.79.0",
"@carbon/web-components": "2.27.1",
"lit": "^3.2.1",
"sass": "^1.64.1"
},
"devDependencies": {
"rimraf": "^3.0.2",
"typescript": "^5.5.3",
"vite": "5.4.18",
"vite-plugin-lit-css": "^2.1.0"
}
}
253 changes: 253 additions & 0 deletions examples/web-components/toolbar/src/toolbar-vertical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/**
* @license
*
* Copyright IBM Corp. 2025, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { LitElement, html } from 'lit';
import { property, customElement, query } from 'lit/decorators.js';
// Carbon components
import '@carbon/web-components/es/components/stack/index.js';
import '@carbon/web-components/es/components/icon-button/index.js';
import '@carbon/web-components/es/components/dropdown/index.js';
import '@carbon/web-components/es/components/overflow-menu/index.js';
// Carbon icons
import ZoomIn from '@carbon/web-components/es/icons/zoom--in/16.js';
import ZoomOut from '@carbon/web-components/es/icons/zoom--out/16.js';
import RulerAlt from '@carbon/web-components/es/icons/ruler--alt/16.js';
import Pin from '@carbon/web-components/es/icons/pin/16.js';
import ColorPalette from '@carbon/web-components/es/icons/color-palette/16.js';
import Draggable from '@carbon/web-components/es/icons/draggable/16.js';
import TextCreation from '@carbon/web-components/es/icons/text--creation/16.js';
import OpenPanelLeft from '@carbon/web-components/es/icons/open-panel--left/16.js';
import OpenPanelRight from '@carbon/web-components/es/icons/open-panel--right/16.js';
import Move from '@carbon/web-components/es/icons/move/16.js';
import Rotate from '@carbon/web-components/es/icons/rotate/16.js';

import styles from './toolbar.scss?lit';

/**
* ToolbarVertical pattern example.
*
* @element clabs-toolbar-vertical
*
*/
@customElement('clabs-toolbar-vertical')
class ToolbarVertical extends LitElement {
@property({ type: String, reflect: true }) orientation = 'horizontal';
@query('cds-stack.toolbar') toolbarEl!: HTMLElement;

/**
* Lifecycle callback that is called after the element's DOM has been updated the first time.
*/
async firstUpdated() {
this.setAttribute('role', 'toolbar');
this.addEventListener('keydown', this._handleKeydown);
this._initializeFocusableElements();

await this.updateComplete;
// Remove dropdown border
const dropdown = this.renderRoot.querySelector('cds-dropdown');
const listBox = dropdown?.shadowRoot?.querySelector('.cds--list-box');
if (listBox) {
(listBox as HTMLElement).style.border = 'none';
}
}

/**
* Lifecycle callback that is called when the element is disconnected from the document's DOM.
*/
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('keydown', this._handleKeydown);
}

/**
* Initializes the focusable elements in the toolbar and sets their tabindex.
*/
private _initializeFocusableElements() {
const focusables = this._getFocusableElements();
focusables.forEach((el, i) =>
el.setAttribute('tabindex', i === 0 ? '0' : '-1')
);
}

/**
* Handles keyboard navigation for toolbar focus management.
* @param {KeyboardEvent} event The keyboard event.
*/
private _handleKeydown(event: KeyboardEvent) {
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp'];
if (!keys.includes(event.key)) {
return;
}

const elements = this._getFocusableElements();

const current = elements.findIndex(
(btn) => btn.getAttribute('tabindex') === '0'
);

if (current === -1) {
return;
}

event.preventDefault();
const horizontal = this.orientation === 'horizontal';

let next = current;
if (event.key === 'ArrowRight' && horizontal) {
next = (current + 1) % elements.length;
}
if (event.key === 'ArrowLeft' && horizontal) {
next = (current - 1 + elements.length) % elements.length;
}
if (event.key === 'ArrowDown' && !horizontal) {
next = (current + 1) % elements.length;
}
if (event.key === 'ArrowUp' && !horizontal) {
next = (current - 1 + elements.length) % elements.length;
}

this._updateTabIndexes(elements, next);
elements[next].focus();
}

/**
*
* @param {HTMLElement[]} elements - The list of focusable elements in the toolbar.
* @param {number} activeIndex - The index of the element to set as active (tabindex=0).
*/
private _updateTabIndexes(elements: HTMLElement[], activeIndex: number) {
elements.forEach((el, i) =>
el.setAttribute('tabindex', i === activeIndex ? '0' : '-1')
);
}

/**
* Gets all focusable elements within the toolbar.
* @returns {HTMLElement[]} An array of focusable elements.
*/
private _getFocusableElements(): HTMLElement[] {
return Array.from(
this.renderRoot.querySelectorAll<HTMLElement>(
'cds-icon-button, cds-dropdown, cds-overflow-menu'
)
);
}

/**
* Renders the toolbar template.
*/
render() {
return html`
<cds-stack class="toolbar" orientation=${this.orientation}>
<cds-stack class="toolbar-group" orientation=${this.orientation}>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${Draggable({ slot: 'icon' })}
<span slot="tooltip-content">Drag</span>
</cds-icon-button>
</cds-stack class="toolbar-group">
<cds-stack class="toolbar-group" orientation=${this.orientation}>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${RulerAlt({ slot: 'icon' })}
<span slot="tooltip-content">Ruler</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${Pin({ slot: 'icon' })}
<span slot="tooltip-content">Pin</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
caret
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${ColorPalette({ slot: 'icon' })}
<span slot="tooltip-content">Color palette</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${TextCreation({ slot: 'icon' })}
<span slot="tooltip-content">Text creation</span>
</cds-icon-button>
</cds-stack class="toolbar-group">
<cds-stack class="toolbar-group" orientation=${this.orientation}>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${OpenPanelLeft({ slot: 'icon' })}
<span slot="tooltip-content">Open panel left</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${OpenPanelRight({ slot: 'icon' })}
<span slot="tooltip-content">Open panel right</span>
</cds-icon-button>
</cds-stack class="toolbar-group">
<cds-stack class="toolbar-group" orientation=${this.orientation}>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${Move({ slot: 'icon' })}
<span slot="tooltip-content">Move</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${Rotate({ slot: 'icon' })}
<span slot="tooltip-content">Rotate</span>
</cds-icon-button>
</cds-stack class="toolbar-group">
<cds-stack class="toolbar-group" orientation=${this.orientation}>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${ZoomIn({ slot: 'icon' })}
<span slot="tooltip-content">Zoom in</span>
</cds-icon-button>
<cds-icon-button
kind="ghost"
enter-delay-ms="100"
leave-delay-ms="100"
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
${ZoomOut({ slot: 'icon' })}
<span slot="tooltip-content">Zoom out</span>
</cds-icon-button>
</cds-stack class="toolbar-group">
</cds-stack class="toolbar">
`;
}

static styles = styles;
}
export default ToolbarVertical;
Loading