无头 UI v1.6、Tailwind UI 团队管理、Tailwind Play 改进等

日期

我已经有一段时间没有写过关于我们一直在做什么的文章了,所以我有很多东西要分享!说实话,太多了——我甚至要发布这个更新的主要动力是,我们下周还有更多东西要发布,我觉得我不能分享那些东西,除非我分享了我们已经发布的所有东西。

所以穿上你的泳衣,坐在你的躺椅上,准备好吸收一些维生素 CSS 吧。


无头 UI v1.6 发布

几周前,我们发布了 无头 UI 的一个新的次要版本,这是一个我们构建的无样式 UI 库,旨在使 Tailwind UI 能够添加 React 和 Vue 支持。

查看 发布说明 以获取所有详细信息,但以下是一些亮点。

多选支持

我们在 ComboboxListbox 组件中添加了一个新的 multiple 属性,以便用户可以选择多个选项。

只需添加 multiple 属性并将数组绑定到 value 属性,即可开始使用。

function MyCombobox({ items }) {
  const [selectedItems, setSelectedItems] = useState([])

  return (
    <Combobox value={selectedItems} onChange={setSelectedItems} multiple>
      {selectedItems.length > 0 && (
        <ul>
          {selectedItems.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}
      <Combobox.Input />
      <Combobox.Options>
        {items.map((item) => (
          <Combobox.Option key={item} value={item}>
            {item}
          </Combobox.Option>
        ))}
      </Combobox.Options>
    </Combobox>
  )
}

查看 组合框文档列表框文档 以了解更多信息。

可空组合框

在 v1.6 之前,如果您删除了组合框的内容并按 Tab 键离开,它将恢复先前选择的选项。这在很多情况下都有意义,但有时您确实想要清除组合框的值。

我们添加了一个新的 nullable 属性来实现这一点 - 只需添加该属性,您就可以删除值而不会恢复先前值。

function MyCombobox({ items }) {
  const [selectedItem, setSelectedItem] = useState([])

  return (
    <Combobox value={selectedItems} onChange={setSelectedItem} nullable>
      <Combobox.Input />
      <Combobox.Options>
        {items.map((item) => (
          <Combobox.Option key={item} value={item}>
            {item}
          </Combobox.Option>
        ))}
      </Combobox.Options>
    </Combobox>
  )
}

轻松支持 HTML 表单

现在,如果您在表单组件(如 ListboxComboboxSwitchRadioGroup)中添加 name 属性,我们将自动创建一个隐藏的输入,该输入与组件的值同步。

这使得使用常规表单提交或使用 Remix 中的 <Form> 组件将数据发送到服务器变得非常容易。

<form action="/projects/1/assignee" method="post">
  <Listbox
    value={selectedPerson}
    onChange={setSelectedPerson}
    name="assignee"
  >
    {/* ... */}
  </Listbox>
  <button>Submit</button>
</form>

这适用于数字和字符串等简单值,也适用于对象 - 我们会自动使用 1996 年的方括号表示法将它们序列化为多个字段。

<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

查看 文档,如果您想再次阅读我刚刚写的所有内容,但是在不同的域名上。

可滚动对话框改进

对话框是地球上最难构建的东西。我们一直在努力解决棘手的 滚动 问题,现在我们认为在 v1.6 中终于解决了所有问题。

我们改变了“点击外部关闭”对话框的方式。以前,我们使用了一个名为 `Dialog.Overlay` 的组件,它位于对话框的后面,并有一个点击处理程序,在点击时关闭对话框。从原理上讲,我真的很喜欢这种方法的简单性——检测特定元素被点击比检测除了特定元素之外的所有元素被点击要容易得多,尤其是在对话框内部渲染了其他元素,这些元素本身又在门户中渲染其他元素的情况下。

这种方法的问题是,如果对话框很长需要滚动,那么覆盖层会位于滚动条的顶部,尝试点击滚动条会导致对话框关闭。这不是你想要的!

为了以不破坏的方式解决这个问题,我们添加了一个新的 `Dialog.Panel` 组件,你可以用它来代替,现在只要你点击该组件外部,就会关闭对话框,而不是专门在点击覆盖层时关闭它。

<Dialog
  open={isOpen}
  onClose={closeModal}
  className="fixed inset-0 flex items-center justify-center ..."
>
  <Dialog.Overlay className="fixed inset-0 bg-black/25" />
  <div className="fixed inset-0 bg-black/25" />

  <div className="bg-white shadow-xl rounded-2xl ...">
  <Dialog.Panel className="bg-white shadow-xl rounded-2xl ...">
    <Dialog.Title>Payment successful</Dialog.Title>
    {/* ... */}
  </div>
  </Dialog.Panel>
</Dialog>

查看 更新后的对话框文档,了解使用新面板组件而不是覆盖层的更完整示例。

更好的焦点捕获

对话框是世界上最难构建的东西之一,原因之一是焦点捕获。我们第一次尝试使用这种方法是劫持 Tab 键并手动聚焦下一个/上一个元素,这样我们就可以在到达末尾时循环回到焦点捕获中的第一个元素。

这在人们在焦点捕获内部使用门户之前还算可以。现在基本上不可能管理,因为你可能会按下 Tab 键到一个日期选择器或其他概念上位于对话框内部但实际上并不位于对话框内部的元素,因为它出于样式原因在门户中渲染。

Robin 想出了一个 非常酷的解决方案,它非常简单——与其尝试手动控制 Tab 键的工作方式,不如在焦点捕获的开头和结尾放置一个不可见的可聚焦元素。现在,每当这些哨兵元素之一获得焦点时,你只需将焦点移动到它应该在的位置,这取决于你是在第一个元素还是最后一个元素,以及用户是向前还是向后按下 Tab 键。

使用这种方法,你根本不需要劫持 Tab 键——你只需让浏览器完成所有工作,并且只在你的哨兵元素之一获得焦点时手动移动焦点。

在弄清楚这一点后,我们注意到其他几个库也已经做了同样的事情,所以这并不是什么突破性的新东西,但我认为它非常聪明,值得与任何还没有想到这种技术的人分享。


Tailwind UI 的团队管理功能

当我们首次发布 Tailwind UI 时,“团队” 只有我和 Steve,所以如果我们想让它真正发布,我们就必须简化很多东西,因为只有我们两个人在做这件事。

其中之一就是团队许可。我们没有提供任何花哨的团队成员邀请流程,只是要求人们与他们的团队共享他们的 Tailwind UI 凭据。这对我们来说已经足够了,因为 Tailwind UI 并没有以用户特定的方式做任何事情,而且你的团队中的每个成员都会获得相同的体验。

此外,对我们来说,获取团队中每个人的电子邮件地址、将它们输入到某个表单中、向每个人发送邀请电子邮件并让他们接受邀请,感觉就像是一场行政地狱,尤其是在每个人登录后都会获得相同的体验的情况下。

但与此同时,共享任何东西的凭据都是非常低端的,这不是我们引以为豪的设计决策。我使用与银行帐户相同的密码 (slayerfan1234) 来访问 Tailwind UI——我不想与任何人共享它!

所以几周前我们决定弄清楚并构建一些东西。

Interface with a copyable invite URL and list of team members

我们最终实现的是一个纯粹基于链接的邀请系统,你只需复制你的邀请链接,在 Slack/Discord/任何地方与你的团队共享它,并在需要时重置你的链接。你还可以授予人们“成员”或“所有者”权限,这将控制他们是否可以管理团队成员或查看账单历史记录。

这使得邀请你的团队变得超级容易,无需繁琐的数据输入,并且如果有人离开,你可以在 UI 中直接撤销访问权限,而不是通过更改你的共享密码。

现在,任何拥有 Tailwind UI 团队帐户的人都可以使用它——只需打开下拉菜单并点击“我的团队”来命名你的团队并开始邀请你的同事。

你可以在 Tailwind UI 网站上 购买团队许可证,或者如果你拥有个人许可证并希望开始与你的团队一起使用 Tailwind UI,可以 升级到团队许可证


将 Tailwind UI 中的 Vue 示例更新为 <script setup>

自从发布了对 Tailwind UI 的 Vue 支持以来,Vue 3 中新的 <script setup> 语法已成为编写单文件组件的推荐方法。

我们已经更新了 Tailwind UI 中所有 Vue 示例以使用这种新格式,它可以消除大量的样板代码。

<template>
  <Listbox as="div" v-model="selected">
    <!-- ... -->
  </Listbox>
</template>

<script setup>
import { ref } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/vue'
import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid'

const people = [
  { id: 1, name: 'Wade Cooper' },
  // ...
]

const selected = ref(people[3])
</script>

对我来说,最好的部分是您不再需要在 `components` 下显式注册任何内容——任何在作用域内的组件都会自动在模板中可用。

使用 `