diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 39948051639232..932cb497f1d06a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,9 +6,15 @@ - Improved performance of TypeScript types for internal polymorphic `WordPressComponent` component type ([#80364](https://github.com/WordPress/gutenberg/pull/80364)). +### Breaking Changes + +- Components that compose Emotion style fragments with `cx()` should pass source-order-dependent fragments in a single `css()` call. Passing separate fragments can change override order after the following components stopped rendering styles through Emotion: + - `ToggleGroupControl` ([#80381](https://github.com/WordPress/gutenberg/pull/80381)) + ### Internal - `InputControl`, `SelectControl`, `CustomSelectControl`: Remove obsolete `__unstable-large` from the public `size` type. The value continues to work at runtime, and is equivalent to the `default` size. ([#80081](https://github.com/WordPress/gutenberg/pull/80081)). +- `ToggleGroupControl`: Migrate styles from Emotion to SCSS Modules and use WPDS tokens for migrated visual values ([#80381](https://github.com/WordPress/gutenberg/pull/80381)). - Update `exports` to use subpath patterns instead of deprecated trailing `/` folder mappings ([#80270](https://github.com/WordPress/gutenberg/pull/80270)). ## 37.0.0 (2026-07-14) diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index 879e54b2dac95f..0b330a42da9206 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -27,210 +27,6 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = padding: 0; } -.emotion-6 { - background: var(--wp-components-color-background, var(--wpds-color-background-surface-neutral-strong, #fff)); - border: 1px solid transparent; - border-radius: 2px; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - height: 40px; - min-width: 0; - position: relative; -} - -.emotion-6:hover { - border-color: #ccc; -} - -.emotion-6:focus-within { - z-index: 1; - outline: 1.5px solid var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); - outline-offset: 1px; -} - -@media not ( prefers-reduced-motion ) { - .emotion-6[data-indicator-animated]::before { - transition-property: transform,border-radius; - transition-duration: 0.2s; - transition-timing-function: ease-out; - } -} - -.emotion-6::before { - content: ''; - position: absolute; - pointer-events: none; - background: var(--wp-components-color-gray-100, var(--wpds-color-background-surface-neutral, #fcfcfc)); - border: 1px solid var(--wp-components-color-gray-700, var(--wpds-color-foreground-content-neutral-weak, #707070)); - outline: 2px solid transparent; - outline-offset: -3px; - border-radius: 2px; - top: -1px; - left: -2px; - width: calc( calc( var( --selected-width, 0 ) * 1px ) + 2px ); - height: calc( calc( var( --selected-height, 0 ) * 1px ) + 2px ); - -webkit-transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ); - -moz-transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ); - -ms-transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ); - transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ); - opacity: min( - 1, - max( 0, var( --selected-width, 0 ), var( --selected-height, 0 ) ) - ); -} - -.emotion-7 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - max-width: 100%; - min-width: 0; - position: relative; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-9 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - appearance: none; - background: transparent; - border: none; - border-radius: 1px; - color: var(--wp-components-color-gray-700, var(--wpds-color-foreground-content-neutral-weak, #707070)); - fill: currentColor; - cursor: pointer; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - font-family: inherit; - height: 100%; - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; - line-height: 100%; - outline: none; - padding: 0 12px; - position: relative; - text-align: center; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - width: 100%; - z-index: 2; - height: 38px; - aspect-ratio: 1; - padding-left: 0; - padding-right: 0; - color: var(--wp-components-color-foreground, var(--wpds-color-foreground-content-neutral, #1e1e1e)); - font-weight: 600; -} - -@media not ( prefers-reduced-motion ) { - .emotion-9 { - -webkit-transition: color 160ms linear,font-weight 60ms linear; - transition: color 160ms linear,font-weight 60ms linear; - } -} - -.emotion-9::-moz-focus-inner { - border: 0; -} - -.emotion-9[disabled], -.emotion-9[aria-disabled='true'] { - opacity: 0.4; - cursor: default; -} - -.emotion-9:hover:not( [disabled] ):not( [aria-disabled='true'] ) { - color: var(--wp-components-color-foreground, var(--wpds-color-foreground-content-neutral, #1e1e1e)); -} - -.emotion-10 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - font-size: 13px; - line-height: 1; -} - -.emotion-14 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - appearance: none; - background: transparent; - border: none; - border-radius: 1px; - color: var(--wp-components-color-gray-700, var(--wpds-color-foreground-content-neutral-weak, #707070)); - fill: currentColor; - cursor: pointer; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - font-family: inherit; - height: 100%; - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; - line-height: 100%; - outline: none; - padding: 0 12px; - position: relative; - text-align: center; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - width: 100%; - z-index: 2; - height: 38px; - aspect-ratio: 1; - padding-left: 0; - padding-right: 0; -} - -@media not ( prefers-reduced-motion ) { - .emotion-14 { - -webkit-transition: color 160ms linear,font-weight 60ms linear; - transition: color 160ms linear,font-weight 60ms linear; - } -} - -.emotion-14::-moz-focus-inner { - border: 0; -} - -.emotion-14[disabled], -.emotion-14[aria-disabled='true'] { - opacity: 0.4; - cursor: default; -} - -.emotion-14:hover:not( [disabled] ):not( [aria-disabled='true'] ) { - color: var(--wp-components-color-foreground, var(--wpds-color-foreground-content-neutral, #1e1e1e)); -} -
) : ( } value={ value } > - { children } +
+ { children } +
) } - +
); } diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/style.module.scss b/packages/components/src/toggle-group-control/toggle-group-control-option-base/style.module.scss new file mode 100644 index 00000000000000..67a03c5eed388e --- /dev/null +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/style.module.scss @@ -0,0 +1,88 @@ +@use "@wordpress/base-styles/mixins"; +@use "../../utils/theme-variables" as *; + +.label { + display: inline-flex; + max-width: 100%; + min-width: 0; + position: relative; +} + +.label-block { + flex: 1; +} + +.button { + align-items: center; + appearance: none; + background: transparent; + border: none; + border-radius: var(--wpds-border-radius-xs); + color: $components-color-gray-700; + fill: currentColor; + cursor: var(--wpds-cursor-control); + display: flex; + font-family: inherit; + height: 100%; + justify-content: center; + line-height: 1; + outline: none; + padding: 0 12px; + position: relative; + text-align: center; + + @media not ( prefers-reduced-motion ) { + transition: + color 160ms linear, + font-weight 60ms linear; + } + + user-select: none; + width: 100%; + z-index: 2; + + &::-moz-focus-inner { + border: 0; + } + + &[disabled], + &[aria-disabled="true"] { + opacity: 0.4; + cursor: default; + } + + &:hover:not([disabled]):not([aria-disabled="true"]) { + color: $components-color-foreground; + } +} + +.is-deselectable { + &:focus { + @include mixins.outset-ring__focus(); + // Preserve validated-control accent overrides. + outline-color: $components-color-focus; + + // Hide overlapping border. + &[aria-pressed="false"] { + background: $components-color-background; + box-shadow: 0 0 0 2px $components-color-background; + } + } +} + +.is-icon { + height: 38px; + aspect-ratio: 1; + padding: 0; +} + +.is-pressed { + color: $components-color-foreground; + font-weight: var(--wpds-typography-font-weight-emphasis); +} + +.button-content { + display: flex; + font-size: var(--wpds-typography-font-size-md); + line-height: 1; +} diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts deleted file mode 100644 index 971295bcfb2e29..00000000000000 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * External dependencies - */ -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; - -/** - * Internal dependencies - */ -import { CONFIG, COLORS } from '../../utils'; -import type { - ToggleGroupControlProps, - ToggleGroupControlOptionBaseProps, -} from '../types'; - -export const LabelView = styled.div` - display: inline-flex; - max-width: 100%; - min-width: 0; - position: relative; -`; - -export const labelBlock = css` - flex: 1; -`; - -export const buttonView = ( { - isDeselectable, - isIcon, - isPressed, -}: Pick< ToggleGroupControlProps, 'isDeselectable' > & - Pick< ToggleGroupControlOptionBaseProps, 'isIcon' > & { - isPressed?: boolean; - } ) => css` - align-items: center; - appearance: none; - background: transparent; - border: none; - border-radius: ${ CONFIG.radiusXSmall }; - color: ${ COLORS.theme.gray[ 700 ] }; - fill: currentColor; - cursor: pointer; - display: flex; - font-family: inherit; - height: 100%; - justify-content: center; - line-height: 100%; - outline: none; - padding: 0 12px; - position: relative; - text-align: center; - @media not ( prefers-reduced-motion ) { - transition: - color ${ CONFIG.transitionDurationFast } linear, - font-weight 60ms linear; - } - user-select: none; - width: 100%; - z-index: 2; - - &::-moz-focus-inner { - border: 0; - } - - &[disabled], - &[aria-disabled='true'] { - opacity: 0.4; - cursor: default; - } - - &:hover:not( [disabled] ):not( [aria-disabled='true'] ) { - color: ${ COLORS.theme.foreground }; - } - - ${ isDeselectable && deselectable } - ${ isIcon && isIconStyles } - ${ isPressed && pressed } -`; - -const pressed = css` - color: ${ COLORS.theme.foreground }; - font-weight: ${ CONFIG.fontWeightEmphasis }; -`; - -const deselectable = css` - &:focus { - outline: ${ CONFIG.borderWidthFocus } solid ${ COLORS.ui.borderFocus }; - outline-offset: 2px; - - // Hide overlapping border - &[aria-pressed='false'] { - background: ${ COLORS.ui.background }; - box-shadow: 0 0 0 2px ${ COLORS.ui.background }; - } - } -`; - -export const ButtonContentView = styled.div` - display: flex; - font-size: ${ CONFIG.fontSize }; - line-height: 1; -`; - -const isIconStyles = css` - height: 38px; - aspect-ratio: 1; - padding-left: 0; - padding-right: 0; -`; diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx index b5e0b536405027..c649a5b610cf49 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx @@ -1,12 +1,13 @@ /** * External dependencies */ +import clsx from 'clsx'; import type { ForwardedRef } from 'react'; /** * WordPress dependencies */ -import { useMemo, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { useMergeRefs } from '@wordpress/compose'; /** @@ -14,10 +15,9 @@ import { useMergeRefs } from '@wordpress/compose'; */ import type { WordPressComponentProps } from '../../context'; import { contextConnect, useContextSystem } from '../../context'; -import { useCx } from '../../utils/hooks'; import BaseControl, { useBaseControlProps } from '../../base-control'; import type { ToggleGroupControlProps } from '../types'; -import * as styles from './styles'; +import styles from './style.module.scss'; import { ToggleGroupControlAsRadioGroup } from './as-radio-group'; import { ToggleGroupControlAsButtonGroup } from './as-button-group'; import { useTrackElementOffsetRect } from '../../utils/element-rect'; @@ -67,18 +67,14 @@ function UnconnectedToggleGroupControl( roundRect: false, } ); - const cx = useCx(); - - const classes = useMemo( - () => - cx( - styles.toggleGroupControl( { - isBlock, - isDeselectable, - } ), - className - ), - [ className, cx, isBlock, isDeselectable ] + const classes = clsx( + styles[ 'toggle-group-control' ], + { + [ styles[ 'is-block' ] ]: isBlock, + [ styles[ 'has-border' ] ]: isBlock && ! isDeselectable, + [ styles[ 'has-enclosing-borders' ] ]: ! isDeselectable, + }, + className ); const MainControl = isDeselectable diff --git a/packages/components/src/toggle-group-control/toggle-group-control/style.module.scss b/packages/components/src/toggle-group-control/toggle-group-control/style.module.scss new file mode 100644 index 00000000000000..ba9762d5c02a50 --- /dev/null +++ b/packages/components/src/toggle-group-control/toggle-group-control/style.module.scss @@ -0,0 +1,63 @@ +@use "@wordpress/base-styles/mixins"; +@use "../../utils/theme-variables" as *; + +.toggle-group-control { + background: $components-color-background; + border: 1px solid transparent; + border-radius: var(--wpds-border-radius-sm); + display: inline-flex; + height: 40px; + min-width: 0; + position: relative; + + @media not ( prefers-reduced-motion ) { + &[data-indicator-animated]::before { + transition-property: transform, border-radius; + transition-duration: 0.2s; + transition-timing-function: ease-out; + } + } + + &::before { + content: ""; + position: absolute; + pointer-events: none; + background: $components-color-gray-100; + border: 1px solid $components-color-gray-700; + + outline: 2px solid transparent; + outline-offset: -3px; + + border-radius: var(--wpds-border-radius-sm); + top: -1px; + /* stylelint-disable-next-line plugin/use-logical-properties-and-values -- Preserve the indicator's physical offset. */ + left: -2px; + width: calc(calc(var(--selected-width, 0) * 1px) + 2px); + height: calc(calc(var(--selected-height, 0) * 1px) + 2px); + transform: translateX(calc(var(--selected-left, 0) * 1px)); + /* Hide when dimensions are unset (0) */ + opacity: min(1, max(0, var(--selected-width, 0), var(--selected-height, 0))); + } +} + +.has-border { + border-color: $components-color-gray-300; +} + +.has-enclosing-borders { + &:hover { + border-color: $components-color-gray-400; + } + + &:focus-within { + z-index: 1; + @include mixins.outset-ring__focus(); + // Preserve validated-control accent overrides. + outline-color: $components-color-focus; + } +} + +.is-block { + display: flex; + width: 100%; +} diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts deleted file mode 100644 index 4d0cd424c8d324..00000000000000 --- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * External dependencies - */ -import { css } from '@emotion/react'; - -/** - * Internal dependencies - */ -import { CONFIG, COLORS } from '../../utils'; -import type { ToggleGroupControlProps } from '../types'; - -export const block = css` - display: flex; - width: 100%; -`; - -export const toggleGroupControl = ( { - isBlock, - isDeselectable, -}: Pick< ToggleGroupControlProps, 'isBlock' | 'isDeselectable' > ) => css` - background: ${ COLORS.ui.background }; - border: 1px solid transparent; - border-radius: ${ CONFIG.radiusSmall }; - display: inline-flex; - height: 40px; - min-width: 0; - position: relative; - - ${ ! isDeselectable && enclosingBorders( isBlock ) } - ${ isBlock && block } - - @media not ( prefers-reduced-motion ) { - &[data-indicator-animated]::before { - transition-property: transform, border-radius; - transition-duration: 0.2s; - transition-timing-function: ease-out; - } - } - - &::before { - content: ''; - position: absolute; - pointer-events: none; - background: ${ COLORS.theme.gray[ 100 ] }; - border: 1px solid ${ COLORS.theme.gray[ 700 ] }; - - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - outline-offset: -3px; - - border-radius: ${ CONFIG.radiusSmall }; - top: -1px; - left: -2px; - width: calc( calc( var( --selected-width, 0 ) * 1px ) + 2px ); - height: calc( calc( var( --selected-height, 0 ) * 1px ) + 2px ); - transform: translateX( calc( var( --selected-left, 0 ) * 1px ) ); - /* Hide when dimensions are unset (0) */ - opacity: min( - 1, - max( 0, var( --selected-width, 0 ), var( --selected-height, 0 ) ) - ); - } -`; - -const enclosingBorders = ( isBlock: ToggleGroupControlProps[ 'isBlock' ] ) => { - const enclosingBorder = css` - border-color: ${ COLORS.gray[ 300 ] }; - `; - - return css` - ${ isBlock && enclosingBorder } - - &:hover { - border-color: ${ COLORS.gray[ 400 ] }; - } - - &:focus-within { - z-index: 1; - outline: ${ CONFIG.borderWidthFocus } solid - ${ COLORS.ui.borderFocus }; - outline-offset: 1px; - } - `; -}; diff --git a/packages/components/src/utils/theme-variables.scss b/packages/components/src/utils/theme-variables.scss index 3c0781e59658e1..39293bc2e31a80 100644 --- a/packages/components/src/utils/theme-variables.scss +++ b/packages/components/src/utils/theme-variables.scss @@ -37,3 +37,4 @@ $components-color-light-gray-placeholder: color-mix(in srgb, $components-color-b // Semantic aliases (prefer these over raw gray values when applicable). $components-color-border: #{ $components-color-gray-600 }; $components-color-disabled: #{ $components-color-gray-600 }; +$components-color-focus: var(--wp-components-color-accent, var(--wpds-color-stroke-focus)); diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index a14017eab83b3b..a399bb3c2a91ee 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -854,16 +854,6 @@ "count": 2 } }, - "packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts": { - "no-restricted-imports": { - "count": 2 - } - }, - "packages/components/src/toggle-group-control/toggle-group-control/styles.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "packages/components/src/toggle-group-control/toggle-group-control/utils.ts": { "react-hooks/refs": { "count": 11