diff --git a/packages/block-editor/src/components/inserter/category-tabs/index.js b/packages/block-editor/src/components/inserter/category-tabs/index.js index 6a02cf1a170c45..6d8f5fcbca1334 100644 --- a/packages/block-editor/src/components/inserter/category-tabs/index.js +++ b/packages/block-editor/src/components/inserter/category-tabs/index.js @@ -2,14 +2,10 @@ * WordPress dependencies */ import { usePrevious, useReducedMotion } from '@wordpress/compose'; -import { isRTL } from '@wordpress/i18n'; import { - __experimentalHStack as HStack, - FlexBlock, privateApis as componentsPrivateApis, __unstableMotion as motion, } from '@wordpress/components'; -import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; /** * Internal dependencies @@ -55,18 +51,12 @@ function CategoryTabs( { - - { category.label } - - + { category.label } ) ) } diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index f3fa8d1e7df04b..9e727b13795249 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -214,55 +214,15 @@ $block-inserter-tabs-height: 44px; .block-editor-inserter__media-tabs-container, .block-editor-inserter__block-patterns-tabs-container { + flex-grow: 1; padding: $grid-unit-20; - height: 100%; display: flex; flex-direction: column; justify-content: space-between; } .block-editor-inserter__category-tablist { - display: flex; - flex-direction: column; - border: none; margin-bottom: $grid-unit-10; - // Push the listitem wrapping the "explore" button to the bottom of the panel. - div[role="listitem"]:last-child { - margin-top: auto; - } - - // Temporarily disable the component's indicator animation. - // TODO: remove in favor of using the native component's styles and behavior, - // see https://github.com/WordPress/gutenberg/pull/62879#issuecomment-2219720582 - &[aria-orientation="vertical"]::after { - content: none; - } - - .block-editor-inserter__category-tab { - // Account for the icon on the right so that it's visually balanced. - padding: $grid-unit-10 $grid-unit-05 $grid-unit-10 $grid-unit-15; - text-align: left; - font-weight: inherit; - display: block; - position: relative; - height: auto; - - &[aria-selected="true"] { - color: var(--wp-admin-theme-color); - - .components-flex-item { - filter: brightness(0.95); - } - - svg { - fill: var(--wp-admin-theme-color); - } - } - - &::before { - display: none; - } - } } .block-editor-inserter__category-panel { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 0bf9776d01a5de..4ae124347187e7 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,10 @@ - `RangeControl`: do not tooltip contents to the DOM when not shown ([#65875](https://github.com/WordPress/gutenberg/pull/65875)). - `Tabs`: fix skipping indication animation glitch ([#65878](https://github.com/WordPress/gutenberg/pull/65878)). +### Enhancements + +- `Tabs`: revamped vertical orientation styles ([#65387](https://github.com/WordPress/gutenberg/pull/65387)). + ## 28.9.0 (2024-10-03) ### Bug Fixes diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index 4f6b4a4c7c8dcb..926abc3e34b102 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -9,18 +9,15 @@ import * as Ariakit from '@ariakit/react'; */ import { COLORS, CONFIG } from '../utils'; import { space } from '../utils/space'; +import Icon from '../icon'; export const TabListWrapper = styled.div` - position: relative; display: flex; align-items: stretch; - flex-direction: row; - text-align: center; overflow-x: auto; &[aria-orientation='vertical'] { flex-direction: column; - text-align: start; } :where( [aria-orientation='horizontal'] ) { @@ -40,11 +37,12 @@ export const TabListWrapper = styled.div` @media not ( prefers-reduced-motion ) { &[data-indicator-animated]::before { - transition-property: transform; + transition-property: transform, border-radius, border-block; transition-duration: 0.2s; transition-timing-function: ease-out; } } + position: relative; &::before { content: ''; position: absolute; @@ -59,7 +57,7 @@ export const TabListWrapper = styled.div` /* Using a large value to avoid antialiasing rounding issues when scaling in the transform, see: https://stackoverflow.com/a/52159123 */ --antialiasing-factor: 100; - &:not( [aria-orientation='vertical'] ) { + &[aria-orientation='horizontal'] { --fade-width: 4rem; --fade-gradient-base: transparent 0%, black var( --fade-width ); --fade-gradient-composed: var( --fade-gradient-base ), black 60%, @@ -104,40 +102,67 @@ export const TabListWrapper = styled.div` ${ COLORS.theme.accent }; } } - &[aria-orientation='vertical']::before { - top: 0; - left: 0; - width: 100%; - height: calc( var( --antialiasing-factor ) * 1px ); - transform: translateY( calc( var( --selected-top, 0 ) * 1px ) ) - scaleY( + &[aria-orientation='vertical'] { + &::before { + /* Adjusting the border radius to match the scaling in the y axis. */ + border-radius: ${ CONFIG.radiusSmall } / calc( - var( --selected-height, 0 ) / var( --antialiasing-factor ) - ) + ${ CONFIG.radiusSmall } / + ( + var( --selected-height, 0 ) / + var( --antialiasing-factor ) + ) + ); + top: 0; + left: 0; + width: 100%; + height: calc( var( --antialiasing-factor ) * 1px ); + transform: translateY( calc( var( --selected-top, 0 ) * 1px ) ) + scaleY( + calc( + var( --selected-height, 0 ) / + var( --antialiasing-factor ) + ) + ); + background-color: color-mix( + in srgb, + ${ COLORS.theme.accent }, + transparent 96% ); - background-color: ${ COLORS.theme.gray[ 100 ] }; + } + &[data-select-on-move='true']:has( + :is( :focus-visible, [data-focus-visible] ) + )::before { + box-sizing: border-box; + border: var( --wp-admin-border-width-focus ) solid + ${ COLORS.theme.accent }; + /* Adjusting the border width to match the scaling in the y axis. */ + border-block-width: calc( + var( --wp-admin-border-width-focus, 1px ) / + ( + var( --selected-height, 0 ) / + var( --antialiasing-factor ) + ) + ); + } } `; export const Tab = styled( Ariakit.Tab )` & { - scroll-margin: 24px; - flex-grow: 1; - flex-shrink: 0; - display: inline-flex; - align-items: center; - position: relative; + /* Resets */ border-radius: 0; - height: ${ space( 12 ) }; background: transparent; border: none; box-shadow: none; + + flex: 1 0 auto; + white-space: nowrap; + display: flex; + align-items: center; cursor: pointer; - line-height: 1.2; // Some languages characters e.g. Japanese may have a native higher line-height. - padding: ${ space( 4 ) }; - margin-left: 0; - font-weight: 500; - text-align: inherit; + line-height: 1.2; // Characters in some languages (e.g. Japanese) may have a native higher line-height. + font-weight: 400; color: ${ COLORS.theme.foreground }; &[aria-disabled='true'] { @@ -145,24 +170,19 @@ export const Tab = styled( Ariakit.Tab )` color: ${ COLORS.ui.textDisabled }; } - &:not( [aria-disabled='true'] ):hover { + &:not( [aria-disabled='true'] ):is( :hover, [data-focus-visible] ) { color: ${ COLORS.theme.accent }; } &:focus:not( :disabled ) { - position: relative; box-shadow: none; outline: none; } - // Focus. + // Focus indicator. + position: relative; &::after { - content: ''; position: absolute; - top: ${ space( 3 ) }; - right: ${ space( 3 ) }; - bottom: ${ space( 3 ) }; - left: ${ space( 3 ) }; pointer-events: none; // Draw the indicator. @@ -175,23 +195,69 @@ export const Tab = styled( Ariakit.Tab )` opacity: 0; @media not ( prefers-reduced-motion ) { - transition: opacity 0.1s linear; + transition: opacity 0.15s 0.15s linear; } } - &:focus-visible::after { + &[data-focus-visible]::after { opacity: 1; } } + [aria-orientation='horizontal'] & { + padding-inline: ${ space( 4 ) }; + height: ${ space( 12 ) }; + text-align: center; + scroll-margin: 24px; + + &::after { + content: ''; + inset: ${ space( 3 ) }; + } + } + [aria-orientation='vertical'] & { - min-height: ${ space( - 10 - ) }; // Avoid fixed height to allow for long strings that go in multiple lines. + padding: ${ space( 2 ) } ${ space( 3 ) }; + min-height: ${ space( 10 ) }; + text-align: start; + + &[aria-selected='true'] { + color: ${ COLORS.theme.accent }; + fill: currentColor; + } + } + [aria-orientation='vertical'][data-select-on-move='false'] &::after { + content: ''; + inset: var( --wp-admin-border-width-focus ); } +`; + +export const TabChildren = styled.span` + flex-grow: 1; +`; +export const TabChevron = styled( Icon )` + flex-shrink: 0; + margin-inline-end: ${ space( -1 ) }; [aria-orientation='horizontal'] & { - justify-content: center; + display: none; + } + opacity: 0; + [role='tab']:is( [aria-selected='true'], [data-focus-visible], :hover ) & { + opacity: 1; + } + // The chevron is transitioned into existence when selectOnMove is enabled, + // because otherwise it looks jarring, as it shows up outside of the focus + // indicator that's being animated at the same time. + @media not ( prefers-reduced-motion ) { + [data-select-on-move='true'] + [role='tab']:is( [aria-selected='true'], ) + & { + transition: opacity 0.3s ease-in; + } + } + &:dir( rtl ) { + rotate: 180deg; } `; @@ -201,7 +267,7 @@ export const TabPanel = styled( Ariakit.TabPanel )` outline: none; } - &:focus-visible { + &[data-focus-visible] { box-shadow: 0 0 0 var( --wp-admin-border-width-focus ) ${ COLORS.theme.accent }; // Windows high contrast mode. diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index e1aa85c636cdd1..29f6111adc8397 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -10,8 +10,13 @@ import { forwardRef } from '@wordpress/element'; import type { TabProps } from './types'; import warning from '@wordpress/warning'; import { useTabsContext } from './context'; -import { Tab as StyledTab } from './styles'; +import { + Tab as StyledTab, + TabChildren as StyledTabChildren, + TabChevron as StyledTabChevron, +} from './styles'; import type { WordPressComponentProps } from '../context'; +import { chevronRight } from '@wordpress/icons'; export const Tab = forwardRef< HTMLButtonElement, @@ -33,7 +38,8 @@ export const Tab = forwardRef< render={ render } { ...otherProps } > - { children } + { children } + ); } ); diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index a861d3294aae66..512a3eb6724289 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -115,6 +115,7 @@ export const TabList = forwardRef< render={ } onBlur={ onBlur } tabIndex={ -1 } + data-select-on-move={ selectOnMove ? 'true' : 'false' } { ...otherProps } className={ clsx( overflow.first && 'is-overflowing-first', diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 3ec7318ab89e78..8f63b57fda657f 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -123,7 +123,14 @@ test.describe( 'a11y (@firefox, @webkit)', () => { test( 'should make the modal content focusable when it is scrollable', async ( { page, pageUtils, + browserName, } ) => { + // eslint-disable-next-line playwright/no-skipped-test + test.skip( + browserName === 'webkit', + 'Known bug with focus order in Safari.' + ); + // Note: this test depends on a particular viewport height to determine whether or not // the modal content is scrollable. If this tests fails and needs to be debugged locally, // double-check the viewport height when running locally versus in CI. Additionally,