Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/rules/scratch-gui/smalruby-prettier-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ upstream (Scratch) ファイルは対象外。
- `src/components/koshien-test-modal/`
- `src/components/mobile-bottom-tabs/`
- `src/components/mobile-gui/`
- `src/components/mobile-palette-auto-closer/`
- `src/components/mobile-top-bar/`
- `src/components/narrow-screen-warning/`
- `src/components/palette-toggle/`
Expand Down Expand Up @@ -188,6 +189,7 @@ upstream (Scratch) ファイルは対象外。
- `test/unit/components/action-menu.test.jsx`
- `test/unit/components/connected-step.test.jsx`
- `test/unit/components/mobile-bottom-tabs.test.jsx`
- `test/unit/components/mobile-palette-auto-closer.test.jsx`
- `test/unit/components/mobile-top-bar.test.jsx`
- `test/unit/components/narrow-screen-warning.test.jsx`
- `test/unit/components/palette-toggle.test.jsx`
Expand Down
2 changes: 2 additions & 0 deletions packages/scratch-gui/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ src/components/*
!src/components/koshien-test-modal/
!src/components/mobile-bottom-tabs/
!src/components/mobile-gui/
!src/components/mobile-palette-auto-closer/
!src/components/mobile-top-bar/
!src/components/narrow-screen-warning/
!src/components/palette-toggle/
Expand Down Expand Up @@ -251,6 +252,7 @@ test/unit/components/*
!test/unit/components/action-menu.test.jsx
!test/unit/components/connected-step.test.jsx
!test/unit/components/mobile-bottom-tabs.test.jsx
!test/unit/components/mobile-palette-auto-closer.test.jsx
!test/unit/components/mobile-top-bar.test.jsx
!test/unit/components/narrow-screen-warning.test.jsx
!test/unit/components/palette-toggle.test.jsx
Expand Down
14 changes: 11 additions & 3 deletions packages/scratch-gui/src/components/mobile-gui/mobile-gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import GUI from '../../containers/gui.jsx';
import ConnectedIntlProvider from '../../lib/connected-intl-provider.jsx';
import MobileBottomTabs from '../mobile-bottom-tabs/mobile-bottom-tabs.jsx';
import MobilePaletteAutoCloser from '../mobile-palette-auto-closer/mobile-palette-auto-closer.jsx';
import MobileTopBar from '../mobile-top-bar/mobile-top-bar.jsx';

/**
Expand All @@ -18,14 +19,18 @@ import MobileTopBar from '../mobile-top-bar/mobile-top-bar.jsx';
* 進捗:
* - PR-2A: スケルトン (本コンポーネントの作成、<GUI> 素通し)
* - PR-2B: ボトムタブ × 5 (<MobileBottomTabs /> を追加)
* - PR-2C: ステージ全画面プレビュー (本 PR、<MobileTopBar /> の ▶ で
* - PR-2C: ステージ全画面プレビュー (<MobileTopBar /> の ▶ で
* upstream の isFullScreen mode に入る)
* - PR-2D: ブロックパレットドロワー (予定)
* - PR-2D: ブロックパレットドロワー (本 PR、<MobilePaletteAutoCloser /> で
* 初回エントリー時とブロックドラッグ開始時にパレットを自動クローズ。
* パレット表示・非表示の手動切替は upstream の <PaletteToggle> を
* 共有 — モバイル時のみ CSS でハンドルを大きくする)
* - PR-2E: ハンバーガーメニュー (予定)
*
* 受け取る props は <GUI> と同一 (AppStateHOC / HashParserHOC からの全 props)。
* @param {object} props - <GUI> と同じ props
* @returns {JSX.Element} <GUI> + <MobileTopBar /> + <MobileBottomTabs />
* @returns {JSX.Element} <GUI> + <MobileTopBar /> + <MobileBottomTabs /> +
* <MobilePaletteAutoCloser />
*/
const MobileGui = props => (
<>
Expand All @@ -34,11 +39,14 @@ const MobileGui = props => (
* MobileTopBar / MobileBottomTabs は body 直下に Portal で出すため、
* <GUI> 内側の IntlProvider context を使えない。
* 別途 ConnectedIntlProvider で包んで FormattedMessage を有効化する。
* MobilePaletteAutoCloser は描画しないが、connect でディスパッチを
* 受け取るため同じ Provider 配下に置く。
*/}
<ConnectedIntlProvider>
<>
<MobileTopBar />
<MobileBottomTabs />
<MobilePaletteAutoCloser />
</>
</ConnectedIntlProvider>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './mobile-palette-auto-closer.jsx';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
import { connect } from 'react-redux';

import { hidePalette } from '../../reducers/palette-visibility.js';

/**
* MobileGui 配下で動くロジック専用コンポーネント (描画なし)。
*
* 役割: Blockly のフライアウトからブロックを workspace へドラッグし出した
* (= 確実に block drag が成立した) 瞬間にパレットを自動的に閉じる。
* scratch-blocks の `Blockly.Events.DRAG_OUTSIDE` を購読する。
*
* 設計上の選択:
* - DOM レベルの pointerdown / pointermove は flyout 内のリスト・スクロール
* など block drag 以外のジェスチャーも誤って拾ってしまい、Blockly の
* `WorkspaceDragger` が動作中の dispatch によって `contentLeft` が null
* になりクラッシュするケースがあったため採用しなかった。
* - DRAG_OUTSIDE は「block が flyout を抜けて workspace に入った」時点なので、
* block drag が確実に成立してからの dispatch で安全。
* - 初回マウント時の auto-hide は行わない (起動時はパレット表示のままが直感的、
* 要望)。
*
* Blockly workspace は scratch-blocks の `getMainWorkspace()` で取得する
* (blocks-gesture-recovery と同じ方式)。
* @param {object} props - props
* @param {Function} props.onHide - hidePalette ディスパッチャ
* @returns {null} 描画しない
*/
const MobilePaletteAutoCloserComponent = ({ onHide }) => {
useEffect(() => {
if (typeof window === 'undefined') return () => {};
let cancelled = false;
let detach = () => {};
const tryAttach = () => {
if (cancelled) return;
const ScratchBlocks = require('scratch-blocks');
const workspace = ScratchBlocks.getMainWorkspace?.();
if (!workspace || !workspace.addChangeListener) {
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(tryAttach);
}
return;
}
const Events = ScratchBlocks.Events || {};
// 観察した結果 (issue #572 検証):
// - フライアウトからブロックを drag → workspace に出した瞬間に
// `create` (Events.CREATE) が発火する
// - プロジェクト読み込みや undo でも `create` は発火するので、
// ユーザーが currentGesture を持っている (= 手で drag 中) ときだけ
// 反応する
const createType = Events.CREATE || 'create';
const listener = event => {
if (event.type !== createType) return;
if (!workspace.currentGesture_) return;
onHide();
};
workspace.addChangeListener(listener);
detach = () => {
if (workspace.removeChangeListener) {
workspace.removeChangeListener(listener);
}
};
};
tryAttach();
return () => {
cancelled = true;
detach();
};
}, [onHide]);

return null;
};

MobilePaletteAutoCloserComponent.propTypes = {
onHide: PropTypes.func.isRequired,
};

const mapDispatchToProps = dispatch => ({
onHide: () => dispatch(hidePalette()),
});

const MobilePaletteAutoCloser = connect(null, mapDispatchToProps)(MobilePaletteAutoCloserComponent);

export default MobilePaletteAutoCloser;
export { MobilePaletteAutoCloserComponent };
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,22 @@
border-left: none;
left: 0;
}

/* 狭幅 (mobile_gui モード相当) ではタップしやすいよう拡大 (issue #572 Phase 2-D)。 */
@media (max-width: 767px) {
.palette-toggle-button {
width: 28px;
height: 56px;
font-size: 16px;
font-weight: bold;
background-color: rgba(133, 92, 214, 0.92);
color: white;
border: 0;
border-radius: 0 8px 8px 0;
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.18);
}

.palette-toggle-button:hover {
background-color: rgba(133, 92, 214, 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-env jest */
import React from 'react';
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import { MobilePaletteAutoCloserComponent } from '../../../src/components/mobile-palette-auto-closer/mobile-palette-auto-closer.jsx';

describe('MobilePaletteAutoCloser', () => {
test('does NOT call onHide on mount (palette stays visible at startup)', () => {
const onHide = jest.fn();
render(<MobilePaletteAutoCloserComponent onHide={onHide} />);
expect(onHide).not.toHaveBeenCalled();
});

test('renders nothing (no DOM output)', () => {
const { container } = render(<MobilePaletteAutoCloserComponent onHide={() => {}} />);
expect(container.firstChild).toBeNull();
});
});
Loading