diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index e4d9da813417eb..e0365c9b4d3d29 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -14,8 +14,9 @@ concurrency: jobs: test: - runs-on: macos-12 - if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + runs-on: macos-13 + if: false + #if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: matrix: native-test-name: [gutenberg-editor-rendering] diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index e1e7fd8c755c97..1665d769e25f05 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -14,8 +14,9 @@ concurrency: jobs: test: - runs-on: macos-12 - if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + runs-on: macos-13 + if: false + #if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: matrix: xcode: ['14.2'] diff --git a/packages/block-editor/src/components/global-styles/filters-panel.js b/packages/block-editor/src/components/global-styles/filters-panel.js index 581661e0c84071..c62099596f66c6 100644 --- a/packages/block-editor/src/components/global-styles/filters-panel.js +++ b/packages/block-editor/src/components/global-styles/filters-panel.js @@ -10,10 +10,10 @@ import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalItemGroup as ItemGroup, + __experimentalItem as Item, __experimentalHStack as HStack, __experimentalZStack as ZStack, __experimentalDropdownContentWrapper as DropdownContentWrapper, - Button, MenuGroup, ColorIndicator, DuotonePicker, @@ -189,15 +189,12 @@ export default function FiltersPanel( { return ( - + ); } } diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 844d5dd341437e..d37a6ca5694b09 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -186,7 +186,7 @@ export function BlockTypesTab( continue; } - if ( rootClientId && item.isAllowedInCurrentRoot ) { + if ( item.isAllowedInCurrentRoot ) { itemsForCurrentRoot.push( item ); } else { itemsRemaining.push( item ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index dc90f351732524..ed9e859f028a98 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1586,14 +1586,14 @@ export function getTemplateLock( state, rootClientId ) { * @param {string|Object} blockNameOrType The block type object, e.g., the response * from the block directory; or a string name of * an installed block type, e.g.' core/paragraph'. - * @param {Set} checkedBlocks Set of block names that have already been checked. + * @param {?string} rootClientId Optional root client ID of block list. * * @return {boolean} Whether the given block type is allowed to be inserted. */ const isBlockVisibleInTheInserter = ( state, blockNameOrType, - checkedBlocks = new Set() + rootClientId = null ) => { let blockType; let blockName; @@ -1621,26 +1621,19 @@ const isBlockVisibleInTheInserter = ( return false; } - if ( checkedBlocks.has( blockName ) ) { - return false; - } - - checkedBlocks.add( blockName ); - // If parent blocks are not visible, child blocks should be hidden too. - if ( Array.isArray( blockType.parent ) ) { - return blockType.parent.some( - ( name ) => - ( blockName !== name && - isBlockVisibleInTheInserter( - state, - name, - checkedBlocks - ) ) || - // Exception for blocks with post-content parent, - // the root level is often consider as "core/post-content". - // This exception should only apply to the post editor ideally though. - name === 'core/post-content' + const parents = ( + Array.isArray( blockType.parent ) ? blockType.parent : [] + ).concat( Array.isArray( blockType.ancestor ) ? blockType.ancestor : [] ); + if ( parents.length > 0 ) { + const rootBlockName = getBlockName( state, rootClientId ); + // This is an exception to the rule that says that all blocks are visible in the inserter. + // Blocks that require a given parent or ancestor are only visible if we're within that parent. + return ( + parents.includes( 'core/post-content' ) || + parents.includes( rootBlockName ) || + getBlockParentsByBlockName( state, rootClientId, parents ).length > + 0 ); } @@ -1665,7 +1658,7 @@ const canInsertBlockTypeUnmemoized = ( blockName, rootClientId = null ) => { - if ( ! isBlockVisibleInTheInserter( state, blockName ) ) { + if ( ! isBlockVisibleInTheInserter( state, blockName, rootClientId ) ) { return false; } @@ -2072,6 +2065,7 @@ const buildBlockTypeItem = category: blockType.category, keywords: blockType.keywords, parent: blockType.parent, + ancestor: blockType.ancestor, variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. @@ -2169,7 +2163,11 @@ export const getInserterItems = createRegistrySelector( ( select ) => } else { blockTypeInserterItems = blockTypeInserterItems .filter( ( blockType ) => - isBlockVisibleInTheInserter( state, blockType ) + isBlockVisibleInTheInserter( + state, + blockType, + rootClientId + ) ) .map( ( blockType ) => ( { ...blockType, @@ -2501,7 +2499,11 @@ export const __experimentalGetAllowedPatterns = createRegistrySelector( name, rootClientId ) - : isBlockVisibleInTheInserter( state, name ) + : isBlockVisibleInTheInserter( + state, + name, + rootClientId + ) ) ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 7692bd6bf2cbb6..51949bfd468ca8 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -3324,7 +3324,7 @@ describe( 'selectors', () => { settings: {}, blockEditingModes: new Map(), }; - expect( canInsertBlocks( state, [ '2', '3' ], '1' ) ).toBe( true ); + expect( canInsertBlocks( state, [ '2' ], '1' ) ).toBe( true ); } ); it( 'should deny blocks', () => { diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 1a5fae7ce9cbb7..ac03011d73f63a 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -290,6 +290,7 @@ class="wp-lightbox-overlay zoom" data-wp-on-async--click="actions.hideLightbox" data-wp-on-async-window--resize="callbacks.setOverlayStyles" data-wp-on-async-window--scroll="actions.handleScroll" + data-wp-bind--style="state.overlayStyles" tabindex="-1" > ; +const Mybutton = () => ( + +); ``` +## Props -### Props +### `__next40pxDefaultSize` -The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. +Start opting into the larger default height that will become the +default size in a future version. -Props not included in this set will be applied to the `a` or `button` element. + - Type: `boolean` + - Required: No + - Default: `false` -#### `accessibleWhenDisabled`: `boolean` +### `accessibleWhenDisabled` Whether to keep the button focusable when disabled. -In most cases, it is recommended to set this to `true`. Disabling a control without maintaining focusability can cause accessibility issues, by hiding their presence from screen reader users, or by preventing focus from returning to a trigger element. +In most cases, it is recommended to set this to `true`. Disabling a control without maintaining focusability +can cause accessibility issues, by hiding their presence from screen reader users, +or by preventing focus from returning to a trigger element. -Learn more about the [focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols) in the WAI-ARIA Authoring Practices Guide. +Learn more about the [focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols) +in the WAI-ARIA Authoring Practices Guide. -- Required: No -- Default: `false` + - Type: `boolean` + - Required: No + - Default: `false` -#### `children`: `ReactNode` +### `children` The button's children. -- Required: No + - Type: `ReactNode` + - Required: No -#### `className`: `string` +### `description` -An optional additional class name to apply to the rendered button. +A visually hidden accessible description for the button. -- Required: No + - Type: `string` + - Required: No -#### `description`: `string` +### `disabled` -An accessible description for the button. - -- Required: No - -#### `disabled`: `boolean` - -Whether the button is disabled. If `true`, this will force a `button` element to be rendered, even when an `href` is given. +Whether the button is disabled. If `true`, this will force a `button` element +to be rendered, even when an `href` is given. In most cases, it is recommended to also set the `accessibleWhenDisabled` prop to `true`. -- Required: No + - Type: `boolean` + - Required: No -#### `href`: `string` +### `href` If provided, renders `a` instead of `button`. -- Required: No + - Type: `string` + - Required: Yes -#### `icon`: `IconProps< unknown >[ 'icon' ]` +### `icon` -If provided, renders an [Icon](/packages/components/src/icon/README.md) component inside the button. +If provided, renders an Icon component inside the button. -- Required: No + - Type: `IconType` + - Required: No -#### `iconPosition`: `'left' | 'right'` +### `iconPosition` -If provided with `icon`, sets the position of icon relative to the `text`. Available options are `left|right`. +If provided with `icon`, sets the position of icon relative to the `text`. -- Required: No -- Default: `left` + - Type: `"left" | "right"` + - Required: No + - Default: `'left'` -#### `iconSize`: `IconProps< unknown >[ 'size' ]` +### `iconSize` -If provided with `icon`, sets the icon size. Please refer to the [Icon](/packages/components/src/icon/README.md) component for more details regarding the default value of its `size` prop. +If provided with `icon`, sets the icon size. +Please refer to the Icon component for more details regarding +the default value of its `size` prop. -- Required: No + - Type: `number` + - Required: No -#### `isBusy`: `boolean` +### `isBusy` Indicates activity while a action is being performed. -- Required: No + - Type: `boolean` + - Required: No -#### `isDestructive`: `boolean` +### `isDestructive` Renders a red text-based button style to indicate destructive behavior. -- Required: No - -#### `isLink`: `boolean` + - Type: `boolean` + - Required: No -Deprecated: Renders a button with an anchor style. -Use `variant` prop with `link` value instead. - -- Required: No -- Default: `false` - -#### `isPressed`: `boolean` +### `isPressed` Renders a pressed button style. -If the native `aria-pressed` attribute is also set, it will take precedence. - -- Required: No - -#### `isPrimary`: `boolean` + - Type: `boolean` + - Required: No -Deprecated: Renders a primary button style. -Use `variant` prop with `primary` value instead. +### `label` -- Required: No -- Default: `false` +Sets the `aria-label` of the component, if none is provided. +Sets the Tooltip content if `showTooltip` is provided. -#### `isSecondary`: `boolean` + - Type: `string` + - Required: No -Deprecated: Renders a default button style. -Use `variant` prop with `secondary` value instead. +### `shortcut` -- Required: No -- Default: `false` +If provided with `showTooltip`, appends the Shortcut label to the tooltip content. +If an object is provided, it should contain `display` and `ariaLabel` keys. -#### `isSmall`: `boolean` + - Type: `string | { display: string; ariaLabel: string; }` + - Required: No -Decreases the size of the button. +### `showTooltip` -Deprecated in favor of the `size` prop. If both props are defined, the `size` prop will take precedence. +If provided, renders a Tooltip component for the button. -- Required: No + - Type: `boolean` + - Required: No -#### `isTertiary`: `boolean` - -Deprecated: Renders a text-based button style. -Use `variant` prop with `tertiary` value instead. - -- Required: No -- Default: `false` - -#### `label`: `string` - -Sets the `aria-label` of the component, if none is provided. Sets the Tooltip content if `showTooltip` is provided. - -- Required: No - -#### `shortcut`: `string | { display: string; ariaLabel: string; }` - -If provided with `showTooltip`, appends the Shortcut label to the tooltip content. If an object is provided, it should contain `display` and `ariaLabel` keys. - -- Required: No - -#### `showTooltip`: `boolean` - -If provided, renders a [Tooltip](/packages/components/src/tooltip/README.md) component for the button. - -- Required: No - -#### `size`: `'default'` | `'compact'` | `'small'` +### `size` The size of the button. -- `'default'`: For normal text-label buttons, unless it is a toggle button. -- `'compact'`: For toggle buttons, icon buttons, and buttons when used in context of either. -- `'small'`: For icon buttons associated with more advanced or auxiliary features. +- `'default'`: For normal text-label buttons, unless it is a toggle button. +- `'compact'`: For toggle buttons, icon buttons, and buttons when used in context of either. +- `'small'`: For icon buttons associated with more advanced or auxiliary features. If the deprecated `isSmall` prop is also defined, this prop will take precedence. -- Required: No -- Default: `'default'` - -#### `target`: `string` + - Type: `"small" | "default" | "compact"` + - Required: No + - Default: `'default'` -If provided with `href`, sets the `target` attribute to the `a`. - -- Required: No - -#### `text`: `string` +### `text` If provided, displays the given text inside the button. If the button contains children elements, the text is displayed before them. -- Required: No + - Type: `string` + - Required: No -#### `tooltipPosition`: `PopoverProps[ 'position' ]` +### `tooltipPosition` -If provided with`showTooltip`, sets the position of the tooltip. Please refer to the [Tooltip](/packages/components/src/tooltip/README.md) component for more details regarding the defaults. +If provided with `showTooltip`, sets the position of the tooltip. +Please refer to the Tooltip component for more details regarding the defaults. -- Required: No + - Type: `"top" | "middle" | "bottom" | "top center" | "top left" | "top right" | "middle center" | "middle left" | "middle right" | "bottom center" | ...` + - Required: No -#### `variant`: `'primary' | 'secondary' | 'tertiary' | 'link'` +### `target` -Specifies the button's style. The accepted values are `'primary'` (the primary button styles), `'secondary'` (the default button styles), `'tertiary'` (the text-based button styles), and `'link'` (the link button styles). +If provided with `href`, sets the `target` attribute to the `a`. -- Required: No + - Type: `string` + - Required: No -#### `__next40pxDefaultSize`: `boolean` +### `variant` -Start opting into the larger default height that will become the default size in a future version. +Specifies the button's style. -- Required: No -- Default: `false` +The accepted values are: -## Related components +1. `'primary'` (the primary button styles) +2. `'secondary'` (the default button styles) +3. `'tertiary'` (the text-based button styles) +4. `'link'` (the link button styles) -- To group buttons together, use the [ButtonGroup](/packages/components/src/button-group/README.md) component. + - Type: `"link" | "primary" | "secondary" | "tertiary"` + - Required: No diff --git a/packages/components/src/button/docs-manifest.json b/packages/components/src/button/docs-manifest.json new file mode 100644 index 00000000000000..0fd0f84f44e102 --- /dev/null +++ b/packages/components/src/button/docs-manifest.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../schemas/docs-manifest.json", + "displayName": "Button", + "filePath": "./index.tsx" +} diff --git a/packages/components/src/button/stories/best-practices.mdx b/packages/components/src/button/stories/best-practices.mdx new file mode 100644 index 00000000000000..66ec44e6738d5f --- /dev/null +++ b/packages/components/src/button/stories/best-practices.mdx @@ -0,0 +1,31 @@ +import { Meta } from '@storybook/blocks'; + + + +# Button + +## Usage +Buttons indicate available actions and allow user interaction within the interface. As key elements in the WordPress UI, they appear in toolbars, modals, and forms. Default buttons support most actions, while primary buttons emphasize the main action in a view. Secondary buttons pair as secondary actions next to a primary action. + +Each layout contains one prominently placed, high-emphasis button. If you need multiple buttons, use one primary button for the main action, secondary for the rest of the actions and tertiary sparingly when an action needs to not stand out at all. + +### Sizes + +- `'default'`: For normal text-label buttons, unless it is a toggle button. +- `'compact'`: For toggle buttons, icon buttons, and buttons when used in context of either. +- `'small'`: For icon buttons associated with more advanced or auxiliary features. + +## Best practices + +- Label buttons to show that a click or tap initiates an action. +- Use established color conventions; for example, reserve red buttons for irreversible or dangerous actions. +- Avoid crowding the screen with multiple calls to action, which confuses users. +- Keep button locations consistent across the interface. + +## Content guidelines + +Buttons should be clear and predictable, showing users what will happen when clicked. Make labels reflect actions accurately to avoid confusion. + +Start button text with a strong action verb and include a noun to specify the change, except for common actions like Save, Close, Cancel, or OK. + +For other actions, use a `{verb}+{noun}` format for context. Keep button text brief and remove unnecessary words like "the," "an," or "a" for easy scanning. diff --git a/packages/components/src/button/stories/index.story.tsx b/packages/components/src/button/stories/index.story.tsx index 808914893de610..605b56686c702f 100644 --- a/packages/components/src/button/stories/index.story.tsx +++ b/packages/components/src/button/stories/index.story.tsx @@ -65,30 +65,48 @@ Default.args = { children: 'Code is poetry', }; +/** + * Primary buttons stand out with bold color fills, making them distinct + * from the background. Since they naturally draw attention, each layout should contain + * only one primary button to guide users toward the most important action. + */ export const Primary = Template.bind( {} ); Primary.args = { ...Default.args, variant: 'primary', }; +/** + * Secondary buttons complement primary buttons. Use them for standard actions that may appear alongside a primary action. + */ export const Secondary = Template.bind( {} ); Secondary.args = { ...Default.args, variant: 'secondary', }; +/** + * Tertiary buttons have minimal emphasis. Use them sparingly to subtly highlight an action. + */ export const Tertiary = Template.bind( {} ); Tertiary.args = { ...Default.args, variant: 'tertiary', }; +/** + * Link buttons have low emphasis and blend into the page, making them suitable for supplementary actions, + * especially those involving navigation away from the current view. + */ export const Link = Template.bind( {} ); Link.args = { ...Default.args, variant: 'link', }; +/** + * Use this variant for irreversible actions. Apply sparingly and only for actions with significant impact. + */ export const IsDestructive = Template.bind( {} ); IsDestructive.args = { ...Default.args, diff --git a/packages/components/src/button/types.ts b/packages/components/src/button/types.ts index 7d67b721a5036d..d730f49b1e8138 100644 --- a/packages/components/src/button/types.ts +++ b/packages/components/src/button/types.ts @@ -111,11 +111,13 @@ type BaseButtonProps = { tooltipPosition?: PopoverProps[ 'position' ]; /** * Specifies the button's style. + * * The accepted values are: - * 'primary' (the primary button styles) - * 'secondary' (the default button styles) - * 'tertiary' (the text-based button styles) - * 'link' (the link button styles) + * + * 1. `'primary'` (the primary button styles) + * 2. `'secondary'` (the default button styles) + * 3. `'tertiary'` (the text-based button styles) + * 4. `'link'` (the link button styles) */ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'; }; diff --git a/packages/components/src/custom-select-control/index.tsx b/packages/components/src/custom-select-control/index.tsx index 74da8a5c741060..339944f4198722 100644 --- a/packages/components/src/custom-select-control/index.tsx +++ b/packages/components/src/custom-select-control/index.tsx @@ -154,11 +154,11 @@ function CustomSelectControl< T extends CustomSelectOption >( const renderSelectedValueHint = () => { const selectedOptionHint = options ?.map( applyOptionDeprecations ) - ?.find( ( { name } ) => currentValue === name )?.hint; + ?.find( ( { name } ) => store.getState().value === name )?.hint; return ( - { currentValue } + { store.getState().value } { selectedOptionHint && ( ( { const { showTitle = true, showMedia = true, showDescription = true } = view; const hasBulkAction = useHasAPossibleBulkAction( actions, item ); const id = getItemId( item ); + const instanceId = useInstanceId( GridItem ); const isSelected = selection.includes( id ); const renderedMediaField = mediaField?.render ? ( @@ -89,6 +91,23 @@ function GridItem< Item >( { className: 'dataviews-view-grid__title-field dataviews-title-field', } ); + let mediaA11yProps; + let titleA11yProps; + if ( isItemClickable( item ) && onClickItem ) { + if ( renderedTitleField ) { + mediaA11yProps = { + 'aria-labelledby': `dataviews-view-grid__title-field-${ instanceId }`, + }; + titleA11yProps = { + id: `dataviews-view-grid__title-field-${ instanceId }`, + }; + } else { + mediaA11yProps = { + 'aria-label': __( 'Navigate to item' ), + }; + } + } + return ( ( { } } > { showMedia && renderedMediaField && ( -
{ renderedMediaField }
+
+ { renderedMediaField } +
) } { showMedia && renderedMediaField && ( ( { justify="space-between" className="dataviews-view-grid__title-actions" > -
{ renderedTitleField }
+
+ { renderedTitleField } +
diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index f0a7db5ad966ff..21bf56b578b57d 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -54,7 +54,7 @@ export async function visitSiteEditor( // Wait for the canvas loader to appear first, so that the locator that // waits for the hidden state doesn't resolve prematurely. - await canvasLoader.waitFor( { state: 'visible' } ); + await canvasLoader.waitFor( { state: 'visible', timeout: 60_000 } ); await canvasLoader.waitFor( { state: 'hidden', // Bigger timeout is needed for larger entities, like the Large Post diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index d32666bcf3aa1b..97484c89ed670b 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -34,17 +34,22 @@ const fieldsWithBulkEditSupport = [ function PostEditForm( { postType, postId } ) { const ids = useMemo( () => postId.split( ',' ), [ postId ] ); - const { record } = useSelect( + const { record, hasFinishedResolution } = useSelect( ( select ) => { + const args = [ 'postType', postType, ids[ 0 ] ]; + + const { + getEditedEntityRecord, + hasFinishedResolution: hasFinished, + } = select( coreDataStore ); + return { record: - ids.length === 1 - ? select( coreDataStore ).getEditedEntityRecord( - 'postType', - postType, - ids[ 0 ] - ) - : null, + ids.length === 1 ? getEditedEntityRecord( ...args ) : null, + hasFinishedResolution: hasFinished( + 'getEditedEntityRecord', + args + ), }; }, [ postType, ids ] @@ -159,12 +164,14 @@ function PostEditForm( { postType, postId } ) { return ( - + { hasFinishedResolution && ( + + ) } ); } diff --git a/storybook/components/figma-embed/index.js b/storybook/components/figma-embed/index.js new file mode 100644 index 00000000000000..8d8ac6488a9334 --- /dev/null +++ b/storybook/components/figma-embed/index.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import './style.scss'; + +// See https://www.figma.com/developers/embed#embed-a-figma-file +const CONFIG = { + 'embed-host': 'wordpress-storybook', + footer: false, + 'page-selector': false, + 'viewport-controls': true, +}; + +/** + * Embed Figma links in the Storybook. + * + * @param {Object} props + * @param {string} props.url - Figma URL to embed. + * @param {string} props.title - Accessible title for the iframe. + */ +function FigmaEmbed( { url, title, ...props } ) { + const urlObj = new URL( url ); + + const queryParams = new URLSearchParams( urlObj.search ); + Object.entries( CONFIG ).forEach( ( [ key, value ] ) => { + queryParams.set( key, value ); + } ); + urlObj.search = queryParams.toString(); + + urlObj.hostname = urlObj.hostname.replace( + 'www.figma.com', + 'embed.figma.com' + ); + + const normalizedUrl = urlObj.toString(); + + return ( +