Merge pull request #15447 from Budibase/bbui-typing-1
Type Checkbox.svelte, CheckboxGroup.svelte, and Combobox.svelte.
This commit is contained in:
commit
a8c639687e
|
@ -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)
|
||||
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
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue