Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5f95f9c
feat(breadcrumb): add breadcrumb
Alex-Oxthorn May 4, 2026
44b78a6
chore: add changeset, fix focus indicator and check for pattern consi…
Alex-Oxthorn May 5, 2026
4440327
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn May 5, 2026
104d9ec
chore(breadcrumb): make menu icon primary color fix _ in import
Alex-Oxthorn May 5, 2026
4a6538a
chore: remove unused mixin
Alex-Oxthorn May 5, 2026
070ac7f
chore: test fixes
Alex-Oxthorn May 5, 2026
8b8c088
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn May 6, 2026
782b527
chore: remove delegate
Alex-Oxthorn May 7, 2026
7ff7a96
chore: remove interfaces
Alex-Oxthorn May 7, 2026
27a0dc0
chore: rename breadcrumb to breadcrumbs
Alex-Oxthorn May 7, 2026
03ba9d4
chore: rename folders and files
Alex-Oxthorn May 7, 2026
fdad7fe
chore: update shape and outward-offset in crumb styles
Alex-Oxthorn May 7, 2026
abf7293
fix: update query decorator to make list element non-nullable
Alex-Oxthorn May 7, 2026
ed1d53f
fix: more renaming
Alex-Oxthorn May 7, 2026
6144b0d
chore: update separator property name to be more clear about what it …
Alex-Oxthorn May 11, 2026
36d2c5c
chore: add home button tool tip and the ability to set the value. Tes…
Alex-Oxthorn May 11, 2026
a71571d
chore: add aria-label properties for home button and sibling routes i…
Alex-Oxthorn May 11, 2026
eb2445e
chore: rename crumb to breadcrumbs-item
Alex-Oxthorn May 11, 2026
6507979
chore: remove BEM naming for all but top level elements
Alex-Oxthorn May 11, 2026
e085341
chore: refactor updated method to use await this.updateCompete instea…
Alex-Oxthorn May 11, 2026
48687ca
chore: update missed class rename
Alex-Oxthorn May 11, 2026
4448abe
feat: add slot for declarative items
Alex-Oxthorn May 11, 2026
89ac7ce
chore: replace hard coded px values with tokens
Alex-Oxthorn May 11, 2026
dc2f739
chore: remove constant files, declare inline
Alex-Oxthorn May 11, 2026
7d64bac
fix: renamed icon-theme and separator-theme to icon-color and separat…
Alex-Oxthorn May 11, 2026
44373f4
fix: remove overly cautious if checks
Alex-Oxthorn May 11, 2026
1db7a39
fix: add ability to slot the separator icon
Alex-Oxthorn May 11, 2026
e3652ab
fix: simplify event dispatching for breadcrumb selection
Alex-Oxthorn May 12, 2026
4bcce54
fix: update breadcrumb styles and improve accessibility attributes
Alex-Oxthorn May 12, 2026
0d6d1c6
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn May 12, 2026
b182be1
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn May 13, 2026
07af1f0
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn May 19, 2026
6e6af98
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn Jun 8, 2026
814b856
Merge branch 'main' into feat/add-breadcrumb
Alex-Oxthorn Jun 12, 2026
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
5 changes: 5 additions & 0 deletions .changeset/many-buttons-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tylertech/forge': minor
---

feat(breadcrumb): added breadcrumb component
36 changes: 36 additions & 0 deletions packages/forge/src/dev/pages/breadcrumbs/breadcrumbs.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<section>
<h3 class="forge-typography--heading2">Default</h3>
<div>
<forge-breadcrumbs id="demo-breadcrumbs"></forge-breadcrumbs>
</div>
</section>

<section>
<h3 class="forge-typography--heading2">Constrained width (auto-collapse)</h3>
<div style="width: 300px; border: 1px dashed var(--forge-theme-outline); padding: 8px; resize: horizontal; overflow: hidden;">
<forge-breadcrumbs id="demo-breadcrumbs-constrained"></forge-breadcrumbs>
</div>
</section>

<section>
<h3 class="forge-typography--heading2">Breadcrumb Items (slotted)</h3>
<div>
<forge-breadcrumbs id="demo-breadcrumbs-slot">
<forge-breadcrumbs-item id="slot-item-0"></forge-breadcrumbs-item>
<forge-breadcrumbs-item id="slot-item-1"></forge-breadcrumbs-item>
<forge-breadcrumbs-item id="slot-item-2"></forge-breadcrumbs-item>
<forge-breadcrumbs-item id="slot-item-3"></forge-breadcrumbs-item>
</forge-breadcrumbs>
</div>
</section>

<section>
<h3 class="forge-typography--heading2">Custom separator icon (slotted)</h3>
<div>
<forge-breadcrumbs id="demo-breadcrumbs-custom-sep">
<forge-icon slot="separator-icon" name="chevron_right"></forge-icon>
</forge-breadcrumbs>
</div>
</section>

<script type="module" src="breadcrumbs.ts"></script>
27 changes: 27 additions & 0 deletions packages/forge/src/dev/pages/breadcrumbs/breadcrumbs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<%-
include('./src/partials/page.ejs', {
page: {
title: 'Breadcrumbs',
includePath: './pages/breadcrumbs/breadcrumbs.ejs',
options: [
{ type: 'switch', label: 'Show home', id: 'opt-show-home' },
{ type: 'switch', label: 'Show icons', id: 'opt-icons' },
{ type: 'switch', label: 'Show secondary text', id: 'opt-secondary' },
{ type: 'switch', label: 'Show sibling routes', id: 'opt-siblings' },
{
type: 'select',
label: 'Separator icon name',
id: 'opt-separator',
defaultValue: 'slash_forward',
options: [
{ label: 'Slash forward', value: 'slash_forward' },
{ label: 'Chevron right', value: 'chevron_right' },
{ label: 'Arrow right', value: 'arrow_right' }
]
},
{ type: 'button', id: 'btn-add-crumb', label: 'Add crumb' },
{ type: 'button', id: 'btn-remove-crumb', label: 'Remove crumb' }
]
}
})
%>
7 changes: 7 additions & 0 deletions packages/forge/src/dev/pages/breadcrumbs/breadcrumbs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
section {
margin-block-end: 24px;

h3 {
margin-block-end: 8px;
}
}
125 changes: 125 additions & 0 deletions packages/forge/src/dev/pages/breadcrumbs/breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import '$src/shared';
import '@tylertech/forge/breadcrumbs';
import type { BreadcrumbsComponent, BreadcrumbsItemComponent, ICrumbConfiguration } from '@tylertech/forge/breadcrumbs';
import type { SwitchComponent } from '@tylertech/forge/switch';
import type { SelectComponent } from '@tylertech/forge/select';
import { IconRegistry } from '@tylertech/forge/icon';
import { tylIconFolder, tylIconDescription, tylIconSettings, tylIconHome, tylIconChevronRight, tylIconArrowRight } from '@tylertech/tyler-icons';

import './breadcrumbs.scss';
import { ButtonComponent } from '@tylertech/forge/button';

IconRegistry.define([tylIconFolder, tylIconDescription, tylIconSettings, tylIconHome, tylIconChevronRight, tylIconArrowRight]);

let showIcons = false;
let showSecondary = false;
let showSiblings = false;

const baseCrumbs: ICrumbConfiguration[] = [
{ label: 'Section 1', path: '/' },
{ label: 'Section 2', path: '/projects' },
{ label: 'Section 3', path: '/projects/forge' },
{ label: 'Current' }
];

const buildCrumbs = (): ICrumbConfiguration[] =>
baseCrumbs.map((c, i) => ({
...c,
icon: showIcons && i < baseCrumbs.length - 1 ? ['home', 'folder', 'description', 'settings'][i] : undefined,
secondary: showSecondary && i < baseCrumbs.length - 1 ? `Level ${i + 1}` : undefined,
siblingRoutes:
showSiblings && i > 0 && i < baseCrumbs.length - 1
? [
{ label: `${c.label} Alt A`, path: `${c.path}-a` },
{ label: `${c.label} Alt B`, path: `${c.path}-b` }
]
: undefined
}));

const breadcrumbs = document.querySelectorAll<BreadcrumbsComponent & HTMLElement>('#demo-breadcrumbs, #demo-breadcrumbs-constrained');

const slotCrumbs: ICrumbConfiguration[] = [
{ label: 'Section 1', path: '/' },
{ label: 'Section 2', path: '/projects' },
{ label: 'Section 3', path: '/projects/forge' },
{ label: 'Current' }
];

const slotItems = Array.from(document.querySelectorAll<BreadcrumbsItemComponent>('#demo-breadcrumbs-slot forge-breadcrumbs-item'));
slotItems.forEach((item, i) => {
item.crumb = slotCrumbs[i];
});

const customSepBreadcrumbs = document.getElementById('demo-breadcrumbs-custom-sep') as BreadcrumbsComponent & HTMLElement;
customSepBreadcrumbs.crumbs = baseCrumbs;

const updateAll = (): void => {
const crumbs = buildCrumbs();
breadcrumbs.forEach(bc => {
bc.crumbs = crumbs;
});
};

updateAll();

const showHomeSwitch = document.getElementById('opt-show-home') as SwitchComponent;
const separatorSelect = document.getElementById('opt-separator') as SelectComponent;
const iconsSwitch = document.getElementById('opt-icons') as SwitchComponent;
const secondarySwitch = document.getElementById('opt-secondary') as SwitchComponent;
const siblingsSwitch = document.getElementById('opt-siblings') as SwitchComponent;
const addBtn = document.getElementById('btn-add-crumb') as ButtonComponent;
const removeBtn = document.getElementById('btn-remove-crumb') as ButtonComponent;

iconsSwitch.checked = showIcons;
secondarySwitch.checked = showSecondary;
siblingsSwitch.checked = showSiblings;

showHomeSwitch.addEventListener('forge-switch-change', () => {
breadcrumbs.forEach(bc => {
bc.showHome = showHomeSwitch.checked;
});
});

separatorSelect.addEventListener('change', () => {
breadcrumbs.forEach(bc => {
bc.separatorIconName = separatorSelect.value;
});
});

iconsSwitch.addEventListener('forge-switch-change', () => {
showIcons = iconsSwitch.checked;
updateAll();
});

secondarySwitch.addEventListener('forge-switch-change', () => {
showSecondary = secondarySwitch.checked;
updateAll();
});

siblingsSwitch.addEventListener('forge-switch-change', () => {
showSiblings = siblingsSwitch.checked;
updateAll();
});

addBtn.addEventListener('click', () => {
baseCrumbs.splice(baseCrumbs.length - 1, 0, {
label: `Section ${baseCrumbs.length}`,
path: `/section-${baseCrumbs.length}`
});
updateAll();
});

removeBtn.addEventListener('click', () => {
if (baseCrumbs.length > 1) {
baseCrumbs.splice(baseCrumbs.length - 2, 1);
updateAll();
}
});

window.addEventListener('forge-breadcrumbs-crumb-select', evt => {
console.log('Crumb selected:', (evt as CustomEvent).detail);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that forge-breadcrumbs-crumb-select is a standard Event, there is no detail property anymore...

Also, this may present an issue if the event target is the <forge-breadcrumbs> element rather than the <forge-breadcrumb-item> element since we wouldn't be able to access which breadcrumb item was selected. I assume this is why the index getter was added? I'm not sure how I feel about that approach...

We may want to keep this event as a CustomEvent and just include the crumb data in the detail? @samrichardsontylertech thoughts?

});

window.addEventListener('forge-breadcrumbs-home-click', () => {
console.log('Home clicked');
});
1 change: 1 addition & 0 deletions packages/forge/src/dev/src/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{ "label": "Badge", "path": "/pages/badge/badge.html", "tags": ["notification", "status"] },
{ "label": "Banner", "path": "/pages/banner/banner.html", "tags": ["notification", "message"] },
{ "label": "Bottom sheet", "path": "/pages/bottom-sheet/bottom-sheet.html" },
{ "label": "Breadcrumbs", "path": "/pages/breadcrumbs/breadcrumbs.html", "tags": ["navigation", "crumb", "path"] },
{ "label": "Button", "path": "/pages/button/button.html" },
{ "label": "Button area", "path": "/pages/button-area/button-area.html" },
{ "label": "Button toggle", "path": "/pages/button-toggle/button-toggle.html", "tags": ["form"] },
Expand Down
72 changes: 72 additions & 0 deletions packages/forge/src/lib/breadcrumbs/breadcrumbs-item/_core.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@use './token-utils' as *;
@use '../breadcrumbs/token-utils' as breadcrumb-tokens;
@use '../../icon' as icon;
@use '../../icon-button' as icon-button;
@use '../../core/styles/typography';

@forward './token-utils';

@mixin host {
display: flex;
align-items: flex-start;
white-space: nowrap;
}

@mixin base {
display: flex;
align-items: flex-start;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we're using align-items: flex-start here to account for secondary text while keeping the alignment of the separators? Might it be better to introduce a grid layout with two rows? This would also remove the need for setting padding-top on the separator which seems to cause alignment issues with different separator icons...

}

@mixin link {
color: #{token(text-color)};
cursor: pointer;
padding: var(--forge-spacing-xxsmall) var(--forge-spacing-xsmall);
border-radius: var(--forge-spacing-xxsmall);
font: inherit;
border: none;
background: none;
position: relative;
display: grid;
grid-template-columns: repeat(2, auto);
grid-template-rows: repeat(2, auto);
align-items: flex-start;
outline: none;
}

@mixin label-text {
@include typography.style(body2);
white-space: nowrap;
grid-column-start: 2;
grid-row-start: 1;
align-self: center;
}

@mixin secondary-text {
@include typography.style(label1);
color: #{token(secondary-text-color)};
grid-column-start: 2;
grid-row-start: 2;
text-align: left;
}

@mixin crumb-icon {
color: #{token(icon-color)};
margin-right: var(--forge-spacing-xsmall);
}

@mixin sibling-trigger {
@include icon-button.provide-theme(
(
size: var(--forge-spacing-xlarge)
)
);
}

@mixin active {
padding: var(--forge-spacing-xxsmall) var(--forge-spacing-xsmall);
}

@mixin separator {
color: #{breadcrumb-tokens.token(separator-color)};
padding-top: var(--forge-spacing-xxsmall);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to avoid this, see previous comment related to grid layout.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use '../../core/styles/tokens/breadcrumbs-item/tokens';
@use '../../core/styles/tokens/token-utils';

$_module: crumb;
$_tokens: tokens.$tokens;

@mixin provide-theme($theme) {
@include token-utils.provide-theme($_module, $_tokens, $theme);
}

@function token($name, $type: token) {
@return token-utils.token($_module, $_tokens, $name, $type);
}

@function declare($token) {
@return token-utils.declare($_module, $token);
}

@mixin override($token, $token-or-value, $type: token) {
@include token-utils.override($_module, $_tokens, $token, $token-or-value, $type);
}

@mixin tokens($includes: null, $excludes: null) {
@include token-utils.tokens($_module, $_tokens, $includes, $excludes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@use './core' as *;
@use '../../state-layer';
@use '../../focus-indicator';
@use '../../core/styles/shape';

:host {
@include host;
}

:host([hidden]) {
display: none;
}

.forge-breadcrumbs-item {
@include tokens;
@include base;
}

.link {
@include link;

forge-state-layer {
@include state-layer.provide-theme(
(
color: #{token(text-color)}
)
);
}
}

forge-focus-indicator {
@include focus-indicator.provide-theme(
Comment thread
Alex-Oxthorn marked this conversation as resolved.
(
color: #{token(text-color)},
shape: #{shape.variable(medium)},
outward-offset: 0px
)
);
}

.label-text {
@include label-text;
}

.secondary-text {
@include secondary-text;
}

.icon {
@include crumb-icon;
}

.sibling-trigger {
@include sibling-trigger;
}

.active {
@include active;
}

.separator {
@include separator;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the default separator icon font-size a little too large? Maybe we want 1.5rem or 1.25rem instead? Seemed a little strong compared to the mocks.

}

[hidden] {
display: none;
}
Loading
Loading