Skip to content

Commit

Permalink
feat(UnderlinePanels): Convert UnderlinePanels to CSS modules behind …
Browse files Browse the repository at this point in the history
…team feature flag (#5357)

* convert UnderlineItem

* convert item list

* additional css module migration

* formatting

* fix selectors

* Migrate Loading Counter

* key off of FF

* add toggle for the tabContainer

* update to take additional optional dependencies

* update resize observer to key off of feature flag

* fix lint issue
  • Loading branch information
randall-krauskopf authored Dec 11, 2024
1 parent 5a8138a commit 161e3fd
Show file tree
Hide file tree
Showing 6 changed files with 440 additions and 128 deletions.
5 changes: 5 additions & 0 deletions .changeset/slimy-chefs-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Convert UnderlinePanels to CSS modules behind feature flags
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.StyledUnderlineWrapper {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: auto;
}

.StyledUnderlineWrapper[data-icons-visible='false'] [data-component='icon'] {
display: none;
}
61 changes: 49 additions & 12 deletions packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {Children, isValidElement, cloneElement, useState, useRef, type FC, type PropsWithChildren} from 'react'
import {TabContainerElement} from '@github/tab-container-element'
import type {IconProps} from '@primer/octicons-react'
import {createComponent} from '../../utils/create-component'
import {
StyledUnderlineItemList,
Expand All @@ -10,11 +11,15 @@ import {
import Box, {type BoxProps} from '../../Box'
import {useId} from '../../hooks'
import {invariant} from '../../utils/invariant'
import type {IconProps} from '@primer/octicons-react'
import {merge, type BetterSystemStyleObject, type SxProp} from '../../sx'
import {defaultSxProp} from '../../utils/defaultSxProp'
import {useResizeObserver, type ResizeObserverEntry} from '../../hooks/useResizeObserver'
import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect'
import {useFeatureFlag} from '../../FeatureFlags'
import classes from './UnderlinePanels.module.css'
import {toggleStyledComponent} from '../../internal/utils/toggleStyledComponent'

const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'

export type UnderlinePanelsProps = {
/**
Expand Down Expand Up @@ -59,6 +64,12 @@ export type PanelProps = Omit<BoxProps, 'as'>

const TabContainerComponent = createComponent(TabContainerElement, 'tab-container')

const StyledTabContainerComponent = toggleStyledComponent(
CSS_MODULES_FEATURE_FLAG,
'tab-container',
TabContainerComponent,
)

const UnderlinePanels: FC<UnderlinePanelsProps> = ({
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
Expand Down Expand Up @@ -102,6 +113,8 @@ const UnderlinePanels: FC<UnderlinePanelsProps> = ({
)
const tabsHaveIcons = tabs.current.some(tab => React.isValidElement(tab) && tab.props.icon)

const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)

// this is a workaround to get the list's width on the first render
const [listWidth, setListWidth] = useState(0)
useIsomorphicLayoutEffect(() => {
Expand All @@ -114,15 +127,19 @@ const UnderlinePanels: FC<UnderlinePanelsProps> = ({

// when the wrapper resizes, check if the icons should be visible
// by comparing the wrapper width to the list width
useResizeObserver((resizeObserverEntries: ResizeObserverEntry[]) => {
if (!tabsHaveIcons) {
return
}

const wrapperWidth = resizeObserverEntries[0].contentRect.width

setIconsVisible(wrapperWidth > listWidth)
}, wrapperRef)
useResizeObserver(
(resizeObserverEntries: ResizeObserverEntry[]) => {
if (!tabsHaveIcons) {
return
}

const wrapperWidth = resizeObserverEntries[0].contentRect.width

setIconsVisible(wrapperWidth > listWidth)
},
wrapperRef,
[enabled],
)

if (__DEV__) {
// only one tab can be selected at a time
Expand All @@ -141,8 +158,28 @@ const UnderlinePanels: FC<UnderlinePanelsProps> = ({
)
}

if (enabled) {
return (
<StyledTabContainerComponent>
<StyledUnderlineWrapper
ref={wrapperRef}
slot="tablist-wrapper"
data-icons-visible={iconsVisible}
sx={sxProp}
className={classes.StyledUnderlineWrapper}
{...props}
>
<StyledUnderlineItemList ref={listRef} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} role="tablist">
{tabs.current}
</StyledUnderlineItemList>
</StyledUnderlineWrapper>
{tabPanels.current}
</StyledTabContainerComponent>
)
}

return (
<TabContainerComponent>
<StyledTabContainerComponent>
<StyledUnderlineWrapper
ref={wrapperRef}
slot="tablist-wrapper"
Expand All @@ -166,7 +203,7 @@ const UnderlinePanels: FC<UnderlinePanelsProps> = ({
</StyledUnderlineItemList>
</StyledUnderlineWrapper>
{tabPanels.current}
</TabContainerComponent>
</StyledTabContainerComponent>
)
}

Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/hooks/useResizeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export interface ResizeObserverEntry {
contentRect: DOMRectReadOnly
}

export function useResizeObserver<T extends HTMLElement>(callback: ResizeObserverCallback, target?: RefObject<T>) {
export function useResizeObserver<T extends HTMLElement>(
callback: ResizeObserverCallback,
target?: RefObject<T>,
depsArray: unknown[] = [],
) {
const savedCallback = useRef(callback)

useLayoutEffect(() => {
Expand All @@ -31,5 +35,6 @@ export function useResizeObserver<T extends HTMLElement>(callback: ResizeObserve
return () => {
observer.disconnect()
}
}, [target])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, ...depsArray])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
.UnderlineWrapper {
display: flex;
/* stylelint-disable-next-line primer/spacing */
padding-inline: var(--stack-padding-normal);
justify-content: flex-start;
align-items: center;

/* make space for the underline */
min-height: var(--control-xlarge-size, 48px);

/* using a box-shadow instead of a border to accomodate 'overflow-y: hidden' on UnderlinePanels */
/* stylelint-disable-next-line primer/box-shadow */
box-shadow: inset 0 -1px var(--borderColor-muted);
}

.UnderlineItemList {
position: relative;
display: flex;
padding: 0;
margin: 0;
white-space: nowrap;
list-style: none;
align-items: center;
gap: 8px;
}

.UnderlineItem {
/* underline tab specific styles */
position: relative;
display: inline-flex;
font: inherit;
font-size: var(--text-body-size-medium);
line-height: var(--text-body-lineHeight-medium, 1.4285);
color: var(--fgColor-default);
text-align: center;
text-decoration: none;
cursor: pointer;
background-color: transparent;
border: 0;
border-radius: var(--borderRadius-medium, var(--borderRadius-small));

/* button resets */
appearance: none;
padding-inline: var(--base-size-8);
padding-block: var(--base-size-6);
align-items: center;

@media (hover: hover) {
&:hover {
text-decoration: none;
background-color: var(--bgColor-neutral-muted);
transition: background-color 0.12s ease-out;
}
}
}

.UnderlineItem:focus {
outline: 2px solid transparent;
/* stylelint-disable-next-line primer/box-shadow */
box-shadow: inset 0 0 0 2px var(--fgColor-accent);

/* where focus-visible is supported, remove the focus box-shadow */
&:not(:focus-visible) {
box-shadow: none;
}
}

.UnderlineItem:focus-visible {
outline: 2px solid transparent;
/* stylelint-disable-next-line primer/box-shadow */
box-shadow: inset 0 0 0 2px var(--fgColor-accent);
}

/* renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected */
.UnderlineItem [data-content]::before {
display: block;
height: 0;
font-weight: var(--base-text-weight-semibold);
white-space: nowrap;
visibility: hidden;
content: attr(data-content);
}

.UnderlineItem [data-component='icon'] {
display: inline-flex;
color: var(--fgColor-muted);
align-items: center;
margin-inline-end: var(--base-size-8);
}

.UnderlineItem [data-component='counter'] {
margin-inline-start: var(--base-size-8);
display: flex;
align-items: center;
}

.UnderlineItem::after {
position: absolute;
right: 50%;

/* TODO: see if we can simplify this positioning */

/* 48px total height / 2 (24px) + 1px */
/* stylelint-disable-next-line primer/spacing */
bottom: calc(50% - calc(var(--control-xlarge-size, var(--base-size-48)) / 2 + 1px));
width: 100%;
height: 2px;
content: '';
background-color: transparent;
border-radius: 0;
transform: translate(50%, -50%);
}

.UnderlineItem[aria-current]:not([aria-current='false']) [data-component='text'],
.UnderlineItem[aria-selected='true'] [data-component='text'] {
font-weight: var(--base-text-weight-semibold);
}

.UnderlineItem[aria-current]:not([aria-current='false'])::after,
.UnderlineItem[aria-selected='true']::after {
/* stylelint-disable-next-line primer/colors */
background-color: var(--underlineNav-borderColor-active, var(--color-primer-border-active, #fd8c73));

@media (forced-colors: active) {
/* Support for Window Force Color Mode https://learn.microsoft.com/en-us/fluent-ui/web-components/design-system/high-contrast */
background-color: LinkText;
}
}

.LoadingCounter {
display: inline-block;
width: 1.5rem;
height: 1rem; /* 16px */
background-color: var(--bgColor-neutral-muted);
border-color: var(--borderColor-default);
/* stylelint-disable-next-line primer/borders */
border-radius: 20px;
animation: loadingCounterKeyFrames 1.2s ease-in-out infinite alternate;
}

@keyframes loadingCounterKeyFrames {
from {
opacity: 1;
}

to {
opacity: 0.2;
}
}
Loading

0 comments on commit 161e3fd

Please sign in to comment.