Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/ods-react/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
collectCoverage: false,
testPathIgnorePatterns: [
'components',
'node_modules/',
'dist/',
],
testRegex: 'tests\\/.*\\.spec\\.(ts|tsx)$',
transform: {
'\\.(ts|tsx)$': 'ts-jest',
},
verbose: true,
};
4 changes: 3 additions & 1 deletion packages/ods-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"build:prod": "rimraf dist && tsc -b && vite build",
"clean": "rimraf dist",
"lint:scss": "stylelint 'src/style/*.scss'",
"lint:ts": "eslint '{src,tests}/!(components)/**/*.{ts,tsx}'"
"lint:ts": "eslint '{src,tests}/!(components)/**/*.{ts,tsx}'",
"test:spec": "jest --passWithNoTests",
"test:spec:ci": "npm run test:spec"
},
"dependencies": {
"@ark-ui/react": "5.12.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Tabs } from '@ark-ui/react/tabs';
import classNames from 'classnames';
import { type ComponentPropsWithRef, type FC, type JSX } from 'react';
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react';
import style from './tabContent.module.scss';

interface TabContentProp extends ComponentPropsWithRef<'div'> {
Expand All @@ -10,22 +10,23 @@ interface TabContentProp extends ComponentPropsWithRef<'div'> {
value: string,
}

const TabContent: FC<TabContentProp> = ({
const TabContent: FC<TabContentProp> = forwardRef(({
children,
className,
value,
...props
}): JSX.Element => {
}, ref): JSX.Element => {
return (
<Tabs.Content
className={ classNames(style['tab-content'], className) }
data-ods="tab-content"
ref={ ref }
value={ value }
{ ...props }>
{ children }
</Tabs.Content>
);
};
});

export {
TabContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,122 @@
import { Tabs } from '@ark-ui/react/tabs';
import classNames from 'classnames';
import { type ComponentPropsWithRef, type FC, type JSX } from 'react';
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { debounce } from '../../../../../utils/debounce';
import { BUTTON_SIZE, BUTTON_VARIANT, Button } from '../../../../button/src';
import { ICON_NAME, Icon } from '../../../../icon/src';
import { useTabs } from '../../contexts/useTabs';
import style from './tabList.module.scss';

interface TabListProp extends ComponentPropsWithRef<'div'> {}

const TabList: FC<TabListProp> = ({
const TabList: FC<TabListProp> = forwardRef(({
children,
className,
...props
}): JSX.Element => {
}, ref): JSX.Element => {
const { withArrows } = useTabs();
const [isLeftButtonDisabled, setIsLeftButtonDisabled] = useState(false);
const [isRightButtonDisabled, setIsRightButtonDisabled] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);

const updateScrollButtonState = useCallback(() => {
setIsLeftButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === 0 : false);
setIsRightButtonDisabled(scrollRef.current ? scrollRef.current.scrollLeft === scrollRef.current.scrollWidth - scrollRef.current.offsetWidth : false);
}, []);

const debouncedUpdateScrollButtonState = useMemo(() => {
return debounce(updateScrollButtonState, 50);
}, [updateScrollButtonState]);

useEffect(() => {
updateScrollButtonState();

if (scrollRef.current) {
const observer = new ResizeObserver(() => {
updateScrollButtonState();
});

observer.observe(scrollRef.current);

return () => {
observer.disconnect();
};
}
}, [updateScrollButtonState]);

function scroll(left: number): void {
scrollRef.current?.scrollBy({
behavior: 'smooth',
left,
});
}

function onLeftScrollClick(): void {
if (!scrollRef.current) {
return;
}
scroll(-scrollRef.current.offsetWidth);
}

function onRightScrollClick(): void {
if (!scrollRef.current) {
return;
}
scroll(scrollRef.current.offsetWidth);
}

return (
<Tabs.List
className={ classNames(style['tab-list'], className) }
data-ods="tab-list"
ref={ ref }
{ ...props }>
{ children }
{
withArrows &&
<div className={ classNames(
style['tab-list__left-arrow'],
{ [style['tab-list__left-arrow--active']]: !isLeftButtonDisabled },
)}>
<Button
disabled={ isLeftButtonDisabled }
onClick={ onLeftScrollClick }
size={ BUTTON_SIZE.xs }
tabIndex={ -1 }
variant={ BUTTON_VARIANT.ghost }>
<Icon name={ ICON_NAME.chevronLeft } />
</Button>
</div>
}

<div
className={ style['tab-list__container'] }
onScroll={ debouncedUpdateScrollButtonState }
ref={ scrollRef }
tabIndex={ -1 }>
<div className={ style['tab-list__container__tabs'] }>
{ children }
</div>
</div>

{
withArrows &&
<div className={ classNames(
style['tab-list__right-arrow'],
{ [style['tab-list__right-arrow--active']]: !isRightButtonDisabled },
)}>
<Button
disabled={ isRightButtonDisabled }
onClick={ onRightScrollClick }
size={ BUTTON_SIZE.xs }
tabIndex={ -1 }
variant={ BUTTON_VARIANT.ghost }>
<Icon name={ ICON_NAME.chevronRight } />
</Button>
</div>
}
</Tabs.List>
);
};
});

export {
TabList,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
@use '../../../../../style/button';
@use '../../../../../style/scroll';

$tab-list-border-bottom-size: 2px;

@layer ods-molecules {
.tab-list {
display: inline-flex;
border-bottom: solid $tab-list-border-bottom-size var(--ods-color-neutral-100);
min-width: 100%;
display: flex;
position: relative;
align-items: baseline;

&__left-arrow,
&__right-arrow {
display: flex;
position: relative;
align-items: center;
align-self: stretch;
height: auto;

&::after {
position: absolute;
top: 0;
bottom: 0;
transition: opacity ease .3s;
opacity: 0;
z-index: 1;
width: 60px;
height: 100%;
content: '';
pointer-events: none;
}
}

&__left-arrow {
&::after {
background: linear-gradient(270deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
}

&--active {
&::after {
left: button.$ods-button-size-xs;
opacity: 1;
}
}
}

&__right-arrow {
&::after {
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
}

&--active {
&::after {
right: button.$ods-button-size-xs;
opacity: 1;
}
}
}

&__container {
@include scroll.hide-scrollbar();

display: inline-block;
position: relative;
overflow: auto hidden;
white-space: nowrap;

&__tabs {
display: flex;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Tabs } from '@ark-ui/react/tabs';
import { Tabs, useTabsContext } from '@ark-ui/react/tabs';
import classNames from 'classnames';
import { type ComponentPropsWithRef, type FC, type JSX } from 'react';
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import style from './tab.module.scss';

interface TabProp extends ComponentPropsWithRef<'button'> {
Expand All @@ -10,22 +10,37 @@ interface TabProp extends ComponentPropsWithRef<'button'> {
value: string,
}

const Tab: FC<TabProp> = ({
const Tab: FC<TabProp> = forwardRef(({
children,
className,
value,
...props
}): JSX.Element => {
}, ref): JSX.Element => {
const { focusedValue } = useTabsContext();
const innerRef = useRef<HTMLButtonElement>(null);

useImperativeHandle(ref, () => innerRef.current!, [innerRef]);

useEffect(() => {
if (innerRef.current && focusedValue === value) {
innerRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
}, [focusedValue, value]);

return (
<Tabs.Trigger
className={ classNames(style['tab'], className) }
data-ods="tab"
ref={ innerRef }
value={ value }
{ ...props }>
{ children }
</Tabs.Trigger>
);
};
});

export {
Tab,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
@layer ods-molecules {
.tab {
display: block;
margin-bottom: -#{tab-list.$tab-list-border-bottom-size};
border-top: none;
border-right: none;
border-bottom: solid tab-list.$tab-list-border-bottom-size transparent;
border-bottom: solid tab-list.$tab-list-border-bottom-size var(--ods-color-neutral-100);
border-left: none;
border-top-left-radius: var(--ods-border-radius-md);
border-top-right-radius: var(--ods-border-radius-md);
border-color: transparent;
background: none;
cursor: pointer;
padding: 2px 16px;
Expand All @@ -27,11 +25,12 @@
@include focus.ods-focus();

z-index: 1;
outline-offset: 0;
outline-offset: calc(var(--ods-outline-offset) * -1);
}

&:focus-visible:not([disabled], [aria-selected="true"]),
&:hover:not([disabled], [aria-selected="true"]) {
border-color: var(--ods-color-neutral-050);
background-color: var(--ods-color-neutral-050);
}

Expand Down
Loading