@nkootstra/block-kit 0.3.4 → 0.4.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/dist/{index-D139DZfD.d.ts → index--GXgMjw3.d.ts} +36 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +840 -191
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/web-api.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,7 +31,8 @@ const DEFAULT_IDENTITY = {
|
|
|
31
31
|
apiAppId: "A00000000",
|
|
32
32
|
token: "verification-token",
|
|
33
33
|
triggerId: "000000000000.000000000000.deadbeefdeadbeefdeadbeefdeadbeef",
|
|
34
|
-
responseUrl: "https://hooks.slack.com/actions/T00000000/0000000000000/deadbeefdeadbeefdeadbeefdeadbeef"
|
|
34
|
+
responseUrl: "https://hooks.slack.com/actions/T00000000/0000000000000/deadbeefdeadbeefdeadbeefdeadbeef",
|
|
35
|
+
isEnterpriseInstall: false
|
|
35
36
|
};
|
|
36
37
|
function resolveIdentity(identity) {
|
|
37
38
|
return {
|
|
@@ -49,12 +50,12 @@ function toViewOutput(view, state, identity) {
|
|
|
49
50
|
bot_id: "B00000000",
|
|
50
51
|
callback_id: view.callback_id ?? "",
|
|
51
52
|
type: view.type,
|
|
52
|
-
title: view.title ?? null,
|
|
53
|
-
close: view.close ?? null,
|
|
54
|
-
submit: view.submit ?? null,
|
|
55
|
-
blocks: view.blocks,
|
|
53
|
+
title: normalizeText(view.title ?? null),
|
|
54
|
+
close: normalizeText(view.close ?? null),
|
|
55
|
+
submit: normalizeText(view.submit ?? null),
|
|
56
|
+
blocks: normalizeBlocks(view.blocks),
|
|
56
57
|
private_metadata: view.private_metadata ?? "",
|
|
57
|
-
state: { values: state },
|
|
58
|
+
state: { values: normalizeState(state) },
|
|
58
59
|
hash: view.hash ?? "0.0000000000",
|
|
59
60
|
clear_on_close: view.clear_on_close ?? false,
|
|
60
61
|
notify_on_close: view.notify_on_close ?? false,
|
|
@@ -63,12 +64,91 @@ function toViewOutput(view, state, identity) {
|
|
|
63
64
|
external_id: view.external_id ?? ""
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* A text object as Slack echoes it back: `plain_text` gains `emoji: true` and `mrkdwn` gains
|
|
69
|
+
* `verbatim: false` unless the app set them.
|
|
70
|
+
*/
|
|
71
|
+
function normalizeText(text) {
|
|
72
|
+
if (typeof text !== "object" || text === null) return text;
|
|
73
|
+
const object = text;
|
|
74
|
+
if (object.type === "plain_text") return {
|
|
75
|
+
...text,
|
|
76
|
+
emoji: object.emoji ?? true
|
|
77
|
+
};
|
|
78
|
+
if (object.type === "mrkdwn") return {
|
|
79
|
+
...text,
|
|
80
|
+
verbatim: object.verbatim ?? false
|
|
81
|
+
};
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
function normalizeOption(option) {
|
|
85
|
+
if (typeof option !== "object" || option === null) return option;
|
|
86
|
+
const { text, description } = option;
|
|
87
|
+
return {
|
|
88
|
+
...option,
|
|
89
|
+
...text === void 0 ? {} : { text: normalizeText(text) },
|
|
90
|
+
...description === void 0 ? {} : { description: normalizeText(description) }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** An action or state entry with every text object it echoes normalized like Slack's. */
|
|
94
|
+
function normalizeEchoes(entry) {
|
|
95
|
+
const out = { ...entry };
|
|
96
|
+
if (out.text !== void 0) out.text = normalizeText(out.text);
|
|
97
|
+
if (out.placeholder !== void 0) out.placeholder = normalizeText(out.placeholder);
|
|
98
|
+
if (out.selected_option) out.selected_option = normalizeOption(out.selected_option);
|
|
99
|
+
if (Array.isArray(out.selected_options)) out.selected_options = out.selected_options.map(normalizeOption);
|
|
100
|
+
if (typeof out.confirm === "object" && out.confirm !== null) {
|
|
101
|
+
const confirm = { ...out.confirm };
|
|
102
|
+
for (const key of [
|
|
103
|
+
"title",
|
|
104
|
+
"text",
|
|
105
|
+
"confirm",
|
|
106
|
+
"deny"
|
|
107
|
+
]) if (confirm[key] !== void 0) confirm[key] = normalizeText(confirm[key]);
|
|
108
|
+
out.confirm = confirm;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function normalizeState(state) {
|
|
113
|
+
return Object.fromEntries(Object.entries(state).map(([blockId, elements]) => [blockId, Object.fromEntries(Object.entries(elements).map(([actionId, value]) => [actionId, normalizeEchoes(value)]))]));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Blocks as Slack echoes a stored view: every text object normalized like {@link normalizeText},
|
|
117
|
+
* and an input block's `optional` and `dispatch_action` filled in as `false`.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeBlocks(blocks) {
|
|
120
|
+
return normalizeBlockNode(blocks);
|
|
121
|
+
}
|
|
122
|
+
function normalizeBlockNode(node) {
|
|
123
|
+
if (Array.isArray(node)) return node.map(normalizeBlockNode);
|
|
124
|
+
if (typeof node !== "object" || node === null) return node;
|
|
125
|
+
const object = node;
|
|
126
|
+
if ((object.type === "plain_text" || object.type === "mrkdwn") && typeof object.text === "string") return normalizeText(object);
|
|
127
|
+
const out = Object.fromEntries(Object.entries(object).map(([key, value]) => [key, normalizeBlockNode(value)]));
|
|
128
|
+
if (object.type === "input") {
|
|
129
|
+
out.optional ??= false;
|
|
130
|
+
out.dispatch_action ??= false;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** `enterprise` and `is_enterprise_install`, as every interaction payload carries them. */
|
|
135
|
+
function enterpriseFields(id) {
|
|
136
|
+
return {
|
|
137
|
+
enterprise: id.team?.enterprise_id ? {
|
|
138
|
+
id: id.team.enterprise_id,
|
|
139
|
+
name: id.team.enterprise_name ?? ""
|
|
140
|
+
} : null,
|
|
141
|
+
is_enterprise_install: id.isEnterpriseInstall
|
|
142
|
+
};
|
|
143
|
+
}
|
|
66
144
|
function buildBlockActionsPayload({ action, state, container, identity }) {
|
|
67
145
|
const id = resolveIdentity(identity);
|
|
146
|
+
state = normalizeState(state);
|
|
68
147
|
const base = {
|
|
69
148
|
type: "block_actions",
|
|
70
|
-
actions: [action],
|
|
149
|
+
actions: [normalizeEchoes(action)],
|
|
71
150
|
team: id.team,
|
|
151
|
+
...enterpriseFields(id),
|
|
72
152
|
user: id.user,
|
|
73
153
|
token: id.token,
|
|
74
154
|
response_url: id.responseUrl,
|
|
@@ -108,6 +188,7 @@ function buildViewSubmissionPayload({ view, state, identity, responseUrls }) {
|
|
|
108
188
|
return {
|
|
109
189
|
type: "view_submission",
|
|
110
190
|
team: id.team,
|
|
191
|
+
...enterpriseFields(id),
|
|
111
192
|
user: {
|
|
112
193
|
id: id.user.id,
|
|
113
194
|
name: id.user.name ?? id.user.username ?? "",
|
|
@@ -126,6 +207,7 @@ function buildViewClosedPayload({ view, state, identity, isCleared }) {
|
|
|
126
207
|
return {
|
|
127
208
|
type: "view_closed",
|
|
128
209
|
team: id.team,
|
|
210
|
+
...enterpriseFields(id),
|
|
129
211
|
user: {
|
|
130
212
|
id: id.user.id,
|
|
131
213
|
name: id.user.name ?? id.user.username ?? "",
|
|
@@ -148,7 +230,8 @@ function buildBlockSuggestionPayload({ actionId, blockId, value, container, stat
|
|
|
148
230
|
action_id: actionId,
|
|
149
231
|
block_id: blockId,
|
|
150
232
|
value,
|
|
151
|
-
team: id.team
|
|
233
|
+
team: id.team,
|
|
234
|
+
...enterpriseFields(id)
|
|
152
235
|
};
|
|
153
236
|
if (container.type === "message") return {
|
|
154
237
|
...base,
|
|
@@ -985,10 +1068,10 @@ function BlockKitProvider(props) {
|
|
|
985
1068
|
containerRef.current = container;
|
|
986
1069
|
}, []);
|
|
987
1070
|
const dispatch = useCallback((action, scope) => {
|
|
988
|
-
const full = {
|
|
1071
|
+
const full = normalizeEchoes({
|
|
989
1072
|
...action,
|
|
990
1073
|
action_ts: action.action_ts ?? actionTs()
|
|
991
|
-
};
|
|
1074
|
+
});
|
|
992
1075
|
const message = scope?.message;
|
|
993
1076
|
onAction?.(full, {
|
|
994
1077
|
state: stateRef.current,
|
|
@@ -5927,33 +6010,89 @@ function Text({ text, emojiSize = 22 }) {
|
|
|
5927
6010
|
}
|
|
5928
6011
|
//#endregion
|
|
5929
6012
|
//#region src/confirm/ConfirmDialog.tsx
|
|
5930
|
-
/**
|
|
6013
|
+
/**
|
|
6014
|
+
* Slack's confirmation dialog (`c-dialog`): blocks the triggering action until the user confirms or
|
|
6015
|
+
* denies. Escape, the close button and a click on the overlay all deny.
|
|
6016
|
+
*
|
|
6017
|
+
* Slack marks it `role="dialog"`; this keeps `alertdialog`, the role WAI-ARIA gives a dialog that
|
|
6018
|
+
* interrupts to ask for a confirmation, so screen readers announce its text on open. Focus starts
|
|
6019
|
+
* on the deny button, the least destructive choice, stays inside while it's open, and goes back to
|
|
6020
|
+
* whatever opened it when it closes.
|
|
6021
|
+
*/
|
|
5931
6022
|
function ConfirmDialog({ confirm, onConfirm, onDeny }) {
|
|
5932
6023
|
const { theme } = useBlockKit();
|
|
5933
6024
|
const confirmStyle = confirm.style ?? "primary";
|
|
6025
|
+
const titleId = useId();
|
|
6026
|
+
const textId = useId();
|
|
6027
|
+
const dialogRef = useRef(null);
|
|
6028
|
+
const denyRef = useRef(null);
|
|
6029
|
+
useEffect(() => {
|
|
6030
|
+
const opener = document.activeElement;
|
|
6031
|
+
denyRef.current?.focus();
|
|
6032
|
+
return () => {
|
|
6033
|
+
if (opener?.isConnected) opener.focus();
|
|
6034
|
+
};
|
|
6035
|
+
}, []);
|
|
6036
|
+
function onKeyDown(event) {
|
|
6037
|
+
if (event.key === "Escape") {
|
|
6038
|
+
event.stopPropagation();
|
|
6039
|
+
onDeny();
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
if (event.key !== "Tab") return;
|
|
6043
|
+
const buttons = [...dialogRef.current?.querySelectorAll("button") ?? []];
|
|
6044
|
+
const first = buttons[0];
|
|
6045
|
+
const last = buttons.at(-1);
|
|
6046
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
6047
|
+
event.preventDefault();
|
|
6048
|
+
last?.focus();
|
|
6049
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
6050
|
+
event.preventDefault();
|
|
6051
|
+
first?.focus();
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
5934
6054
|
const node = /* @__PURE__ */ jsx("div", {
|
|
5935
6055
|
className: "sbk-confirm__overlay",
|
|
5936
6056
|
role: "presentation",
|
|
5937
6057
|
"data-theme": theme,
|
|
5938
6058
|
onClick: onDeny,
|
|
5939
6059
|
children: /* @__PURE__ */ jsxs("div", {
|
|
6060
|
+
ref: dialogRef,
|
|
5940
6061
|
className: "sbk-confirm",
|
|
5941
6062
|
role: "alertdialog",
|
|
5942
6063
|
"aria-modal": "true",
|
|
5943
|
-
"aria-
|
|
6064
|
+
"aria-labelledby": confirm.title ? titleId : void 0,
|
|
6065
|
+
"aria-label": confirm.title ? void 0 : confirm.confirm?.text ?? "Confirm",
|
|
6066
|
+
"aria-describedby": textId,
|
|
6067
|
+
onKeyDown,
|
|
5944
6068
|
onClick: (event) => event.stopPropagation(),
|
|
5945
6069
|
children: [
|
|
5946
|
-
|
|
5947
|
-
className: "sbk-
|
|
5948
|
-
children: confirm.title
|
|
6070
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6071
|
+
className: "sbk-confirm__header",
|
|
6072
|
+
children: [confirm.title && /* @__PURE__ */ jsx("h2", {
|
|
6073
|
+
id: titleId,
|
|
6074
|
+
className: "sbk-confirm__title",
|
|
6075
|
+
children: confirm.title.text
|
|
6076
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
6077
|
+
type: "button",
|
|
6078
|
+
className: "sbk-confirm__close",
|
|
6079
|
+
"aria-label": "Close",
|
|
6080
|
+
onClick: onDeny,
|
|
6081
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {
|
|
6082
|
+
width: "20",
|
|
6083
|
+
height: "20"
|
|
6084
|
+
})
|
|
6085
|
+
})]
|
|
5949
6086
|
}),
|
|
5950
6087
|
/* @__PURE__ */ jsx("div", {
|
|
6088
|
+
id: textId,
|
|
5951
6089
|
className: "sbk-confirm__text",
|
|
5952
6090
|
children: /* @__PURE__ */ jsx(Text, { text: confirm.text })
|
|
5953
6091
|
}),
|
|
5954
6092
|
/* @__PURE__ */ jsxs("div", {
|
|
5955
6093
|
className: "sbk-confirm__actions",
|
|
5956
6094
|
children: [/* @__PURE__ */ jsx("button", {
|
|
6095
|
+
ref: denyRef,
|
|
5957
6096
|
type: "button",
|
|
5958
6097
|
className: "sbk-confirm__button sbk-confirm__button--deny",
|
|
5959
6098
|
onClick: onDeny,
|
|
@@ -6017,7 +6156,8 @@ function Button({ element, blockId }) {
|
|
|
6017
6156
|
},
|
|
6018
6157
|
...element.value !== void 0 ? { value: element.value } : {},
|
|
6019
6158
|
...element.url !== void 0 ? { url: element.url } : {},
|
|
6020
|
-
...element.style !== void 0 ? { style: element.style } : {}
|
|
6159
|
+
...element.style !== void 0 ? { style: element.style } : {},
|
|
6160
|
+
...element.confirm ? { confirm: element.confirm } : {}
|
|
6021
6161
|
});
|
|
6022
6162
|
if (element.url) window.open(element.url, "_blank", "noopener,noreferrer");
|
|
6023
6163
|
};
|
|
@@ -6125,31 +6265,107 @@ function todayISO() {
|
|
|
6125
6265
|
const now = /* @__PURE__ */ new Date();
|
|
6126
6266
|
return toISO(now.getFullYear(), now.getMonth(), now.getDate());
|
|
6127
6267
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6268
|
+
/** Slack's calendar header icons, 20×20. */
|
|
6269
|
+
const NAV_PATHS = {
|
|
6270
|
+
"previous-year": "M11.03 2.78a.75.75 0 1 0-1.06-1.06L2.22 9.47a.75.75 0 0 0 0 1.06l7.75 7.75a.75.75 0 1 0 1.06-1.06L3.81 10zm6.25 0a.75.75 0 0 0-1.06-1.06L8.47 9.47a.75.75 0 0 0 0 1.06l7.75 7.75a.75.75 0 1 0 1.06-1.06L10.06 10z",
|
|
6271
|
+
"previous-month": "M14.53 1.72a.75.75 0 0 1 0 1.06L7.31 10l7.22 7.22a.75.75 0 1 1-1.06 1.06l-7.75-7.75a.75.75 0 0 1 0-1.06l7.75-7.75a.75.75 0 0 1 1.06 0",
|
|
6272
|
+
"next-month": "M5.72 1.72a.75.75 0 0 1 1.06 0l7.75 7.75a.75.75 0 0 1 0 1.06l-7.75 7.75a.75.75 0 0 1-1.06-1.06L12.94 10 5.72 2.78a.75.75 0 0 1 0-1.06",
|
|
6273
|
+
"next-year": "M3.78 1.72a.75.75 0 0 0-1.06 1.06L9.94 10l-7.22 7.22a.75.75 0 1 0 1.06 1.06l7.75-7.75a.75.75 0 0 0 0-1.06zm6.25 0a.75.75 0 1 0-1.06 1.06L16.19 10l-7.22 7.22a.75.75 0 1 0 1.06 1.06l7.75-7.75a.75.75 0 0 0 0-1.06z"
|
|
6274
|
+
};
|
|
6275
|
+
/** A header button that moves the calendar by `months`. */
|
|
6276
|
+
function NavButton({ icon, label, onClick }) {
|
|
6277
|
+
return /* @__PURE__ */ jsx("button", {
|
|
6278
|
+
type: "button",
|
|
6279
|
+
className: "sbk-calendar__nav",
|
|
6280
|
+
onClick,
|
|
6281
|
+
"aria-label": label,
|
|
6282
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
6283
|
+
viewBox: "0 0 20 20",
|
|
6284
|
+
width: "20",
|
|
6285
|
+
height: "20",
|
|
6286
|
+
"aria-hidden": "true",
|
|
6287
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
6288
|
+
fill: "currentColor",
|
|
6289
|
+
fillRule: "evenodd",
|
|
6290
|
+
clipRule: "evenodd",
|
|
6291
|
+
d: NAV_PATHS[icon]
|
|
6292
|
+
})
|
|
6139
6293
|
})
|
|
6140
6294
|
});
|
|
6141
6295
|
}
|
|
6296
|
+
/** The day `delta` days after the `YYYY-MM-DD` date, as `YYYY-MM-DD`. */
|
|
6297
|
+
function addDays(iso, delta) {
|
|
6298
|
+
const d = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
6299
|
+
d.setUTCDate(d.getUTCDate() + delta);
|
|
6300
|
+
return toISO(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
6301
|
+
}
|
|
6302
|
+
/** Days the arrow keys move the focused day: a day sideways, a week up or down. */
|
|
6303
|
+
const ARROW_DAYS = {
|
|
6304
|
+
ArrowLeft: -1,
|
|
6305
|
+
ArrowRight: 1,
|
|
6306
|
+
ArrowUp: -7,
|
|
6307
|
+
ArrowDown: 7
|
|
6308
|
+
};
|
|
6142
6309
|
/** A month grid matching Slack's `c-date_picker_calendar`: a bordered 7-column grid of 44×42
|
|
6143
|
-
* day cells, a ring around today, and solid blue for the selected day.
|
|
6310
|
+
* day cells, a ring around today, and solid blue for the selected day. The grid is one tab stop:
|
|
6311
|
+
* the focused day (the selected one, else today, else the 1st) takes Tab, and the arrow keys move
|
|
6312
|
+
* it a day or a week, turning the month when they cross its edge. */
|
|
6144
6313
|
function Calendar({ value, onSelect, onClear }) {
|
|
6145
6314
|
const initial = value ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : /* @__PURE__ */ new Date();
|
|
6146
6315
|
const [year, setYear] = useState(initial.getUTCFullYear());
|
|
6147
6316
|
const [month, setMonth] = useState(initial.getUTCMonth());
|
|
6317
|
+
const [focused, setFocused] = useState(() => {
|
|
6318
|
+
if (value) return value;
|
|
6319
|
+
const today = todayISO();
|
|
6320
|
+
return today.startsWith(toISO(year, month, 1).slice(0, 7)) ? today : toISO(year, month, 1);
|
|
6321
|
+
});
|
|
6322
|
+
const gridRef = useRef(null);
|
|
6323
|
+
const [keyboard, setKeyboard] = useState(false);
|
|
6324
|
+
const moveFocus = useRef(false);
|
|
6325
|
+
useEffect(() => {
|
|
6326
|
+
if (!moveFocus.current) return;
|
|
6327
|
+
moveFocus.current = false;
|
|
6328
|
+
gridRef.current?.querySelector(`[data-date="${focused}"]`)?.focus();
|
|
6329
|
+
}, [
|
|
6330
|
+
focused,
|
|
6331
|
+
year,
|
|
6332
|
+
month
|
|
6333
|
+
]);
|
|
6334
|
+
function onGridKeyDown(e) {
|
|
6335
|
+
const delta = ARROW_DAYS[e.key];
|
|
6336
|
+
if (delta === void 0) return;
|
|
6337
|
+
e.preventDefault();
|
|
6338
|
+
setKeyboard(true);
|
|
6339
|
+
const next = addDays(focused, delta);
|
|
6340
|
+
const [y = year, m = month + 1] = next.split("-").map(Number);
|
|
6341
|
+
moveFocus.current = true;
|
|
6342
|
+
setFocused(next);
|
|
6343
|
+
if (y !== year || m - 1 !== month) {
|
|
6344
|
+
setYear(y);
|
|
6345
|
+
setMonth(m - 1);
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
const shown = toISO(year, month, 1).slice(0, 7);
|
|
6349
|
+
const tabStop = focused.startsWith(shown) ? focused : toISO(year, month, 1);
|
|
6148
6350
|
const first = new Date(Date.UTC(year, month, 1));
|
|
6149
6351
|
const startWeekday = first.getUTCDay();
|
|
6150
6352
|
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
6151
6353
|
const cells = [...Array(startWeekday).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
|
|
6152
6354
|
const today = todayISO();
|
|
6355
|
+
const lastRow = Math.floor((startWeekday + daysInMonth - 1) / 7);
|
|
6356
|
+
const lastRowFull = (startWeekday + daysInMonth) % 7 === 0;
|
|
6357
|
+
/** The grid's outer corners a day sits on, which Slack rounds. */
|
|
6358
|
+
function corners(day) {
|
|
6359
|
+
const at = startWeekday + day - 1;
|
|
6360
|
+
const row = Math.floor(at / 7);
|
|
6361
|
+
const col = at % 7;
|
|
6362
|
+
const out = [];
|
|
6363
|
+
if (day === 1 || row === 1 && col === 0) out.push("top-left");
|
|
6364
|
+
if (row === 0 && col === 6) out.push("top-right");
|
|
6365
|
+
if (row === lastRow && col === 0) out.push("bottom-left");
|
|
6366
|
+
if (day === daysInMonth || !lastRowFull && row === lastRow - 1 && col === 6) out.push("bottom-right");
|
|
6367
|
+
return out;
|
|
6368
|
+
}
|
|
6153
6369
|
function go(delta) {
|
|
6154
6370
|
const d = new Date(Date.UTC(year, month + delta, 1));
|
|
6155
6371
|
setYear(d.getUTCFullYear());
|
|
@@ -6160,50 +6376,59 @@ function Calendar({ value, onSelect, onClear }) {
|
|
|
6160
6376
|
year: "numeric",
|
|
6161
6377
|
timeZone: "UTC"
|
|
6162
6378
|
});
|
|
6163
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
6379
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
6164
6380
|
className: "sbk-calendar",
|
|
6165
6381
|
children: [
|
|
6166
6382
|
/* @__PURE__ */ jsxs("div", {
|
|
6167
6383
|
className: "sbk-calendar__header",
|
|
6168
6384
|
children: [
|
|
6169
|
-
/* @__PURE__ */ jsx(
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
onClick: () => go(-
|
|
6173
|
-
|
|
6174
|
-
|
|
6385
|
+
/* @__PURE__ */ jsx(NavButton, {
|
|
6386
|
+
icon: "previous-year",
|
|
6387
|
+
label: "Previous year",
|
|
6388
|
+
onClick: () => go(-12)
|
|
6389
|
+
}),
|
|
6390
|
+
/* @__PURE__ */ jsx(NavButton, {
|
|
6391
|
+
icon: "previous-month",
|
|
6392
|
+
label: "Previous month",
|
|
6393
|
+
onClick: () => go(-1)
|
|
6175
6394
|
}),
|
|
6176
6395
|
/* @__PURE__ */ jsx("span", {
|
|
6177
6396
|
className: "sbk-calendar__label",
|
|
6178
6397
|
children: monthLabel
|
|
6179
6398
|
}),
|
|
6180
|
-
/* @__PURE__ */ jsx(
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
onClick: () => go(1)
|
|
6184
|
-
|
|
6185
|
-
|
|
6399
|
+
/* @__PURE__ */ jsx(NavButton, {
|
|
6400
|
+
icon: "next-month",
|
|
6401
|
+
label: "Next month",
|
|
6402
|
+
onClick: () => go(1)
|
|
6403
|
+
}),
|
|
6404
|
+
/* @__PURE__ */ jsx(NavButton, {
|
|
6405
|
+
icon: "next-year",
|
|
6406
|
+
label: "Next year",
|
|
6407
|
+
onClick: () => go(12)
|
|
6186
6408
|
})
|
|
6187
6409
|
]
|
|
6188
6410
|
}),
|
|
6189
6411
|
/* @__PURE__ */ jsx("div", {
|
|
6190
6412
|
className: "sbk-calendar__weekdays",
|
|
6191
6413
|
children: [
|
|
6192
|
-
"
|
|
6193
|
-
"
|
|
6194
|
-
"
|
|
6195
|
-
"
|
|
6196
|
-
"
|
|
6197
|
-
"
|
|
6198
|
-
"
|
|
6414
|
+
"Su",
|
|
6415
|
+
"Mo",
|
|
6416
|
+
"Tu",
|
|
6417
|
+
"We",
|
|
6418
|
+
"Th",
|
|
6419
|
+
"Fr",
|
|
6420
|
+
"Sa"
|
|
6199
6421
|
].map((w) => /* @__PURE__ */ jsx("span", { children: w }, w))
|
|
6200
6422
|
}),
|
|
6201
6423
|
/* @__PURE__ */ jsx("div", {
|
|
6202
|
-
className: "sbk-calendar__grid"
|
|
6424
|
+
className: `sbk-calendar__grid${keyboard ? " sbk-calendar__grid--keyboard" : ""}`,
|
|
6425
|
+
ref: gridRef,
|
|
6426
|
+
onKeyDown: onGridKeyDown,
|
|
6427
|
+
onPointerDown: () => setKeyboard(false),
|
|
6203
6428
|
children: cells.map((day, i) => {
|
|
6204
6429
|
if (day === null) return /* @__PURE__ */ jsx("span", { className: "sbk-calendar__cell sbk-calendar__cell--empty" }, i);
|
|
6205
6430
|
const iso = toISO(year, month, day);
|
|
6206
|
-
const classes = ["sbk-calendar__cell"];
|
|
6431
|
+
const classes = ["sbk-calendar__cell", ...corners(day).map((c) => `sbk-calendar__cell--${c}`)];
|
|
6207
6432
|
if (iso === value) classes.push("sbk-calendar__cell--selected");
|
|
6208
6433
|
if (iso === today) classes.push("sbk-calendar__cell--today");
|
|
6209
6434
|
return /* @__PURE__ */ jsx("button", {
|
|
@@ -6211,22 +6436,24 @@ function Calendar({ value, onSelect, onClear }) {
|
|
|
6211
6436
|
className: classes.join(" "),
|
|
6212
6437
|
"aria-pressed": iso === value,
|
|
6213
6438
|
"aria-current": iso === today ? "date" : void 0,
|
|
6439
|
+
"data-date": iso,
|
|
6440
|
+
tabIndex: iso === tabStop ? 0 : -1,
|
|
6441
|
+
onFocus: () => setFocused(iso),
|
|
6214
6442
|
onClick: () => onSelect(iso),
|
|
6215
6443
|
children: day
|
|
6216
6444
|
}, iso);
|
|
6217
6445
|
})
|
|
6218
|
-
}),
|
|
6219
|
-
onClear && value && /* @__PURE__ */ jsx("div", {
|
|
6220
|
-
className: "sbk-calendar__footer",
|
|
6221
|
-
children: /* @__PURE__ */ jsx("button", {
|
|
6222
|
-
type: "button",
|
|
6223
|
-
className: "sbk-calendar__clear",
|
|
6224
|
-
onClick: onClear,
|
|
6225
|
-
children: "Clear selection"
|
|
6226
|
-
})
|
|
6227
6446
|
})
|
|
6228
6447
|
]
|
|
6229
|
-
})
|
|
6448
|
+
}), onClear && value && /* @__PURE__ */ jsx("div", {
|
|
6449
|
+
className: "sbk-calendar__footer",
|
|
6450
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
6451
|
+
type: "button",
|
|
6452
|
+
className: "sbk-calendar__clear",
|
|
6453
|
+
onClick: onClear,
|
|
6454
|
+
children: "Clear selection"
|
|
6455
|
+
})
|
|
6456
|
+
})] });
|
|
6230
6457
|
}
|
|
6231
6458
|
//#endregion
|
|
6232
6459
|
//#region src/elements/dateFormat.ts
|
|
@@ -6325,6 +6552,11 @@ const InputBlockContext = createContext(false);
|
|
|
6325
6552
|
function useInInputBlock() {
|
|
6326
6553
|
return useContext(InputBlockContext);
|
|
6327
6554
|
}
|
|
6555
|
+
/** True while rendering the element of an `input` block marked `optional`, which Slack lets the user empty. */
|
|
6556
|
+
const InputOptionalContext = createContext(false);
|
|
6557
|
+
function useInOptionalInput() {
|
|
6558
|
+
return useContext(InputOptionalContext);
|
|
6559
|
+
}
|
|
6328
6560
|
/** The id of the error shown under an `input` block's element, while it has one. */
|
|
6329
6561
|
const InputErrorContext = createContext(void 0);
|
|
6330
6562
|
/**
|
|
@@ -6342,14 +6574,18 @@ function useInvalidProps() {
|
|
|
6342
6574
|
//#region src/elements/Popover.tsx
|
|
6343
6575
|
/** Space between the anchor and the popover, as in Slack's menus. */
|
|
6344
6576
|
const DEFAULT_GAP = 4;
|
|
6577
|
+
/** The least space kept between a popover and the window's left and right edges. */
|
|
6578
|
+
const EDGE = 8;
|
|
6345
6579
|
/**
|
|
6346
6580
|
* The layer a menu or calendar opens in. Slack mounts its popovers at the top of the page, so a
|
|
6347
6581
|
* message list, a modal body or a carousel never clips them; this portals to `<body>` the same way
|
|
6348
6582
|
* and keeps the popover pinned to its anchor as the page scrolls or resizes, or its content does.
|
|
6583
|
+
* Where keeping its alignment would run it past the window's left or right edge (a narrow phone
|
|
6584
|
+
* screen), it shifts just enough to stay inside.
|
|
6349
6585
|
* The layer is as wide as the anchor, so a child sized `width: 100%` matches the control it opened
|
|
6350
6586
|
* from.
|
|
6351
6587
|
*/
|
|
6352
|
-
function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
|
|
6588
|
+
function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, offsetX = 0, onPlaced, children }) {
|
|
6353
6589
|
const { theme } = useBlockKit();
|
|
6354
6590
|
const layerRef = useRef(null);
|
|
6355
6591
|
const [position, setPosition] = useState(null);
|
|
@@ -6370,9 +6606,19 @@ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
|
|
|
6370
6606
|
const fitsAbove = above >= 0;
|
|
6371
6607
|
let top = !fitsBelow && fitsAbove ? above : below;
|
|
6372
6608
|
if (!fitsBelow && !fitsAbove) top = Math.max(gap, Math.min(below, window.innerHeight - height - gap));
|
|
6609
|
+
layer.style.width = `${a.width}px`;
|
|
6610
|
+
const content = layer.firstElementChild?.getBoundingClientRect();
|
|
6611
|
+
const layerLeft = layer.getBoundingClientRect().left;
|
|
6612
|
+
let left = a.left + offsetX;
|
|
6613
|
+
if (content) {
|
|
6614
|
+
const contentLeft = left + content.left - layerLeft;
|
|
6615
|
+
const room = document.documentElement.clientWidth - EDGE;
|
|
6616
|
+
const shift = Math.max(EDGE - contentLeft, Math.min(0, room - (contentLeft + content.width)));
|
|
6617
|
+
left += shift;
|
|
6618
|
+
}
|
|
6373
6619
|
setPosition({
|
|
6374
6620
|
top,
|
|
6375
|
-
left
|
|
6621
|
+
left,
|
|
6376
6622
|
width: a.width
|
|
6377
6623
|
});
|
|
6378
6624
|
}
|
|
@@ -6387,7 +6633,18 @@ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
|
|
|
6387
6633
|
window.removeEventListener("resize", place);
|
|
6388
6634
|
resize?.disconnect();
|
|
6389
6635
|
};
|
|
6390
|
-
}, [
|
|
6636
|
+
}, [
|
|
6637
|
+
anchorRef,
|
|
6638
|
+
gap,
|
|
6639
|
+
offsetX
|
|
6640
|
+
]);
|
|
6641
|
+
const placed = position !== null;
|
|
6642
|
+
const notified = useRef(false);
|
|
6643
|
+
useEffect(() => {
|
|
6644
|
+
if (!placed || notified.current) return;
|
|
6645
|
+
notified.current = true;
|
|
6646
|
+
onPlaced?.();
|
|
6647
|
+
}, [placed]);
|
|
6391
6648
|
useEffect(() => {
|
|
6392
6649
|
function onDocDown(e) {
|
|
6393
6650
|
const target = e.target;
|
|
@@ -6427,14 +6684,27 @@ function formatDate$1(date, long) {
|
|
|
6427
6684
|
function DatePicker({ element, blockId }) {
|
|
6428
6685
|
const { setValue, dispatch } = useBlockKit();
|
|
6429
6686
|
const inInputBlock = useInInputBlock();
|
|
6687
|
+
const clearable = useInOptionalInput();
|
|
6430
6688
|
const { ask, dialog } = useConfirm(element.confirm);
|
|
6431
6689
|
const [date, setDate] = useState(element.initial_date);
|
|
6432
6690
|
const [open, setOpen] = useState(false);
|
|
6433
6691
|
const rootRef = useRef(null);
|
|
6434
6692
|
const inputRef = useFocusOnLoad(element);
|
|
6435
6693
|
const invalid = useInvalidProps();
|
|
6436
|
-
const
|
|
6694
|
+
const quietFocus = useRef(element.focus_on_load === true);
|
|
6695
|
+
const focusCalendar = useRef(false);
|
|
6696
|
+
const popupRef = useRef(null);
|
|
6437
6697
|
const actionId = element.action_id ?? "";
|
|
6698
|
+
function focusDay() {
|
|
6699
|
+
popupRef.current?.querySelector(".sbk-calendar__grid [tabindex='0']")?.focus();
|
|
6700
|
+
}
|
|
6701
|
+
function close() {
|
|
6702
|
+
setOpen(false);
|
|
6703
|
+
const input = inputRef.current;
|
|
6704
|
+
if (!input || document.activeElement === input) return;
|
|
6705
|
+
quietFocus.current = true;
|
|
6706
|
+
input.focus();
|
|
6707
|
+
}
|
|
6438
6708
|
useEffect(() => {
|
|
6439
6709
|
if (date) setValue(blockId, actionId, {
|
|
6440
6710
|
type: "datepicker",
|
|
@@ -6444,7 +6714,7 @@ function DatePicker({ element, blockId }) {
|
|
|
6444
6714
|
async function pick(next) {
|
|
6445
6715
|
if (!await ask()) return;
|
|
6446
6716
|
setDate(next);
|
|
6447
|
-
|
|
6717
|
+
close();
|
|
6448
6718
|
setValue(blockId, actionId, {
|
|
6449
6719
|
type: "datepicker",
|
|
6450
6720
|
selected_date: next
|
|
@@ -6453,13 +6723,14 @@ function DatePicker({ element, blockId }) {
|
|
|
6453
6723
|
type: "datepicker",
|
|
6454
6724
|
action_id: actionId,
|
|
6455
6725
|
block_id: blockId,
|
|
6456
|
-
selected_date: next
|
|
6726
|
+
selected_date: next,
|
|
6727
|
+
...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
|
|
6457
6728
|
});
|
|
6458
6729
|
}
|
|
6459
6730
|
async function clear() {
|
|
6460
6731
|
if (!await ask()) return;
|
|
6461
6732
|
setDate(void 0);
|
|
6462
|
-
|
|
6733
|
+
close();
|
|
6463
6734
|
setValue(blockId, actionId, {
|
|
6464
6735
|
type: "datepicker",
|
|
6465
6736
|
selected_date: null
|
|
@@ -6468,7 +6739,8 @@ function DatePicker({ element, blockId }) {
|
|
|
6468
6739
|
type: "datepicker",
|
|
6469
6740
|
action_id: actionId,
|
|
6470
6741
|
block_id: blockId,
|
|
6471
|
-
selected_date: null
|
|
6742
|
+
selected_date: null,
|
|
6743
|
+
...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
|
|
6472
6744
|
});
|
|
6473
6745
|
}
|
|
6474
6746
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -6489,12 +6761,27 @@ function DatePicker({ element, blockId }) {
|
|
|
6489
6761
|
...invalid,
|
|
6490
6762
|
ref: inputRef,
|
|
6491
6763
|
onFocus: () => {
|
|
6492
|
-
if (
|
|
6764
|
+
if (quietFocus.current) quietFocus.current = false;
|
|
6493
6765
|
else setOpen(true);
|
|
6494
6766
|
},
|
|
6495
|
-
|
|
6767
|
+
onMouseDown: () => {
|
|
6768
|
+
focusCalendar.current = true;
|
|
6769
|
+
},
|
|
6770
|
+
onClick: () => {
|
|
6771
|
+
if (open) focusDay();
|
|
6772
|
+
else focusCalendar.current = true;
|
|
6773
|
+
setOpen(true);
|
|
6774
|
+
},
|
|
6496
6775
|
onKeyDown: (e) => {
|
|
6497
6776
|
if (e.key === "Escape") setOpen(false);
|
|
6777
|
+
if (e.key === "ArrowDown") {
|
|
6778
|
+
e.preventDefault();
|
|
6779
|
+
if (open) focusDay();
|
|
6780
|
+
else {
|
|
6781
|
+
focusCalendar.current = true;
|
|
6782
|
+
setOpen(true);
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6498
6785
|
}
|
|
6499
6786
|
}),
|
|
6500
6787
|
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datepicker__chevron" })
|
|
@@ -6502,13 +6789,26 @@ function DatePicker({ element, blockId }) {
|
|
|
6502
6789
|
}),
|
|
6503
6790
|
open && /* @__PURE__ */ jsx(Popover, {
|
|
6504
6791
|
anchorRef: rootRef,
|
|
6792
|
+
gap: -4,
|
|
6505
6793
|
onDismiss: () => setOpen(false),
|
|
6794
|
+
onPlaced: () => {
|
|
6795
|
+
if (focusCalendar.current) focusDay();
|
|
6796
|
+
focusCalendar.current = false;
|
|
6797
|
+
},
|
|
6506
6798
|
children: /* @__PURE__ */ jsx("div", {
|
|
6507
6799
|
className: "sbk-datepicker__popup",
|
|
6800
|
+
ref: popupRef,
|
|
6801
|
+
role: "dialog",
|
|
6802
|
+
"aria-label": "Choose a date",
|
|
6803
|
+
onKeyDown: (e) => {
|
|
6804
|
+
if (e.key !== "Escape") return;
|
|
6805
|
+
e.preventDefault();
|
|
6806
|
+
close();
|
|
6807
|
+
},
|
|
6508
6808
|
children: /* @__PURE__ */ jsx(Calendar, {
|
|
6509
6809
|
value: date,
|
|
6510
6810
|
onSelect: pick,
|
|
6511
|
-
onClear: clear
|
|
6811
|
+
onClear: clearable ? clear : void 0
|
|
6512
6812
|
})
|
|
6513
6813
|
})
|
|
6514
6814
|
}),
|
|
@@ -6884,9 +7184,11 @@ function Overflow({ element, blockId }) {
|
|
|
6884
7184
|
const triggerRef = useRef(null);
|
|
6885
7185
|
const listRef = useRef(null);
|
|
6886
7186
|
const actionId = element.action_id ?? "";
|
|
7187
|
+
const idPrefix = useId();
|
|
6887
7188
|
async function choose(option) {
|
|
6888
7189
|
if (!await ask()) return;
|
|
6889
7190
|
setOpen(false);
|
|
7191
|
+
triggerRef.current?.focus();
|
|
6890
7192
|
dispatch({
|
|
6891
7193
|
type: "overflow",
|
|
6892
7194
|
action_id: actionId,
|
|
@@ -6927,11 +7229,15 @@ function Overflow({ element, blockId }) {
|
|
|
6927
7229
|
open && /* @__PURE__ */ jsx(Popover, {
|
|
6928
7230
|
anchorRef: rootRef,
|
|
6929
7231
|
onDismiss: () => setOpen(false),
|
|
7232
|
+
onPlaced: () => listRef.current?.focus(),
|
|
6930
7233
|
children: /* @__PURE__ */ jsx("div", {
|
|
6931
7234
|
className: "sbk-overflow__menu",
|
|
6932
7235
|
role: "menu",
|
|
6933
7236
|
ref: listRef,
|
|
7237
|
+
tabIndex: -1,
|
|
7238
|
+
"aria-activedescendant": nav.active >= 0 ? `${idPrefix}-${nav.active}` : void 0,
|
|
6934
7239
|
children: element.options.map((option, i) => /* @__PURE__ */ jsx("div", {
|
|
7240
|
+
id: `${idPrefix}-${i}`,
|
|
6935
7241
|
role: "menuitem",
|
|
6936
7242
|
className: "sbk-overflow__option",
|
|
6937
7243
|
onClick: () => choose(option),
|
|
@@ -7222,6 +7528,236 @@ function RichTextInput({ element, blockId }) {
|
|
|
7222
7528
|
});
|
|
7223
7529
|
}
|
|
7224
7530
|
//#endregion
|
|
7531
|
+
//#region src/elements/useCombobox.ts
|
|
7532
|
+
/**
|
|
7533
|
+
* The typeable trigger Slack's select and time pickers share (`c-select_input`): a text input with
|
|
7534
|
+
* role="combobox" that shows the chosen value while closed, and the query while its list is open.
|
|
7535
|
+
* Typing filters the list and highlights its first row; the arrow keys move the highlight, Enter
|
|
7536
|
+
* picks it, Escape and Tab close the list and put the chosen value back.
|
|
7537
|
+
*/
|
|
7538
|
+
function useCombobox({ query, onQueryChange, count, initialIndex, onChoose, onSubmitQuery, listRef, returnFocusRef, open: controlledOpen, onOpenChange }) {
|
|
7539
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
7540
|
+
const open = controlledOpen ?? ownOpen;
|
|
7541
|
+
const setOpenState = onOpenChange ?? setOwnOpen;
|
|
7542
|
+
const inputRef = useRef(null);
|
|
7543
|
+
const id = useId();
|
|
7544
|
+
const listId = `${id}-list`;
|
|
7545
|
+
const optionId = (index) => `${id}-option-${index}`;
|
|
7546
|
+
function setOpen(next) {
|
|
7547
|
+
setOpenState(next);
|
|
7548
|
+
if (!next) onQueryChange("");
|
|
7549
|
+
}
|
|
7550
|
+
const nav = useMenuNavigation({
|
|
7551
|
+
open,
|
|
7552
|
+
count,
|
|
7553
|
+
initialIndex,
|
|
7554
|
+
onChoose,
|
|
7555
|
+
onClose: () => {
|
|
7556
|
+
setOpen(false);
|
|
7557
|
+
(returnFocusRef ?? inputRef).current?.focus();
|
|
7558
|
+
},
|
|
7559
|
+
onOpen: () => setOpenState(true),
|
|
7560
|
+
listRef
|
|
7561
|
+
});
|
|
7562
|
+
/** Keys for the input, or for a container around a button trigger and its list. */
|
|
7563
|
+
function onKeyDown(e) {
|
|
7564
|
+
if (e.key === "Tab") {
|
|
7565
|
+
if (open) setOpen(false);
|
|
7566
|
+
return;
|
|
7567
|
+
}
|
|
7568
|
+
if (open && e.key === "Enter" && nav.active < 0 && query.trim()) {
|
|
7569
|
+
e.preventDefault();
|
|
7570
|
+
onSubmitQuery?.(query.trim());
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
nav.onKeyDown(e);
|
|
7574
|
+
}
|
|
7575
|
+
/** Props for the input. `display` is the chosen value, shown while the list is closed. */
|
|
7576
|
+
function inputProps(display, placeholder) {
|
|
7577
|
+
return {
|
|
7578
|
+
ref: inputRef,
|
|
7579
|
+
type: "text",
|
|
7580
|
+
role: "combobox",
|
|
7581
|
+
autoComplete: "off",
|
|
7582
|
+
size: 1,
|
|
7583
|
+
spellCheck: false,
|
|
7584
|
+
"aria-autocomplete": "list",
|
|
7585
|
+
"aria-expanded": open,
|
|
7586
|
+
"aria-controls": open ? listId : void 0,
|
|
7587
|
+
"aria-activedescendant": open && nav.active >= 0 ? optionId(nav.active) : void 0,
|
|
7588
|
+
"aria-label": placeholder,
|
|
7589
|
+
placeholder: open ? display ?? placeholder : placeholder,
|
|
7590
|
+
value: open ? query : display ?? "",
|
|
7591
|
+
onClick: () => setOpenState(true),
|
|
7592
|
+
onChange: (e) => {
|
|
7593
|
+
onQueryChange(e.target.value);
|
|
7594
|
+
setOpenState(true);
|
|
7595
|
+
nav.setActive(0);
|
|
7596
|
+
},
|
|
7597
|
+
onKeyDown
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7600
|
+
/** Props for the row at `index`, tying it to the input's aria-activedescendant. */
|
|
7601
|
+
function optionProps(index) {
|
|
7602
|
+
return {
|
|
7603
|
+
id: optionId(index),
|
|
7604
|
+
...nav.itemProps(index)
|
|
7605
|
+
};
|
|
7606
|
+
}
|
|
7607
|
+
return {
|
|
7608
|
+
open,
|
|
7609
|
+
setOpen,
|
|
7610
|
+
onKeyDown,
|
|
7611
|
+
listId,
|
|
7612
|
+
inputRef,
|
|
7613
|
+
inputProps,
|
|
7614
|
+
optionProps,
|
|
7615
|
+
active: nav.active
|
|
7616
|
+
};
|
|
7617
|
+
}
|
|
7618
|
+
//#endregion
|
|
7619
|
+
//#region src/elements/accessoryContext.ts
|
|
7620
|
+
/**
|
|
7621
|
+
* True for an element rendered as a section block's accessory. Slack draws some elements
|
|
7622
|
+
* differently there: a multi-select becomes a small button that opens a "Select options" dialog.
|
|
7623
|
+
*/
|
|
7624
|
+
const SectionAccessoryContext = createContext(false);
|
|
7625
|
+
//#endregion
|
|
7626
|
+
//#region src/elements/SelectDialog.tsx
|
|
7627
|
+
const optionId = (option) => option.value ?? option.text.text;
|
|
7628
|
+
/**
|
|
7629
|
+
* Slack's "Select options" dialog, which a `multi_static_select` in a section accessory opens
|
|
7630
|
+
* instead of an inline menu. Picks are a draft: the option list stays open while choosing, and
|
|
7631
|
+
* nothing is sent until Confirm. Cancel, the close button and Escape discard the draft.
|
|
7632
|
+
*/
|
|
7633
|
+
function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
|
|
7634
|
+
const { theme } = useBlockKit();
|
|
7635
|
+
const titleId = useId();
|
|
7636
|
+
const [draft, setDraft] = useState(initial);
|
|
7637
|
+
const [query, setQuery] = useState("");
|
|
7638
|
+
const fieldRef = useRef(null);
|
|
7639
|
+
const listRef = useRef(null);
|
|
7640
|
+
const rows = options.filter((o) => o.text.text.toLowerCase().includes(query.toLowerCase()));
|
|
7641
|
+
const chosen = (option) => draft.some((d) => optionId(d) === optionId(option));
|
|
7642
|
+
const toggle = (option) => setDraft((current) => chosen(option) ? current.filter((d) => optionId(d) !== optionId(option)) : [...current, option]);
|
|
7643
|
+
const combo = useCombobox({
|
|
7644
|
+
query,
|
|
7645
|
+
onQueryChange: setQuery,
|
|
7646
|
+
count: rows.length,
|
|
7647
|
+
initialIndex: 0,
|
|
7648
|
+
onChoose: (i) => {
|
|
7649
|
+
const option = rows[i];
|
|
7650
|
+
if (option) toggle(option);
|
|
7651
|
+
},
|
|
7652
|
+
listRef
|
|
7653
|
+
});
|
|
7654
|
+
useEffect(() => {
|
|
7655
|
+
combo.inputRef.current?.focus();
|
|
7656
|
+
}, []);
|
|
7657
|
+
if (typeof document === "undefined") return null;
|
|
7658
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
7659
|
+
className: "sbk-root sbk-select-dialog__overlay",
|
|
7660
|
+
"data-theme": theme,
|
|
7661
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
7662
|
+
className: "sbk-select-dialog",
|
|
7663
|
+
role: "dialog",
|
|
7664
|
+
"aria-modal": "true",
|
|
7665
|
+
"aria-labelledby": titleId,
|
|
7666
|
+
onKeyDown: (e) => {
|
|
7667
|
+
if (e.key !== "Escape") return;
|
|
7668
|
+
e.preventDefault();
|
|
7669
|
+
e.stopPropagation();
|
|
7670
|
+
onCancel();
|
|
7671
|
+
},
|
|
7672
|
+
children: [
|
|
7673
|
+
/* @__PURE__ */ jsxs("div", {
|
|
7674
|
+
className: "sbk-select-dialog__header",
|
|
7675
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
7676
|
+
className: "sbk-select-dialog__title",
|
|
7677
|
+
id: titleId,
|
|
7678
|
+
children: "Select options"
|
|
7679
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
7680
|
+
type: "button",
|
|
7681
|
+
className: "sbk-select-dialog__close",
|
|
7682
|
+
"aria-label": "Close",
|
|
7683
|
+
onClick: onCancel,
|
|
7684
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
7685
|
+
})]
|
|
7686
|
+
}),
|
|
7687
|
+
/* @__PURE__ */ jsxs("div", {
|
|
7688
|
+
className: "sbk-select-dialog__body",
|
|
7689
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
7690
|
+
className: "sbk-select-dialog__field",
|
|
7691
|
+
ref: fieldRef,
|
|
7692
|
+
children: [draft.map((option) => /* @__PURE__ */ jsxs("span", {
|
|
7693
|
+
className: "sbk-select__chip",
|
|
7694
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
7695
|
+
className: "sbk-select__chip-label",
|
|
7696
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
7697
|
+
className: "sbk-select__chip-text",
|
|
7698
|
+
children: /* @__PURE__ */ jsx("span", { children: option.text.text })
|
|
7699
|
+
})
|
|
7700
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
7701
|
+
type: "button",
|
|
7702
|
+
className: "sbk-select__chip-remove",
|
|
7703
|
+
"aria-label": `Remove ${option.text.text}`,
|
|
7704
|
+
onClick: () => toggle(option),
|
|
7705
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
7706
|
+
})]
|
|
7707
|
+
}, optionId(option))), /* @__PURE__ */ jsx("input", {
|
|
7708
|
+
...combo.inputProps(void 0, placeholder),
|
|
7709
|
+
placeholder: draft.length > 0 ? "" : placeholder,
|
|
7710
|
+
className: "sbk-select-dialog__input"
|
|
7711
|
+
})]
|
|
7712
|
+
}), combo.open && /* @__PURE__ */ jsx(Popover, {
|
|
7713
|
+
anchorRef: fieldRef,
|
|
7714
|
+
onDismiss: () => combo.setOpen(false),
|
|
7715
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
7716
|
+
className: "sbk-select__menu sbk-select-dialog__menu",
|
|
7717
|
+
role: "listbox",
|
|
7718
|
+
"aria-multiselectable": "true",
|
|
7719
|
+
id: combo.listId,
|
|
7720
|
+
ref: listRef,
|
|
7721
|
+
children: rows.map((option, i) => /* @__PURE__ */ jsx("div", {
|
|
7722
|
+
role: "option",
|
|
7723
|
+
"aria-selected": chosen(option),
|
|
7724
|
+
className: `sbk-select__option${chosen(option) ? " sbk-select__option--selected" : ""}`,
|
|
7725
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
7726
|
+
onClick: () => toggle(option),
|
|
7727
|
+
...combo.optionProps(i),
|
|
7728
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
7729
|
+
className: "sbk-select__option-text",
|
|
7730
|
+
children: /* @__PURE__ */ jsx(Text, { text: option.text })
|
|
7731
|
+
})
|
|
7732
|
+
}, optionId(option)))
|
|
7733
|
+
})
|
|
7734
|
+
})]
|
|
7735
|
+
}),
|
|
7736
|
+
/* @__PURE__ */ jsxs("div", {
|
|
7737
|
+
className: "sbk-select-dialog__footer",
|
|
7738
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
7739
|
+
type: "button",
|
|
7740
|
+
className: "sbk-button",
|
|
7741
|
+
onClick: onCancel,
|
|
7742
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
7743
|
+
className: "sbk-button__label",
|
|
7744
|
+
children: "Cancel"
|
|
7745
|
+
})
|
|
7746
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
7747
|
+
type: "button",
|
|
7748
|
+
className: "sbk-button sbk-button--primary",
|
|
7749
|
+
onClick: () => onConfirm(draft),
|
|
7750
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
7751
|
+
className: "sbk-button__label",
|
|
7752
|
+
children: "Confirm"
|
|
7753
|
+
})
|
|
7754
|
+
})]
|
|
7755
|
+
})
|
|
7756
|
+
]
|
|
7757
|
+
})
|
|
7758
|
+
}), document.body);
|
|
7759
|
+
}
|
|
7760
|
+
//#endregion
|
|
7225
7761
|
//#region src/elements/Select.tsx
|
|
7226
7762
|
function parseType(type) {
|
|
7227
7763
|
const multi = type.startsWith("multi_");
|
|
@@ -7291,7 +7827,6 @@ function Select({ element, blockId }) {
|
|
|
7291
7827
|
const [query, setQuery] = useState("");
|
|
7292
7828
|
const rootRef = useRef(null);
|
|
7293
7829
|
const triggerRef = useRef(null);
|
|
7294
|
-
useFocusOnLoad(element, triggerRef);
|
|
7295
7830
|
const invalid = useInvalidProps();
|
|
7296
7831
|
const listRef = useRef(null);
|
|
7297
7832
|
const remote = source === "external" && loadOptions !== void 0;
|
|
@@ -7378,7 +7913,8 @@ function Select({ element, blockId }) {
|
|
|
7378
7913
|
type: element.type,
|
|
7379
7914
|
action_id: actionId,
|
|
7380
7915
|
block_id: blockId,
|
|
7381
|
-
...fields
|
|
7916
|
+
...fields,
|
|
7917
|
+
...element.placeholder ? { placeholder: element.placeholder } : {}
|
|
7382
7918
|
});
|
|
7383
7919
|
}
|
|
7384
7920
|
function buildStateFor(next) {
|
|
@@ -7411,28 +7947,166 @@ function Select({ element, blockId }) {
|
|
|
7411
7947
|
})) : [{ options: filteredOptions }];
|
|
7412
7948
|
const flatOptions = visibleGroups.flatMap((g) => g.options);
|
|
7413
7949
|
const isSelected = (option) => items.some((i) => i.id === (option.value ?? option.text.text));
|
|
7414
|
-
const
|
|
7950
|
+
const showSearchOnly = source !== "static";
|
|
7951
|
+
const typeable = source === "static" && !multi;
|
|
7952
|
+
const dialogMode = useContext(SectionAccessoryContext) && multi && source === "static";
|
|
7953
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
7954
|
+
const combo = useCombobox({
|
|
7415
7955
|
open,
|
|
7956
|
+
onOpenChange: setOpen,
|
|
7957
|
+
query,
|
|
7958
|
+
onQueryChange: setQuery,
|
|
7416
7959
|
count: flatOptions.length,
|
|
7417
7960
|
initialIndex: Math.max(0, flatOptions.findIndex(isSelected)),
|
|
7418
7961
|
onChoose: (i) => {
|
|
7419
7962
|
const option = flatOptions[i];
|
|
7420
7963
|
if (option) selectItem(optionToItem(option));
|
|
7421
7964
|
},
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7965
|
+
onSubmitQuery: (typed) => {
|
|
7966
|
+
if (showSearchOnly && !remote) selectItem({
|
|
7967
|
+
id: typed,
|
|
7968
|
+
label: resolveLabel(typed)
|
|
7969
|
+
});
|
|
7425
7970
|
},
|
|
7426
|
-
|
|
7427
|
-
|
|
7971
|
+
listRef,
|
|
7972
|
+
returnFocusRef: typeable ? void 0 : triggerRef
|
|
7428
7973
|
});
|
|
7974
|
+
useFocusOnLoad(element, typeable ? combo.inputRef : triggerRef);
|
|
7429
7975
|
const closedLabel = multi ? void 0 : items[0] ? items[0].label || resolveLabel(items[0].id) : void 0;
|
|
7430
7976
|
const placeholder = element.placeholder?.text ?? "Select an item";
|
|
7431
|
-
const
|
|
7977
|
+
const menu = open && /* @__PURE__ */ jsx(Popover, {
|
|
7978
|
+
anchorRef: rootRef,
|
|
7979
|
+
onDismiss: () => combo.setOpen(false),
|
|
7980
|
+
offsetX: typeable ? -12 : 0,
|
|
7981
|
+
gap: -4,
|
|
7982
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
7983
|
+
className: `sbk-select__menu${typeable ? " sbk-select__menu--typeable" : ""}`,
|
|
7984
|
+
role: "listbox",
|
|
7985
|
+
id: combo.listId,
|
|
7986
|
+
ref: listRef,
|
|
7987
|
+
children: [
|
|
7988
|
+
!typeable && (showSearchOnly || allOptions(element).length > 8) && /* @__PURE__ */ jsxs("div", {
|
|
7989
|
+
className: "sbk-select__search",
|
|
7990
|
+
children: [/* @__PURE__ */ jsx(SearchIcon$1, {}), /* @__PURE__ */ jsx("input", {
|
|
7991
|
+
autoFocus: true,
|
|
7992
|
+
className: "sbk-select__search-input",
|
|
7993
|
+
placeholder: source === "static" || source === "external" ? "Search options" : `Search ${source}`,
|
|
7994
|
+
value: query,
|
|
7995
|
+
onChange: (e) => setQuery(e.target.value)
|
|
7996
|
+
})]
|
|
7997
|
+
}),
|
|
7998
|
+
(() => {
|
|
7999
|
+
let index = 0;
|
|
8000
|
+
return visibleGroups.map((group, gi) => {
|
|
8001
|
+
const rows = group.options.map((option) => {
|
|
8002
|
+
const i = index++;
|
|
8003
|
+
return /* @__PURE__ */ jsx(SelectOption, {
|
|
8004
|
+
option,
|
|
8005
|
+
selected: isSelected(option),
|
|
8006
|
+
onSelect: () => selectItem(optionToItem(option)),
|
|
8007
|
+
navProps: combo.optionProps(i)
|
|
8008
|
+
}, option.value ?? i);
|
|
8009
|
+
});
|
|
8010
|
+
return group.label === void 0 ? rows : /* @__PURE__ */ jsxs("div", {
|
|
8011
|
+
className: "sbk-select__group",
|
|
8012
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
8013
|
+
className: "sbk-select__group-label",
|
|
8014
|
+
children: group.label
|
|
8015
|
+
}), rows]
|
|
8016
|
+
}, gi);
|
|
8017
|
+
});
|
|
8018
|
+
})(),
|
|
8019
|
+
remote && loading && /* @__PURE__ */ jsx("div", {
|
|
8020
|
+
className: "sbk-select__status",
|
|
8021
|
+
children: "Loading…"
|
|
8022
|
+
}),
|
|
8023
|
+
remote && !loading && remoteGroups !== void 0 && flatOptions.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
8024
|
+
className: "sbk-select__status",
|
|
8025
|
+
children: "No results"
|
|
8026
|
+
}),
|
|
8027
|
+
showSearchOnly && items.length > 0 && multi && /* @__PURE__ */ jsx("div", {
|
|
8028
|
+
className: "sbk-select__group-label",
|
|
8029
|
+
children: "Selected"
|
|
8030
|
+
}),
|
|
8031
|
+
showSearchOnly && multi && items.map((item) => /* @__PURE__ */ jsx("div", {
|
|
8032
|
+
className: "sbk-select__option sbk-select__option--selected",
|
|
8033
|
+
children: item.label || resolveLabel(item.id)
|
|
8034
|
+
}, item.id))
|
|
8035
|
+
]
|
|
8036
|
+
})
|
|
8037
|
+
});
|
|
8038
|
+
if (dialogMode) {
|
|
8039
|
+
const closeDialog = () => {
|
|
8040
|
+
setDialogOpen(false);
|
|
8041
|
+
triggerRef.current?.focus();
|
|
8042
|
+
};
|
|
8043
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8044
|
+
className: `sbk-select sbk-select--multi${sizeClass}`,
|
|
8045
|
+
ref: rootRef,
|
|
8046
|
+
children: [
|
|
8047
|
+
/* @__PURE__ */ jsx("button", {
|
|
8048
|
+
ref: triggerRef,
|
|
8049
|
+
type: "button",
|
|
8050
|
+
className: "sbk-select__control",
|
|
8051
|
+
onClick: () => setDialogOpen(true),
|
|
8052
|
+
"aria-haspopup": "dialog",
|
|
8053
|
+
"aria-expanded": dialogOpen,
|
|
8054
|
+
...invalid,
|
|
8055
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
8056
|
+
className: "sbk-select__placeholder",
|
|
8057
|
+
children: items.length > 0 ? `${items.length} selected` : placeholder
|
|
8058
|
+
})
|
|
8059
|
+
}),
|
|
8060
|
+
dialogOpen && /* @__PURE__ */ jsx(SelectDialog, {
|
|
8061
|
+
options: allOptions(element),
|
|
8062
|
+
initial: items.flatMap((i) => i.option ? [i.option] : []),
|
|
8063
|
+
placeholder,
|
|
8064
|
+
onCancel: closeDialog,
|
|
8065
|
+
onConfirm: async (selected) => {
|
|
8066
|
+
closeDialog();
|
|
8067
|
+
if (!await ask()) return;
|
|
8068
|
+
await commit(selected.map(optionToItem));
|
|
8069
|
+
}
|
|
8070
|
+
}),
|
|
8071
|
+
dialog
|
|
8072
|
+
]
|
|
8073
|
+
});
|
|
8074
|
+
}
|
|
8075
|
+
if (typeable) {
|
|
8076
|
+
const display = items[0] !== void 0 && isUnresolved(items[0]) ? void 0 : closedLabel;
|
|
8077
|
+
const overlay = display !== void 0 && !open;
|
|
8078
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8079
|
+
className: `sbk-select sbk-select--typeable${sizeClass}`,
|
|
8080
|
+
ref: rootRef,
|
|
8081
|
+
children: [
|
|
8082
|
+
/* @__PURE__ */ jsxs("label", {
|
|
8083
|
+
className: "sbk-select__control",
|
|
8084
|
+
children: [
|
|
8085
|
+
/* @__PURE__ */ jsx("input", {
|
|
8086
|
+
...combo.inputProps(display, placeholder),
|
|
8087
|
+
...invalid,
|
|
8088
|
+
className: `sbk-select__input${overlay ? " sbk-select__input--behind" : ""}`
|
|
8089
|
+
}),
|
|
8090
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" }),
|
|
8091
|
+
overlay && /* @__PURE__ */ jsx("span", {
|
|
8092
|
+
className: "sbk-select__content",
|
|
8093
|
+
"aria-hidden": "true",
|
|
8094
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
8095
|
+
className: "sbk-select__content-text",
|
|
8096
|
+
children: display
|
|
8097
|
+
})
|
|
8098
|
+
})
|
|
8099
|
+
]
|
|
8100
|
+
}),
|
|
8101
|
+
menu,
|
|
8102
|
+
dialog
|
|
8103
|
+
]
|
|
8104
|
+
});
|
|
8105
|
+
}
|
|
7432
8106
|
return /* @__PURE__ */ jsxs("div", {
|
|
7433
8107
|
className: `sbk-select${multi ? " sbk-select--multi" : ""}${multi && items.length > 0 ? " sbk-select--chips" : ""}${sizeClass}`,
|
|
7434
8108
|
ref: rootRef,
|
|
7435
|
-
onKeyDown:
|
|
8109
|
+
onKeyDown: combo.onKeyDown,
|
|
7436
8110
|
children: [
|
|
7437
8111
|
multi && element.max_selected_items && /* @__PURE__ */ jsxs("p", {
|
|
7438
8112
|
className: "sbk-select__max-info",
|
|
@@ -7491,70 +8165,7 @@ function Select({ element, blockId }) {
|
|
|
7491
8165
|
children: placeholder
|
|
7492
8166
|
}), /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" })]
|
|
7493
8167
|
}),
|
|
7494
|
-
|
|
7495
|
-
anchorRef: rootRef,
|
|
7496
|
-
onDismiss: () => setOpen(false),
|
|
7497
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
7498
|
-
className: "sbk-select__menu",
|
|
7499
|
-
role: "listbox",
|
|
7500
|
-
ref: listRef,
|
|
7501
|
-
children: [
|
|
7502
|
-
(showSearchOnly || allOptions(element).length > 8) && /* @__PURE__ */ jsxs("div", {
|
|
7503
|
-
className: "sbk-select__search",
|
|
7504
|
-
children: [/* @__PURE__ */ jsx(SearchIcon$1, {}), /* @__PURE__ */ jsx("input", {
|
|
7505
|
-
autoFocus: true,
|
|
7506
|
-
className: "sbk-select__search-input",
|
|
7507
|
-
placeholder: source === "static" || source === "external" ? "Search options" : `Search ${source}`,
|
|
7508
|
-
value: query,
|
|
7509
|
-
onChange: (e) => setQuery(e.target.value),
|
|
7510
|
-
onKeyDown: (e) => {
|
|
7511
|
-
if (e.key === "Enter" && query.trim() && showSearchOnly && !remote) selectItem({
|
|
7512
|
-
id: query.trim(),
|
|
7513
|
-
label: resolveLabel(query.trim())
|
|
7514
|
-
});
|
|
7515
|
-
}
|
|
7516
|
-
})]
|
|
7517
|
-
}),
|
|
7518
|
-
(() => {
|
|
7519
|
-
let index = 0;
|
|
7520
|
-
return visibleGroups.map((group, gi) => {
|
|
7521
|
-
const rows = group.options.map((option) => {
|
|
7522
|
-
const i = index++;
|
|
7523
|
-
return /* @__PURE__ */ jsx(SelectOption, {
|
|
7524
|
-
option,
|
|
7525
|
-
selected: isSelected(option),
|
|
7526
|
-
onSelect: () => selectItem(optionToItem(option)),
|
|
7527
|
-
navProps: nav.itemProps(i)
|
|
7528
|
-
}, option.value ?? i);
|
|
7529
|
-
});
|
|
7530
|
-
return group.label === void 0 ? rows : /* @__PURE__ */ jsxs("div", {
|
|
7531
|
-
className: "sbk-select__group",
|
|
7532
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
7533
|
-
className: "sbk-select__group-label",
|
|
7534
|
-
children: group.label
|
|
7535
|
-
}), rows]
|
|
7536
|
-
}, gi);
|
|
7537
|
-
});
|
|
7538
|
-
})(),
|
|
7539
|
-
remote && loading && /* @__PURE__ */ jsx("div", {
|
|
7540
|
-
className: "sbk-select__status",
|
|
7541
|
-
children: "Loading…"
|
|
7542
|
-
}),
|
|
7543
|
-
remote && !loading && remoteGroups !== void 0 && flatOptions.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
7544
|
-
className: "sbk-select__status",
|
|
7545
|
-
children: "No results"
|
|
7546
|
-
}),
|
|
7547
|
-
showSearchOnly && items.length > 0 && multi && /* @__PURE__ */ jsx("div", {
|
|
7548
|
-
className: "sbk-select__group-label",
|
|
7549
|
-
children: "Selected"
|
|
7550
|
-
}),
|
|
7551
|
-
showSearchOnly && multi && items.map((item) => /* @__PURE__ */ jsx("div", {
|
|
7552
|
-
className: "sbk-select__option sbk-select__option--selected",
|
|
7553
|
-
children: item.label || resolveLabel(item.id)
|
|
7554
|
-
}, item.id))
|
|
7555
|
-
]
|
|
7556
|
-
})
|
|
7557
|
-
}),
|
|
8168
|
+
menu,
|
|
7558
8169
|
dialog
|
|
7559
8170
|
]
|
|
7560
8171
|
});
|
|
@@ -7656,28 +8267,47 @@ function TextInput({ element, blockId }) {
|
|
|
7656
8267
|
}
|
|
7657
8268
|
//#endregion
|
|
7658
8269
|
//#region src/elements/TimePicker.tsx
|
|
7659
|
-
/** Formats `HH:mm` the way Slack's
|
|
8270
|
+
/** Formats `HH:mm` the way Slack's timepicker shows it, e.g. "1:37 PM". */
|
|
7660
8271
|
function formatTime(time) {
|
|
7661
8272
|
const [h = 0, m = 0] = time.split(":").map(Number);
|
|
7662
8273
|
const period = h >= 12 ? "PM" : "AM";
|
|
7663
8274
|
return `${h % 12 === 0 ? 12 : h % 12}:${String(m).padStart(2, "0")} ${period}`;
|
|
7664
8275
|
}
|
|
7665
|
-
/** Every
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
8276
|
+
/** Every hour of the day, as Slack's timepicker list offers. */
|
|
8277
|
+
const HOURS = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, "0")}:00`);
|
|
8278
|
+
/**
|
|
8279
|
+
* Reads a typed time as `HH:mm`: "2:15 PM", "2:15pm", "2pm", "14:15" or "9". Without AM or PM the
|
|
8280
|
+
* hour is read on a 24-hour clock. Slack accepts typed times too; whether it keeps one off the hour
|
|
8281
|
+
* (2:15 PM) or rounds it hasn't been verified, so the minutes are kept as typed.
|
|
8282
|
+
*/
|
|
8283
|
+
function parseTime(input) {
|
|
8284
|
+
const match = /^(\d{1,2})(?::?(\d{2}))?\s*([ap])\.?m?\.?$|^(\d{1,2})(?::(\d{2}))?$/i.exec(input.trim());
|
|
8285
|
+
if (!match) return void 0;
|
|
8286
|
+
const meridiem = match[3]?.toLowerCase();
|
|
8287
|
+
let hour = Number(match[1] ?? match[4]);
|
|
8288
|
+
const minute = Number(match[2] ?? match[5] ?? 0);
|
|
8289
|
+
if (minute > 59) return void 0;
|
|
8290
|
+
if (meridiem) {
|
|
8291
|
+
if (hour < 1 || hour > 12) return void 0;
|
|
8292
|
+
hour = hour % 12 + (meridiem === "p" ? 12 : 0);
|
|
8293
|
+
} else if (hour > 23) return;
|
|
8294
|
+
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
8295
|
+
}
|
|
8296
|
+
const squash = (text) => text.toLowerCase().replace(/\s+/g, "");
|
|
8297
|
+
/** The rows for a query: the hours whose label starts with it, after a typed time the list lacks. */
|
|
8298
|
+
function timesFor(query) {
|
|
8299
|
+
if (!query.trim()) return HOURS;
|
|
8300
|
+
const typed = parseTime(query);
|
|
8301
|
+
const hours = HOURS.filter((t) => squash(formatTime(t)).startsWith(squash(query)));
|
|
8302
|
+
return typed && !hours.includes(typed) && !HOURS.includes(typed) ? [typed, ...hours] : hours;
|
|
7670
8303
|
}
|
|
7671
8304
|
function TimePicker({ element, blockId }) {
|
|
7672
8305
|
const { setValue, dispatch } = useBlockKit();
|
|
7673
8306
|
const { ask, dialog } = useConfirm(element.confirm);
|
|
7674
8307
|
const [time, setTime] = useState(element.initial_time);
|
|
7675
|
-
const [open, setOpen] = useState(false);
|
|
7676
8308
|
const rootRef = useRef(null);
|
|
7677
|
-
const triggerRef = useRef(null);
|
|
7678
|
-
useFocusOnLoad(element, triggerRef);
|
|
7679
|
-
const invalid = useInvalidProps();
|
|
7680
8309
|
const listRef = useRef(null);
|
|
8310
|
+
const invalid = useInvalidProps();
|
|
7681
8311
|
const actionId = element.action_id ?? "";
|
|
7682
8312
|
useEffect(() => {
|
|
7683
8313
|
if (time) setValue(blockId, actionId, {
|
|
@@ -7686,9 +8316,9 @@ function TimePicker({ element, blockId }) {
|
|
|
7686
8316
|
});
|
|
7687
8317
|
}, []);
|
|
7688
8318
|
async function pick(next) {
|
|
8319
|
+
combo.setOpen(false);
|
|
7689
8320
|
if (!await ask()) return;
|
|
7690
8321
|
setTime(next);
|
|
7691
|
-
setOpen(false);
|
|
7692
8322
|
setValue(blockId, actionId, {
|
|
7693
8323
|
type: "timepicker",
|
|
7694
8324
|
selected_time: next
|
|
@@ -7697,60 +8327,72 @@ function TimePicker({ element, blockId }) {
|
|
|
7697
8327
|
type: "timepicker",
|
|
7698
8328
|
action_id: actionId,
|
|
7699
8329
|
block_id: blockId,
|
|
7700
|
-
selected_time: next
|
|
8330
|
+
selected_time: next,
|
|
8331
|
+
...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
|
|
7701
8332
|
});
|
|
7702
8333
|
}
|
|
7703
|
-
const
|
|
7704
|
-
const
|
|
7705
|
-
|
|
8334
|
+
const [query, setQuery] = useState("");
|
|
8335
|
+
const times = timesFor(query);
|
|
8336
|
+
const combo = useCombobox({
|
|
8337
|
+
query,
|
|
8338
|
+
onQueryChange: setQuery,
|
|
7706
8339
|
count: times.length,
|
|
7707
8340
|
initialIndex: time ? times.indexOf(time) : -1,
|
|
7708
8341
|
onChoose: (i) => {
|
|
7709
8342
|
const t = times[i];
|
|
7710
8343
|
if (t) pick(t);
|
|
7711
8344
|
},
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
8345
|
+
onSubmitQuery: (typed) => {
|
|
8346
|
+
const t = parseTime(typed);
|
|
8347
|
+
if (t) pick(t);
|
|
7715
8348
|
},
|
|
7716
|
-
onOpen: () => setOpen(true),
|
|
7717
8349
|
listRef
|
|
7718
8350
|
});
|
|
8351
|
+
useFocusOnLoad(element, combo.inputRef);
|
|
8352
|
+
const display = time ? formatTime(time) : void 0;
|
|
8353
|
+
const input = combo.inputProps(display, element.placeholder?.text ?? "Select time");
|
|
8354
|
+
const overlay = display !== void 0 && !combo.open;
|
|
7719
8355
|
return /* @__PURE__ */ jsxs("div", {
|
|
7720
8356
|
className: "sbk-timepicker",
|
|
7721
8357
|
ref: rootRef,
|
|
7722
|
-
onKeyDown: nav.onKeyDown,
|
|
7723
8358
|
children: [
|
|
7724
|
-
/* @__PURE__ */ jsxs("
|
|
7725
|
-
ref: triggerRef,
|
|
7726
|
-
type: "button",
|
|
8359
|
+
/* @__PURE__ */ jsxs("label", {
|
|
7727
8360
|
className: "sbk-timepicker__control",
|
|
7728
|
-
onClick: () => setOpen((o) => !o),
|
|
7729
|
-
"aria-haspopup": "listbox",
|
|
7730
|
-
"aria-expanded": open,
|
|
7731
|
-
...invalid,
|
|
7732
8361
|
children: [
|
|
7733
8362
|
/* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
|
|
7734
|
-
/* @__PURE__ */ jsx("
|
|
7735
|
-
|
|
7736
|
-
|
|
8363
|
+
/* @__PURE__ */ jsx("input", {
|
|
8364
|
+
...input,
|
|
8365
|
+
...invalid,
|
|
8366
|
+
className: `sbk-timepicker__input${overlay ? " sbk-timepicker__input--behind" : ""}`
|
|
7737
8367
|
}),
|
|
7738
|
-
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-timepicker__chevron" })
|
|
8368
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-timepicker__chevron" }),
|
|
8369
|
+
overlay && /* @__PURE__ */ jsx("span", {
|
|
8370
|
+
className: "sbk-timepicker__content",
|
|
8371
|
+
"aria-hidden": "true",
|
|
8372
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
8373
|
+
className: "sbk-timepicker__content-text",
|
|
8374
|
+
children: display
|
|
8375
|
+
})
|
|
8376
|
+
})
|
|
7739
8377
|
]
|
|
7740
8378
|
}),
|
|
7741
|
-
open && /* @__PURE__ */ jsx(Popover, {
|
|
8379
|
+
combo.open && /* @__PURE__ */ jsx(Popover, {
|
|
7742
8380
|
anchorRef: rootRef,
|
|
7743
|
-
onDismiss: () => setOpen(false),
|
|
8381
|
+
onDismiss: () => combo.setOpen(false),
|
|
8382
|
+
offsetX: -12,
|
|
8383
|
+
gap: -4,
|
|
7744
8384
|
children: /* @__PURE__ */ jsx("div", {
|
|
7745
8385
|
className: "sbk-timepicker__menu",
|
|
7746
8386
|
role: "listbox",
|
|
8387
|
+
id: combo.listId,
|
|
7747
8388
|
ref: listRef,
|
|
7748
8389
|
children: times.map((t, i) => /* @__PURE__ */ jsx("div", {
|
|
7749
8390
|
role: "option",
|
|
7750
8391
|
"aria-selected": t === time,
|
|
7751
8392
|
className: `sbk-timepicker__option${t === time ? " sbk-timepicker__option--selected" : ""}`,
|
|
8393
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
7752
8394
|
onClick: () => pick(t),
|
|
7753
|
-
...
|
|
8395
|
+
...combo.optionProps(i),
|
|
7754
8396
|
children: formatTime(t)
|
|
7755
8397
|
}, t))
|
|
7756
8398
|
})
|
|
@@ -9603,15 +10245,18 @@ function Input({ block, blockId }) {
|
|
|
9603
10245
|
className: "sbk-input__element",
|
|
9604
10246
|
children: /* @__PURE__ */ jsx(InputBlockContext.Provider, {
|
|
9605
10247
|
value: true,
|
|
9606
|
-
children: /* @__PURE__ */ jsx(
|
|
9607
|
-
value:
|
|
9608
|
-
children:
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
10248
|
+
children: /* @__PURE__ */ jsx(InputOptionalContext.Provider, {
|
|
10249
|
+
value: block.optional === true,
|
|
10250
|
+
children: /* @__PURE__ */ jsx(InputErrorContext.Provider, {
|
|
10251
|
+
value: error ? errorId : void 0,
|
|
10252
|
+
children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
|
|
10253
|
+
element,
|
|
10254
|
+
blockId
|
|
10255
|
+
}) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
|
|
10256
|
+
element,
|
|
10257
|
+
blockId
|
|
10258
|
+
}) })
|
|
10259
|
+
})
|
|
9615
10260
|
})
|
|
9616
10261
|
})
|
|
9617
10262
|
}),
|
|
@@ -10050,6 +10695,7 @@ function TaskRow({ task }) {
|
|
|
10050
10695
|
const expandable = Boolean(task.details || task.output || sources.length > 0);
|
|
10051
10696
|
return /* @__PURE__ */ jsxs("li", {
|
|
10052
10697
|
className: "sbk-plan__task",
|
|
10698
|
+
tabIndex: 0,
|
|
10053
10699
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
10054
10700
|
className: "sbk-plan__step",
|
|
10055
10701
|
"aria-hidden": "true",
|
|
@@ -10500,9 +11146,12 @@ function Section({ block, blockId }) {
|
|
|
10500
11146
|
})]
|
|
10501
11147
|
}), accessory && /* @__PURE__ */ jsx("div", {
|
|
10502
11148
|
className: `sbk-section__accessory sbk-section__accessory--${accessory.type}`,
|
|
10503
|
-
children: /* @__PURE__ */ jsx(
|
|
10504
|
-
|
|
10505
|
-
|
|
11149
|
+
children: /* @__PURE__ */ jsx(SectionAccessoryContext.Provider, {
|
|
11150
|
+
value: true,
|
|
11151
|
+
children: /* @__PURE__ */ jsx(Element, {
|
|
11152
|
+
element: accessory,
|
|
11153
|
+
blockId
|
|
11154
|
+
})
|
|
10506
11155
|
})
|
|
10507
11156
|
})]
|
|
10508
11157
|
});
|