Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: safari focus #997

Merged
merged 13 commits into from
Oct 17, 2023
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"__MESSAGING_GLOBALS__": true,
"__ENV__": true
},
"plugins": ["prettier"],
"plugins": ["prettier","eslint-comments"],
"rules": {
"arrow-body-style": "off",
"unicorn/prefer-spread": "off",
Expand Down
12 changes: 0 additions & 12 deletions src/components/modal/lib/hooks/helpers.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import { useEffect, useLayoutEffect, useRef } from 'preact/hooks';

export function useAutoFocus() {
const ref = useRef();

useEffect(() => {
if (ref.current) {
ref.current.focus();
}
});

return ref;
}

export function useDidUpdateEffect(fn, deps) {
const mounted = useRef(false);

Expand Down
12 changes: 0 additions & 12 deletions src/components/modal/v2/lib/hooks/helpers.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import { useEffect, useLayoutEffect, useRef } from 'preact/hooks';

export function useAutoFocus() {
const ref = useRef();

useEffect(() => {
if (ref.current) {
ref.current.focus();
}
});

return ref;
}

export function useDidUpdateEffect(fn, deps) {
const mounted = useRef(false);

Expand Down
8 changes: 4 additions & 4 deletions src/components/modal/v2/lib/providers/transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ export const STATUS = {
};

const TransitionContext = createContext({
status: STATUS.OPEN,
status: STATUS.CLOSED,
setStatus: () => {}
});

export const TransitionStateProvider = ({ children }) => {
const { onShow } = useXProps();
const [state, setState] = useState(STATUS.OPEN);
const [state, setState] = useState(STATUS.CLOSED);

/**
* Set iniitial focus on modal open to the close button.
* Particularly useful for those using screen readers and other accessibility functions.
*/
const focusCloseBtnOnModalOpen = () => {
const btn = document.querySelector('.close');
btn?.focus();
// focus the close button
document.querySelector('.close')?.focus();
};

useEffect(() => {
Expand Down
32 changes: 21 additions & 11 deletions src/components/modal/v2/parts/BodyContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import {
import Header from './Header';
import { LongTerm, ShortTerm, NoInterest, ProductList, PayIn1 } from './views';

const VIEW_IDS = {
// TODO: add an error view in case we receive an invalid view?
PAYPAL_CREDIT_NO_INTEREST: 'PAYPAL_CREDIT_NO_INTEREST',
PAY_LATER_LONG_TERM: 'PAY_LATER_LONG_TERM',
PAY_LATER_PAY_IN_1: 'PAY_LATER_PAY_IN_1',
PAY_LATER_SHORT_TERM: 'PAY_LATER_SHORT_TERM',
PRODUCT_LIST: 'PRODUCT_LIST'
};
Seavenly marked this conversation as resolved.
Show resolved Hide resolved

const BodyContent = () => {
const { views } = useServerData();
const { offer } = useXProps();
Expand All @@ -29,12 +38,12 @@ const BodyContent = () => {

let defaultViewName;

const productViews = views.filter(view => view?.meta?.product !== 'PRODUCT_LIST');
const hasProductList = views.find(view => view?.meta?.product === 'PRODUCT_LIST');
const productViews = views.filter(view => view?.meta?.product !== VIEW_IDS.PRODUCT_LIST);
const hasProductList = views.find(view => view?.meta?.product === VIEW_IDS.PRODUCT_LIST);
if (productViews?.length === 1) {
defaultViewName = productViews[0]?.meta?.product;
} else if (productViews?.length > 1 && hasProductList) {
defaultViewName = 'PRODUCT_LIST';
defaultViewName = VIEW_IDS.PRODUCT_LIST;
} else if (productViews?.length > 1 && !hasProductList) {
defaultViewName = productViews[0]?.meta?.product;
}
Expand All @@ -49,12 +58,13 @@ const BodyContent = () => {
const { headline, subheadline, qualifyingSubheadline = '', closeButtonLabel } = content;
const isQualifying = productMeta?.qualifying;

const openProductList = () => setViewName('PRODUCT_LIST');
const openProductList = () => setViewName(VIEW_IDS.PRODUCT_LIST);

useDidUpdateEffect(() => {
scrollTo(0); // Reset scroll position to top when view changes
const closeButton = window.document.querySelector('#close-btn');
if (closeButton) closeButton.focus();
if (transitionState === 'OPEN') {
window.document.querySelector('#close-btn')?.focus();
}
}, [viewName]);

useDidUpdateEffect(() => {
Expand All @@ -71,13 +81,13 @@ const BodyContent = () => {

// Add views to viewComponents object where the keys are the product name and the values are the view component
const viewComponents = {
PAYPAL_CREDIT_NO_INTEREST: <NoInterest content={content} openProductList={openProductList} />,
PAY_LATER_LONG_TERM: <LongTerm content={content} openProductList={openProductList} />,
PAY_LATER_PAY_IN_1: <PayIn1 content={content} openProductList={openProductList} />,
PAY_LATER_SHORT_TERM: (
[VIEW_IDS.PAYPAL_CREDIT_NO_INTEREST]: <NoInterest content={content} openProductList={openProductList} />,
[VIEW_IDS.PAY_LATER_LONG_TERM]: <LongTerm content={content} openProductList={openProductList} />,
[VIEW_IDS.PAY_LATER_PAY_IN_1]: <PayIn1 content={content} openProductList={openProductList} />,
[VIEW_IDS.PAY_LATER_SHORT_TERM]: (
<ShortTerm content={content} productMeta={productMeta} openProductList={openProductList} />
),
PRODUCT_LIST: <ProductList content={content} setViewName={setViewName} />
[VIEW_IDS.PRODUCT_LIST]: <ProductList content={content} setViewName={setViewName} />
};

// IMPORTANT: These elements cannot be nested inside of other elements.
Expand Down
1 change: 1 addition & 0 deletions src/components/modal/v2/parts/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const Header = ({
aria-label={closeButtonLabel}
type="button"
id="close-btn"
aria-keyshortcuts="escape"
onClick={() => handleClose('Close Button')}
>
<Icon name="close" />
Expand Down
4 changes: 4 additions & 0 deletions src/components/modal/v2/styles/components/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@
cursor: pointer;
z-index: 9;

&:focus {
outline: -webkit-focus-ring-color solid 2px;
}

@include desktop {
margin-left: auto;
margin-right: 2px;
Expand Down
17 changes: 2 additions & 15 deletions src/library/zoid/message/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,7 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () =>
const { onClick } = props;

return ({ meta }) => {
const {
modal,
index,
account,
merchantId,
currency,
amount,
buyerCountry,
onApply,
getContainer
} = props;
const { modal, index, account, merchantId, currency, amount, buyerCountry, onApply } = props;
const { offerType, offerCountry, messageRequestId } = meta;

// Avoid spreading message props because both message and modal
Expand All @@ -160,10 +150,7 @@ export default createGlobalVariableGetter('__paypal_credit_message__', () =>
offerCountry,
refId: messageRequestId,
refIndex: index,
src: 'message_click',
onClose: () => {
getContainer().querySelector('iframe').focus();
}
src: 'message_click'
});

logger.track({
Expand Down
17 changes: 16 additions & 1 deletion src/library/zoid/modal/containerTemplate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon
// cannot overlay across the entire screen
if (context === 'popup') return undefined;

state.previousFocus = document.activeElement;
Seavenly marked this conversation as resolved.
Show resolved Hide resolved
const [hijackViewport, replaceViewport] = viewportHijack();

const CLASS = {
Expand All @@ -30,13 +31,19 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon

const handleShow = () => {
state.open = true;
state.previousFocus = document.activeElement;
wrapper.classList.remove(CLASS.HIDDEN);
hijackViewport();
// Browser needs to repaint otherwise the transition happens immediately
// Firefox requires 2 RAFs due to where they are called in the event loop
requestAnimationFrame(() => {
requestAnimationFrame(() => {
overlay.classList.add(CLASS.MODAL_SHOW);
if (state.renderedModal && window.document.activeElement !== frame) {
frame.focus();
} else if (window.document.activeElement !== prerenderFrame) {
prerenderFrame.focus();
}
});
});
};
Expand All @@ -47,6 +54,7 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon
replaceViewport();
setTimeout(() => {
wrapper.classList.add(CLASS.HIDDEN);
state.previousFocus.focus();
}, TRANSITION_DELAY);
};

Expand All @@ -57,11 +65,18 @@ export default ({ uid, frame, prerenderFrame, doc, event, state, props: { cspNon
};

const handleTransition = () => {
state.renderedModal = true;
ZalgoPromise.delay(TRANSITION_DELAY)
.then(() => overlay.classList.add(CLASS.TRANSITION))
.then(() => ZalgoPromise.delay(TRANSITION_DELAY))
.then(() => destroyElement(prerenderFrame));
.then(() => destroyElement(prerenderFrame))
.then(() => {
if (state.open && document.activeElement !== frame) {
Seavenly marked this conversation as resolved.
Show resolved Hide resolved
frame.focus();
}
});
};

// When the show function was called before zoid had a chance to render
if (state.open) {
handleShow();
Expand Down
Loading
Loading