Skip to content

Commit

Permalink
Adaptive width using JS (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
ycs77 committed Oct 15, 2022
1 parent 901df7c commit 1ec4a58
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 50 deletions.
4 changes: 3 additions & 1 deletion examples/example-react-ts/src/components/ExampleListbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ export default function ExampleListbox() {
<Listbox value={selected} onChange={setSelected}>
<Float
as="div"
className="relative w-56"
className="relative w-full"
placement="bottom"
offset={4}
flip={10}
floatingAs={Fragment}
adaptiveWidth
portal
>
<Listbox.Button className="relative w-full bg-white pl-3.5 pr-10 py-2 text-left text-amber-500 text-sm leading-5 border border-gray-200 rounded-lg shadow-md">
{selected.name}
Expand Down
4 changes: 3 additions & 1 deletion examples/example-react/src/components/ExampleListbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ export default function ExampleListbox() {
<Listbox value={selected} onChange={setSelected}>
<Float
as="div"
className="relative w-56"
className="relative w-full"
placement="bottom"
offset={4}
flip={10}
floatingAs={Fragment}
adaptiveWidth
portal
>
<Listbox.Button className="relative w-full bg-white pl-3.5 pr-10 py-2 text-left text-amber-500 text-sm leading-5 border border-gray-200 rounded-lg shadow-md">
{selected.name}
Expand Down
4 changes: 3 additions & 1 deletion examples/example-vue-ts/src/components/ExampleListbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
<Listbox v-model="selected">
<Float
as="div"
class="relative w-56"
class="relative w-full"
placement="bottom"
:offset="4"
:flip="10"
floating-as="template"
adaptive-width
portal
>
<ListboxButton class="relative w-full bg-white pl-3.5 pr-10 py-2 text-left text-amber-500 text-sm leading-5 border border-gray-200 rounded-lg shadow-md">
{{ selected.name }}
Expand Down
4 changes: 3 additions & 1 deletion examples/example-vue/src/components/ExampleListbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
<Listbox v-model="selected">
<Float
as="div"
class="relative w-56"
class="relative w-full"
placement="bottom"
:offset="4"
:flip="10"
floating-as="template"
adaptive-width
portal
>
<ListboxButton class="relative w-full bg-white pl-3.5 pr-10 py-2 text-left text-amber-500 text-sm leading-5 border border-gray-200 rounded-lg shadow-md">
{{ selected.name }}
Expand Down
82 changes: 61 additions & 21 deletions packages/react/src/float.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import { useId } from './hooks/use-id'
import { useIsoMorphicEffect } from './hooks/use-iso-morphic-effect'
import { type OriginClassResolver, tailwindcssOriginClassResolver } from './origin-class-resolvers'

const autoUpdateCleanerMap = new Map<ReturnType<typeof useId>, (() => void)>()
const showStateMap = new Map<ReturnType<typeof useId>, boolean>()
const autoUpdateCleanerMap = new Map<ReturnType<typeof useId>, (() => void)>()
const referenceElResizeObserveCleanerMap = new Map<ReturnType<typeof useId>, (() => void)>()

interface ArrowState {
arrowRef: RefObject<HTMLElement>
placement: Placement
x: number | undefined
y: number | undefined
x: number | null
y: number | null
}

const ArrowContext = createContext<ArrowState | null>(null)
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface FloatProps {
tailwindcssOriginClass?: boolean
portal?: boolean | string
transform?: boolean
adaptiveWidth?: boolean
middleware?: Middleware[] | ((refs: {
referenceEl: MutableRefObject<Element | VirtualElement | null>
floatingEl: MutableRefObject<HTMLElement | null>
Expand Down Expand Up @@ -102,7 +104,7 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {
update: props.onUpdate || (() => {}),
}

const { x, y, placement, strategy, reference, floating, update, refs, middlewareData } = useFloating({
const { x, y, placement, strategy, reference, floating, update, refs, middlewareData } = useFloating<HTMLElement>({
placement: props.placement || 'bottom-start',
strategy: props.strategy,
middleware,
Expand All @@ -119,6 +121,8 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {
return ''
}, [props.originClass, props.tailwindcssOriginClass])

const [referenceElWidth, setReferenceElWidth] = useState<number | null>(null)

const updateFloating = useCallback(() => {
update()
events.update()
Expand Down Expand Up @@ -212,6 +216,32 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {
}
}

function startReferenceElResizeObserver() {
if (props.adaptiveWidth &&
window &&
'ResizeObserver' in window &&
refs.reference.current &&
!referenceElResizeObserveCleanerMap.get(id)
) {
const observer = new ResizeObserver(([entry]) => {
const width = entry.borderBoxSize.reduce((acc, { inlineSize }) => acc + inlineSize, 0)
setReferenceElWidth(width)
})
observer.observe(refs.reference.current)
referenceElResizeObserveCleanerMap.set(id, () => {
observer.disconnect()
})
}
}

function clearReferenceElResizeObserver() {
const disconnectResizeObserver = referenceElResizeObserveCleanerMap.get(id)
if (disconnectResizeObserver) {
disconnectResizeObserver()
referenceElResizeObserveCleanerMap.delete(id)
}
}

useIsoMorphicEffect(() => {
if (refs.reference.current &&
refs.floating.current &&
Expand All @@ -221,8 +251,8 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {
showStateMap.set(id, true)

// show...
events.show()
startAutoUpdate()
events.show()
} else if (
show === false &&
showStateMap.get(id) &&
Expand All @@ -239,13 +269,18 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {

useEffect(() => {
setIsMounted(true)
startReferenceElResizeObserver()

return () => {
clearReferenceElResizeObserver()
}
}, [])

const arrowApi = {
arrowRef,
placement,
x: middlewareData.arrow?.x,
y: middlewareData.arrow?.y,
x: middlewareData.arrow?.x ?? null,
y: middlewareData.arrow?.y ?? null,
} as ArrowState

const [ReferenceNode, FloatingNode] = props.children
Expand All @@ -272,19 +307,24 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {

const floatingProps = {
ref: floating,
style: props.transform || props.transform === undefined ? {
position: strategy,
zIndex: props.zIndex || 9999,
top: 0,
left: 0,
right: 'auto',
bottom: 'auto',
transform: `translate(${Math.round(x || 0)}px,${Math.round(y || 0)}px)`,
} : {
position: strategy,
zIndex: props.zIndex || 9999,
top: `${y || 0}px`,
left: `${x || 0}px`,
style: {
...(props.transform || props.transform === undefined ? {
position: strategy,
zIndex: props.zIndex || 9999,
top: 0,
left: 0,
right: 'auto',
bottom: 'auto',
transform: `translate(${Math.round(x || 0)}px,${Math.round(y || 0)}px)`,
} : {
position: strategy,
zIndex: props.zIndex || 9999,
top: `${y || 0}px`,
left: `${x || 0}px`,
}),
width: props.adaptiveWidth && typeof referenceElWidth === 'number'
? `${referenceElWidth}px`
: null,
},
}

Expand All @@ -303,7 +343,7 @@ const FloatRoot = forwardRef<ElementType, FloatProps>((props, ref) => {

function renderPortal(children: ReactElement) {
if (isMounted && props.portal) {
const root = document?.querySelector(props.portal === true ? 'body' : props.portal)
const root = document.querySelector(props.portal === true ? 'body' : props.portal)
if (root) {
return createPortal(children, root)
}
Expand Down
94 changes: 69 additions & 25 deletions packages/vue/src/float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
defineComponent,
h,
inject,
onBeforeUnmount,
onMounted,
provide,
ref,
Expand All @@ -32,8 +33,8 @@ import { type OriginClassResolver, tailwindcssOriginClassResolver } from './orig
interface ArrowState {
ref: Ref<HTMLElement | null>
placement: Ref<Placement>
x: Ref<number | undefined>
y: Ref<number | undefined>
x: Ref<number | null>
y: Ref<number | null>
}

const ArrowContext = Symbol('ArrowState') as InjectionKey<ArrowState>
Expand Down Expand Up @@ -76,6 +77,7 @@ export interface FloatPropsType {
tailwindcssOriginClass?: boolean
portal?: boolean | string
transform?: boolean
adaptiveWidth?: boolean
middleware?: Middleware[] | ((refs: {
referenceEl: Ref<HTMLElement | null>
floatingEl: Ref<HTMLElement | null>
Expand Down Expand Up @@ -153,6 +155,10 @@ export const FloatProps = {
type: Boolean,
default: true,
},
adaptiveWidth: {
type: Boolean,
default: false,
},
middleware: {
type: [Array, Function] as PropType<Middleware[] | ((refs: {
referenceEl: Ref<HTMLElement | null>
Expand All @@ -175,8 +181,8 @@ export const Float = defineComponent({
const middleware = shallowRef(undefined) as ShallowRef<Middleware[] | undefined>

const arrowRef = ref(null) as Ref<HTMLElement | null>
const arrowX = ref<number | undefined>(undefined)
const arrowY = ref<number | undefined>(undefined)
const arrowX = ref<number | null>(null)
const arrowY = ref<number | null>(null)

const { x, y, placement, strategy, reference, floating, middlewareData, update } = useFloating({
placement: propPlacement,
Expand All @@ -198,18 +204,21 @@ export const Float = defineComponent({
const referenceEl = ref(dom(reference)) as Ref<HTMLElement | null>
const floatingEl = ref(dom(floating)) as Ref<HTMLElement | null>

const referenceElWidth = ref<number | null>(null)

function updateElements() {
referenceEl.value = dom(reference)
floatingEl.value = dom(floating)
}

function updateFloating() {
if (
!isVisibleDOMElement(referenceEl) ||
!isVisibleDOMElement(floatingEl)
) return
update()
emit('update')
isVisibleDOMElement(referenceEl) &&
isVisibleDOMElement(floatingEl)
) {
update()
emit('update')
}
}

watch(propPlacement, () => {
Expand Down Expand Up @@ -290,8 +299,8 @@ export const Float = defineComponent({

watch(middlewareData, () => {
const arrowData = middlewareData.value.arrow as { x?: number, y?: number }
arrowX.value = arrowData?.x
arrowY.value = arrowData?.y
arrowX.value = arrowData?.x ?? null
arrowY.value = arrowData?.y ?? null
})

let disposeAutoUpdate: (() => void) | undefined
Expand Down Expand Up @@ -319,6 +328,31 @@ export const Float = defineComponent({
}
}

let referenceElResizeObserver: ResizeObserver | undefined

function startReferenceElResizeObserver() {
updateElements()

if (props.adaptiveWidth &&
window &&
'ResizeObserver' in window &&
referenceEl.value
) {
referenceElResizeObserver = new ResizeObserver(([entry]) => {
referenceElWidth.value = entry.borderBoxSize.reduce((acc, { inlineSize }) => acc + inlineSize, 0)
})
referenceElResizeObserver.observe(referenceEl.value)
}
}

function clearReferenceElResizeObserver() {
if (referenceElResizeObserver) {
referenceElResizeObserver.disconnect()
referenceElResizeObserver = undefined
referenceElWidth.value = null
}
}

function handleShow() {
updateElements()

Expand All @@ -327,8 +361,8 @@ export const Float = defineComponent({
show.value === true
) {
// show...
emit('show')
startAutoUpdate()
emit('show')
} else if (show.value === false && disposeAutoUpdate) {
// hide...
clearAutoUpdate()
Expand All @@ -340,9 +374,14 @@ export const Float = defineComponent({

onMounted(() => {
isMounted.value = true
startReferenceElResizeObserver()
handleShow()
})

onBeforeUnmount(() => {
clearReferenceElResizeObserver()
})

const arrowApi = {
ref: arrowRef,
placement,
Expand Down Expand Up @@ -388,19 +427,24 @@ export const Float = defineComponent({

const floatingProps = {
ref: floating,
style: props.transform ? {
position: strategy.value,
zIndex: props.zIndex,
top: '0',
left: '0',
right: 'auto',
bottom: 'auto',
transform: `translate(${Math.round(x.value || 0)}px,${Math.round(y.value || 0)}px)`,
} : {
position: strategy.value,
zIndex: props.zIndex,
top: `${y.value || 0}px`,
left: `${x.value || 0}px`,
style: {
...(props.transform ? {
position: strategy.value,
zIndex: props.zIndex,
top: '0',
left: '0',
right: 'auto',
bottom: 'auto',
transform: `translate(${Math.round(x.value || 0)}px,${Math.round(y.value || 0)}px)`,
} : {
position: strategy.value,
zIndex: props.zIndex,
top: `${y.value || 0}px`,
left: `${x.value || 0}px`,
}),
width: props.adaptiveWidth && typeof referenceElWidth.value === 'number'
? `${referenceElWidth.value}px`
: null,
},
}

Expand Down

0 comments on commit 1ec4a58

Please sign in to comment.