diff --git a/.changeset/css-no-scollbar.md b/.changeset/css-no-scollbar.md new file mode 100644 index 000000000..b48452c49 --- /dev/null +++ b/.changeset/css-no-scollbar.md @@ -0,0 +1,5 @@ +--- +'@cypress-design/css': minor +--- + +add the no-scrollbar utillity class diff --git a/.changeset/overflowing-tabs.md b/.changeset/overflowing-tabs.md new file mode 100644 index 000000000..633e5d6b1 --- /dev/null +++ b/.changeset/overflowing-tabs.md @@ -0,0 +1,7 @@ +--- +'@cypress-design/constants-tabs': minor +'@cypress-design/react-tabs': minor +'@cypress-design/vue-tabs': minor +--- + +manage the overflowing of tabs diff --git a/components/Tabs/assertions.ts b/components/Tabs/assertions.ts index 9d4e369dc..e01e5a661 100644 --- a/components/Tabs/assertions.ts +++ b/components/Tabs/assertions.ts @@ -6,7 +6,29 @@ const tabs = [ { id: 'ov', label: 'Overview' }, { id: 'cl', label: 'Command Log' }, { id: 'err', label: 'Errors' }, - { id: 'reco', label: 'Recommendations' }, + { id: 'reco', label: 'Recommend' }, +] + +const longTabs = [ + { id: 'ov1', label: 'Overview' }, + { id: 'cl1', label: 'Command Log' }, + { id: 'err1', label: 'Errors' }, + { id: 'o', label: 'o' }, + { id: 'd', label: 'd' }, + { id: 's', label: 's' }, + { id: 'reco1', label: 'Recommendations' }, + { id: 'ov2', label: 'Overview 1' }, + { id: 'cl2', label: 'Command Log 1' }, + { id: 'err2', label: 'Errors 1' }, + { id: 'reco2', label: 'Recommendations 1' }, + { id: 'ov3', label: 'Overview 2', active: true }, + { id: 'cl3', label: 'Command Log 2' }, + { id: 'err3', label: 'Errors 2' }, + { id: 'reco3', label: 'Recommendations 2' }, + { id: 'ov4', label: 'Overview 3' }, + { id: 'cl4', label: 'Command Log 3' }, + { id: 'err4', label: 'Errors 3' }, + { id: 'reco4', label: 'Recommendations 3' }, ] export default function assertions( @@ -42,5 +64,47 @@ export default function assertions( }) }) }) + + describe('overflowing tabs', () => { + it( + 'displays ellipsis when tabs are overflowing', + { viewportHeight: 500 }, + () => { + mountStory({ + tabs: longTabs, + // variant: 'underline-small', + }) + cy.findByText('Show more tabs').should('exist') + } + ) + + it( + 'displays ellipsis as active tab when tabs are overflowing', + { viewportHeight: 500 }, + () => { + mountStory({ + tabs: longTabs, + // variant: 'underline-small', + }) + cy.contains('button', 'Show more tabs').should( + 'have.attr', + 'aria-selected' + ) + } + ) + + it( + 'displays active tab when tabs are overflowing', + { viewportHeight: 500 }, + () => { + mountStory({ + tabs: longTabs, + // variant: 'underline-small', + }) + cy.findByText('Show more tabs').click({ force: true }) + cy.findByText('Overview').click() + } + ) + }) }) } diff --git a/components/Tabs/constants/src/index.ts b/components/Tabs/constants/src/index.ts index 52525ec32..cf789ecbb 100644 --- a/components/Tabs/constants/src/index.ts +++ b/components/Tabs/constants/src/index.ts @@ -35,6 +35,9 @@ export interface Tab { href?: string } +export const overflowContainerClass = + 'overflow-x-auto overflow-y-hidden no-scrollbar px-[1px] pb-[4px]' + export const variants = { default: { classes: { @@ -110,7 +113,7 @@ export const variants = { 'underline-small': { classes: { wrapper: - 'py-[4px] flex gap-[8px] border-b border-gray-100 text-gray-700 dark:text-white relative', + 'py-[4px] inline-flex gap-[8px] border-b border-gray-100 text-gray-700 dark:text-white relative', button: 'flex items-center px-[12px] h-[24px] leading-[20px] text-[14px] rounded font-medium whitespace-nowrap relative', active: 'text-gray-900 dark:text-gray-400 z-20', @@ -134,7 +137,7 @@ export const variants = { 'underline-large': { classes: { wrapper: - 'py-[4px] flex gap-[8px] border-b border-gray-100 text-gray-700 dark:text-white relative', + 'py-[4px] inline-flex gap-[8px] border-b border-gray-100 text-gray-700 dark:text-white relative', button: 'flex items-center px-[12px] h-[32px] leading-[24px] text-[16px] rounded font-medium whitespace-nowrap relative', active: 'text-gray-900 dark:text-gray-400 z-20', diff --git a/components/Tabs/react/Tabs.tsx b/components/Tabs/react/Tabs.tsx index 3741ad20e..80ee8e26d 100644 --- a/components/Tabs/react/Tabs.tsx +++ b/components/Tabs/react/Tabs.tsx @@ -1,6 +1,10 @@ import * as React from 'react' import clsx from 'clsx' -import { Tab, variants } from '@cypress-design/constants-tabs' +import { + Tab, + overflowContainerClass, + variants, +} from '@cypress-design/constants-tabs' export interface TabsProps { /** @@ -41,6 +45,7 @@ export const Tabs: React.FC> = ({ }, [activeIdProp]) const $tab = React.useRef<(HTMLButtonElement | HTMLAnchorElement)[]>([]) + const $overflowContainer = React.useRef(null) const [activeMarkerStyle, setActiveMarkerStyle] = React.useState<{ left?: string @@ -48,21 +53,44 @@ export const Tabs: React.FC> = ({ transitionProperty?: string }>({}) - React.useEffect(() => { + function getActiveTabEl() { const activeTab = tabs.findIndex((tab) => tab.id === activeId) if (activeTab > -1) { const activeTabEl = $tab.current?.[activeTab] if (activeTabEl) { - setActiveMarkerStyle({ - left: `${activeTabEl.offsetLeft}px`, - width: `${activeTabEl.offsetWidth}px`, - transitionProperty: 'left, width', - }) + return activeTabEl } } + return null + } + + React.useEffect(() => { + const activeTabEl = getActiveTabEl() + if (activeTabEl) { + setActiveMarkerStyle({ + left: `${activeTabEl.offsetLeft}px`, + width: `${activeTabEl.offsetWidth}px`, + transitionProperty: 'left, width', + }) + } setMounted(true) }, [activeId]) + React.useEffect(() => { + const activeTabEl = getActiveTabEl() + if ($overflowContainer.current && activeTabEl) { + // Scroll to active tab if it is not visible + const leftBoundary = + $overflowContainer.current.offsetWidth / 2 - activeTabEl.offsetWidth / 2 + + if (activeTabEl.offsetLeft > leftBoundary) { + $overflowContainer.current.scrollTo({ + left: activeTabEl.offsetLeft - leftBoundary, + }) + } + } + }, []) + function navigate(shift: number) { const shiftedIndex = tabs.findIndex((tab) => tab.id === activeId) + shift const nextIndex = @@ -83,69 +111,71 @@ export const Tabs: React.FC> = ({ variant in variants ? variants[variant].icon : variants.default.icon return ( -
- {tabs.map((tab, index) => { - const ButtonTag = tab.href ? 'a' : 'button' - return ( - (el ? ($tab.current[index] = el) : null)} - tabIndex={tab.id === activeId ? undefined : -1} - aria-selected={tab.id === activeId ? true : undefined} - onClick={(e) => { - if (e.ctrlKey || e.metaKey) return - e.preventDefault() - setActiveId(tab.id) - onSwitch?.(tab) - }} - onKeyUp={(e) => { - if (e.key === 'ArrowRight') { - navigate(1) - } else if (e.key === 'ArrowLeft') { - navigate(-1) - } - }} - > - <> - {(() => { - const IconBefore = tab.iconBefore ?? tab.icon - return IconBefore ? ( - - ) : null - })()} - {tab.label} - {tab.tag ?
{tab.tag}
: null} - {tab.iconAfter ? ( - +
+
+ {tabs.map((tab, index) => { + const ButtonTag = tab.href ? 'a' : 'button' + return ( + (el ? ($tab.current[index] = el) : null)} + tabIndex={tab.id === activeId ? undefined : -1} + aria-selected={tab.id === activeId ? true : undefined} + onClick={(e) => { + if (e.ctrlKey || e.metaKey) return + e.preventDefault() + setActiveId(tab.id) + onSwitch?.(tab) + }} + onKeyUp={(e) => { + if (e.key === 'ArrowRight') { + navigate(1) + } else if (e.key === 'ArrowLeft') { + navigate(-1) + } + }} + > + <> + {(() => { + const IconBefore = tab.iconBefore ?? tab.icon + return IconBefore ? ( + + ) : null + })()} + {tab.label} + {tab.tag ?
{tab.tag}
: null} + {tab.iconAfter ? ( + + ) : null} + + {tab.id === activeId && !activeMarkerStyle.left ? ( +
) : null} - - {tab.id === activeId && !activeMarkerStyle.left ? ( -
- ) : null} - - ) - })} -
-
+ + ) + })} +
+
+
) } diff --git a/components/Tabs/vue/Tabs.vue b/components/Tabs/vue/Tabs.vue index 5741cfc5e..89b114d6d 100644 --- a/components/Tabs/vue/Tabs.vue +++ b/components/Tabs/vue/Tabs.vue @@ -1,13 +1,29 @@ diff --git a/components/Tabs/vue/_Tab.vue b/components/Tabs/vue/_Tab.vue new file mode 100644 index 000000000..457adfe21 --- /dev/null +++ b/components/Tabs/vue/_Tab.vue @@ -0,0 +1,61 @@ + + + diff --git a/components/Tabs/vue/package.json b/components/Tabs/vue/package.json index 51496641a..d2e6e9f59 100644 --- a/components/Tabs/vue/package.json +++ b/components/Tabs/vue/package.json @@ -20,10 +20,12 @@ "build:types": "yarn vue-tsc --project ./tsconfig.build.json" }, "dependencies": { - "@cypress-design/constants-tabs": "*" + "@cypress-design/constants-tabs": "*", + "@cypress-design/place-floating-vue": "*" }, "devDependencies": { - "@cypress-design/rollup-plugin-tailwind-keep": "*" + "@cypress-design/rollup-plugin-tailwind-keep": "*", + "@vueuse/core": "*" }, "license": "MIT" } \ No newline at end of file diff --git a/components/Tag/react/Tag.cy.tsx b/components/Tag/react/Tag.cy.tsx deleted file mode 100644 index eb9c591c2..000000000 --- a/components/Tag/react/Tag.cy.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/// - -import * as React from 'react' -import { mount } from 'cypress/react18' -import TagStory from './Tag.rootstory' -import assertions from '../assertions' - -describe('Tag', () => { - function mountStory() { - mount() - } - assertions(mountStory) -}) diff --git a/components/Tag/react/Tag.rootstory.tsx b/components/Tag/react/TagReact.cy.tsx similarity index 73% rename from components/Tag/react/Tag.rootstory.tsx rename to components/Tag/react/TagReact.cy.tsx index 4b8fc9a06..9b3c44d8b 100644 --- a/components/Tag/react/Tag.rootstory.tsx +++ b/components/Tag/react/TagReact.cy.tsx @@ -1,18 +1,15 @@ -import clsx from 'clsx' -import Tag from './Tag' +/// + import * as React from 'react' -import { SizeClasses, ColorClasses } from '../constants' +import { mount } from 'cypress/react18' +import assertions from '../assertions' -export default () => ( +const TagStory = () => (
{(Object.keys(SizeClasses) as Array).map( (size) => { return ( -
+

{size}

{( Object.keys(ColorClasses) as Array @@ -38,3 +35,10 @@ export default () => ( )}
) + +describe('Tag', () => { + function mountStory() { + mount() + } + assertions(mountStory) +}) diff --git a/components/Tag/vue/Tag.cy.tsx b/components/Tag/vue/Tag.cy.tsx deleted file mode 100644 index cd3ee4825..000000000 --- a/components/Tag/vue/Tag.cy.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/// -import { mount } from 'cypress/vue' -import assertions from '../assertions' -import TagStory from './Tag.rootstory' - -describe('', () => { - function mountStory() { - mount(() => ) - } - assertions(mountStory) -}) diff --git a/components/Tag/vue/Tag.rootstory.tsx b/components/Tag/vue/TagVue.cy.tsx similarity index 69% rename from components/Tag/vue/Tag.rootstory.tsx rename to components/Tag/vue/TagVue.cy.tsx index 7d5201bd8..5434e07df 100644 --- a/components/Tag/vue/Tag.rootstory.tsx +++ b/components/Tag/vue/TagVue.cy.tsx @@ -1,17 +1,15 @@ -import clsx from 'clsx' +/// +import { mount } from 'cypress/vue' +import { ColorClasses, SizeClasses } from '@cypress-design/constants-tag' +import assertions from '../assertions' import Tag from './Tag.vue' -import { SizeClasses, ColorClasses } from '../constants' -export default () => ( +const TagStory = () => (
{(Object.keys(SizeClasses) as Array).map( (size) => { return ( -
+

{size}

{(Object.keys(ColorClasses) as Array) .reverse() @@ -37,3 +35,10 @@ export default () => ( )}
) + +describe('', () => { + function mountStory() { + mount(() => ) + } + assertions(mountStory) +}) diff --git a/components/Tooltip/vue/Tooltip.vue b/components/Tooltip/vue/Tooltip.vue index ec4f6f4b5..70f3f6021 100644 --- a/components/Tooltip/vue/Tooltip.vue +++ b/components/Tooltip/vue/Tooltip.vue @@ -1,6 +1,6 @@