Add virtual prop to Combobox component (#2740)

* type timezones in playground data

* add `@tanstack/react-virtual` and `@tanstack/vue-virtual`

* use latest stable Tailwind CSS version

* add Combobox with virtual prop example

* add `virtual` prop to `Combobox`

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* add tests for `virtual` prop

- Also wrap `click` helpers in `act` for React (use `rawClick` without
  `act` in tests related to `Transition`)

* update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
Robin Malfait
2023-09-15 14:29:18 +02:00
committed by GitHub
parent f2179f36c0
commit f016dc51db
19 changed files with 7869 additions and 6329 deletions
+1
View File
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add `immediate` prop to `<Combobox />` for immediately opening the Combobox when the `input` receives focus ([#2686](https://github.com/tailwindlabs/headlessui/pull/2686))
- Add `virtual` prop to `Combobox` component ([#2740](https://github.com/tailwindlabs/headlessui/pull/2740))
## [1.7.17] - 2023-08-17
+1
View File
@@ -51,6 +51,7 @@
"snapshot-diff": "^0.8.1"
},
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
}
}
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,4 @@
import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual'
import React, {
createContext,
createRef,
@@ -8,6 +9,7 @@ import React, {
useMemo,
useReducer,
useRef,
type CSSProperties,
type ElementType,
type FocusEvent as ReactFocusEvent,
type KeyboardEvent as ReactKeyboardEvent,
@@ -49,7 +51,6 @@ import {
type PropsForFeatures,
type RefProp,
} from '../../utils/render'
import { Keys } from '../keyboard'
enum ComboboxState {
@@ -69,10 +70,11 @@ enum ActivationTrigger {
}
type ComboboxOptionDataRef<T> = MutableRefObject<{
textValue?: string
disabled: boolean
value: T
domRef: MutableRefObject<HTMLElement | null>
order: number | null
onVirtualRangeUpdate: (virtualizer: Virtualizer<any, any>) => void
}>
interface StateDefinition<T> {
@@ -107,10 +109,13 @@ function adjustOrderedState<T>(
let currentActiveOption =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
let sortedOptions = sortByDomNode(
adjustment(state.options.slice()),
(option) => option.dataRef.current.domRef.current
)
let list = adjustment(state.options.slice())
let sortedOptions =
list.length > 0 && list[0].dataRef.current.order !== null
? // Prefer sorting based on the `order`
list.sort((a, z) => a.dataRef.current.order! - z.dataRef.current.order!)
: // Fallback to much slower DOM order
sortByDomNode(list, (option) => option.dataRef.current.domRef.current)
// If we inserted an option before the current active option then the active option index
// would be wrong. To fix this, we will re-lookup the correct index.
@@ -203,17 +208,29 @@ let reducers: {
resolveId: (item) => item.id,
resolveDisabled: (item) => item.dataRef.current.disabled,
})
let activationTrigger = action.trigger ?? ActivationTrigger.Other
if (
state.activeOptionIndex === activeOptionIndex &&
state.activationTrigger === activationTrigger
) {
return state
}
return {
...state,
...adjustedState,
activeOptionIndex,
activationTrigger: action.trigger ?? ActivationTrigger.Other,
activationTrigger,
}
},
[ActionTypes.RegisterOption]: (state, action) => {
let option = { id: action.id, dataRef: action.dataRef }
let adjustedState = adjustOrderedState(state, (options) => [...options, option])
let adjustedState = adjustOrderedState(state, (options) => {
options.push(option)
return options
})
// Check if we need to make the newly registered option active.
if (state.activeOptionIndex === null) {
@@ -286,6 +303,69 @@ function useActions(component: string) {
}
type _Actions = ReturnType<typeof useActions>
let VirtualContext = createContext<Virtualizer<any, any> | null>(null)
function VirtualProvider(props: React.PropsWithChildren<{}>) {
let data = useData('VirtualProvider')
let firstAvailableOption = data.options.find((option) => option.dataRef.current.domRef.current)
let measuredHeight = useMemo(() => {
let height =
firstAvailableOption?.dataRef.current.domRef.current?.getBoundingClientRect().height
return height ?? 40
}, [firstAvailableOption])
let [paddingStart, paddingEnd] = useMemo(() => {
let el = data.optionsRef.current
if (!el) return [0, 0]
let styles = window.getComputedStyle(el)
return [
parseFloat(styles.paddingBlockStart || styles.paddingTop),
parseFloat(styles.paddingBlockEnd || styles.paddingBottom),
]
}, [data.optionsRef.current])
let virtualizer = useVirtualizer({
scrollPaddingStart: paddingStart,
scrollPaddingEnd: paddingEnd,
count: data.options.length,
estimateSize() {
return measuredHeight
},
getScrollElement() {
return (data.optionsRef.current ?? null) as HTMLElement | null
},
overscan: 12,
onChange(event) {
let list = event.getVirtualItems()
if (list.length === 0) return
let min = list[0].index
let max = list[list.length - 1].index + 1
for (let option of data.options.slice(min, max)) {
option.dataRef.current.onVirtualRangeUpdate(event)
}
},
})
return (
<VirtualContext.Provider value={virtualizer}>
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
{props.children}
</div>
</VirtualContext.Provider>
)
}
let ComboboxDataContext = createContext<
| ({
value: unknown
@@ -299,6 +379,8 @@ let ComboboxDataContext = createContext<
isSelected(value: unknown): boolean
__demoMode: boolean
virtual: boolean
optionsPropsRef: MutableRefObject<{
static: boolean
hold: boolean
@@ -393,6 +475,7 @@ export type ComboboxProps<
form?: string
name?: string
immediate?: boolean
virtual?: boolean
}
function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
@@ -428,6 +511,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
nullable = false,
multiple = false,
immediate = false,
virtual = false,
...theirProps
} = props
let [value = multiple ? [] : undefined, theirOnChange] = useControllable<any>(
@@ -474,7 +558,6 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
}),
[value]
)
let data = useMemo<_Data>(
() => ({
...state,
@@ -488,6 +571,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
defaultValue,
disabled,
mode: multiple ? ValueMode.Multi : ValueMode.Single,
virtual,
get activeOptionIndex() {
if (
defaultToFirstOption.current &&
@@ -510,7 +594,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
nullable,
__demoMode,
}),
[value, defaultValue, disabled, multiple, nullable, __demoMode, state]
[value, defaultValue, disabled, multiple, nullable, __demoMode, state, virtual]
)
let lastActiveOption = useRef(
@@ -1185,6 +1269,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
if (data.comboboxState === ComboboxState.Closed) {
actions.openCombobox()
}
return d.nextFrame(() => data.inputRef.current?.focus({ preventScroll: true }))
case Keys.ArrowUp:
@@ -1377,6 +1462,13 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
ref: optionsRef,
}
// Map the children in a scrollable container when virtualization is enabled
if (data.virtual && data.comboboxState === ComboboxState.Open) {
Object.assign(theirProps, {
children: <VirtualProvider>{theirProps.children}</VirtualProvider>,
})
}
return render({
ourProps,
theirProps,
@@ -1405,6 +1497,7 @@ export type ComboboxOptionProps<TTag extends ElementType, TType> = Props<
{
disabled?: boolean
value: TType
order?: number
}
>
@@ -1419,41 +1512,57 @@ function OptionFn<
id = `headlessui-combobox-option-${internalId}`,
disabled = false,
value,
order = null,
...theirProps
} = props
let data = useData('Combobox.Option')
let actions = useActions('Combobox.Option')
let active =
data.activeOptionIndex !== null ? data.options[data.activeOptionIndex].id === id : false
if (order === null && data.virtual) {
throw new Error(
`The \`order\` prop on <Combobox.Option /> is required when using <Combobox virtual />.`
)
}
let [, rerender] = useReducer((v) => !v, true)
let selected = data.isSelected(value)
let internalOptionRef = useRef<HTMLLIElement | null>(null)
let bag = useLatestValue<ComboboxOptionDataRef<TType>['current']>({
disabled,
value,
domRef: internalOptionRef,
textValue: internalOptionRef.current?.textContent?.toLowerCase(),
order,
onVirtualRangeUpdate: rerender,
})
let optionRef = useSyncRefs(ref, internalOptionRef)
let virtualizer = useContext(VirtualContext)
let optionRef = useSyncRefs(
ref,
internalOptionRef,
virtualizer ? virtualizer.measureElement : null
)
let select = useEvent(() => actions.selectOption(id))
useIsoMorphicEffect(() => actions.registerOption(id, bag), [bag, id])
let enableScrollIntoView = useRef(data.__demoMode ? false : true)
let enableScrollIntoView = useRef(data.virtual || data.__demoMode ? false : true)
useIsoMorphicEffect(() => {
if (!data.virtual) return
if (!data.__demoMode) return
let d = disposables()
d.requestAnimationFrame(() => {
enableScrollIntoView.current = true
})
return d.dispose
}, [])
}, [data.virtual, data.__demoMode])
useIsoMorphicEffect(() => {
if (!enableScrollIntoView.current) return
if (data.comboboxState !== ComboboxState.Open) return
if (!active) return
if (!enableScrollIntoView.current) return
if (data.activationTrigger === ActivationTrigger.Pointer) return
let d = disposables()
d.requestAnimationFrame(() => {
@@ -1522,6 +1631,43 @@ function OptionFn<
[active, selected, disabled]
)
let virtualIdx = useMemo(() => {
if (!data.virtual) return -1
return data.options.findIndex((o) => o.id === id) ?? 0
}, [virtualizer, data.options, id])
let virtualItem =
virtualIdx === -1
? undefined
: (virtualizer?.getVirtualItems() ?? []).find((item) => item.index === virtualIdx)
let d = useDisposables()
let shouldScroll =
virtualizer && data.activationTrigger !== ActivationTrigger.Pointer && data.virtual && active
useEffect(() => {
if (!shouldScroll) return
// Try scrolling to the item
virtualizer!.scrollToIndex(virtualIdx)
// Ensure we scrolled to the correct location
;(function ensureScrolledCorrectly() {
if (virtualizer?.isScrolling) {
d.requestAnimationFrame(ensureScrolledCorrectly)
return
}
virtualizer!.scrollToIndex(virtualIdx)
})()
return d.dispose
}, [active, virtualizer, virtualIdx, shouldScroll])
if (data.virtual && !virtualItem) {
return null
}
let ourProps = {
id,
ref: optionRef,
@@ -1532,6 +1678,9 @@ function OptionFn<
// multi-select,but Voice-Over disagrees. So we use aria-checked instead for
// both single and multi-select.
'aria-selected': selected,
'data-index': virtualizer && virtualIdx !== -1 ? virtualIdx : undefined,
'aria-setsize': virtualizer ? data.options.length : undefined,
'aria-posinset': virtualizer && virtualIdx !== -1 ? virtualIdx + 1 : undefined,
disabled: undefined, // Never forward the `disabled` prop
onClick: handleClick,
onFocus: handleFocus,
@@ -1543,6 +1692,21 @@ function OptionFn<
onMouseLeave: handleLeave,
}
if (virtualItem) {
let localOurProps = ourProps as typeof ourProps & { style: CSSProperties }
localOurProps.style = {
...localOurProps.style,
position: 'absolute',
top: 0,
left: 0,
transform: `translateY(${virtualItem.start}px)`,
}
// Technically unnecessary
ourProps = localOurProps
}
return render({
ourProps,
theirProps,
@@ -30,6 +30,7 @@ import {
mouseLeave,
mouseMove,
press,
rawClick,
shift,
type,
word,
@@ -1353,7 +1354,7 @@ describe('Composition', () => {
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
await rawClick(getListboxButton())
assertListboxButton({
state: ListboxState.Visible,
@@ -1364,7 +1365,7 @@ describe('Composition', () => {
textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
})
await click(getListboxButton())
await rawClick(getListboxButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
@@ -24,6 +24,7 @@ import {
mouseLeave,
mouseMove,
press,
rawClick,
shift,
type,
word,
@@ -646,7 +647,7 @@ describe('Composition', () => {
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
await rawClick(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
@@ -657,7 +658,7 @@ describe('Composition', () => {
textContent: JSON.stringify({ active: false, disabled: false }),
})
await click(getMenuButton())
await rawClick(getMenuButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
@@ -700,7 +701,7 @@ describe('Composition', () => {
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
await rawClick(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
@@ -711,7 +712,7 @@ describe('Composition', () => {
textContent: JSON.stringify({ active: false, disabled: false }),
})
await click(getMenuButton())
await rawClick(getMenuButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
@@ -1,4 +1,4 @@
import { fireEvent } from '@testing-library/react'
import { act, fireEvent } from '@testing-library/react'
import { pointer } from './fake-pointer'
function nextFrame(cb: Function): void {
@@ -227,6 +227,13 @@ export enum MouseButton {
export async function click(
element: Document | Element | Window | Node | null,
button = MouseButton.Left
) {
return act(() => rawClick(element, button))
}
export async function rawClick(
element: Document | Element | Window | Node | null,
button = MouseButton.Left
) {
try {
if (element === null) return expect(element).not.toBe(null)
+1
View File
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add `immediate` prop to `<Combobox />` for immediately opening the Combobox when the `input` receives focus ([#2686](https://github.com/tailwindlabs/headlessui/pull/2686))
- Add `virtual` prop to `Combobox` component ([#2740](https://github.com/tailwindlabs/headlessui/pull/2740))
## [1.7.16] - 2023-08-17
+3
View File
@@ -44,5 +44,8 @@
"@testing-library/vue": "^5.8.2",
"@vue/test-utils": "^2.0.0-rc.18",
"vue": "^3.2.29"
},
"dependencies": {
"@tanstack/vue-virtual": "^3.0.0-beta.60"
}
}
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,5 @@
import type { Virtualizer } from '@tanstack/virtual-core'
import { useVirtualizer } from '@tanstack/vue-virtual'
import {
computed,
defineComponent,
@@ -8,15 +10,20 @@ import {
onMounted,
onUnmounted,
provide,
reactive,
ref,
shallowRef,
toRaw,
watch,
watchEffect,
watchPostEffect,
type ComputedRef,
type CSSProperties,
type InjectionKey,
type PropType,
type Ref,
type UnwrapNestedRefs,
type UnwrapRef,
} from 'vue'
import { useControllable } from '../../hooks/use-controllable'
import { useId } from '../../hooks/use-id'
@@ -62,6 +69,8 @@ type ComboboxOptionData = {
disabled: boolean
value: unknown
domRef: Ref<HTMLElement | null>
order: Ref<number | null>
onVirtualRangeUpdate: (virtualizer: Virtualizer<any, any>) => void
}
type StateDefinition = {
// State
@@ -72,6 +81,7 @@ type StateDefinition = {
mode: ComputedRef<ValueMode>
nullable: ComputedRef<boolean>
immediate: ComputedRef<boolean>
virtual: ComputedRef<boolean>
compare: (a: unknown, z: unknown) => boolean
@@ -84,6 +94,7 @@ type StateDefinition = {
disabled: Ref<boolean>
options: Ref<{ id: string; dataRef: ComputedRef<ComboboxOptionData> }[]>
indexes: Ref<Record<string, number>>
activeOptionIndex: Ref<number | null>
activationTrigger: Ref<ActivationTrigger>
@@ -116,6 +127,82 @@ function useComboboxContext(component: string) {
// ---
let VirtualContext = Symbol('VirtualContext') as InjectionKey<Ref<Virtualizer<any, any>> | null>
let VirtualProvider = defineComponent({
name: 'VirtualProvider',
setup(_, { slots }) {
let api = useComboboxContext('VirtualProvider')
let measuredHeight = computed(() => {
let firstAvailableOption = api.options.value.find(
(option) => dom(option.dataRef.value.domRef) !== null
)
let height = dom(firstAvailableOption?.dataRef.value.domRef)?.getBoundingClientRect().height
return height ?? 40
})
let padding = computed(() => {
let el = dom(api.optionsRef)
if (!el) return { start: 0, end: 0 }
let styles = window.getComputedStyle(el)
return {
start: parseFloat(styles.paddingBlockStart || styles.paddingTop),
end: parseFloat(styles.paddingBlockEnd || styles.paddingBottom),
}
})
let virtualizer = useVirtualizer<HTMLDivElement, HTMLLIElement>(
computed(() => {
return {
scrollPaddingStart: padding.value.start,
scrollPaddingEnd: padding.value.end,
count: api.options.value.length,
estimateSize() {
return measuredHeight.value
},
getScrollElement() {
return dom(api.optionsRef)
},
overscan: 12,
onChange(event) {
let list = event.getVirtualItems()
if (list.length === 0) return
let min = list[0].index
let max = list[list.length - 1].index + 1
for (let option of api.options.value.slice(min, max)) {
let dataRef = option.dataRef as unknown as UnwrapRef<typeof option.dataRef>
dataRef.onVirtualRangeUpdate(event)
}
},
}
})
)
provide(VirtualContext, api.virtual.value ? virtualizer : null)
return () => [
h(
'div',
{
style: {
position: 'relative',
width: '100%',
height: `${virtualizer.value.getTotalSize()}px`,
},
},
slots.default?.()
),
]
},
})
// ---
export let Combobox = defineComponent({
name: 'Combobox',
emits: { 'update:modelValue': (_value: any) => true },
@@ -140,6 +227,7 @@ export let Combobox = defineComponent({
nullable: { type: Boolean, default: false },
multiple: { type: [Boolean], default: false },
immediate: { type: [Boolean], default: false },
virtual: { type: [Boolean], default: false },
},
inheritAttrs: false,
setup(props, { slots, attrs, emit }) {
@@ -155,12 +243,19 @@ export let Combobox = defineComponent({
hold: false,
}) as StateDefinition['optionsPropsRef']
let options = ref<StateDefinition['options']['value']>([])
let indexes = shallowRef<Record<string, number>>({})
let activeOptionIndex = ref<StateDefinition['activeOptionIndex']['value']>(null)
let activationTrigger = ref<StateDefinition['activationTrigger']['value']>(
ActivationTrigger.Other
)
let defaultToFirstOption = ref(false)
// This is not a "computed" ref because we eventually
// want to calculate this only when the length or order can actually change
function recalculateIndexes() {
indexes.value = Object.fromEntries(options.value.map((v, idx) => [v.id, idx]))
}
function adjustOrderedState(
adjustment: (
options: UnwrapNestedRefs<StateDefinition['options']['value']>
@@ -169,9 +264,14 @@ export let Combobox = defineComponent({
let currentActiveOption =
activeOptionIndex.value !== null ? options.value[activeOptionIndex.value] : null
let sortedOptions = sortByDomNode(adjustment(options.value.slice()), (option) =>
dom(option.dataRef.domRef)
)
let list = adjustment(options.value.slice())
let sortedOptions =
list.length > 0 && list[0].dataRef.order.value !== null
? // Prefer sorting based on the `order`
list.sort((a, z) => a.dataRef.order.value! - z.dataRef.order.value!)
: // Fallback to much slower DOM order
sortByDomNode(list, (option) => dom(option.dataRef.domRef))
// If we inserted an option before the current active option then the active option index
// would be wrong. To fix this, we will re-lookup the correct index.
@@ -224,12 +324,14 @@ export let Combobox = defineComponent({
defaultValue: computed(() => props.defaultValue),
nullable,
immediate: computed(() => props.immediate),
virtual: computed(() => props.virtual),
inputRef,
labelRef,
buttonRef,
optionsRef,
disabled: computed(() => props.disabled),
options,
indexes,
change(value: unknown) {
theirOnChange(value as typeof props.modelValue)
},
@@ -241,7 +343,7 @@ export let Combobox = defineComponent({
) {
let localActiveOptionIndex = options.value.findIndex((option) => !option.dataRef.disabled)
if (localActiveOptionIndex !== -1) {
activeOptionIndex.value = localActiveOptionIndex
return localActiveOptionIndex
}
}
@@ -333,6 +435,7 @@ export let Combobox = defineComponent({
activeOptionIndex.value = nextActiveOptionIndex
activationTrigger.value = trigger ?? ActivationTrigger.Other
options.value = adjustedState.options
recalculateIndexes()
})
},
selectOption(id: string) {
@@ -389,7 +492,10 @@ export let Combobox = defineComponent({
registerOption(id: string, dataRef: ComboboxOptionData) {
if (orderOptionsRaf) cancelAnimationFrame(orderOptionsRaf)
let option = { id, dataRef }
let option = reactive({ id, dataRef }) as unknown as {
id: typeof id
dataRef: typeof dataRef
}
let adjustedState = adjustOrderedState((options) => {
options.push(option)
@@ -415,13 +521,16 @@ export let Combobox = defineComponent({
options.value = adjustedState.options
activeOptionIndex.value = adjustedState.activeOptionIndex
activationTrigger.value = ActivationTrigger.Other
recalculateIndexes()
// If some of the DOM elements aren't ready yet, then we can retry in the next tick.
if (adjustedState.options.some((option) => !dom(option.dataRef.domRef))) {
orderOptionsRaf = requestAnimationFrame(() => {
let adjustedState = adjustOrderedState()
options.value = adjustedState.options
activeOptionIndex.value = adjustedState.activeOptionIndex
recalculateIndexes()
})
}
},
@@ -451,6 +560,7 @@ export let Combobox = defineComponent({
options.value = adjustedState.options
activeOptionIndex.value = adjustedState.activeOptionIndex
activationTrigger.value = ActivationTrigger.Other
recalculateIndexes()
},
}
@@ -533,13 +643,14 @@ export let Combobox = defineComponent({
theirProps: {
...attrs,
...omit(theirProps, [
'modelValue',
'defaultValue',
'nullable',
'multiple',
'immediate',
'onUpdate:modelValue',
'by',
'defaultValue',
'immediate',
'modelValue',
'multiple',
'nullable',
'onUpdate:modelValue',
'virtual',
]),
},
ourProps: {},
@@ -1164,7 +1275,13 @@ export let ComboboxOptions = defineComponent({
theirProps,
slot,
attrs,
slots,
slots:
api.virtual.value && api.comboboxState.value === ComboboxStates.Open
? {
...slots,
default: () => [h(VirtualProvider, {}, slots.default)],
}
: slots,
features: Features.RenderStrategy | Features.Static,
visible: visible.value,
name: 'ComboboxOptions',
@@ -1183,6 +1300,7 @@ export let ComboboxOption = defineComponent({
>,
},
disabled: { type: Boolean, default: false },
order: { type: [Number], default: null },
},
setup(props, { slots, attrs, expose }) {
let api = useComboboxContext('ComboboxOption')
@@ -1191,6 +1309,14 @@ export let ComboboxOption = defineComponent({
expose({ el: internalOptionRef, $el: internalOptionRef })
watchEffect(() => {
if (props.order === null && api.virtual.value) {
throw new Error(
`The \`order\` prop on <ComboboxOption /> is required when using <Combobox virtual />.`
)
}
})
let active = computed(() => {
return api.activeOptionIndex.value !== null
? api.options.value[api.activeOptionIndex.value].id === id
@@ -1207,18 +1333,29 @@ export let ComboboxOption = defineComponent({
})
)
let virtualizer = inject(VirtualContext, null)
let dataRef = computed<ComboboxOptionData>(() => ({
disabled: props.disabled,
value: props.value,
domRef: internalOptionRef,
order: computed(() => props.order),
onVirtualRangeUpdate: () => {},
}))
onMounted(() => api.registerOption(id, dataRef))
onUnmounted(() => api.unregisterOption(id))
watchEffect(() => {
let el = dom(internalOptionRef)
if (!el) return
virtualizer?.value.measureElement(el)
})
watchEffect(() => {
if (api.comboboxState.value !== ComboboxStates.Open) return
if (!active.value) return
if (api.virtual.value) return
if (api.activationTrigger.value === ActivationTrigger.Pointer) return
nextTick(() => dom(internalOptionRef)?.scrollIntoView?.({ block: 'nearest' }))
})
@@ -1274,7 +1411,53 @@ export let ComboboxOption = defineComponent({
api.goToOption(Focus.Nothing)
}
let virtualIdx = computed(() => {
if (!api.virtual.value) return -1
return api.indexes.value[id] ?? 0
})
let virtualItem = computed(() => {
return virtualIdx.value === -1
? undefined
: virtualizer?.value.getVirtualItems().find((item) => item.index === virtualIdx.value)
})
let d = disposables()
onUnmounted(() => d.dispose())
let shouldScroll = computed(() => {
return (
virtualizer?.value &&
api.activationTrigger.value !== ActivationTrigger.Pointer &&
api.virtual.value &&
active.value
)
})
watchPostEffect((onCleanup) => {
if (!shouldScroll.value) return
// Try scrolling to the item
virtualizer!.value.scrollToIndex(virtualIdx.value)
// Ensure we scrolled to the correct location
;(function ensureScrolledCorrectly() {
if (virtualizer?.value.isScrolling) {
d.requestAnimationFrame(ensureScrolledCorrectly)
return
}
virtualizer!.value.scrollToIndex(virtualIdx.value)
})()
onCleanup(d.dispose)
})
return () => {
if (api.virtual.value && !virtualItem.value) {
return null
}
let { disabled } = props
let slot = { active: active.value, selected: selected.value, disabled }
let ourProps = {
@@ -1287,6 +1470,9 @@ export let ComboboxOption = defineComponent({
// multi-select,but Voice-Over disagrees. So we use aria-selected instead for
// both single and multi-select.
'aria-selected': selected.value,
'data-index': virtualizer && virtualIdx.value !== -1 ? virtualIdx.value : undefined,
'aria-setsize': virtualizer ? api.options.value.length : undefined,
'aria-posinset': virtualizer && virtualIdx.value !== -1 ? virtualIdx.value + 1 : undefined,
disabled: undefined, // Never forward the `disabled` prop
onClick: handleClick,
onFocus: handleFocus,
@@ -1298,7 +1484,22 @@ export let ComboboxOption = defineComponent({
onMouseleave: handleLeave,
}
let theirProps = props
if (virtualItem.value) {
let localOurProps = ourProps as typeof ourProps & { style: CSSProperties }
localOurProps.style = {
...localOurProps.style,
position: 'absolute',
top: 0,
left: 0,
transform: `translateY(${virtualItem.value!.start}px)`,
}
// Technically unnecessary
ourProps = localOurProps
}
let theirProps = omit(props, ['order'])
return render({
ourProps,
+3
View File
@@ -249,3 +249,6 @@ export let countries = [
'Zimbabwe',
'Åland Islands',
]
// @ts-expect-error
export let timezones: string[] = Intl.supportedValuesOf('timeZone')
+1 -1
View File
@@ -28,7 +28,7 @@
"react-dom": "^18.0.0",
"react-flatpickr": "^3.10.9",
"react-hot-toast": "2.3.0",
"tailwindcss": "^0.0.0-insiders.9faf109"
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@floating-ui/react": "^0.24.8"
@@ -0,0 +1,124 @@
import { Combobox } from '@headlessui/react'
import { useState } from 'react'
import { Button } from '../../components/button'
import { timezones as allTimezones } from '../../data'
import { classNames } from '../../utils/class-names'
export default function Home() {
return (
<div className="flex">
<Example virtual={true} initial="Europe/Brussels" />
<Example virtual={false} initial="Europe/Brussels" />
</div>
)
}
function Example({ virtual = true, initial }: { virtual?: boolean; initial: string }) {
let [query, setQuery] = useState('')
let [activeTimezone, setActiveTimezone] = useState(initial)
let timezones =
query === ''
? allTimezones
: allTimezones.filter((timezone) => timezone.toLowerCase().includes(query.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="py-8 font-mono text-xs">Selected timezone: {activeTimezone}</div>
<div className="space-y-1">
<Combobox
virtual={virtual}
value={activeTimezone}
nullable
onChange={(value) => {
setActiveTimezone(value)
setQuery('')
}}
as="div"
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Timezone {virtual ? `(virtual)` : ''}
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{timezones.map((timezone, idx) => {
return (
<Combobox.Option
key={timezone}
order={virtual ? idx : undefined}
value={timezone}
className={({ active }) => {
return classNames(
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{timezone}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
)
})}
</Combobox.Options>
</div>
</div>
</Combobox>
</div>
</div>
</div>
)
}
+1 -1
View File
@@ -22,7 +22,7 @@
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"tailwindcss": "^0.0.0-insiders.9faf109",
"tailwindcss": "^3.3.3",
"vue": "^3.2.27",
"vue-flatpickr-component": "^9.0.5",
"vue-router": "^4.0.0"
@@ -0,0 +1,111 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="py-8 font-mono text-xs">Selected timezone: {{ activeTimezone }}</div>
<div class="space-y-1">
<Combobox nullable v-model="activeTimezone" as="div" :virtual="virtual">
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Timezone {{ virtual ? '(virtual)' : '' }}
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="query = $event.target.value"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
>
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="(timezone, idx) in timezones"
:key="timezone"
:value="timezone"
:order="virtual ? idx : undefined"
v-slot="{ active, selected }"
as="template"
>
<li
:class="[
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ timezone }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
<script setup>
import { timezones as allTimezones } from '../../data'
import { ref, computed } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
defineProps({
virtual: {
type: Boolean,
default: false,
},
})
let query = ref('')
let activeTimezone = ref('Europe/Brussels')
let timezones = computed(() => {
return query.value === ''
? allTimezones
: allTimezones.filter((timezone) => timezone.toLowerCase().includes(query.value.toLowerCase()))
})
</script>
@@ -0,0 +1,10 @@
<template>
<div class="flex">
<Example :virtual="true" />
<Example :virtual="false" />
</div>
</template>
<script setup>
import Example from './_virtual-example.vue'
</script>
+3
View File
@@ -249,3 +249,6 @@ export let countries = [
'Zimbabwe',
'Åland Islands',
]
// @ts-expect-error
export let timezones: string[] = Intl.supportedValuesOf('timeZone')
+54 -218
View File
@@ -932,66 +932,6 @@
dependencies:
mini-svg-data-uri "^1.2.3"
"@tailwindcss/oxide-darwin-arm64@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-0.0.0-insiders.9faf109.tgz#a44f63ca1e8f1fc1355ab4e65d54a3249dc31196"
integrity sha512-SnJBw4j8uZddhXEhfsQHXUgpECVLn+icCXDTLh58cRPekKZ9JTj/uYyFYbnZHcMyIHiAKx1mQhoCP0FDXCUiWA==
"@tailwindcss/oxide-darwin-x64@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-0.0.0-insiders.9faf109.tgz#9146d714997e485292515011152319108862b923"
integrity sha512-lZvruT3X8tBQ/m/ShvGUxoMSaqfRh1jXoqKX6oZjuqRuTNPgG0hV2u02VDst+kL3WsZ+NoNuzDYk5jYQNUIV9A==
"@tailwindcss/oxide-freebsd-x64@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-0.0.0-insiders.9faf109.tgz#de09732e1aefa3c1046b2d0a022091f92685aa0b"
integrity sha512-FGh/kwk2oVYJXed+GCmVkqv0COYdNwYBeW5rLVumHT8S5yyZo7xQtEto9ehKjfHuhuN49rD5asgtNK2/pbCN0Q==
"@tailwindcss/oxide-linux-arm-gnueabihf@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-0.0.0-insiders.9faf109.tgz#8db1007aec8c08b150262d3c4733cce238bafb68"
integrity sha512-7rN2EuKTfZpAajhWRf+Qe0YgnomRpBYFwjL8KV8RvZ9z3rRlO1hG72JKLEegCoCH6atTqKO1c8oGRMZMhPQFDg==
"@tailwindcss/oxide-linux-arm64-gnu@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-0.0.0-insiders.9faf109.tgz#f23a8b1a6f1e8fd2cdd71c612f39e2a14bf7e7ec"
integrity sha512-4pczdi52hqp2Hm9zO0OtMQyAr6JWrKNIoMF/UQQ2H61iBfaNmvaqWdpo4OVx2MjFq983nvLr8/fLC06cJp/zpA==
"@tailwindcss/oxide-linux-arm64-musl@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-0.0.0-insiders.9faf109.tgz#d0c155b72e88dd9556e04cc7162123009a1132af"
integrity sha512-OHGesCnEx8sMvle3mNLHVveDXCNIM2nlFyBxoHb7Xj58bmbgwDBXYI0GDl/KxhV+zYhGJxJm1K5+7XbUBXfgYw==
"@tailwindcss/oxide-linux-x64-gnu@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-0.0.0-insiders.9faf109.tgz#02b1d2470c020494f8cf547bca236779bb23ab4e"
integrity sha512-cSmIxJrPsns31RcWHah2XxQs1BBfyWy4Q7hTKen8aIGfYhjY9jhXVt0jeMm64ZOmmWzirV9y4Qkt4nt9ULSU2w==
"@tailwindcss/oxide-linux-x64-musl@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-0.0.0-insiders.9faf109.tgz#3771a21c87c511944d8bb341e0a6abd60f477f4b"
integrity sha512-zvNLNfmPtoXQnEsVnRh+dkeJCMqa6/XVYG3eSlejSzzKcz9+ZM6XXl+LBkm15a9gGYvew8jfQ9IsohIexErCbw==
"@tailwindcss/oxide-win32-x64-msvc@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-0.0.0-insiders.9faf109.tgz#df51683c2d82cd743e3503e83de270e84b54ab8e"
integrity sha512-VVIGzP3WB/xfOqf9QJRvtvs2uWqq3VAxYjdR6ilIxP6tn3cbJdOjqN7w9c8dhzgHAulEmBgW1Ea1roXeZKd39g==
"@tailwindcss/oxide@0.0.0-insiders.9faf109":
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-0.0.0-insiders.9faf109.tgz#031ec5a8d731e58b4782918d3a9d9a4315e69fe7"
integrity sha512-MsYO1lUlUigkiQCcqhS+V2OdseN59fn1odJpraX8Nqk70Xfz/6U2JS0mcXsCOYrtPTt0u6zTSxTojEc8MYPlaA==
optionalDependencies:
"@tailwindcss/oxide-darwin-arm64" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-darwin-x64" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-freebsd-x64" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-linux-arm-gnueabihf" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-linux-arm64-gnu" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-linux-arm64-musl" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-linux-x64-gnu" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-linux-x64-musl" "0.0.0-insiders.9faf109"
"@tailwindcss/oxide-win32-x64-msvc" "0.0.0-insiders.9faf109"
"@tailwindcss/typography@^0.5.2":
version "0.5.2"
resolved "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz"
@@ -1001,6 +941,25 @@
lodash.isplainobject "^4.0.6"
lodash.merge "^4.6.2"
"@tanstack/react-virtual@^3.0.0-beta.60":
version "3.0.0-beta.60"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.60.tgz#2b37c0d72997a54f7927f6b159a77311429fec1e"
integrity sha512-F0wL9+byp7lf/tH6U5LW0ZjBqs+hrMXJrj5xcIGcklI0pggvjzMNW9DdIBcyltPNr6hmHQ0wt8FDGe1n1ZAThA==
dependencies:
"@tanstack/virtual-core" "3.0.0-beta.60"
"@tanstack/virtual-core@3.0.0-beta.60":
version "3.0.0-beta.60"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.60.tgz#fcac07cb182d41929208899062de8c9510cf42ed"
integrity sha512-QlCdhsV1+JIf0c0U6ge6SQmpwsyAT0oQaOSZk50AtEeAyQl9tQrd6qCHAslxQpgphrfe945abvKG8uYvw3hIGA==
"@tanstack/vue-virtual@^3.0.0-beta.60":
version "3.0.0-beta.60"
resolved "https://registry.yarnpkg.com/@tanstack/vue-virtual/-/vue-virtual-3.0.0-beta.60.tgz#f32c41f1b5dfacc40f8d427874947a24f71aba60"
integrity sha512-sJdNB4IAHzM8a4rEozQlp7RjXJ/0nFf9tIaJNfJ1mCygORCmoJBBoepvkVSgzPLxJROQNNNm2sSlp+2d+R15rw==
dependencies:
"@tanstack/virtual-core" "3.0.0-beta.60"
"@testing-library/dom@^7.26.6":
version "7.31.2"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz"
@@ -1680,16 +1639,6 @@ browserslist@^4.20.3:
node-releases "^2.0.3"
picocolors "^1.0.0"
browserslist@^4.21.10:
version "4.21.10"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
dependencies:
caniuse-lite "^1.0.30001517"
electron-to-chromium "^1.4.477"
node-releases "^2.0.13"
update-browserslist-db "^1.0.11"
bser@2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz"
@@ -1760,11 +1709,6 @@ caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz"
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
caniuse-lite@^1.0.30001517:
version "1.0.30001532"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz#c6a4d5d2da6d2b967f0ee5e12e7f680db6ad2fca"
integrity sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==
capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz"
@@ -2161,11 +2105,6 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz"
@@ -2242,11 +2181,6 @@ electron-to-chromium@^1.4.17:
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.49.tgz"
integrity sha512-k/0t1TRfonHIp8TJKfjBu2cKj8MqYTiEpOhci+q7CVEE5xnCQnx1pTa+V8b/sdhe4S3PR4p4iceEQWhGrKQORQ==
electron-to-chromium@^1.4.477:
version "1.4.513"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz#41a50bf749aa7d8058ffbf7a131fc3327a7b1675"
integrity sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==
emittery@^0.7.1:
version "0.7.2"
resolved "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz"
@@ -2647,17 +2581,6 @@ fast-glob@^3.2.12:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
@@ -3828,7 +3751,7 @@ jest@26:
import-local "^3.0.2"
jest-cli "^26.6.3"
jiti@^1.19.3:
jiti@^1.18.2:
version "1.20.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
@@ -3953,68 +3876,6 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lightningcss-darwin-arm64@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.8.tgz#b4ea8d5133236bff361623ce8c30639a1b024240"
integrity sha512-BOMoGfcgkk2f4ltzsJqmkjiqRtlZUK+UdwhR+P6VgIsnpQBV3G01mlL6GzYxYqxq+6/3/n/D+4oy2NeknmADZw==
lightningcss-darwin-x64@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.8.tgz#81f4671cf9c245bb25a6536c01ddac76973fd283"
integrity sha512-YhF64mcVDPKKufL4aNFBnVH7uvzE0bW3YUsPXdP4yUcT/8IXChypOZ/PE1pmt2RlbmsyVuuIIeZU4zTyZe5Amw==
lightningcss-freebsd-x64@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.21.8.tgz#d1b18c5a1b894e1332b23870afdbe23d07f22614"
integrity sha512-CV6A/vTG2Ryd3YpChEgfWWv4TXCAETo9TcHSNx0IP0dnKcnDEiAko4PIKhCqZL11IGdN1ZLBCVPw+vw5ZYwzfA==
lightningcss-linux-arm-gnueabihf@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.8.tgz#523366a683d3545d3a36c133079ff6af0a3d95c0"
integrity sha512-9PMbqh8n/Xq0F4/j2NR/hHM2HRDiFXFSF0iOvV67pNWKJkHIO6mR8jBw/88Aro5Ye/ILsX5OuWsxIVJDFv0NXA==
lightningcss-linux-arm64-gnu@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.8.tgz#6a74eff0680dd0759591962a3b92353f9b2bf49a"
integrity sha512-JTM/TuMMllkzaXV7/eDjG4IJKLlCl+RfYZwtsVmC82gc0QX0O37csGAcY2OGleiuA4DnEo/Qea5WoFfZUNC6zg==
lightningcss-linux-arm64-musl@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.8.tgz#98c74b70d99e08efb3cc6dacd0c57d516a15c2e7"
integrity sha512-01gWShXrgoIb8urzShpn1RWtZuaSyKSzF2hfO+flzlTPoACqcO3rgcu/3af4Cw54e8vKzL5hPRo4kROmgaOMLg==
lightningcss-linux-x64-gnu@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.8.tgz#96c691c0852eaae9b6a15d238b7bdd9fbfc3cc85"
integrity sha512-yVB5vYJjJb/Aku0V9QaGYIntvK/1TJOlNB9GmkNpXX5bSSP2pYW4lWW97jxFMHO908M0zjEt1qyOLMyqojHL+Q==
lightningcss-linux-x64-musl@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.8.tgz#19787f71eeabdcec34af6e74509a2902548d45f9"
integrity sha512-TYi+KNtBVK0+FZvxTX/d5XJb+tw3Jq+2Rr9hW359wp1afsi1Vkg+uVGgbn+m2dipa5XwpCseQq81ylMlXuyfPw==
lightningcss-win32-x64-msvc@1.21.8:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.8.tgz#eb10b607b464bd19c966de0065c95ff47e6acb1b"
integrity sha512-mww+kqbPx0/C44l2LEloECtRUuOFDjq9ftp+EHTPiCp2t+avy0sh8MaFwGsrKkj2XfZhaRhi4CPVKBoqF1Qlwg==
lightningcss@^1.21.7:
version "1.21.8"
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.21.8.tgz#a02e4a8979208ffb61d7c6deebb75c4abce0b5d6"
integrity sha512-jEqaL7m/ZckZJjlMAfycr1Kpz7f93k6n7KGF5SJjuPSm6DWI6h3ayLZmgRHgy1OfrwoCed6h4C/gHYPOd1OFMA==
dependencies:
detect-libc "^1.0.3"
optionalDependencies:
lightningcss-darwin-arm64 "1.21.8"
lightningcss-darwin-x64 "1.21.8"
lightningcss-freebsd-x64 "1.21.8"
lightningcss-linux-arm-gnueabihf "1.21.8"
lightningcss-linux-arm64-gnu "1.21.8"
lightningcss-linux-arm64-musl "1.21.8"
lightningcss-linux-x64-gnu "1.21.8"
lightningcss-linux-x64-musl "1.21.8"
lightningcss-win32-x64-msvc "1.21.8"
lilconfig@2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz"
@@ -4395,11 +4256,6 @@ node-releases@^2.0.1:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz"
integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
node-releases@^2.0.13:
version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
node-releases@^2.0.3:
version "2.0.4"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz"
@@ -4823,14 +4679,6 @@ postcss-selector-parser@^6.0.11:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.12:
version "6.0.13"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
@@ -4881,7 +4729,7 @@ postcss@^8.4.16:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.28:
postcss@^8.4.23:
version "8.4.29"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd"
integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==
@@ -5161,7 +5009,7 @@ resolve@^1.10.0, resolve@^1.18.1:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^1.22.4:
resolve@^1.22.2:
version "1.22.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==
@@ -5667,7 +5515,7 @@ styled-jsx@5.0.1:
resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz"
integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==
sucrase@^3.34.0:
sucrase@^3.32.0:
version "3.34.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
@@ -5722,38 +5570,6 @@ tabbable@^6.0.1:
resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwindcss@^0.0.0-insiders.9faf109:
version "0.0.0-insiders.9faf109"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-0.0.0-insiders.9faf109.tgz#d0dfe2f0a5013dae9eab576a9503878bad1d12f4"
integrity sha512-WJ++yMXHE9TvwxU8Pl7Cw9tBQtCmtaH72XOwfGfhpI0pT9NPYzO3I+8PT+SNAwAwy/CMPYUn9Nm8UoZ8EHC3lw==
dependencies:
"@alloc/quick-lru" "^5.2.0"
"@tailwindcss/oxide" "0.0.0-insiders.9faf109"
arg "^5.0.2"
browserslist "^4.21.10"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.3.1"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.19.3"
lightningcss "^1.21.7"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.28"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.12"
postcss-value-parser "^4.2.0"
resolve "^1.22.4"
sucrase "^3.34.0"
tailwindcss@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz"
@@ -5783,6 +5599,34 @@ tailwindcss@^3.2.7:
quick-lru "^5.1.1"
resolve "^1.22.1"
tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.18.2"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.23"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
resolve "^1.22.2"
sucrase "^3.32.0"
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz"
@@ -5969,14 +5813,6 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
update-browserslist-db@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz"
@@ -6222,9 +6058,9 @@ yaml@^1.10.0, yaml@^1.10.2:
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.1.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144"
integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==
version "2.3.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
yargs-parser@^18.1.2:
version "18.1.3"