Skip to content

Commit a004d96

Browse files
chore: add toolbar example as a pattern (story only) (#560)
* chore: add toolbar example as a pattern (story only) * fix: lint, format, license * refactor: scss styles and add caret class name * chore: add docs for the pattern building of toolbar * chore: yarn format * fix: lint * Update packages/web-components/src/patterns/toolbar/__stories__/toolbar.mdx Co-authored-by: elysia <[email protected]> * refactor: everything about how we deliver patterns consistently * fix: lint and license * fix: lint * chore: remove irrelevant docs information * chore: add basic html * chore: update docs --------- Co-authored-by: elysia <[email protected]>
1 parent 54496f4 commit a004d96

File tree

15 files changed

+1030
-1
lines changed

15 files changed

+1030
-1
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
yarn.lock
15+
.yarn
16+
17+
# Editor directories and files
18+
.vscode/*
19+
!.vscode/extensions.json
20+
.idea
21+
.DS_Store
22+
*.suo
23+
*.ntvs*
24+
*.njsproj
25+
*.sln
26+
*.sw?
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"includePaths": [
3+
"node_modules",
4+
"../../node_modules"
5+
]
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"label": "Toolbar",
3+
"template": "node"
4+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!--
2+
@license
3+
4+
Copyright IBM Corp. 2025
5+
6+
This source code is licensed under the Apache-2.0 license found in the
7+
LICENSE file in the root directory of this source tree.
8+
-->
9+
10+
<html>
11+
<head>
12+
<title>carbon-labs-web-components toolbar pattern examples</title>
13+
<meta charset="UTF-8" />
14+
<link rel="stylesheet" href="src/toolbar.scss" />
15+
<script type="module" src="src/toolbar.ts"></script>
16+
<script type="module" src="src/toolbar-vertical.ts"></script>
17+
</head>
18+
<body>
19+
<div class="example-container">
20+
<clabs-toolbar orientation="horizontal"></clabs-toolbar>
21+
<clabs-toolbar-vertical orientation="vertical"></clabs-toolbar-vertical>
22+
</div>
23+
</body>
24+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "toolbar-example",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "Sample project for getting started with the Web Components from Carbon for IBM Products.",
6+
"license": "Apache-2",
7+
"main": "index.html",
8+
"type": "module",
9+
"scripts": {
10+
"dev": "vite",
11+
"build": "vite build",
12+
"clean": "rimraf node_modules dist .cache"
13+
},
14+
"dependencies": {
15+
"@carbon/ibm-products-styles": "^2.62.0-rc.0",
16+
"@carbon/styles": "1.79.0",
17+
"@carbon/web-components": "2.27.1",
18+
"lit": "^3.2.1",
19+
"sass": "^1.64.1"
20+
},
21+
"devDependencies": {
22+
"rimraf": "^3.0.2",
23+
"typescript": "^5.5.3",
24+
"vite": "5.4.18",
25+
"vite-plugin-lit-css": "^2.1.0"
26+
}
27+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/**
2+
* @license
3+
*
4+
* Copyright IBM Corp. 2025, 2025
5+
*
6+
* This source code is licensed under the Apache-2.0 license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*/
9+
10+
import { LitElement, html } from 'lit';
11+
import { property, customElement, query } from 'lit/decorators.js';
12+
// Carbon components
13+
import '@carbon/web-components/es/components/stack/index.js';
14+
import '@carbon/web-components/es/components/icon-button/index.js';
15+
import '@carbon/web-components/es/components/dropdown/index.js';
16+
import '@carbon/web-components/es/components/overflow-menu/index.js';
17+
// Carbon icons
18+
import ZoomIn from '@carbon/web-components/es/icons/zoom--in/16.js';
19+
import ZoomOut from '@carbon/web-components/es/icons/zoom--out/16.js';
20+
import RulerAlt from '@carbon/web-components/es/icons/ruler--alt/16.js';
21+
import Pin from '@carbon/web-components/es/icons/pin/16.js';
22+
import ColorPalette from '@carbon/web-components/es/icons/color-palette/16.js';
23+
import Draggable from '@carbon/web-components/es/icons/draggable/16.js';
24+
import TextCreation from '@carbon/web-components/es/icons/text--creation/16.js';
25+
import OpenPanelLeft from '@carbon/web-components/es/icons/open-panel--left/16.js';
26+
import OpenPanelRight from '@carbon/web-components/es/icons/open-panel--right/16.js';
27+
import Move from '@carbon/web-components/es/icons/move/16.js';
28+
import Rotate from '@carbon/web-components/es/icons/rotate/16.js';
29+
30+
import styles from './toolbar.scss?lit';
31+
32+
/**
33+
* ToolbarVertical pattern example.
34+
*
35+
* @element clabs-toolbar-vertical
36+
*
37+
*/
38+
@customElement('clabs-toolbar-vertical')
39+
class ToolbarVertical extends LitElement {
40+
@property({ type: String, reflect: true }) orientation = 'horizontal';
41+
@query('cds-stack.toolbar') toolbarEl!: HTMLElement;
42+
43+
/**
44+
* Lifecycle callback that is called after the element's DOM has been updated the first time.
45+
*/
46+
async firstUpdated() {
47+
this.setAttribute('role', 'toolbar');
48+
this.addEventListener('keydown', this._handleKeydown);
49+
this._initializeFocusableElements();
50+
51+
await this.updateComplete;
52+
// Remove dropdown border
53+
const dropdown = this.renderRoot.querySelector('cds-dropdown');
54+
const listBox = dropdown?.shadowRoot?.querySelector('.cds--list-box');
55+
if (listBox) {
56+
(listBox as HTMLElement).style.border = 'none';
57+
}
58+
}
59+
60+
/**
61+
* Lifecycle callback that is called when the element is disconnected from the document's DOM.
62+
*/
63+
disconnectedCallback() {
64+
super.disconnectedCallback();
65+
this.removeEventListener('keydown', this._handleKeydown);
66+
}
67+
68+
/**
69+
* Initializes the focusable elements in the toolbar and sets their tabindex.
70+
*/
71+
private _initializeFocusableElements() {
72+
const focusables = this._getFocusableElements();
73+
focusables.forEach((el, i) =>
74+
el.setAttribute('tabindex', i === 0 ? '0' : '-1')
75+
);
76+
}
77+
78+
/**
79+
* Handles keyboard navigation for toolbar focus management.
80+
* @param {KeyboardEvent} event The keyboard event.
81+
*/
82+
private _handleKeydown(event: KeyboardEvent) {
83+
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp'];
84+
if (!keys.includes(event.key)) {
85+
return;
86+
}
87+
88+
const elements = this._getFocusableElements();
89+
90+
const current = elements.findIndex(
91+
(btn) => btn.getAttribute('tabindex') === '0'
92+
);
93+
94+
if (current === -1) {
95+
return;
96+
}
97+
98+
event.preventDefault();
99+
const horizontal = this.orientation === 'horizontal';
100+
101+
let next = current;
102+
if (event.key === 'ArrowRight' && horizontal) {
103+
next = (current + 1) % elements.length;
104+
}
105+
if (event.key === 'ArrowLeft' && horizontal) {
106+
next = (current - 1 + elements.length) % elements.length;
107+
}
108+
if (event.key === 'ArrowDown' && !horizontal) {
109+
next = (current + 1) % elements.length;
110+
}
111+
if (event.key === 'ArrowUp' && !horizontal) {
112+
next = (current - 1 + elements.length) % elements.length;
113+
}
114+
115+
this._updateTabIndexes(elements, next);
116+
elements[next].focus();
117+
}
118+
119+
/**
120+
*
121+
* @param {HTMLElement[]} elements - The list of focusable elements in the toolbar.
122+
* @param {number} activeIndex - The index of the element to set as active (tabindex=0).
123+
*/
124+
private _updateTabIndexes(elements: HTMLElement[], activeIndex: number) {
125+
elements.forEach((el, i) =>
126+
el.setAttribute('tabindex', i === activeIndex ? '0' : '-1')
127+
);
128+
}
129+
130+
/**
131+
* Gets all focusable elements within the toolbar.
132+
* @returns {HTMLElement[]} An array of focusable elements.
133+
*/
134+
private _getFocusableElements(): HTMLElement[] {
135+
return Array.from(
136+
this.renderRoot.querySelectorAll<HTMLElement>(
137+
'cds-icon-button, cds-dropdown, cds-overflow-menu'
138+
)
139+
);
140+
}
141+
142+
/**
143+
* Renders the toolbar template.
144+
*/
145+
render() {
146+
return html`
147+
<cds-stack class="toolbar" orientation=${this.orientation}>
148+
<cds-stack class="toolbar-group" orientation=${this.orientation}>
149+
<cds-icon-button
150+
kind="ghost"
151+
enter-delay-ms="100"
152+
leave-delay-ms="100"
153+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
154+
${Draggable({ slot: 'icon' })}
155+
<span slot="tooltip-content">Drag</span>
156+
</cds-icon-button>
157+
</cds-stack class="toolbar-group">
158+
<cds-stack class="toolbar-group" orientation=${this.orientation}>
159+
<cds-icon-button
160+
kind="ghost"
161+
enter-delay-ms="100"
162+
leave-delay-ms="100"
163+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
164+
${RulerAlt({ slot: 'icon' })}
165+
<span slot="tooltip-content">Ruler</span>
166+
</cds-icon-button>
167+
<cds-icon-button
168+
kind="ghost"
169+
enter-delay-ms="100"
170+
leave-delay-ms="100"
171+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
172+
${Pin({ slot: 'icon' })}
173+
<span slot="tooltip-content">Pin</span>
174+
</cds-icon-button>
175+
<cds-icon-button
176+
kind="ghost"
177+
enter-delay-ms="100"
178+
leave-delay-ms="100"
179+
caret
180+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
181+
${ColorPalette({ slot: 'icon' })}
182+
<span slot="tooltip-content">Color palette</span>
183+
</cds-icon-button>
184+
<cds-icon-button
185+
kind="ghost"
186+
enter-delay-ms="100"
187+
leave-delay-ms="100"
188+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
189+
${TextCreation({ slot: 'icon' })}
190+
<span slot="tooltip-content">Text creation</span>
191+
</cds-icon-button>
192+
</cds-stack class="toolbar-group">
193+
<cds-stack class="toolbar-group" orientation=${this.orientation}>
194+
<cds-icon-button
195+
kind="ghost"
196+
enter-delay-ms="100"
197+
leave-delay-ms="100"
198+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
199+
${OpenPanelLeft({ slot: 'icon' })}
200+
<span slot="tooltip-content">Open panel left</span>
201+
</cds-icon-button>
202+
<cds-icon-button
203+
kind="ghost"
204+
enter-delay-ms="100"
205+
leave-delay-ms="100"
206+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
207+
${OpenPanelRight({ slot: 'icon' })}
208+
<span slot="tooltip-content">Open panel right</span>
209+
</cds-icon-button>
210+
</cds-stack class="toolbar-group">
211+
<cds-stack class="toolbar-group" orientation=${this.orientation}>
212+
<cds-icon-button
213+
kind="ghost"
214+
enter-delay-ms="100"
215+
leave-delay-ms="100"
216+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
217+
${Move({ slot: 'icon' })}
218+
<span slot="tooltip-content">Move</span>
219+
</cds-icon-button>
220+
<cds-icon-button
221+
kind="ghost"
222+
enter-delay-ms="100"
223+
leave-delay-ms="100"
224+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
225+
${Rotate({ slot: 'icon' })}
226+
<span slot="tooltip-content">Rotate</span>
227+
</cds-icon-button>
228+
</cds-stack class="toolbar-group">
229+
<cds-stack class="toolbar-group" orientation=${this.orientation}>
230+
<cds-icon-button
231+
kind="ghost"
232+
enter-delay-ms="100"
233+
leave-delay-ms="100"
234+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
235+
${ZoomIn({ slot: 'icon' })}
236+
<span slot="tooltip-content">Zoom in</span>
237+
</cds-icon-button>
238+
<cds-icon-button
239+
kind="ghost"
240+
enter-delay-ms="100"
241+
leave-delay-ms="100"
242+
align=${this.orientation === 'vertical' ? 'right' : 'top'}>
243+
${ZoomOut({ slot: 'icon' })}
244+
<span slot="tooltip-content">Zoom out</span>
245+
</cds-icon-button>
246+
</cds-stack class="toolbar-group">
247+
</cds-stack class="toolbar">
248+
`;
249+
}
250+
251+
static styles = styles;
252+
}
253+
export default ToolbarVertical;

0 commit comments

Comments
 (0)