@daoyi/nmtl-ui 2.0.1-beta.136 → 2.0.1-beta.138
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/Breadcrumbs.js +33 -20
- package/lib/components/{ContactModal-19979528.js → ContactModal-af7fa280.js} +1 -1
- package/lib/components/ContactModal.js +2 -2
- package/lib/components/Footer.js +2 -2
- package/lib/components/Header.js +37 -8
- package/lib/components/Keywords.js +1 -1
- package/lib/components/{Modal-b984f381.js → Modal-aa77d4d8.js} +45 -1
- package/lib/components/Modal.js +1 -1
- package/lib/components/globalConfig.js +27 -6
- package/lib/utils/prepareTimelineData.js +8 -6
- package/lib/utils/syncTimelineA11y.js +241 -18
- package/package.json +1 -1
|
@@ -51,7 +51,7 @@ BreadcrumbItem.propTypes = {
|
|
|
51
51
|
onClick: PropTypes.func
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
var css_248z = ".breadcrumbs__link {\n cursor: pointer;\n text-decoration: none !important;\n}";
|
|
54
|
+
var css_248z = "@charset \"UTF-8\";\n.breadcrumbs__link {\n cursor: pointer;\n text-decoration: none !important;\n /* WCAG 2.4.7(2026-10-08 人工測試回饋):連結內是 Typography span > div.breadcrumbItem(flex),\n inline 的 <a> 自己沒有文字列,Chrome 的虛線焦點框沿空的行框畫,畫面上看不到。\n 改為 inline-flex,焦點框才會包住整個項目 */\n display: inline-flex;\n}";
|
|
55
55
|
styleInject(css_248z);
|
|
56
56
|
|
|
57
57
|
var Breadcrumbs = function Breadcrumbs(_ref) {
|
|
@@ -69,48 +69,61 @@ var Breadcrumbs = function Breadcrumbs(_ref) {
|
|
|
69
69
|
var icon = _ref2.icon,
|
|
70
70
|
text = _ref2.text,
|
|
71
71
|
href = _ref2.href,
|
|
72
|
-
|
|
72
|
+
_onClick = _ref2.onClick;
|
|
73
73
|
var lastLink = i === links.length - 1;
|
|
74
74
|
|
|
75
|
-
/* 尾巴=目前所在頁:aria-current="page" 標在這裡(WCAG 4.1.2,數位部人工檢測 GN1410200E,2026-10-07
|
|
75
|
+
/* 尾巴=目前所在頁:aria-current="page" 標在這裡(WCAG 4.1.2,數位部人工檢測 GN1410200E,2026-10-07)。
|
|
76
|
+
目前頁不是互動元件:不掛 onClick(原本滑鼠點得到、鍵盤卻聚焦不到,2026-10-08 人工測試回饋) */
|
|
76
77
|
if (lastLink) return /*#__PURE__*/React__default.createElement(BreadcrumbItem, {
|
|
77
78
|
key: i.toString(),
|
|
78
79
|
lastItem: true,
|
|
79
80
|
current: true,
|
|
80
81
|
icon: icon,
|
|
81
|
-
text: text
|
|
82
|
-
onClick: onClick
|
|
82
|
+
text: text
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
/*
|
|
85
|
+
/* 頭、中間有網址:用 Link 而不是 NavLink。NavLink 沒加 exact 時,「首頁」的 /zh-tw/ 是所有網址的開頭,
|
|
86
86
|
會被自動標成 aria-current="page"+active,讀屏誤報首頁為目前頁(GN1410200E) */
|
|
87
|
-
if (i === 0) {
|
|
87
|
+
if (href || i === 0 && !_onClick) {
|
|
88
88
|
return /*#__PURE__*/React__default.createElement(Link, {
|
|
89
89
|
className: "breadcrumbs__link",
|
|
90
90
|
key: i.toString(),
|
|
91
91
|
to: href || '/',
|
|
92
|
-
onClick:
|
|
92
|
+
onClick: _onClick
|
|
93
93
|
}, /*#__PURE__*/React__default.createElement(BreadcrumbItem, {
|
|
94
94
|
icon: icon,
|
|
95
95
|
text: text
|
|
96
96
|
}));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
/* 只有 onClick(程式導頁):原本渲染成純文字,滑鼠點得到但鍵盤無法聚焦(WCAG 2.1.1,2026-10-08 人工測試回饋)。
|
|
100
|
+
作用是前往其他頁,語意是連結 → <a href="#">,可 Tab 聚焦、Enter 啟動,焦點框沿用各站 :focus-visible */
|
|
101
|
+
if (_onClick) {
|
|
102
|
+
return (
|
|
103
|
+
/*#__PURE__*/
|
|
104
|
+
/* 點下去確實會換頁(由呼叫端的 onClick 導頁),不是按鈕 */
|
|
105
|
+
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
|
106
|
+
React__default.createElement("a", {
|
|
107
|
+
className: "breadcrumbs__link",
|
|
108
|
+
key: i.toString(),
|
|
109
|
+
href: "#",
|
|
110
|
+
onClick: function onClick(e) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
_onClick(e);
|
|
113
|
+
}
|
|
114
|
+
}, /*#__PURE__*/React__default.createElement(BreadcrumbItem, {
|
|
115
|
+
icon: icon,
|
|
116
|
+
text: text
|
|
117
|
+
}))
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* 中間沒有網址也沒有動作:純文字 */
|
|
122
|
+
return /*#__PURE__*/React__default.createElement(BreadcrumbItem, {
|
|
109
123
|
key: i.toString(),
|
|
110
124
|
lastItem: true,
|
|
111
125
|
icon: icon,
|
|
112
|
-
text: text
|
|
113
|
-
onClick: onClick
|
|
126
|
+
text: text
|
|
114
127
|
});
|
|
115
128
|
}));
|
|
116
129
|
};
|
|
@@ -8,7 +8,7 @@ import '@material-ui/styles';
|
|
|
8
8
|
import { u as useTheme } from './useTheme-98b6cf17.js';
|
|
9
9
|
import './withTheme-3486f38b.js';
|
|
10
10
|
import axios from 'axios';
|
|
11
|
-
import { M as Modal } from './Modal-
|
|
11
|
+
import { M as Modal } from './Modal-aa77d4d8.js';
|
|
12
12
|
import { c as commonjsGlobal, a as getDefaultExportFromCjs } from './_commonjsHelpers-70de2c37.js';
|
|
13
13
|
import { c as classNames } from './index-c33afcb5.js';
|
|
14
14
|
import { Input, IconButton, Button, Box, Typography, FormControl, RadioGroup, FormControlLabel, Radio, TextField, TextareaAutosize } from '@material-ui/core';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as default } from './ContactModal-
|
|
1
|
+
export { C as default } from './ContactModal-af7fa280.js';
|
|
2
2
|
import './_rollupPluginBabelHelpers-a52356f6.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import 'prop-types';
|
|
@@ -9,7 +9,7 @@ import '@material-ui/styles';
|
|
|
9
9
|
import './useTheme-98b6cf17.js';
|
|
10
10
|
import './withTheme-3486f38b.js';
|
|
11
11
|
import 'axios';
|
|
12
|
-
import './Modal-
|
|
12
|
+
import './Modal-aa77d4d8.js';
|
|
13
13
|
import './index-c33afcb5.js';
|
|
14
14
|
import '@material-ui/core';
|
|
15
15
|
import '@material-ui/icons';
|
package/lib/components/Footer.js
CHANGED
|
@@ -3,7 +3,7 @@ import React__default, { useState, useEffect } from 'react';
|
|
|
3
3
|
import { c as classNames } from './index-c33afcb5.js';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import axios from 'axios';
|
|
6
|
-
import { C as ContactModal } from './ContactModal-
|
|
6
|
+
import { C as ContactModal } from './ContactModal-af7fa280.js';
|
|
7
7
|
import { S as Spin } from './Spin-07679812.js';
|
|
8
8
|
import { getIntl } from './globalConfig';
|
|
9
9
|
import { s as styleInject } from './style-inject.es-1f59c1d0.js';
|
|
@@ -13,7 +13,7 @@ import './defaultTheme-c5dd0f64.js';
|
|
|
13
13
|
import '@material-ui/styles';
|
|
14
14
|
import './useTheme-98b6cf17.js';
|
|
15
15
|
import './withTheme-3486f38b.js';
|
|
16
|
-
import './Modal-
|
|
16
|
+
import './Modal-aa77d4d8.js';
|
|
17
17
|
import '@material-ui/core';
|
|
18
18
|
import '@material-ui/icons';
|
|
19
19
|
import './Button-8cf35bc0.js';
|
package/lib/components/Header.js
CHANGED
|
@@ -3,7 +3,7 @@ import React__default, { useState, useRef, useEffect } from 'react';
|
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { c as classNames } from './index-c33afcb5.js';
|
|
5
5
|
import { d as debounce_1 } from './debounce-127da66c.js';
|
|
6
|
-
import { useHistory, useRouteMatch, NavLink } from 'react-router-dom';
|
|
6
|
+
import { useHistory, useRouteMatch, useLocation, NavLink } from 'react-router-dom';
|
|
7
7
|
import { Typography, IconButton } from '@material-ui/core';
|
|
8
8
|
import { SearchOutlined } from '@material-ui/icons';
|
|
9
9
|
import { K as KeywordSearch } from './KeywordSearch-4e621ded.js';
|
|
@@ -33,6 +33,7 @@ styleInject(css_248z);
|
|
|
33
33
|
var showDarkModeSwitch = false;
|
|
34
34
|
var Header = function Header(props) {
|
|
35
35
|
var title = props.title,
|
|
36
|
+
siteName = props.siteName,
|
|
36
37
|
logo = props.logo,
|
|
37
38
|
locale = props.locale,
|
|
38
39
|
supportLanguages = props.supportLanguages,
|
|
@@ -53,6 +54,33 @@ var Header = function Header(props) {
|
|
|
53
54
|
var search = history.location.search;
|
|
54
55
|
var _useRouteMatch = useRouteMatch(),
|
|
55
56
|
url = _useRouteMatch.url;
|
|
57
|
+
var _useLocation = useLocation(),
|
|
58
|
+
pathname = _useLocation.pathname;
|
|
59
|
+
|
|
60
|
+
/* 首頁的 logo 不該念「回首頁」(2026-10-08 人工測試回饋):在首頁時念「{站名},目前在首頁」並標 aria-current,
|
|
61
|
+
其他頁念「{站名},回首頁」。站名優先用 siteName(只供報讀、依語系),沒有才用畫面上的 title。
|
|
62
|
+
首頁:主站 /{locale}/main/home、子站 /{locale}/home,也含 / 與 /{locale} */
|
|
63
|
+
var isHome = new RegExp("^(/".concat(locale, ")?(/main)?(/home)?/?$")).test(pathname);
|
|
64
|
+
var logoName = siteName || title;
|
|
65
|
+
var intl = getIntl();
|
|
66
|
+
var logoLabel;
|
|
67
|
+
if (isHome) {
|
|
68
|
+
logoLabel = logoName ? intl.formatMessage({
|
|
69
|
+
id: 'aria.header.logoCurrentHomeWithTitle'
|
|
70
|
+
}, {
|
|
71
|
+
title: logoName
|
|
72
|
+
}) : intl.formatMessage({
|
|
73
|
+
id: 'aria.header.logoCurrentHome'
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
logoLabel = logoName ? intl.formatMessage({
|
|
77
|
+
id: 'aria.header.logoHomeWithTitle'
|
|
78
|
+
}, {
|
|
79
|
+
title: logoName
|
|
80
|
+
}) : intl.formatMessage({
|
|
81
|
+
id: 'aria.header.logoHome'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
56
84
|
// const { keyword } = queryString.parse(search);
|
|
57
85
|
var _useState = useState(false),
|
|
58
86
|
_useState2 = _slicedToArray(_useState, 2),
|
|
@@ -86,13 +114,11 @@ var Header = function Header(props) {
|
|
|
86
114
|
}, /*#__PURE__*/React__default.createElement(NavLink, {
|
|
87
115
|
to: "/".concat(locale, "/main/home"),
|
|
88
116
|
className: "header__logo",
|
|
89
|
-
"aria-label":
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
id: 'aria.header.logoHome'
|
|
95
|
-
})
|
|
117
|
+
"aria-label": logoLabel
|
|
118
|
+
/* aria-current 由是否在首頁決定(子站首頁不是 /main/home,不能靠 NavLink 自動比對) */,
|
|
119
|
+
isActive: function isActive() {
|
|
120
|
+
return isHome;
|
|
121
|
+
}
|
|
96
122
|
}, logo && /*#__PURE__*/React__default.createElement("div", {
|
|
97
123
|
className: "header__logo__img"
|
|
98
124
|
}, logo), !menuOpen && title && /*#__PURE__*/React__default.createElement(Typography, {
|
|
@@ -182,6 +208,7 @@ var Header = function Header(props) {
|
|
|
182
208
|
Header.defaultProps = {
|
|
183
209
|
className: undefined,
|
|
184
210
|
title: undefined,
|
|
211
|
+
siteName: undefined,
|
|
185
212
|
logo: undefined,
|
|
186
213
|
locale: undefined,
|
|
187
214
|
supportLanguages: defaultSupportLanguage,
|
|
@@ -202,6 +229,8 @@ Header.propTypes = {
|
|
|
202
229
|
className: PropTypes.string,
|
|
203
230
|
/** 標題(通常為網站名稱) */
|
|
204
231
|
title: PropTypes.string,
|
|
232
|
+
/** 站名,只用於 logo 連結的可及名稱(依語系;不顯示在畫面上)。未提供時用 title */
|
|
233
|
+
siteName: PropTypes.string,
|
|
205
234
|
/** 網站Logo */
|
|
206
235
|
logo: PropTypes.node,
|
|
207
236
|
/** 語系 */
|
|
@@ -2,7 +2,7 @@ import { a as _slicedToArray } from './_rollupPluginBabelHelpers-a52356f6.js';
|
|
|
2
2
|
import React__default, { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { c as classNames } from './index-c33afcb5.js';
|
|
5
|
-
import { M as Modal } from './Modal-
|
|
5
|
+
import { M as Modal } from './Modal-aa77d4d8.js';
|
|
6
6
|
import { Typography } from '@material-ui/core';
|
|
7
7
|
import { r as requireCreateSvgIcon, i as interopRequireDefault, a as interopRequireWildcard } from './createSvgIcon-370e163a.js';
|
|
8
8
|
import { getIntl } from './globalConfig';
|
|
@@ -13,6 +13,28 @@ styleInject(css_248z);
|
|
|
13
13
|
|
|
14
14
|
/* 每個 Modal 實例需要唯一 id,才能把標題和 role="dialog" 綁起來 */
|
|
15
15
|
var modalSeq = 0;
|
|
16
|
+
|
|
17
|
+
/* WCAG 2.4.3 對話框關閉後,焦點要回到開啟它的按鈕。MUI Dialog 會還給「開啟當下」的那個 DOM 元素,
|
|
18
|
+
但開啟期間若頁面資料才回來、版面重排,觸發按鈕會被換成新的元素(例:主站版本頁「我要補充資料」
|
|
19
|
+
依有無書影放在不同位置),舊元素已不在頁面上,焦點就掉回 body。
|
|
20
|
+
因此開啟時記下觸發元素的特徵,關閉後若舊元素已脫離頁面,找特徵相同且看得到的元素補還焦點。 */
|
|
21
|
+
var describeTrigger = function describeTrigger(el) {
|
|
22
|
+
if (!el || el === document.body || el === document.documentElement) return null;
|
|
23
|
+
return {
|
|
24
|
+
el: el,
|
|
25
|
+
tag: el.tagName,
|
|
26
|
+
label: el.getAttribute('aria-label'),
|
|
27
|
+
text: (el.textContent || '').trim()
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
var findTrigger = function findTrigger(trigger) {
|
|
31
|
+
if (!trigger) return null;
|
|
32
|
+
if (trigger.el.isConnected) return trigger.el;
|
|
33
|
+
var candidates = Array.from(document.getElementsByTagName(trigger.tag));
|
|
34
|
+
return candidates.find(function (el) {
|
|
35
|
+
return el.offsetParent !== null && el.getAttribute('aria-label') === trigger.label && (el.textContent || '').trim() === trigger.text;
|
|
36
|
+
}) || null;
|
|
37
|
+
};
|
|
16
38
|
var Modal = function Modal(props) {
|
|
17
39
|
var className = props.className,
|
|
18
40
|
title = props.title,
|
|
@@ -40,6 +62,25 @@ var Modal = function Modal(props) {
|
|
|
40
62
|
titleIdRef.current = "nmtl-modal-".concat(modalSeq, "-title");
|
|
41
63
|
}
|
|
42
64
|
var titleId = titleIdRef.current;
|
|
65
|
+
|
|
66
|
+
/* 在 render 階段(MUI 把焦點移進對話框之前)記下觸發元素 */
|
|
67
|
+
var triggerRef = useRef(null);
|
|
68
|
+
var wasVisibleRef = useRef(false);
|
|
69
|
+
if (visible && !wasVisibleRef.current && typeof document !== 'undefined') {
|
|
70
|
+
triggerRef.current = describeTrigger(document.activeElement);
|
|
71
|
+
}
|
|
72
|
+
wasVisibleRef.current = Boolean(visible);
|
|
73
|
+
|
|
74
|
+
/* onExited 觸發時對話框還沒卸載、焦點仍在對話框內;等下一輪卸載後再判斷焦點是否掉回 body */
|
|
75
|
+
var restoreFocus = function restoreFocus() {
|
|
76
|
+
setTimeout(function () {
|
|
77
|
+
var active = document.activeElement;
|
|
78
|
+
/* MUI 已成功還原(焦點不在 body)就不動 */
|
|
79
|
+
if (active && active !== document.body) return;
|
|
80
|
+
var target = findTrigger(triggerRef.current);
|
|
81
|
+
if (target) target.focus();
|
|
82
|
+
}, 0);
|
|
83
|
+
};
|
|
43
84
|
return /*#__PURE__*/React__default.createElement(Dialog, {
|
|
44
85
|
className: classNames('modal', {
|
|
45
86
|
'modal--transparent': transparent
|
|
@@ -49,7 +90,10 @@ var Modal = function Modal(props) {
|
|
|
49
90
|
onClose: function onClose() {
|
|
50
91
|
return closable && onCancel();
|
|
51
92
|
},
|
|
52
|
-
"aria-labelledby": title ? titleId : undefined
|
|
93
|
+
"aria-labelledby": title ? titleId : undefined,
|
|
94
|
+
TransitionProps: {
|
|
95
|
+
onExited: restoreFocus
|
|
96
|
+
}
|
|
53
97
|
}, /*#__PURE__*/React__default.createElement(DialogTitle, {
|
|
54
98
|
className: "modal__title"
|
|
55
99
|
}, /*#__PURE__*/React__default.createElement("span", {
|
package/lib/components/Modal.js
CHANGED
|
@@ -64,6 +64,8 @@ var zhtw = {
|
|
|
64
64
|
"aria.header.menuClose": "關閉主選單",
|
|
65
65
|
"header.language": "語言",
|
|
66
66
|
"aria.breadcrumbs": "麵包屑",
|
|
67
|
+
"aria.header.logoCurrentHome": "目前在首頁",
|
|
68
|
+
"aria.header.logoCurrentHomeWithTitle": "{title},目前在首頁",
|
|
67
69
|
"aria.header.logoHome": "回首頁",
|
|
68
70
|
"aria.header.logoHomeWithTitle": "{title},回首頁",
|
|
69
71
|
"aria.header.navigation": "主選單",
|
|
@@ -71,8 +73,13 @@ var zhtw = {
|
|
|
71
73
|
"aria.autocomplete.search": "執行搜尋",
|
|
72
74
|
"aria.loading": "載入中",
|
|
73
75
|
"aria.modal.close": "關閉視窗",
|
|
74
|
-
"chronology.
|
|
75
|
-
"chronology.
|
|
76
|
+
"chronology.slideAnnounce": "第 {index} 張,共 {total} 張:{date} {title}",
|
|
77
|
+
"chronology.eventsLabel": "大事年表事件(方向鍵切換)",
|
|
78
|
+
"chronology.zoomIn": "放大時間刻度",
|
|
79
|
+
"chronology.zoomOut": "縮小時間刻度",
|
|
80
|
+
"chronology.backToStart": "回到第一張",
|
|
81
|
+
"chronology.driveImage": "{title}相關圖片",
|
|
82
|
+
"chronology.openDriveFile": "在 Google 雲端硬碟檢視原始檔案",
|
|
76
83
|
"aria.newWindow": "(另開新視窗)",
|
|
77
84
|
"aria.vcode.image": "驗證碼圖片,如無法辨識可刷新驗證碼",
|
|
78
85
|
"aria.vcode.refresh": "刷新驗證碼",
|
|
@@ -196,6 +203,8 @@ var zhcn = {
|
|
|
196
203
|
"aria.header.menuClose": "关闭主选单",
|
|
197
204
|
"header.language": "语言",
|
|
198
205
|
"aria.breadcrumbs": "面包屑",
|
|
206
|
+
"aria.header.logoCurrentHome": "目前在首页",
|
|
207
|
+
"aria.header.logoCurrentHomeWithTitle": "{title},目前在首页",
|
|
199
208
|
"aria.header.logoHome": "回首页",
|
|
200
209
|
"aria.header.logoHomeWithTitle": "{title},回首页",
|
|
201
210
|
"aria.header.navigation": "主选单",
|
|
@@ -203,8 +212,13 @@ var zhcn = {
|
|
|
203
212
|
"aria.autocomplete.search": "执行搜寻",
|
|
204
213
|
"aria.loading": "载入中",
|
|
205
214
|
"aria.modal.close": "关闭视窗",
|
|
206
|
-
"chronology.
|
|
207
|
-
"chronology.
|
|
215
|
+
"chronology.slideAnnounce": "第 {index} 张,共 {total} 张:{date} {title}",
|
|
216
|
+
"chronology.eventsLabel": "大事年表事件(方向键切换)",
|
|
217
|
+
"chronology.zoomIn": "放大时间刻度",
|
|
218
|
+
"chronology.zoomOut": "缩小时间刻度",
|
|
219
|
+
"chronology.backToStart": "回到第一张",
|
|
220
|
+
"chronology.driveImage": "{title}相关图片",
|
|
221
|
+
"chronology.openDriveFile": "在 Google 云端硬盘查看原始文件",
|
|
208
222
|
"aria.newWindow": "(另开新视窗)",
|
|
209
223
|
"aria.vcode.image": "验证码图片,如无法辨识可刷新验证码",
|
|
210
224
|
"aria.vcode.refresh": "刷新验证码",
|
|
@@ -330,6 +344,8 @@ var enus = {
|
|
|
330
344
|
"aria.header.menuClose": "Close main menu",
|
|
331
345
|
"header.language": "Language",
|
|
332
346
|
"aria.breadcrumbs": "Breadcrumb",
|
|
347
|
+
"aria.header.logoCurrentHome": "You are on the home page",
|
|
348
|
+
"aria.header.logoCurrentHomeWithTitle": "{title}, you are on the home page",
|
|
333
349
|
"aria.header.logoHome": "Back to home",
|
|
334
350
|
"aria.header.logoHomeWithTitle": "{title}, back to home",
|
|
335
351
|
"aria.header.navigation": "Main menu",
|
|
@@ -337,8 +353,13 @@ var enus = {
|
|
|
337
353
|
"aria.autocomplete.search": "Search",
|
|
338
354
|
"aria.loading": "Loading",
|
|
339
355
|
"aria.modal.close": "Close dialog",
|
|
340
|
-
"chronology.
|
|
341
|
-
"chronology.
|
|
356
|
+
"chronology.slideAnnounce": "Slide {index} of {total}: {date} {title}",
|
|
357
|
+
"chronology.eventsLabel": "Timeline events (use arrow keys)",
|
|
358
|
+
"chronology.zoomIn": "Zoom in the timeline",
|
|
359
|
+
"chronology.zoomOut": "Zoom out the timeline",
|
|
360
|
+
"chronology.backToStart": "Back to the first slide",
|
|
361
|
+
"chronology.driveImage": "Image related to {title}",
|
|
362
|
+
"chronology.openDriveFile": "View the original file on Google Drive",
|
|
342
363
|
"aria.newWindow": " (opens in new window)",
|
|
343
364
|
"aria.vcode.image": "CAPTCHA image, refresh if unreadable",
|
|
344
365
|
"aria.vcode.refresh": "Refresh CAPTCHA",
|
|
@@ -33,22 +33,24 @@ var prepareMedia = function prepareMedia(media, headline) {
|
|
|
33
33
|
id = _match[1];
|
|
34
34
|
var _getIntl = getIntl(),
|
|
35
35
|
formatMessage = _getIntl.formatMessage;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/* 雲端硬碟的檔案多半是圖片、少數是影片,無法從網址判斷 → 用詞不限定影片(2026-10-08 人工測試回饋)。
|
|
37
|
+
縮圖就是檔案內容本身:alt 優先用圖說(通常是圖片的說明與來源),沒有圖說才用「{標題}相關圖片」 */
|
|
38
|
+
var alt = stripTags(media.caption) || formatMessage({
|
|
39
|
+
id: 'chronology.driveImage'
|
|
38
40
|
}, {
|
|
39
41
|
title: stripTags(headline)
|
|
40
42
|
});
|
|
41
|
-
var
|
|
42
|
-
id: 'chronology.
|
|
43
|
+
var open = "<a href=\"".concat(driveView(id), "\" target=\"_blank\" rel=\"noopener noreferrer\">").concat(formatMessage({
|
|
44
|
+
id: 'chronology.openDriveFile'
|
|
43
45
|
}), "<span class=\"").concat(TIMELINE_HINT_CLASS, "\">").concat(newWindowHintText(), "</span></a>");
|
|
44
46
|
return _objectSpread2(_objectSpread2({}, media), {}, {
|
|
45
47
|
url: driveThumbnail(id),
|
|
46
48
|
thumbnail: driveThumbnail(id),
|
|
47
49
|
alt: alt,
|
|
48
|
-
/* 套件沒有 title 時拿圖說當圖片 title
|
|
50
|
+
/* 套件沒有 title 時拿圖說當圖片 title,會帶進連結文字,但圖片本身不是連結 */
|
|
49
51
|
title: alt,
|
|
50
52
|
/* 連結放在圖說,不包在圖片上:同一個目的地只留一個連結,文字也看得到 */
|
|
51
|
-
caption: [
|
|
53
|
+
caption: [open, hint(media.caption)].filter(Boolean).join('<br />'),
|
|
52
54
|
link: undefined
|
|
53
55
|
});
|
|
54
56
|
};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '../components/globalConfig';
|
|
1
|
+
import { b as _slicedToArray } from './_rollupPluginBabelHelpers-9802e640.js';
|
|
2
|
+
import { getIntl } from '../components/globalConfig';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
|
|
4
|
+
/* 視覺隱藏的播報區(不依賴各站的 .visually-hidden) */
|
|
5
|
+
var VISUALLY_HIDDEN = 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';
|
|
6
|
+
var MENU_BUTTON_LABELS = [['.tl-icon-zoom-in', 'chronology.zoomIn'], ['.tl-icon-zoom-out', 'chronology.zoomOut'], ['.tl-icon-goback', 'chronology.backToStart']];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 建立後的鍵盤與報讀整理(數位部人工檢測退回 2026-10-07、人工測試回饋 2026-10-08;主站 nmtl-web timelineA11y.js 同步),回傳解除的函式:
|
|
6
10
|
* 1. 只有目前這張投影片可以被 Tab 走到:其餘投影片加 inert
|
|
7
|
-
*
|
|
8
|
-
*
|
|
11
|
+
* 2. 箭頭的 aria-label 拿掉標題連結上的「(另開新視窗)」(箭頭本身不會另開)
|
|
12
|
+
* 3. 焦點順序與畫面一致:上一張 → 投影片內容 → 下一張(套件 DOM 是 內容 → 下一張 → 上一張)
|
|
13
|
+
* 4. 換頁時以 aria-live 播報「第 x 張,共 y 張:年份 標題」(原本焦點留在箭頭上,報讀軟體聽不到換到哪一張)
|
|
14
|
+
* 5. 下方時間刻度的事件標記:原本只能滑鼠點選。改為 tablist/tab(只有目前事件可 Tab 到),
|
|
15
|
+
* 方向鍵/Home/End/PageUp/PageDown 換到對應事件
|
|
16
|
+
* 6. 放大、縮小、回到第一張:原本是沒有名稱的 span,補 role="button"、tabindex、名稱,Enter/空白鍵可操作
|
|
17
|
+
* 7. 套件在 document 上監聽左右鍵換頁(每建一次就多綁一個、從不移除),焦點在頁籤、搜尋框等時間軸外的元件時
|
|
18
|
+
* 按左右鍵也會換投影片 → 只有焦點在這個時間軸的箭頭上才讓它生效
|
|
19
|
+
* @param {object} timeline new Timeline(...) 的實例
|
|
20
|
+
* @param {HTMLElement} root 時間軸容器
|
|
9
21
|
*
|
|
10
22
|
* 用法:
|
|
11
23
|
* const timeline = new Timeline('timeline-embed', prepareTimelineData(data), options);
|
|
@@ -13,40 +25,251 @@ import '../components/globalConfig';
|
|
|
13
25
|
*/
|
|
14
26
|
var syncTimelineA11y = function syncTimelineA11y(timeline, root) {
|
|
15
27
|
if (!timeline || !root) return function () {};
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
var t = function t(id, values) {
|
|
29
|
+
return getIntl().formatMessage({
|
|
30
|
+
id: id
|
|
31
|
+
}, values);
|
|
32
|
+
};
|
|
33
|
+
var hint = t('aria.newWindow');
|
|
34
|
+
var text = function text(el) {
|
|
35
|
+
return el ? el.textContent.split(hint).join('').replace(/\s+/g, ' ').trim() : '';
|
|
36
|
+
};
|
|
37
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
38
|
+
var currentIdOf = function currentIdOf() {
|
|
39
|
+
return timeline.current_id || timeline._storyslider && timeline._storyslider.current_id;
|
|
40
|
+
};
|
|
41
|
+
var slides = function slides() {
|
|
42
|
+
return Array.from(root.querySelectorAll('.tl-slide')).filter(function (s) {
|
|
43
|
+
return !s.classList.contains('tl-slide-titleslide');
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var syncInert = function syncInert(currentId) {
|
|
23
47
|
root.querySelectorAll('.tl-slide').forEach(function (slide) {
|
|
24
48
|
if (slide.id === currentId) slide.removeAttribute('inert');else slide.setAttribute('inert', '');
|
|
25
49
|
});
|
|
26
50
|
};
|
|
27
51
|
var syncNavLabels = function syncNavLabels() {
|
|
28
|
-
if (!hint) return;
|
|
29
52
|
root.querySelectorAll('.tl-slidenav-previous, .tl-slidenav-next').forEach(function (nav) {
|
|
30
53
|
var label = nav.getAttribute('aria-label');
|
|
31
54
|
if (label && label.indexOf(hint) !== -1) nav.setAttribute('aria-label', label.split(hint).join(''));
|
|
55
|
+
/* 箭頭的預覽區(aria-hidden)會複製相鄰投影片的標題,標題裡的站內連結(span role="link" tabindex="0")
|
|
56
|
+
跟著進來,變成「按鈕裡又一個可聚焦的連結」→ 移出 Tab 順序 */
|
|
57
|
+
var previewFocusables = nav.querySelectorAll('.tl-slidenav-content-container a, .tl-slidenav-content-container [tabindex]');
|
|
58
|
+
previewFocusables.forEach(function (el) {
|
|
59
|
+
return el.setAttribute('tabindex', '-1');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
/* 3. 「上一張」移到投影片內容之前;兩個箭頭都是絕對定位,畫面不變 */
|
|
64
|
+
var orderNav = function orderNav() {
|
|
65
|
+
var prev = root.querySelector('.tl-slidenav-previous');
|
|
66
|
+
var mask = root.querySelector('.tl-slider-container-mask');
|
|
67
|
+
if (prev && mask && mask.parentNode === prev.parentNode && prev.nextElementSibling !== mask) {
|
|
68
|
+
mask.parentNode.insertBefore(prev, mask);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/* 4. 換頁播報:播報區放在時間軸容器外面(套件重建內容時會清掉容器內不認得的節點) */
|
|
73
|
+
var live = root.nextElementSibling && root.nextElementSibling.classList.contains('tl-a11y-live') ? root.nextElementSibling : null;
|
|
74
|
+
if (!live) {
|
|
75
|
+
live = document.createElement('div');
|
|
76
|
+
live.className = 'tl-a11y-live';
|
|
77
|
+
live.setAttribute('role', 'status');
|
|
78
|
+
live.setAttribute('aria-live', 'polite');
|
|
79
|
+
live.setAttribute('style', VISUALLY_HIDDEN);
|
|
80
|
+
root.parentNode.insertBefore(live, root.nextSibling);
|
|
81
|
+
}
|
|
82
|
+
/* 只在使用者剛操作過時間軸(按鍵、點擊)之後播報:套件初次載入也會觸發 change,不能一進頁面就念 */
|
|
83
|
+
var lastUserAction = 0;
|
|
84
|
+
var markUserAction = function markUserAction() {
|
|
85
|
+
lastUserAction = Date.now();
|
|
86
|
+
};
|
|
87
|
+
var announce = function announce(currentId) {
|
|
88
|
+
if (Date.now() - lastUserAction > 2000) return;
|
|
89
|
+
var list = slides();
|
|
90
|
+
var index = list.findIndex(function (s) {
|
|
91
|
+
return s.id === currentId;
|
|
92
|
+
});
|
|
93
|
+
if (index === -1) return;
|
|
94
|
+
var slide = list[index];
|
|
95
|
+
live.textContent = t('chronology.slideAnnounce', {
|
|
96
|
+
index: index + 1,
|
|
97
|
+
total: list.length,
|
|
98
|
+
date: text(slide.querySelector('.tl-headline-date')),
|
|
99
|
+
title: text(slide.querySelector('.tl-headline:not(.tl-headline-date)'))
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/* 5. 時間刻度標記:tablist/tab */
|
|
104
|
+
var markerContainer = function markerContainer() {
|
|
105
|
+
return root.querySelector('.tl-timenav-item-container');
|
|
106
|
+
};
|
|
107
|
+
var markers = function markers() {
|
|
108
|
+
return Array.from(root.querySelectorAll('.tl-timemarker'));
|
|
109
|
+
};
|
|
110
|
+
var slideIdOf = function slideIdOf(marker) {
|
|
111
|
+
return marker.id.replace(/-marker$/, '');
|
|
112
|
+
};
|
|
113
|
+
var syncMarkers = function syncMarkers(currentId) {
|
|
114
|
+
var container = markerContainer();
|
|
115
|
+
if (!container) return;
|
|
116
|
+
container.setAttribute('role', 'tablist');
|
|
117
|
+
container.setAttribute('aria-label', t('chronology.eventsLabel'));
|
|
118
|
+
markers().forEach(function (marker) {
|
|
119
|
+
var slide = document.getElementById(slideIdOf(marker));
|
|
120
|
+
var selected = slideIdOf(marker) === currentId;
|
|
121
|
+
marker.setAttribute('role', 'tab');
|
|
122
|
+
marker.setAttribute('aria-selected', String(selected));
|
|
123
|
+
marker.setAttribute('aria-controls', slideIdOf(marker));
|
|
124
|
+
marker.setAttribute('tabindex', selected ? '0' : '-1');
|
|
125
|
+
var label = [text(slide && slide.querySelector('.tl-headline-date')), text(marker.querySelector('.tl-headline'))].filter(Boolean).join(' ');
|
|
126
|
+
if (label) marker.setAttribute('aria-label', label);
|
|
127
|
+
/* 標記內的標題常是連結:標記已是 tab,裡面的連結不再各自佔一個 Tab 停點 */
|
|
128
|
+
marker.querySelectorAll('a, button, [tabindex]').forEach(function (el) {
|
|
129
|
+
return el.setAttribute('tabindex', '-1');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
var focusMarkerAfterChange = false;
|
|
134
|
+
var onMarkerKeyDown = function onMarkerKeyDown(e) {
|
|
135
|
+
var marker = e.target.closest && e.target.closest('.tl-timemarker');
|
|
136
|
+
if (!marker) return;
|
|
137
|
+
var list = markers();
|
|
138
|
+
var index = list.indexOf(marker);
|
|
139
|
+
var last = list.length - 1;
|
|
140
|
+
var target = {
|
|
141
|
+
ArrowRight: Math.min(index + 1, last),
|
|
142
|
+
ArrowLeft: Math.max(index - 1, 0),
|
|
143
|
+
Home: 0,
|
|
144
|
+
End: last,
|
|
145
|
+
PageDown: Math.min(index + 10, last),
|
|
146
|
+
PageUp: Math.max(index - 10, 0)
|
|
147
|
+
}[e.key];
|
|
148
|
+
if (target === undefined) return;
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
if (target === index) return;
|
|
151
|
+
focusMarkerAfterChange = true;
|
|
152
|
+
timeline.goToId(slideIdOf(list[target]));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/* 6. 放大/縮小/回到第一張 */
|
|
156
|
+
var syncMenuButtons = function syncMenuButtons() {
|
|
157
|
+
MENU_BUTTON_LABELS.forEach(function (_ref) {
|
|
158
|
+
var _ref2 = _slicedToArray(_ref, 2),
|
|
159
|
+
iconSelector = _ref2[0],
|
|
160
|
+
id = _ref2[1];
|
|
161
|
+
var icon = root.querySelector(".tl-menubar-button ".concat(iconSelector));
|
|
162
|
+
var button = icon && icon.closest('.tl-menubar-button');
|
|
163
|
+
if (!button) return;
|
|
164
|
+
button.setAttribute('role', 'button');
|
|
165
|
+
button.tabIndex = 0;
|
|
166
|
+
button.setAttribute('aria-label', t(id));
|
|
167
|
+
button.setAttribute('aria-disabled', String(button.classList.contains('tl-menubar-button-inactive')));
|
|
168
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
32
169
|
});
|
|
33
170
|
};
|
|
171
|
+
var onMenuKeyDown = function onMenuKeyDown(e) {
|
|
172
|
+
var button = e.target.closest && e.target.closest('.tl-menubar-button');
|
|
173
|
+
if (!button || e.key !== 'Enter' && e.key !== ' ') return;
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
button.click();
|
|
176
|
+
setTimeout(syncMenuButtons, 0);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/* 7. 套件的全域左右鍵:焦點不在本時間軸的箭頭上就不換頁(時間刻度標記由 5. 自己處理) */
|
|
180
|
+
var blockKeyNav = false;
|
|
181
|
+
/* 套件每建一次時間軸就多綁一個 document 監聽器、從不移除;元件卸載或重建後,舊實例仍會被左右鍵觸發 →
|
|
182
|
+
解除時不還原 goToNext/goToPrev,而是讓舊實例永久停用 */
|
|
183
|
+
var disposed = false;
|
|
184
|
+
var onKeyCapture = function onKeyCapture(e) {
|
|
185
|
+
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
186
|
+
var onArrow = root.contains(e.target) && e.target.closest('.tl-slidenav-previous, .tl-slidenav-next');
|
|
187
|
+
blockKeyNav = !onArrow;
|
|
188
|
+
};
|
|
189
|
+
var onKeyBubble = function onKeyBubble() {
|
|
190
|
+
blockKeyNav = false;
|
|
191
|
+
};
|
|
192
|
+
var originalNext = timeline.goToNext;
|
|
193
|
+
var originalPrev = timeline.goToPrev;
|
|
194
|
+
/* 套件的監聽器是匿名函式、拿不到也移除不了,只能在它呼叫的 goToNext/goToPrev 上把關 */
|
|
195
|
+
/* eslint-disable no-param-reassign */
|
|
196
|
+
timeline.goToNext = function () {
|
|
197
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
198
|
+
args[_key] = arguments[_key];
|
|
199
|
+
}
|
|
200
|
+
return blockKeyNav || disposed ? undefined : originalNext.apply(timeline, args);
|
|
201
|
+
};
|
|
202
|
+
timeline.goToPrev = function () {
|
|
203
|
+
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
204
|
+
args[_key2] = arguments[_key2];
|
|
205
|
+
}
|
|
206
|
+
return blockKeyNav || disposed ? undefined : originalPrev.apply(timeline, args);
|
|
207
|
+
};
|
|
208
|
+
/* eslint-enable no-param-reassign */
|
|
209
|
+
window.addEventListener('keydown', onKeyCapture, true);
|
|
210
|
+
window.addEventListener('keydown', onKeyBubble);
|
|
211
|
+
root.addEventListener('keydown', onMarkerKeyDown);
|
|
212
|
+
root.addEventListener('keydown', onMenuKeyDown);
|
|
34
213
|
var timer;
|
|
35
|
-
|
|
36
|
-
|
|
214
|
+
/* changed:只有真正換頁(change 事件)才播報,初次載入與重建不播 */
|
|
215
|
+
var apply = function apply(changed) {
|
|
216
|
+
orderNav();
|
|
217
|
+
syncMenuButtons();
|
|
218
|
+
var currentId = currentIdOf();
|
|
219
|
+
/* 剛載入時 current_id 可能還是空的:先不動 inert,避免把畫面上的這張也設成 inert */
|
|
220
|
+
if (currentId) {
|
|
221
|
+
syncInert(currentId);
|
|
222
|
+
syncMarkers(currentId);
|
|
223
|
+
if (changed === true) announce(currentId);
|
|
224
|
+
if (focusMarkerAfterChange) {
|
|
225
|
+
focusMarkerAfterChange = false;
|
|
226
|
+
var marker = document.getElementById("".concat(currentId, "-marker"));
|
|
227
|
+
if (marker) marker.focus({
|
|
228
|
+
preventScroll: true
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
37
232
|
syncNavLabels();
|
|
38
233
|
/* 套件在同一輪事件裡才更新箭頭標題,下一輪再整理一次 */
|
|
39
234
|
clearTimeout(timer);
|
|
40
235
|
timer = setTimeout(syncNavLabels, 0);
|
|
41
236
|
};
|
|
42
237
|
/* 投影片是套件非同步建出來的:建好(loaded)與每次換頁(change)都重新套用 */
|
|
238
|
+
var onChange = function onChange() {
|
|
239
|
+
return apply(true);
|
|
240
|
+
};
|
|
43
241
|
timeline.on('loaded', apply);
|
|
44
|
-
timeline.on('change',
|
|
242
|
+
timeline.on('change', onChange);
|
|
243
|
+
root.addEventListener('keydown', markUserAction, true);
|
|
244
|
+
root.addEventListener('click', markUserAction, true);
|
|
245
|
+
/* 套件是非同步分批建出投影片、時間刻度、工具列的(loaded 事件時工具列還沒建好):
|
|
246
|
+
結構有變動就再整理一次(不播報)。自己做的 setAttribute 不會觸發 childList */
|
|
247
|
+
var frame;
|
|
248
|
+
var observer = new MutationObserver(function () {
|
|
249
|
+
cancelAnimationFrame(frame);
|
|
250
|
+
frame = requestAnimationFrame(function () {
|
|
251
|
+
return apply(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
observer.observe(root, {
|
|
255
|
+
childList: true,
|
|
256
|
+
subtree: true
|
|
257
|
+
});
|
|
45
258
|
apply();
|
|
46
259
|
return function () {
|
|
47
260
|
clearTimeout(timer);
|
|
48
261
|
timeline.off('loaded', apply);
|
|
49
|
-
timeline.off('change',
|
|
262
|
+
timeline.off('change', onChange);
|
|
263
|
+
observer.disconnect();
|
|
264
|
+
cancelAnimationFrame(frame);
|
|
265
|
+
root.removeEventListener('keydown', markUserAction, true);
|
|
266
|
+
root.removeEventListener('click', markUserAction, true);
|
|
267
|
+
disposed = true;
|
|
268
|
+
window.removeEventListener('keydown', onKeyCapture, true);
|
|
269
|
+
window.removeEventListener('keydown', onKeyBubble);
|
|
270
|
+
root.removeEventListener('keydown', onMarkerKeyDown);
|
|
271
|
+
root.removeEventListener('keydown', onMenuKeyDown);
|
|
272
|
+
if (live.parentNode) live.parentNode.removeChild(live);
|
|
50
273
|
};
|
|
51
274
|
};
|
|
52
275
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daoyi/nmtl-ui",
|
|
3
3
|
"packageManager": "pnpm@10.34.5",
|
|
4
|
-
"version": "2.0.1-beta.
|
|
4
|
+
"version": "2.0.1-beta.138",
|
|
5
5
|
"description": "This project is to be used as a starter kit for building React component using Storybook and Rollup",
|
|
6
6
|
"author": "daoyi",
|
|
7
7
|
"main": "lib/index.js",
|