Skip to content

feature-request: enable input "free text" CSS value in every block attributes that accpet a CSS value #53595

Open

Description

@tresorama

What problem does this address?

Every time we can input a CSS value, for example for :

  • width
  • height
  • margin
  • color
  • background-color
  • ...

We cannot input a valid CSS value if the UI doesn't support it yet.

It will be great to have a new custom option "free text" input that accepts any text, then the consumer is in charge to ensure that the value is a CSS valid value for that property.

I'm thinking this features for every attribute (that manage a CSS value) in every block.

The implementation of the UI control will be a simple <input type="text">.

Example - Spacer

Schermata 2023-08-11 alle 23 53 30

Taking for example the Spacer block, looking at "Height" attribute, we can inject as value:

  • a spacing preset value of the scale defined in theme json
  • arbitrary value with NON-ARBITRARY "unit" (px, rem, vw, ....) but chosen from an available set of "unit"
    • as shown in image above

We cannot type other shapes.

We cannot type these value:

  • calc(100vh + 12px)
  • clamp(20px, 2vw, 40px)

Example - Image

Schermata 2023-08-12 alle 00 11 20

In "Width" attribute we can't type "50%" for example, and in current design of the core library, we should wait that the core Image block will gain the percent unit in "width" input.


Which benefits this feature will add?

Dual benefits:

  • power user (web dev/web designer) has more familiar toolset
  • Wp core devs can ship less code (potentially)

In the current design of the core library, the core block author decides which unit a control support, so if the desired unit (from the consumer perspective) is not available he/she should wait that a "code" update of the block to add a specific unit to the input control.

Doing this kind of upgrade in every attribute, in every block is time-consuming, and we can "forget/postpone" this kind of update if we already have a fallback "free text" option.

I think that having a completely new "free text" option will ensure that the "power user" can gain flexibility while implementing a design and WP Core dev doesn't need to add a lot of UI code in React (in every block).

Future Vision

In a future, the "units" select (one that lets selecting px,rem, vw, ...), could be removed in favor of "free text" only.
This will simplify the code in React even more and leave full CSS spec for defining values to the user.


Useful Reference

Bricks builder started with a "select" like

Schermata 2023-08-12 alle 00 27 07

and now is "free text only" like

Schermata 2023-08-12 alle 00 27 23

Bricks builder is for dev/web designer that know the CSS spec.
But also "ch" unit is a thing that only a dev/web designer will know about.
So even if Wordpress Core Libary wants to be "abstract" and user friendly we already have some power user features like selecting "ch", "vh" and so on for some control.
So why not extend this ??

Activity

  1. changed the title [-]feature-request: enable input "free text" value in every block attributes input[/-] [+]feature-request: enable input "free text" CSS value in every block attributes that accpet a CSS value[/+] on Aug 11, 2023
  2. robizzt commented on Dec 24, 2024

    @robizzt

    Here is my custom width control that can be used to extend core blocks:

    WidthControl.js

    import {__} from '@wordpress/i18n';
    
    import {
    	useSettings
    } from '@wordpress/block-editor';
    
    import {
    	__experimentalUseCustomUnits as useCustomUnits,
    	TextControl,
    	SelectControl
    } from '@wordpress/components';
    
    import {
    	useState,
    	useEffect
    } from '@wordpress/element';
    
    import '../assets/css/custom-control-width.scss';
    
    const WidthControl = ({label, value, onChange}) => {
    	const [ availableUnits ] = useSettings('spacing.units');
    	const units = useCustomUnits({
    		availableUnits: availableUnits || [ '%', 'px', 'em', 'rem', 'vw' ],
    	});
    	units.push({
    		value: 'custom',
    		label: '?', // Add some simple label for custom unit
    		a11yLabel: __('Custom', 'text-domain')
    	});
    
    	const getNumberFromValue = (value) => {
    		const match = value.match(/^[0-9.]+/);
    		return match ? match[0] : '';
    	};
    	const getUnitFromValue = (value) => {
    		const match = value.match(/^[0-9.]+([a-zA-Z%]+)$/);
    		if (match) {
    			const extractedUnit = match[1];
    			const unitExists = units.some(unit => unit.value === extractedUnit);
    			return unitExists ? extractedUnit : 'custom';
    		}
    		return 'custom';
    	};
    	const getStepForUnit = (unit) => {
    		const unitObject = units.find(u => u.value === unit);
    		return unitObject ? unitObject.step : 1;
    	};
    
    	const [width, setWidth] = useState(getNumberFromValue(value));
    	const [unit, setUnit] = useState(getUnitFromValue(value));
    	const [customValue, setCustomValue] = useState(value);
    
    	useEffect(() => {
    		if (unit === 'custom') {
    			onChange(customValue);
    		} else {
    			onChange(`${width}${unit}`);
    		}
    	}, [width, unit, customValue]);
    
    	return (
    		<div className="custom__control__width__wrapper">
    			<div className="custom__control__width__label">
    				<label>{label}</label>
    			</div>
    			<div className="custom__control__width">
    				<div className="custom__control__width__input">
    					{unit !== 'custom' && (
    						<TextControl
    							value={width}
    							onChange={(newValue) => {
    								setWidth(newValue);
    								if (newValue !== '') {
    									setCustomValue(`${newValue}${unit}`);
    								} else {
    									setCustomValue('');
    								}
    							}}
    							type="number"
    							step={getStepForUnit(unit)}
    						/>
    					)}
    					{unit === 'custom' && (
    						<TextControl
    							value={customValue}
    							onChange={(newValue) => {
    								const numberValue = getNumberFromValue(newValue);
    								if (numberValue !== '') {
    									setWidth(numberValue);
    								} else {
    									setWidth('');
    								}
    								setCustomValue(newValue);
    							}}
    						/>
    					)}
    				</div>
    				<div className="custom__control__width__unit">
    					<SelectControl
    						value={unit}
    						options={units}
    						onChange={(newUnit) => {
    							setUnit(newUnit);
    							if (newUnit !== 'custom') {
    								setCustomValue(`${width}${newUnit}`);
    							} else {
    								const numberValue = getNumberFromValue(value);
    								if (numberValue === '') {
    									setCustomValue('');
    								}
    							}
    						}}
    					/>
    				</div>
    			</div>
    		</div>
    	);
    };
    
    export default WidthControl;
    
    

    And some styling to look like the default control:

    custom-control-width.scss

    .custom__control__width__wrapper {
    	display: flex;
    	align-items: center;
    	flex-direction: row;
    	gap: calc(8px);
    	justify-content: space-between;
    	width: 100%;
    	box-sizing: border-box;
    	position: relative;
    	border-radius: 2px;
    	padding-top: 0px;
    }
    .custom__control__width__wrapper .custom__control__width__label {
    	display: block;
    	max-height: 100%;
    	min-height: 0px;
    	min-width: 0px;
    	max-width: calc(100% - 10px);
    }
    .custom__control__width__wrapper .custom__control__width__label label {
    	font-size: 11px;
    	font-weight: 500;
    	line-height: 1.4;
    	text-transform: uppercase;
    	box-sizing: border-box;
    	display: block;
    	padding-top: 0px;
    	padding-bottom: 0px;
    	max-width: 100%;
    	z-index: 1;
    	overflow: hidden;
    	text-overflow: ellipsis;
    	white-space: nowrap;
    }
    .custom__control__width__wrapper .custom__control__width {
    	-webkit-box-align: center;
    	align-items: center;
    	box-sizing: border-box;
    	border-radius: inherit;
    	display: flex;
    	position: relative;
    	background-color: var(--wp-components-color-background, #fff);
    	flex: 0 0 80px;
    	box-sizing: border-box;
    	border-color: var(--wp-components-color-gray-600, #949494);
    	border-radius: inherit;
    	border-style: solid;
    	border-width: 1px;
    	inset: 0px;
    	margin: 0px;
    	padding: 0px;
    }
    .custom__control__width__wrapper .custom__control__width:has(.custom__control__width__input div.components-base-control input[type="text"]),
    .custom__control__width__wrapper .custom__control__width .custom__control__width__input:has(div.components-base-control input[type="text"]) {
    	flex-grow: 1;
    }
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__input div.components-base-control,
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__unit div.components-base-control,
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__input div.components-base-control > div,
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__unit div.components-base-control > div {
    	margin-bottom: 0;
    }
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__input div.components-base-control input {
    	border: none;
    	background-color: transparent;
    	box-sizing: border-box;
    	border: none;
    	color: var(--wp-components-color-foreground, #1e1e1e);
    	display: block;
    	font-family: inherit;
    	margin: 0px;
    	outline: none;
    	width: 100%;
    	height: 30px;
    	line-height: 1;
    	min-height: 30px;
    	padding-left: calc(8px);
    	padding-right: calc(8px);
    	padding-inline-end: calc(8px);
    	box-shadow: none !important;
    }
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__unit div.components-base-control select {
    	font-size: 14px;
    	line-height: 2;
    	color: #2c3338;
    	border-color: #8c8f94;
    	box-shadow: none;
    	border-radius: 3px;
    	padding: 0 24px 0 8px;
    	min-height: 30px;
    	max-width: 25rem;
    	-webkit-appearance: none;
    	background: #fff url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) no-repeat right 5px top 55%;
    	background-size: 16px 16px;
    	cursor: pointer;
    	vertical-align: middle;
    	appearance: none;
    	background: transparent;
    	border-radius: 0px 2px 2px 0px;
    	display: block;
    	outline: none;
    	margin: 0px;
    	min-height: auto;
    	font-family: inherit;
    	box-sizing: border-box;
    	padding: 2px 1px;
    	width: 20px;
    	color: rgb(47, 47, 47);
    	font-size: 8px;
    	line-height: 1;
    	letter-spacing: -0.5px;
    	text-transform: uppercase;
    	text-align-last: center;
    	height: 100%;
    	border: 1px solid transparent;
    	transition: box-shadow 0.1s linear, border 0.1s linear;
    }
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__unit div.components-base-control select ~ div,
    .block-editor-block-inspector .custom__control__width__wrapper .custom__control__width .custom__control__width__unit div.components-base-control select ~ span {
    	display: none;
    }
    
    

    It should support all default units or those set in the theme.json file, and it adds a new one called Custom where you can enter anything you like, like a calc() or variables. There is no validation except the one in the browser, which I'm fine with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions