Merge pull request #15447 from Budibase/bbui-typing-1

Type Checkbox.svelte, CheckboxGroup.svelte, and Combobox.svelte.
This commit is contained in:
Sam Rose 2025-01-28 09:30:55 +00:00 committed by GitHub
commit a8c639687e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 143 additions and 99 deletions

View File

@ -8,27 +8,52 @@
// Strategies are defined as [Popover]To[Anchor].
// They can apply for both horizontal and vertical alignment.
const Strategies = {
StartToStart: "StartToStart", // e.g. left alignment
EndToEnd: "EndToEnd", // e.g. right alignment
StartToEnd: "StartToEnd", // e.g. right-outside alignment
EndToStart: "EndToStart", // e.g. left-outside alignment
MidPoint: "MidPoint", // centers relative to midpoints
ScreenEdge: "ScreenEdge", // locks to screen edge
type Strategy =
| "StartToStart"
| "EndToEnd"
| "StartToEnd"
| "EndToStart"
| "MidPoint"
| "ScreenEdge"
export interface Styles {
maxHeight?: number
minWidth?: number
maxWidth?: number
offset?: number
left: number
top: number
}
export default function positionDropdown(element, opts) {
let resizeObserver
export type UpdateHandler = (
anchorBounds: DOMRect,
elementBounds: DOMRect,
styles: Styles
) => Styles
interface Opts {
anchor?: HTMLElement
align: string
maxHeight?: number
maxWidth?: number
minWidth?: number
useAnchorWidth: boolean
offset: number
customUpdate?: UpdateHandler
resizable: boolean
wrap: boolean
}
export default function positionDropdown(element: HTMLElement, opts: Opts) {
let resizeObserver: ResizeObserver
let latestOpts = opts
// We need a static reference to this function so that we can properly
// clean up the scroll listener.
const scrollUpdate = () => {
updatePosition(latestOpts)
}
const scrollUpdate = () => updatePosition(latestOpts)
// Updates the position of the dropdown
const updatePosition = opts => {
const updatePosition = (opts: Opts) => {
const {
anchor,
align,
@ -51,12 +76,12 @@ export default function positionDropdown(element, opts) {
const winWidth = window.innerWidth
const winHeight = window.innerHeight
const screenOffset = 8
let styles = {
let styles: Styles = {
maxHeight,
minWidth: useAnchorWidth ? anchorBounds.width : minWidth,
maxWidth: useAnchorWidth ? anchorBounds.width : maxWidth,
left: null,
top: null,
left: 0,
top: 0,
}
// Ignore all our logic for custom logic
@ -81,67 +106,67 @@ export default function positionDropdown(element, opts) {
}
// Applies a dynamic max height constraint if appropriate
const applyMaxHeight = height => {
const applyMaxHeight = (height: number) => {
if (!styles.maxHeight && resizable) {
styles.maxHeight = height
}
}
// Applies the X strategy to our styles
const applyXStrategy = strategy => {
const applyXStrategy = (strategy: Strategy) => {
switch (strategy) {
case Strategies.StartToStart:
case "StartToStart":
default:
styles.left = anchorBounds.left
break
case Strategies.EndToEnd:
case "EndToEnd":
styles.left = anchorBounds.right - elementBounds.width
break
case Strategies.StartToEnd:
case "StartToEnd":
styles.left = anchorBounds.right + offset
break
case Strategies.EndToStart:
case "EndToStart":
styles.left = anchorBounds.left - elementBounds.width - offset
break
case Strategies.MidPoint:
case "MidPoint":
styles.left =
anchorBounds.left +
anchorBounds.width / 2 -
elementBounds.width / 2
break
case Strategies.ScreenEdge:
case "ScreenEdge":
styles.left = winWidth - elementBounds.width - screenOffset
break
}
}
// Applies the Y strategy to our styles
const applyYStrategy = strategy => {
const applyYStrategy = (strategy: Strategy) => {
switch (strategy) {
case Strategies.StartToStart:
case "StartToStart":
styles.top = anchorBounds.top
applyMaxHeight(winHeight - anchorBounds.top - screenOffset)
break
case Strategies.EndToEnd:
case "EndToEnd":
styles.top = anchorBounds.bottom - elementBounds.height
applyMaxHeight(anchorBounds.bottom - screenOffset)
break
case Strategies.StartToEnd:
case "StartToEnd":
default:
styles.top = anchorBounds.bottom + offset
applyMaxHeight(winHeight - anchorBounds.bottom - screenOffset)
break
case Strategies.EndToStart:
case "EndToStart":
styles.top = anchorBounds.top - elementBounds.height - offset
applyMaxHeight(anchorBounds.top - screenOffset)
break
case Strategies.MidPoint:
case "MidPoint":
styles.top =
anchorBounds.top +
anchorBounds.height / 2 -
elementBounds.height / 2
break
case Strategies.ScreenEdge:
case "ScreenEdge":
styles.top = winHeight - elementBounds.height - screenOffset
applyMaxHeight(winHeight - 2 * screenOffset)
break
@ -150,81 +175,78 @@ export default function positionDropdown(element, opts) {
// Determine X strategy
if (align === "right") {
applyXStrategy(Strategies.EndToEnd)
applyXStrategy("EndToEnd")
} else if (align === "right-outside" || align === "right-context-menu") {
applyXStrategy(Strategies.StartToEnd)
applyXStrategy("StartToEnd")
} else if (align === "left-outside" || align === "left-context-menu") {
applyXStrategy(Strategies.EndToStart)
applyXStrategy("EndToStart")
} else if (align === "center") {
applyXStrategy(Strategies.MidPoint)
applyXStrategy("MidPoint")
} else {
applyXStrategy(Strategies.StartToStart)
applyXStrategy("StartToStart")
}
// Determine Y strategy
if (align === "right-outside" || align === "left-outside") {
applyYStrategy(Strategies.MidPoint)
applyYStrategy("MidPoint")
} else if (
align === "right-context-menu" ||
align === "left-context-menu"
) {
applyYStrategy(Strategies.StartToStart)
styles.top -= 5 // Manual adjustment for action menu padding
applyYStrategy("StartToStart")
if (styles.top) {
styles.top -= 5 // Manual adjustment for action menu padding
}
} else {
applyYStrategy(Strategies.StartToEnd)
applyYStrategy("StartToEnd")
}
// Handle screen overflow
if (doesXOverflow()) {
// Swap left to right
if (align === "left") {
applyXStrategy(Strategies.EndToEnd)
applyXStrategy("EndToEnd")
}
// Swap right-outside to left-outside
else if (align === "right-outside") {
applyXStrategy(Strategies.EndToStart)
applyXStrategy("EndToStart")
}
}
if (doesYOverflow()) {
// If wrapping, lock to the bottom of the screen and also reposition to
// the side to not block the anchor
if (wrap) {
applyYStrategy(Strategies.MidPoint)
applyYStrategy("MidPoint")
if (doesYOverflow()) {
applyYStrategy(Strategies.ScreenEdge)
applyYStrategy("ScreenEdge")
}
applyXStrategy(Strategies.StartToEnd)
applyXStrategy("StartToEnd")
if (doesXOverflow()) {
applyXStrategy(Strategies.EndToStart)
applyXStrategy("EndToStart")
}
}
// Othewise invert as normal
else {
// If using an outside strategy then lock to the bottom of the screen
if (align === "left-outside" || align === "right-outside") {
applyYStrategy(Strategies.ScreenEdge)
applyYStrategy("ScreenEdge")
}
// Otherwise flip above
else {
applyYStrategy(Strategies.EndToStart)
applyYStrategy("EndToStart")
}
}
}
}
// Apply styles
Object.entries(styles).forEach(([style, value]) => {
if (value != null) {
element.style[style] = `${value.toFixed(0)}px`
} else {
element.style[style] = null
}
})
for (const [key, value] of Object.entries(styles)) {
element.style.setProperty(key, value ? `${value}px` : null)
}
}
// The actual svelte action callback which creates observers on the relevant
// DOM elements
const update = newOpts => {
const update = (newOpts: Opts) => {
latestOpts = newOpts
// Cleanup old state

View File

@ -1,22 +1,23 @@
<script>
<script lang="ts">
import "@spectrum-css/checkbox/dist/index-vars.css"
import "@spectrum-css/fieldgroup/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import type { ChangeEventHandler } from "svelte/elements"
export let value = false
export let id = null
export let text = null
export let id: string | undefined = undefined
export let text: string | undefined = undefined
export let disabled = false
export let readonly = false
export let size
export let size: "S" | "M" | "L" | "XL" = "M"
export let indeterminate = false
const dispatch = createEventDispatcher()
const onChange = event => {
dispatch("change", event.target.checked)
const onChange: ChangeEventHandler<HTMLInputElement> = event => {
dispatch("change", event.currentTarget.checked)
}
$: sizeClass = `spectrum-Checkbox--size${size || "M"}`
$: sizeClass = `spectrum-Checkbox--size${size}`
</script>
<label

View File

@ -1,19 +1,24 @@
<script>
<script lang="ts" context="module">
type O = any
type V = any
</script>
<script lang="ts" generics="O, V">
import "@spectrum-css/fieldgroup/dist/index-vars.css"
import "@spectrum-css/radio/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let direction = "vertical"
export let value = []
export let options = []
export let direction: "horizontal" | "vertical" = "vertical"
export let value: V[] = []
export let options: O[] = []
export let disabled = false
export let readonly = false
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionLabel = (option: O) => `${option}`
export let getOptionValue = (option: O) => option as unknown as V
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{ change: V[] }>()
const onChange = optionValue => {
const onChange = (optionValue: V) => {
if (!value.includes(optionValue)) {
dispatch("change", [...value, optionValue])
} else {

View File

@ -1,4 +1,10 @@
<script>
<script lang="ts" context="module">
type O = any
</script>
<script lang="ts" generics="O">
import type { ChangeEventHandler } from "svelte/elements"
import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css"
@ -6,33 +12,38 @@
import clickOutside from "../../Actions/click_outside"
import Popover from "../../Popover/Popover.svelte"
export let value = null
export let id = null
export let value: string | undefined = undefined
export let id: string | undefined = undefined
export let placeholder = "Choose an option or type"
export let disabled = false
export let readonly = false
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let options: O[] = []
export let getOptionLabel = (option: O) => `${option}`
export let getOptionValue = (option: O) => `${option}`
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
change: string
blur: void
type: string
pick: string
}>()
let open = false
let focus = false
let anchor
const selectOption = value => {
const selectOption = (value: string) => {
dispatch("change", value)
open = false
}
const onType = e => {
const value = e.target.value
const onType: ChangeEventHandler<HTMLInputElement> = e => {
const value = e.currentTarget.value
dispatch("type", value)
selectOption(value)
}
const onPick = value => {
const onPick = (value: string) => {
dispatch("pick", value)
selectOption(value)
}

View File

@ -1,28 +1,33 @@
<script>
<script lang="ts">
import "@spectrum-css/popover/dist/index-vars.css"
// @ts-expect-error no types for the version of svelte-portal we're on.
import Portal from "svelte-portal"
import { createEventDispatcher, getContext, onDestroy } from "svelte"
import positionDropdown from "../Actions/position_dropdown"
import positionDropdown, {
type UpdateHandler,
} from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
import Context from "../context"
import type { KeyboardEventHandler } from "svelte/elements"
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{ open: void; close: void }>()
export let anchor
export let align = "right"
export let portalTarget
export let minWidth
export let maxWidth
export let maxHeight
export let anchor: HTMLElement
export let align: "left" | "right" | "left-outside" | "right-outside" =
"right"
export let portalTarget: string | undefined = undefined
export let minWidth: number | undefined = undefined
export let maxWidth: number | undefined = undefined
export let maxHeight: number | undefined = undefined
export let open = false
export let useAnchorWidth = false
export let dismissible = true
export let offset = 4
export let customHeight
export let customHeight: string | undefined = undefined
export let animate = true
export let customZindex
export let handlePostionUpdate
export let customZindex: string | undefined = undefined
export let handlePostionUpdate: UpdateHandler | undefined = undefined
export let showPopover = true
export let clickOutsideOverride = false
export let resizable = true
@ -30,7 +35,7 @@
const animationDuration = 260
let timeout
let timeout: ReturnType<typeof setTimeout>
let blockPointerEvents = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
@ -65,13 +70,13 @@
}
}
const handleOutsideClick = e => {
const handleOutsideClick = (e: MouseEvent) => {
if (clickOutsideOverride) {
return
}
if (open) {
// Stop propagation if the source is the anchor
let node = e.target
let node = e.target as Node | null
let fromAnchor = false
while (!fromAnchor && node && node.parentNode) {
fromAnchor = node === anchor
@ -86,7 +91,7 @@
}
}
function handleEscape(e) {
const handleEscape: KeyboardEventHandler<HTMLDivElement> = e => {
if (!clickOutsideOverride) {
return
}