Skip to content

Commit

Permalink
feat(tabs): display highlight on tabs when mouse-in (#696)
Browse files Browse the repository at this point in the history
* feat(use-classes): add tool to handle class name strings

* feat(highlight): add component follows the position of the current component

* feat(tabs): display highlight on tabs when mouse-in

* docs(tabs): append props

* test: fix paths for testcase
  • Loading branch information
unix authored Feb 1, 2022
1 parent 9453b47 commit e632c3e
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
},

testRegex: '.*\\.test\\.(j|t)sx?$',
// testRegex: 'modal\\/.*\\.test\\.(j|t)sx?$',
// testRegex: 'use-classes\\/.*\\.test\\.(j|t)sx?$',

collectCoverageFrom: [
'components/**/*.{ts,tsx}',
Expand Down
1 change: 1 addition & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,4 @@ export { default as useClickAway } from './use-click-away'
export { default as useCurrentState } from './use-current-state'
export { default as CssBaseline } from './css-baseline'
export { default as useTheme } from './use-theme'
export { default as useClasses } from './use-classes'
38 changes: 8 additions & 30 deletions components/shared/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CssTransition from './css-transition'
import useClickAnyWhere from '../utils/use-click-anywhere'
import useDOMObserver from '../utils/use-dom-observer'
import useWarning from '../utils/use-warning'
import { getRefRect } from '../utils/layouts'

interface Props {
parent?: MutableRefObject<HTMLElement | null> | undefined
Expand All @@ -28,34 +29,6 @@ const defaultRect: ReactiveDomReact = {
width: 0,
}

const getOffset = (el?: HTMLElement | null | undefined) => {
if (!el)
return {
top: 0,
left: 0,
}
const { top, left } = el.getBoundingClientRect()
return { top, left }
}

const getRect = (
ref: MutableRefObject<HTMLElement | null>,
getContainer?: () => HTMLElement | null,
): ReactiveDomReact => {
if (!ref || !ref.current) return defaultRect
const rect = ref.current.getBoundingClientRect()
const container = getContainer ? getContainer() : null
const scrollElement = container || document.documentElement
const { top: offsetTop, left: offsetLeft } = getOffset(container)

return {
...rect,
width: rect.width || rect.right - rect.left,
top: rect.bottom + scrollElement.scrollTop - offsetTop,
left: rect.left + scrollElement.scrollLeft - offsetLeft,
}
}

const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
({ children, parent, visible, disableMatchWidth, getPopupContainer }) => {
const el = usePortal('dropdown', getPopupContainer)
Expand All @@ -76,13 +49,18 @@ const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
}

const updateRect = () => {
const { top, left, right, width: nativeWidth } = getRect(parent, getPopupContainer)
const {
top,
left,
right,
width: nativeWidth,
} = getRefRect(parent, getPopupContainer)
setRect({ top, left, right, width: nativeWidth })
}

useResize(updateRect)
useClickAnyWhere(() => {
const { top, left } = getRect(parent, getPopupContainer)
const { top, left } = getRefRect(parent, getPopupContainer)
const shouldUpdatePosition = top !== rect.top || left !== rect.left
if (!shouldUpdatePosition) return
updateRect()
Expand Down
63 changes: 63 additions & 0 deletions components/shared/highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useMemo, useRef } from 'react'
import { isUnplacedRect, ReactiveDomReact } from '../utils/layouts'
import usePrevious from '../utils/use-previous'
import useTheme from '../use-theme'

export type HighlightProps = {
rect: ReactiveDomReact
visible?: boolean
hoverHeightRatio?: number
hoverWidthRatio?: number
}

type HighlightPosition = {
width: string
left: string
height: string
top: string
transition: string
}

const Highlight: React.FC<HighlightProps> = ({
rect,
visible,
hoverHeightRatio = 1,
hoverWidthRatio = 1,
...props
}) => {
const theme = useTheme()
const ref = useRef<HTMLDivElement | null>(null)
const isFirstVisible = usePrevious<boolean>(isUnplacedRect(rect))
const position = useMemo<HighlightPosition>(() => {
const width = rect.width * hoverWidthRatio
const height = rect.height * hoverHeightRatio
return {
width: `${width}px`,
left: `${rect.left + (rect.width - width) / 2}px`,
height: `${height}px`,
top: `${rect.elementTop + (rect.height - height) / 2}px`,
transition: isFirstVisible ? 'opacity' : 'opacity, width, left',
}
}, [rect, hoverWidthRatio, hoverHeightRatio])

return (
<div ref={ref} className="highlight" {...props}>
<style jsx>{`
.highlight {
background: ${theme.palette.accents_2};
position: absolute;
border-radius: 5px;
width: ${position.width};
left: ${position.left};
height: ${position.height};
top: ${position.top};
opacity: ${visible ? 0.8 : 0};
transition: 0.15s ease;
transition-property: ${position.transition};
}
`}</style>
</div>
)
}

export default Highlight
2 changes: 1 addition & 1 deletion components/table/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mount } from 'enzyme'
import { Table, Code } from 'components'
import { nativeEvent, updateWrapper } from 'tests/utils'
import { act } from 'react-dom/test-utils'
import { TableColumnRender } from 'components/table/table-types'
import { TableColumnRender } from '../table-types'

const data = [
{ property: 'type', description: 'Content type', default: '-' },
Expand Down
83 changes: 33 additions & 50 deletions components/tabs/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Tabs should render correctly 1`] = `
"<div class=\\"tabs \\"><header><div class=\\"scroll-container \\"><div class=\\"tab \\" role=\\"button\\">label1<style>
"<div class=\\"tabs \\"><header><div class=\\"highlight\\"><style>
.highlight {
background: #eaeaea;
position: absolute;
border-radius: 5px;
width: 0px;
left: -1000px;
height: 0px;
top: -1000px;
opacity: 0;
transition: 0.15s ease;
transition-property: opacity, width, left;
}
</style></div><div class=\\"scroll-container \\"><div class=\\"tab\\" role=\\"button\\" data-geist=\\"tab-item\\">label1<style>
.tab {
position: relative;
box-sizing: border-box;
Expand All @@ -26,21 +39,6 @@ exports[`Tabs should render correctly 1`] = `
--tabs-item-hover-left: calc(-1 * calc(0.28 * 16px));
--tabs-item-hover-right: calc(-1 * calc(0.28 * 16px));
}
.tab:before {
position: absolute;
top: calc(0.48 * 16px);
left: var(--tabs-item-hover-left);
right: var(--tabs-item-hover-right);
bottom: calc(0.48 * 16px);
content: '';
z-index: -1;
transition: opacity 150ms ease;
background-color: transparent;
border-radius: 4px;
}
.tab:hover::before {
background-color: #eaeaea;
}
.tab:hover {
color: #000;
}
Expand Down Expand Up @@ -76,7 +74,7 @@ exports[`Tabs should render correctly 1`] = `
color: #999;
cursor: not-allowed;
}
</style></div><div class=\\"tab \\" role=\\"button\\">label2<style>
</style></div><div class=\\"tab\\" role=\\"button\\" data-geist=\\"tab-item\\">label2<style>
.tab {
position: relative;
box-sizing: border-box;
Expand All @@ -101,21 +99,6 @@ exports[`Tabs should render correctly 1`] = `
--tabs-item-hover-left: calc(-1 * calc(0.28 * 16px));
--tabs-item-hover-right: calc(-1 * calc(0.28 * 16px));
}
.tab:before {
position: absolute;
top: calc(0.48 * 16px);
left: var(--tabs-item-hover-left);
right: var(--tabs-item-hover-right);
bottom: calc(0.48 * 16px);
content: '';
z-index: -1;
transition: opacity 150ms ease;
background-color: transparent;
border-radius: 4px;
}
.tab:hover::before {
background-color: #eaeaea;
}
.tab:hover {
color: #000;
}
Expand Down Expand Up @@ -175,8 +158,9 @@ exports[`Tabs should render correctly 1`] = `
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: left;
border-bottom: 1px solid #eaeaea;
padding-left: 20px;
padding-left: 12px;
}
header::-webkit-scrollbar {
display: none;
Expand All @@ -191,7 +175,20 @@ exports[`Tabs should render correctly 1`] = `
`;
exports[`Tabs should work correctly with different styles 1`] = `
"<div class=\\"tabs \\"><header><div class=\\"scroll-container hide-divider\\"><div class=\\"tab \\" role=\\"button\\">label1<style>
"<div class=\\"tabs \\"><header><div class=\\"highlight\\"><style>
.highlight {
background: #eaeaea;
position: absolute;
border-radius: 5px;
width: 0px;
left: -1000px;
height: 0px;
top: -1000px;
opacity: 0;
transition: 0.15s ease;
transition-property: opacity, width, left;
}
</style></div><div class=\\"scroll-container hide-divider\\"><div class=\\"tab\\" role=\\"button\\" data-geist=\\"tab-item\\">label1<style>
.tab {
position: relative;
box-sizing: border-box;
Expand All @@ -216,21 +213,6 @@ exports[`Tabs should work correctly with different styles 1`] = `
--tabs-item-hover-left: calc(-1 * calc(0.28 * 16px));
--tabs-item-hover-right: calc(-1 * calc(0.28 * 16px));
}
.tab:before {
position: absolute;
top: calc(0.48 * 16px);
left: var(--tabs-item-hover-left);
right: var(--tabs-item-hover-right);
bottom: calc(0.48 * 16px);
content: '';
z-index: -1;
transition: opacity 150ms ease;
background-color: transparent;
border-radius: 4px;
}
.tab:hover::before {
background-color: #eaeaea;
}
.tab:hover {
color: #000;
}
Expand Down Expand Up @@ -290,8 +272,9 @@ exports[`Tabs should work correctly with different styles 1`] = `
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: left;
border-bottom: 1px solid #eaeaea;
padding-left: 20px;
padding-left: 12px;
}
header::-webkit-scrollbar {
display: none;
Expand Down
5 changes: 4 additions & 1 deletion components/tabs/tabs-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { CSSProperties } from 'react'
import React, { CSSProperties, MouseEvent } from 'react'

export type TabsInternalCellProps = {
onClick: (value: string) => void
onMouseOver: (e: MouseEvent<HTMLDivElement>) => void
activeClassName?: string
activeStyle?: CSSProperties
}

export type TabsInternalCell = React.FC<TabsInternalCellProps>
Expand Down
48 changes: 26 additions & 22 deletions components/tabs/tabs-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useEffect, useMemo } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { TabsInternalCellProps, useTabsContext } from './tabs-context'
import useTheme from '../use-theme'
import useScale, { withScale } from '../use-scale'
import useClasses from '../use-classes'

interface Props {
label: string | React.ReactNode
Expand All @@ -23,24 +24,42 @@ const TabsItemComponent: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
disabled,
}: React.PropsWithChildren<TabsItemProps> & typeof defaultProps) => {
const { SCALES } = useScale()
const { register, currentValue, leftSpace } = useTabsContext()
const { register, currentValue } = useTabsContext()
const isActive = useMemo(() => currentValue === value, [currentValue, value])

const TabsInternalCell: React.FC<TabsInternalCellProps> = ({ onClick }) => {
const TabsInternalCell: React.FC<TabsInternalCellProps> = ({
onClick,
onMouseOver,
activeClassName,
activeStyle,
}) => {
const theme = useTheme()
const ref = useRef<HTMLDivElement | null>(null)
const { currentValue } = useTabsContext()
const active = currentValue === value
const classes = useClasses(
'tab',
{
active,
disabled,
},
activeClassName,
)
const clickHandler = () => {
if (disabled) return
onClick && onClick(value)
}

return (
<div
className={`tab ${value === currentValue ? 'active' : ''} ${
disabled ? 'disabled' : ''
}`}
ref={ref}
className={classes}
role="button"
key={value}
onClick={clickHandler}>
onMouseOver={onMouseOver}
onClick={clickHandler}
style={active ? activeStyle : {}}
data-geist="tab-item">
{label}
<style jsx>{`
.tab {
Expand All @@ -67,21 +86,6 @@ const TabsItemComponent: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
--tabs-item-hover-left: calc(-1 * ${SCALES.pl(0.28)});
--tabs-item-hover-right: calc(-1 * ${SCALES.pr(0.28)});
}
.tab:before {
position: absolute;
top: ${SCALES.pt(0.48)};
left: ${leftSpace ? 'var(--tabs-item-hover-left)' : 0};
right: ${leftSpace ? 'var(--tabs-item-hover-right)' : 0};
bottom: ${SCALES.pb(0.48)};
content: '';
z-index: -1;
transition: opacity 150ms ease;
background-color: transparent;
border-radius: 4px;
}
.tab:hover::before {
background-color: ${theme.palette.accents_2};
}
.tab:hover {
color: ${theme.palette.foreground};
}
Expand Down
Loading

0 comments on commit e632c3e

Please sign in to comment.