Skip to content

Commit

Permalink
feat: make Tabs a composite component
Browse files Browse the repository at this point in the history
  • Loading branch information
alaa-yahia committed Jun 14, 2024
1 parent 87bb465 commit aa28c70
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 169 deletions.
44 changes: 28 additions & 16 deletions components/tab/src/tab-bar/tabs.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
import { colors } from '@dhis2/ui-constants'
import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { useRef } from 'react'
import React, { useRef, useMemo } from 'react'

const Tabs = ({ children, fixed, dataTest }) => {
const tabContainer = useRef(null)

const childrenRefs = useMemo(
() => React.Children.map(children, () => React.createRef()),
[children]
)

const handleKeyDown = (event) => {
const currentFocus = document.activeElement
if (
!tabContainer.current ||
!tabContainer.current.contains(currentFocus)
) {

if (tabContainer.current && tabContainer.current === currentFocus) {
if (childrenRefs.length > 0 && childrenRefs[0].current) {
childrenRefs[0].current.focus()
}
return
}

const role = currentFocus.getAttribute('role')
if (role !== 'tab') {
const currentIndex = childrenRefs.findIndex(
(ref) => ref.current === currentFocus
)

if (currentIndex === -1) {
return
}
const tabs = Array.from(
tabContainer.current.querySelectorAll('[role="tab"]')
)
const currentIndex = tabs.indexOf(currentFocus)

if (event.key === 'ArrowRight') {
event.preventDefault()
const nextIndex = (currentIndex + 1) % tabs.length
tabs[nextIndex].focus()
const nextIndex = (currentIndex + 1) % childrenRefs.length
childrenRefs[nextIndex].current.focus()
}

if (event.key === 'ArrowLeft') {
event.preventDefault()
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length
tabs[prevIndex].focus()
const prevIndex =
(currentIndex - 1 + childrenRefs.length) % childrenRefs.length
childrenRefs[prevIndex].current.focus()
}
}

Expand All @@ -42,9 +49,14 @@ const Tabs = ({ children, fixed, dataTest }) => {
ref={tabContainer}
data-test={dataTest}
role="tablist"
tabIndex={0}
onKeyDown={handleKeyDown}
>
{children}
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
ref: childrenRefs[index],
})
)}

<style jsx>{`
div {
Expand Down
308 changes: 155 additions & 153 deletions components/tab/src/tab/tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,160 +4,162 @@ import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { useState, useEffect, useRef } from 'react'

const Tab = ({
icon,
onClick,
selected,
disabled,
children,
className,
dataTest,
}) => {
const tabRef = useRef(null)
const [isOverflowing, setIsOverflowing] = useState(false)

useEffect(() => {
const checkOverflow = () => {
const isOverflow =
tabRef.current.scrollWidth > tabRef.current.clientWidth
setIsOverflowing(isOverflow)
export const Tab = React.forwardRef(
(
{ icon, onClick, selected, disabled, children, className, dataTest },
ref
) => {
let tabRef = useRef(null)
if (ref) {
tabRef = ref
}
checkOverflow()
}, [])

return (
<button
className={`${cx('tab', className, {
selected,
disabled,
})}`}
onClick={disabled ? undefined : (event) => onClick({}, event)}
data-test={dataTest}
role="tab"
aria-selected={selected ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : 'false'}
onFocus={disabled ? undefined : (event) => onClick({}, event)}
>
{icon}
{isOverflowing ? (
<Tooltip content={children} maxWidth={'100%'}>
const [isOverflowing, setIsOverflowing] = useState(false)

useEffect(() => {
const checkOverflow = () => {
const isOverflow =
tabRef.current.scrollWidth > tabRef.current.clientWidth
setIsOverflowing(isOverflow)
}
checkOverflow()
}, [])

return (
<button
className={`${cx('tab', className, {
selected,
disabled,
})}`}
onClick={disabled ? undefined : (event) => onClick({}, event)}
data-test={dataTest}
ref={tabRef}
role="tab"
aria-selected={selected ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : 'false'}
onFocus={disabled ? undefined : (event) => onClick({}, event)}
tabIndex={-1}
>
{icon}
{isOverflowing ? (
<Tooltip content={children} maxWidth={'100%'}>
<span ref={tabRef}>{children}</span>
</Tooltip>
) : (
<span ref={tabRef}>{children}</span>
</Tooltip>
) : (
<span ref={tabRef}>{children}</span>
)}

<style jsx>{`
button {
flex-grow: 0;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: bottom;
height: 100%;
padding: 16px 16px 11px;
background-color: transparent;
outline: none;
border: none;
border-bottom: 1px solid ${colors.grey400};
color: ${colors.grey600};
font-size: 14px;
line-height: 20px;
cursor: pointer;
}
:global(.fixed) > button {
flex-grow: 1;
}
button::after {
content: ' ';
display: block;
position: absolute;
bottom: -1px;
inset-inline-start: 0;
height: 4px;
width: 100%;
background-color: transparent;
}
span {
display: inline-flex;
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: fill 150ms ease-in-out;
}
/*focus-visible backwards compatibility for safari: https://css-tricks.com/platform-news-using-focus-visible-bbcs-new-typeface-declarative-shadow-doms-a11y-and-placeholders/*/
button:focus {
outline: 3px solid ${theme.focus};
outline-offset: -3px;
}
button:focus:not(:focus-visible) {
outline: none;
}
button > :global(svg) {
fill: ${colors.grey600};
width: 14px;
height: 14px;
margin: 0 4px 0 0;
}
button:hover {
color: ${colors.grey900};
}
button:hover::after {
background-color: ${colors.grey600};
height: 2px;
}
button:active::after {
background-color: ${colors.grey800};
}
button.selected {
color: ${theme.primary800};
}
button.selected::after {
background-color: ${theme.primary700};
transition: background-color 150ms ease-in-out;
}
button.selected:hover::after {
background-color: ${theme.primary700};
height: 4px;
}
button.selected > :global(svg) {
fill: ${theme.primary700};
}
button.disabled {
color: ${colors.grey500};
cursor: not-allowed;
}
button.disabled:hover,
button.selected:hover {
background-color: transparent;
}
button.disabled > :global(svg) {
fill: ${colors.grey500};
}
`}</style>
</button>
)
}
)}

<style jsx>{`
button {
flex-grow: 0;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
vertical-align: bottom;
height: 100%;
padding: 16px 16px 11px;
background-color: transparent;
outline: none;
border: none;
border-bottom: 1px solid ${colors.grey400};
color: ${colors.grey600};
font-size: 14px;
line-height: 20px;
cursor: pointer;
}
:global(.fixed) > button {
flex-grow: 1;
}
button::after {
content: ' ';
display: block;
position: absolute;
bottom: -1px;
inset-inline-start: 0;
height: 4px;
width: 100%;
background-color: transparent;
}
span {
display: inline-flex;
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: fill 150ms ease-in-out;
}
/*focus-visible backwards compatibility for safari: https://css-tricks.com/platform-news-using-focus-visible-bbcs-new-typeface-declarative-shadow-doms-a11y-and-placeholders/*/
button:focus {
outline: 3px solid ${theme.focus};
outline-offset: -3px;
}
button:focus:not(:focus-visible) {
outline: none;
}
button > :global(svg) {
fill: ${colors.grey600};
width: 14px;
height: 14px;
margin: 0 4px 0 0;
}
button:hover {
color: ${colors.grey900};
}
button:hover::after {
background-color: ${colors.grey600};
height: 2px;
}
button:active::after {
background-color: ${colors.grey800};
}
button.selected {
color: ${theme.primary800};
}
button.selected::after {
background-color: ${theme.primary700};
transition: background-color 150ms ease-in-out;
}
button.selected:hover::after {
background-color: ${theme.primary700};
height: 4px;
}
button.selected > :global(svg) {
fill: ${theme.primary700};
}
button.disabled {
color: ${colors.grey500};
cursor: not-allowed;
}
button.disabled:hover,
button.selected:hover {
background-color: transparent;
}
button.disabled > :global(svg) {
fill: ${colors.grey500};
}
`}</style>
</button>
)
}
)

Tab.defaultProps = {
dataTest: 'dhis2-uicore-tab',
Expand All @@ -175,4 +177,4 @@ Tab.propTypes = {
onClick: PropTypes.func,
}

export { Tab }
Tab.displayName = 'Tab'

0 comments on commit aa28c70

Please sign in to comment.