text-field.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { TextField as Kobalte } from "@kobalte/core/text-field"
  2. import { createSignal, Show, splitProps } from "solid-js"
  3. import type { ComponentProps } from "solid-js"
  4. import { useI18n } from "../context/i18n"
  5. import { IconButton } from "./icon-button"
  6. import { Tooltip } from "./tooltip"
  7. export interface TextFieldProps
  8. extends ComponentProps<typeof Kobalte.Input>,
  9. Partial<
  10. Pick<
  11. ComponentProps<typeof Kobalte>,
  12. | "name"
  13. | "defaultValue"
  14. | "value"
  15. | "onChange"
  16. | "onKeyDown"
  17. | "validationState"
  18. | "required"
  19. | "disabled"
  20. | "readOnly"
  21. >
  22. > {
  23. label?: string
  24. hideLabel?: boolean
  25. description?: string
  26. error?: string
  27. variant?: "normal" | "ghost"
  28. copyable?: boolean
  29. copyKind?: "clipboard" | "link"
  30. multiline?: boolean
  31. }
  32. export function TextField(props: TextFieldProps) {
  33. const i18n = useI18n()
  34. const [local, others] = splitProps(props, [
  35. "name",
  36. "defaultValue",
  37. "value",
  38. "onChange",
  39. "onKeyDown",
  40. "validationState",
  41. "required",
  42. "disabled",
  43. "readOnly",
  44. "class",
  45. "label",
  46. "hideLabel",
  47. "description",
  48. "error",
  49. "variant",
  50. "copyable",
  51. "copyKind",
  52. "multiline",
  53. ])
  54. const [copied, setCopied] = createSignal(false)
  55. const label = () => {
  56. if (copied()) return i18n.t("ui.textField.copied")
  57. if (local.copyKind === "link") return i18n.t("ui.textField.copyLink")
  58. return i18n.t("ui.textField.copyToClipboard")
  59. }
  60. const icon = () => {
  61. if (copied()) return "check"
  62. if (local.copyKind === "link") return "link"
  63. return "copy"
  64. }
  65. async function handleCopy() {
  66. const value = local.value ?? local.defaultValue ?? ""
  67. await navigator.clipboard.writeText(value)
  68. setCopied(true)
  69. setTimeout(() => setCopied(false), 2000)
  70. }
  71. function handleClick() {
  72. if (local.copyable) void handleCopy()
  73. }
  74. return (
  75. <Kobalte
  76. data-component="input"
  77. data-variant={local.variant || "normal"}
  78. name={local.name}
  79. defaultValue={local.defaultValue}
  80. value={local.value}
  81. onChange={local.onChange}
  82. onKeyDown={local.onKeyDown}
  83. onClick={handleClick}
  84. required={local.required}
  85. disabled={local.disabled}
  86. readOnly={local.readOnly}
  87. validationState={local.validationState}
  88. >
  89. <Show when={local.label}>
  90. <Kobalte.Label data-slot="input-label" classList={{ "sr-only": local.hideLabel }}>
  91. {local.label}
  92. </Kobalte.Label>
  93. </Show>
  94. <div data-slot="input-wrapper">
  95. <Show
  96. when={local.multiline}
  97. fallback={<Kobalte.Input {...others} data-slot="input-input" class={local.class} />}
  98. >
  99. <Kobalte.TextArea {...others} autoResize data-slot="input-input" class={local.class} />
  100. </Show>
  101. <Show when={local.copyable}>
  102. <Tooltip value={label()} placement="top" gutter={4} forceOpen={copied()} skipDelayDuration={0}>
  103. <IconButton
  104. type="button"
  105. icon={icon()}
  106. variant="ghost"
  107. onClick={handleCopy}
  108. tabIndex={-1}
  109. data-slot="input-copy-button"
  110. aria-label={label()}
  111. />
  112. </Tooltip>
  113. </Show>
  114. </div>
  115. <Show when={local.description}>
  116. <Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
  117. </Show>
  118. <Kobalte.ErrorMessage data-slot="input-error">{local.error}</Kobalte.ErrorMessage>
  119. </Kobalte>
  120. )
  121. }