diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index add3778275f863..49a8a43c393cc6 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -93,15 +93,29 @@ export function Button( props ) { label, shortcut, tooltipPosition, + isActiveStyle, + customContainerStyles, } = props; const preferredColorScheme = usePreferredColorScheme(); const isDisabled = ariaDisabled || disabled; + const containerStyle = [ + styles.container, + customContainerStyles && { ...customContainerStyles }, + ]; + const buttonViewStyle = { opacity: isDisabled ? 0.3 : 1, ...( fixedRatio && styles.fixedRatio ), ...( isPressed ? styles.buttonActive : styles.buttonInactive ), + ...( isPressed && + isActiveStyle?.borderRadius && { + borderRadius: isActiveStyle.borderRadius, + } ), + ...( isActiveStyle?.backgroundColor && { + backgroundColor: isActiveStyle.backgroundColor, + } ), }; const states = []; @@ -159,7 +173,7 @@ export function Button( props ) { accessibilityHint={ hint } onPress={ onClick } onLongPress={ onLongPress } - style={ styles.container } + style={ containerStyle } disabled={ isDisabled } testID={ testID } > diff --git a/packages/components/src/mobile/color-settings/index.native.js b/packages/components/src/mobile/color-settings/index.native.js index 2b898b7c63f906..9c877782b92822 100644 --- a/packages/components/src/mobile/color-settings/index.native.js +++ b/packages/components/src/mobile/color-settings/index.native.js @@ -28,6 +28,7 @@ const ColorSettingsMemo = memo( gradientValue, onGradientChange, label, + hideNavigation, } ) => { useEffect( () => { shouldEnableBottomSheetMaxHeight( true ); @@ -44,6 +45,7 @@ const ColorSettingsMemo = memo( gradientValue, onGradientChange, label, + hideNavigation, } } > diff --git a/packages/components/src/mobile/color-settings/palette.screen.native.js b/packages/components/src/mobile/color-settings/palette.screen.native.js index f606b407b88326..9c6ae92486538a 100644 --- a/packages/components/src/mobile/color-settings/palette.screen.native.js +++ b/packages/components/src/mobile/color-settings/palette.screen.native.js @@ -26,7 +26,7 @@ import { colorsUtils } from './utils'; import styles from './style.scss'; -const HIT_SLOP = { top: 22, bottom: 22, left: 22, right: 22 }; +const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 }; const PaletteScreen = () => { const route = useRoute(); @@ -38,6 +38,7 @@ const PaletteScreen = () => { onGradientChange, colorValue, defaultSettings, + hideNavigation = false, } = route.params || {}; const { segments, isGradient } = colorsUtils; const [ currentValue, setCurrentValue ] = useState( colorValue ); @@ -164,10 +165,12 @@ const PaletteScreen = () => { } return ( - - - { label } - + { ! hideNavigation && ( + + + { label } + + ) } setIsAddingColor( true ), [ + setIsAddingColor, + ] ); + const disableIsAddingColor = useCallback( () => setIsAddingColor( false ), [ + setIsAddingColor, + ] ); + const colorIndicatorStyle = useMemo( + () => + fillComputedColors( + contentRef, + getActiveColors( value, name, colors ) + ), + [ value, colors ] + ); + + const hasColorsToChoose = ! isEmpty( colors ) || ! allowCustomControl; + + const onPressButton = useCallback( () => { + if ( hasColorsToChoose ) { + enableIsAddingColor(); + } else { + onChange( removeFormat( value, name ) ); + } + }, [ hasColorsToChoose, value ] ); + + const outlineStyle = usePreferredColorSchemeStyle( + styles[ 'components-inline-color__outline' ], + styles[ 'components-inline-color__outline--dark' ] + ); + + if ( ! hasColorsToChoose && ! isActive ) { + return null; + } + + const isActiveStyle = { + ...colorIndicatorStyle, + ...( ! colorIndicatorStyle?.backgroundColor + ? { backgroundColor: 'transparent' } + : {} ), + ...styles[ 'components-inline-color--is-active' ], + }; + + const customContainerStyles = + styles[ 'components-inline-color__button-container' ]; + + return ( + <> + + + { isActive && ( + + ) } + + + } + title={ title } + extraProps={ { + isActiveStyle, + customContainerStyles, + } } + // If has no colors to choose but a color is active remove the color onClick + onClick={ onPressButton } + /> + + + { isAddingColor && ( + + ) } + + ); +} + +export const textColor = { + name, + title, + tagName: 'mark', + className: 'has-inline-color', + attributes: { + style: 'style', + class: 'class', + }, + /* + * Since this format relies on the tag, it's important to + * prevent the default yellow background color applied by most + * browsers. The solution is to detect when this format is used with a + * text color but no background color, and in such cases to override + * the default styling with a transparent background. + * + * @see https://github.com/WordPress/gutenberg/pull/35516 + */ + __unstableFilterAttributeValue( key, value ) { + if ( key !== 'style' ) return value; + // We should not add a background-color if it's already set + if ( value && value.includes( 'background-color' ) ) return value; + const addedCSS = [ 'background-color', transparentValue ].join( ':' ); + // Prepend `addedCSS` to avoid a double `;;` as any the existing CSS + // rules will already include a `;`. + return value ? [ addedCSS, value ].join( ';' ) : addedCSS; + }, + edit: TextColorEdit, +}; diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index 37f2d6f9401f63..03a80fb18e6871 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -42,7 +42,7 @@ function parseCSS( css = '' ) { }, {} ); } -function parseClassName( className = '', colorSettings ) { +export function parseClassName( className = '', colorSettings ) { return className.split( ' ' ).reduce( ( accumulator, name ) => { // `colorSlug` could contain dashes, so simply match the start and end. if ( name.startsWith( 'has-' ) && name.endsWith( '-color' ) ) { diff --git a/packages/format-library/src/text-color/inline.native.js b/packages/format-library/src/text-color/inline.native.js new file mode 100644 index 00000000000000..efbf6db545e5dd --- /dev/null +++ b/packages/format-library/src/text-color/inline.native.js @@ -0,0 +1,163 @@ +/** + * WordPress dependencies + */ +import { useCallback, useMemo } from '@wordpress/element'; +import { + applyFormat, + removeFormat, + getActiveFormat, +} from '@wordpress/rich-text'; +import { + useSetting, + getColorClassName, + getColorObjectByColorValue, +} from '@wordpress/block-editor'; +import { BottomSheet, ColorSettings } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { textColor as settings } from './index'; +import { transparentValue } from './index.js'; +import { parseClassName } from './inline.js'; + +function parseCSS( css = '' ) { + return css.split( ';' ).reduce( ( accumulator, rule ) => { + if ( rule ) { + const [ property, value ] = rule.replace( / /g, '' ).split( ':' ); + if ( property === 'color' ) accumulator.color = value; + if ( property === 'background-color' && value !== transparentValue ) + accumulator.backgroundColor = value; + } + return accumulator; + }, {} ); +} + +function getActiveColors( value, name, colorSettings ) { + const activeColorFormat = getActiveFormat( value, name ); + + if ( ! activeColorFormat ) { + return {}; + } + + return { + ...parseCSS( activeColorFormat.attributes.style ), + ...parseClassName( activeColorFormat.attributes.class, colorSettings ), + }; +} + +function setColors( value, name, colorSettings, colors ) { + const { color, backgroundColor } = { + ...getActiveColors( value, name, colorSettings ), + ...colors, + }; + + if ( ! color && ! backgroundColor ) { + return removeFormat( value, name ); + } + + const styles = []; + const classNames = []; + const attributes = {}; + + if ( backgroundColor ) { + styles.push( [ 'background-color', backgroundColor ].join( ':' ) ); + } else { + // Override default browser color for mark element. + styles.push( [ 'background-color', transparentValue ].join( ':' ) ); + } + + if ( color ) { + const colorObject = getColorObjectByColorValue( colorSettings, color ); + + if ( colorObject ) { + classNames.push( getColorClassName( 'color', colorObject.slug ) ); + styles.push( [ 'color', colorObject.color ].join( ':' ) ); + } else { + styles.push( [ 'color', color ].join( ':' ) ); + } + } + + if ( styles.length ) attributes.style = styles.join( ';' ); + if ( classNames.length ) attributes.class = classNames.join( ' ' ); + + const format = { type: name, attributes }; + + // For cases when there is no text selected, formatting is forced + // for the first empty character + if ( + value?.start === value?.end && + ( value?.text.length === 0 || + value.text?.charAt( value.end - 1 ) === ' ' ) + ) { + return applyFormat( value, format, value?.start - 1, value?.end + 1 ); + } + + return applyFormat( value, format ); +} + +function ColorPicker( { name, value, onChange } ) { + const property = 'color'; + const colors = useSetting( 'color.palette' ) || settings.colors; + const colorSettings = { + colors, + }; + + const onColorChange = useCallback( + ( color ) => { + if ( color !== '' ) { + onChange( + setColors( value, name, colors, { [ property ]: color } ) + ); + // Remove formatting if the color was reset, there's no + // current selection and the previous character is a space + } else if ( + value?.start === value?.end && + value.text?.charAt( value?.end - 1 ) === ' ' + ) { + onChange( + removeFormat( value, name, value.end - 1, value.end ) + ); + } else { + onChange( removeFormat( value, name ) ); + } + }, + [ colors, onChange, property ] + ); + const activeColors = useMemo( + () => getActiveColors( value, name, colors ), + [ name, value, colors ] + ); + + return ( + + ); +} + +export default function InlineColorUI( { name, value, onChange, onClose } ) { + return ( + + + + + + + + ); +} diff --git a/packages/format-library/src/text-color/style.native.scss b/packages/format-library/src/text-color/style.native.scss new file mode 100644 index 00000000000000..43e84e81ea7e99 --- /dev/null +++ b/packages/format-library/src/text-color/style.native.scss @@ -0,0 +1,23 @@ +.components-inline-color--is-active { + border-radius: 18px; +} + +.components-inline-color__outline { + border-color: $light-dim; + top: 6px; + bottom: 6px; + left: 11px; + right: 11px; + border-radius: 24px; + border-width: $border-width; + position: absolute; + z-index: 2; +} + +.components-inline-color__outline--dark { + border-color: $dark-ultra-dim; +} + +.components-inline-color__button-container { + padding: 6px; +} diff --git a/packages/react-native-aztec/RNTAztecView.podspec b/packages/react-native-aztec/RNTAztecView.podspec index cdb8deed1e12c9..970a8b6af6a709 100644 --- a/packages/react-native-aztec/RNTAztecView.podspec +++ b/packages/react-native-aztec/RNTAztecView.podspec @@ -18,6 +18,6 @@ Pod::Spec.new do |s| s.xcconfig = {'OTHER_LDFLAGS' => '-lxml2', 'HEADER_SEARCH_PATHS' => '/usr/include/libxml2'} s.dependency 'React-Core' - s.dependency 'WordPress-Aztec-iOS', '~> 1.19.5' + s.dependency 'WordPress-Aztec-iOS', '~> 1.19.6' end diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle index 1d207cd6bd7baf..27d4e4de038851 100644 --- a/packages/react-native-aztec/android/build.gradle +++ b/packages/react-native-aztec/android/build.gradle @@ -9,7 +9,7 @@ buildscript { jSoupVersion = '1.10.3' wordpressUtilsVersion = '1.22' espressoVersion = '3.0.1' - aztecVersion = 'v1.5.1' + aztecVersion = 'v1.5.2' willPublishReactNativeAztecBinary = properties["willPublishReactNativeAztecBinary"]?.toBoolean() ?: false } } diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index cb702492349a8d..13340484b0ab9a 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -93,6 +93,7 @@ public class ReactAztecText extends AztecText { put(AztecTextFormat.FORMAT_CITE, "italic"); put(AztecTextFormat.FORMAT_STRIKETHROUGH, "strikethrough"); put(AztecTextFormat.FORMAT_UNDERLINE, "underline"); + put(AztecTextFormat.FORMAT_MARK, "mark"); } }; @@ -327,6 +328,9 @@ private void updateToolbarButtons(ArrayList appliedStyles) { if (currentStyle == AztecTextFormat.FORMAT_STRIKETHROUGH) { formattingOptions.add("strikethrough"); } + if (currentStyle == AztecTextFormat.FORMAT_MARK) { + formattingOptions.add("mark"); + } } // Check if the same formatting event was already sent @@ -535,6 +539,8 @@ public void setActiveFormats(Iterable newFormats) { break; case "underline": newFormatsSet.add(AztecTextFormat.FORMAT_UNDERLINE); + case "mark": + newFormatsSet.add(AztecTextFormat.FORMAT_MARK); break; } } diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift index cdd28ea38d39eb..8e94be11f8323e 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift @@ -121,6 +121,7 @@ class RCTAztecView: Aztec.TextView { .italic: "italic", .strikethrough: "strikethrough", .link: "link", + .mark: "mark" ] override init(defaultFont: UIFont, defaultParagraphStyle: ParagraphStyle, defaultMissingImage: UIImage) { @@ -689,6 +690,7 @@ class RCTAztecView: Aztec.TextView { case "bold": toggleBold(range: emptyRange) case "italic": toggleItalic(range: emptyRange) case "strikethrough": toggleStrikethrough(range: emptyRange) + case "mark": toggleMark(range: emptyRange) default: print("Format not recognized") } } diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index c88168127c07a2..4c4ec357ee9406 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [**] [iOS] Fix scroll update when typing in RichText component [#36914] - [*] [Preformatted block] Fix an issue where the background color is not showing up for standard themes. [#36883] +- [***] Highlight text - enables color customization for specific text within a Pragraph block [#36028] ## 1.67.0 - [**] Adds Clipboard Link Suggestion to Image block and Button block [#35972] diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 4577ee4de87ba9..83d654fab35a05 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -305,8 +305,8 @@ PODS: - React-Core - RNTAztecView (1.67.0): - React-Core - - WordPress-Aztec-iOS (~> 1.19.5) - - WordPress-Aztec-iOS (1.19.5) + - WordPress-Aztec-iOS (~> 1.19.6) + - WordPress-Aztec-iOS (1.19.6) - Yoga (1.14.0) DEPENDENCIES: @@ -457,7 +457,7 @@ SPEC CHECKSUMS: BVLinearGradient: 1e5474c982efcfcaed47f368a61431bb38a4faf8 DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5 - FBReactNativeSpec: ee3fc80110975e231c8537d2e434a8afabe66fdc + FBReactNativeSpec: 80e9cf1155002ee4720084d8813326d913815e2f glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 Gutenberg: cb22fce31133194d87ce74b3f3d45ebf91b585cf RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c @@ -496,8 +496,8 @@ SPEC CHECKSUMS: RNReanimated: 39a9478eb635667c9a4da08ac906add9901b145e RNScreens: 185dcb481fab2f3dc77413f62b43dc3df826029c RNSVG: 9c0db12736608e32841e90fe9773db70ea40de20 - RNTAztecView: 59ba50af03168074634543b40595a9c29afa39ff - WordPress-Aztec-iOS: af36d9cb86a0109b568f516874870e2801ba1bd9 + RNTAztecView: 28dd2b1cdb74cb8161d76a7c1defbda1ad2f737a + WordPress-Aztec-iOS: 129276e7d1391acb08157641a54394668bb0d7f8 Yoga: 8c8436d4171c87504c648ae23b1d81242bdf3bbf PODFILE CHECKSUM: db5f67a29ecba75541dad181ff59246b6da2fb09 diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 860a1448f25c35..98491637663629 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -42,6 +42,7 @@ import { toHTMLString } from '../to-html-string'; import { removeLineSeparator } from '../remove-line-separator'; import { isCollapsed } from '../is-collapsed'; import { remove } from '../remove'; +import { getFormatColors } from '../get-format-colors'; import styles from './style.scss'; import ToolbarButtonWithOptions from './toolbar-button-with-options'; @@ -53,6 +54,7 @@ const gutenbergFormatNamesToAztec = { 'core/bold': 'bold', 'core/italic': 'italic', 'core/strikethrough': 'strikethrough', + 'core/text-color': 'mark', }; const EMPTY_PARAGRAPH_TAGS = '

'; @@ -143,13 +145,26 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { selectionStart: start, selectionEnd: end } = this.props; + const { + selectionStart: start, + selectionEnd: end, + colorPalette, + } = this.props; const { value } = this.props; + const currentValue = this.formatToValue( value ); - const { formats, replacements, text } = this.formatToValue( value ); + const { formats, replacements, text } = currentValue; const { activeFormats } = this.state; + const newFormats = getFormatColors( value, formats, colorPalette ); - return { formats, replacements, text, start, end, activeFormats }; + return { + formats: newFormats, + replacements, + text, + start, + end, + activeFormats, + }; } /** @@ -1113,6 +1128,7 @@ export class RichText extends Component { { isSelected && ( <> { + format.forEach( ( currentFormat ) => { + if ( currentFormat?.type === FORMAT_TYPE ) { + const className = currentFormat?.attributes?.class; + currentFormat.attributes.style = currentFormat.attributes.style.replace( + / /g, + '' + ); + + className?.split( ' ' ).forEach( ( currentClass ) => { + const match = currentClass.match( REGEX_TO_MATCH ); + if ( match ) { + const [ , colorSlug ] = currentClass.match( + REGEX_TO_MATCH + ); + const colorObject = getColorObjectByAttributeValues( + colors, + colorSlug + ); + const currentStyles = + currentFormat?.attributes?.style; + if ( + colorObject && + ( ! currentStyles || + currentStyles?.indexOf( + colorObject.color + ) === -1 ) + ) { + currentFormat.attributes.style = [ + `color: ${ colorObject.color }`, + currentStyles, + ].join( ';' ); + } + } + } ); + } + } ); + } ); + + return newFormats; + } + + return formats; +}