Neovici/cosmoz-tabs

View on GitHub
src/use-tabs.js

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
import { useState, useEffect, useMemo, useCallback } from '@pionjs/pion';
import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
/* eslint-disable-next-line import/no-unresolved */
import { useHashParam, link } from '@neovici/cosmoz-router/use-hash-param';
import { choose, collect, getName, isValid } from './utils';
import { compute } from 'compute-scroll-into-view';

const useTabSelectedEffect = (host, selectedTab) => {
        useEffect(() => {
            notifyProperty(host, 'selectedItem', selectedTab);
            if (selectedTab == null) {
                return;
            }
            const selected = getName(selectedTab);
            if (selected !== host.selected) {
                requestAnimationFrame(() => notifyProperty(host, 'selected', selected));
            }
            selectedTab.toggleAttribute('is-selected', true);
            const eventOpts = { composed: true };
            if (!selectedTab._active) {
                selectedTab.dispatchEvent(
                    new CustomEvent('tab-first-select', eventOpts),
                );
                selectedTab._active = true;
            }
            selectedTab.dispatchEvent(new CustomEvent('tab-select', eventOpts));
            if (!host.noResize) {
                requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
            }
            return () => {
                selectedTab.toggleAttribute('is-selected', false);
                selectedTab._fallbackFor = undefined;
            };
        }, [selectedTab]);
    },
    useAutoScroll = (host, selectedTab) => {
        useEffect(() => {
            const el = host.shadowRoot.querySelector('a[aria-selected]');
            if (!el) {
                return;
            }
            compute(el, {
                block: 'nearest',
                inline: 'center',
                boundary: el.parentElement,
            }).forEach(({ el, top, left }) =>
                el.scroll({ top, left, behavior: 'smooth' }),
            );
        }, [selectedTab]);
    },
    useTabs = (host) => {
        const { selected, hashParam } = host,
            [tabs, setTabs] = useState([]),
            [param] = useHashParam(hashParam),
            selection =
                hashParam == null || (param == null && selected != null)
                    ? selected
                    : param,
            selectedTab = useMemo(() => choose(tabs, selection), [tabs, selection]);

        useTabSelectedEffect(host, selectedTab);

        useEffect(() => {
            const onTabAlter = (e) => {
                e.stopPropagation();
                const { target: tab } = e;
                if (
                    selectedTab != null &&
                    selectedTab._fallbackFor === tab &&
                    isValid(tab)
                ) {
                    selectedTab._fallbackFor = undefined;
                    host.selected = getName(tab);
                }
                setTabs((prev) => prev.slice());
            };
            host.addEventListener('cosmoz-tab-alter', onTabAlter);
            return () => host.removeEventListener('cosmoz-tab-alter', onTabAlter);
        }, [selectedTab]);

        useAutoScroll(host, selectedTab);

        const href = useCallback(
            (tab) => (isValid(tab) ? link(hashParam, getName(tab)) : undefined),
            [hashParam],
        );

        return {
            tabs,
            selectedTab,
            onSlot: useCallback(
                ({ target }) => requestAnimationFrame(() => setTabs(collect(target))),
                [],
            ),
            onSelect: useCallback((e) => {
                if (e.button !== 0 || e.metaKey || e.ctrlKey) {
                    return;
                }
                const { tab } = e.currentTarget;
                notifyProperty(host, 'selected', getName(tab));

                if (hashParam == null) {
                    return;
                }

                e.preventDefault();
                window.history.pushState({}, '', href(tab));
                requestAnimationFrame(() =>
                    window.dispatchEvent(new CustomEvent('hashchange')),
                );
            }, []),
            href,
        };
    };

export { useTabs };