@geckou/ui-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/modal-stack.d.ts +39 -0
- package/dist/modal-stack.js +46 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,36 @@ const onKeyDown = (event: KeyboardEvent) => {
|
|
|
128
128
|
|
|
129
129
|
`ModalBox`(Vue / React)はこれを使っています。
|
|
130
130
|
|
|
131
|
+
### モーダルの重なり順
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createModalLayer } from '@geckou/ui-core'
|
|
135
|
+
|
|
136
|
+
const layer = createModalLayer()
|
|
137
|
+
layer.toggle(isOpen, dialogElement) // 表示状態と、判定に使う要素(要素は必須)
|
|
138
|
+
layer.isTopmost() // キー入力を処理してよいのは true のときだけ
|
|
139
|
+
layer.release() // アンマウント時
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
モーダルを重ねたとき、Escape や Tab を処理してよいのは最前面の 1 枚だけです。
|
|
143
|
+
`ModalBox` はハンドラを `document` に bubble で登録するため、重なると全部が同じ
|
|
144
|
+
イベントを受け取ります。実行順は DOM の深さではなく登録順で決まる(React は
|
|
145
|
+
`onClose` の同一性が変わると再登録され、effect は子から先に走る)ので、
|
|
146
|
+
順序には頼れません。
|
|
147
|
+
|
|
148
|
+
判定の決め手は **DOM の包含関係**です。入れ子のモーダルは内側が外側の中に
|
|
149
|
+
描画されるので、他のレイヤーを内包しているものは外側だと分かります。
|
|
150
|
+
互いに内包しない(入れ子でない)モーダルが並んだときだけ、後から開いたものを
|
|
151
|
+
最前面とします。
|
|
152
|
+
|
|
153
|
+
| メソッド | 説明 |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `toggle(shouldBeActive, element)` | 真偽で登録・解除する。`element` は最前面判定に使う要素(ダイアログ本体)。省略可にすると要素の無いレイヤーが積まれて包含判定が効かなくなるため必須(未取得なら明示的に `null`) |
|
|
156
|
+
| `isTopmost()` | このレイヤーが最前面か。登録していなければ `false` |
|
|
157
|
+
| `release()` | アンマウント時に呼ぶ。登録中なら解除する |
|
|
158
|
+
|
|
159
|
+
`ModalBox`(Vue / React)はこれを使っています。
|
|
160
|
+
|
|
131
161
|
### 定数・型
|
|
132
162
|
|
|
133
163
|
```ts
|
|
@@ -139,6 +169,11 @@ import type { Validates, Option, StateVariation, DateObject } from '@geckou/ui-c
|
|
|
139
169
|
|
|
140
170
|
型の一覧は [Vue パッケージの README](../vue/README.md#types) を参照してください。
|
|
141
171
|
|
|
172
|
+
## 0.6.0 の変更
|
|
173
|
+
|
|
174
|
+
- `modal-stack` を追加(`createModalLayer`)。重なったモーダルのうち最前面の 1 枚を
|
|
175
|
+
DOM の包含関係で決める。React / Vue の `ModalBox` が Escape / Tab の担当判定に使う
|
|
176
|
+
|
|
142
177
|
## 0.4.0 の変更
|
|
143
178
|
|
|
144
179
|
- `focus-trap` を追加(`handleTabKey` / `getFocusableElements` / `FOCUSABLE_SELECTOR`)。
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 開いているモーダルの重なり順。
|
|
3
|
+
*
|
|
4
|
+
* ModalBox は Escape / Tab のハンドラを `document` に bubble で登録するため、
|
|
5
|
+
* モーダルを重ねると全部のハンドラが同じイベントを受け取る。実行順は DOM の
|
|
6
|
+
* 深さではなく登録順で決まる(React は onClose の同一性が変わると再登録されて
|
|
7
|
+
* 順序が入れ替わり、React の effect は子から先に走る)ので、順序には頼れない。
|
|
8
|
+
*
|
|
9
|
+
* そこで「誰が最前面か」をここで判定する。決め手は DOM の包含関係。
|
|
10
|
+
* 入れ子のモーダルは内側が外側の中に描画されるので、他のレイヤーを内包している
|
|
11
|
+
* ものは外側だと分かる。互いに内包しない(入れ子でない)モーダルが並んだときだけ、
|
|
12
|
+
* 後から開いたものを最前面とする。
|
|
13
|
+
*
|
|
14
|
+
* 呼び出し側は 1 コンポーネント 1 ハンドルを持ち、表示状態と自分の要素を
|
|
15
|
+
* `toggle()` に渡して、アンマウント時に `release()` する。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 包含判定しか使わないので、DOM の型は要求しない
|
|
19
|
+
* (core をフレームワーク・実行環境から独立に保つため。lib に DOM を足さない)。
|
|
20
|
+
* メソッド記法なので引数は双変になり、`HTMLElement` をそのまま渡せる
|
|
21
|
+
*/
|
|
22
|
+
type ElementLike = {
|
|
23
|
+
contains(other: ElementLike | null): boolean;
|
|
24
|
+
};
|
|
25
|
+
export type ModalLayer = {
|
|
26
|
+
/**
|
|
27
|
+
* 引数の真偽で登録・解除を切り替える。
|
|
28
|
+
* `element` には最前面判定に使う要素(ダイアログ本体)を渡す。
|
|
29
|
+
* 省略可にすると `toggle(true)` だけで要素の無いレイヤーが積まれ、
|
|
30
|
+
* 包含判定が効かなくなるので必須にしている(未取得なら明示的に null)
|
|
31
|
+
*/
|
|
32
|
+
toggle: (shouldBeActive: boolean, element: ElementLike | null) => void;
|
|
33
|
+
/** このレイヤーが最前面か。キーイベントを処理してよいのは true のときだけ */
|
|
34
|
+
isTopmost: () => boolean;
|
|
35
|
+
/** アンマウント時に呼ぶ。登録中なら解除する */
|
|
36
|
+
release: () => void;
|
|
37
|
+
};
|
|
38
|
+
export declare function createModalLayer(): ModalLayer;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 開いているモーダルの重なり順。
|
|
3
|
+
*
|
|
4
|
+
* ModalBox は Escape / Tab のハンドラを `document` に bubble で登録するため、
|
|
5
|
+
* モーダルを重ねると全部のハンドラが同じイベントを受け取る。実行順は DOM の
|
|
6
|
+
* 深さではなく登録順で決まる(React は onClose の同一性が変わると再登録されて
|
|
7
|
+
* 順序が入れ替わり、React の effect は子から先に走る)ので、順序には頼れない。
|
|
8
|
+
*
|
|
9
|
+
* そこで「誰が最前面か」をここで判定する。決め手は DOM の包含関係。
|
|
10
|
+
* 入れ子のモーダルは内側が外側の中に描画されるので、他のレイヤーを内包している
|
|
11
|
+
* ものは外側だと分かる。互いに内包しない(入れ子でない)モーダルが並んだときだけ、
|
|
12
|
+
* 後から開いたものを最前面とする。
|
|
13
|
+
*
|
|
14
|
+
* 呼び出し側は 1 コンポーネント 1 ハンドルを持ち、表示状態と自分の要素を
|
|
15
|
+
* `toggle()` に渡して、アンマウント時に `release()` する。
|
|
16
|
+
*/
|
|
17
|
+
const layers = [];
|
|
18
|
+
function contains(layer, other) {
|
|
19
|
+
return Boolean(layer.element && other.element && layer.element.contains(other.element));
|
|
20
|
+
}
|
|
21
|
+
export function createModalLayer() {
|
|
22
|
+
const layer = { element: null };
|
|
23
|
+
let isActive = false;
|
|
24
|
+
const toggle = (shouldBeActive, element) => {
|
|
25
|
+
// 要素は再描画で差し替わりうるので、状態が変わらなくても取り直す
|
|
26
|
+
layer.element = shouldBeActive ? element : null;
|
|
27
|
+
if (isActive === shouldBeActive)
|
|
28
|
+
return;
|
|
29
|
+
isActive = shouldBeActive;
|
|
30
|
+
if (shouldBeActive) {
|
|
31
|
+
layers.push(layer);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const index = layers.lastIndexOf(layer);
|
|
35
|
+
if (index >= 0)
|
|
36
|
+
layers.splice(index, 1);
|
|
37
|
+
};
|
|
38
|
+
const isTopmost = () => {
|
|
39
|
+
if (!isActive)
|
|
40
|
+
return false;
|
|
41
|
+
// 他のレイヤーを内包しているものは「外側」なので候補から外す
|
|
42
|
+
const innermost = layers.filter((candidate) => !layers.some((other) => other !== candidate && contains(candidate, other)));
|
|
43
|
+
return innermost[innermost.length - 1] === layer;
|
|
44
|
+
};
|
|
45
|
+
return { toggle, isTopmost, release: () => toggle(false, null) };
|
|
46
|
+
}
|