From afaea91a4512fcb6c8ca4c7b4d0093c94b2cce53 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 08:23:55 +0900 Subject: [PATCH 1/6] fix(mobile-ui): fix full-screen modal layout broken on SP viewport (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mobile-gui.css の `[class*="box_box"]` セレクターが子孫セレクターになっており、 モーダル内部の全 Box 要素に `height: 100% !important` が適用されていた。 Scratch は全コンテナに Box コンポーネントを使うため、prompt の label / buttonRow 等にも同ルールが当たり、それぞれが ~342px に膨張して OK ボタンが画面外に 押し出されスクロールしないと押せない状態になっていた。 `> [class*="box_box"]` の直下セレクターに変更し、modal-content の直接の子 (outerBox = Modal が wrap する Box) にのみ適用するよう修正。 `height: 100%` は削除し `flex: 1 1 auto` で代替する。 Playwright 実測 (844×390 viewport): - prompt_body.scrollHeight: 1134px → 246px - label height: 342px → 19.5px - buttonRow height: 342px → 46.5px - OK ボタン visible: false → true Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/mobile-gui/mobile-gui.css | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css b/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css index 828d49d1ef7..aa752e20c95 100644 --- a/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css +++ b/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css @@ -394,17 +394,21 @@ } /* - * モーダル内部の box (Modal component の Box) を「縦スクロールコンテナ」と - * して扱う。box_box が overflow-y: auto となり、本文が viewport を超えても - * 内部でスクロールできる。ヘッダーは下記 sticky ルールで上部に固定する。 + * モーダル直下の outerBox だけを「縦スクロールコンテナ」として扱う。 + * `> [class*="box_box"]` の直下セレクターで modal-content の直接の子 Box + * (= Modal component が wrap する outerBox) にのみ適用する。 + * + * descendant selector (スペース) にすると、Scratch が全コンテナに Box を + * 使うため prompt の label / buttonRow などネストされた Box 全てに + * height: 100% が当たり、それぞれが ~342px に膨張して OK ボタンが + * 画面外に押し出される (#698)。 * * iOS Safari の慣性スクロール対応として `-webkit-overflow-scrolling: touch` も付ける。 */ -:global(body.smalruby-mobile-mode [class*="modal_modal-content"] [class*="box_box"]) { +:global(body.smalruby-mobile-mode [class*="modal_modal-content"] > [class*="box_box"]) { flex: 1 1 auto !important; min-height: 0 !important; width: 100% !important; - height: 100% !important; overflow-y: auto !important; -webkit-overflow-scrolling: touch !important; } From 75ccd4b26947624d369b55c1317384d114c868a3 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 09:09:25 +0900 Subject: [PATCH 2/6] fix(mobile-ui): cancel spurious variable prompt when Make a Block is tapped on iOS (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On iOS Safari with MobileGui, tapping "ブロックを作る" in the Blockly flyout can also fire the "変数を作る" button callback, showing a variable creation modal before the custom procedures modal appears. Root cause: iOS touch event propagation in the Blockly flyout causes the CREATE_VARIABLE callback to fire together with CREATE_PROCEDURE when the user taps the "ブロックを作る" button. Both callbacks use rAF+setTimeout delay, so CREATE_VARIABLE (earlier in DOM) fires first. Fix: defer handlePromptStart by 50ms. If externalProcedureDefCallback fires within that window (meaning the user tapped "ブロックを作る"), the pending variable prompt is cancelled before it can setState. Co-Authored-By: Claude Sonnet 4.6 --- .claude/rules/scratch-gui/smalruby-markers.md | 1 + .../scratch-gui/src/containers/blocks.jsx | 60 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.claude/rules/scratch-gui/smalruby-markers.md b/.claude/rules/scratch-gui/smalruby-markers.md index b5656ee3051..8dde1b5daef 100644 --- a/.claude/rules/scratch-gui/smalruby-markers.md +++ b/.claude/rules/scratch-gui/smalruby-markers.md @@ -98,6 +98,7 @@ upstream ファイルに追加した Smalruby 固有コードのマーカー一 | `src/lib/calculatePopupPosition.js` | viewport-aware popup flip | LEFT/RIGHT 配置で配置側にポップアップが収まらない場合、反対側にフリップする (issue #671: SP モードのスプライト削除確認ポップアップが画面外で押せない問題への対策) | | `src/containers/menu.jsx` | iPad menu item click fix | メニュー項目クリック時の `setTimeout` 遅延を 0 → 100ms に拡大。iPadOS Safari は `pointerup` から `click` 発火まで ~16–32ms 程度のラグがあり、setTimeout(0) で close すると `
  • ` が click 発火前に unmount され React onClick が skip される問題への対応 | | `src/containers/blocks.jsx` | palette-toggle initial render | `componentDidMount` 末尾で `_applyPaletteVisibility` を呼び `forceUpdate()` を起動。`this.workspace` はインスタンス変数なので `inject()` 後に再レンダーが走らず、初回 `render()` で workspace=null のまま PaletteToggle がスキップされる問題への対応 (issue #695) | +| `src/containers/blocks.jsx` | iOS flyout touch bleed fix | MobileGui (SP) で「ブロックを作る」タップ時に iOS の SVG タッチイベントが「変数を作る」にも伝播する問題の修正。`handlePromptStart` を 50ms 遅延して `externalProcedureDefCallback` が先に呼ばれた場合にキャンセル (issue #698) | ## 関連ファイル diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index ba23ebdd68a..77c784b6290 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -117,6 +117,15 @@ class Blocks extends React.Component { // so that all blocks get correct measurements. this._hasBeenVisible = false; // === Smalruby: End of deferred flyout rebuild === + // === Smalruby: Start of iOS flyout touch bleed fix === + // On iOS Safari, tapping "ブロックを作る" in the Blockly flyout can + // spuriously fire the "変数を作る" callback first due to SVG touch + // event propagation. These are used by handlePromptStart and the + // externalProcedureDefCallback wrapper below to cancel the spurious + // variable prompt when a procedure modal is about to open. (#698) + this._pendingPromptTimer = null; + this._pendingPromptArgs = null; + // === Smalruby: End of iOS flyout touch bleed fix === } componentDidMount () { this.ScratchBlocks = VMScratchBlocks(this.props.vm); @@ -129,7 +138,20 @@ class Blocks extends React.Component { this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; - this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; + // === Smalruby: Start of iOS flyout touch bleed fix === + // On iOS Safari with MobileGui, tapping "ブロックを作る" can also fire + // the "変数を作る" callback due to Blockly flyout event propagation. + // Wrap externalProcedureDefCallback to cancel any pending variable + // prompt before opening the custom procedures modal. (#698) + this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = (data, callback) => { + if (this._pendingPromptTimer) { + clearTimeout(this._pendingPromptTimer); + this._pendingPromptTimer = null; + } + this._pendingPromptArgs = null; + this.props.onActivateCustomProcedures(data, callback); + }; + // === Smalruby: End of iOS flyout touch bleed fix === this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); const workspaceConfig = defaultsDeep({}, @@ -918,19 +940,33 @@ class Blocks extends React.Component { setBlocks (blocks) { this.blocks = blocks; } + // === Smalruby: Start of iOS flyout touch bleed fix === + // Defer opening the variable/list prompt by 50ms so that if + // externalProcedureDefCallback fires in the same rAF+setTimeout cycle + // (iOS touch bleed), the pending prompt is cancelled before it shows. (#698) handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { - const p = {prompt: {callback, message, defaultValue}}; - p.prompt.title = optTitle ? optTitle : - this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; - p.prompt.varType = typeof optVarType === 'string' ? - optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; - p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope - optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && - p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && - p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; - p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; - this.setState(p); + if (this._pendingPromptTimer) clearTimeout(this._pendingPromptTimer); + this._pendingPromptArgs = [message, defaultValue, callback, optTitle, optVarType]; + this._pendingPromptTimer = setTimeout(() => { + const args = this._pendingPromptArgs; + this._pendingPromptArgs = null; + this._pendingPromptTimer = null; + if (!args) return; + const [msg, defVal, cb, title, varType] = args; + const p = {prompt: {callback: cb, message: msg, defaultValue: defVal}}; + p.prompt.title = title ? title : + this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; + p.prompt.varType = typeof varType === 'string' ? + varType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; + p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope + varType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && + p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && + p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; + p.prompt.showCloudOption = (varType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; + this.setState(p); + }, 50); } + // === Smalruby: End of iOS flyout touch bleed fix === handleConnectionModalStart (extensionId) { this.props.onOpenConnectionModal(extensionId); } From 1c6c8c79e22682d8219e8708d01db85b20760376 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 09:16:58 +0900 Subject: [PATCH 3/6] chore(mobile-ui): add console logs to diagnose iOS flyout touch bleed (#698) Log when handlePromptStart and externalProcedureDefCallback are called, including timestamps and stack traces, so the execution order and timing can be observed in iOS Safari devtools. Co-Authored-By: Claude Sonnet 4.6 --- packages/scratch-gui/src/containers/blocks.jsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index 77c784b6290..93ea912c6df 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -144,7 +144,15 @@ class Blocks extends React.Component { // Wrap externalProcedureDefCallback to cancel any pending variable // prompt before opening the custom procedures modal. (#698) this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = (data, callback) => { + // eslint-disable-next-line no-console + console.log('[#698 debug] externalProcedureDefCallback called', { + hasPendingPrompt: !!this._pendingPromptTimer, + t: Date.now(), + stack: new Error().stack + }); if (this._pendingPromptTimer) { + // eslint-disable-next-line no-console + console.log('[#698 debug] cancelling pending variable prompt'); clearTimeout(this._pendingPromptTimer); this._pendingPromptTimer = null; } @@ -945,6 +953,13 @@ class Blocks extends React.Component { // externalProcedureDefCallback fires in the same rAF+setTimeout cycle // (iOS touch bleed), the pending prompt is cancelled before it shows. (#698) handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { + // eslint-disable-next-line no-console + console.log('[#698 debug] handlePromptStart called', { + title: optTitle, + varType: optVarType, + t: Date.now(), + stack: new Error().stack + }); if (this._pendingPromptTimer) clearTimeout(this._pendingPromptTimer); this._pendingPromptArgs = [message, defaultValue, callback, optTitle, optVarType]; this._pendingPromptTimer = setTimeout(() => { @@ -952,6 +967,8 @@ class Blocks extends React.Component { this._pendingPromptArgs = null; this._pendingPromptTimer = null; if (!args) return; + // eslint-disable-next-line no-console + console.log('[#698 debug] handlePromptStart timer fired — showing modal', {t: Date.now()}); const [msg, defVal, cb, title, varType] = args; const p = {prompt: {callback: cb, message: msg, defaultValue: defVal}}; p.prompt.title = title ? title : From d609c84fd5b15cec2fe5f61ca4a34ee59d33ef4b Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 09:19:55 +0900 Subject: [PATCH 4/6] fix(mobile-ui): suppress spurious variable prompt when procedure modal opens first on iOS (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug logs revealed the actual execution order: - externalProcedureDefCallback fires first (t=531) - handlePromptStart fires 152ms later (t=683) - 50ms timer fires (t=734) → variable modal shows The previous fix only handled the reverse order. This adds a _procedureJustActivated flag (300ms TTL) set when externalProcedureDefCallback fires, so handlePromptStart's deferred timer suppresses the variable modal when the procedure modal has already opened. Co-Authored-By: Claude Sonnet 4.6 --- packages/scratch-gui/src/containers/blocks.jsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index 93ea912c6df..b015558a347 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -125,6 +125,8 @@ class Blocks extends React.Component { // variable prompt when a procedure modal is about to open. (#698) this._pendingPromptTimer = null; this._pendingPromptArgs = null; + this._procedureJustActivated = false; + this._procedureActivatedTimer = null; // === Smalruby: End of iOS flyout touch bleed fix === } componentDidMount () { @@ -157,6 +159,14 @@ class Blocks extends React.Component { this._pendingPromptTimer = null; } this._pendingPromptArgs = null; + // Set a flag so handlePromptStart (which may fire up to ~200ms after + // this callback) knows to skip showing the variable modal. (#698) + this._procedureJustActivated = true; + if (this._procedureActivatedTimer) clearTimeout(this._procedureActivatedTimer); + this._procedureActivatedTimer = setTimeout(() => { + this._procedureJustActivated = false; + this._procedureActivatedTimer = null; + }, 300); this.props.onActivateCustomProcedures(data, callback); }; // === Smalruby: End of iOS flyout touch bleed fix === @@ -957,6 +967,7 @@ class Blocks extends React.Component { console.log('[#698 debug] handlePromptStart called', { title: optTitle, varType: optVarType, + procedureJustActivated: this._procedureJustActivated, t: Date.now(), stack: new Error().stack }); @@ -967,6 +978,11 @@ class Blocks extends React.Component { this._pendingPromptArgs = null; this._pendingPromptTimer = null; if (!args) return; + if (this._procedureJustActivated) { + // eslint-disable-next-line no-console + console.log('[#698 debug] handlePromptStart suppressed — procedure modal just opened', {t: Date.now()}); + return; + } // eslint-disable-next-line no-console console.log('[#698 debug] handlePromptStart timer fired — showing modal', {t: Date.now()}); const [msg, defVal, cb, title, varType] = args; From bc3d7235b1c1d587fff40c600e34d66a4005b0ff Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 09:33:54 +0900 Subject: [PATCH 5/6] chore(mobile-ui): remove debug console.log for iOS flyout touch bleed fix (#698) Co-Authored-By: Claude Sonnet 4.6 --- .../scratch-gui/src/containers/blocks.jsx | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index b015558a347..768e60f3cb1 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -146,15 +146,7 @@ class Blocks extends React.Component { // Wrap externalProcedureDefCallback to cancel any pending variable // prompt before opening the custom procedures modal. (#698) this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = (data, callback) => { - // eslint-disable-next-line no-console - console.log('[#698 debug] externalProcedureDefCallback called', { - hasPendingPrompt: !!this._pendingPromptTimer, - t: Date.now(), - stack: new Error().stack - }); if (this._pendingPromptTimer) { - // eslint-disable-next-line no-console - console.log('[#698 debug] cancelling pending variable prompt'); clearTimeout(this._pendingPromptTimer); this._pendingPromptTimer = null; } @@ -963,14 +955,6 @@ class Blocks extends React.Component { // externalProcedureDefCallback fires in the same rAF+setTimeout cycle // (iOS touch bleed), the pending prompt is cancelled before it shows. (#698) handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { - // eslint-disable-next-line no-console - console.log('[#698 debug] handlePromptStart called', { - title: optTitle, - varType: optVarType, - procedureJustActivated: this._procedureJustActivated, - t: Date.now(), - stack: new Error().stack - }); if (this._pendingPromptTimer) clearTimeout(this._pendingPromptTimer); this._pendingPromptArgs = [message, defaultValue, callback, optTitle, optVarType]; this._pendingPromptTimer = setTimeout(() => { @@ -978,13 +962,7 @@ class Blocks extends React.Component { this._pendingPromptArgs = null; this._pendingPromptTimer = null; if (!args) return; - if (this._procedureJustActivated) { - // eslint-disable-next-line no-console - console.log('[#698 debug] handlePromptStart suppressed — procedure modal just opened', {t: Date.now()}); - return; - } - // eslint-disable-next-line no-console - console.log('[#698 debug] handlePromptStart timer fired — showing modal', {t: Date.now()}); + if (this._procedureJustActivated) return; const [msg, defVal, cb, title, varType] = args; const p = {prompt: {callback: cb, message: msg, defaultValue: defVal}}; p.prompt.title = title ? title : From e737a7dc98056b159a89ed534a1f99dd12d68673 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 22 May 2026 09:43:47 +0900 Subject: [PATCH 6/6] fix(mobile-ui): compact custom-procedures modal to fit iPhone landscape (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce workspace min/max-height 200→110px and compress body padding, option cards, and button row so OK button stays within 375px viewport. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/mobile-gui/mobile-gui.css | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css b/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css index aa752e20c95..d83e88d40f1 100644 --- a/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css +++ b/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css @@ -423,3 +423,46 @@ z-index: 1 !important; flex-shrink: 0 !important; } + +/* + * 「ブロックを作る」モーダルを iPhone 横向き (375px 高さ) でも画面内に収める。 + * デフォルトは workspace=200px、options-row=143px 等で合計 ~544px あり、 + * OK ボタンが完全に画面外に押し出される (#698 Fix 2)。 + * workspace を 110px に縮め、body の padding・card・button を圧縮する。 + */ +:global(body.smalruby-mobile-mode [class*="custom-procedures_workspace"]) { + min-height: 110px !important; + max-height: 110px !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_body"]) { + padding: 0.5rem 1rem !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_option-card"]) { + padding: 0.5rem !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_option-icon"]) { + max-height: 28px !important; + margin-bottom: 0.25rem !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_option-title"]) { + font-size: 0.75rem !important; + line-height: 1.2 !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_checkbox-row"]) { + margin-top: 0.5rem !important; + font-size: 0.8rem !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_button-row"]) { + margin-top: 0.5rem !important; +} + +:global(body.smalruby-mobile-mode [class*="custom-procedures_button-row"] button) { + padding: 0.5rem 0.75rem !important; + font-size: 0.8rem !important; +}