diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml index 669c3f09dfc..5f7ab100c63 100644 --- a/.github/workflows/stalebot.yml +++ b/.github/workflows/stalebot.yml @@ -1,51 +1,52 @@ name: 'Process stale needs-feedback issues' on: - schedule: - - cron: '21 0 * * *' - workflow_dispatch: + schedule: + - cron: '21 0 * * *' + workflow_dispatch: -permissions: { } +permissions: {} jobs: - stale: - runs-on: ubuntu-20.04 - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Scan issues - uses: actions/stale@v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." - close-issue-message: 'This issue was closed because it has been 14 days with no activity.' - operations-per-run: 140 - days-before-stale: -1 - days-before-close: -1 - days-before-issue-stale: 7 - days-before-issue-close: 7 - stale-issue-label: 'status: stale' - stale-pr-label: 'status: stale' - exempt-issue-labels: 'type: enhancement' - only-issue-labels: 'needs: author feedback' - close-issue-label: "status: can't reproduce" - ascending: true - - name: Process Stale Flaky Test Issues - uses: actions/stale@v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - only-issue-labels: 'metric: flaky e2e test' - days-before-stale: -1 - days-before-close: -1 - days-before-issue-stale: 5 - days-before-issue-close: 2 - stale-issue-label: 'status: stale' - stale-issue-message: 'This issue is being marked as stale due to inactivity. It will be auto-closed if no further activity occurs within the next 2 days.' - close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' - close-issue-reason: 'not_planned' - remove-stale-when-updated: true - exempt-all-assignees: false - enable-statistics: true - ascending: true - operations-per-run: 120 + stale: + runs-on: ubuntu-20.04 + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Scan issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." + close-issue-message: 'This issue was closed because it has been 14 days with no activity.' + operations-per-run: 140 + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 7 + days-before-issue-close: 7 + stale-issue-label: 'status: stale' + stale-pr-label: 'status: stale' + exempt-issue-labels: 'type: enhancement' + only-issue-labels: 'needs: author feedback' + close-issue-label: "status: can't reproduce" + ascending: true + - name: Process Stale Flaky Test Issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-issue-labels: 'metric: flaky e2e test' + exempt-issue-labels: 'metric: skipped test' + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 5 + days-before-issue-close: 2 + stale-issue-label: 'status: stale' + stale-issue-message: 'This issue is being marked as stale due to inactivity. It will be auto-closed if no further activity occurs within the next 2 days.' + close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' + close-issue-reason: 'not_planned' + remove-stale-when-updated: true + exempt-all-assignees: false + enable-statistics: true + ascending: true + operations-per-run: 120 diff --git a/changelog.txt b/changelog.txt index 2ba165e2bdc..592a7f919cf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,12 @@ == Changelog == += 9.3.2 2024-09-18 = + +- Fix - Improve the product importer's handling of filepaths under Windows [#51456](https://github.com/woocommerce/woocommerce/pull/51456) +- Fix - Revert changes related to low stock product notifications [#51441](https://github.com/woocommerce/woocommerce/pull/51441) +- Fix - Resolve a bug where manually triggering `added_to_cart` event without a button element caused an Exception [#51449](https://github.com/woocommerce/woocommerce/pull/51449) + + = 9.3.1 2024-09-12 = * Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312) diff --git a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md index 3b7a53c9fe8..d5cbef24096 100644 --- a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md +++ b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md @@ -254,4 +254,4 @@ Displaying the variation in the front store works a bit differently for variable ## How to find hooks? -Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`. +Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommerce plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`. diff --git a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md index d3457214654..664e64370ab 100644 --- a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md +++ b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md @@ -43,12 +43,16 @@ The options you feed the configuration instance should be an object in this shap ```js const options = { name: 'my_payment_method', - content: <div>A React node</div>, - edit: <div>A React node</div>, + title: 'My Mayment Method', + description: 'A setence or two about your payment method', + gatewayId: 'gateway-id', + content: <ReactNode />, + edit: <ReactNode />, canMakePayment: () => true, paymentMethodId: 'new_payment_method', supports: { features: [], + style: [], }, }; ``` @@ -59,6 +63,18 @@ Here's some more details on the configuration options: This should be a unique string (wise to try to pick something unique for your gateway that wouldn't be used by another implementation) that is used as the identifier for the gateway client side. If `paymentMethodId` is not provided, `name` is used for `paymentMethodId` as well. +#### `title` (optional) + +This should be a human readable string with the name of your payment method. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. If it is not provided, the `name` will be used as the title. + +#### `description` (optional) + +This is one or two sentences maximum describing your payment gateway. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. + +#### `gatewayId` (optional) + +This is the ID of the Payment Gateway that your plugin registers server side, and which registers the express payment method. It is used to link your express payment method on the clinet, to a payment gateway defined on the server. It is used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page. + #### `content` (required) This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)). @@ -97,7 +113,11 @@ This is the only optional configuration object. The value of this property is wh This is an array of payment features supported by the gateway. It is used to crosscheck if the payment method can be used for the content of the cart. By default payment methods should support at least `products` feature. If no value is provided then this assumes that `['products']` are supported. ---- +#### `supports:style` + +This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`. + +![Express Checkout Uniform Styles](https://github.com/user-attachments/assets/f0f99f3f-dca7-42b0-8685-3b098a825020) ### Payment Methods - `registerPaymentMethod( options )` @@ -139,23 +159,24 @@ The options you feed the configuration instance are the same as those for expres A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts). -| Property | Type | Description | Values | -| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | -| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | -| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | -| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | +| Property | Type | Description | Values | +| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | +| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | +| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | +| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | | `components` | Object | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. | | | `emitResponse` | Object | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. | | -| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | -| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | -| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | -| `onSubmit` | Function | Submits the checkout and begins processing | - | -| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful` (see below for explanation) | -| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | -| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | +| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | +| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | +| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | +| `onSubmit` | Function | Submits the checkout and begins processing | - | +| `buttonAttributes` | Object | Styles set by the merchant that should be respected by all express payment buttons | `height, borderRadius` | +| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful`(see below for explanation) | +| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | +| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | | `shippingStatus` | Object | Various shipping status helpers. | | -| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | +| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | - `isPristine`: This is true when the current payment status is `PRISTINE`. - `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`. @@ -167,6 +188,29 @@ A big part of the payment method integration is the interface that is exposed fo Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security). +### Button Attributes for Express Payment Methods + +This API provides a way to synchronise the look and feel of the express payment buttons for a coherent shopper experience. Express Payment Methods must prefer the values provided in the `buttonAttributes`, and use it's own configuration settings as backup when the buttons are rendered somewhere other than the Cart or Checkout block. + +For example, in your button component, you would do something like this: + +```js +// Get your extension specific settings and set defaults if not available +let { + borderRadius = '4', + height = '48', +} = getButtonSettingsFromConfig(); + +// In a cart & checkout block context, we receive `buttonAttributes` as a prop which overwrite the extension specific settings +if ( typeof buttonAttributes !== 'undefined' ) { + height = buttonAttributes.height; + borderRadius = buttonAttributes.borderRadius; +} +... + +return <button style={height: `${height}px`, borderRadius: `${borderRadius}px`} /> +``` + ## Server Side Integration ### Processing Payment diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index d18a4367869..0b71e764ab1 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -83,7 +83,7 @@ "menu_title": "Add Custom Fields to Products", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", - "hash": "fe8cf43940f5166bf69f102aa4643cbe32415b1167d6b6d8968d434a4d113879", + "hash": "df61c93febc234fe0dbb4826a20ae120b153ab6f6c92d8778177fcac8d6696fe", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", "id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b" } @@ -223,7 +223,7 @@ "menu_title": "Payment Method Integration", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", - "hash": "138ffbf27e79ec8b35d2c46e87e3663c203d91fc9ba3f76c43f3cbe76258e5bf", + "hash": "015aae25bb331364c224fe8eb2b7675e4cbed0a9e6bee0dde5f5311388161b0a", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", "id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420", "links": { @@ -1229,7 +1229,7 @@ "menu_title": "Core critical flows", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md", - "hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e", + "hash": "c7122979df14f46646b3f1472ba071bc560b99e6462c5790a9aeaa3b4238ce15", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md", "id": "e561b46694dba223c38b87613ce4907e4e14333a" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3" + "hash": "a88d9ea54465c8bbd820042a92df79cbd48943e785b418fcaa04d0c0e66116c0" } \ No newline at end of file diff --git a/packages/js/product-editor/changelog/update-dataviews_package b/packages/js/product-editor/changelog/update-dataviews_package new file mode 100644 index 00000000000..c1eb32092ed --- /dev/null +++ b/packages/js/product-editor/changelog/update-dataviews_package @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update @wordpress/dataviews package and fix class names for products dataviews app. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 52ec6eacb88..7ec2d530d27 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -56,7 +56,7 @@ "@wordpress/compose": "wp-6.0", "@wordpress/core-data": "wp-6.0", "@wordpress/data": "wp-6.0", - "@wordpress/dataviews": "^4.2.0", + "@wordpress/dataviews": "^4.3.0", "@wordpress/date": "wp-6.0", "@wordpress/deprecated": "wp-6.0", "@wordpress/edit-post": "wp-6.0", diff --git a/packages/js/product-editor/src/products.scss b/packages/js/product-editor/src/products.scss index 463564ff88e..73b5e0afd57 100644 --- a/packages/js/product-editor/src/products.scss +++ b/packages/js/product-editor/src/products.scss @@ -1,15 +1,15 @@ @include wordpress-admin-schemes(); -.woocommerce_page_woocommerce-products-dashboard #wpadminbar, -.woocommerce_page_woocommerce-products-dashboard #adminmenumain { +.product_page_woocommerce-products-dashboard #wpadminbar, +.product_page_woocommerce-products-dashboard #adminmenumain { display: none; } -.woocommerce_page_woocommerce-products-dashboard #wpcontent { +.product_page_woocommerce-products-dashboard #wpcontent { margin-left: 0; } -body.woocommerce_page_woocommerce-products-dashboard #woocommerce-products-dashboard { +body.product_page_woocommerce-products-dashboard #woocommerce-products-dashboard { @include wp-admin-reset("#woocommerce-products-dashboard"); @include reset; display: block !important; @@ -40,7 +40,7 @@ body.js.is-fullscreen-mode { } } -.woocommerce_page_woocommerce-products-dashboard { +.product_page_woocommerce-products-dashboard { @import "products-app/sidebar-dataviews/style.scss"; @import "products-app/product-edit/style.scss"; } diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss index d713efc1a80..952780f05e2 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss @@ -1,13 +1,17 @@ @import "../../stylesheets/_variables.scss"; .woocommerce-marketplace__category-selector { + position: relative; display: flex; align-items: stretch; - margin: $grid-unit-20 0 0 0; + margin: 0; + overflow-x: auto; } .woocommerce-marketplace__category-item { cursor: pointer; + white-space: nowrap; + margin-bottom: 0; .components-dropdown { height: 100%; @@ -50,7 +54,6 @@ .woocommerce-marketplace__category-selector--full-width { display: none; - margin-top: $grid-unit-15; } @media screen and (max-width: $break-medium) { @@ -122,3 +125,22 @@ background-color: $gray-900; } } + +.woocommerce-marketplace__category-navigation-button { + border: none; + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 50px; +} + +.woocommerce-marketplace__category-navigation-button--prev { + background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + left: 0; +} + +.woocommerce-marketplace__category-navigation-button--next { + background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + right: 0; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx index 07b298cde66..9625aef5862 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx @@ -1,20 +1,21 @@ /** * External dependencies */ -import { useState, useEffect } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useRef } from '@wordpress/element'; import { useQuery } from '@woocommerce/navigation'; -import clsx from 'clsx'; +import { Icon } from '@wordpress/components'; +import { useDebounce } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import CategoryLink from './category-link'; -import CategoryDropdown from './category-dropdown'; import { Category, CategoryAPIItem } from './types'; import { fetchCategories } from '../../utils/functions'; -import './category-selector.scss'; import { ProductType } from '../product-list/types'; +import CategoryDropdown from './category-dropdown'; +import './category-selector.scss'; const ALL_CATEGORIES_SLUGS = { [ ProductType.extension ]: '_all', @@ -29,32 +30,21 @@ interface CategorySelectorProps { export default function CategorySelector( props: CategorySelectorProps ): JSX.Element { - const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] ); - const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] ); const [ selected, setSelected ] = useState< Category >(); const [ isLoading, setIsLoading ] = useState( false ); + const [ categoriesToShow, setCategoriesToShow ] = useState< Category[] >( + [] + ); + const [ isOverflowing, setIsOverflowing ] = useState( false ); + const [ scrollPosition, setScrollPosition ] = useState< + 'start' | 'middle' | 'end' + >( 'start' ); + + const categorySelectorRef = useRef< HTMLUListElement >( null ); + const selectedCategoryRef = useRef< HTMLLIElement >( null ); const query = useQuery(); - useEffect( () => { - // If no category is selected, show All as selected - let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ]; - - if ( query.category ) { - categoryToSearch = query.category; - } - - const allCategories = visibleItems.concat( dropdownItems ); - - const selectedCategory = allCategories.find( - ( category ) => category.slug === categoryToSearch - ); - - if ( selectedCategory ) { - setSelected( selectedCategory ); - } - }, [ query.category, props.type, visibleItems, dropdownItems ] ); - useEffect( () => { setIsLoading( true ); @@ -72,21 +62,125 @@ export default function CategorySelector( return category.slug !== '_featured'; } ); - // Split array into two from 7th item - const visibleCategoryItems = categories.slice( 0, 7 ); - const dropdownCategoryItems = categories.slice( 7 ); - - setVisibleItems( visibleCategoryItems ); - setDropdownItems( dropdownCategoryItems ); + setCategoriesToShow( categories ); } ) .catch( () => { - setVisibleItems( [] ); - setDropdownItems( [] ); + setCategoriesToShow( [] ); } ) .finally( () => { setIsLoading( false ); } ); - }, [ props.type ] ); + }, [ props.type, setCategoriesToShow ] ); + + useEffect( () => { + // If no category is selected, show All as selected + let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ]; + + if ( query.category ) { + categoryToSearch = query.category; + } + + const selectedCategory = categoriesToShow.find( + ( category ) => category.slug === categoryToSearch + ); + + if ( selectedCategory ) { + setSelected( selectedCategory ); + } + }, [ query.category, props.type, categoriesToShow ] ); + + useEffect( () => { + if ( selectedCategoryRef.current ) { + selectedCategoryRef.current.scrollIntoView( { + block: 'nearest', + inline: 'center', + } ); + } + }, [ selected ] ); + + function checkOverflow() { + if ( + categorySelectorRef.current && + categorySelectorRef.current.parentElement?.scrollWidth + ) { + const isContentOverflowing = + categorySelectorRef.current.scrollWidth > + categorySelectorRef.current.parentElement.scrollWidth; + + setIsOverflowing( isContentOverflowing ); + } + } + + function checkScrollPosition() { + const ulElement = categorySelectorRef.current; + + if ( ! ulElement ) { + return; + } + + const { scrollLeft, scrollWidth, clientWidth } = ulElement; + + if ( scrollLeft < 10 ) { + setScrollPosition( 'start' ); + + return; + } + + if ( scrollLeft + clientWidth < scrollWidth ) { + setScrollPosition( 'middle' ); + + return; + } + + if ( scrollLeft + clientWidth === scrollWidth ) { + setScrollPosition( 'end' ); + } + } + + const debouncedCheckOverflow = useDebounce( checkOverflow, 300 ); + const debouncedScrollPosition = useDebounce( checkScrollPosition, 100 ); + + function scrollCategories( scrollAmount: number ) { + if ( categorySelectorRef.current ) { + categorySelectorRef.current.scrollTo( { + left: categorySelectorRef.current.scrollLeft + scrollAmount, + behavior: 'smooth', + } ); + } + } + + function scrollToNextCategories() { + scrollCategories( 200 ); + } + + function scrollToPrevCategories() { + scrollCategories( -200 ); + } + + useEffect( () => { + window.addEventListener( 'resize', debouncedCheckOverflow ); + + const ulElement = categorySelectorRef.current; + + if ( ulElement ) { + ulElement.addEventListener( 'scroll', debouncedScrollPosition ); + } + + return () => { + window.removeEventListener( 'resize', debouncedCheckOverflow ); + + if ( ulElement ) { + ulElement.removeEventListener( + 'scroll', + debouncedScrollPosition + ); + } + }; + }, [ debouncedCheckOverflow, debouncedScrollPosition ] ); + + useEffect( () => { + checkOverflow(); + }, [ categoriesToShow ] ); function mobileCategoryDropdownLabel() { const allCategoriesText = __( 'All Categories', 'woocommerce' ); @@ -102,16 +196,6 @@ export default function CategorySelector( return selected.label; } - function isSelectedInDropdown() { - if ( ! selected ) { - return false; - } - - return dropdownItems.find( - ( category ) => category.slug === selected.slug - ); - } - if ( isLoading ) { return ( <> @@ -131,50 +215,62 @@ export default function CategorySelector( return ( <> -