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/components/mobile-gui/mobile-gui.css b/packages/scratch-gui/src/components/mobile-gui/mobile-gui.css index 828d49d1ef7..d83e88d40f1 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; } @@ -419,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; +} diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index ba23ebdd68a..768e60f3cb1 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -117,6 +117,17 @@ 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; + this._procedureJustActivated = false; + this._procedureActivatedTimer = null; + // === Smalruby: End of iOS flyout touch bleed fix === } componentDidMount () { this.ScratchBlocks = VMScratchBlocks(this.props.vm); @@ -129,7 +140,28 @@ 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; + // 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 === this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); const workspaceConfig = defaultsDeep({}, @@ -918,19 +950,34 @@ 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; + 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 : + 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); }