mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-11-04 04:04:17 -05:00
When mobile users open the quick-add offcanvas and tap 'New saved seed' or 'New harvest', the click was intercepted to launch a React modal without closing the offcanvas. Bootstrap's active FocusTrap on the offcanvas intercepted subsequent focusin events on document and pulled focus back to the offcanvas, immediately blurring input fields and preventing Android's virtual keyboard from showing. Dismiss any open offcanvases when record dialog links are clicked and when Modal mounts, and restore focus to the offcanvas trigger button when the modal closes. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Brenda Wallace <brenda@wallace.net.nz>
72 lines
2.7 KiB
React
72 lines
2.7 KiB
React
import React, {useEffect, useRef, useState} from 'react';
|
|
|
|
import {getJson} from '../api';
|
|
import PlantingFlow from './PlantingFlow';
|
|
|
|
function closeOpenOffcanvases(trigger) {
|
|
const container = trigger?.closest?.('.offcanvas');
|
|
if (container) {
|
|
const toggle = document.querySelector(`[data-bs-target="#${container.id}"], [href="#${container.id}"]`);
|
|
if (toggle && toggle.focus) toggle.focus();
|
|
}
|
|
|
|
const openOffcanvases = Array.from(document.querySelectorAll('.offcanvas.show'));
|
|
const targets = new Set(container ? [container, ...openOffcanvases] : openOffcanvases);
|
|
|
|
targets.forEach((el) => {
|
|
if (window.bootstrap?.Offcanvas) {
|
|
const instance = window.bootstrap.Offcanvas.getInstance(el) || window.bootstrap.Offcanvas.getOrCreateInstance(el);
|
|
instance.hide();
|
|
} else {
|
|
el.classList.remove('show');
|
|
}
|
|
});
|
|
}
|
|
|
|
// The record harvest and save seeds dialogs for every server-rendered link to
|
|
// the full form. Nothing of its own is drawn: it listens for clicks on any
|
|
// [data-record-dialog="harvest"] or [data-record-dialog="seeds"] link on the
|
|
// page and opens the same dialog the home page does. A link for a particular
|
|
// crop says which in data-crop-id and data-crop-name. The layout mounts it once.
|
|
//
|
|
// The member's plantings come from plantingsUrl when first needed. The link's
|
|
// own href stays the fallback without JavaScript, and when the plantings cannot
|
|
// be loaded.
|
|
export default function RecordDialog({plantings_url: plantingsUrl}) {
|
|
const [opened, setOpened] = useState(null); // {action, crop}, or null when closed
|
|
const [plantings, setPlantings] = useState(null);
|
|
const open = useRef(null);
|
|
open.current = async (trigger) => {
|
|
const action = trigger.dataset.recordDialog;
|
|
const crop = trigger.dataset.cropId ? {id: Number(trigger.dataset.cropId), name: trigger.dataset.cropName} : null;
|
|
if (!plantings) {
|
|
try {
|
|
const {ok, data} = await getJson(plantingsUrl);
|
|
if (!ok) throw new Error('load failed');
|
|
setPlantings(data);
|
|
} catch (error) {
|
|
window.location.href = trigger.getAttribute('href');
|
|
return;
|
|
}
|
|
}
|
|
setOpened({action, crop});
|
|
};
|
|
|
|
useEffect(() => {
|
|
function onClick(event) {
|
|
const trigger = event.target.closest('[data-record-dialog]');
|
|
if (!trigger) return;
|
|
event.preventDefault();
|
|
closeOpenOffcanvases(trigger);
|
|
open.current(trigger);
|
|
}
|
|
|
|
document.addEventListener('click', onClick);
|
|
return () => document.removeEventListener('click', onClick);
|
|
}, []);
|
|
|
|
if (!opened || !plantings) return null;
|
|
|
|
return <PlantingFlow action={opened.action} crop={opened.crop} plantings={plantings} onClose={() => setOpened(null)} />;
|
|
}
|