Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabs: indicator animation #60560

Merged
merged 27 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
898f29d
Initial tab indicator animation implementation
DaniGuardiola Apr 8, 2024
ce628d3
Add changelog entry
DaniGuardiola Apr 8, 2024
7e43c02
Minor tweak
DaniGuardiola Apr 8, 2024
47f029d
Merge branch 'trunk' into tabs-animation
DaniGuardiola Apr 8, 2024
0a66bbd
Fix downstream issues.
DaniGuardiola Apr 12, 2024
3dae51c
Use ResizeObserver.
DaniGuardiola Apr 12, 2024
a0b0581
Add width transition.
DaniGuardiola Apr 12, 2024
a5d5d27
Simplify and use framer motion
DaniGuardiola Apr 17, 2024
22909d8
vertical indicator
DaniGuardiola Apr 17, 2024
77d208f
Revert to previous implementation.
DaniGuardiola Apr 22, 2024
e9ccc02
Fix bug due to some animations breaking measurement of the tab element.
DaniGuardiola Apr 22, 2024
283f109
Abstracted and fixed all previous issues.
DaniGuardiola Apr 22, 2024
b3ab95f
Follow naming convention for classes.
DaniGuardiola Apr 23, 2024
45d8215
Support vertical orientation + misc fixes and improvements.
DaniGuardiola Apr 24, 2024
f364cc2
Clean up styles a bit.
DaniGuardiola Apr 24, 2024
f0a5a65
Merge branch 'trunk' into tabs-animation
DaniGuardiola Apr 24, 2024
539be1e
Better focus ring animation + minor style cleanup.
DaniGuardiola Apr 24, 2024
fa9f34e
Fix changelog (oops).
DaniGuardiola Apr 24, 2024
16fac9f
Actually fix changelog.
DaniGuardiola Apr 24, 2024
6b2f541
Remove deprecated `reduceMotion` utility.
DaniGuardiola Apr 26, 2024
aa6e66b
Merge branch 'trunk' into tabs-animation
DaniGuardiola May 22, 2024
2f14f6d
Fix open/closed
DaniGuardiola May 22, 2024
02099ac
Add vertical tabs story
DaniGuardiola May 22, 2024
2c4d085
Move ResizeObserver unobserve to effect cleanup
DaniGuardiola May 22, 2024
59523e4
Merge branch 'trunk' of https://github.com/WordPress/gutenberg into t…
DaniGuardiola May 23, 2024
bed8a36
Remove outdated type cast.
DaniGuardiola May 23, 2024
f0b896b
Hide vertical indicator for now.
DaniGuardiola May 23, 2024
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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Enhancements

- `Tabs`: Animate indicator ([#60560](https://github.com/WordPress/gutenberg/pull/60560)).
- `ExternalLink`: Use unicode arrow instead of svg icon ([#60255](https://github.com/WordPress/gutenberg/pull/60255)).
- `ProgressBar`: Move the indicator width styles from emotion to a CSS variable ([#60388](https://github.com/WordPress/gutenberg/pull/60388)).
- `Text`: Add `text-wrap: pretty;` to improve wrapping. ([#60164](https://github.com/WordPress/gutenberg/pull/60164)).
Expand Down
49 changes: 21 additions & 28 deletions packages/components/src/tabs/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,33 @@ import { space } from '../utils/space';
import { reduceMotion } from '../utils/reduce-motion';

export const TabListWrapper = styled.div`
position: relative;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
display: flex;
align-items: stretch;
flex-direction: row;
&[aria-orientation='vertical'] {
flex-direction: column;
}
&::after {
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
content: '';
position: absolute;
left: var( --indicator-left );
bottom: 0;
width: var( --indicator-width );
height: 0;
border-bottom: var( --wp-admin-border-width-focus ) solid
${ COLORS.theme.accent };
pointer-events: none;
@media not ( prefers-reduced-motion: reduce ) {
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
transition-property: left width;
transition-duration: 0.2s;
transition-timing-function: ease-out;
}

// Windows high contrast mode.
outline: 2px solid transparent;
outline-offset: -1px;
}
`;

export const Tab = styled( Ariakit.Tab )`
Expand Down Expand Up @@ -51,34 +72,6 @@ export const Tab = styled( Ariakit.Tab )`
outline: none;
}

// Tab indicator
&::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;

// Draw the indicator.
background: ${ COLORS.theme.accent };
height: calc( 0 * var( --wp-admin-border-width-focus ) );
border-radius: 0;

// Animation
transition: all 0.1s linear;
${ reduceMotion( 'transition' ) };
}

// Active.
&[aria-selected='true']::after {
height: calc( 1 * var( --wp-admin-border-width-focus ) );

// Windows high contrast mode.
outline: 2px solid transparent;
outline-offset: -1px;
}

// Focus.
&::before {
content: '';
Expand Down
52 changes: 50 additions & 2 deletions packages/components/src/tabs/tablist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as Ariakit from '@ariakit/react';
* WordPress dependencies
*/
import warning from '@wordpress/warning';
import { forwardRef } from '@wordpress/element';
import { forwardRef, useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -17,19 +17,61 @@ import type { TabListProps } from './types';
import { useTabsContext } from './context';
import { TabListWrapper } from './styles';
import type { WordPressComponentProps } from '../context';
import type { CSSProperties } from 'react';

export const TabList = forwardRef<
HTMLDivElement,
WordPressComponentProps< TabListProps, 'div', false >
>( function TabList( { children, ...otherProps }, ref ) {
const context = useTabsContext();

const [ indicatorPosition, setIndicatorPosition ] = useState( {
left: 0,
width: 0,
} );
const selectedId = context?.store.useState( 'selectedId' );
const selectedTabEl = context?.store.item( selectedId )?.element;
const resizeObserverRef = useRef< ResizeObserver >();
const observedElementRef = useRef< HTMLElement >();

useEffect( () => {
if ( selectedTabEl === observedElementRef.current ) return;
observedElementRef.current = selectedTabEl ?? undefined;

function updateIndicator( element: HTMLElement ) {
setIndicatorPosition( {
left: element.offsetLeft,
width: element.getBoundingClientRect().width,
} );
}

// Set up a ResizeObserver.
if ( ! resizeObserverRef.current ) {
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
resizeObserverRef.current = new ResizeObserver( () => {
if ( observedElementRef.current )
updateIndicator( observedElementRef.current );
} );
}
const { current: resizeObserver } = resizeObserverRef;

// Unobserve previous element.
const { current: observedElement } = observedElementRef;
if ( observedElement ) resizeObserver.unobserve( observedElement );
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

// Observe new element.
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
if ( selectedTabEl ) {
updateIndicator( selectedTabEl );
resizeObserver.observe( selectedTabEl );
}
}, [ selectedTabEl ] );
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

if ( ! context ) {
warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' );
return null;
}
const { store } = context;

const { selectedId, activeId, selectOnMove } = store.useState();
const { activeId, selectOnMove } = store.useState();
const { setActiveId } = store;

const onBlur = () => {
Expand All @@ -50,6 +92,12 @@ export const TabList = forwardRef<
<Ariakit.TabList
ref={ ref }
store={ store }
style={
{
'--indicator-left': `${ indicatorPosition.left }px`,
'--indicator-width': `${ indicatorPosition.width }px`,
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
} as CSSProperties
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
}
render={ <TabListWrapper /> }
onBlur={ onBlur }
{ ...otherProps }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
$vertical-tabs-width: 160px;

.preferences__tabs-tablist {
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
position: absolute;
position: absolute !important;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
top: $header-height + $grid-unit-30;
// Aligns button text instead of button box.
left: $grid-unit-20;
width: $vertical-tabs-width;

&::after {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we need to handle in the component itself? Perhaps if we provide an API to declare the indicator to be optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an escape hatch and it's only used here. You could also say the same about everything else that's being customized or overriden here (a lot). I would advise against supporting this to prevent configuration-creep from building up. If this pattern becomes "official" or, at least, common enough, then I would agree.

At the root of this I think the problem is that Tabs from @wordpress/components is not what was needed here. Rather, it seems like we need tabs that look a certain way, and for that it seems like using Ariakit with the styles wanted for this interface is the way to go.

content: none !important;
}
}

.preferences__tabs-tab {
Expand All @@ -19,10 +22,6 @@ $vertical-tabs-width: 160px;
font-weight: 500;
}

&[aria-selected="true"]::after {
content: none;
}

&[role="tab"]:focus:not(:disabled) {
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
// Windows high contrast mode.
Expand Down
Loading