@nkootstra/block-kit 0.3.2 → 0.3.4

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
@@ -6325,6 +6325,19 @@ const InputBlockContext = createContext(false);
6325
6325
  function useInInputBlock() {
6326
6326
  return useContext(InputBlockContext);
6327
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
+ }
6328
6341
  //#endregion
6329
6342
  //#region src/elements/Popover.tsx
6330
6343
  /** Space between the anchor and the popover, as in Slack's menus. */
@@ -6419,6 +6432,7 @@ function DatePicker({ element, blockId }) {
6419
6432
  const [open, setOpen] = useState(false);
6420
6433
  const rootRef = useRef(null);
6421
6434
  const inputRef = useFocusOnLoad(element);
6435
+ const invalid = useInvalidProps();
6422
6436
  const loadFocus = useRef(element.focus_on_load === true);
6423
6437
  const actionId = element.action_id ?? "";
6424
6438
  useEffect(() => {
@@ -6472,6 +6486,7 @@ function DatePicker({ element, blockId }) {
6472
6486
  placeholder: element.placeholder?.text ?? "Select a date",
6473
6487
  "aria-haspopup": "dialog",
6474
6488
  "aria-expanded": open,
6489
+ ...invalid,
6475
6490
  ref: inputRef,
6476
6491
  onFocus: () => {
6477
6492
  if (loadFocus.current) loadFocus.current = false;
@@ -7044,19 +7059,26 @@ const FORMATTING_GROUPS = [
7044
7059
  }
7045
7060
  ]
7046
7061
  ];
7047
- /** The (inert) formatting toolbar Slack shows above a modal's rich-text editor; its buttons stay
7048
- * dimmed because they only act on a selection in Slack's own editor. */
7062
+ /** The formatting bar Slack shows above a modal's rich-text editor. The editor here is plain text,
7063
+ * so its buttons are there to look like Slack's but marked unavailable; they dim until the editor
7064
+ * has focus, as Slack's do. */
7049
7065
  function RichTextToolbar() {
7050
7066
  return /* @__PURE__ */ jsx("div", {
7051
7067
  className: "sbk-rich-text-input__toolbar",
7052
- "aria-hidden": "true",
7053
- children: FORMATTING_GROUPS.map((group, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && /* @__PURE__ */ jsx("span", { className: "sbk-rich-text-input__separator" }), group.map((icon) => /* @__PURE__ */ jsx("span", {
7068
+ role: "toolbar",
7069
+ "aria-orientation": "horizontal",
7070
+ "aria-label": "Formatting",
7071
+ children: FORMATTING_GROUPS.map((group, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && /* @__PURE__ */ jsx("span", { className: "sbk-rich-text-input__separator" }), group.map((icon) => /* @__PURE__ */ jsx("button", {
7072
+ type: "button",
7054
7073
  className: "sbk-rich-text-input__tool",
7055
- title: icon.label,
7074
+ "aria-label": icon.label,
7075
+ "aria-disabled": "true",
7076
+ tabIndex: -1,
7056
7077
  children: /* @__PURE__ */ jsx("svg", {
7057
7078
  viewBox: "0 0 20 20",
7058
7079
  width: "18",
7059
7080
  height: "18",
7081
+ "aria-hidden": "true",
7060
7082
  children: /* @__PURE__ */ jsx("path", {
7061
7083
  fill: "currentColor",
7062
7084
  fillRule: icon.evenOdd ? "evenodd" : void 0,
@@ -7067,11 +7089,52 @@ function RichTextToolbar() {
7067
7089
  }, icon.label))] }, group[0]?.label))
7068
7090
  });
7069
7091
  }
7092
+ /** Slack's "Aa" (show formatting) and emoji glyphs, 20-unit paths as Slack ships them. */
7093
+ const FORMATTING_ICON = "M6.941 3.952c-.459-1.378-2.414-1.363-2.853.022l-4.053 12.8a.75.75 0 0 0 1.43.452l1.101-3.476h6.06l1.163 3.487a.75.75 0 1 0 1.423-.474zm1.185 8.298L5.518 4.427 3.041 12.25zm6.198-5.537a4.74 4.74 0 0 1 3.037-.081A3.74 3.74 0 0 1 20 10.208V17a.75.75 0 0 1-1.5 0v-.745a8 8 0 0 1-2.847 1.355 3 3 0 0 1-3.15-1.143C10.848 14.192 12.473 11 15.287 11H18.5v-.792c0-.984-.641-1.853-1.581-2.143a3.24 3.24 0 0 0-2.077.056l-.242.089a2.22 2.22 0 0 0-1.34 1.382l-.048.145a.75.75 0 0 1-1.423-.474l.048-.145a3.72 3.72 0 0 1 2.244-2.315zM18.5 12.5h-3.213c-1.587 0-2.504 1.801-1.57 3.085.357.491.98.717 1.572.57a6.5 6.5 0 0 0 2.47-1.223l.741-.593z";
7094
+ const EMOJI_ICON = "M2.5 10a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1-15 0M10 1a9 9 0 1 0 0 18 9 9 0 0 0 0-18M7.5 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M14 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m-6.385 3.766a.75.75 0 1 0-1.425.468C6.796 14.08 8.428 15 10.027 15s3.23-.92 3.838-2.766a.75.75 0 1 0-1.425-.468c-.38 1.155-1.38 1.734-2.413 1.734s-2.032-.58-2.412-1.734";
7095
+ function Glyph({ d }) {
7096
+ return /* @__PURE__ */ jsx("svg", {
7097
+ viewBox: "0 0 20 20",
7098
+ width: "18",
7099
+ height: "18",
7100
+ "aria-hidden": "true",
7101
+ children: /* @__PURE__ */ jsx("path", {
7102
+ fill: "currentColor",
7103
+ fillRule: "evenodd",
7104
+ clipRule: "evenodd",
7105
+ d
7106
+ })
7107
+ });
7108
+ }
7109
+ /** The row Slack draws under the editor: "Aa" shows or hides the formatting bar (underlined while
7110
+ * it's shown), and an emoji button, unavailable until block-kit has an emoji picker. */
7111
+ function RichTextComposerActions({ formatting, onToggleFormatting }) {
7112
+ return /* @__PURE__ */ jsxs("div", {
7113
+ className: "sbk-rich-text-input__footer",
7114
+ role: "toolbar",
7115
+ "aria-orientation": "horizontal",
7116
+ "aria-label": "Composer actions",
7117
+ children: [/* @__PURE__ */ jsx("button", {
7118
+ type: "button",
7119
+ className: "sbk-rich-text-input__action sbk-rich-text-input__action--formatting",
7120
+ "aria-label": "Show formatting",
7121
+ "aria-pressed": formatting,
7122
+ onClick: onToggleFormatting,
7123
+ children: /* @__PURE__ */ jsx(Glyph, { d: FORMATTING_ICON })
7124
+ }), /* @__PURE__ */ jsx("button", {
7125
+ type: "button",
7126
+ className: "sbk-rich-text-input__action",
7127
+ "aria-label": "Emoji",
7128
+ "aria-disabled": "true",
7129
+ tabIndex: -1,
7130
+ children: /* @__PURE__ */ jsx(Glyph, { d: EMOJI_ICON })
7131
+ })]
7132
+ });
7133
+ }
7070
7134
  //#endregion
7071
7135
  //#region src/elements/RichTextInput.tsx
7072
- /** Wraps plain text into the `rich_text` block shape Slack uses for this element's value. A
7073
- * full WYSIWYG (bold/lists/links) is out of scope; the brief allows a contentEditable here, under
7074
- * Slack's (inert) formatting toolbar. */
7136
+ /** Wraps plain text into the `rich_text` block shape Slack uses for this element's value. The
7137
+ * editor is plain text; Slack's formatting bar and composer row are drawn around it for parity. */
7075
7138
  function toRichText(text) {
7076
7139
  return {
7077
7140
  type: "rich_text",
@@ -7097,8 +7160,11 @@ function RichTextInput({ element, blockId }) {
7097
7160
  const { setValue, dispatch } = useBlockKit();
7098
7161
  const ref = useRef(null);
7099
7162
  useFocusOnLoad(element, ref);
7163
+ const invalid = useInvalidProps();
7100
7164
  const actionId = element.action_id ?? "";
7101
7165
  const initialText = fromRichText(element.initial_value);
7166
+ const [formatting, setFormatting] = useState(true);
7167
+ const [empty, setEmpty] = useState(initialText === "");
7102
7168
  useEffect(() => {
7103
7169
  if (element.initial_value) setValue(blockId, actionId, {
7104
7170
  type: "rich_text_input",
@@ -7106,7 +7172,9 @@ function RichTextInput({ element, blockId }) {
7106
7172
  });
7107
7173
  }, []);
7108
7174
  function onInput() {
7109
- const richText = toRichText(ref.current?.innerText ?? "");
7175
+ const text = ref.current?.innerText ?? "";
7176
+ setEmpty(text.trim() === "");
7177
+ const richText = toRichText(text);
7110
7178
  setValue(blockId, actionId, {
7111
7179
  type: "rich_text_input",
7112
7180
  rich_text_value: richText
@@ -7118,24 +7186,39 @@ function RichTextInput({ element, blockId }) {
7118
7186
  rich_text_value: richText
7119
7187
  });
7120
7188
  }
7189
+ const placeholder = element.placeholder?.text;
7121
7190
  return /* @__PURE__ */ jsxs("div", {
7122
7191
  className: "sbk-rich-text-input",
7123
- children: [/* @__PURE__ */ jsx(RichTextToolbar, {}), /* @__PURE__ */ jsx("div", {
7124
- ref,
7125
- className: "sbk-rich-text-input__editor",
7126
- contentEditable: true,
7127
- suppressContentEditableWarning: true,
7128
- role: "textbox",
7129
- "aria-multiline": "true",
7130
- "aria-label": element.placeholder?.text ?? actionId,
7131
- "data-placeholder": element.placeholder?.text,
7132
- onInput,
7133
- style: {
7134
- minHeight: element.min_lines ? `${element.min_lines * 22}px` : void 0,
7135
- maxHeight: element.max_lines ? `${element.max_lines * 22}px` : void 0
7136
- },
7137
- children: initialText
7138
- })]
7192
+ children: [
7193
+ formatting && /* @__PURE__ */ jsx(RichTextToolbar, {}),
7194
+ /* @__PURE__ */ jsxs("div", {
7195
+ className: "sbk-rich-text-input__body",
7196
+ children: [/* @__PURE__ */ jsx("div", {
7197
+ ref,
7198
+ className: "sbk-rich-text-input__editor",
7199
+ contentEditable: true,
7200
+ suppressContentEditableWarning: true,
7201
+ role: "textbox",
7202
+ "aria-multiline": "true",
7203
+ ...invalid,
7204
+ "aria-label": placeholder ?? actionId,
7205
+ onInput,
7206
+ style: {
7207
+ minHeight: element.min_lines ? `${element.min_lines * 22}px` : void 0,
7208
+ maxHeight: element.max_lines ? `${element.max_lines * 22}px` : void 0
7209
+ },
7210
+ children: initialText
7211
+ }), placeholder && empty && /* @__PURE__ */ jsx("div", {
7212
+ className: "sbk-rich-text-input__placeholder",
7213
+ "aria-hidden": "true",
7214
+ children: placeholder
7215
+ })]
7216
+ }),
7217
+ /* @__PURE__ */ jsx(RichTextComposerActions, {
7218
+ formatting,
7219
+ onToggleFormatting: () => setFormatting((shown) => !shown)
7220
+ })
7221
+ ]
7139
7222
  });
7140
7223
  }
7141
7224
  //#endregion
@@ -7209,6 +7292,7 @@ function Select({ element, blockId }) {
7209
7292
  const rootRef = useRef(null);
7210
7293
  const triggerRef = useRef(null);
7211
7294
  useFocusOnLoad(element, triggerRef);
7295
+ const invalid = useInvalidProps();
7212
7296
  const listRef = useRef(null);
7213
7297
  const remote = source === "external" && loadOptions !== void 0;
7214
7298
  const minQueryLength = element.min_query_length ?? DEFAULT_MIN_QUERY_LENGTH;
@@ -7367,6 +7451,7 @@ function Select({ element, blockId }) {
7367
7451
  onClick: () => setOpen((o) => !o),
7368
7452
  "aria-haspopup": "listbox",
7369
7453
  "aria-expanded": open,
7454
+ ...invalid,
7370
7455
  children: [multi ? items.length > 0 ? /* @__PURE__ */ jsx("span", {
7371
7456
  className: "sbk-select__chips",
7372
7457
  children: items.map((item) => /* @__PURE__ */ jsxs("span", {
@@ -7386,6 +7471,12 @@ function Select({ element, blockId }) {
7386
7471
  e.stopPropagation();
7387
7472
  removeItem(item.id);
7388
7473
  },
7474
+ onKeyDown: (e) => {
7475
+ if (e.key !== "Enter" && e.key !== " ") return;
7476
+ e.preventDefault();
7477
+ e.stopPropagation();
7478
+ removeItem(item.id);
7479
+ },
7389
7480
  children: /* @__PURE__ */ jsx(CloseIcon, {})
7390
7481
  })]
7391
7482
  }, item.id))
@@ -7496,6 +7587,7 @@ function TextInput({ element, blockId }) {
7496
7587
  const { setValue, dispatch, surface } = useBlockKit();
7497
7588
  const sizeClass = surface === "message" ? "" : " sbk-text-input--medium";
7498
7589
  const [value, setLocalValue] = useState(element.initial_value ?? "");
7590
+ const invalid = useInvalidProps();
7499
7591
  const focusRef = useFocusOnLoad(element);
7500
7592
  const actionId = element.action_id ?? "";
7501
7593
  const dispatchEnabled = element.__dispatchAction === true;
@@ -7539,7 +7631,8 @@ function TextInput({ element, blockId }) {
7539
7631
  placeholder,
7540
7632
  onKeyDown,
7541
7633
  ref: focusRef,
7542
- "aria-label": element.action_id
7634
+ "aria-label": element.action_id,
7635
+ ...invalid
7543
7636
  };
7544
7637
  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;
7545
7638
  if (element.multiline) return /* @__PURE__ */ jsx("textarea", {
@@ -7583,6 +7676,7 @@ function TimePicker({ element, blockId }) {
7583
7676
  const rootRef = useRef(null);
7584
7677
  const triggerRef = useRef(null);
7585
7678
  useFocusOnLoad(element, triggerRef);
7679
+ const invalid = useInvalidProps();
7586
7680
  const listRef = useRef(null);
7587
7681
  const actionId = element.action_id ?? "";
7588
7682
  useEffect(() => {
@@ -7634,6 +7728,7 @@ function TimePicker({ element, blockId }) {
7634
7728
  onClick: () => setOpen((o) => !o),
7635
7729
  "aria-haspopup": "listbox",
7636
7730
  "aria-expanded": open,
7731
+ ...invalid,
7637
7732
  children: [
7638
7733
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
7639
7734
  /* @__PURE__ */ jsx("span", {
@@ -9491,6 +9586,7 @@ function Input({ block, blockId }) {
9491
9586
  __dispatchAction: block.dispatch_action === true
9492
9587
  };
9493
9588
  const error = errors[blockId];
9589
+ const errorId = useId();
9494
9590
  const showDispatchHint = dispatchHint(block);
9495
9591
  const showOptional = block.optional && surface !== "message";
9496
9592
  return /* @__PURE__ */ jsxs("div", {
@@ -9507,13 +9603,16 @@ function Input({ block, blockId }) {
9507
9603
  className: "sbk-input__element",
9508
9604
  children: /* @__PURE__ */ jsx(InputBlockContext.Provider, {
9509
9605
  value: true,
9510
- children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
9511
- element,
9512
- blockId
9513
- }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
9514
- element,
9515
- blockId
9516
- }) })
9606
+ children: /* @__PURE__ */ jsx(InputErrorContext.Provider, {
9607
+ value: error ? errorId : void 0,
9608
+ children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
9609
+ element,
9610
+ blockId
9611
+ }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
9612
+ element,
9613
+ blockId
9614
+ }) })
9615
+ })
9517
9616
  })
9518
9617
  }),
9519
9618
  block.hint && /* @__PURE__ */ jsx("div", {
@@ -9539,6 +9638,7 @@ function Input({ block, blockId }) {
9539
9638
  }),
9540
9639
  error && /* @__PURE__ */ jsx("div", {
9541
9640
  className: "sbk-input__error",
9641
+ id: errorId,
9542
9642
  children: error
9543
9643
  })
9544
9644
  ]
@@ -10079,7 +10179,23 @@ function renderTopLevel(elements, ctx) {
10079
10179
  i++;
10080
10180
  }
10081
10181
  const forest = buildListForest(run);
10082
- for (const group of forest) nodes.push(renderListGroup(group, ctx, key++, 0));
10182
+ const lists = forest.map((group) => renderListGroup(group, ctx, key++, 0));
10183
+ if (!forest.some((group) => group.border)) {
10184
+ nodes.push(...lists);
10185
+ continue;
10186
+ }
10187
+ const quoted = [];
10188
+ while (i < elements.length && elements[i]?.type === "rich_text_quote") {
10189
+ quoted.push(/* @__PURE__ */ jsx("div", {
10190
+ className: "sbk-rich-text__quote-text",
10191
+ children: renderSectionChildren(elements[i].elements, ctx)
10192
+ }, key++));
10193
+ i++;
10194
+ }
10195
+ nodes.push(/* @__PURE__ */ jsxs("blockquote", {
10196
+ className: "sbk-rich-text__quote sbk-rich-text__quote--group",
10197
+ children: [lists, quoted]
10198
+ }, key++));
10083
10199
  continue;
10084
10200
  }
10085
10201
  switch (el.type) {