diff --git a/lerna.json b/lerna.json index 13040cb50c..8d7460f053 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.3.1", + "version": "3.3.3", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.ts similarity index 72% rename from packages/bbui/src/Actions/position_dropdown.js rename to packages/bbui/src/Actions/position_dropdown.ts index e95c7dd1b6..303af2b53c 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.ts @@ -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,83 @@ 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]) => { + for (const [key, value] of Object.entries(styles)) { + const name = key as keyof Styles if (value != null) { - element.style[style] = `${value.toFixed(0)}px` + element.style[name] = `${value}px` } else { - element.style[style] = null + element.style[name] = "" } - }) + } } // 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 diff --git a/packages/bbui/src/Form/Core/Checkbox.svelte b/packages/bbui/src/Form/Core/Checkbox.svelte index 21fe00e864..0af9be87d9 100644 --- a/packages/bbui/src/Form/Core/Checkbox.svelte +++ b/packages/bbui/src/Form/Core/Checkbox.svelte @@ -1,22 +1,23 @@ -