QuestブラウザでWebXRでのシステムキーボードを試す

**この記事は [WebXR ( WebVR/WebAR ) Advent Calendar 2022](https://adventar.org/calendars/7401) 11日目になります。**

WebXR関係の先行的な機能対応に積極的なMetaQuest ブラウザで、2022年12月ににv24.4で突然実験的機能が実装されました。

待望の、WebXRのVR session内で、システムキーボードを呼び出すというものです。これがあれば自前でバーチャルキーボードを実装する必要がなく、文字入力が可能になります。

![com.oculus.browser-20221210-231849.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74838/3bf81731-5f17-964b-6d26-c2918b417501.jpeg)


注意点としては、現段階ではこれはあくまでMetaQuestブラウザの独自機能であり、まだW3Cの俎上にも上がっていないものです。今後標準化の方向に向かうことを期待したいと思います。

## システムキーボードの使い方

仕様はこちら

https://developer.oculus.com/documentation/web/webxr-keyboard/

まず、この機能を使うには、chromeのフラグで有効にする必要があります。
chrome://flags ページで、WebXR Experiments をEnableに設定します。

![com.oculus.shellenv-20221210-000227.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74838/8ecdf24e-01fa-dd92-d574-3d20ed9aeb2b.jpeg)

この機能が使えるかどうかのチェックはVRSessionにフラグがあります。

```js
if (session.isSystemKeyboardSupported) {
  // ...
}
```

キーボードの出し方は至ってシンプル。普通のinputタグに対して、VR session内でfocus()を呼び出すと、システムキーボードが現れるというものです。

```js
    inputElement.focus()
```

入力された文字は、inputElement.value で取得できますが、自動的にどこかに表示されるわけではないので、そこは自前で実装する必要があります。

入力に対してはoninputでリアルタイムな入力値が取得できます。またキーボードが現れたり消えたりするタイミングのイベントも取得できます。

現時点でキーのdown/upイベントは取得できないようです。

inputフィールドは最初からあるものでも、セッション開始時に作成したエレメントでもどちらでも使えますが、セッション終了時に破棄する必要があり、再利用はできないようです。

## サンプルを作ってみた

とりあえず、キーボードを出して、入力された文字を表示するだけのサンプルです。

EnterVRかEnterARでVR sessionに入った後、右コントローラのAボタンでキーボードが開きます。

https://polybaas.com/pbe/?m7h3RrAYD4WONgVR7DN5

[OculusQuest で開く](https://www.oculus.com/open_url/?url=https%3A%2F%2Fpolybaas.com%2Fpbe%2F%3Fm7h3RrAYD4WONgVR7DN5)

:::note
上のリンク先のサンプルは、ブラウザIDE Polybaasで作成されています。左のソースペインにはA-Frameのタグが表示されていますが、タブをcomponentに切り替えるとcomponentのソースを見ることができます。
:::

https://twitter.com/wakufactory/status/1601588875520266240

以下はA-Frameのコンポーネント設定部分のソースです。

```js
// キー入力パネル
AFRAME.registerComponent('kbdpanel',{
	init:function() {
		this.line = 0 
		this.settext("text area",true)
		// enter VR 
		this.el.sceneEl.addEventListener("enter-vr", ev=>{
			POXA.log("enter")
			const ss = this.el.sceneEl.xrSession
			// キーボードが使えるかチェック
			if(!ss.isSystemKeyboardSupported) {POXA.log("kbd disabled"); return }
			this.settext("kbd enabled")
			// input field 作成
			const text = document.createElement('input')
			text.type = "text"
			this.telem = text 
			document.body.appendChild(text)
			// input event
			text.addEventListener("input",ev=>{
				this.settext(ev.target.value,false)
			})
			// visibility change event
			ss.addEventListener('visibilitychange',ev=>{
				const st = ev.session.visibilityState
				if(st=="visible-blurred") {
					POXA.log("kbd open")
				} else if(st=="visible") {
					POXA.log("kbd close")
					this.settext(this.telem.value,true)
				}
				POXA.log(ev.session.visibilityState)
			})
			//Aボタンでキーボード表示
			$('contr').addEventListener("abuttonchanged",ev=>{
	  		if(ev.detail.pressed) this.telem.focus()
		  })
		})
		// exit VR 
		this.el.sceneEl.addEventListener("exit-vr", ev=>{
			POXA.log("exit")
			this.telem.remove()	// inputの削除
		})
	}
    //テキスト描画
	settext:function(text,nl=false) {
		const cp = this.el.components.cpanel 
		cp.data.text[this.line] = text 
		cp.update()
		if(nl) if(this.line++>10) this.line = 1
	}
})
```