Skip to content

Commit

Permalink
fix(canary-web-components): update tree view to improve FOUC
Browse files Browse the repository at this point in the history
Update tree view to prevent FOUC - set tree items visibility to hidden until loaded when inherited
props from tree view used. Removed timeouts on truncation - tree view now truncates when ready
rather than waiting 0.1 seconds to help reduce flashing.
  • Loading branch information
GCHQ-Developer-847 committed Feb 7, 2025
1 parent 26a6b50 commit 6c26721
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,25 +141,21 @@ export class TreeItem {

componentWillLoad(): void {
removeDisabledFalse(this.disabled, this.el);
}

componentDidLoad(): void {
this.childTreeItems = Array.from((this.el as HTMLElement).children).filter(
(child) => child.tagName === this.treeItemTag
) as HTMLIcTreeItemElement[];

if (this.childTreeItems.length > 0) {
this.isParent = true;
}
}

componentDidLoad(): void {
this.setTreeItemPadding();

this.updateAriaLabel();

setTimeout(() => {
this.truncateTreeItem && this.truncateTreeItemLabel(this.el);
}, 100);

!isSlotUsed(this.el, "label") &&
onComponentRequiredPropUndefined(
[{ prop: this.label, propName: "label" }],
Expand All @@ -175,6 +171,9 @@ export class TreeItem {
}

componentDidUpdate(): void {
// Runs on second render - when truncateTreeItem prop has now been applied in the DOM
this.truncateTreeItem && this.truncateTreeItemLabel(this.el);

if (this.hasParentExpanded) {
this.childTreeItems.forEach((child: HTMLIcTreeItemElement) => {
child.truncateTreeItem && this.truncateTreeItemLabel(child);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ic-tree-item component should render 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-0" label="Item 1">
<ic-tree-item id="ic-tree-item-0" label="Item 1">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="tree-item-label">
Item 1
</ic-typography>
Expand All @@ -13,7 +13,7 @@ exports[`ic-tree-item component should render 1`] = `
`;

exports[`ic-tree-item component should render disabled 1`] = `
<ic-tree-item class="ic-tree-item-dark ic-tree-item-disabled" disabled="true" id="ic-tree-item-0" label="Item 1">
<ic-tree-item class="ic-tree-item-disabled" disabled="true" id="ic-tree-item-6" label="Item 1">
<mock:shadow-root>
<div aria-disabled="true" aria-live="polite" class="tree-item-content" tabindex="-1">
<ic-typography class="tree-item-label">
Expand All @@ -25,7 +25,7 @@ exports[`ic-tree-item component should render disabled 1`] = `
`;

exports[`ic-tree-item component should render disabled: disabled-removed 1`] = `
<ic-tree-item class="ic-tree-item-dark" id="ic-tree-item-0" label="Item 1">
<ic-tree-item id="ic-tree-item-6" label="Item 1">
<mock:shadow-root>
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="tree-item-label">
Expand All @@ -37,9 +37,9 @@ exports[`ic-tree-item component should render disabled: disabled-removed 1`] = `
`;

exports[`ic-tree-item component should render selected 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark ic-tree-item-selected" id="ic-tree-item-7" label="Item 1" selected="true">
<ic-tree-item class="ic-tree-item-selected" id="ic-tree-item-7" label="Item 1" selected="true">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="tree-item-label">
Item 1
</ic-typography>
Expand All @@ -49,9 +49,9 @@ exports[`ic-tree-item component should render selected 1`] = `
`;

exports[`ic-tree-item component should render with child tree items 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-2" label="Item 1">
<ic-tree-item id="ic-tree-item-2" label="Item 1">
<mock:shadow-root>
<div class="tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<span aria-hidden="true" class="arrow-dropdown">
svg
</span>
Expand All @@ -60,18 +60,18 @@ exports[`ic-tree-item component should render with child tree items 1`] = `
</ic-typography>
</div>
</mock:shadow-root>
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-3" label="Item 1.1">
<ic-tree-item id="ic-tree-item-3" label="Item 1.1">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="ic-tree-item-single tree-item-content" tabindex="0" style="padding-left: calc(var(--ic-space-xs) + 48px;">
<ic-typography class="tree-item-label">
Item 1.1
</ic-typography>
</div>
</mock:shadow-root>
</ic-tree-item>
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-4" label="Item 1.2">
<ic-tree-item id="ic-tree-item-4" label="Item 1.2">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="ic-tree-item-single tree-item-content" tabindex="0" style="padding-left: calc(var(--ic-space-xs) + 48px;">
<ic-typography class="tree-item-label">
Item 1.2
</ic-typography>
Expand All @@ -82,9 +82,9 @@ exports[`ic-tree-item component should render with child tree items 1`] = `
`;

exports[`ic-tree-item component should render with icon 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-1" label="Item 1">
<ic-tree-item id="ic-tree-item-1" label="Item 1">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<div class="icon-container">
<slot name="icon"></slot>
</div>
Expand All @@ -101,7 +101,7 @@ exports[`ic-tree-item component should render with icon 1`] = `
`;

exports[`ic-tree-item component should render with router item slot 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-5" label="Item 1">
<ic-tree-item id="ic-tree-item-5" label="Item 1">
<mock:shadow-root>
<slot name="router-item"></slot>
</mock:shadow-root>
Expand All @@ -112,9 +112,9 @@ exports[`ic-tree-item component should render with router item slot 1`] = `
`;

exports[`ic-tree-item component should render with slotted label 1`] = `
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" id="ic-tree-item-8">
<ic-tree-item id="ic-tree-item-8">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="ic-typography-body tree-item-label">
<mock:shadow-root>
<slot></slot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

.icon-container {
width: var(--ic-space-lg);
min-width: var(--ic-space-lg);
height: var(--ic-space-lg);
margin: 0 var(--ic-space-xs) 0 0;
}
Expand All @@ -60,6 +61,15 @@
overflow: hidden;
}

/* Ensures truncation works - accounts for text width increase as component hydrates */
.tree-view-header.with-padding {
padding-right: var(--ic-space-xs);
}

.tree-items-container-hidden {
visibility: hidden;
}

::slotted([slot="icon"]) {
fill: var(--ic-tree-view-icon);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ An example with truncated tree items. When the heading/label exceeds the width o
d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z"
/>
</svg>
<ic-tree-item label="Coffee">
<ic-tree-item label="Coffee" expanded="true">
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -613,7 +613,7 @@ An example with truncated tree items. When the heading/label exceeds the width o
></ic-tree-item>
<ic-tree-item label="Espresso"></ic-tree-item>
</ic-tree-item>
<ic-tree-item label="Tea">
<ic-tree-item label="Tea" expanded="true">
<ic-tree-item label="Earl Grey"></ic-tree-item>
<ic-tree-item
label="Earl Grey with truncation false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class TreeView {
private treeViewId = `ic-tree-view-${treeViewIds++}`;
private treeItemTag = "IC-TREE-ITEM";
private hostMutationObserver: MutationObserver = null;
private isLoaded = false;

@Element() el: HTMLIcTreeViewElement;

Expand Down Expand Up @@ -106,9 +107,7 @@ export class TreeView {
this.watchThemeHandler();
this.watchTruncateTreeItemsHandler();

setTimeout(() => {
this.truncateHeading && this.truncateTreeViewHeading();
}, 100);
this.truncateHeading && this.truncateTreeViewHeading();

this.addSlotChangeListener();

Expand All @@ -118,6 +117,8 @@ export class TreeView {
this.hostMutationObserver.observe(this.el, {
childList: true,
});

this.isLoaded = true;
}

@Listen("icTreeItemSelected")
Expand Down Expand Up @@ -294,15 +295,23 @@ export class TreeView {
};

render() {
const { heading, size, theme } = this;
const {
focusInset,
heading,
isLoaded,
size,
theme,
truncateHeading,
truncateTreeItems,
} = this;

return (
<Host
context-id={this.treeViewId}
class={{
[`ic-tree-view-${size}`]: size !== "medium",
[`ic-theme-${theme}`]: theme !== "inherit",
"ic-tree-view-truncate": this.truncateHeading,
"ic-tree-view-truncate": truncateHeading,
}}
onKeyDown={this.handleKeyDown}
aria-label={this.isHeadingDefined() ? heading : null}
Expand All @@ -314,7 +323,13 @@ export class TreeView {
<slot name="icon" />
</div>
)}
<ic-typography variant="subtitle-large" class="tree-view-header">
<ic-typography
variant="subtitle-large"
class={{
"tree-view-header": true,
"with-padding": this.truncateHeading && !isLoaded,
}}
>
{isSlotUsed(this.el, "heading") ? (
<slot name="heading" />
) : (
Expand All @@ -323,7 +338,16 @@ export class TreeView {
</ic-typography>
</div>
)}
<slot></slot>
<div
// Hide tree items until fully loaded with props passed down from tree view - prevents FOUC
class={{
"tree-items-container-hidden":
(focusInset || size !== "medium" || truncateTreeItems) &&
!isLoaded,
}}
>
<slot></slot>
</div>
</Host>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ic-tree-view component should render 1`] = `
<ic-tree-view class="ic-tree-view-dark" context-id="ic-tree-view-0" heading="Heading">
<ic-tree-view aria-label="Heading" context-id="ic-tree-view-0" heading="Heading">
<mock:shadow-root>
<div class="heading-area-container">
<ic-typography class="tree-view-header" variant="subtitle-large">
Heading
</ic-typography>
</div>
<slot></slot>
<div>
<slot></slot>
</div>
</mock:shadow-root>
</ic-tree-view>
`;

exports[`ic-tree-view component should render with icon 1`] = `
<ic-tree-view class="ic-tree-view-dark" context-id="ic-tree-view-1" heading="Heading">
<ic-tree-view aria-label="Heading" context-id="ic-tree-view-1" heading="Heading">
<mock:shadow-root>
<div class="heading-area-container">
<div class="icon-container">
Expand All @@ -24,7 +26,9 @@ exports[`ic-tree-view component should render with icon 1`] = `
Heading
</ic-typography>
</div>
<slot></slot>
<div>
<slot></slot>
</div>
</mock:shadow-root>
<svg fill="#000000" height="24px" slot="icon" viewBox="0 0 24 24" width="24px" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0V0z" fill="none"></path>
Expand All @@ -34,7 +38,7 @@ exports[`ic-tree-view component should render with icon 1`] = `
`;

exports[`ic-tree-view component should render with slotted heading 1`] = `
<ic-tree-view class="ic-tree-view-dark" context-id="ic-tree-view-3">
<ic-tree-view context-id="ic-tree-view-3">
<mock:shadow-root>
<div class="heading-area-container">
<ic-typography class="ic-typography-subtitle-large tree-view-header">
Expand All @@ -44,7 +48,9 @@ exports[`ic-tree-view component should render with slotted heading 1`] = `
<slot name="heading"></slot>
</ic-typography>
</div>
<slot></slot>
<div>
<slot></slot>
</div>
</mock:shadow-root>
<ic-typography class="ic-typography-subtitle-large" slot="heading" variant="subtitle-large">
<mock:shadow-root>
Expand All @@ -56,27 +62,29 @@ exports[`ic-tree-view component should render with slotted heading 1`] = `
`;

exports[`ic-tree-view component should render with tree items 1`] = `
<ic-tree-view class="ic-tree-view-dark" context-id="ic-tree-view-2" heading="Heading">
<ic-tree-view aria-label="Heading" context-id="ic-tree-view-2" heading="Heading">
<mock:shadow-root>
<div class="heading-area-container">
<ic-typography class="tree-view-header" variant="subtitle-large">
Heading
</ic-typography>
</div>
<slot></slot>
<div>
<slot></slot>
</div>
</mock:shadow-root>
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" context-id="ic-tree-view-2" id="ic-tree-item-0" label="Item 1">
<ic-tree-item context-id="ic-tree-view-2" id="ic-tree-item-0" label="Item 1">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="tree-item-label">
Item 1
</ic-typography>
</div>
</mock:shadow-root>
</ic-tree-item>
<ic-tree-item aria-disabled="false" class="ic-tree-item-dark" context-id="ic-tree-view-2" id="ic-tree-item-1" label="Item 2">
<ic-tree-item context-id="ic-tree-view-2" id="ic-tree-item-1" label="Item 2">
<mock:shadow-root>
<div class="ic-tree-item-single tree-item-content" tabindex="0">
<div aria-disabled="false" aria-live="polite" class="tree-item-content" tabindex="0">
<ic-typography class="tree-item-label">
Item 2
</ic-typography>
Expand Down

0 comments on commit 6c26721

Please sign in to comment.