@wistia/ui 3.7.1 → 3.8.0-beta.0376af91.6c91cbd

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
@@ -1,6 +1,6 @@
1
1
 
2
2
  /*
3
- * @license @wistia/ui v3.7.1
3
+ * @license @wistia/ui v3.8.0-beta.0376af91.6c91cbd
4
4
  *
5
5
  * Copyright (c) 2024-2026, Wistia, Inc. and its affiliates.
6
6
  *
@@ -7156,6 +7156,12 @@ const Thumb$2 = styled(ScrollArea$1.Thumb)`
7156
7156
  height: ${THUMB_SIZE_PX}px;
7157
7157
  }
7158
7158
  `;
7159
+ const scrollbarVisibleCss = css`
7160
+ opacity: 1;
7161
+ pointer-events: auto;
7162
+ transition-delay: 0s;
7163
+ transition-duration: var(--wui-motion-duration-01);
7164
+ `;
7159
7165
  const Scrollbar = styled(ScrollArea$1.Scrollbar)`
7160
7166
  display: flex;
7161
7167
  opacity: 0;
@@ -7175,14 +7181,18 @@ const Scrollbar = styled(ScrollArea$1.Scrollbar)`
7175
7181
  height: ${SCROLLBAR_SIZE_PX}px;
7176
7182
  }
7177
7183
 
7178
- &[data-hovering],
7179
- &[data-scrolling] {
7180
- opacity: 1;
7181
- pointer-events: auto;
7182
- transition-delay: 0s;
7183
- transition-duration: var(--wui-motion-duration-01);
7184
+ /* a scrollbar under the pointer or being dragged stays put once it has appeared */
7185
+ &[data-scrolling],
7186
+ &:is(:hover, :active) {
7187
+ ${scrollbarVisibleCss}
7184
7188
  }
7185
7189
 
7190
+ ${({ $showOnHover }) => $showOnHover && css`
7191
+ &[data-hovering] {
7192
+ ${scrollbarVisibleCss}
7193
+ }
7194
+ `}
7195
+
7186
7196
  &:is(:hover, :active) > ${Thumb$2} {
7187
7197
  background-color: var(--wui-gray-alpha-9);
7188
7198
  }
@@ -7205,9 +7215,10 @@ const FILL_HEIGHT_CONTENT_STYLE = {
7205
7215
  /**
7206
7216
  * ScrollArea is a scrollable container with shadow or fade effects to indicate
7207
7217
  * scrollability. Its scrollbars overlay the content and only appear while the
7208
- * pointer is over the area or the content is being scrolled.
7218
+ * pointer is over the area or the content is being scrolled, or only while it
7219
+ * is being scrolled when `showScrollbarsOnHover` is false.
7209
7220
  */
7210
- const ScrollArea = ({ children, style, locked = false, fillHeight = false, overflowIndicator = "shadow", ref, ...props }) => {
7221
+ const ScrollArea = ({ children, style, locked = false, fillHeight = false, overflowIndicator = "shadow", showScrollbarsOnHover = true, ref, ...props }) => {
7211
7222
  return /* @__PURE__ */ jsxs(Root$1, {
7212
7223
  style,
7213
7224
  children: [
@@ -7230,10 +7241,12 @@ const ScrollArea = ({ children, style, locked = false, fillHeight = false, overf
7230
7241
  }),
7231
7242
  locked ? null : /* @__PURE__ */ jsxs(Fragment$1, { children: [
7232
7243
  /* @__PURE__ */ jsx(Scrollbar, {
7244
+ $showOnHover: showScrollbarsOnHover,
7233
7245
  orientation: "vertical",
7234
7246
  children: /* @__PURE__ */ jsx(Thumb$2, {})
7235
7247
  }),
7236
7248
  /* @__PURE__ */ jsx(Scrollbar, {
7249
+ $showOnHover: showScrollbarsOnHover,
7237
7250
  orientation: "horizontal",
7238
7251
  children: /* @__PURE__ */ jsx(Thumb$2, {})
7239
7252
  }),
@@ -13126,64 +13139,76 @@ const EditableHeading = ({ children, onValueChange, onEditingChange, tooltipText
13126
13139
  }) })] });
13127
13140
  };
13128
13141
  // src/components/EditableText/EditableTextRoot.tsx
13129
- const LARGE_PADDING = "var(--wui-space-02)";
13130
- const SMALL_PADDING = "var(--wui-space-01)";
13131
- const getPaddingForVariant = (variant) => {
13132
- const largePaddingVariants = [
13133
- "hero",
13134
- "heading1",
13135
- "heading2",
13136
- "heading3",
13137
- "body1",
13138
- "label1",
13139
- "body1Mono"
13140
- ];
13141
- const smallPaddingVariants = [
13142
- "heading4",
13143
- "heading5",
13144
- "heading6",
13145
- "body2",
13146
- "body3",
13147
- "body4",
13148
- "label2",
13149
- "label3",
13150
- "label4",
13151
- "body2Mono",
13152
- "body3Mono",
13153
- "body4Mono"
13154
- ];
13155
- if (largePaddingVariants.includes(variant)) return LARGE_PADDING;
13156
- if (smallPaddingVariants.includes(variant)) return SMALL_PADDING;
13157
- return SMALL_PADDING;
13158
- };
13142
+ const SMALL_PADDING = {
13143
+ block: "2px",
13144
+ inline: "var(--wui-space-01)"
13145
+ };
13146
+ const LARGE_PADDING = {
13147
+ block: "var(--wui-space-01)",
13148
+ inline: "var(--wui-space-02)"
13149
+ };
13150
+ const LARGE_PADDING_VARIANTS = /* @__PURE__ */ new Set([
13151
+ "hero",
13152
+ "heading1",
13153
+ "heading2",
13154
+ "heading3"
13155
+ ]);
13156
+ const getPaddingForVariant = (variant) => LARGE_PADDING_VARIANTS.has(variant) ? LARGE_PADDING : SMALL_PADDING;
13159
13157
  const StyledEditableTextRoot = styled.div`
13160
13158
  display: contents;
13161
13159
 
13162
- --wui-editable-text-padding: ${({ $typographicVariant }) => getPaddingForVariant($typographicVariant)};
13160
+ --wui-editable-text-padding-block: ${({ $typographicVariant }) => getPaddingForVariant($typographicVariant).block};
13161
+ --wui-editable-text-padding-inline: ${({ $typographicVariant }) => getPaddingForVariant($typographicVariant).inline};
13163
13162
  --wui-editable-text-border-radius: var(--wui-border-radius-01);
13163
+
13164
+ /* Negative margins that pull the display and input out by their padding, so the text
13165
+ * sits where it would without EditableText and doesn't move when editing starts. */
13166
+ --wui-editable-text-bleed-block: ${({ $bleed }) => $bleed ? "calc(var(--wui-editable-text-padding-block) * -1)" : "0px"};
13167
+ --wui-editable-text-bleed-inline: ${({ $bleed }) => $bleed ? "calc(var(--wui-editable-text-padding-inline) * -1)" : "0px"};
13164
13168
  `;
13165
13169
  const EditableTextContext = createContext(null);
13166
- const EditableTextRoot = ({ children, defaultValue = "", value: controlledValue, onValueChange, onValueCommit, onValueRevert, onEditingChange, typographicVariant = "body2", submitMode = "both", readOnly = false, id, label, placeholder = "Click to edit this text", minLines = 1, maxLines, finalFocusEl, ...props }) => {
13170
+ const getEditableTextState = (readOnly, isEditing) => {
13171
+ if (readOnly) return "read-only";
13172
+ if (isEditing) return "editing";
13173
+ return "idle";
13174
+ };
13175
+ /**
13176
+ * Holds the value (controlled or uncontrolled), the value to revert to and the editing state.
13177
+ */
13178
+ const useEditableTextState = ({ controlledValue, defaultValue, onValueChange, onEditingChange }) => {
13167
13179
  const isControlled = controlledValue !== void 0;
13168
13180
  const [internalValue, setInternalValue] = useState(defaultValue);
13169
13181
  const [originalValue, setOriginalValue] = useState(defaultValue);
13170
13182
  const [isEditing, setIsEditing] = useState(false);
13171
13183
  const value = isControlled ? controlledValue : internalValue;
13172
- const generatedId = useId();
13173
- const computedId = isNonEmptyString(id) ? id : `wistia-ui-editable-text-${generatedId}`;
13174
- const handleSetIsEditing = useCallback((editing) => {
13175
- if (editing && !isEditing) setOriginalValue(value);
13176
- setIsEditing(editing);
13177
- onEditingChange?.(editing);
13178
- }, [
13184
+ return {
13179
13185
  isEditing,
13186
+ handleSetIsEditing: useCallback((editing) => {
13187
+ if (editing && !isEditing) setOriginalValue(value);
13188
+ setIsEditing(editing);
13189
+ onEditingChange?.(editing);
13190
+ }, [
13191
+ isEditing,
13192
+ value,
13193
+ onEditingChange
13194
+ ]),
13180
13195
  value,
13196
+ setValue: useCallback((newValue) => {
13197
+ if (!isControlled) setInternalValue(newValue);
13198
+ onValueChange?.(newValue);
13199
+ }, [isControlled, onValueChange]),
13200
+ originalValue
13201
+ };
13202
+ };
13203
+ const EditableTextRoot = ({ children, defaultValue = "", value: controlledValue, onValueChange, onValueCommit, onValueRevert, onEditingChange, typographicVariant = "body2", submitMode = "both", readOnly = false, id, label, placeholder = "Click to edit this text", minLines = 1, maxLines, finalFocusEl, bleed = false, ...props }) => {
13204
+ const { isEditing, handleSetIsEditing, value, setValue, originalValue } = useEditableTextState({
13205
+ controlledValue,
13206
+ defaultValue,
13207
+ onValueChange,
13181
13208
  onEditingChange
13182
- ]);
13183
- const setValue = useCallback((newValue) => {
13184
- if (!isControlled) setInternalValue(newValue);
13185
- onValueChange?.(newValue);
13186
- }, [isControlled, onValueChange]);
13209
+ });
13210
+ const generatedId = useId();
13211
+ const computedId = isNonEmptyString(id) ? id : `wistia-ui-editable-text-${generatedId}`;
13187
13212
  const context = useMemo(() => {
13188
13213
  return {
13189
13214
  isEditing,
@@ -13221,16 +13246,13 @@ const EditableTextRoot = ({ children, defaultValue = "", value: controlledValue,
13221
13246
  maxLines,
13222
13247
  finalFocusEl
13223
13248
  ]);
13224
- const getState = () => {
13225
- if (readOnly) return "read-only";
13226
- if (isEditing) return "editing";
13227
- return "idle";
13228
- };
13229
13249
  return /* @__PURE__ */ jsx(StyledEditableTextRoot, {
13250
+ $bleed: bleed,
13230
13251
  $typographicVariant: typographicVariant,
13231
13252
  "data-testid": "editable-text-root",
13253
+ "data-wui-editable-text-bleed": bleed || void 0,
13232
13254
  "data-wui-editable-text-root": true,
13233
- "data-wui-editable-text-state": getState(),
13255
+ "data-wui-editable-text-state": getEditableTextState(readOnly, isEditing),
13234
13256
  ...props,
13235
13257
  children: /* @__PURE__ */ jsx(EditableTextContext, {
13236
13258
  value: context,
@@ -13240,9 +13262,11 @@ const EditableTextRoot = ({ children, defaultValue = "", value: controlledValue,
13240
13262
  };
13241
13263
  const PolymorphicStyledEditableTextDisplay = withPolymorphicAs(styled.div`
13242
13264
  ${({ $typographicVariant }) => getTypographicStyles($typographicVariant)}
13243
- padding: var(--wui-editable-text-padding);
13265
+ padding-block: var(--wui-editable-text-padding-block);
13266
+ padding-inline: var(--wui-editable-text-padding-inline);
13244
13267
  border-radius: var(--wui-editable-text-border-radius);
13245
- margin: 0;
13268
+ margin-block: var(--wui-editable-text-bleed-block, 0);
13269
+ margin-inline: var(--wui-editable-text-bleed-inline, 0);
13246
13270
  transition: all var(--wui-motion-duration-02) var(--wui-motion-ease);
13247
13271
  ${({ $maxLines }) => {
13248
13272
  if (isNotNil($maxLines)) return css`
@@ -13251,7 +13275,7 @@ const PolymorphicStyledEditableTextDisplay = withPolymorphicAs(styled.div`
13251
13275
  `;
13252
13276
  }}
13253
13277
  ${({ $minLines }) => isNotNil($minLines) && css`
13254
- min-height: calc(${$minLines}lh + calc(var(--wui-editable-text-padding) * 2));
13278
+ min-height: calc(${$minLines}lh + calc(var(--wui-editable-text-padding-block) * 2));
13255
13279
  `}
13256
13280
  word-break: normal;
13257
13281
  overflow-wrap: anywhere;
@@ -13315,22 +13339,97 @@ const EditableTextDisplayComponent = ({ ref, asTrigger, renderAs, ...props }) =>
13315
13339
  };
13316
13340
  const EditableTextDisplay = makePolymorphic(EditableTextDisplayComponent);
13317
13341
  // src/components/EditableText/EditableTextInput.tsx
13342
+ const StyledInputWrapper = styled.div`
13343
+ display: flex;
13344
+ min-width: 0;
13345
+
13346
+ & > [data-wui-input-container] {
13347
+ flex: 1 1 auto;
13348
+ min-width: 0;
13349
+ }
13350
+ `;
13318
13351
  const StyledInput = styled(Input)`
13319
13352
  && {
13320
- ${({ $minLines }) => isNotNil($minLines) && `min-height: calc(${$minLines}lh + calc(var(--wui-editable-text-padding) * 2));`}
13321
- ${({ $maxLines }) => isNotNil($maxLines) && `max-height: calc(${$maxLines}lh + calc(var(--wui-editable-text-padding) * 2));`}
13353
+ ${({ $minLines }) => isNotNil($minLines) && `min-height: calc(${$minLines}lh + calc(var(--wui-editable-text-padding-block) * 2));`}
13354
+ ${({ $maxLines }) => isNotNil($maxLines) && `max-height: calc(${$maxLines}lh + calc(var(--wui-editable-text-padding-block) * 2));`}
13322
13355
  ${({ $typographicVariant }) => getTypographicStyles($typographicVariant)}
13323
- background-color: var(--wui-color-bg-surface);
13356
+ /* No fill: the text sits on the page as it does when reading, and the ring marks the field */
13357
+ background-color: transparent;
13358
+
13359
+ /* With no fill of its own, the text takes the surrounding color and alignment, like the display */
13360
+ color: inherit;
13361
+ text-align: inherit;
13324
13362
  border-radius: var(--wui-editable-text-border-radius);
13325
- padding: var(--wui-editable-text-padding);
13363
+ padding-block: var(--wui-editable-text-padding-block);
13364
+ padding-inline: var(--wui-editable-text-padding-inline);
13365
+ margin-block: var(--wui-editable-text-bleed-block, 0);
13366
+ margin-inline: var(--wui-editable-text-bleed-inline, 0);
13367
+ width: calc(100% - (var(--wui-editable-text-bleed-inline, 0px) * 2));
13326
13368
  resize: none;
13369
+
13370
+ /*
13371
+ * Size to the text, like the display, instead of the textarea's default 20-column width.
13372
+ * Otherwise the field changes width when editing starts wherever its parent shrink-wraps.
13373
+ */
13374
+ field-sizing: content;
13375
+
13376
+ /*
13377
+ * maxLines={1}: behave like a single-line text input. The value stays on one line and
13378
+ * scrolls sideways instead of wrapping and scrolling vertically inside a one-line box.
13379
+ */
13380
+ ${({ $maxLines }) => $maxLines === 1 && css`
13381
+ white-space: pre;
13382
+ overflow-x: auto;
13383
+ overflow-y: hidden;
13384
+ scrollbar-width: none;
13385
+
13386
+ &::-webkit-scrollbar {
13387
+ display: none;
13388
+ }
13389
+ `}
13390
+
13391
+ /* Same color as the display's placeholder, so it doesn't change when editing starts */
13392
+ &::placeholder {
13393
+ color: var(--wui-color-text-secondary);
13394
+ }
13395
+
13396
+ /* Full-perimeter focus ring, inset like Input's so an overflow-hidden ancestor can't clip it */
13397
+ &:focus {
13398
+ outline: 2px solid var(--wui-color-focus-ring);
13399
+ outline-offset: -2px;
13400
+ }
13401
+
13402
+ /*
13403
+ * With submitMode "enter" or "none" the field stays open when focus moves to the
13404
+ * save/cancel buttons. Keep a visible 1px boundary so it still meets non-text contrast.
13405
+ */
13406
+ &:not(:focus) {
13407
+ outline: 1px solid var(--wui-color-border-secondary);
13408
+ outline-offset: -1px;
13409
+ }
13327
13410
  }
13328
13411
  `;
13329
- const EditableTextInput = (props) => {
13412
+ const KEYBOARD_HINTS = {
13413
+ both: "Press Enter to save or Escape to cancel. Changes also save when you leave the field.",
13414
+ enter: "Press Enter to save or Escape to cancel.",
13415
+ blur: "Changes save when you leave the field. Press Escape to cancel.",
13416
+ none: "Press Escape to cancel."
13417
+ };
13418
+ /** Whether this key press submits: Enter without Shift, in a submit mode that uses Enter */
13419
+ const isCommitEnter = (event, submitMode) => event.key === "Enter" && !event.shiftKey && (submitMode === "enter" || submitMode === "both");
13420
+ /** In a single-line field, scroll sideways so the end of the value (and a caret there) is visible */
13421
+ const scrollToEnd = (element) => {
13422
+ element.scrollLeft = element.scrollWidth;
13423
+ };
13424
+ const EditableTextInput = ({ autoSelect = false, ...props }) => {
13330
13425
  const context = useContext(EditableTextContext);
13331
13426
  if (!context) throw new Error("EditableTextInput must be used within an EditableTextRoot context");
13332
13427
  const { isEditing, value, setValue, onValueCommit, setIsEditing, typographicVariant, submitMode, originalValue, onValueRevert, id, placeholder, minLines, maxLines, finalFocusEl } = context;
13333
13428
  const inputRef = useRef(null);
13429
+ const isEnteringRef = useRef(false);
13430
+ useEffect(() => {
13431
+ isEnteringRef.current = isEditing;
13432
+ }, [isEditing]);
13334
13433
  useEffect(() => {
13335
13434
  if (inputRef.current) {
13336
13435
  if (isEditing) {
@@ -13340,63 +13439,93 @@ const EditableTextInput = (props) => {
13340
13439
  const { scrollHeight } = element;
13341
13440
  style.height = `${scrollHeight}px`;
13342
13441
  }
13343
- if (isEditing) inputRef.current.focus();
13442
+ if (isEditing) {
13443
+ inputRef.current.focus();
13444
+ isEnteringRef.current = false;
13445
+ }
13344
13446
  }
13345
13447
  }, [value, isEditing]);
13448
+ const isSingleLine = maxLines === 1;
13346
13449
  const handleChange = (event) => {
13347
- setValue(event.target.value);
13450
+ const { target } = event;
13451
+ const nextValue = target.value;
13452
+ if (!isSingleLine) {
13453
+ setValue(nextValue);
13454
+ return;
13455
+ }
13456
+ const singleLineValue = nextValue.replaceAll(/\r\n|\r|\n/g, " ");
13457
+ if (singleLineValue !== nextValue) {
13458
+ const { selectionStart, selectionEnd } = target;
13459
+ target.value = singleLineValue;
13460
+ target.setSelectionRange(selectionStart, selectionEnd);
13461
+ }
13462
+ setValue(singleLineValue);
13463
+ };
13464
+ const closeAndRestoreFocus = () => {
13465
+ setIsEditing(false);
13466
+ setTimeout(() => {
13467
+ finalFocusEl?.()?.focus();
13468
+ }, 0);
13348
13469
  };
13349
13470
  const handleKeyDown = (event) => {
13350
- if ((submitMode === "enter" || submitMode === "both") && event.key === "Enter" && !event.shiftKey) {
13351
- event.preventDefault();
13352
- onValueCommit?.(value);
13353
- setIsEditing(false);
13354
- setTimeout(() => {
13355
- const element = finalFocusEl?.();
13356
- if (element) element.focus();
13357
- }, 0);
13358
- }
13359
13471
  if (event.key === "Escape") {
13360
13472
  event.preventDefault();
13361
13473
  setValue(originalValue);
13362
13474
  onValueRevert?.(originalValue);
13363
- setIsEditing(false);
13364
- setTimeout(() => {
13365
- const element = finalFocusEl?.();
13366
- if (element) element.focus();
13367
- }, 0);
13475
+ closeAndRestoreFocus();
13476
+ return;
13368
13477
  }
13478
+ if (isCommitEnter(event, submitMode)) {
13479
+ event.preventDefault();
13480
+ onValueCommit?.(value);
13481
+ closeAndRestoreFocus();
13482
+ return;
13483
+ }
13484
+ if (isSingleLine && event.key === "Enter") event.preventDefault();
13369
13485
  };
13370
13486
  const handleBlur = () => {
13371
13487
  if (submitMode === "blur" || submitMode === "both") {
13372
13488
  onValueCommit?.(value);
13373
- setIsEditing(false);
13374
- setTimeout(() => {
13375
- const element = finalFocusEl?.();
13376
- if (element) element.focus();
13377
- }, 0);
13489
+ closeAndRestoreFocus();
13378
13490
  }
13379
13491
  };
13492
+ const handleFocus = (event) => {
13493
+ const element = event.currentTarget;
13494
+ const shouldSelect = autoSelect && isEnteringRef.current;
13495
+ isEnteringRef.current = false;
13496
+ const { length } = element.value;
13497
+ element.setSelectionRange(length, length);
13498
+ if (isSingleLine) scrollToEnd(element);
13499
+ if (!shouldSelect && !isSingleLine) return;
13500
+ const valueOnFocus = element.value;
13501
+ requestAnimationFrame(() => {
13502
+ if (shouldSelect && element.value === valueOnFocus) element.select();
13503
+ if (isSingleLine && element.selectionEnd === element.value.length) scrollToEnd(element);
13504
+ });
13505
+ };
13380
13506
  if (!isEditing) return null;
13381
- return /* @__PURE__ */ jsx(StyledInput, {
13507
+ const keyboardHintId = `${id}-keyboard-hint`;
13508
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(StyledInputWrapper, { children: /* @__PURE__ */ jsx(StyledInput, {
13382
13509
  ref: inputRef,
13383
13510
  $maxLines: maxLines ?? "infinity",
13384
13511
  $minLines: minLines,
13385
13512
  $typographicVariant: typographicVariant,
13513
+ "aria-describedby": keyboardHintId,
13386
13514
  "data-wui-editable-text-input": true,
13387
13515
  id,
13388
13516
  onBlur: handleBlur,
13389
13517
  onChange: handleChange,
13390
- onFocus: (event) => {
13391
- const { length } = event.currentTarget.value;
13392
- event.currentTarget.setSelectionRange(length, length);
13393
- },
13518
+ onFocus: handleFocus,
13394
13519
  onKeyDown: handleKeyDown,
13395
13520
  placeholder,
13396
13521
  type: "multiline",
13397
13522
  value,
13398
13523
  ...props
13399
- });
13524
+ }) }), /* @__PURE__ */ jsx(ScreenReaderOnly, {
13525
+ "data-wui-editable-text-keyboard-hint": true,
13526
+ id: keyboardHintId,
13527
+ text: KEYBOARD_HINTS[submitMode]
13528
+ })] });
13400
13529
  };
13401
13530
  // src/components/EditableText/EditableTextLabel.tsx
13402
13531
  const EditableTextLabel = ({ ...props }) => {