From bcd61fcf5e094aaee9cd0f6378446760a0af3fab Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 15 Oct 2026 00:45:05 +0200 Subject: [PATCH] u --- src/components/article/searchFocus.js | 3 + src/components/manifest/QuoteList.jsx | 149 +++++++++++++------------- src/main.jsx | 5 + src/theme/themes/default.js | 6 ++ src/utils/touch.js | 55 ++++++++++ 5 files changed, 145 insertions(+), 73 deletions(-) create mode 100644 src/utils/touch.js diff --git a/src/components/article/searchFocus.js b/src/components/article/searchFocus.js index 3c51f02..c4164e0 100644 --- a/src/components/article/searchFocus.js +++ b/src/components/article/searchFocus.js @@ -1,3 +1,5 @@ +import { isTouchDevice } from '../../utils/touch'; + const FOCUSABLE = 'a, button, input, textarea, select, [role="button"], [role="menuitem"], [tabindex]:not([tabindex="-1"]), [contenteditable="true"]'; @@ -9,6 +11,7 @@ const FOCUSABLE = * focus afterwards, unless the user just selected some text. */ export function preserveSearchFocus(e) { + if (isTouchDevice()) return; if (e.target.closest(FOCUSABLE)) return; const selection = window.getSelection?.(); if (selection && !selection.isCollapsed) return; diff --git a/src/components/manifest/QuoteList.jsx b/src/components/manifest/QuoteList.jsx index de1ace1..1caa075 100644 --- a/src/components/manifest/QuoteList.jsx +++ b/src/components/manifest/QuoteList.jsx @@ -88,9 +88,6 @@ class QuoteRow extends PureComponent { } return ( - - - @@ -104,6 +101,9 @@ class QuoteRow extends PureComponent { + + + @@ -121,75 +121,6 @@ class QuoteRow extends PureComponent { return ( onOpen?.(quote)}> - {/* Document SkeleHash / PDF Previews (one small icon per file) */} - - {files.map((file, index) => { - const fileId = file.fileId ?? file.id; - const title = file.name ? `${file.name} (Öffnen)` : 'Datei öffnen'; - return ( - - { - e.stopPropagation(); - if (quote.id != null && fileId != null) { - window.open(`/api/quote/${encodeURIComponent(quote.id)}/file/${encodeURIComponent(fileId)}`, '_blank'); - } else { - onOpen?.(quote); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.stopPropagation(); - e.preventDefault(); - if (quote.id != null && fileId != null) { - window.open(`/api/quote/${encodeURIComponent(quote.id)}/file/${encodeURIComponent(fileId)}`, '_blank'); - } - } - }} - sx={{ - width: 18, - height: 24, - flexShrink: 0, - borderRadius: 0.5, - overflow: 'hidden', - bgcolor: 'background.paper', - border: '1px solid', - borderColor: 'divider', - boxShadow: '0 1px 2px rgba(0,0,0,0.08)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - cursor: 'pointer', - transition: 'transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease', - '&:hover': { - boxShadow: '0 2px 6px rgba(0,0,0,0.22)', - borderColor: 'primary.main', - transform: 'scale(1.2)', - zIndex: 2, - }, - }} - > - {file.skeledash ? ( - - ) : ( - - )} - - - ); - })} - - {quote.number} @@ -228,6 +159,78 @@ class QuoteRow extends PureComponent { }} /> + {/* Files / Attachments column after Pos.: space for 4 per row, 2 lines max */} + + {files.slice(0, 8).map((file, index) => { + const fileId = file.fileId ?? file.id; + const title = file.name ? `${file.name} (Öffnen)` : 'Datei öffnen'; + return ( + + { + e.stopPropagation(); + if (quote.id != null && fileId != null) { + window.open(`/api/quote/${encodeURIComponent(quote.id)}/file/${encodeURIComponent(fileId)}`, '_blank'); + } else { + onOpen?.(quote); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + e.preventDefault(); + if (quote.id != null && fileId != null) { + window.open(`/api/quote/${encodeURIComponent(quote.id)}/file/${encodeURIComponent(fileId)}`, '_blank'); + } + } + }} + sx={{ + width: 15, + height: 20, + flexShrink: 0, + borderRadius: 0.5, + overflow: 'hidden', + bgcolor: 'background.paper', + border: '1px solid', + borderColor: 'divider', + boxShadow: '0 1px 2px rgba(0,0,0,0.08)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + transition: 'transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease', + '&:hover': { + boxShadow: '0 2px 6px rgba(0,0,0,0.25)', + borderColor: 'primary.main', + transform: 'scale(1.3)', + zIndex: 3, + }, + }} + > + {file.skeledash ? ( + + ) : ( + + )} + + + ); + })} + + - Nr. / Ref. Datum Kunde Pos. + Dateien Gesamt diff --git a/src/main.jsx b/src/main.jsx index 55e2824..642095e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -8,6 +8,11 @@ import AuthBootstrap from './components/AuthBootstrap'; import App from './App'; import { getActiveTheme } from './theme'; import { installCssVars } from './colors'; +import { disableTouchAutofocus } from './utils/touch'; + +// Globally suppress automatic focus on touch devices to avoid virtual keyboard popups +disableTouchAutofocus(); + // Side-effect imports: each module registers its automat and subscribes to // authAutomat, so the stores must be evaluated before the session bootstraps. import './automats'; diff --git a/src/theme/themes/default.js b/src/theme/themes/default.js index 90cc239..c87eca6 100644 --- a/src/theme/themes/default.js +++ b/src/theme/themes/default.js @@ -15,6 +15,8 @@ import { NAV_ITEM_ACTIVE_TEXT, } from '../../colors'; +import { isTouchDevice } from '../../utils/touch'; + const nav = { background: NAV_BACKGROUND, backgroundColor: NAV_SURFACE, @@ -33,6 +35,7 @@ const nav = { export const defaultThemeId = 'default'; export function createDefaultTheme() { + const isTouch = isTouchDevice(); return createTheme({ palette: { mode: 'light', @@ -47,11 +50,13 @@ export function createDefaultTheme() { MuiModal: { defaultProps: { disableScrollLock: true, + disableAutoFocus: isTouch, }, }, MuiDialog: { defaultProps: { disableScrollLock: true, + disableAutoFocus: isTouch, }, }, MuiMenu: { @@ -91,6 +96,7 @@ export function createDefaultTheme() { defaultProps: { transitionDuration: 0, disableScrollLock: true, + disableAutoFocus: isTouch, }, }, }, diff --git a/src/utils/touch.js b/src/utils/touch.js new file mode 100644 index 0000000..361d5aa --- /dev/null +++ b/src/utils/touch.js @@ -0,0 +1,55 @@ +/** + * Detects if the primary pointing device or environment is a touch device + * (e.g. tablet, phone, POS touchscreen terminal). + * Returns false on desktop devices with mice/trackpads even if they have touchscreens. + */ +export function isTouchDevice() { + if (typeof window === 'undefined') return false; + if (window.matchMedia) { + if (window.matchMedia('(pointer: coarse)').matches) { + return true; + } + if (window.matchMedia('(pointer: fine)').matches) { + return false; + } + } + return 'ontouchstart' in window || (navigator.maxTouchPoints > 0); +} + +/** + * Globally disables autofocus on touch devices to prevent virtual keyboard popups. + * Physical taps on inputs still focus and open the keyboard normally. + */ +export function disableTouchAutofocus() { + if (!isTouchDevice()) return; + + // 1. Intercept programmatic focus called during mount/render on autofocus elements + const originalFocus = HTMLElement.prototype.focus; + HTMLElement.prototype.focus = function (options) { + if (this.hasAttribute?.('autofocus') || this.autofocus) { + this.removeAttribute?.('autofocus'); + try { + this.autofocus = false; + } catch { + // ignore + } + return; + } + + const stack = new Error().stack || ''; + if (stack.includes('commitMount') || stack.includes('commitHostUpdate')) { + return; + } + + return originalFocus.call(this, options); + }; + + // 2. Prevent browser from honoring native autofocus attribute on dynamically inserted elements + const originalSetAttribute = Element.prototype.setAttribute; + Element.prototype.setAttribute = function (name, value) { + if (typeof name === 'string' && name.toLowerCase() === 'autofocus') { + return; + } + return originalSetAttribute.call(this, name, value); + }; +}