diff --git a/src/components/button/button.e2e.ts b/src/components/button/button.e2e.ts index b7e095a5df1..48a37bd6df2 100644 --- a/src/components/button/button.e2e.ts +++ b/src/components/button/button.e2e.ts @@ -1,5 +1,5 @@ import { E2EElement, newE2EPage } from "@stencil/core/testing"; -import { accessible, disabled, HYDRATED_ATTR, labelable, defaults, hidden } from "../../tests/commonTests"; +import { accessible, disabled, HYDRATED_ATTR, labelable, defaults, hidden, t9n } from "../../tests/commonTests"; import { CSS } from "./resources"; import { GlobalTestProps } from "../../tests/utils"; import { html } from "../../../support/formatting"; @@ -43,10 +43,6 @@ describe("calcite-button", () => { propertyName: "iconStart", defaultValue: undefined }, - { - propertyName: "intlLoading", - defaultValue: "Loading" - }, { propertyName: "loading", defaultValue: false @@ -601,4 +597,6 @@ describe("calcite-button", () => { it("submits", async () => assertOnFormButtonType("submit")); it("resets", async () => assertOnFormButtonType("reset")); }); + + it("supports translation", () => t9n("calcite-button")); }); diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx index 4608f0fd2b6..09cd970eb8e 100644 --- a/src/components/button/button.tsx +++ b/src/components/button/button.tsx @@ -1,6 +1,6 @@ import "form-request-submit-polyfill/form-request-submit-polyfill"; import { Component, Element, h, Method, Prop, Build, State, VNode, Watch } from "@stencil/core"; -import { CSS, TEXT } from "./resources"; +import { CSS } from "./resources"; import { closestElementCrossShadowBoundary } from "../../utils/dom"; import { ButtonAlignment, ButtonAppearance, ButtonColor } from "./interfaces"; import { FlipContext, Scale, Width } from "../interfaces"; @@ -8,6 +8,15 @@ import { LabelableComponent, connectLabel, disconnectLabel, getLabelText } from import { createObserver } from "../../utils/observers"; import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive"; import { submitForm, resetForm, FormOwner } from "../../utils/form"; +import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale"; +import { + connectMessages, + disconnectMessages, + setUpMessages, + T9nComponent, + updateMessages +} from "../../utils/t9n"; +import { Messages } from "./assets/button/t9n"; /** Passing a 'href' will render an anchor link, instead of a button. Role will be set to link, or button, depending on this. */ /** It is the consumers responsibility to add aria information, rel, target, for links, and any button attributes for form submission */ @@ -16,9 +25,12 @@ import { submitForm, resetForm, FormOwner } from "../../utils/form"; @Component({ tag: "calcite-button", styleUrl: "button.scss", - shadow: true + shadow: true, + assetsDirs: ["assets"] }) -export class Button implements LabelableComponent, InteractiveComponent, FormOwner { +export class Button + implements LabelableComponent, InteractiveComponent, FormOwner, LocalizedComponent, T9nComponent +{ //-------------------------------------------------------------------------- // // Element @@ -33,74 +45,103 @@ export class Button implements LabelableComponent, InteractiveComponent, FormOwn // //-------------------------------------------------------------------------- - /** optionally specify alignment of button elements. */ + /** Specifies the alignment of the component's elements. */ @Prop({ reflect: true }) alignment?: ButtonAlignment = "center"; - /** specify the appearance style of the button, defaults to solid. */ + /** Specifies the appearance style of the component. */ @Prop({ reflect: true }) appearance: ButtonAppearance = "solid"; - /** Applies to the aria-label attribute on the button or hyperlink */ + /** Accessible name for the component. */ @Prop() label?: string; - /** specify the color of the button, defaults to blue */ + /** Specifies the color of the component. */ @Prop({ reflect: true }) color: ButtonColor = "blue"; - /** is the button disabled */ + /** When `true`, interaction is prevented and the component is displayed with lower opacity. */ @Prop({ reflect: true }) disabled = false; - /** optionally pass a href - used to determine if the component should render as a button or an anchor */ + /** + * Specifies the URL of the linked resource, which can be set as an absolute or relative path. + */ @Prop({ reflect: true }) href?: string; /** Specifies an icon to display at the end of the component. */ @Prop({ reflect: true }) iconEnd?: string; - /** When true, the icon will be flipped when the element direction is right-to-left (`"rtl"`). */ + /** When `true`, the icon will be flipped when the element direction is right-to-left (`"rtl"`). */ @Prop({ reflect: true }) iconFlipRtl?: FlipContext; /** Specifies an icon to display at the start of the component. */ @Prop({ reflect: true }) iconStart?: string; /** - * string to override English loading text + * Accessible name when the component is loading. * * @default "Loading" + * @deprecated - translations are now built-in, if you need to override a string, please use `messageOverrides` */ - @Prop() intlLoading?: string = TEXT.loading; + @Prop() intlLoading?: string; - /** optionally add a calcite-loader component to the button, disabling interaction. */ + /** + * When `true`, a busy indicator is displayed and interaction is disabled. + */ @Prop({ reflect: true }) loading = false; - /** The name attribute to apply to the button */ + /** Specifies the name of the component on form submission. */ @Prop({ reflect: true }) name?: string; - /** The rel attribute to apply to the hyperlink */ + /** + * Defines the relationship between the `href` value and the current document. + * + * @mdn [rel](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) + */ @Prop({ reflect: true }) rel?: string; /** - * The form ID to associate with the component + * The form ID to associate with the component. * - * @deprecated – this property is no longer needed if placed inside a form. + * @deprecated – The property is no longer needed if the component is placed inside a form. */ @Prop() form?: string; - /** optionally add a round style to the button */ + /** When `true`, adds a round style to the component. */ @Prop({ reflect: true }) round = false; - /** specify the scale of the button, defaults to m */ + /** Specifies the size of the component. */ @Prop({ reflect: true }) scale: Scale = "m"; - /** is the button a child of a calcite-split-button */ + /** Specifies if the component is a child of a `calcite-split-button`. */ @Prop({ reflect: true }) splitChild?: "primary" | "secondary" | false = false; - /** The target attribute to apply to the hyperlink */ + /** + * Specifies where to open the linked document defined in the `href` property. + * + * @mdn [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target) + */ @Prop({ reflect: true }) target?: string; - /** The type attribute to apply to the button */ + /** + * Specifies the default behavior of the button. + * + * @mdn [type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type) + */ @Prop({ mutable: true, reflect: true }) type = "button"; - /** specify the width of the button, defaults to auto */ + /** Specifies the width of the component. */ @Prop({ reflect: true }) width: Width = "auto"; + /** + * Made into a prop for testing purposes only + * + * @internal + */ + @Prop({ mutable: true }) messages: Messages; + + /** + * Use this property to override individual strings used by the component. + */ + @Prop({ mutable: true }) messageOverrides: Partial; + @Watch("loading") loadingChanged(newValue: boolean, oldValue: boolean): void { if (!!newValue && !oldValue) { @@ -113,13 +154,22 @@ export class Button implements LabelableComponent, InteractiveComponent, FormOwn } } + @Watch("intlLoading") + @Watch("defaultMessages") + @Watch("messageOverrides") + onMessagesChange(): void { + /** referred in t9n util */ + } + //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- - connectedCallback(): void { + async connectedCallback(): Promise { + connectLocalized(this); + connectMessages(this); this.hasLoader = this.loading; this.setupTextContentObserver(); connectLabel(this); @@ -132,12 +182,15 @@ export class Button implements LabelableComponent, InteractiveComponent, FormOwn disconnectedCallback(): void { this.mutationObserver?.disconnect(); disconnectLabel(this); + disconnectLocalized(this); + disconnectMessages(this); this.formEl = null; } - componentWillLoad(): void { + async componentWillLoad(): Promise { if (Build.isBrowser) { this.updateHasContent(); + await setUpMessages(this); } } @@ -154,7 +207,7 @@ export class Button implements LabelableComponent, InteractiveComponent, FormOwn active class={this.loading ? CSS.loadingIn : CSS.loadingOut} inline - label={this.intlLoading} + label={this.messages.loading} scale={this.scale === "l" ? "m" : "s"} /> @@ -244,6 +297,15 @@ export class Button implements LabelableComponent, InteractiveComponent, FormOwn /** determine if loader present for styling purposes */ @State() private hasLoader = false; + @State() effectiveLocale = ""; + + @Watch("effectiveLocale") + effectiveLocaleChange(): void { + updateMessages(this, this.effectiveLocale); + } + + @State() defaultMessages: Messages; + private updateHasContent() { const slottedContent = this.el.textContent.trim().length > 0 || this.el.childNodes.length > 0; this.hasContent = diff --git a/src/components/button/resources.ts b/src/components/button/resources.ts index 19205fe40d8..d6c5caed49c 100644 --- a/src/components/button/resources.ts +++ b/src/components/button/resources.ts @@ -10,7 +10,3 @@ export const CSS = { iconStartEmpty: "icon-start-empty", iconEndEmpty: "icon-end-empty" }; - -export const TEXT = { - loading: "Loading" -}; diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 00000000000..189fdfd5d28 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,21 @@ +/** + * This module helps users provide custom configuration for component internals. + * + * @internal + */ + +const configOverrides = globalThis["calciteComponentsConfig"]; + +const config = { + /** + * We apply a custom fix to improve positioning for non-Chromium browsers. + * The fix comes at a performance cost, so provides users a way to opt-out if necessary. + * + * @internal + */ + floatingUINonChromiumPositioningFix: true, + + ...configOverrides +}; + +export { config }; diff --git a/src/utils/floating-ui/nonChromiumPlatformUtils.ts b/src/utils/floating-ui/nonChromiumPlatformUtils.ts new file mode 100644 index 00000000000..ef3e974ae0d --- /dev/null +++ b/src/utils/floating-ui/nonChromiumPlatformUtils.ts @@ -0,0 +1,599 @@ +import { rectToClientRect, Strategy } from "@floating-ui/core"; +import type { ElementRects } from "@floating-ui/dom"; +import { FloatingElement, ReferenceElement } from "@floating-ui/dom/src/types"; + +/** + * This module provides utils to fix positioning across shadow DOM in non-Chromium browsers + * + * It is based on floating-ui's distributable + */ + +/** + * 👇 the following are needed to fix shadow DOM positioning 👇️ + * + * @param element + */ +function getTrueOffsetParent(element) { + if (!isHTMLElement(element) || getComputedStyle(element).position === "fixed") { + return null; + } + + return composedOffsetParent(element); +} + +/** + * Polyfills the old offsetParent behavior from before the spec was changed: + * https://github.com/w3c/csswg-drafts/issues/159 + * + * @param element + */ +function composedOffsetParent(element) { + let { offsetParent } = element; + let ancestor = element; + let foundInsideSlot = false; + + while (ancestor && ancestor !== offsetParent) { + const { assignedSlot } = ancestor; + + if (assignedSlot) { + let newOffsetParent = assignedSlot.offsetParent; + + if (getComputedStyle(assignedSlot).display === "contents") { + const hadStyleAttribute = assignedSlot.hasAttribute("style"); + const oldDisplay = assignedSlot.style.display; + assignedSlot.style.display = getComputedStyle(ancestor).display; + newOffsetParent = assignedSlot.offsetParent; + assignedSlot.style.display = oldDisplay; + + if (!hadStyleAttribute) { + assignedSlot.removeAttribute("style"); + } + } + + ancestor = assignedSlot; + + if (offsetParent !== newOffsetParent) { + offsetParent = newOffsetParent; + foundInsideSlot = true; + } + } else if (isShadowRoot(ancestor) && ancestor.host && foundInsideSlot) { + break; + } + + ancestor = (isShadowRoot(ancestor) && ancestor.host) || ancestor.parentNode; + } + + return offsetParent; +} + +function getElementRects(_ref: { + reference: ReferenceElement; + floating: FloatingElement; + strategy: Strategy; +}): ElementRects { + const { reference, floating, strategy } = _ref; + return { + reference: getRectRelativeToOffsetParent(reference, getOffsetParent(floating), strategy), + floating: { ...getDimensions(floating), x: 0, y: 0 } + }; +} + +export { getClippingRect, getElementRects, getOffsetParent }; + +/** + * ☝️ the following are needed to fix shadow DOM positioning ☝️ + */ + +/** + * 👇 the following are taken directly from floating-ui's ESM distributable to support the exports above 👇️ + * + * **Notes**: + * unused functions are removed + * ESLint is disabled + * TS-warnings are suppressed + */ +/* eslint-disable */ + +function isWindow(value) { + return value && value.document && value.location && value.alert && value.setInterval; +} +function getWindow(node) { + if (node == null) { + return window; + } + + if (!isWindow(node)) { + const ownerDocument = node.ownerDocument; + return ownerDocument ? ownerDocument.defaultView || window : window; + } + + return node; +} + +function getComputedStyle(element) { + return getWindow(element).getComputedStyle(element); +} + +function getNodeName(node) { + return isWindow(node) ? "" : node ? (node.nodeName || "").toLowerCase() : ""; +} + +function getUAString() { + // @ts-ignore + const uaData = navigator.userAgentData; + + if (uaData != null && uaData.brands) { + return uaData.brands.map((item) => item.brand + "/" + item.version).join(" "); + } + + return navigator.userAgent; +} + +function isHTMLElement(value) { + return value instanceof getWindow(value).HTMLElement; +} +function isElement(value) { + return value instanceof getWindow(value).Element; +} +function isNode(value) { + return value instanceof getWindow(value).Node; +} +function isShadowRoot(node) { + // Browsers without `ShadowRoot` support + if (typeof ShadowRoot === "undefined") { + return false; + } + + const OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; +} +function isOverflowElement(element) { + // Firefox wants us to check `-x` and `-y` variations as well + const { overflow, overflowX, overflowY, display } = getComputedStyle(element); + return ( + /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX) && !["inline", "contents"].includes(display) + ); +} +function isTableElement(element) { + return ["table", "td", "th"].includes(getNodeName(element)); +} +function isContainingBlock(element) { + // TODO: Try and use feature detection here instead + const isFirefox = /firefox/i.test(getUAString()); + const css = getComputedStyle(element); // This is non-exhaustive but covers the most common CSS properties that + // create a containing block. + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + + return ( + css.transform !== "none" || + css.perspective !== "none" || + (isFirefox && css.willChange === "filter") || + (isFirefox && (css.filter ? css.filter !== "none" : false)) || + ["transform", "perspective"].some((value) => css.willChange.includes(value)) || + ["paint", "layout", "strict", "content"].some( + // TS 4.1 compat + (value) => { + const contain = css.contain; + return contain != null ? contain.includes(value) : false; + } + ) + ); +} +function isLayoutViewport() { + // Not Safari + return !/^((?!chrome|android).)*safari/i.test(getUAString()); // Feature detection for this fails in various ways + // • Always-visible scrollbar or not + // • Width of , etc. + // const vV = win.visualViewport; + // return vV ? Math.abs(win.innerWidth / vV.scale - vV.width) < 0.5 : true; +} +function isLastTraversableNode(node) { + return ["html", "body", "#document"].includes(getNodeName(node)); +} + +const min = Math.min; +const max = Math.max; +const round = Math.round; + +function getBoundingClientRect(element, includeScale, isFixedStrategy) { + var _win$visualViewport$o, _win$visualViewport, _win$visualViewport$o2, _win$visualViewport2; + + if (includeScale === void 0) { + includeScale = false; + } + + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + + const clientRect = element.getBoundingClientRect(); + let scaleX = 1; + let scaleY = 1; + + if (includeScale && isHTMLElement(element)) { + scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1; + scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1; + } + + const win = isElement(element) ? getWindow(element) : window; + const addVisualOffsets = !isLayoutViewport() && isFixedStrategy; + const x = + (clientRect.left + + (addVisualOffsets + ? (_win$visualViewport$o = + (_win$visualViewport = win.visualViewport) == null ? void 0 : _win$visualViewport.offsetLeft) != null + ? _win$visualViewport$o + : 0 + : 0)) / + scaleX; + const y = + (clientRect.top + + (addVisualOffsets + ? (_win$visualViewport$o2 = + (_win$visualViewport2 = win.visualViewport) == null ? void 0 : _win$visualViewport2.offsetTop) != null + ? _win$visualViewport$o2 + : 0 + : 0)) / + scaleY; + const width = clientRect.width / scaleX; + const height = clientRect.height / scaleY; + return { + width, + height, + top: y, + right: x + width, + bottom: y + height, + left: x, + x, + y + }; +} + +function getDocumentElement(node) { + return ((isNode(node) ? node.ownerDocument : node.document) || window.document).documentElement; +} + +function getNodeScroll(element) { + if (isElement(element)) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + + return { + scrollLeft: element.pageXOffset, + scrollTop: element.pageYOffset + }; +} + +function getWindowScrollBarX(element) { + // If has a CSS width greater than the viewport, then this will be + // incorrect for RTL. + // @ts-ignore + return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft; +} + +function isScaled(element) { + // @ts-ignore + const rect = getBoundingClientRect(element); + return round(rect.width) !== element.offsetWidth || round(rect.height) !== element.offsetHeight; +} + +function getRectRelativeToOffsetParent(element, offsetParent, strategy) { + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + const rect = getBoundingClientRect( + element, // @ts-ignore - checked above (TS 4.1 compat) + isOffsetParentAnElement && isScaled(offsetParent), + strategy === "fixed" + ); + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + const offsets = { + x: 0, + y: 0 + }; + + if (isOffsetParentAnElement || (!isOffsetParentAnElement && strategy !== "fixed")) { + if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + + if (isHTMLElement(offsetParent)) { + // @ts-ignore + const offsetRect = getBoundingClientRect(offsetParent, true); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; +} + +function getParentNode(node) { + if (getNodeName(node) === "html") { + return node; + } + + return ( + // this is a quicker (but less type safe) way to save quite some bytes from the bundle + // @ts-ignore + node.assignedSlot || // step into the shadow DOM of the parent of a slotted node + node.parentNode || // DOM Element detected + (isShadowRoot(node) ? node.host : null) || // ShadowRoot detected + getDocumentElement(node) // fallback + ); +} + +function getContainingBlock(element) { + let currentNode = getParentNode(element); + + if (isShadowRoot(currentNode)) { + currentNode = currentNode.host; + } + + while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { + if (isContainingBlock(currentNode)) { + return currentNode; + } else { + const parent = currentNode.parentNode; + currentNode = isShadowRoot(parent) ? parent.host : parent; + } + } + + return null; +} // Gets the closest ancestor positioned element. Handles some edge cases, +// such as table ancestors and cross browser bugs. + +function getOffsetParent(element) { + const window = getWindow(element); + let offsetParent = getTrueOffsetParent(element); + + while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === "static") { + offsetParent = getTrueOffsetParent(offsetParent); + } + + if ( + offsetParent && + (getNodeName(offsetParent) === "html" || + (getNodeName(offsetParent) === "body" && + getComputedStyle(offsetParent).position === "static" && + !isContainingBlock(offsetParent))) + ) { + return window; + } + + return offsetParent || getContainingBlock(element) || window; +} + +function getDimensions(element) { + if (isHTMLElement(element)) { + return { + width: element.offsetWidth, + height: element.offsetHeight + }; + } + + // @ts-ignore + const rect = getBoundingClientRect(element); + return { + width: rect.width, + height: rect.height + }; +} + +function getViewportRect(element, strategy) { + const win = getWindow(element); + const html = getDocumentElement(element); + const visualViewport = win.visualViewport; + let width = html.clientWidth; + let height = html.clientHeight; + let x = 0; + let y = 0; + + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + const layoutViewport = isLayoutViewport(); + + if (layoutViewport || (!layoutViewport && strategy === "fixed")) { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + + return { + width, + height, + x, + y + }; +} + +// of the `` and `` rect bounds if horizontally scrollable + +function getDocumentRect(element) { + var _element$ownerDocumen; + + const html = getDocumentElement(element); + const scroll = getNodeScroll(element); + const body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; + const width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); + const height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); + let x = -scroll.scrollLeft + getWindowScrollBarX(element); + const y = -scroll.scrollTop; + + if (getComputedStyle(body || html).direction === "rtl") { + x += max(html.clientWidth, body ? body.clientWidth : 0) - width; + } + + return { + width, + height, + x, + y + }; +} + +function getNearestOverflowAncestor(node) { + const parentNode = getParentNode(node); + + if (isLastTraversableNode(parentNode)) { + // @ts-ignore assume body is always available + return node.ownerDocument.body; + } + + if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { + return parentNode; + } + + return getNearestOverflowAncestor(parentNode); +} + +function getOverflowAncestors(node, list) { + var _node$ownerDocument; + + if (list === void 0) { + list = []; + } + + const scrollableAncestor = getNearestOverflowAncestor(node); + const isBody = + scrollableAncestor === ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.body); + const win = getWindow(scrollableAncestor); + const target = isBody + ? [win].concat(win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : []) + : scrollableAncestor; + const updatedList = list.concat(target); + return isBody + ? updatedList // @ts-ignore: isBody tells us target will be an HTMLElement here + : updatedList.concat(getOverflowAncestors(target)); +} + +function contains(parent, child) { + const rootNode = child.getRootNode == null ? void 0 : child.getRootNode(); // First, attempt with faster native method + + if (parent.contains(child)) { + return true; + } // then fallback to custom implementation with Shadow DOM support + else if (rootNode && isShadowRoot(rootNode)) { + let next = child; + + do { + // use `===` replace node.isSameNode() + if (next && parent === next) { + return true; + } // @ts-ignore: need a better way to handle this... + + next = next.parentNode || next.host; + } while (next); + } + + return false; +} + +function getNearestParentCapableOfEscapingClipping(element, clippingAncestors) { + let currentNode = element; + + while (currentNode && !isLastTraversableNode(currentNode) && !clippingAncestors.includes(currentNode)) { + if (isElement(currentNode) && ["absolute", "fixed"].includes(getComputedStyle(currentNode).position)) { + break; + } + + const parentNode = getParentNode(currentNode); + currentNode = isShadowRoot(parentNode) ? parentNode.host : parentNode; + } + + return currentNode; +} + +function getInnerBoundingClientRect(element, strategy) { + const clientRect = getBoundingClientRect(element, false, strategy === "fixed"); + const top = clientRect.top + element.clientTop; + const left = clientRect.left + element.clientLeft; + return { + top, + left, + x: left, + y: top, + right: left + element.clientWidth, + bottom: top + element.clientHeight, + width: element.clientWidth, + height: element.clientHeight + }; +} + +function getClientRectFromClippingAncestor(element, clippingParent, strategy) { + if (clippingParent === "viewport") { + return rectToClientRect(getViewportRect(element, strategy)); + } + + if (isElement(clippingParent)) { + return getInnerBoundingClientRect(clippingParent, strategy); + } + + return rectToClientRect(getDocumentRect(getDocumentElement(element))); +} // A "clipping ancestor" is an overflowable container with the characteristic of +// clipping (or hiding) overflowing elements with a position different from +// `initial` + +function getClippingAncestors(element) { + // @ts-ignore + const clippingAncestors = getOverflowAncestors(element); + const nearestEscapableParent = getNearestParentCapableOfEscapingClipping(element, clippingAncestors); + let clipperElement = null; + + if (nearestEscapableParent && isHTMLElement(nearestEscapableParent)) { + const offsetParent = getOffsetParent(nearestEscapableParent); + + if (isOverflowElement(nearestEscapableParent)) { + clipperElement = nearestEscapableParent; + } else if (isHTMLElement(offsetParent)) { + clipperElement = offsetParent; + } + } + + if (!isElement(clipperElement)) { + return []; + } // @ts-ignore isElement check ensures we return Array + + return clippingAncestors.filter( + (clippingAncestors) => + clipperElement && + isElement(clippingAncestors) && + contains(clippingAncestors, clipperElement) && + getNodeName(clippingAncestors) !== "body" + ); +} // Gets the maximum area that the element is visible in due to any number of +// clipping ancestors + +function getClippingRect(_ref) { + let { element, boundary, rootBoundary, strategy } = _ref; + const mainClippingAncestors = boundary === "clippingAncestors" ? getClippingAncestors(element) : [].concat(boundary); + const clippingAncestors = [...mainClippingAncestors, rootBoundary]; + const firstClippingAncestor = clippingAncestors[0]; + const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { + const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy)); + return { + width: clippingRect.right - clippingRect.left, + height: clippingRect.bottom - clippingRect.top, + x: clippingRect.left, + y: clippingRect.top + }; +} diff --git a/src/utils/locale.ts b/src/utils/locale.ts index 5a65f88731f..7035f580a10 100644 --- a/src/utils/locale.ts +++ b/src/utils/locale.ts @@ -147,19 +147,18 @@ export const getSupportedNumberingSystem = (numberingSystem: string): NumberingS */ export function getSupportedLocale(locale: string, context: "cldr" | "t9n" = "cldr"): string { const contextualLocales = context === "cldr" ? locales : t9nLocales; - + if (contextualLocales.includes(locale)) { return locale; } locale = locale.toLowerCase(); - + // we support both 'nb' and 'no' (BCP 47) for Norwegian but only `no` has corresponding bundle - if (locale === "nb" ) { + if (locale === "nb") { return "no"; } - // we use `pt-BR` as it will have the same translations as `pt`, which has no corresponding bundle if (context === "t9n" && locale === "pt") { return "pt-BR";