diff --git a/assets/react/admin-dashboard/segments/options.js b/assets/react/admin-dashboard/segments/options.js index 1577c2472..6e506635c 100644 --- a/assets/react/admin-dashboard/segments/options.js +++ b/assets/react/admin-dashboard/segments/options.js @@ -1,4 +1,3 @@ - import { get_response_message } from '../../helper/response'; // SVG Icons Totor V2 @@ -384,15 +383,14 @@ document.addEventListener('DOMContentLoaded', function () { function highlightSearchedItem(dataKey) { const target = document.querySelector(`#${dataKey}`); const targetEl = target && target.querySelector(`[tutor-option-name]`); - const scrollTargetEl = target && target.parentNode.querySelector('.tutor-option-field-row'); - if (scrollTargetEl) { + if (targetEl) { targetEl.classList.add('isHighlighted'); setTimeout(() => { targetEl.classList.remove('isHighlighted'); }, 6000); - scrollTargetEl.scrollIntoView({ + targetEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest', diff --git a/assets/react/v3/entries/course-builder/components/ai-course-modal/BasicPrompt.tsx b/assets/react/v3/entries/course-builder/components/ai-course-modal/BasicPrompt.tsx index 43b76dd90..02e3b6584 100644 --- a/assets/react/v3/entries/course-builder/components/ai-course-modal/BasicPrompt.tsx +++ b/assets/react/v3/entries/course-builder/components/ai-course-modal/BasicPrompt.tsx @@ -77,6 +77,7 @@ const BasicPrompt = ({ onClose }: BasicPromptProps) => { }; export default BasicPrompt; + const styles = { container: css` position: absolute; diff --git a/assets/react/v3/entries/course-builder/components/modals/AICourseBuilderModal.tsx b/assets/react/v3/entries/course-builder/components/modals/AICourseBuilderModal.tsx index 7072e9af3..fdb47bc3b 100644 --- a/assets/react/v3/entries/course-builder/components/modals/AICourseBuilderModal.tsx +++ b/assets/react/v3/entries/course-builder/components/modals/AICourseBuilderModal.tsx @@ -1,10 +1,11 @@ -import type { ModalProps } from '@TutorShared/components/modals/Modal'; import BasicPrompt from '@CourseBuilderComponents/ai-course-modal/BasicPrompt'; import ContentGeneration from '@CourseBuilderComponents/ai-course-modal/ContentGeneration'; import ContentGenerationContextProvider, { useContentGenerationContext, } from '@CourseBuilderComponents/ai-course-modal/ContentGenerationContext'; import { css } from '@emotion/react'; +import FocusTrap from '@TutorShared/components/FocusTrap'; +import type { ModalProps } from '@TutorShared/components/modals/Modal'; import { useEffect } from 'react'; type AICourseBuilderModalProps = ModalProps; @@ -29,9 +30,11 @@ const AICourseBuilderModal = ({ closeModal }: AICourseBuilderModalProps) => { return ( -
- -
+ +
+ +
+
); }; diff --git a/assets/react/v3/entries/payment-settings/components/PaymentItem.tsx b/assets/react/v3/entries/payment-settings/components/PaymentItem.tsx index bc90d6a46..b65681058 100644 --- a/assets/react/v3/entries/payment-settings/components/PaymentItem.tsx +++ b/assets/react/v3/entries/payment-settings/components/PaymentItem.tsx @@ -164,7 +164,7 @@ const PaymentItem = ({ data, paymentIndex, isOverlay = false }: PaymentItemProps {__('Update now', 'tutor')} - + }> {__('Plugin not installed', 'tutor')} @@ -175,7 +175,6 @@ const PaymentItem = ({ data, paymentIndex, isOverlay = false }: PaymentItemProps render={(controllerProps) => ( { const isValid = await form.trigger(`payment_methods.${paymentIndex}.fields`); diff --git a/assets/react/v3/shared/components/FocusTrap.tsx b/assets/react/v3/shared/components/FocusTrap.tsx new file mode 100644 index 000000000..155cb1a76 --- /dev/null +++ b/assets/react/v3/shared/components/FocusTrap.tsx @@ -0,0 +1,68 @@ +import { Children, cloneElement, useEffect, useRef, type ReactElement, type ReactNode } from 'react'; + +const FocusTrap = ({ children }: { children: ReactNode }) => { + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const getFocusableElements = () => { + const focusableSelectors = 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'; + return Array.from(container.querySelectorAll(focusableSelectors)).filter( + (focusableElement) => !focusableElement.hasAttribute('disabled') && !(focusableElement as HTMLElement).hidden, + ) as HTMLElement[]; + }; + + const handleKeyDown = (event: KeyboardEvent) => { + // Check if this is the topmost trap + const allFocusTraps = document.querySelectorAll('[data-focus-trap="true"]'); + const isTopmostFocusTrap = + allFocusTraps.length > 0 && Array.from(allFocusTraps)[allFocusTraps.length - 1] === container; + + if (!isTopmostFocusTrap || event.key !== 'Tab') { + return; + } + + const focusableElements = getFocusableElements(); + if (focusableElements.length === 0) { + return; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + const activeElement = document.activeElement; + + if (!container.contains(activeElement)) { + event.preventDefault(); + firstElement.focus(); + return; + } + + if (event.shiftKey && activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + } else if (!event.shiftKey && activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + }; + + document.addEventListener('keydown', handleKeyDown, true); + + return () => { + document.removeEventListener('keydown', handleKeyDown, true); + }; + }, []); + + return cloneElement(Children.only(children) as ReactElement, { + ref: containerRef, + 'data-focus-trap': 'true', + tabIndex: -1, + }); +}; + +export default FocusTrap; diff --git a/assets/react/v3/shared/components/modals/BasicModalWrapper.tsx b/assets/react/v3/shared/components/modals/BasicModalWrapper.tsx index 5b3f69d12..d39862e28 100644 --- a/assets/react/v3/shared/components/modals/BasicModalWrapper.tsx +++ b/assets/react/v3/shared/components/modals/BasicModalWrapper.tsx @@ -1,12 +1,14 @@ +import { type SerializedStyles, css } from '@emotion/react'; +import type React from 'react'; +import { useEffect } from 'react'; + import SVGIcon from '@TutorShared/atoms/SVGIcon'; +import FocusTrap from '@TutorShared/components/FocusTrap'; import { modal } from '@TutorShared/config/constants'; import { Breakpoint, borderRadius, colorTokens, shadow, spacing } from '@TutorShared/config/styles'; import { typography } from '@TutorShared/config/typography'; import Show from '@TutorShared/controls/Show'; import { styleUtils } from '@TutorShared/utils/style-utils'; -import { type SerializedStyles, css } from '@emotion/react'; -import type React from 'react'; -import { useEffect } from 'react'; interface BasicModalWrapperProps { children: React.ReactNode; @@ -44,31 +46,32 @@ const BasicModalWrapper = ({ }, []); return ( -
+
- -
-
- {icon} - -

{title}

+
+ +
+
+ {icon} + +

{title}

+
+
+ + {subtitle}
- - {subtitle} - -
-
- + +
- - + + + } > {actions}
- +
+
{children}
-
{children}
-
+
); }; diff --git a/assets/react/v3/shared/components/modals/ModalWrapper.tsx b/assets/react/v3/shared/components/modals/ModalWrapper.tsx index 55fc40f29..a0fa540a3 100644 --- a/assets/react/v3/shared/components/modals/ModalWrapper.tsx +++ b/assets/react/v3/shared/components/modals/ModalWrapper.tsx @@ -7,6 +7,7 @@ import { styleUtils } from '@TutorShared/utils/style-utils'; import { css } from '@emotion/react'; import type React from 'react'; import { useEffect } from 'react'; +import FocusTrap from '../FocusTrap'; interface ModalWrapperProps { children: React.ReactNode; @@ -40,56 +41,58 @@ const ModalWrapper = ({ }, []); return ( -
+
- -
-
- {icon} - -
- {title} -
+
+ +
+
+ {icon} + +
+ {title} +
+
+
+ + {subtitle}
- - {subtitle} - -
-
- {headerChildren} -
-
- - - - } - > - {actions} - -
- - } - > - {entireHeader} -
+
+ {headerChildren} +
+
+ + + + } + > + {actions} + +
+ + } + > + {entireHeader} + +
+
{children}
-
{children}
-
+
); }; diff --git a/assets/react/v3/shared/hooks/usePortalPopover.tsx b/assets/react/v3/shared/hooks/usePortalPopover.tsx index ecddbd6a2..62da30ae9 100644 --- a/assets/react/v3/shared/hooks/usePortalPopover.tsx +++ b/assets/react/v3/shared/hooks/usePortalPopover.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/react'; import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import FocusTrap from '@TutorShared/components/FocusTrap'; import { useModal } from '@TutorShared/components/modals/Modal'; import { zIndex } from '@TutorShared/config/styles'; import { AnimatedDiv, AnimationType, useAnimation } from '@TutorShared/hooks/useAnimation'; @@ -223,17 +224,19 @@ export const Portal = ({ if (openState) { return createPortal( -
-
{ - event.stopPropagation(); - onClickOutside?.(); - }} - /> - {children} -
+ +
+
{ + event.stopPropagation(); + onClickOutside?.(); + }} + /> + {children} +
+ , document.body, ); diff --git a/includes/tutor-general-functions.php b/includes/tutor-general-functions.php index 78d659571..185a63fa9 100644 --- a/includes/tutor-general-functions.php +++ b/includes/tutor-general-functions.php @@ -1451,7 +1451,7 @@ function tutor_get_all_active_payment_gateways() { $name = $method['name']; $basename = "tutor-{$name}/tutor-{$name}.php"; $is_plugin_activated = is_plugin_active( $basename ); - if ( ! $is_manual && ! $is_plugin_activated ) { + if ( ! $is_manual && 'paypal' !== $name && ! $is_plugin_activated ) { continue; }