smarthr-ui 96.1.3 → 97.0.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/lib/components/Combobox/helper.cjs +29 -13
- package/lib/components/Combobox/helper.cjs.map +1 -1
- package/lib/components/Combobox/helper.d.ts +1 -1
- package/lib/components/Combobox/helper.js +29 -13
- package/lib/components/Combobox/helper.js.map +1 -1
- package/lib/components/Combobox/useOptions.cjs +21 -17
- package/lib/components/Combobox/useOptions.cjs.map +1 -1
- package/lib/components/Combobox/useOptions.js +21 -17
- package/lib/components/Combobox/useOptions.js.map +1 -1
- package/lib/components/Dialog/DialogContentInner.cjs +8 -1
- package/lib/components/Dialog/DialogContentInner.cjs.map +1 -1
- package/lib/components/Dialog/DialogContentInner.js +9 -2
- package/lib/components/Dialog/DialogContentInner.js.map +1 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs +13 -11
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs.map +1 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.js +13 -11
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.js.map +1 -1
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs +5 -2
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs.map +1 -1
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js +5 -2
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js.map +1 -1
- package/lib/components/Disclosure/DisclosureTrigger.cjs +39 -18
- package/lib/components/Disclosure/DisclosureTrigger.cjs.map +1 -1
- package/lib/components/Disclosure/DisclosureTrigger.js +40 -19
- package/lib/components/Disclosure/DisclosureTrigger.js.map +1 -1
- package/lib/components/Disclosure/useDisclosure.cjs +8 -14
- package/lib/components/Disclosure/useDisclosure.cjs.map +1 -1
- package/lib/components/Disclosure/useDisclosure.js +8 -14
- package/lib/components/Disclosure/useDisclosure.js.map +1 -1
- package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.cjs +4 -2
- package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.cjs.map +1 -1
- package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.js +4 -2
- package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.js.map +1 -1
- package/lib/components/Dropdown/DropdownTrigger.cjs +3 -0
- package/lib/components/Dropdown/DropdownTrigger.cjs.map +1 -1
- package/lib/components/Dropdown/DropdownTrigger.js +3 -0
- package/lib/components/Dropdown/DropdownTrigger.js.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +12 -17
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.js +12 -17
- package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
- package/lib/components/LineClamp/LineClamp.cjs +14 -3
- package/lib/components/LineClamp/LineClamp.cjs.map +1 -1
- package/lib/components/LineClamp/LineClamp.js +14 -3
- package/lib/components/LineClamp/LineClamp.js.map +1 -1
- package/lib/components/Select/Select.cjs +11 -11
- package/lib/components/Select/Select.cjs.map +1 -1
- package/lib/components/Select/Select.js +12 -12
- package/lib/components/Select/Select.js.map +1 -1
- package/lib/components/TabBar/TabItem.cjs +11 -9
- package/lib/components/TabBar/TabItem.cjs.map +1 -1
- package/lib/components/TabBar/TabItem.d.ts +2 -2
- package/lib/components/TabBar/TabItem.js +13 -11
- package/lib/components/TabBar/TabItem.js.map +1 -1
- package/lib/hooks/useHandleEscape.cjs +4 -4
- package/lib/hooks/useHandleEscape.cjs.map +1 -1
- package/lib/hooks/useHandleEscape.d.ts +1 -1
- package/lib/hooks/useHandleEscape.js +4 -4
- package/lib/hooks/useHandleEscape.js.map +1 -1
- package/lib/intl/Localizer.d.ts +3 -3
- package/lib/intl/locales/en_us.json.cjs +133 -0
- package/lib/intl/locales/en_us.json.cjs.map +1 -0
- package/lib/intl/locales/en_us.json.js +131 -0
- package/lib/intl/locales/en_us.json.js.map +1 -0
- package/lib/intl/locales/id_id.json.cjs +133 -0
- package/lib/intl/locales/id_id.json.cjs.map +1 -0
- package/lib/intl/locales/id_id.json.js +131 -0
- package/lib/intl/locales/id_id.json.js.map +1 -0
- package/lib/intl/locales/index.cjs +24 -18
- package/lib/intl/locales/index.cjs.map +1 -1
- package/lib/intl/locales/index.d.ts +1135 -1136
- package/lib/intl/locales/index.js +25 -19
- package/lib/intl/locales/index.js.map +1 -1
- package/lib/intl/locales/ja.json.cjs +133 -0
- package/lib/intl/locales/ja.json.cjs.map +1 -0
- package/lib/intl/locales/ja.json.js +131 -0
- package/lib/intl/locales/ja.json.js.map +1 -0
- package/lib/intl/locales/ja_easy.json.cjs +133 -0
- package/lib/intl/locales/ja_easy.json.cjs.map +1 -0
- package/lib/intl/locales/ja_easy.json.js +131 -0
- package/lib/intl/locales/ja_easy.json.js.map +1 -0
- package/lib/intl/locales/ko_kr.json.cjs +133 -0
- package/lib/intl/locales/ko_kr.json.cjs.map +1 -0
- package/lib/intl/locales/ko_kr.json.js +131 -0
- package/lib/intl/locales/ko_kr.json.js.map +1 -0
- package/lib/intl/locales/pt_br.json.cjs +133 -0
- package/lib/intl/locales/pt_br.json.cjs.map +1 -0
- package/lib/intl/locales/pt_br.json.js +131 -0
- package/lib/intl/locales/pt_br.json.js.map +1 -0
- package/lib/intl/locales/vi_vn.json.cjs +133 -0
- package/lib/intl/locales/vi_vn.json.cjs.map +1 -0
- package/lib/intl/locales/vi_vn.json.js +131 -0
- package/lib/intl/locales/vi_vn.json.js.map +1 -0
- package/lib/intl/locales/zh_hans_cn.json.cjs +133 -0
- package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -0
- package/lib/intl/locales/zh_hans_cn.json.js +131 -0
- package/lib/intl/locales/zh_hans_cn.json.js.map +1 -0
- package/lib/intl/locales/zh_hant_tw.json.cjs +133 -0
- package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -0
- package/lib/intl/locales/zh_hant_tw.json.js +131 -0
- package/lib/intl/locales/zh_hant_tw.json.js.map +1 -0
- package/lib/intl/useIntl.cjs.map +1 -1
- package/lib/intl/useIntl.d.ts +3 -3
- package/lib/intl/useIntl.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +2 -1
- package/lib/intl/locales/en_us.cjs +0 -135
- package/lib/intl/locales/en_us.cjs.map +0 -1
- package/lib/intl/locales/en_us.d.ts +0 -128
- package/lib/intl/locales/en_us.js +0 -133
- package/lib/intl/locales/en_us.js.map +0 -1
- package/lib/intl/locales/id_id.cjs +0 -135
- package/lib/intl/locales/id_id.cjs.map +0 -1
- package/lib/intl/locales/id_id.d.ts +0 -128
- package/lib/intl/locales/id_id.js +0 -133
- package/lib/intl/locales/id_id.js.map +0 -1
- package/lib/intl/locales/ja_easy.cjs +0 -135
- package/lib/intl/locales/ja_easy.cjs.map +0 -1
- package/lib/intl/locales/ja_easy.d.ts +0 -128
- package/lib/intl/locales/ja_easy.js +0 -133
- package/lib/intl/locales/ja_easy.js.map +0 -1
- package/lib/intl/locales/ko_kr.cjs +0 -135
- package/lib/intl/locales/ko_kr.cjs.map +0 -1
- package/lib/intl/locales/ko_kr.d.ts +0 -128
- package/lib/intl/locales/ko_kr.js +0 -133
- package/lib/intl/locales/ko_kr.js.map +0 -1
- package/lib/intl/locales/pt_br.cjs +0 -135
- package/lib/intl/locales/pt_br.cjs.map +0 -1
- package/lib/intl/locales/pt_br.d.ts +0 -128
- package/lib/intl/locales/pt_br.js +0 -133
- package/lib/intl/locales/pt_br.js.map +0 -1
- package/lib/intl/locales/vi_vn.cjs +0 -135
- package/lib/intl/locales/vi_vn.cjs.map +0 -1
- package/lib/intl/locales/vi_vn.d.ts +0 -128
- package/lib/intl/locales/vi_vn.js +0 -133
- package/lib/intl/locales/vi_vn.js.map +0 -1
- package/lib/intl/locales/zh_hans_cn.cjs +0 -135
- package/lib/intl/locales/zh_hans_cn.cjs.map +0 -1
- package/lib/intl/locales/zh_hans_cn.d.ts +0 -128
- package/lib/intl/locales/zh_hans_cn.js +0 -133
- package/lib/intl/locales/zh_hans_cn.js.map +0 -1
- package/lib/intl/locales/zh_hant_tw.cjs +0 -135
- package/lib/intl/locales/zh_hant_tw.cjs.map +0 -1
- package/lib/intl/locales/zh_hant_tw.d.ts +0 -128
- package/lib/intl/locales/zh_hant_tw.js +0 -133
- package/lib/intl/locales/zh_hant_tw.js.map +0 -1
|
@@ -2,19 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
var index = require('../../_virtual/index3.cjs');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
const CHAR_MAP = {
|
|
6
|
+
'\u2019': "'", // ’
|
|
7
|
+
'\u201C': '"', // “
|
|
8
|
+
'\u201D': '"', // ”
|
|
9
|
+
'`': '`',
|
|
10
|
+
'¥': '¥',
|
|
11
|
+
'−': '-',
|
|
12
|
+
'〜': '~',
|
|
13
|
+
};
|
|
14
|
+
// 変換対象:空白・特殊な全角記号(マップ変換用)および Unicode [!]〜[}] の全角英数記号
|
|
15
|
+
// ※ [−](全角マイナス)は正規表現の範囲指定ハイフンと誤認されないよう、エスケープなしで安全な位置に配置
|
|
16
|
+
const NORMALIZE_PATTERN = /[\s’“”`¥−〜\uFF01-\uFF5D]/g;
|
|
17
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
18
|
+
const normalizeChar = (match) => {
|
|
19
|
+
// 空白文字の正規化
|
|
20
|
+
if (WHITESPACE_PATTERN.test(match))
|
|
21
|
+
return ' ';
|
|
22
|
+
// マッピングテーブルでの変換
|
|
23
|
+
const mappedMatch = CHAR_MAP[match];
|
|
24
|
+
if (mappedMatch !== undefined)
|
|
25
|
+
return mappedMatch;
|
|
26
|
+
// 全角英数記号の半角化
|
|
27
|
+
const code = match.charCodeAt(0);
|
|
28
|
+
if (code >= 0xff01 && code <= 0xff5d) {
|
|
29
|
+
return String.fromCharCode(code - 0xfee0);
|
|
30
|
+
}
|
|
31
|
+
return match;
|
|
32
|
+
};
|
|
33
|
+
const convertMatchableString = (original) => original.replace(NORMALIZE_PATTERN, normalizeChar).toLowerCase();
|
|
18
34
|
function areItemsEqual(a, b) {
|
|
19
35
|
return a.value === b.value && index(a.label) === index(b.label);
|
|
20
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.cjs","sources":["../../../src/components/Combobox/helper.ts"],"sourcesContent":["import innerText from 'react-innertext'\n\nimport type { ComboboxItem } from './types'\n\
|
|
1
|
+
{"version":3,"file":"helper.cjs","sources":["../../../src/components/Combobox/helper.ts"],"sourcesContent":["import innerText from 'react-innertext'\n\nimport type { ComboboxItem } from './types'\n\nconst CHAR_MAP: Record<string, string> = {\n '\\u2019': \"'\", // ’\n '\\u201C': '\"', // “\n '\\u201D': '\"', // ”\n '`': '`',\n '¥': '¥',\n '−': '-',\n '〜': '~',\n}\n\n// 変換対象:空白・特殊な全角記号(マップ変換用)および Unicode [!]〜[}] の全角英数記号\n// ※ [−](全角マイナス)は正規表現の範囲指定ハイフンと誤認されないよう、エスケープなしで安全な位置に配置\nconst NORMALIZE_PATTERN = /[\\s’“”`¥−〜\\uFF01-\\uFF5D]/g\nconst WHITESPACE_PATTERN = /\\s/\n\nconst normalizeChar = (match: string): string => {\n // 空白文字の正規化\n if (WHITESPACE_PATTERN.test(match)) return ' '\n\n // マッピングテーブルでの変換\n const mappedMatch = CHAR_MAP[match]\n if (mappedMatch !== undefined) return mappedMatch\n\n // 全角英数記号の半角化\n const code = match.charCodeAt(0)\n if (code >= 0xff01 && code <= 0xff5d) {\n return String.fromCharCode(code - 0xfee0)\n }\n\n return match\n}\n\nexport const convertMatchableString = (original: string) =>\n original.replace(NORMALIZE_PATTERN, normalizeChar).toLowerCase()\n\nexport function areItemsEqual<T>(a: ComboboxItem<T>, b: ComboboxItem<T>) {\n return a.value === b.value && innerText(a.label) === innerText(b.label)\n}\n"],"names":["innerText"],"mappings":";;;;AAIA,MAAM,QAAQ,GAA2B;IACvC,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;AACb,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;CACT;AAED;AACA;AACA,MAAM,iBAAiB,GAAG,2BAA2B;AACrD,MAAM,kBAAkB,GAAG,IAAI;AAE/B,MAAM,aAAa,GAAG,CAAC,KAAa,KAAY;;AAE9C,IAAA,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,GAAG;;AAG9C,IAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC;IACnC,IAAI,WAAW,KAAK,SAAS;AAAE,QAAA,OAAO,WAAW;;IAGjD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAChC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE;QACpC,OAAO,MAAM,CAAC,YAAY,CAAC,IAAI,GAAG,MAAM,CAAC;IAC3C;AAEA,IAAA,OAAO,KAAK;AACd,CAAC;MAEY,sBAAsB,GAAG,CAAC,QAAgB,KACrD,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,WAAW;AAE1D,SAAU,aAAa,CAAI,CAAkB,EAAE,CAAkB,EAAA;IACrE,OAAO,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,IAAIA,KAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAKA,KAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACzE;;;;;"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ComboboxItem } from './types';
|
|
2
|
-
export declare
|
|
2
|
+
export declare const convertMatchableString: (original: string) => string;
|
|
3
3
|
export declare function areItemsEqual<T>(a: ComboboxItem<T>, b: ComboboxItem<T>): boolean;
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import innerText from '../../_virtual/index3.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
const CHAR_MAP = {
|
|
4
|
+
'\u2019': "'", // ’
|
|
5
|
+
'\u201C': '"', // “
|
|
6
|
+
'\u201D': '"', // ”
|
|
7
|
+
'`': '`',
|
|
8
|
+
'¥': '¥',
|
|
9
|
+
'−': '-',
|
|
10
|
+
'〜': '~',
|
|
11
|
+
};
|
|
12
|
+
// 変換対象:空白・特殊な全角記号(マップ変換用)および Unicode [!]〜[}] の全角英数記号
|
|
13
|
+
// ※ [−](全角マイナス)は正規表現の範囲指定ハイフンと誤認されないよう、エスケープなしで安全な位置に配置
|
|
14
|
+
const NORMALIZE_PATTERN = /[\s’“”`¥−〜\uFF01-\uFF5D]/g;
|
|
15
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
16
|
+
const normalizeChar = (match) => {
|
|
17
|
+
// 空白文字の正規化
|
|
18
|
+
if (WHITESPACE_PATTERN.test(match))
|
|
19
|
+
return ' ';
|
|
20
|
+
// マッピングテーブルでの変換
|
|
21
|
+
const mappedMatch = CHAR_MAP[match];
|
|
22
|
+
if (mappedMatch !== undefined)
|
|
23
|
+
return mappedMatch;
|
|
24
|
+
// 全角英数記号の半角化
|
|
25
|
+
const code = match.charCodeAt(0);
|
|
26
|
+
if (code >= 0xff01 && code <= 0xff5d) {
|
|
27
|
+
return String.fromCharCode(code - 0xfee0);
|
|
28
|
+
}
|
|
29
|
+
return match;
|
|
30
|
+
};
|
|
31
|
+
const convertMatchableString = (original) => original.replace(NORMALIZE_PATTERN, normalizeChar).toLowerCase();
|
|
16
32
|
function areItemsEqual(a, b) {
|
|
17
33
|
return a.value === b.value && innerText(a.label) === innerText(b.label);
|
|
18
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.js","sources":["../../../src/components/Combobox/helper.ts"],"sourcesContent":["import innerText from 'react-innertext'\n\nimport type { ComboboxItem } from './types'\n\
|
|
1
|
+
{"version":3,"file":"helper.js","sources":["../../../src/components/Combobox/helper.ts"],"sourcesContent":["import innerText from 'react-innertext'\n\nimport type { ComboboxItem } from './types'\n\nconst CHAR_MAP: Record<string, string> = {\n '\\u2019': \"'\", // ’\n '\\u201C': '\"', // “\n '\\u201D': '\"', // ”\n '`': '`',\n '¥': '¥',\n '−': '-',\n '〜': '~',\n}\n\n// 変換対象:空白・特殊な全角記号(マップ変換用)および Unicode [!]〜[}] の全角英数記号\n// ※ [−](全角マイナス)は正規表現の範囲指定ハイフンと誤認されないよう、エスケープなしで安全な位置に配置\nconst NORMALIZE_PATTERN = /[\\s’“”`¥−〜\\uFF01-\\uFF5D]/g\nconst WHITESPACE_PATTERN = /\\s/\n\nconst normalizeChar = (match: string): string => {\n // 空白文字の正規化\n if (WHITESPACE_PATTERN.test(match)) return ' '\n\n // マッピングテーブルでの変換\n const mappedMatch = CHAR_MAP[match]\n if (mappedMatch !== undefined) return mappedMatch\n\n // 全角英数記号の半角化\n const code = match.charCodeAt(0)\n if (code >= 0xff01 && code <= 0xff5d) {\n return String.fromCharCode(code - 0xfee0)\n }\n\n return match\n}\n\nexport const convertMatchableString = (original: string) =>\n original.replace(NORMALIZE_PATTERN, normalizeChar).toLowerCase()\n\nexport function areItemsEqual<T>(a: ComboboxItem<T>, b: ComboboxItem<T>) {\n return a.value === b.value && innerText(a.label) === innerText(b.label)\n}\n"],"names":[],"mappings":";;AAIA,MAAM,QAAQ,GAA2B;IACvC,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;AACb,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;AACR,IAAA,GAAG,EAAE,GAAG;CACT;AAED;AACA;AACA,MAAM,iBAAiB,GAAG,2BAA2B;AACrD,MAAM,kBAAkB,GAAG,IAAI;AAE/B,MAAM,aAAa,GAAG,CAAC,KAAa,KAAY;;AAE9C,IAAA,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,GAAG;;AAG9C,IAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC;IACnC,IAAI,WAAW,KAAK,SAAS;AAAE,QAAA,OAAO,WAAW;;IAGjD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAChC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE;QACpC,OAAO,MAAM,CAAC,YAAY,CAAC,IAAI,GAAG,MAAM,CAAC;IAC3C;AAEA,IAAA,OAAO,KAAK;AACd,CAAC;MAEY,sBAAsB,GAAG,CAAC,QAAgB,KACrD,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,WAAW;AAE1D,SAAU,aAAa,CAAI,CAAkB,EAAE,CAAkB,EAAA;IACrE,OAAO,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACzE;;;;"}
|
|
@@ -16,27 +16,31 @@ const useMultiOptions = ({ selected, isItemSelected = defaultIsItemSelected, ...
|
|
|
16
16
|
function useOptions({ items, creatable, inputValue = '', isFilteringDisabled = false }, isSelected) {
|
|
17
17
|
const newItemId = React.useId();
|
|
18
18
|
const optionIdPrefix = React.useId();
|
|
19
|
-
const existedOptions = React.useMemo(() => items.map((item, i) => ({
|
|
20
|
-
id: `${optionIdPrefix}-${i}`,
|
|
21
|
-
selected: isSelected(item),
|
|
22
|
-
isNew: false,
|
|
23
|
-
item,
|
|
24
|
-
})), [isSelected, items, optionIdPrefix]);
|
|
25
|
-
const addingOption = React.useMemo(() => creatable && inputValue && items.every((item) => item.label !== inputValue)
|
|
26
|
-
? {
|
|
27
|
-
id: newItemId,
|
|
28
|
-
isNew: true,
|
|
29
|
-
selected: false,
|
|
30
|
-
item: { label: inputValue, value: inputValue },
|
|
31
|
-
}
|
|
32
|
-
: null, [inputValue, items, creatable, newItemId]);
|
|
33
|
-
const allOptions = React.useMemo(() => (addingOption ? [addingOption, ...existedOptions] : existedOptions), [existedOptions, addingOption]);
|
|
34
19
|
const options = React.useMemo(() => {
|
|
20
|
+
const existedOptions = items.map((item, i) => ({
|
|
21
|
+
id: `${optionIdPrefix}-${i}`,
|
|
22
|
+
selected: isSelected(item),
|
|
23
|
+
isNew: false,
|
|
24
|
+
item,
|
|
25
|
+
}));
|
|
26
|
+
const allOptions = creatable && inputValue && items.every((item) => item.label !== inputValue)
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
id: newItemId,
|
|
30
|
+
isNew: true,
|
|
31
|
+
selected: false,
|
|
32
|
+
item: { label: inputValue, value: inputValue },
|
|
33
|
+
},
|
|
34
|
+
...existedOptions,
|
|
35
|
+
]
|
|
36
|
+
: existedOptions;
|
|
35
37
|
if (isFilteringDisabled || !inputValue) {
|
|
36
38
|
return allOptions;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const convertedInputtedValue = components_Combobox_helper.convertMatchableString(inputValue);
|
|
41
|
+
return allOptions.filter(({ item: { label } }) => components_Combobox_helper.convertMatchableString(index(label)).includes(convertedInputtedValue));
|
|
42
|
+
// TODO: itemsの安定化方法を検討中
|
|
43
|
+
}, [isSelected, items, optionIdPrefix, inputValue, creatable, newItemId, isFilteringDisabled]);
|
|
40
44
|
return {
|
|
41
45
|
options,
|
|
42
46
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOptions.cjs","sources":["../../../src/components/Combobox/useOptions.ts"],"sourcesContent":["import { useCallback, useId, useMemo } from 'react'\nimport innerText from 'react-innertext'\n\nimport { areItemsEqual, convertMatchableString } from './helper'\n\nimport type { ComboboxItem, ComboboxOption } from './types'\n\nconst defaultIsItemSelected = <T>(\n targetItem: ComboboxItem<T>,\n selectedItems: Array<ComboboxItem<T>>,\n) => selectedItems.some((item) => areItemsEqual(item, targetItem))\n\ntype BaseUseOptionsProps<T> = {\n items: Array<ComboboxItem<T>>\n creatable?: boolean\n inputValue?: string\n isFilteringDisabled?: boolean\n}\n\nexport const useSingleOptions = <T>({\n selected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: ComboboxItem<T> | null\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => selected !== null && areItemsEqual(selected, item),\n [selected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nexport const useMultiOptions = <T>({\n selected,\n isItemSelected = defaultIsItemSelected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: Array<ComboboxItem<T>>\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => isItemSelected(item, selected),\n [selected, isItemSelected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nfunction useOptions<T>(\n { items, creatable, inputValue = '', isFilteringDisabled = false }: BaseUseOptionsProps<T>,\n isSelected: (item: ComboboxItem<T>) => boolean,\n) {\n const newItemId = useId()\n const optionIdPrefix = useId()\n\n const existedOptions: Array<ComboboxOption<T>> =
|
|
1
|
+
{"version":3,"file":"useOptions.cjs","sources":["../../../src/components/Combobox/useOptions.ts"],"sourcesContent":["import { useCallback, useId, useMemo } from 'react'\nimport innerText from 'react-innertext'\n\nimport { areItemsEqual, convertMatchableString } from './helper'\n\nimport type { ComboboxItem, ComboboxOption } from './types'\n\nconst defaultIsItemSelected = <T>(\n targetItem: ComboboxItem<T>,\n selectedItems: Array<ComboboxItem<T>>,\n) => selectedItems.some((item) => areItemsEqual(item, targetItem))\n\ntype BaseUseOptionsProps<T> = {\n items: Array<ComboboxItem<T>>\n creatable?: boolean\n inputValue?: string\n isFilteringDisabled?: boolean\n}\n\nexport const useSingleOptions = <T>({\n selected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: ComboboxItem<T> | null\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => selected !== null && areItemsEqual(selected, item),\n [selected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nexport const useMultiOptions = <T>({\n selected,\n isItemSelected = defaultIsItemSelected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: Array<ComboboxItem<T>>\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => isItemSelected(item, selected),\n [selected, isItemSelected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nfunction useOptions<T>(\n { items, creatable, inputValue = '', isFilteringDisabled = false }: BaseUseOptionsProps<T>,\n isSelected: (item: ComboboxItem<T>) => boolean,\n) {\n const newItemId = useId()\n const optionIdPrefix = useId()\n\n const options = useMemo(() => {\n const existedOptions: Array<ComboboxOption<T>> = items.map((item, i) => ({\n id: `${optionIdPrefix}-${i}`,\n selected: isSelected(item),\n isNew: false,\n item,\n }))\n\n const allOptions =\n creatable && inputValue && items.every((item) => item.label !== inputValue)\n ? [\n {\n id: newItemId,\n isNew: true,\n selected: false,\n item: { label: inputValue, value: inputValue },\n } as ComboboxOption<T>,\n ...existedOptions,\n ]\n : existedOptions\n\n if (isFilteringDisabled || !inputValue) {\n return allOptions\n }\n\n const convertedInputtedValue = convertMatchableString(inputValue)\n\n return allOptions.filter(({ item: { label } }) =>\n convertMatchableString(innerText(label)).includes(convertedInputtedValue),\n )\n // TODO: itemsの安定化方法を検討中\n }, [isSelected, items, optionIdPrefix, inputValue, creatable, newItemId, isFilteringDisabled])\n\n return {\n options,\n }\n}\n"],"names":["areItemsEqual","useCallback","useId","useMemo","convertMatchableString","innerText"],"mappings":";;;;;;AAOA,MAAM,qBAAqB,GAAG,CAC5B,UAA2B,EAC3B,aAAqC,KAClC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,KAAKA,wCAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAS3D,MAAM,gBAAgB,GAAG,CAAI,EAClC,QAAQ,EACR,GAAG,IAAI,EAGR,KAAI;IACH,MAAM,UAAU,GAAGC,iBAAW,CAC5B,CAAC,IAAqB,KAAK,QAAQ,KAAK,IAAI,IAAID,wCAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,EAC7E,CAAC,QAAQ,CAAC,CACX;AAED,IAAA,OAAO,UAAU,CAAI,IAAI,EAAE,UAAU,CAAC;AACxC;AAEO,MAAM,eAAe,GAAG,CAAI,EACjC,QAAQ,EACR,cAAc,GAAG,qBAAqB,EACtC,GAAG,IAAI,EAIR,KAAI;IACH,MAAM,UAAU,GAAGC,iBAAW,CAC5B,CAAC,IAAqB,KAAK,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,EACzD,CAAC,QAAQ,EAAE,cAAc,CAAC,CAC3B;AAED,IAAA,OAAO,UAAU,CAAI,IAAI,EAAE,UAAU,CAAC;AACxC;AAEA,SAAS,UAAU,CACjB,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,GAAG,EAAE,EAAE,mBAAmB,GAAG,KAAK,EAA0B,EAC1F,UAA8C,EAAA;AAE9C,IAAA,MAAM,SAAS,GAAGC,WAAK,EAAE;AACzB,IAAA,MAAM,cAAc,GAAGA,WAAK,EAAE;AAE9B,IAAA,MAAM,OAAO,GAAGC,aAAO,CAAC,MAAK;AAC3B,QAAA,MAAM,cAAc,GAA6B,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM;AACvE,YAAA,EAAE,EAAE,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,CAAC,CAAA,CAAE;AAC5B,YAAA,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC;AAC1B,YAAA,KAAK,EAAE,KAAK;YACZ,IAAI;AACL,SAAA,CAAC,CAAC;QAEH,MAAM,UAAU,GACd,SAAS,IAAI,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,KAAK,UAAU;AACxE,cAAE;AACE,gBAAA;AACE,oBAAA,EAAE,EAAE,SAAS;AACb,oBAAA,KAAK,EAAE,IAAI;AACX,oBAAA,QAAQ,EAAE,KAAK;oBACf,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;AAC1B,iBAAA;AACtB,gBAAA,GAAG,cAAc;AAClB;cACD,cAAc;AAEpB,QAAA,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE;AACtC,YAAA,OAAO,UAAU;QACnB;AAEA,QAAA,MAAM,sBAAsB,GAAGC,iDAAsB,CAAC,UAAU,CAAC;AAEjE,QAAA,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,KAC3CA,iDAAsB,CAACC,KAAS,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAC1E;;AAEH,IAAA,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAE9F,OAAO;QACL,OAAO;KACR;AACH;;;;;"}
|
|
@@ -14,27 +14,31 @@ const useMultiOptions = ({ selected, isItemSelected = defaultIsItemSelected, ...
|
|
|
14
14
|
function useOptions({ items, creatable, inputValue = '', isFilteringDisabled = false }, isSelected) {
|
|
15
15
|
const newItemId = useId();
|
|
16
16
|
const optionIdPrefix = useId();
|
|
17
|
-
const existedOptions = useMemo(() => items.map((item, i) => ({
|
|
18
|
-
id: `${optionIdPrefix}-${i}`,
|
|
19
|
-
selected: isSelected(item),
|
|
20
|
-
isNew: false,
|
|
21
|
-
item,
|
|
22
|
-
})), [isSelected, items, optionIdPrefix]);
|
|
23
|
-
const addingOption = useMemo(() => creatable && inputValue && items.every((item) => item.label !== inputValue)
|
|
24
|
-
? {
|
|
25
|
-
id: newItemId,
|
|
26
|
-
isNew: true,
|
|
27
|
-
selected: false,
|
|
28
|
-
item: { label: inputValue, value: inputValue },
|
|
29
|
-
}
|
|
30
|
-
: null, [inputValue, items, creatable, newItemId]);
|
|
31
|
-
const allOptions = useMemo(() => (addingOption ? [addingOption, ...existedOptions] : existedOptions), [existedOptions, addingOption]);
|
|
32
17
|
const options = useMemo(() => {
|
|
18
|
+
const existedOptions = items.map((item, i) => ({
|
|
19
|
+
id: `${optionIdPrefix}-${i}`,
|
|
20
|
+
selected: isSelected(item),
|
|
21
|
+
isNew: false,
|
|
22
|
+
item,
|
|
23
|
+
}));
|
|
24
|
+
const allOptions = creatable && inputValue && items.every((item) => item.label !== inputValue)
|
|
25
|
+
? [
|
|
26
|
+
{
|
|
27
|
+
id: newItemId,
|
|
28
|
+
isNew: true,
|
|
29
|
+
selected: false,
|
|
30
|
+
item: { label: inputValue, value: inputValue },
|
|
31
|
+
},
|
|
32
|
+
...existedOptions,
|
|
33
|
+
]
|
|
34
|
+
: existedOptions;
|
|
33
35
|
if (isFilteringDisabled || !inputValue) {
|
|
34
36
|
return allOptions;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const convertedInputtedValue = convertMatchableString(inputValue);
|
|
39
|
+
return allOptions.filter(({ item: { label } }) => convertMatchableString(innerText(label)).includes(convertedInputtedValue));
|
|
40
|
+
// TODO: itemsの安定化方法を検討中
|
|
41
|
+
}, [isSelected, items, optionIdPrefix, inputValue, creatable, newItemId, isFilteringDisabled]);
|
|
38
42
|
return {
|
|
39
43
|
options,
|
|
40
44
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOptions.js","sources":["../../../src/components/Combobox/useOptions.ts"],"sourcesContent":["import { useCallback, useId, useMemo } from 'react'\nimport innerText from 'react-innertext'\n\nimport { areItemsEqual, convertMatchableString } from './helper'\n\nimport type { ComboboxItem, ComboboxOption } from './types'\n\nconst defaultIsItemSelected = <T>(\n targetItem: ComboboxItem<T>,\n selectedItems: Array<ComboboxItem<T>>,\n) => selectedItems.some((item) => areItemsEqual(item, targetItem))\n\ntype BaseUseOptionsProps<T> = {\n items: Array<ComboboxItem<T>>\n creatable?: boolean\n inputValue?: string\n isFilteringDisabled?: boolean\n}\n\nexport const useSingleOptions = <T>({\n selected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: ComboboxItem<T> | null\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => selected !== null && areItemsEqual(selected, item),\n [selected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nexport const useMultiOptions = <T>({\n selected,\n isItemSelected = defaultIsItemSelected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: Array<ComboboxItem<T>>\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => isItemSelected(item, selected),\n [selected, isItemSelected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nfunction useOptions<T>(\n { items, creatable, inputValue = '', isFilteringDisabled = false }: BaseUseOptionsProps<T>,\n isSelected: (item: ComboboxItem<T>) => boolean,\n) {\n const newItemId = useId()\n const optionIdPrefix = useId()\n\n const existedOptions: Array<ComboboxOption<T>> =
|
|
1
|
+
{"version":3,"file":"useOptions.js","sources":["../../../src/components/Combobox/useOptions.ts"],"sourcesContent":["import { useCallback, useId, useMemo } from 'react'\nimport innerText from 'react-innertext'\n\nimport { areItemsEqual, convertMatchableString } from './helper'\n\nimport type { ComboboxItem, ComboboxOption } from './types'\n\nconst defaultIsItemSelected = <T>(\n targetItem: ComboboxItem<T>,\n selectedItems: Array<ComboboxItem<T>>,\n) => selectedItems.some((item) => areItemsEqual(item, targetItem))\n\ntype BaseUseOptionsProps<T> = {\n items: Array<ComboboxItem<T>>\n creatable?: boolean\n inputValue?: string\n isFilteringDisabled?: boolean\n}\n\nexport const useSingleOptions = <T>({\n selected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: ComboboxItem<T> | null\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => selected !== null && areItemsEqual(selected, item),\n [selected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nexport const useMultiOptions = <T>({\n selected,\n isItemSelected = defaultIsItemSelected,\n ...rest\n}: BaseUseOptionsProps<T> & {\n selected: Array<ComboboxItem<T>>\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n}) => {\n const isSelected = useCallback(\n (item: ComboboxItem<T>) => isItemSelected(item, selected),\n [selected, isItemSelected],\n )\n\n return useOptions<T>(rest, isSelected)\n}\n\nfunction useOptions<T>(\n { items, creatable, inputValue = '', isFilteringDisabled = false }: BaseUseOptionsProps<T>,\n isSelected: (item: ComboboxItem<T>) => boolean,\n) {\n const newItemId = useId()\n const optionIdPrefix = useId()\n\n const options = useMemo(() => {\n const existedOptions: Array<ComboboxOption<T>> = items.map((item, i) => ({\n id: `${optionIdPrefix}-${i}`,\n selected: isSelected(item),\n isNew: false,\n item,\n }))\n\n const allOptions =\n creatable && inputValue && items.every((item) => item.label !== inputValue)\n ? [\n {\n id: newItemId,\n isNew: true,\n selected: false,\n item: { label: inputValue, value: inputValue },\n } as ComboboxOption<T>,\n ...existedOptions,\n ]\n : existedOptions\n\n if (isFilteringDisabled || !inputValue) {\n return allOptions\n }\n\n const convertedInputtedValue = convertMatchableString(inputValue)\n\n return allOptions.filter(({ item: { label } }) =>\n convertMatchableString(innerText(label)).includes(convertedInputtedValue),\n )\n // TODO: itemsの安定化方法を検討中\n }, [isSelected, items, optionIdPrefix, inputValue, creatable, newItemId, isFilteringDisabled])\n\n return {\n options,\n }\n}\n"],"names":[],"mappings":";;;;AAOA,MAAM,qBAAqB,GAAG,CAC5B,UAA2B,EAC3B,aAAqC,KAClC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAS3D,MAAM,gBAAgB,GAAG,CAAI,EAClC,QAAQ,EACR,GAAG,IAAI,EAGR,KAAI;IACH,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,IAAqB,KAAK,QAAQ,KAAK,IAAI,IAAI,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,EAC7E,CAAC,QAAQ,CAAC,CACX;AAED,IAAA,OAAO,UAAU,CAAI,IAAI,EAAE,UAAU,CAAC;AACxC;AAEO,MAAM,eAAe,GAAG,CAAI,EACjC,QAAQ,EACR,cAAc,GAAG,qBAAqB,EACtC,GAAG,IAAI,EAIR,KAAI;IACH,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,IAAqB,KAAK,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,EACzD,CAAC,QAAQ,EAAE,cAAc,CAAC,CAC3B;AAED,IAAA,OAAO,UAAU,CAAI,IAAI,EAAE,UAAU,CAAC;AACxC;AAEA,SAAS,UAAU,CACjB,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,GAAG,EAAE,EAAE,mBAAmB,GAAG,KAAK,EAA0B,EAC1F,UAA8C,EAAA;AAE9C,IAAA,MAAM,SAAS,GAAG,KAAK,EAAE;AACzB,IAAA,MAAM,cAAc,GAAG,KAAK,EAAE;AAE9B,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,MAAK;AAC3B,QAAA,MAAM,cAAc,GAA6B,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM;AACvE,YAAA,EAAE,EAAE,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,CAAC,CAAA,CAAE;AAC5B,YAAA,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC;AAC1B,YAAA,KAAK,EAAE,KAAK;YACZ,IAAI;AACL,SAAA,CAAC,CAAC;QAEH,MAAM,UAAU,GACd,SAAS,IAAI,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,KAAK,UAAU;AACxE,cAAE;AACE,gBAAA;AACE,oBAAA,EAAE,EAAE,SAAS;AACb,oBAAA,KAAK,EAAE,IAAI;AACX,oBAAA,QAAQ,EAAE,KAAK;oBACf,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;AAC1B,iBAAA;AACtB,gBAAA,GAAG,cAAc;AAClB;cACD,cAAc;AAEpB,QAAA,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE;AACtC,YAAA,OAAO,UAAU;QACnB;AAEA,QAAA,MAAM,sBAAsB,GAAG,sBAAsB,CAAC,UAAU,CAAC;AAEjE,QAAA,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,KAC3C,sBAAsB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAC1E;;AAEH,IAAA,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAE9F,OAAO;QACL,OAAO;KACR;AACH;;;;"}
|
|
@@ -57,7 +57,14 @@ const DialogContentInner = ({ onClickOverlay, onPressEscape, isOpen, id, width,
|
|
|
57
57
|
return actualWidth ? { width: actualWidth } : undefined;
|
|
58
58
|
}, [width, size]);
|
|
59
59
|
const innerRef = React.useRef(null);
|
|
60
|
-
|
|
60
|
+
// 外部propsのonPressEscapeをrefに保存
|
|
61
|
+
const onPressEscapeRef = React.useRef(onPressEscape);
|
|
62
|
+
onPressEscapeRef.current = onPressEscape;
|
|
63
|
+
// stableなcallbackを作成
|
|
64
|
+
const memoizedOnPressEscape = React.useCallback(() => {
|
|
65
|
+
onPressEscapeRef.current?.();
|
|
66
|
+
}, []);
|
|
67
|
+
hooks_useHandleEscape.useHandleEscape(isOpen ? memoizedOnPressEscape : undefined);
|
|
61
68
|
components_Dialog_useBodyScrollLock.useBodyScrollLock(isOpen);
|
|
62
69
|
return (jsxRuntime.jsx(components_Dialog_DialogOverlap.DialogOverlap, { isOpen: isOpen, children: jsxRuntime.jsxs("div", { id: id, className: classNames.layout, style: style, children: [jsxRuntime.jsx(Overlay, { isOpen: isOpen, onClickOverlay: onClickOverlay, className: classNames.background }), jsxRuntime.jsx("div", { ...rest, ref: innerRef, role: "dialog", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-modal": "true", className: classNames.inner, children: jsxRuntime.jsx(components_Dialog_FocusTrap.FocusTrap, { firstFocusTarget: firstFocusTarget, ref: focusTrapRef, children: children }) })] }) }));
|
|
63
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DialogContentInner.cjs","sources":["../../../src/components/Dialog/DialogContentInner.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type PropsWithChildren,\n type RefObject,\n memo,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../hooks/useHandleEscape'\nimport { dialogSize } from '../../tailwind'\n\nimport { DialogOverlap } from './DialogOverlap'\nimport { FocusTrap, type FocusTrapRef } from './FocusTrap'\nimport { useBodyScrollLock } from './useBodyScrollLock'\n\nimport type { DialogSize } from './types'\n\nexport type DialogContentInnerProps = PropsWithChildren<{\n /**\n * オーバーレイをクリックした時に発火するコールバック関数\n */\n onClickOverlay?: () => void\n /**\n * エスケープキーを押下した時に発火するコールバック関数\n */\n onPressEscape?: () => void\n /**\n * ダイアログを開いているかどうか\n */\n isOpen: boolean\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの `id`\n * TODO 使われてなさそうなので確認\n */\n id?: string\n /**\n * ダイアログを開いた時にフォーカスする対象\n */\n firstFocusTarget?: RefObject<HTMLElement>\n /**\n * ダイアログの `aria-label`\n */\n ariaLabel?: string\n /**\n * ダイアログの `aria-labelledby`\n */\n ariaLabelledby?: string\n /**\n * ダイアログトップのフォーカストラップへの ref\n */\n focusTrapRef?: RefObject<FocusTrapRef>\n}>\ntype Props = DialogContentInnerProps & Omit<ComponentProps<'div'>, keyof DialogContentInnerProps>\n\nconst classNameGenerator = tv({\n slots: {\n layout: ['smarthr-ui-Dialog-wrapper', 'shr-max-w-[calc(100dvw-theme(spacing.1))]'],\n inner: [\n 'smarthr-ui-Dialog',\n 'shr-border-shorthand shr-relative shr-z-1 shr-rounded-m shr-bg-white shr-shadow-layer-3',\n 'contrast-more:shr-border-high-contrast',\n ],\n background: ['smarthr-ui-Dialog-background', 'shr-absolute shr-inset-0 shr-bg-scrim'],\n },\n variants: {\n size: {\n XS: { layout: dialogSize.XS },\n S: { layout: dialogSize.S },\n M: { layout: dialogSize.M },\n L: { layout: dialogSize.L },\n XL: { layout: dialogSize.XL },\n XXL: { layout: dialogSize.XXL },\n FULL: { layout: dialogSize.FULL },\n },\n },\n})\n\nexport const DialogContentInner: FC<Props> = ({\n onClickOverlay,\n onPressEscape,\n isOpen,\n id,\n width,\n size,\n firstFocusTarget,\n ariaLabel,\n ariaLabelledby,\n children,\n className,\n focusTrapRef,\n ...rest\n}) => {\n const classNames = useMemo(() => {\n const { layout, inner, background } = classNameGenerator()\n\n return {\n layout: layout({ size }),\n inner: inner({ className }),\n background: background(),\n }\n }, [size, className])\n const style = useMemo(() => {\n // width は deprecated なので、size が指定されている場合は width を無視する\n const actualWidth = size ? undefined : typeof width === 'number' ? `${width}px` : width\n\n return actualWidth ? { width: actualWidth } : undefined\n }, [width, size])\n\n const innerRef = useRef<HTMLDivElement>(null)\n\n
|
|
1
|
+
{"version":3,"file":"DialogContentInner.cjs","sources":["../../../src/components/Dialog/DialogContentInner.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type PropsWithChildren,\n type RefObject,\n memo,\n useCallback,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../hooks/useHandleEscape'\nimport { dialogSize } from '../../tailwind'\n\nimport { DialogOverlap } from './DialogOverlap'\nimport { FocusTrap, type FocusTrapRef } from './FocusTrap'\nimport { useBodyScrollLock } from './useBodyScrollLock'\n\nimport type { DialogSize } from './types'\n\nexport type DialogContentInnerProps = PropsWithChildren<{\n /**\n * オーバーレイをクリックした時に発火するコールバック関数\n */\n onClickOverlay?: () => void\n /**\n * エスケープキーを押下した時に発火するコールバック関数\n */\n onPressEscape?: () => void\n /**\n * ダイアログを開いているかどうか\n */\n isOpen: boolean\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの `id`\n * TODO 使われてなさそうなので確認\n */\n id?: string\n /**\n * ダイアログを開いた時にフォーカスする対象\n */\n firstFocusTarget?: RefObject<HTMLElement>\n /**\n * ダイアログの `aria-label`\n */\n ariaLabel?: string\n /**\n * ダイアログの `aria-labelledby`\n */\n ariaLabelledby?: string\n /**\n * ダイアログトップのフォーカストラップへの ref\n */\n focusTrapRef?: RefObject<FocusTrapRef>\n}>\ntype Props = DialogContentInnerProps & Omit<ComponentProps<'div'>, keyof DialogContentInnerProps>\n\nconst classNameGenerator = tv({\n slots: {\n layout: ['smarthr-ui-Dialog-wrapper', 'shr-max-w-[calc(100dvw-theme(spacing.1))]'],\n inner: [\n 'smarthr-ui-Dialog',\n 'shr-border-shorthand shr-relative shr-z-1 shr-rounded-m shr-bg-white shr-shadow-layer-3',\n 'contrast-more:shr-border-high-contrast',\n ],\n background: ['smarthr-ui-Dialog-background', 'shr-absolute shr-inset-0 shr-bg-scrim'],\n },\n variants: {\n size: {\n XS: { layout: dialogSize.XS },\n S: { layout: dialogSize.S },\n M: { layout: dialogSize.M },\n L: { layout: dialogSize.L },\n XL: { layout: dialogSize.XL },\n XXL: { layout: dialogSize.XXL },\n FULL: { layout: dialogSize.FULL },\n },\n },\n})\n\nexport const DialogContentInner: FC<Props> = ({\n onClickOverlay,\n onPressEscape,\n isOpen,\n id,\n width,\n size,\n firstFocusTarget,\n ariaLabel,\n ariaLabelledby,\n children,\n className,\n focusTrapRef,\n ...rest\n}) => {\n const classNames = useMemo(() => {\n const { layout, inner, background } = classNameGenerator()\n\n return {\n layout: layout({ size }),\n inner: inner({ className }),\n background: background(),\n }\n }, [size, className])\n const style = useMemo(() => {\n // width は deprecated なので、size が指定されている場合は width を無視する\n const actualWidth = size ? undefined : typeof width === 'number' ? `${width}px` : width\n\n return actualWidth ? { width: actualWidth } : undefined\n }, [width, size])\n\n const innerRef = useRef<HTMLDivElement>(null)\n\n // 外部propsのonPressEscapeをrefに保存\n const onPressEscapeRef = useRef(onPressEscape)\n onPressEscapeRef.current = onPressEscape\n\n // stableなcallbackを作成\n const memoizedOnPressEscape = useCallback(() => {\n onPressEscapeRef.current?.()\n }, [])\n\n useHandleEscape(isOpen ? memoizedOnPressEscape : undefined)\n\n useBodyScrollLock(isOpen)\n\n return (\n <DialogOverlap isOpen={isOpen}>\n <div id={id} className={classNames.layout} style={style}>\n <Overlay\n isOpen={isOpen}\n onClickOverlay={onClickOverlay}\n className={classNames.background}\n />\n <div\n {...rest}\n ref={innerRef}\n role=\"dialog\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-modal=\"true\"\n className={classNames.inner}\n >\n <FocusTrap firstFocusTarget={firstFocusTarget} ref={focusTrapRef}>\n {children}\n </FocusTrap>\n </div>\n </div>\n </DialogOverlap>\n )\n}\n\nconst Overlay = memo<Pick<Props, 'onClickOverlay' | 'isOpen'> & { className: string }>(\n ({ onClickOverlay, isOpen, className }) => {\n const onClick = useMemo(\n () => (onClickOverlay && isOpen ? onClickOverlay : undefined),\n [isOpen, onClickOverlay],\n )\n\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n return <div onClick={onClick} className={className} role=\"presentation\" />\n },\n)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqEA;AACE;AACE;AACA;;;;AAIC;AACD;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AACF;AACF;AAEM;AAeL;;;AAII;AACA;;;AAGJ;AACA;;;AAIE;AACF;AAEA;;AAGA;AACA;;AAGA;AACE;;;;AAOF;AAwBF;AAEA;;;AAQI;AACF;;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { memo, useMemo, useRef } from 'react';
|
|
3
|
+
import { memo, useMemo, useRef, useCallback } from 'react';
|
|
4
4
|
import { tv as ce } from './../../vendor/.pnpm/tailwind-variants@0.3.1_tailwindcss@3.4.19_ts-node@10.9.2_@swc_core@1.15.17_@types_node@20.19.35_typescript@5.9.3__/vendor/tailwind-variants/dist/index.js';
|
|
5
5
|
import { useHandleEscape } from '../../hooks/useHandleEscape.js';
|
|
6
6
|
import { dialogSize } from '../../tailwind/dialogSize.js';
|
|
@@ -55,7 +55,14 @@ const DialogContentInner = ({ onClickOverlay, onPressEscape, isOpen, id, width,
|
|
|
55
55
|
return actualWidth ? { width: actualWidth } : undefined;
|
|
56
56
|
}, [width, size]);
|
|
57
57
|
const innerRef = useRef(null);
|
|
58
|
-
|
|
58
|
+
// 外部propsのonPressEscapeをrefに保存
|
|
59
|
+
const onPressEscapeRef = useRef(onPressEscape);
|
|
60
|
+
onPressEscapeRef.current = onPressEscape;
|
|
61
|
+
// stableなcallbackを作成
|
|
62
|
+
const memoizedOnPressEscape = useCallback(() => {
|
|
63
|
+
onPressEscapeRef.current?.();
|
|
64
|
+
}, []);
|
|
65
|
+
useHandleEscape(isOpen ? memoizedOnPressEscape : undefined);
|
|
59
66
|
useBodyScrollLock(isOpen);
|
|
60
67
|
return (jsx(DialogOverlap, { isOpen: isOpen, children: jsxs("div", { id: id, className: classNames.layout, style: style, children: [jsx(Overlay, { isOpen: isOpen, onClickOverlay: onClickOverlay, className: classNames.background }), jsx("div", { ...rest, ref: innerRef, role: "dialog", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-modal": "true", className: classNames.inner, children: jsx(FocusTrap, { firstFocusTarget: firstFocusTarget, ref: focusTrapRef, children: children }) })] }) }));
|
|
61
68
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DialogContentInner.js","sources":["../../../src/components/Dialog/DialogContentInner.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type PropsWithChildren,\n type RefObject,\n memo,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../hooks/useHandleEscape'\nimport { dialogSize } from '../../tailwind'\n\nimport { DialogOverlap } from './DialogOverlap'\nimport { FocusTrap, type FocusTrapRef } from './FocusTrap'\nimport { useBodyScrollLock } from './useBodyScrollLock'\n\nimport type { DialogSize } from './types'\n\nexport type DialogContentInnerProps = PropsWithChildren<{\n /**\n * オーバーレイをクリックした時に発火するコールバック関数\n */\n onClickOverlay?: () => void\n /**\n * エスケープキーを押下した時に発火するコールバック関数\n */\n onPressEscape?: () => void\n /**\n * ダイアログを開いているかどうか\n */\n isOpen: boolean\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの `id`\n * TODO 使われてなさそうなので確認\n */\n id?: string\n /**\n * ダイアログを開いた時にフォーカスする対象\n */\n firstFocusTarget?: RefObject<HTMLElement>\n /**\n * ダイアログの `aria-label`\n */\n ariaLabel?: string\n /**\n * ダイアログの `aria-labelledby`\n */\n ariaLabelledby?: string\n /**\n * ダイアログトップのフォーカストラップへの ref\n */\n focusTrapRef?: RefObject<FocusTrapRef>\n}>\ntype Props = DialogContentInnerProps & Omit<ComponentProps<'div'>, keyof DialogContentInnerProps>\n\nconst classNameGenerator = tv({\n slots: {\n layout: ['smarthr-ui-Dialog-wrapper', 'shr-max-w-[calc(100dvw-theme(spacing.1))]'],\n inner: [\n 'smarthr-ui-Dialog',\n 'shr-border-shorthand shr-relative shr-z-1 shr-rounded-m shr-bg-white shr-shadow-layer-3',\n 'contrast-more:shr-border-high-contrast',\n ],\n background: ['smarthr-ui-Dialog-background', 'shr-absolute shr-inset-0 shr-bg-scrim'],\n },\n variants: {\n size: {\n XS: { layout: dialogSize.XS },\n S: { layout: dialogSize.S },\n M: { layout: dialogSize.M },\n L: { layout: dialogSize.L },\n XL: { layout: dialogSize.XL },\n XXL: { layout: dialogSize.XXL },\n FULL: { layout: dialogSize.FULL },\n },\n },\n})\n\nexport const DialogContentInner: FC<Props> = ({\n onClickOverlay,\n onPressEscape,\n isOpen,\n id,\n width,\n size,\n firstFocusTarget,\n ariaLabel,\n ariaLabelledby,\n children,\n className,\n focusTrapRef,\n ...rest\n}) => {\n const classNames = useMemo(() => {\n const { layout, inner, background } = classNameGenerator()\n\n return {\n layout: layout({ size }),\n inner: inner({ className }),\n background: background(),\n }\n }, [size, className])\n const style = useMemo(() => {\n // width は deprecated なので、size が指定されている場合は width を無視する\n const actualWidth = size ? undefined : typeof width === 'number' ? `${width}px` : width\n\n return actualWidth ? { width: actualWidth } : undefined\n }, [width, size])\n\n const innerRef = useRef<HTMLDivElement>(null)\n\n
|
|
1
|
+
{"version":3,"file":"DialogContentInner.js","sources":["../../../src/components/Dialog/DialogContentInner.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type PropsWithChildren,\n type RefObject,\n memo,\n useCallback,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../hooks/useHandleEscape'\nimport { dialogSize } from '../../tailwind'\n\nimport { DialogOverlap } from './DialogOverlap'\nimport { FocusTrap, type FocusTrapRef } from './FocusTrap'\nimport { useBodyScrollLock } from './useBodyScrollLock'\n\nimport type { DialogSize } from './types'\n\nexport type DialogContentInnerProps = PropsWithChildren<{\n /**\n * オーバーレイをクリックした時に発火するコールバック関数\n */\n onClickOverlay?: () => void\n /**\n * エスケープキーを押下した時に発火するコールバック関数\n */\n onPressEscape?: () => void\n /**\n * ダイアログを開いているかどうか\n */\n isOpen: boolean\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの `id`\n * TODO 使われてなさそうなので確認\n */\n id?: string\n /**\n * ダイアログを開いた時にフォーカスする対象\n */\n firstFocusTarget?: RefObject<HTMLElement>\n /**\n * ダイアログの `aria-label`\n */\n ariaLabel?: string\n /**\n * ダイアログの `aria-labelledby`\n */\n ariaLabelledby?: string\n /**\n * ダイアログトップのフォーカストラップへの ref\n */\n focusTrapRef?: RefObject<FocusTrapRef>\n}>\ntype Props = DialogContentInnerProps & Omit<ComponentProps<'div'>, keyof DialogContentInnerProps>\n\nconst classNameGenerator = tv({\n slots: {\n layout: ['smarthr-ui-Dialog-wrapper', 'shr-max-w-[calc(100dvw-theme(spacing.1))]'],\n inner: [\n 'smarthr-ui-Dialog',\n 'shr-border-shorthand shr-relative shr-z-1 shr-rounded-m shr-bg-white shr-shadow-layer-3',\n 'contrast-more:shr-border-high-contrast',\n ],\n background: ['smarthr-ui-Dialog-background', 'shr-absolute shr-inset-0 shr-bg-scrim'],\n },\n variants: {\n size: {\n XS: { layout: dialogSize.XS },\n S: { layout: dialogSize.S },\n M: { layout: dialogSize.M },\n L: { layout: dialogSize.L },\n XL: { layout: dialogSize.XL },\n XXL: { layout: dialogSize.XXL },\n FULL: { layout: dialogSize.FULL },\n },\n },\n})\n\nexport const DialogContentInner: FC<Props> = ({\n onClickOverlay,\n onPressEscape,\n isOpen,\n id,\n width,\n size,\n firstFocusTarget,\n ariaLabel,\n ariaLabelledby,\n children,\n className,\n focusTrapRef,\n ...rest\n}) => {\n const classNames = useMemo(() => {\n const { layout, inner, background } = classNameGenerator()\n\n return {\n layout: layout({ size }),\n inner: inner({ className }),\n background: background(),\n }\n }, [size, className])\n const style = useMemo(() => {\n // width は deprecated なので、size が指定されている場合は width を無視する\n const actualWidth = size ? undefined : typeof width === 'number' ? `${width}px` : width\n\n return actualWidth ? { width: actualWidth } : undefined\n }, [width, size])\n\n const innerRef = useRef<HTMLDivElement>(null)\n\n // 外部propsのonPressEscapeをrefに保存\n const onPressEscapeRef = useRef(onPressEscape)\n onPressEscapeRef.current = onPressEscape\n\n // stableなcallbackを作成\n const memoizedOnPressEscape = useCallback(() => {\n onPressEscapeRef.current?.()\n }, [])\n\n useHandleEscape(isOpen ? memoizedOnPressEscape : undefined)\n\n useBodyScrollLock(isOpen)\n\n return (\n <DialogOverlap isOpen={isOpen}>\n <div id={id} className={classNames.layout} style={style}>\n <Overlay\n isOpen={isOpen}\n onClickOverlay={onClickOverlay}\n className={classNames.background}\n />\n <div\n {...rest}\n ref={innerRef}\n role=\"dialog\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-modal=\"true\"\n className={classNames.inner}\n >\n <FocusTrap firstFocusTarget={firstFocusTarget} ref={focusTrapRef}>\n {children}\n </FocusTrap>\n </div>\n </div>\n </DialogOverlap>\n )\n}\n\nconst Overlay = memo<Pick<Props, 'onClickOverlay' | 'isOpen'> & { className: string }>(\n ({ onClickOverlay, isOpen, className }) => {\n const onClick = useMemo(\n () => (onClickOverlay && isOpen ? onClickOverlay : undefined),\n [isOpen, onClickOverlay],\n )\n\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n return <div onClick={onClick} className={className} role=\"presentation\" />\n },\n)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAqEA;AACE;AACE;AACA;;;;AAIC;AACD;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AACF;AACF;AAEM;AAeL;;;AAII;AACA;;;AAGJ;AACA;;;AAIE;AACF;AAEA;;AAGA;AACA;;AAGA;AACE;;;;AAOF;AAwBF;AAEA;;;AAQI;AACF;;"}
|
|
@@ -118,6 +118,9 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
118
118
|
});
|
|
119
119
|
debounceLiveRegionText(txt);
|
|
120
120
|
}, [localize, wrapperPosition, debounceLiveRegionText]);
|
|
121
|
+
// 外部propsをrefに保存
|
|
122
|
+
const unstableRef = React.useRef({ isOpen, onClickClose, onPressEscape });
|
|
123
|
+
unstableRef.current = { isOpen, onClickClose, onPressEscape };
|
|
121
124
|
const positionStyle = React.useMemo(() => ({
|
|
122
125
|
top: centering.top ?? top,
|
|
123
126
|
left: centering.left ?? left,
|
|
@@ -127,7 +130,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
127
130
|
height,
|
|
128
131
|
}), [centering, top, left, right, bottom, width, height, size]);
|
|
129
132
|
const handleArrowKey = React.useCallback((e) => {
|
|
130
|
-
if (!isOpen || document.activeElement !== e.currentTarget) {
|
|
133
|
+
if (!unstableRef.current.isOpen || document.activeElement !== e.currentTarget) {
|
|
131
134
|
return;
|
|
132
135
|
}
|
|
133
136
|
const movingDistance = 20;
|
|
@@ -161,7 +164,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
161
164
|
e.preventDefault();
|
|
162
165
|
break;
|
|
163
166
|
}
|
|
164
|
-
}, [
|
|
167
|
+
}, []);
|
|
165
168
|
React.useEffect(() => {
|
|
166
169
|
if (wrapperRef.current instanceof Element) {
|
|
167
170
|
setWrapperPosition(wrapperRef.current.getBoundingClientRect());
|
|
@@ -202,15 +205,14 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
202
205
|
}, [isOpen]);
|
|
203
206
|
const actualOnClickClose = React.useCallback((e) => {
|
|
204
207
|
lastFocusElementRef.current?.focus();
|
|
205
|
-
onClickClose?.(e);
|
|
206
|
-
}, [
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
hooks_useHandleEscape.useHandleEscape(React.useMemo(() => (actualOnPressEscape && isOpen ? actualOnPressEscape : undefined), [isOpen, actualOnPressEscape]));
|
|
208
|
+
unstableRef.current.onClickClose?.(e);
|
|
209
|
+
}, []);
|
|
210
|
+
// stableなcallbackを作成
|
|
211
|
+
const memoizedOnPressEscape = React.useCallback(() => {
|
|
212
|
+
lastFocusElementRef.current?.focus();
|
|
213
|
+
unstableRef.current.onPressEscape?.();
|
|
214
|
+
}, []);
|
|
215
|
+
hooks_useHandleEscape.useHandleEscape(isOpen ? memoizedOnPressEscape : undefined);
|
|
214
216
|
React.useEffect(() => {
|
|
215
217
|
const focusHandler = (e) => {
|
|
216
218
|
// e.target(現在フォーカスがあたっている要素)がModeless dialog外の要素であれば、lastFocusElementRefに代入する
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModelessDialog.cjs","sources":["../../../../src/components/Dialog/ModelessDialog/ModelessDialog.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type KeyboardEvent,\n type MouseEvent,\n type PropsWithChildren,\n type ReactNode,\n type RefObject,\n memo,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport Draggable from 'react-draggable'\nimport { type VariantProps, tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../../hooks/useHandleEscape'\nimport { useIntl } from '../../../intl'\nimport { debounce } from '../../../libs/debounce'\nimport { dialogSize } from '../../../tailwind'\nimport { Base, type BaseElementProps } from '../../Base'\nimport { Button } from '../../Button'\nimport { Heading } from '../../Heading'\nimport { FaGripIcon, FaXmarkIcon } from '../../Icon'\nimport { DialogBody, type Props as DialogBodyProps } from '../DialogBody'\nimport { DialogOverlap } from '../DialogOverlap'\nimport { useDialogPortal } from '../useDialogPortal'\n\nimport type { DialogSize } from '../types'\n\ntype AbstractProps = PropsWithChildren<{\n /**\n * ダイアログのタイトルの内容\n */\n heading: ReactNode\n /**\n * ダイアログのフッタ部分の内容\n */\n footer?: ReactNode\n /**\n * ダイアログが開かれているかどうかの真偽値\n */\n isOpen: boolean\n /**\n * 閉じるボタンを押下したときのハンドラ\n */\n onClickClose?: (e: MouseEvent<HTMLButtonElement>) => void\n /**\n * ダイアログが開いている状態で Escape キーを押下したときのハンドラ\n */\n onPressEscape?: () => void\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの高さ\n */\n height?: string | number\n /**\n * ダイアログを開いたときの初期 top 位置\n */\n top?: string | number\n /**\n * ダイアログを開いたときの初期 left 位置\n */\n left?: string | number\n /**\n * ダイアログを開いたときの初期 right 位置\n */\n right?: string | number\n /**\n * ダイアログを開いたときの初期 bottom 位置\n */\n bottom?: string | number\n /**\n * ポータルの container となる DOM 要素を追加する親要素\n */\n portalParent?: HTMLElement | RefObject<HTMLElement>\n}>\ntype Props = AbstractProps &\n Omit<DialogBodyProps, keyof AbstractProps> &\n Omit<BaseElementProps, keyof AbstractProps> &\n Omit<VariantProps<typeof classNameGenerator>, keyof AbstractProps>\n\nconst classNameGenerator = tv({\n slots: {\n overlap: 'shr-inset-[unset]',\n wrapper: 'smarthr-ui-ModelessDialog shr-fixed shr-flex shr-flex-col',\n headerEl: [\n 'smarthr-ui-ModelessDialog-header shr-border-b-shorthand shr-relative shr-flex shr-cursor-move shr-items-center shr-rounded-tl-l shr-rounded-tr-l shr-pe-1 shr-ps-1.5',\n 'hover:shr-bg-white-darken',\n /* DialogHandlerにフォーカスが当たっているときは、headerもフォーカス状態のスタイルにする。 */\n 'has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-focus-indicator has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-bg-white-darken has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-transition-colors has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-duration-100 has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-ease-in-out',\n ],\n dialogHandler: [\n 'smarthr-ui-ModelessDialog-handle shr-absolute shr-inset-x-0 shr-bottom-0 shr-top-[2px] shr-m-auto shr-flex shr-justify-center shr-rounded-tl-s shr-rounded-tr-s shr-border-none shr-text-grey shr-transition-colors shr-duration-100 shr-ease-in-out',\n 'focus-visible:shr-focus-indicator--none shr-cursor-[inherit] shr-bg-[unset]',\n ],\n headingEl: ['shr-my-1 shr-me-1'],\n closeButtonLayout: [\n 'shr-relative' /* DialogHandlerの上に出すためにスタッキングコンテキストを生成 */,\n 'shr-ml-auto shr-shrink-0',\n ],\n footerEl: 'smarthr-ui-ModelessDialog-footer shr-border-t-shorthand',\n },\n variants: {\n size: {\n XS: { wrapper: dialogSize.XS },\n S: { wrapper: dialogSize.S },\n M: { wrapper: dialogSize.M },\n L: { wrapper: dialogSize.L },\n XL: { wrapper: dialogSize.XL },\n XXL: { wrapper: dialogSize.XXL },\n FULL: { wrapper: dialogSize.FULL },\n },\n resizable: {\n true: {\n wrapper: 'shr-resize shr-overflow-auto',\n },\n false: {},\n },\n },\n})\n\nexport const ModelessDialog: FC<Props> = ({\n heading,\n children,\n contentBgColor,\n contentPadding,\n footer,\n isOpen,\n onPressEscape,\n resizable = false,\n width,\n size,\n height,\n top,\n left,\n right,\n bottom,\n portalParent,\n className,\n id,\n onClickClose,\n ...rest\n}) => {\n const labelId = useId()\n const lastFocusElementRef = useRef<HTMLElement | null>(null)\n const { createPortal } = useDialogPortal(portalParent, id)\n const { localize } = useIntl()\n\n const classNames = useMemo(() => {\n const { overlap, wrapper, headerEl, headingEl, dialogHandler, closeButtonLayout, footerEl } =\n classNameGenerator()\n\n return {\n overlap: overlap({ className }),\n wrapper: wrapper({ size, resizable }),\n header: headerEl(),\n heading: headingEl(),\n dialogHandler: dialogHandler(),\n closeButtonLayout: closeButtonLayout(),\n footer: footerEl(),\n }\n }, [className, size, resizable])\n\n const wrapperRef = useRef<HTMLDivElement>(null)\n const focusTargetRef = useRef<HTMLDivElement>(null)\n\n const [wrapperPosition, setWrapperPosition] = useState<DOMRect | undefined>(undefined)\n const [debouncedLiveRegionText, setDebouncedLiveRegionText] = useState<string>('')\n const [centering, setCentering] = useState<{\n top?: number\n left?: number\n }>({})\n const [position, setPosition] = useState<{ x: number; y: number }>({\n x: 0,\n y: 0,\n })\n const [draggableBounds, setDraggableBounds] =\n useState<ComponentProps<typeof Draggable>['bounds']>()\n const debounceLiveRegionText = useMemo(() => debounce(setDebouncedLiveRegionText, 600), [])\n\n useEffect(() => {\n if (!wrapperPosition) {\n setDebouncedLiveRegionText('')\n return\n }\n\n const txt = localize(\n {\n id: 'smarthr-ui/ModelessDialog/dialogHandlerLiveRegionText',\n defaultText: '上から{top}px、左から{left}px',\n },\n {\n top: Math.trunc(wrapperPosition.top).toString(),\n left: Math.trunc(wrapperPosition.left).toString(),\n },\n )\n\n debounceLiveRegionText(txt)\n }, [localize, wrapperPosition, debounceLiveRegionText])\n\n const positionStyle = useMemo(\n () => ({\n top: centering.top ?? top,\n left: centering.left ?? left,\n right,\n bottom,\n width: size ? undefined : width,\n height,\n }),\n [centering, top, left, right, bottom, width, height, size],\n )\n\n const handleArrowKey = useCallback(\n (e: KeyboardEvent) => {\n if (!isOpen || document.activeElement !== e.currentTarget) {\n return\n }\n\n const movingDistance = 20\n\n switch (e.key) {\n case 'ArrowUp':\n setPosition((prev) => ({\n x: prev.x,\n y: prev.y - movingDistance,\n }))\n e.preventDefault()\n break\n case 'ArrowDown':\n setPosition((prev) => ({\n x: prev.x,\n y: prev.y + movingDistance,\n }))\n e.preventDefault()\n break\n case 'ArrowLeft':\n setPosition((prev) => ({\n x: prev.x - movingDistance,\n y: prev.y,\n }))\n e.preventDefault()\n break\n case 'ArrowRight':\n setPosition((prev) => ({\n x: prev.x + movingDistance,\n y: prev.y,\n }))\n e.preventDefault()\n break\n }\n },\n [isOpen],\n )\n\n useEffect(() => {\n if (wrapperRef.current instanceof Element) {\n setWrapperPosition(wrapperRef.current.getBoundingClientRect())\n }\n }, [position])\n\n useEffect(() => {\n // 中央寄せの座標計算を行う\n if (!wrapperRef.current || !isOpen) {\n return\n }\n\n const isXCenter = left === undefined && right === undefined\n const isYCenter = top === undefined && bottom === undefined\n\n if (isXCenter || isYCenter) {\n const rect = wrapperRef.current.getBoundingClientRect()\n\n setCentering({\n top: isYCenter ? window.innerHeight / 2 - rect.height / 2 : undefined,\n left: isXCenter ? window.innerWidth / 2 - rect.width / 2 : undefined,\n })\n }\n }, [bottom, isOpen, left, right, top])\n\n useEffect(() => {\n if (!isOpen) return\n\n if (centering.top) {\n setDraggableBounds({ top: centering.top * -1 })\n\n return\n }\n\n if (wrapperRef.current) {\n const rect = wrapperRef.current.getBoundingClientRect()\n\n setDraggableBounds({ top: rect.top * -1 })\n }\n }, [isOpen, centering.top])\n\n useEffect(() => {\n if (isOpen) {\n setPosition({ x: 0, y: 0 })\n focusTargetRef.current?.focus()\n }\n }, [isOpen])\n\n const actualOnClickClose = useCallback(\n (e: MouseEvent<HTMLButtonElement>) => {\n lastFocusElementRef.current?.focus()\n onClickClose?.(e)\n },\n [onClickClose],\n )\n\n const actualOnPressEscape = useMemo(\n () =>\n onPressEscape\n ? () => {\n lastFocusElementRef.current?.focus()\n onPressEscape()\n }\n : undefined,\n [onPressEscape],\n )\n\n useHandleEscape(\n useMemo(\n () => (actualOnPressEscape && isOpen ? actualOnPressEscape : undefined),\n [isOpen, actualOnPressEscape],\n ),\n )\n\n useEffect(() => {\n const focusHandler = (e: FocusEvent) => {\n // e.target(現在フォーカスがあたっている要素)がModeless dialog外の要素であれば、lastFocusElementRefに代入する\n if (e.target instanceof HTMLElement && !wrapperRef?.current?.contains(e.target)) {\n lastFocusElementRef.current = e.target\n }\n }\n\n document.addEventListener('focus', focusHandler, true)\n\n return () => document.removeEventListener('focus', focusHandler, true)\n }, [])\n\n const onDragStart = useCallback((_: any, data: { x: number; y: number }) => setPosition(data), [])\n const onDrag = useCallback((_: any, data: { deltaX: number; deltaY: number }) => {\n setPosition((prev) => ({\n x: prev.x + data.deltaX,\n y: prev.y + data.deltaY,\n }))\n }, [])\n\n return createPortal(\n <DialogOverlap isOpen={isOpen} className={classNames.overlap} as=\"section\">\n <Draggable\n handle=\".smarthr-ui-ModelessDialog-handle\"\n onStart={onDragStart}\n onDrag={onDrag}\n position={position}\n bounds={draggableBounds}\n nodeRef={wrapperRef}\n >\n <Base\n {...rest}\n ref={wrapperRef}\n role=\"dialog\"\n aria-labelledby={labelId}\n radius=\"m\"\n layer={3}\n overflow=\"auto\"\n className={classNames.wrapper}\n style={positionStyle}\n >\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div tabIndex={-1} ref={focusTargetRef} />\n <div className={classNames.header}>\n <Handler onArrowKeyDown={handleArrowKey} className={classNames.dialogHandler} />\n <div id={labelId} className={classNames.heading}>\n {/* eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content */}\n <Heading>{heading}</Heading>\n </div>\n <CloseButton onClick={actualOnClickClose} className={classNames.closeButtonLayout} />\n </div>\n <DialogBody\n contentBgColor={contentBgColor}\n contentPadding={contentPadding}\n className=\"smarthr-ui-ModelessDialog-content shr-overscroll-contain\"\n >\n {children}\n </DialogBody>\n {footer && <div className={classNames.footer}>{footer}</div>}\n <LiveRegion regionText={debouncedLiveRegionText} />\n </Base>\n </Draggable>\n </DialogOverlap>,\n )\n}\n\nconst Handler = memo<{\n className: string\n onArrowKeyDown: (e: KeyboardEvent) => void\n}>(({ onArrowKeyDown: onDelegateKeyDown, ...rest }) => {\n const { localize } = useIntl()\n const accessibleDefaultTexts = useMemo(\n () => ({\n dialogHandlerAriaRoleDescription: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaRoleDescription',\n defaultText: 'ドラッグ可能',\n }),\n dialogHandlerDescription: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerDescription',\n defaultText: '矢印キーを押して上下左右に移動できます',\n }),\n dialogHandlerAriaLabel: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaLabel',\n defaultText: 'ダイアログの位置',\n }),\n }),\n [localize],\n )\n\n return (\n <>\n <button\n {...rest}\n type=\"button\"\n aria-label={accessibleDefaultTexts.dialogHandlerAriaLabel}\n aria-roledescription={accessibleDefaultTexts.dialogHandlerAriaRoleDescription}\n aria-describedby=\"handler-description\"\n onKeyDown={onDelegateKeyDown}\n >\n <FaGripIcon />\n </button>\n <div className=\"shr-hidden\" id=\"handler-description\">\n {accessibleDefaultTexts.dialogHandlerDescription}\n </div>\n </>\n )\n})\n\nconst LiveRegion = ({ regionText }: { regionText: string | undefined }) => (\n <div\n role=\"status\"\n className=\"shr-fixed -shr-m-px shr-h-px shr-w-px shr-overflow-hidden shr-whitespace-nowrap\"\n >\n {regionText}\n </div>\n)\n\nconst CloseButton = memo<{\n className: string\n onClick: (e: MouseEvent<HTMLButtonElement>) => void\n}>(({ onClick, className }) => {\n const { localize } = useIntl()\n const closeButtonIconAlt = localize({\n id: 'smarthr-ui/ModelessDialog/closeButtonIconAlt',\n defaultText: '閉じる',\n })\n\n return (\n <div className={className}>\n <Button\n type=\"button\"\n size=\"S\"\n onClick={onClick}\n className=\"smarthr-ui-ModelessDialog-closeButton\"\n >\n <FaXmarkIcon alt={closeButtonIconAlt} />\n </Button>\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+FA;AACE;AACE;AACA;AACA;;;;;AAKC;AACD;;;AAGC;;AAED;AACE;;AAED;AACD;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AACD;AACE;AACE;AACD;AACD;AACD;AACF;AACF;;AAwBC;AACA;;AAEA;AAEA;AACE;;AAIE;;;;;;;;;AAUJ;AACA;;;;AAQA;AACE;AACA;AACD;;AAGD;;;;;;;AAUM;AACA;;;;AAKD;;;AAML;AAEI;AACA;;;;;AAKD;AAIH;;;;;AAQI;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;AAIP;;AAKA;;;AAGF;;;;;;;;AAWE;;AAGE;AACE;AACA;AACD;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;AAEI;AACA;AACF;AAIF;;AAIU;AACA;;AAEJ;;;AAYJ;;AAEE;AACE;;AAEJ;;AAIA;;AAGF;;AAEE;AACE;AACA;AACD;;;AA+CL;AAEA;AAIE;AACA;;AAGM;AACA;;;AAGA;AACA;;;AAGA;AACA;;AAEH;AAIH;AAiBF;AAEA;AASA;AAIE;;AAEE;AACA;AACD;AAED;AAYF;;"}
|
|
1
|
+
{"version":3,"file":"ModelessDialog.cjs","sources":["../../../../src/components/Dialog/ModelessDialog/ModelessDialog.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ComponentProps,\n type FC,\n type KeyboardEvent,\n type MouseEvent,\n type PropsWithChildren,\n type ReactNode,\n type RefObject,\n memo,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport Draggable from 'react-draggable'\nimport { type VariantProps, tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../../hooks/useHandleEscape'\nimport { useIntl } from '../../../intl'\nimport { debounce } from '../../../libs/debounce'\nimport { dialogSize } from '../../../tailwind'\nimport { Base, type BaseElementProps } from '../../Base'\nimport { Button } from '../../Button'\nimport { Heading } from '../../Heading'\nimport { FaGripIcon, FaXmarkIcon } from '../../Icon'\nimport { DialogBody, type Props as DialogBodyProps } from '../DialogBody'\nimport { DialogOverlap } from '../DialogOverlap'\nimport { useDialogPortal } from '../useDialogPortal'\n\nimport type { DialogSize } from '../types'\n\ntype AbstractProps = PropsWithChildren<{\n /**\n * ダイアログのタイトルの内容\n */\n heading: ReactNode\n /**\n * ダイアログのフッタ部分の内容\n */\n footer?: ReactNode\n /**\n * ダイアログが開かれているかどうかの真偽値\n */\n isOpen: boolean\n /**\n * 閉じるボタンを押下したときのハンドラ\n */\n onClickClose?: (e: MouseEvent<HTMLButtonElement>) => void\n /**\n * ダイアログが開いている状態で Escape キーを押下したときのハンドラ\n */\n onPressEscape?: () => void\n /**\n * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n * ダイアログの幅\n */\n width?: string | number\n /**\n * ダイアログの大きさ\n */\n size?: DialogSize\n /**\n * ダイアログの高さ\n */\n height?: string | number\n /**\n * ダイアログを開いたときの初期 top 位置\n */\n top?: string | number\n /**\n * ダイアログを開いたときの初期 left 位置\n */\n left?: string | number\n /**\n * ダイアログを開いたときの初期 right 位置\n */\n right?: string | number\n /**\n * ダイアログを開いたときの初期 bottom 位置\n */\n bottom?: string | number\n /**\n * ポータルの container となる DOM 要素を追加する親要素\n */\n portalParent?: HTMLElement | RefObject<HTMLElement>\n}>\ntype Props = AbstractProps &\n Omit<DialogBodyProps, keyof AbstractProps> &\n Omit<BaseElementProps, keyof AbstractProps> &\n Omit<VariantProps<typeof classNameGenerator>, keyof AbstractProps>\n\nconst classNameGenerator = tv({\n slots: {\n overlap: 'shr-inset-[unset]',\n wrapper: 'smarthr-ui-ModelessDialog shr-fixed shr-flex shr-flex-col',\n headerEl: [\n 'smarthr-ui-ModelessDialog-header shr-border-b-shorthand shr-relative shr-flex shr-cursor-move shr-items-center shr-rounded-tl-l shr-rounded-tr-l shr-pe-1 shr-ps-1.5',\n 'hover:shr-bg-white-darken',\n /* DialogHandlerにフォーカスが当たっているときは、headerもフォーカス状態のスタイルにする。 */\n 'has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-focus-indicator has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-bg-white-darken has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-transition-colors has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-duration-100 has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-ease-in-out',\n ],\n dialogHandler: [\n 'smarthr-ui-ModelessDialog-handle shr-absolute shr-inset-x-0 shr-bottom-0 shr-top-[2px] shr-m-auto shr-flex shr-justify-center shr-rounded-tl-s shr-rounded-tr-s shr-border-none shr-text-grey shr-transition-colors shr-duration-100 shr-ease-in-out',\n 'focus-visible:shr-focus-indicator--none shr-cursor-[inherit] shr-bg-[unset]',\n ],\n headingEl: ['shr-my-1 shr-me-1'],\n closeButtonLayout: [\n 'shr-relative' /* DialogHandlerの上に出すためにスタッキングコンテキストを生成 */,\n 'shr-ml-auto shr-shrink-0',\n ],\n footerEl: 'smarthr-ui-ModelessDialog-footer shr-border-t-shorthand',\n },\n variants: {\n size: {\n XS: { wrapper: dialogSize.XS },\n S: { wrapper: dialogSize.S },\n M: { wrapper: dialogSize.M },\n L: { wrapper: dialogSize.L },\n XL: { wrapper: dialogSize.XL },\n XXL: { wrapper: dialogSize.XXL },\n FULL: { wrapper: dialogSize.FULL },\n },\n resizable: {\n true: {\n wrapper: 'shr-resize shr-overflow-auto',\n },\n false: {},\n },\n },\n})\n\nexport const ModelessDialog: FC<Props> = ({\n heading,\n children,\n contentBgColor,\n contentPadding,\n footer,\n isOpen,\n onPressEscape,\n resizable = false,\n width,\n size,\n height,\n top,\n left,\n right,\n bottom,\n portalParent,\n className,\n id,\n onClickClose,\n ...rest\n}) => {\n const labelId = useId()\n const lastFocusElementRef = useRef<HTMLElement | null>(null)\n const { createPortal } = useDialogPortal(portalParent, id)\n const { localize } = useIntl()\n\n const classNames = useMemo(() => {\n const { overlap, wrapper, headerEl, headingEl, dialogHandler, closeButtonLayout, footerEl } =\n classNameGenerator()\n\n return {\n overlap: overlap({ className }),\n wrapper: wrapper({ size, resizable }),\n header: headerEl(),\n heading: headingEl(),\n dialogHandler: dialogHandler(),\n closeButtonLayout: closeButtonLayout(),\n footer: footerEl(),\n }\n }, [className, size, resizable])\n\n const wrapperRef = useRef<HTMLDivElement>(null)\n const focusTargetRef = useRef<HTMLDivElement>(null)\n\n const [wrapperPosition, setWrapperPosition] = useState<DOMRect | undefined>(undefined)\n const [debouncedLiveRegionText, setDebouncedLiveRegionText] = useState<string>('')\n const [centering, setCentering] = useState<{\n top?: number\n left?: number\n }>({})\n const [position, setPosition] = useState<{ x: number; y: number }>({\n x: 0,\n y: 0,\n })\n const [draggableBounds, setDraggableBounds] =\n useState<ComponentProps<typeof Draggable>['bounds']>()\n const debounceLiveRegionText = useMemo(() => debounce(setDebouncedLiveRegionText, 600), [])\n\n useEffect(() => {\n if (!wrapperPosition) {\n setDebouncedLiveRegionText('')\n return\n }\n\n const txt = localize(\n {\n id: 'smarthr-ui/ModelessDialog/dialogHandlerLiveRegionText',\n defaultText: '上から{top}px、左から{left}px',\n },\n {\n top: Math.trunc(wrapperPosition.top).toString(),\n left: Math.trunc(wrapperPosition.left).toString(),\n },\n )\n\n debounceLiveRegionText(txt)\n }, [localize, wrapperPosition, debounceLiveRegionText])\n\n // 外部propsをrefに保存\n const unstableRef = useRef({ isOpen, onClickClose, onPressEscape })\n unstableRef.current = { isOpen, onClickClose, onPressEscape }\n\n const positionStyle = useMemo(\n () => ({\n top: centering.top ?? top,\n left: centering.left ?? left,\n right,\n bottom,\n width: size ? undefined : width,\n height,\n }),\n [centering, top, left, right, bottom, width, height, size],\n )\n\n const handleArrowKey = useCallback((e: KeyboardEvent) => {\n if (!unstableRef.current.isOpen || document.activeElement !== e.currentTarget) {\n return\n }\n\n const movingDistance = 20\n\n switch (e.key) {\n case 'ArrowUp':\n setPosition((prev) => ({\n x: prev.x,\n y: prev.y - movingDistance,\n }))\n e.preventDefault()\n break\n case 'ArrowDown':\n setPosition((prev) => ({\n x: prev.x,\n y: prev.y + movingDistance,\n }))\n e.preventDefault()\n break\n case 'ArrowLeft':\n setPosition((prev) => ({\n x: prev.x - movingDistance,\n y: prev.y,\n }))\n e.preventDefault()\n break\n case 'ArrowRight':\n setPosition((prev) => ({\n x: prev.x + movingDistance,\n y: prev.y,\n }))\n e.preventDefault()\n break\n }\n }, [])\n\n useEffect(() => {\n if (wrapperRef.current instanceof Element) {\n setWrapperPosition(wrapperRef.current.getBoundingClientRect())\n }\n }, [position])\n\n useEffect(() => {\n // 中央寄せの座標計算を行う\n if (!wrapperRef.current || !isOpen) {\n return\n }\n\n const isXCenter = left === undefined && right === undefined\n const isYCenter = top === undefined && bottom === undefined\n\n if (isXCenter || isYCenter) {\n const rect = wrapperRef.current.getBoundingClientRect()\n\n setCentering({\n top: isYCenter ? window.innerHeight / 2 - rect.height / 2 : undefined,\n left: isXCenter ? window.innerWidth / 2 - rect.width / 2 : undefined,\n })\n }\n }, [bottom, isOpen, left, right, top])\n\n useEffect(() => {\n if (!isOpen) return\n\n if (centering.top) {\n setDraggableBounds({ top: centering.top * -1 })\n\n return\n }\n\n if (wrapperRef.current) {\n const rect = wrapperRef.current.getBoundingClientRect()\n\n setDraggableBounds({ top: rect.top * -1 })\n }\n }, [isOpen, centering.top])\n\n useEffect(() => {\n if (isOpen) {\n setPosition({ x: 0, y: 0 })\n focusTargetRef.current?.focus()\n }\n }, [isOpen])\n\n const actualOnClickClose = useCallback((e: MouseEvent<HTMLButtonElement>) => {\n lastFocusElementRef.current?.focus()\n unstableRef.current.onClickClose?.(e)\n }, [])\n\n // stableなcallbackを作成\n const memoizedOnPressEscape = useCallback(() => {\n lastFocusElementRef.current?.focus()\n unstableRef.current.onPressEscape?.()\n }, [])\n\n useHandleEscape(isOpen ? memoizedOnPressEscape : undefined)\n\n useEffect(() => {\n const focusHandler = (e: FocusEvent) => {\n // e.target(現在フォーカスがあたっている要素)がModeless dialog外の要素であれば、lastFocusElementRefに代入する\n if (e.target instanceof HTMLElement && !wrapperRef?.current?.contains(e.target)) {\n lastFocusElementRef.current = e.target\n }\n }\n\n document.addEventListener('focus', focusHandler, true)\n\n return () => document.removeEventListener('focus', focusHandler, true)\n }, [])\n\n const onDragStart = useCallback((_: any, data: { x: number; y: number }) => setPosition(data), [])\n const onDrag = useCallback((_: any, data: { deltaX: number; deltaY: number }) => {\n setPosition((prev) => ({\n x: prev.x + data.deltaX,\n y: prev.y + data.deltaY,\n }))\n }, [])\n\n return createPortal(\n <DialogOverlap isOpen={isOpen} className={classNames.overlap} as=\"section\">\n <Draggable\n handle=\".smarthr-ui-ModelessDialog-handle\"\n onStart={onDragStart}\n onDrag={onDrag}\n position={position}\n bounds={draggableBounds}\n nodeRef={wrapperRef}\n >\n <Base\n {...rest}\n ref={wrapperRef}\n role=\"dialog\"\n aria-labelledby={labelId}\n radius=\"m\"\n layer={3}\n overflow=\"auto\"\n className={classNames.wrapper}\n style={positionStyle}\n >\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div tabIndex={-1} ref={focusTargetRef} />\n <div className={classNames.header}>\n <Handler onArrowKeyDown={handleArrowKey} className={classNames.dialogHandler} />\n <div id={labelId} className={classNames.heading}>\n {/* eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content */}\n <Heading>{heading}</Heading>\n </div>\n <CloseButton onClick={actualOnClickClose} className={classNames.closeButtonLayout} />\n </div>\n <DialogBody\n contentBgColor={contentBgColor}\n contentPadding={contentPadding}\n className=\"smarthr-ui-ModelessDialog-content shr-overscroll-contain\"\n >\n {children}\n </DialogBody>\n {footer && <div className={classNames.footer}>{footer}</div>}\n <LiveRegion regionText={debouncedLiveRegionText} />\n </Base>\n </Draggable>\n </DialogOverlap>,\n )\n}\n\nconst Handler = memo<{\n className: string\n onArrowKeyDown: (e: KeyboardEvent) => void\n}>(({ onArrowKeyDown: onDelegateKeyDown, ...rest }) => {\n const { localize } = useIntl()\n const accessibleDefaultTexts = useMemo(\n () => ({\n dialogHandlerAriaRoleDescription: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaRoleDescription',\n defaultText: 'ドラッグ可能',\n }),\n dialogHandlerDescription: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerDescription',\n defaultText: '矢印キーを押して上下左右に移動できます',\n }),\n dialogHandlerAriaLabel: localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaLabel',\n defaultText: 'ダイアログの位置',\n }),\n }),\n [localize],\n )\n\n return (\n <>\n <button\n {...rest}\n type=\"button\"\n aria-label={accessibleDefaultTexts.dialogHandlerAriaLabel}\n aria-roledescription={accessibleDefaultTexts.dialogHandlerAriaRoleDescription}\n aria-describedby=\"handler-description\"\n onKeyDown={onDelegateKeyDown}\n >\n <FaGripIcon />\n </button>\n <div className=\"shr-hidden\" id=\"handler-description\">\n {accessibleDefaultTexts.dialogHandlerDescription}\n </div>\n </>\n )\n})\n\nconst LiveRegion = ({ regionText }: { regionText: string | undefined }) => (\n <div\n role=\"status\"\n className=\"shr-fixed -shr-m-px shr-h-px shr-w-px shr-overflow-hidden shr-whitespace-nowrap\"\n >\n {regionText}\n </div>\n)\n\nconst CloseButton = memo<{\n className: string\n onClick: (e: MouseEvent<HTMLButtonElement>) => void\n}>(({ onClick, className }) => {\n const { localize } = useIntl()\n const closeButtonIconAlt = localize({\n id: 'smarthr-ui/ModelessDialog/closeButtonIconAlt',\n defaultText: '閉じる',\n })\n\n return (\n <div className={className}>\n <Button\n type=\"button\"\n size=\"S\"\n onClick={onClick}\n className=\"smarthr-ui-ModelessDialog-closeButton\"\n >\n <FaXmarkIcon alt={closeButtonIconAlt} />\n </Button>\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+FA;AACE;AACE;AACA;AACA;;;;;AAKC;AACD;;;AAGC;;AAED;AACE;;AAED;AACD;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AACD;AACE;AACE;AACD;AACD;AACD;AACF;AACF;;AAwBC;AACA;;AAEA;AAEA;AACE;;AAIE;;;;;;;;;AAUJ;AACA;;;;AAQA;AACE;AACA;AACD;;AAGD;;;;;;;AAUM;AACA;;;;AAKD;;;;AAOL;;AAGA;AAEI;AACA;;;;;AAKD;AAIH;AACE;;;;AAMA;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;;;AAOL;;;AAGF;;;;;;;;AAWE;;AAGE;AACE;AACA;AACD;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;AACE;;;;AAKF;AACE;AACA;;;;AAMA;;AAEE;AACE;;AAEJ;;AAIA;;AAGF;;AAEE;AACE;AACA;AACD;;;AA+CL;AAEA;AAIE;AACA;;AAGM;AACA;;;AAGA;AACA;;;AAGA;AACA;;AAEH;AAIH;AAiBF;AAEA;AASA;AAIE;;AAEE;AACA;AACD;AAED;AAYF;;"}
|
|
@@ -116,6 +116,9 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
116
116
|
});
|
|
117
117
|
debounceLiveRegionText(txt);
|
|
118
118
|
}, [localize, wrapperPosition, debounceLiveRegionText]);
|
|
119
|
+
// 外部propsをrefに保存
|
|
120
|
+
const unstableRef = useRef({ isOpen, onClickClose, onPressEscape });
|
|
121
|
+
unstableRef.current = { isOpen, onClickClose, onPressEscape };
|
|
119
122
|
const positionStyle = useMemo(() => ({
|
|
120
123
|
top: centering.top ?? top,
|
|
121
124
|
left: centering.left ?? left,
|
|
@@ -125,7 +128,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
125
128
|
height,
|
|
126
129
|
}), [centering, top, left, right, bottom, width, height, size]);
|
|
127
130
|
const handleArrowKey = useCallback((e) => {
|
|
128
|
-
if (!isOpen || document.activeElement !== e.currentTarget) {
|
|
131
|
+
if (!unstableRef.current.isOpen || document.activeElement !== e.currentTarget) {
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
134
|
const movingDistance = 20;
|
|
@@ -159,7 +162,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
159
162
|
e.preventDefault();
|
|
160
163
|
break;
|
|
161
164
|
}
|
|
162
|
-
}, [
|
|
165
|
+
}, []);
|
|
163
166
|
useEffect(() => {
|
|
164
167
|
if (wrapperRef.current instanceof Element) {
|
|
165
168
|
setWrapperPosition(wrapperRef.current.getBoundingClientRect());
|
|
@@ -200,15 +203,14 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
|
|
|
200
203
|
}, [isOpen]);
|
|
201
204
|
const actualOnClickClose = useCallback((e) => {
|
|
202
205
|
lastFocusElementRef.current?.focus();
|
|
203
|
-
onClickClose?.(e);
|
|
204
|
-
}, [
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
useHandleEscape(useMemo(() => (actualOnPressEscape && isOpen ? actualOnPressEscape : undefined), [isOpen, actualOnPressEscape]));
|
|
206
|
+
unstableRef.current.onClickClose?.(e);
|
|
207
|
+
}, []);
|
|
208
|
+
// stableなcallbackを作成
|
|
209
|
+
const memoizedOnPressEscape = useCallback(() => {
|
|
210
|
+
lastFocusElementRef.current?.focus();
|
|
211
|
+
unstableRef.current.onPressEscape?.();
|
|
212
|
+
}, []);
|
|
213
|
+
useHandleEscape(isOpen ? memoizedOnPressEscape : undefined);
|
|
212
214
|
useEffect(() => {
|
|
213
215
|
const focusHandler = (e) => {
|
|
214
216
|
// e.target(現在フォーカスがあたっている要素)がModeless dialog外の要素であれば、lastFocusElementRefに代入する
|