@nkootstra/block-kit 0.3.1 → 0.3.3

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.js CHANGED
@@ -6285,6 +6285,39 @@ const TIME_ZONE_LABELS = {
6285
6285
  function timeZoneLabel(timeZone) {
6286
6286
  return TIME_ZONE_LABELS[timeZone] ?? timeZone;
6287
6287
  }
6288
+ /** A timestamp's wall-clock date ("2026-01-01") and time ("11:00") in `timeZone`. */
6289
+ function wallClock(ts, timeZone) {
6290
+ const parts = new Intl.DateTimeFormat("en-US", {
6291
+ timeZone,
6292
+ year: "numeric",
6293
+ month: "2-digit",
6294
+ day: "2-digit",
6295
+ hour: "2-digit",
6296
+ minute: "2-digit",
6297
+ hourCycle: "h23"
6298
+ }).formatToParts(/* @__PURE__ */ new Date(ts * 1e3));
6299
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
6300
+ return {
6301
+ date: `${get("year")}-${get("month")}-${get("day")}`,
6302
+ time: `${get("hour")}:${get("minute")}`
6303
+ };
6304
+ }
6305
+ /** The timestamp of a wall-clock date and time in `timeZone`. Around a daylight saving change it
6306
+ * resolves like JavaScript's `Date`: a time the clocks skip moves forward by the gap, and a time
6307
+ * they repeat takes its first occurrence. */
6308
+ function fromWallClock(date, time, timeZone) {
6309
+ const asUtc = Date.parse(`${date}T${time}:00Z`) / 1e3;
6310
+ const offsetAt = (ts) => {
6311
+ const wall = wallClock(ts, timeZone);
6312
+ return Date.parse(`${wall.date}T${wall.time}:00Z`) / 1e3 - ts;
6313
+ };
6314
+ const before = asUtc - offsetAt(asUtc - 86400);
6315
+ const matches = [before, asUtc - offsetAt(asUtc + 86400)].filter((ts) => {
6316
+ const wall = wallClock(ts, timeZone);
6317
+ return wall.date === date && wall.time === time;
6318
+ });
6319
+ return matches.length > 0 ? Math.min(...matches) : before;
6320
+ }
6288
6321
  //#endregion
6289
6322
  //#region src/elements/inputBlockContext.ts
6290
6323
  /** True while rendering the element of an `input` block, where Slack lays some elements out differently. */
@@ -6292,6 +6325,19 @@ const InputBlockContext = createContext(false);
6292
6325
  function useInInputBlock() {
6293
6326
  return useContext(InputBlockContext);
6294
6327
  }
6328
+ /** The id of the error shown under an `input` block's element, while it has one. */
6329
+ const InputErrorContext = createContext(void 0);
6330
+ /**
6331
+ * Props for the element's focusable control: an `input` block with an error marks it invalid,
6332
+ * which also gives it Slack's red focus ring, and describes it with the error text.
6333
+ */
6334
+ function useInvalidProps() {
6335
+ const errorId = useContext(InputErrorContext);
6336
+ return errorId ? {
6337
+ "aria-invalid": true,
6338
+ "aria-describedby": errorId
6339
+ } : {};
6340
+ }
6295
6341
  //#endregion
6296
6342
  //#region src/elements/Popover.tsx
6297
6343
  /** Space between the anchor and the popover, as in Slack's menus. */
@@ -6386,6 +6432,7 @@ function DatePicker({ element, blockId }) {
6386
6432
  const [open, setOpen] = useState(false);
6387
6433
  const rootRef = useRef(null);
6388
6434
  const inputRef = useFocusOnLoad(element);
6435
+ const invalid = useInvalidProps();
6389
6436
  const loadFocus = useRef(element.focus_on_load === true);
6390
6437
  const actionId = element.action_id ?? "";
6391
6438
  useEffect(() => {
@@ -6439,6 +6486,7 @@ function DatePicker({ element, blockId }) {
6439
6486
  placeholder: element.placeholder?.text ?? "Select a date",
6440
6487
  "aria-haspopup": "dialog",
6441
6488
  "aria-expanded": open,
6489
+ ...invalid,
6442
6490
  ref: inputRef,
6443
6491
  onFocus: () => {
6444
6492
  if (loadFocus.current) loadFocus.current = false;
@@ -6470,16 +6518,6 @@ function DatePicker({ element, blockId }) {
6470
6518
  }
6471
6519
  //#endregion
6472
6520
  //#region src/elements/DateTimePicker.tsx
6473
- function toISODate(ts) {
6474
- return (/* @__PURE__ */ new Date(ts * 1e3)).toISOString().slice(0, 10);
6475
- }
6476
- function toHM(ts) {
6477
- const d = /* @__PURE__ */ new Date(ts * 1e3);
6478
- return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
6479
- }
6480
- function combine(date, time) {
6481
- return Math.floor((/* @__PURE__ */ new Date(`${date}T${time}:00Z`)).getTime() / 1e3);
6482
- }
6483
6521
  /** Slack's Builder splits a datetimepicker's closed control into a date box ("January 1st,
6484
6522
  * 2026") and a separate time box ("11:00 AM"), rather than one combined string. */
6485
6523
  function formatDate(ts, timeZone) {
@@ -6488,7 +6526,7 @@ function formatDate(ts, timeZone) {
6488
6526
  month: "long",
6489
6527
  day: "numeric",
6490
6528
  year: "numeric",
6491
- timeZone: timeZone ?? "UTC"
6529
+ timeZone
6492
6530
  }).formatToParts(d);
6493
6531
  const month = parts.find((p) => p.type === "month")?.value ?? "";
6494
6532
  const day = Number(parts.find((p) => p.type === "day")?.value ?? "1");
@@ -6499,7 +6537,7 @@ function formatTime$1(ts, timeZone) {
6499
6537
  return (/* @__PURE__ */ new Date(ts * 1e3)).toLocaleString("en-US", {
6500
6538
  hour: "numeric",
6501
6539
  minute: "2-digit",
6502
- timeZone: timeZone ?? "UTC"
6540
+ timeZone
6503
6541
  });
6504
6542
  }
6505
6543
  function DateTimePicker({ element, blockId }) {
@@ -6507,8 +6545,10 @@ function DateTimePicker({ element, blockId }) {
6507
6545
  const { ask, dialog } = useConfirm(element.confirm);
6508
6546
  const [value, setValueState] = useState(element.initial_date_time);
6509
6547
  const [open, setOpen] = useState(false);
6510
- const [draftDate, setDraftDate] = useState(element.initial_date_time ? toISODate(element.initial_date_time) : void 0);
6511
- const [draftTime, setDraftTime] = useState(element.initial_date_time ? toHM(element.initial_date_time) : "12:00");
6548
+ const zone = timeZone ?? "UTC";
6549
+ const initial = element.initial_date_time !== void 0 ? wallClock(element.initial_date_time, zone) : void 0;
6550
+ const [draftDate, setDraftDate] = useState(initial?.date);
6551
+ const [draftTime, setDraftTime] = useState(initial?.time ?? "12:00");
6512
6552
  const rootRef = useRef(null);
6513
6553
  const focusRef = useFocusOnLoad(element);
6514
6554
  const actionId = element.action_id ?? "";
@@ -6521,7 +6561,7 @@ function DateTimePicker({ element, blockId }) {
6521
6561
  async function apply() {
6522
6562
  if (!draftDate) return;
6523
6563
  if (!await ask()) return;
6524
- const ts = combine(draftDate, draftTime);
6564
+ const ts = fromWallClock(draftDate, draftTime, zone);
6525
6565
  setValueState(ts);
6526
6566
  setOpen(false);
6527
6567
  setValue(blockId, actionId, {
@@ -6552,7 +6592,7 @@ function DateTimePicker({ element, blockId }) {
6552
6592
  /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datetimepicker__icon" }),
6553
6593
  /* @__PURE__ */ jsx("span", {
6554
6594
  className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6555
- children: value !== void 0 ? formatDate(value, timeZone) : "Select a date"
6595
+ children: value !== void 0 ? formatDate(value, zone) : "Select a date"
6556
6596
  }),
6557
6597
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6558
6598
  ]
@@ -6567,13 +6607,13 @@ function DateTimePicker({ element, blockId }) {
6567
6607
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-datetimepicker__icon" }),
6568
6608
  /* @__PURE__ */ jsx("span", {
6569
6609
  className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6570
- children: value !== void 0 ? formatTime$1(value, timeZone) : "Select a time"
6610
+ children: value !== void 0 ? formatTime$1(value, zone) : "Select a time"
6571
6611
  }),
6572
6612
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6573
6613
  ]
6574
6614
  }), /* @__PURE__ */ jsxs("p", {
6575
6615
  className: "sbk-datetimepicker__timezone",
6576
- children: ["Time zone: ", timeZoneLabel(timeZone ?? "UTC")]
6616
+ children: ["Time zone: ", timeZoneLabel(zone)]
6577
6617
  })]
6578
6618
  })]
6579
6619
  }),
@@ -7072,6 +7112,7 @@ function RichTextInput({ element, blockId }) {
7072
7112
  const { setValue, dispatch } = useBlockKit();
7073
7113
  const ref = useRef(null);
7074
7114
  useFocusOnLoad(element, ref);
7115
+ const invalid = useInvalidProps();
7075
7116
  const actionId = element.action_id ?? "";
7076
7117
  const initialText = fromRichText(element.initial_value);
7077
7118
  useEffect(() => {
@@ -7102,6 +7143,7 @@ function RichTextInput({ element, blockId }) {
7102
7143
  suppressContentEditableWarning: true,
7103
7144
  role: "textbox",
7104
7145
  "aria-multiline": "true",
7146
+ ...invalid,
7105
7147
  "aria-label": element.placeholder?.text ?? actionId,
7106
7148
  "data-placeholder": element.placeholder?.text,
7107
7149
  onInput,
@@ -7184,6 +7226,7 @@ function Select({ element, blockId }) {
7184
7226
  const rootRef = useRef(null);
7185
7227
  const triggerRef = useRef(null);
7186
7228
  useFocusOnLoad(element, triggerRef);
7229
+ const invalid = useInvalidProps();
7187
7230
  const listRef = useRef(null);
7188
7231
  const remote = source === "external" && loadOptions !== void 0;
7189
7232
  const minQueryLength = element.min_query_length ?? DEFAULT_MIN_QUERY_LENGTH;
@@ -7342,6 +7385,7 @@ function Select({ element, blockId }) {
7342
7385
  onClick: () => setOpen((o) => !o),
7343
7386
  "aria-haspopup": "listbox",
7344
7387
  "aria-expanded": open,
7388
+ ...invalid,
7345
7389
  children: [multi ? items.length > 0 ? /* @__PURE__ */ jsx("span", {
7346
7390
  className: "sbk-select__chips",
7347
7391
  children: items.map((item) => /* @__PURE__ */ jsxs("span", {
@@ -7361,6 +7405,12 @@ function Select({ element, blockId }) {
7361
7405
  e.stopPropagation();
7362
7406
  removeItem(item.id);
7363
7407
  },
7408
+ onKeyDown: (e) => {
7409
+ if (e.key !== "Enter" && e.key !== " ") return;
7410
+ e.preventDefault();
7411
+ e.stopPropagation();
7412
+ removeItem(item.id);
7413
+ },
7364
7414
  children: /* @__PURE__ */ jsx(CloseIcon, {})
7365
7415
  })]
7366
7416
  }, item.id))
@@ -7471,6 +7521,7 @@ function TextInput({ element, blockId }) {
7471
7521
  const { setValue, dispatch, surface } = useBlockKit();
7472
7522
  const sizeClass = surface === "message" ? "" : " sbk-text-input--medium";
7473
7523
  const [value, setLocalValue] = useState(element.initial_value ?? "");
7524
+ const invalid = useInvalidProps();
7474
7525
  const focusRef = useFocusOnLoad(element);
7475
7526
  const actionId = element.action_id ?? "";
7476
7527
  const dispatchEnabled = element.__dispatchAction === true;
@@ -7514,7 +7565,8 @@ function TextInput({ element, blockId }) {
7514
7565
  placeholder,
7515
7566
  onKeyDown,
7516
7567
  ref: focusRef,
7517
- "aria-label": element.action_id
7568
+ "aria-label": element.action_id,
7569
+ ...invalid
7518
7570
  };
7519
7571
  const leadingIcon = element.type === "email_text_input" ? /* @__PURE__ */ jsx(EmailIcon, { className: "sbk-text-input__icon" }) : element.type === "url_text_input" ? /* @__PURE__ */ jsx(LinkGlyphIcon, { className: "sbk-text-input__icon" }) : null;
7520
7572
  if (element.multiline) return /* @__PURE__ */ jsx("textarea", {
@@ -7558,6 +7610,7 @@ function TimePicker({ element, blockId }) {
7558
7610
  const rootRef = useRef(null);
7559
7611
  const triggerRef = useRef(null);
7560
7612
  useFocusOnLoad(element, triggerRef);
7613
+ const invalid = useInvalidProps();
7561
7614
  const listRef = useRef(null);
7562
7615
  const actionId = element.action_id ?? "";
7563
7616
  useEffect(() => {
@@ -7609,6 +7662,7 @@ function TimePicker({ element, blockId }) {
7609
7662
  onClick: () => setOpen((o) => !o),
7610
7663
  "aria-haspopup": "listbox",
7611
7664
  "aria-expanded": open,
7665
+ ...invalid,
7612
7666
  children: [
7613
7667
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
7614
7668
  /* @__PURE__ */ jsx("span", {
@@ -9466,6 +9520,7 @@ function Input({ block, blockId }) {
9466
9520
  __dispatchAction: block.dispatch_action === true
9467
9521
  };
9468
9522
  const error = errors[blockId];
9523
+ const errorId = useId();
9469
9524
  const showDispatchHint = dispatchHint(block);
9470
9525
  const showOptional = block.optional && surface !== "message";
9471
9526
  return /* @__PURE__ */ jsxs("div", {
@@ -9482,13 +9537,16 @@ function Input({ block, blockId }) {
9482
9537
  className: "sbk-input__element",
9483
9538
  children: /* @__PURE__ */ jsx(InputBlockContext.Provider, {
9484
9539
  value: true,
9485
- children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
9486
- element,
9487
- blockId
9488
- }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
9489
- element,
9490
- blockId
9491
- }) })
9540
+ children: /* @__PURE__ */ jsx(InputErrorContext.Provider, {
9541
+ value: error ? errorId : void 0,
9542
+ children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
9543
+ element,
9544
+ blockId
9545
+ }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
9546
+ element,
9547
+ blockId
9548
+ }) })
9549
+ })
9492
9550
  })
9493
9551
  }),
9494
9552
  block.hint && /* @__PURE__ */ jsx("div", {
@@ -9514,6 +9572,7 @@ function Input({ block, blockId }) {
9514
9572
  }),
9515
9573
  error && /* @__PURE__ */ jsx("div", {
9516
9574
  className: "sbk-input__error",
9575
+ id: errorId,
9517
9576
  children: error
9518
9577
  })
9519
9578
  ]