diff --git a/build-tools/utils/custom-css-properties.js b/build-tools/utils/custom-css-properties.js index 8f8225825c..895243883a 100644 --- a/build-tools/utils/custom-css-properties.js +++ b/build-tools/utils/custom-css-properties.js @@ -95,6 +95,9 @@ const customCssPropertiesList = [ 'styleBoxShadowDefault', 'styleBoxShadowDisabled', 'styleBoxShadowHover', + 'stylePaddingInline', + 'stylePaddingInlineStart', + 'stylePaddingInlineEnd', // Readonly state 'styleBackgroundReadonly', 'styleBorderColorReadonly', diff --git a/src/input/__tests__/styles.test.tsx b/src/input/__tests__/styles.test.tsx index b209b6a2d6..187d629c19 100644 --- a/src/input/__tests__/styles.test.tsx +++ b/src/input/__tests__/styles.test.tsx @@ -2,12 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 import customCssProps from '../../internal/generated/custom-css-properties'; import { getInputStyles } from '../styles'; +import { parsePaddingInline } from '../utils'; // Mock the environment module jest.mock('../../internal/environment', () => ({ SYSTEM: 'core', })); +describe('parsePaddingInline', () => { + test('returns undefined for both start and end when input is undefined', () => { + expect(parsePaddingInline(undefined)).toEqual({ start: undefined, end: undefined }); + }); + + test('handles single value - applies to both start and end', () => { + expect(parsePaddingInline('10px')).toEqual({ start: '10px', end: '10px' }); + expect(parsePaddingInline('1rem')).toEqual({ start: '1rem', end: '1rem' }); + expect(parsePaddingInline('0')).toEqual({ start: '0', end: '0' }); + }); + + test('handles shorthand notation - first value is start, second is end', () => { + expect(parsePaddingInline('10px 20px')).toEqual({ start: '10px', end: '20px' }); + expect(parsePaddingInline('1rem 2rem')).toEqual({ start: '1rem', end: '2rem' }); + expect(parsePaddingInline('5px 10px')).toEqual({ start: '5px', end: '10px' }); + }); + + test('handles extra whitespace', () => { + expect(parsePaddingInline(' 10px 20px ')).toEqual({ start: '10px', end: '20px' }); + expect(parsePaddingInline('10px 20px')).toEqual({ start: '10px', end: '20px' }); + }); + + test('ignores values beyond the first two', () => { + expect(parsePaddingInline('10px 20px 30px 40px')).toEqual({ start: '10px', end: '20px' }); + }); +}); + describe('getInputStyles', () => { afterEach(() => { jest.resetModules(); @@ -68,7 +96,9 @@ describe('getInputStyles', () => { fontSize: '14px', fontWeight: '400', paddingBlock: '8px', - paddingInline: '12px', + [customCssProps.stylePaddingInline]: '12px', + [customCssProps.stylePaddingInlineStart]: '12px', + [customCssProps.stylePaddingInlineEnd]: '12px', [customCssProps.styleBackgroundDefault]: '#ffffff', [customCssProps.styleBackgroundDisabled]: '#f0f0f0', [customCssProps.styleBackgroundHover]: '#fafafa', @@ -96,6 +126,55 @@ describe('getInputStyles', () => { }); }); + test('handles shorthand paddingInline values correctly', () => { + const styleWithShorthand = { + root: { + paddingInline: '10px 20px', + }, + }; + + const result = getInputStyles(styleWithShorthand); + + expect(result).toEqual({ + [customCssProps.stylePaddingInline]: '10px 20px', + [customCssProps.stylePaddingInlineStart]: '10px', + [customCssProps.stylePaddingInlineEnd]: '20px', + }); + }); + + test('handles single value paddingInline correctly', () => { + const styleWithSingleValue = { + root: { + paddingInline: '15px', + }, + }; + + const result = getInputStyles(styleWithSingleValue); + + expect(result).toEqual({ + [customCssProps.stylePaddingInline]: '15px', + [customCssProps.stylePaddingInlineStart]: '15px', + [customCssProps.stylePaddingInlineEnd]: '15px', + }); + }); + + test('does not add padding properties when paddingInline is not provided', () => { + const styleWithoutPadding = { + root: { + fontSize: '14px', + }, + }; + + const result = getInputStyles(styleWithoutPadding); + + expect(result).toEqual({ + fontSize: '14px', + }); + expect(result).not.toHaveProperty(customCssProps.stylePaddingInline); + expect(result).not.toHaveProperty(customCssProps.stylePaddingInlineStart); + expect(result).not.toHaveProperty(customCssProps.stylePaddingInlineEnd); + }); + test('returns undefined when SYSTEM is not core', async () => { jest.resetModules(); jest.doMock('../../internal/environment', () => ({ diff --git a/src/input/styles.scss b/src/input/styles.scss index e6b3856ac1..154261e544 100644 --- a/src/input/styles.scss +++ b/src/input/styles.scss @@ -34,7 +34,7 @@ .input { @include styles.styles-reset; padding-block: styles.$control-padding-vertical; - padding-inline: styles.$control-padding-horizontal; + padding-inline: var(#{custom-props.$stylePaddingInline}, #{styles.$control-padding-horizontal}); color: var(#{custom-props.$styleColorDefault}, awsui.$color-text-body-default); inline-size: 100%; cursor: text; @@ -128,9 +128,12 @@ &.input-invalid { @include styles.form-invalid-control(); &.input-has-icon-left { - padding-inline-start: calc( - #{styles.$control-icon-horizontal-padding} - - (#{styles.$invalid-control-left-border} - #{awsui.$border-width-field}) + padding-inline-start: max( + var(#{custom-props.$stylePaddingInlineStart}, #{styles.$control-padding-horizontal}), + calc( + #{styles.$control-icon-horizontal-padding} - + (#{styles.$invalid-control-left-border} - #{awsui.$border-width-field}) + ) ); } } @@ -138,9 +141,12 @@ &.input-warning { @include styles.form-warning-control(); &.input-has-icon-left { - padding-inline-start: calc( - #{styles.$control-icon-horizontal-padding} - - (#{styles.$invalid-control-left-border} - #{awsui.$border-width-field}) + padding-inline-start: max( + var(#{custom-props.$stylePaddingInlineStart}, #{styles.$control-padding-horizontal}), + calc( + #{styles.$control-icon-horizontal-padding} - + (#{styles.$invalid-control-left-border} - #{awsui.$border-width-field}) + ) ); } } @@ -159,10 +165,16 @@ } } &.input-has-icon-left { - padding-inline-start: styles.$control-icon-horizontal-padding; + padding-inline-start: max( + var(#{custom-props.$stylePaddingInlineStart}, #{styles.$control-padding-horizontal}), + #{styles.$control-icon-horizontal-padding} + ); } &.input-has-icon-right { - padding-inline-end: styles.$control-icon-horizontal-padding; + padding-inline-end: max( + var(#{custom-props.$stylePaddingInlineEnd}, #{styles.$control-padding-horizontal}), + #{styles.$control-icon-horizontal-padding} + ); } &.input-has-no-border-radius { border-start-start-radius: awsui.$border-radius-dropdown; diff --git a/src/input/styles.tsx b/src/input/styles.tsx index f0e17cf567..c873f0288d 100644 --- a/src/input/styles.tsx +++ b/src/input/styles.tsx @@ -3,18 +3,32 @@ import { SYSTEM } from '../internal/environment'; import customCssProps from '../internal/generated/custom-css-properties'; import { InputProps } from './interfaces'; +import { parsePaddingInline } from './utils'; export function getInputStyles(style: InputProps['style']) { let properties = {}; if (style?.root && SYSTEM === 'core') { + // We are only supporting logical shorthand properties for padding (e.g. "10px 30px"), + // so wen ened to deconstruct this into start- and end-padding to be able to style + // them separately. + const { start: paddingStart, end: paddingEnd } = parsePaddingInline(style?.root?.paddingInline); + properties = { borderRadius: style?.root?.borderRadius, borderWidth: style?.root?.borderWidth, fontSize: style?.root?.fontSize, fontWeight: style?.root?.fontWeight, paddingBlock: style?.root?.paddingBlock, - paddingInline: style?.root?.paddingInline, + ...(style?.root?.paddingInline && { + [customCssProps.stylePaddingInline]: style.root.paddingInline, + }), + ...(paddingStart && { + [customCssProps.stylePaddingInlineStart]: paddingStart, + }), + ...(paddingEnd && { + [customCssProps.stylePaddingInlineEnd]: paddingEnd, + }), ...(style?.root?.backgroundColor && { [customCssProps.styleBackgroundDefault]: style.root.backgroundColor?.default, [customCssProps.styleBackgroundDisabled]: style.root.backgroundColor?.disabled, diff --git a/src/input/utils.ts b/src/input/utils.ts index 88518aa9cd..be45a5fb50 100644 --- a/src/input/utils.ts +++ b/src/input/utils.ts @@ -37,3 +37,18 @@ export const convertAutoComplete = (propertyValue: boolean | string = false): st } return propertyValue || 'off'; }; + +/** + * Parses CSS paddingInline value into separate start and end values. + * Handles both single values ('10px') and shorthand notation ('10px 20px'). + */ +export const parsePaddingInline = ( + paddingInline: string | undefined +): { start: string | undefined; end: string | undefined } => { + if (!paddingInline) { + return { start: undefined, end: undefined }; + } + + const [start, end = start] = paddingInline.trim().split(/\s+/); + return { start, end }; +};