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(js): inbox tabs #6149

Merged
merged 11 commits into from
Aug 1, 2024
Merged

feat(js): inbox tabs #6149

merged 11 commits into from
Aug 1, 2024

Conversation

LetItRock
Copy link
Contributor

@LetItRock LetItRock commented Jul 24, 2024

What changed? Why was the change needed?

The Inbox tabs UI component implementation:

  • the tabs prop drilling from the NovuUI
  • the tabs primitive component
  • dynamic visible tabs rendering logic based on available parent container space

Known limitations:

  • it doesn't support keyboard navigation
  • it doesn't recalculate the "visible tabs" when the container size changes

Screenshots

Screenshot 2024-07-24 at 17 35 33

Screenshot 2024-07-24 at 17 35 24

Screenshot 2024-07-24 at 17 52 11

Screen.Recording.2024-07-24.at.17.08.16.mov
Screen.Recording.2024-07-24.at.19.07.46.mov

Copy link

linear bot commented Jul 24, 2024

export const InboxTab = (props: { label: string; class?: string }) => {
const style = useStyle();

// TODO: Replace with actual count from API
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the fetching logic for the counts will be in the separate PR/ticket

Comment on lines 11 to 28
<span class={style('tabsTabLabel', 'nt-text-sm nt-font-medium')}>{props.label}</span>
<span
class={style('tabsTabCount', 'nt-rounded-full nt-bg-primary nt-px-[6px] nt-text-primary-foreground nt-text-sm')}
>
{count >= 100 ? '99+' : count}
</span>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure if these should be part of the Tabs.Tab or not

Copy link
Contributor

Choose a reason for hiding this comment

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

Generally, they should, but in our case, we'd only do it for the appearance keys. When we add an element, we are forced to add an appearance key anyway, and more specific that the primitive one. This means that we would redeclare one anyway when using Tab.Label for example. I'm fine with them not being actual components.

tabs: Array<{ label: string; value: Array<string> }>;
};

export const InboxTabs = (props: InboxTabsProps) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the stateful component for tabs

Comment on lines 17 to 54
onMount(() => {
const tabsListEl = tabsList();
if (!tabsListEl) return;

const tabs = [...tabsListEl.querySelectorAll('[role="tab"]')];

const observer = new IntersectionObserver(
(entries) => {
let visibleTabIds = entries
.filter((entry) => entry.isIntersecting && entry.intersectionRatio === 1)
.map((entry) => entry.target.id);

if (tabs.length === visibleTabIds.length) {
setVisibleTabs(props.tabs.filter((tab) => visibleTabIds.includes(tab.label)));
observer.disconnect();
return;
}

visibleTabIds = visibleTabIds.slice(0, -1);
setVisibleTabs(props.tabs.filter((tab) => visibleTabIds.includes(tab.label)));
setDropdownTabs(props.tabs.filter((tab) => !visibleTabIds.includes(tab.label)));
observer.disconnect();
},
{ root: tabsListEl }
);

for (const tabElement of tabs) {
observer.observe(tabElement);
}
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Calculate which tabs are fully visible in the parent container. The ones that are not visible will be shown in the dropdown.
To have enough space to render the dropdown we remove one tab from the visible tabs list in line 35;

Comment on lines 53 to 50
<Tabs.List ref={setTabsList}>
{props.tabs.map((tab) => (
<InboxTab label={tab.label} class="nt-invisible" />
))}
</Tabs.List>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tabs are rendered hidden at the beginning (otherwise there would be a blink). Then we calculate which ones are fully visible and don't overflow in the parent container. Later we render the visible tabs.

packages/js/src/ui/components/InboxTabs/InboxTabs.tsx Outdated Show resolved Hide resolved
'moreActions__dropdownItemLabelContainer',
'moreActions__dropdownItemLeftIcon',
//Actions dropdown
'actionsContainer',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

check the comment in MoreTabsDropdown

Comment on lines +14 to +31
createEffect(() => {
const handleTabKey = (event: KeyboardEvent) => {
if (event.key !== 'Tab') {
return;
}

const tabs = tabsContainer()?.querySelectorAll('[role="tab"]');
if (!tabs || !document.activeElement) {
return;
}

setKeyboardNavigation(Array.from(tabs).includes(document.activeElement));
};

document.addEventListener('keyup', handleTabKey);

return onCleanup(() => document.removeEventListener('keyup', handleTabKey));
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

listen to the tab keyboard events and check if document.activeElement is the "tab dom element", if yes allow keyboard navigation with ArrowLeft or ArrowRight

Comment on lines 33 to 60
createEffect(() => {
const handleArrowKeys = (event: KeyboardEvent) => {
if (!keyboardNavigation() || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) {
return;
}

const tabElements = Array.from<HTMLButtonElement>(tabsContainer()?.querySelectorAll('[role="tab"]') ?? []);
const tabIds = tabElements.map((tab) => tab.id);
let currentIndex = tabIds.indexOf(activeTab());
const length = tabIds.length;
let activeIndex = currentIndex;
let newTab = activeTab();
if (event.key === 'ArrowLeft') {
activeIndex = currentIndex === 0 ? length - 1 : currentIndex - 1;
newTab = tabIds[activeIndex];
} else if (event.key === 'ArrowRight') {
activeIndex = currentIndex === length - 1 ? 0 : currentIndex + 1;
newTab = tabIds[activeIndex];
}

tabElements[activeIndex].focus();
setActiveTab(newTab);
};

document.addEventListener('keydown', handleArrowKeys);

return onCleanup(() => document.removeEventListener('keydown', handleArrowKeys));
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the keyboard arrow keys navigation between the tabs; calculations for the next tab to focus and set active tab;

Copy link
Contributor

@desiprisg desiprisg left a comment

Choose a reason for hiding this comment

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

Generally, I'd prefer it if we implemented the tabs more the <Popover>, as we could set the default tabs styles in a function (like popoverContentVariants()), and default to those, unless another class prop is specified. See PopoverContent.tsx for reference.

We only need one instance of Tabs for now, so I'd say this could also be a cleanup ticket post-release.

packages/js/src/ui/components/primitives/Tabs/Tabs.tsx Outdated Show resolved Hide resolved
packages/js/src/ui/components/primitives/Tabs/Tabs.tsx Outdated Show resolved Hide resolved
packages/js/src/ui/components/primitives/Tabs/TabsTab.tsx Outdated Show resolved Hide resolved
packages/js/src/ui/icons/Check.tsx Outdated Show resolved Hide resolved
packages/js/src/ui/novuUI.tsx Outdated Show resolved Hide resolved
@@ -51,11 +60,8 @@ export const Inbox = (props: InboxProps) => {
</Button>
)}
/>
<Popover.Content
appearanceKey="inbox__popoverContent"
class={cn(popoverContentVariants(), 'nt-max-w-96 nt-w-full')}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved to the variants function

@LetItRock LetItRock merged commit 229b3c1 into next Aug 1, 2024
24 checks passed
@LetItRock LetItRock deleted the com-43-inbox-tabs branch August 1, 2024 11:36
BiswaViraj pushed a commit that referenced this pull request Aug 1, 2024
BiswaViraj pushed a commit that referenced this pull request Aug 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants