UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

245 lines • 12.7 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { getLogicalBoundingClientRect } from '@awsui/component-toolkit/internal'; import { getBreakpointValue } from '../../breakpoints'; import { getOverflowParentDimensions, getOverflowParents } from '../../utils/scrollable-containers'; import styles from './styles.css.js'; const AVAILABLE_SPACE_RESERVE_DEFAULT = 50; const AVAILABLE_SPACE_RESERVE_MOBILE_VERTICAL = 19; // 50 - 31 const AVAILABLE_SPACE_RESERVE_MOBILE_HORIZONTAL = 20; const getClosestParentDimensions = (element) => { const parents = getOverflowParents(element).map(element => { const { blockSize, inlineSize, insetBlockStart, insetInlineStart } = getLogicalBoundingClientRect(element); return { blockSize, inlineSize, insetBlockStart, insetInlineStart, }; }); return parents.shift(); }; // By default, most dropdowns should expand their content as necessary, but to a maximum of 465px (the XXS breakpoint). // This value was determined by UX but may be subject to change in the future, depending on the feedback. export const defaultMaxDropdownWidth = getBreakpointValue('xxs'); const getAvailableSpace = ({ trigger, overflowParents, stretchWidth = false, stretchHeight = false, isMobile, }) => { const availableSpaceReserveVertical = stretchHeight ? 0 : isMobile ? AVAILABLE_SPACE_RESERVE_MOBILE_VERTICAL : AVAILABLE_SPACE_RESERVE_DEFAULT; const availableSpaceReserveHorizontal = stretchWidth ? 0 : isMobile ? AVAILABLE_SPACE_RESERVE_MOBILE_HORIZONTAL : AVAILABLE_SPACE_RESERVE_DEFAULT; const { insetBlockEnd: triggerBlockEnd, insetInlineStart: triggerInlineStart, insetInlineEnd: triggerInlineEnd, } = getLogicalBoundingClientRect(trigger); return overflowParents.reduce(({ blockStart, blockEnd, inlineStart, inlineEnd }, overflowParent) => { const offsetTop = triggerBlockEnd - overflowParent.insetBlockStart; const currentBlockStart = offsetTop - trigger.offsetHeight - availableSpaceReserveVertical; const currentBlockEnd = overflowParent.blockSize - offsetTop - availableSpaceReserveVertical; const currentInlineStart = triggerInlineEnd - overflowParent.insetInlineStart - availableSpaceReserveHorizontal; const currentInlineEnd = overflowParent.insetInlineStart + overflowParent.inlineSize - triggerInlineStart - availableSpaceReserveHorizontal; return { blockStart: Math.min(blockStart, currentBlockStart), blockEnd: Math.min(blockEnd, currentBlockEnd), inlineStart: Math.min(inlineStart, currentInlineStart), inlineEnd: Math.min(inlineEnd, currentInlineEnd), }; }, { blockStart: Number.MAX_VALUE, blockEnd: Number.MAX_VALUE, inlineStart: Number.MAX_VALUE, inlineEnd: Number.MAX_VALUE, }); }; const getInteriorAvailableSpace = ({ trigger, overflowParents, isMobile, }) => { const AVAILABLE_SPACE_RESERVE_VERTICAL = isMobile ? AVAILABLE_SPACE_RESERVE_MOBILE_VERTICAL : AVAILABLE_SPACE_RESERVE_DEFAULT; const AVAILABLE_SPACE_RESERVE_HORIZONTAL = isMobile ? AVAILABLE_SPACE_RESERVE_MOBILE_HORIZONTAL : AVAILABLE_SPACE_RESERVE_DEFAULT; const { insetBlockEnd: triggerBlockEnd, insetBlockStart: triggerBlockStart, insetInlineStart: triggerInlineStart, insetInlineEnd: triggerInlineEnd, } = getLogicalBoundingClientRect(trigger); return overflowParents.reduce(({ blockStart, blockEnd, inlineStart, inlineEnd }, overflowParent) => { const currentBlockStart = triggerBlockEnd - overflowParent.insetBlockStart - AVAILABLE_SPACE_RESERVE_VERTICAL; const currentBlockEnd = overflowParent.blockSize - triggerBlockStart + overflowParent.insetBlockStart - AVAILABLE_SPACE_RESERVE_VERTICAL; const currentInlineStart = triggerInlineStart - overflowParent.insetInlineStart - AVAILABLE_SPACE_RESERVE_HORIZONTAL; const currentInlineEnd = overflowParent.insetInlineStart + overflowParent.inlineSize - triggerInlineEnd - AVAILABLE_SPACE_RESERVE_HORIZONTAL; return { blockStart: Math.min(blockStart, currentBlockStart), blockEnd: Math.min(blockEnd, currentBlockEnd), inlineStart: Math.min(inlineStart, currentInlineStart), inlineEnd: Math.min(inlineEnd, currentInlineEnd), }; }, { blockStart: Number.MAX_VALUE, blockEnd: Number.MAX_VALUE, inlineStart: Number.MAX_VALUE, inlineEnd: Number.MAX_VALUE, }); }; const getWidths = ({ triggerElement, dropdownElement, desiredMinWidth, stretchBeyondTriggerWidth = false, }) => { // Determine the width of the trigger const { inlineSize: triggerInlineSize } = getLogicalBoundingClientRect(triggerElement); // Minimum width is determined by either an explicit number (desiredMinWidth) or the trigger width const minWidth = desiredMinWidth ? Math.min(triggerInlineSize, desiredMinWidth) : triggerInlineSize; // If stretchBeyondTriggerWidth is true, the maximum width is the XS breakpoint (465px) or the trigger width (if bigger). const maxWidth = stretchBeyondTriggerWidth ? Math.max(defaultMaxDropdownWidth, triggerInlineSize) : Number.MAX_VALUE; // Determine the actual dropdown width, the size that it "wants" to be const { inlineSize: requiredWidth } = getLogicalBoundingClientRect(dropdownElement); // Try to achieve the required/desired width within the given parameters const idealWidth = Math.min(Math.max(requiredWidth, minWidth), maxWidth); return { idealWidth, minWidth, triggerInlineSize }; }; export const hasEnoughSpaceToStretchBeyondTriggerWidth = ({ triggerElement, dropdownElement, desiredMinWidth, expandToViewport, stretchWidth, stretchHeight, isMobile, }) => { const overflowParents = getOverflowParentDimensions({ element: dropdownElement, excludeClosestParent: false, expandToViewport, canExpandOutsideViewport: stretchHeight, }); const { idealWidth } = getWidths({ triggerElement: triggerElement, dropdownElement, desiredMinWidth, stretchBeyondTriggerWidth: true, }); const availableSpace = getAvailableSpace({ trigger: triggerElement, overflowParents, stretchWidth, stretchHeight, isMobile, }); return idealWidth <= availableSpace.inlineStart || idealWidth <= availableSpace.inlineEnd; }; export const getDropdownPosition = ({ triggerElement, dropdownElement, overflowParents, minWidth: desiredMinWidth, preferCenter = false, stretchWidth = false, stretchHeight = false, isMobile = false, stretchBeyondTriggerWidth = false, }) => { // Determine the space available around the dropdown that it can grow in const availableSpace = getAvailableSpace({ trigger: triggerElement, overflowParents, stretchWidth, stretchHeight, isMobile, }); const { idealWidth, minWidth, triggerInlineSize } = getWidths({ triggerElement, dropdownElement, desiredMinWidth, stretchBeyondTriggerWidth, }); let dropInlineStart; let insetInlineStart = null; let inlineSize = idealWidth; //1. Can it be positioned with ideal width to the right? if (idealWidth <= availableSpace.inlineEnd) { dropInlineStart = false; //2. Can it be positioned with ideal width to the left? } else if (idealWidth <= availableSpace.inlineStart) { dropInlineStart = true; //3. Fit into biggest available space either on left or right } else { dropInlineStart = availableSpace.inlineStart > availableSpace.inlineEnd; inlineSize = Math.max(availableSpace.inlineStart, availableSpace.inlineEnd, minWidth); } if (preferCenter) { const spillOver = (idealWidth - triggerInlineSize) / 2; // availableSpace always includes the trigger width, but we want to exclude that const availableOutsideLeft = availableSpace.inlineStart - triggerInlineSize; const availableOutsideRight = availableSpace.inlineEnd - triggerInlineSize; const fitsInCenter = availableOutsideLeft >= spillOver && availableOutsideRight >= spillOver; if (fitsInCenter) { insetInlineStart = -spillOver; } } const dropBlockStart = availableSpace.blockEnd < dropdownElement.offsetHeight && availableSpace.blockStart > availableSpace.blockEnd; const availableHeight = dropBlockStart ? availableSpace.blockStart : availableSpace.blockEnd; // Try and crop the bottom item when all options can't be displayed, affordance for "there's more" const croppedHeight = Math.max(stretchHeight ? availableHeight : Math.floor(availableHeight / 31) * 31 + 16, 15); return { dropBlockStart, dropInlineStart, insetInlineStart: insetInlineStart === null ? 'auto' : `${insetInlineStart}px`, blockSize: `${croppedHeight}px`, inlineSize: `${inlineSize}px`, }; }; const getInteriorDropdownPosition = (trigger, dropdown, overflowParents, isMobile) => { const availableSpace = getInteriorAvailableSpace({ trigger, overflowParents, isMobile }); const { insetBlockEnd: triggerBlockEnd, insetBlockStart: triggerBlockStart, inlineSize: triggerInlineSize, } = getLogicalBoundingClientRect(trigger); const { insetBlockStart: parentDropdownBlockStart, blockSize: parentDropdownHeight } = getClosestParentDimensions(trigger); let dropInlineStart; let { inlineSize } = getLogicalBoundingClientRect(dropdown); const insetBlockStart = triggerBlockStart - parentDropdownBlockStart; if (inlineSize <= availableSpace.inlineEnd) { dropInlineStart = false; } else if (inlineSize <= availableSpace.inlineStart) { dropInlineStart = true; } else { dropInlineStart = availableSpace.inlineStart > availableSpace.inlineEnd; inlineSize = Math.max(availableSpace.inlineStart, availableSpace.inlineEnd); } const insetInlineStart = dropInlineStart ? 0 - inlineSize : triggerInlineSize; const dropBlockStart = availableSpace.blockEnd < dropdown.offsetHeight && availableSpace.blockStart > availableSpace.blockEnd; const insetBlockEnd = dropBlockStart ? parentDropdownBlockStart + parentDropdownHeight - triggerBlockEnd : 0; const availableHeight = dropBlockStart ? availableSpace.blockStart : availableSpace.blockEnd; // Try and crop the bottom item when all options can't be displayed, affordance for "there's more" const croppedHeight = Math.floor(availableHeight / 31) * 31 + 16; return { dropBlockStart, dropInlineStart, blockSize: `${croppedHeight}px`, inlineSize: `${inlineSize}px`, insetBlockStart: `${insetBlockStart}px`, insetBlockEnd: `${insetBlockEnd}px`, insetInlineStart: `${insetInlineStart}px`, }; }; export const calculatePosition = (dropdownElement, triggerElement, verticalContainerElement, interior, expandToViewport, preferCenter, stretchWidth, stretchHeight, isMobile, minWidth, stretchBeyondTriggerWidth) => { // cleaning previously assigned values, // so that they are not reused in case of screen resize and similar events verticalContainerElement.style.maxBlockSize = ''; dropdownElement.style.inlineSize = ''; dropdownElement.style.insetBlockStart = ''; dropdownElement.style.insetBlockEnd = ''; dropdownElement.style.insetInlineStart = ''; dropdownElement.classList.remove(styles['dropdown-drop-left']); dropdownElement.classList.remove(styles['dropdown-drop-right']); dropdownElement.classList.remove(styles['dropdown-drop-up']); const overflowParents = getOverflowParentDimensions({ element: dropdownElement, excludeClosestParent: interior, expandToViewport, canExpandOutsideViewport: stretchHeight, }); const position = interior ? getInteriorDropdownPosition(triggerElement, dropdownElement, overflowParents, isMobile) : getDropdownPosition({ triggerElement, dropdownElement, overflowParents, minWidth, preferCenter, stretchWidth, stretchHeight, isMobile, stretchBeyondTriggerWidth, }); const triggerBox = getLogicalBoundingClientRect(triggerElement); return [position, triggerBox]; }; //# sourceMappingURL=dropdown-fit-handler.js.map