diff --git a/changelog/add-stripe-floating-labels b/changelog/add-stripe-floating-labels new file mode 100644 index 00000000000..c82d0361748 --- /dev/null +++ b/changelog/add-stripe-floating-labels @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Using Floating Labels with Stripe Appearance API for Blocks Checkout diff --git a/client/checkout/api/test/index.test.js b/client/checkout/api/test/index.test.js index dd291f4f8a7..7fe7e8c3f70 100644 --- a/client/checkout/api/test/index.test.js +++ b/client/checkout/api/test/index.test.js @@ -50,6 +50,7 @@ const mockAppearance = { fontFamily: undefined, fontSizeBase: undefined, }, + labels: 'above', }; describe( 'WCPayAPI', () => { diff --git a/client/checkout/blocks/payment-elements.js b/client/checkout/blocks/payment-elements.js index 84ac4be9393..7359b89a162 100644 --- a/client/checkout/blocks/payment-elements.js +++ b/client/checkout/blocks/payment-elements.js @@ -36,7 +36,7 @@ const PaymentElements = ( { api, ...props } ) => { useEffect( () => { async function generateUPEAppearance() { // Generate UPE input styles. - let upeAppearance = getAppearance( 'blocks_checkout' ); + let upeAppearance = getAppearance( 'blocks_checkout', false, true ); upeAppearance = await api.saveUPEAppearance( upeAppearance, 'blocks_checkout' diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js index 73f84596924..3636c4cbbe2 100644 --- a/client/checkout/upe-styles/index.js +++ b/client/checkout/upe-styles/index.js @@ -8,6 +8,7 @@ import { dashedToCamelCase, isColorLight, getBackgroundColor, + handleAppearanceForFloatingLabel, } from './utils.js'; export const appearanceSelectors = { @@ -15,6 +16,7 @@ export const appearanceSelectors = { hiddenContainer: '#wcpay-hidden-div', hiddenInput: '#wcpay-hidden-input', hiddenInvalidInput: '#wcpay-hidden-invalid-input', + hiddenValidActiveLabel: '#wcpay-hidden-valid-active-label', }, classicCheckout: { appendTarget: '.woocommerce-billing-fields__field-wrapper', @@ -40,16 +42,15 @@ export const appearanceSelectors = { linkSelectors: [ 'a' ], }, blocksCheckout: { - appendTarget: '#billing.wc-block-components-address-form', - upeThemeInputSelector: '#billing-first_name', - upeThemeLabelSelector: - '.wc-block-components-checkout-step__description', + appendTarget: '#contact-fields', + upeThemeInputSelector: '.wc-block-components-text-input #email', + upeThemeLabelSelector: '.wc-block-components-text-input label', rowElement: 'div', - validClasses: [ 'wc-block-components-text-input' ], + validClasses: [ 'wc-block-components-text-input', 'is-active' ], invalidClasses: [ 'wc-block-components-text-input', 'has-error' ], alternateSelectors: { - appendTarget: '#shipping.wc-block-components-address-form', - upeThemeInputSelector: '#shipping-first_name', + appendTarget: '#billing.wc-block-components-address-form', + upeThemeInputSelector: '#billing-first_name', upeThemeLabelSelector: '.wc-block-components-checkout-step__description', }, @@ -326,6 +327,13 @@ const hiddenElementsForUPE = { selectors.hiddenInput ); + // Clone & append target label to hidden valid row. + this.appendClone( + hiddenValidRow, + selectors.upeThemeLabelSelector, + selectors.hiddenValidActiveLabel + ); + // Clone & append target input to hidden invalid row. this.appendClone( hiddenInvalidRow, @@ -489,24 +497,40 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => { fontSizeBase: labelRules.fontSize, }; - const appearance = { + const isFloatingLabel = elementsLocation === 'blocks_checkout'; + + let appearance = { variables: globalRules, theme: isColorLight( backgroundColor ) ? 'stripe' : 'night', - rules: { - '.Input': inputRules, - '.Input--invalid': inputInvalidRules, - '.Label': labelRules, - '.Block': blockRules, - '.Tab': tabRules, - '.Tab:hover': tabHoverRules, - '.Tab--selected': selectedTabRules, - '.TabIcon:hover': tabIconHoverRules, - '.TabIcon--selected': selectedTabIconRules, - '.Text': labelRules, - '.Text--redirect': labelRules, - }, + labels: isFloatingLabel ? 'floating' : 'above', + // We need to clone the object to avoid modifying other rules when updating the appearance for floating labels. + rules: JSON.parse( + JSON.stringify( { + '.Input': inputRules, + '.Input--invalid': inputInvalidRules, + '.Label': labelRules, + '.Block': blockRules, + '.Tab': tabRules, + '.Tab:hover': tabHoverRules, + '.Tab--selected': selectedTabRules, + '.TabIcon:hover': tabIconHoverRules, + '.TabIcon--selected': selectedTabIconRules, + '.Text': labelRules, + '.Text--redirect': labelRules, + } ) + ), }; + if ( isFloatingLabel ) { + appearance = handleAppearanceForFloatingLabel( + appearance, + getFieldStyles( + selectors.hiddenValidActiveLabel, + '.Label--floating' + ) + ); + } + if ( forWooPay ) { appearance.rules = { ...appearance.rules, diff --git a/client/checkout/upe-styles/test/index.js b/client/checkout/upe-styles/test/index.js index fc63030ddb0..bc79053555a 100644 --- a/client/checkout/upe-styles/test/index.js +++ b/client/checkout/upe-styles/test/index.js @@ -223,6 +223,7 @@ describe( 'Getting styles for automated theming', () => { padding: '10px', }, }, + labels: 'above', } ); } ); diff --git a/client/checkout/upe-styles/upe-styles.js b/client/checkout/upe-styles/upe-styles.js index 98c54f50b34..ffc27cee0d2 100644 --- a/client/checkout/upe-styles/upe-styles.js +++ b/client/checkout/upe-styles/upe-styles.js @@ -93,6 +93,7 @@ const restrictedTabIconSelectedProperties = [ 'color' ]; export const upeRestrictedProperties = { '.Label': upeSupportedProperties[ '.Label' ], + '.Label--floating': [ ...upeSupportedProperties[ '.Label' ], 'transform' ], '.Input': [ ...upeSupportedProperties[ '.Input' ], 'outlineColor', diff --git a/client/checkout/upe-styles/utils.js b/client/checkout/upe-styles/utils.js index d8d5cbfe81a..5db80b42a19 100644 --- a/client/checkout/upe-styles/utils.js +++ b/client/checkout/upe-styles/utils.js @@ -138,3 +138,79 @@ export const getBackgroundColor = ( selectors ) => { export const isColorLight = ( color ) => { return tinycolor( color ).getBrightness() > 125; }; + +/** + * Modifies the appearance object to include styles for floating label. + * + * @param {Object} appearance object to modify. + * @param {Object} floatingLabelStyles Floating label styles. + * @return {Object} Modified appearance object. + */ +export const handleAppearanceForFloatingLabel = ( + appearance, + floatingLabelStyles +) => { + // Add floating label styles. + appearance.rules[ '.Label--floating' ] = floatingLabelStyles; + + // Update line-height for floating label to account for scaling. + if ( + appearance.rules[ '.Label--floating' ].transform && + appearance.rules[ '.Label--floating' ].transform !== 'none' + ) { + // Extract the scaling factors from the matrix + const transformMatrix = + appearance.rules[ '.Label--floating' ].transform; + const matrixValues = transformMatrix.match( /matrix\((.+)\)/ ); + if ( matrixValues && matrixValues[ 1 ] ) { + const splitMatrixValues = matrixValues[ 1 ].split( ', ' ); + const scaleX = parseFloat( splitMatrixValues[ 0 ] ); + const scaleY = parseFloat( splitMatrixValues[ 3 ] ); + const scale = ( scaleX + scaleY ) / 2; + + const lineHeight = parseFloat( + appearance.rules[ '.Label--floating' ].lineHeight + ); + const newLineHeight = Math.floor( lineHeight * scale ); + appearance.rules[ + '.Label--floating' + ].lineHeight = `${ newLineHeight }px`; + appearance.rules[ + '.Label--floating' + ].fontSize = `${ newLineHeight }px`; + } + delete appearance.rules[ '.Label--floating' ].transform; + } + + // Subtract the label's lineHeight from padding-top to account for floating label height. + // Minus 4px which is a constant value added by stripe to the padding-top. + // Minus 1px for each vertical padding to account for the unpredictable input height + // (see https://github.com/Automattic/woocommerce-payments/issues/9476#issuecomment-2374766540). + // When the result is less than 0, it will automatically use 0. + if ( appearance.rules[ '.Input' ].paddingTop ) { + appearance.rules[ + '.Input' + // eslint-disable-next-line max-len + ].paddingTop = `calc(${ appearance.rules[ '.Input' ].paddingTop } - ${ appearance.rules[ '.Label--floating' ].lineHeight } - 4px - 1px)`; + } + if ( appearance.rules[ '.Input' ].paddingBottom ) { + const originalPaddingBottom = parseFloat( + appearance.rules[ '.Input' ].paddingBottom + ); + appearance.rules[ + '.Input' + // eslint-disable-next-line max-len + ].paddingBottom = `${ originalPaddingBottom - 1 }px`; + + const originalLabelMarginTop = + appearance.rules[ '.Label' ].marginTop ?? '0'; + appearance.rules[ '.Label' ].marginTop = `${ Math.floor( + ( originalPaddingBottom - 1 ) / 3 + ) }px`; + appearance.rules[ + '.Label--floating' + ].marginTop = originalLabelMarginTop; + } + + return appearance; +}; diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index d31f4e19151..90b35164fe8 100644 --- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -98,6 +98,7 @@ describe( 'WoopayExpressCheckoutButton', () => { fontFamily: undefined, fontSizeBase: undefined, }, + labels: 'above', }; beforeEach( () => {