Repository: palantir/blueprint
Pull request #7349: [Datetime] Port over `react-day-picker` and `date-fns` components from `@blueprintjs/datetime2`
Author: login: ggdouglas
Labels: name: Package: datetime
name: no changelog
Created: 2025-03-13T17:12:14Z | Merged: 2025-03-13T19:01:03Z

#### Part of #7342

#### Proposed changes
- Port over `datetime2` components from [`react-day-picker/`](https://github.com/palantir/blueprint/tree/ca86eebdf9e7ed242d0cfd8abaf73ac376c9a5f1/packages/datetime2/src/components/react-day-picker)
- Port over [`dateFnsLocalizedComponent.tsx`](https://github.com/palantir/blueprint/blob/ca86eebdf9e7ed242d0cfd8abaf73ac376c9a5f1/packages/datetime2/src/components/dateFnsLocalizedComponent.tsx) from  `datetime2`


Diff:
diff --git a/packages/datetime/package.json b/packages/datetime/package.json
index 46c5ac2168c..e011a44cc28 100644
--- a/packages/datetime/package.json
+++ b/packages/datetime/package.json
@@ -48,6 +48,7 @@
         "date-fns-tz": "^2.0.0",
         "react-day-picker": "7.4.9",
         "react-day-picker-8": "npm:react-day-picker@^8.10.0",
+        "react-innertext": "^1.1.5",
         "tslib": "~2.6.2"
     },
     "peerDependencies": {
diff --git a/packages/datetime/src/components/date-picker3/datePicker3Context.tsx b/packages/datetime/src/components/date-picker3/datePicker3Context.tsx
new file mode 100644
index 00000000000..8d384183dce
--- /dev/null
+++ b/packages/datetime/src/components/date-picker3/datePicker3Context.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as React from "react";
+
+import type { DatePickerBaseProps } from "../../common";
+
+import type { DatePicker3State } from "./datePicker3State";
+
+export type DatePicker3ContextState = Pick<DatePickerBaseProps, "reverseMonthAndYearMenus"> &
+    Pick<DatePicker3State, "locale">;
+
+/**
+ * Context used to pass DatePicker3 & DateRangePicker3 props and state down to custom react-day-picker components
+ * like DatePicker3Caption.
+ */
+export const DatePicker3Context = React.createContext<DatePicker3ContextState>({
+    locale: undefined,
+});
+
+export const DatePicker3Provider = (props: React.PropsWithChildren<DatePicker3ContextState>) => {
+    return <DatePicker3Context.Provider value={props}>{props.children}</DatePicker3Context.Provider>;
+};
diff --git a/packages/datetime/src/components/date-picker3/datePicker3State.ts b/packages/datetime/src/components/date-picker3/datePicker3State.ts
new file mode 100644
index 00000000000..e4f44554b90
--- /dev/null
+++ b/packages/datetime/src/components/date-picker3/datePicker3State.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Locale } from "date-fns";
+
+export interface DatePicker3State {
+    displayMonth: number;
+    displayYear: number;
+    locale: Locale | undefined;
+    selectedDay: number | null;
+    value: Date | null;
+    selectedShortcutIndex?: number;
+}
diff --git a/packages/datetime/src/components/dateFnsLocalizedComponent.tsx b/packages/datetime/src/components/dateFnsLocalizedComponent.tsx
new file mode 100644
index 00000000000..12ff7941e07
--- /dev/null
+++ b/packages/datetime/src/components/dateFnsLocalizedComponent.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Locale } from "date-fns";
+
+import { AbstractPureComponent } from "@blueprintjs/core";
+
+import type { DateFnsLocaleProps } from "../common/dateFnsLocaleProps";
+import { loadDateFnsLocale } from "../common/dateFnsLocaleUtils";
+
+interface DateFnsLocaleState {
+    locale?: Locale | undefined;
+}
+
+/**
+ * Abstract component which accepts a date-fns locale prop and loads the corresponding `Locale` object as necessary.
+ *
+ * Currently used by DatePicker3, DateRangePicker3, and DateRangeInput3, but we would ideally migrate to the
+ * `useDateFnsLocale()` hook once those components are refactored into functional components.
+ */
+export abstract class DateFnsLocalizedComponent<
+    P extends DateFnsLocaleProps,
+    S extends DateFnsLocaleState,
+> extends AbstractPureComponent<P, S> {
+    // Keeping track of `isMounted` state is generally considered an anti-pattern, but since there is no way to
+    // cancel/abort dyanmic ES module `import()` calls to load the date-fns locale, this is the best way to avoid
+    // setting state on an unmounted component, which creates noise in the console (especially while running tests).
+    // N.B. this cannot be named `isMounted` because that conflicts with a React internal property.
+    private isComponentMounted = false;
+
+    // HACKHACK: type fix for setState which does not accept partial state objects in our version of
+    // @types/react (v16.14.x)
+    public setState<K extends keyof S>(
+        nextStateOrAction: ((prevState: S, prevProps: P) => Pick<S, K> | null) | Pick<S, K> | Partial<S> | null,
+        callback?: () => void,
+    ) {
+        if (typeof nextStateOrAction === "function") {
+            super.setState(nextStateOrAction, callback);
+        } else {
+            super.setState(nextStateOrAction as S);
+        }
+    }
+
+    public async componentDidMount() {
+        this.isComponentMounted = true;
+        await this.loadLocale(this.props.locale);
+    }
+
+    public async componentDidUpdate(prevProps: DateFnsLocaleProps) {
+        if (this.props.locale !== prevProps.locale) {
+            await this.loadLocale(this.props.locale);
+        }
+    }
+
+    public componentWillUnmount(): void {
+        this.isComponentMounted = false;
+    }
+
+    private async loadLocale(localeOrCode: string | Locale | undefined) {
+        if (localeOrCode === undefined) {
+            return;
+        } else if (this.state.locale?.code === localeOrCode) {
+            return;
+        }
+
+        if (typeof localeOrCode === "string") {
+            const loader = this.props.dateFnsLocaleLoader ?? loadDateFnsLocale;
+            const locale = await loader(localeOrCode);
+            if (this.isComponentMounted) {
+                this.setState({ locale });
+            }
+        } else {
+            this.setState({ locale: localeOrCode });
+        }
+    }
+}
diff --git a/packages/datetime/src/components/react-day-picker/datePicker3Caption.tsx b/packages/datetime/src/components/react-day-picker/datePicker3Caption.tsx
new file mode 100644
index 00000000000..90897993d61
--- /dev/null
+++ b/packages/datetime/src/components/react-day-picker/datePicker3Caption.tsx
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import classNames from "classnames";
+import * as React from "react";
+import { CaptionLabel, type CaptionProps, useDayPicker, useNavigation } from "react-day-picker-8";
+import innerText from "react-innertext";
+
+import { Button, DISPLAYNAME_PREFIX, HTMLSelect, type OptionProps } from "@blueprintjs/core";
+import { ChevronLeft, ChevronRight } from "@blueprintjs/icons";
+
+import { DateUtils, Months } from "../../common";
+import { DatePicker3CaptionClasses as CaptionClasses, ReactDayPickerClasses } from "../../common/classes";
+import { useMonthSelectRightOffset } from "../../common/useMonthSelectRightOffset";
+import { DatePicker3Context } from "../date-picker3/datePicker3Context";
+
+/**
+ * Custom react-day-picker caption component used in non-contiguous two-month date range pickers.
+ *
+ * We need to override the whole caption instead of its lower-level components because react-day-picker
+ * does not have built-in support for non-contiguous range pickers.
+ *
+ * @see https://daypicker.dev/guides/custom-components
+ */
+export const DatePicker3Caption = (props: CaptionProps) => {
+    const { classNames: rdpClassNames, formatters, fromDate, toDate, labels } = useDayPicker();
+    const { locale, reverseMonthAndYearMenus } = React.useContext(DatePicker3Context);
+
+    // non-null assertion because we define these values in defaultProps
+    const minYear = fromDate!.getFullYear();
+    const maxYear = toDate!.getFullYear();
+
+    const displayMonth = props.displayMonth.getMonth();
+    const displayYear = props.displayMonth.getFullYear();
+
+    const containerElement = React.useRef<HTMLDivElement>(null);
+    const monthSelectElement = React.useRef<HTMLSelectElement>(null);
+    const { currentMonth, goToMonth, nextMonth, previousMonth } = useNavigation();
+
+    const handlePreviousClick = React.useCallback(
+        () => previousMonth && goToMonth(previousMonth),
+        [previousMonth, goToMonth],
+    );
+    const handleNextClick = React.useCallback(() => nextMonth && goToMonth(nextMonth), [nextMonth, goToMonth]);
+
+    const prevButton = (
+        <Button
+            aria-label={labels.labelPrevious(previousMonth, { locale })}
+            className={classNames(
+                CaptionClasses.DATEPICKER3_NAV_BUTTON,
+                CaptionClasses.DATEPICKER3_NAV_BUTTON_PREVIOUS,
+            )}
+            disabled={!previousMonth}
+            icon={<ChevronLeft />}
+            onClick={handlePreviousClick}
+            variant="minimal"
+        />
+    );
+    const nextButton = (
+        <Button
+            aria-label={labels.labelNext(nextMonth, { locale })}
+            className={classNames(CaptionClasses.DATEPICKER3_NAV_BUTTON, CaptionClasses.DATEPICKER3_NAV_BUTTON_NEXT)}
+            disabled={!nextMonth}
+            icon={<ChevronRight />}
+            onClick={handleNextClick}
+            variant="minimal"
+        />
+    );
+
+    // build the list of available years, relying on react-day-picker's default date-fns formatter or a
+    // user-provided formatter to localize the year "names"
+    const { formatYearCaption } = formatters;
+    const allYearOptions = React.useMemo<OptionProps[]>(() => {
+        const years: OptionProps[] = [];
+        for (let year = minYear; year <= maxYear; year++) {
+            const yearDate = new Date(year, 0);
+            const yearCaption = formatYearCaption(yearDate, { locale });
+            years.push({ label: innerText(yearCaption), value: year });
+        }
+        return years;
+    }, [formatYearCaption, maxYear, minYear, locale]);
+
+    // allow out-of-bounds years but disable the option.
+    // this handles the Dec 2016 case in https://github.com/palantir/blueprint/issues/391
+    if (displayYear > maxYear) {
+        const displayYearDate = new Date(displayYear, 0);
+        const displayYearCaption = formatYearCaption(displayYearDate, { locale });
+        allYearOptions.push({ disabled: true, label: innerText(displayYearCaption), value: displayYear });
+    }
+
+    const handleMonthSelectChange = React.useCallback(
+        (e: React.FormEvent<HTMLSelectElement>) => {
+            const newMonth = parseInt((e.target as HTMLSelectElement).value, 10);
+            // ignore change events with invalid values to prevent crash on iOS Safari (#4178)
+            if (isNaN(newMonth)) {
+                return;
+            }
+            const newDate = DateUtils.clone(currentMonth);
+            newDate.setMonth(newMonth);
+            goToMonth(newDate);
+        },
+        [currentMonth, goToMonth],
+    );
+
+    const startMonth = displayYear === minYear ? fromDate!.getMonth() : 0;
+    const endMonth = displayYear === maxYear ? toDate!.getMonth() + 1 : 12;
+
+    // build the list of available months, relying on react-day-picker's default date-fns formatter or a
+    // user-provided formatter to localize the month names
+    const { formatMonthCaption } = formatters;
+    const allMonths = React.useMemo<string[]>(() => {
+        const months: string[] = [];
+        for (let i = Months.JANUARY; i <= Months.DECEMBER; i++) {
+            const monthDate = new Date(displayYear, i);
+            const formattedMonth = formatMonthCaption(monthDate, { locale });
+            months.push(innerText(formattedMonth));
+        }
+        return months;
+    }, [displayYear, formatMonthCaption, locale]);
+    const allMonthOptions = allMonths.map<OptionProps>((month, i) => ({ label: month, value: i }));
+    const availableMonthOptions = allMonthOptions.slice(startMonth, endMonth);
+    const displayedMonthText = allMonths[displayMonth];
+
+    const monthSelectRightOffset = useMonthSelectRightOffset(monthSelectElement, containerElement, displayedMonthText);
+    const monthSelect = (
+        <HTMLSelect
+            aria-label={labels.labelMonthDropdown()}
+            iconProps={{ style: { right: monthSelectRightOffset } }}
+            className={classNames(CaptionClasses.DATEPICKER3_DROPDOWN_MONTH, rdpClassNames.dropdown_month)}
+            key="month"
+            minimal={true}
+            onChange={handleMonthSelectChange}
+            ref={monthSelectElement}
+            value={displayMonth}
+            options={availableMonthOptions}
+        />
+    );
+
+    const handleYearSelectChange = React.useCallback(
+        (e: React.FormEvent<HTMLSelectElement>) => {
+            const newYear = parseInt((e.target as HTMLSelectElement).value, 10);
+            // ignore change events with invalid values to prevent crash on iOS Safari (#4178)
+            if (isNaN(newYear)) {
+                return;
+            }
+            const newDate = DateUtils.clone(currentMonth);
+            newDate.setFullYear(newYear);
+            goToMonth(newDate);
+        },
+        [currentMonth, goToMonth],
+    );
+
+    const yearSelect = (
+        <HTMLSelect
+            aria-label={labels.labelYearDropdown()}
+            className={classNames(CaptionClasses.DATEPICKER3_DROPDOWN_YEAR, rdpClassNames.dropdown_year)}
+            key="year"
+            minimal={true}
+            onChange={handleYearSelectChange}
+            value={displayYear}
+            options={allYearOptions}
+        />
+    );
+
+    const orderedSelects = reverseMonthAndYearMenus ? [yearSelect, monthSelect] : [monthSelect, yearSelect];
+
+    const hiddenCaptionLabel = (
+        <div className={ReactDayPickerClasses.RDP_VHIDDEN}>
+            <CaptionLabel displayMonth={props.displayMonth} id={props.id} />
+        </div>
+    );
+
+    return (
+        <div className={classNames(CaptionClasses.DATEPICKER3_CAPTION, rdpClassNames.caption)} ref={containerElement}>
+            {hiddenCaptionLabel}
+            {prevButton}
+            {orderedSelects}
+            {nextButton}
+        </div>
+    );
+};
+DatePicker3Caption.displayName = `${DISPLAYNAME_PREFIX}.DatePicker3Caption`;
diff --git a/packages/datetime/src/components/react-day-picker/datePicker3Dropdown.tsx b/packages/datetime/src/components/react-day-picker/datePicker3Dropdown.tsx
new file mode 100644
index 00000000000..ff3349cc6d5
--- /dev/null
+++ b/packages/datetime/src/components/react-day-picker/datePicker3Dropdown.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as React from "react";
+import type { DropdownProps } from "react-day-picker-8";
+
+import { HTMLSelect } from "@blueprintjs/core";
+
+import { useMonthSelectRightOffset } from "../../common/useMonthSelectRightOffset";
+
+/**
+ * Custom react-day-picker dropdown component which implements Blueprint's datepicker design
+ * for month and year dropdowns.
+ *
+ * @see https://daypicker.dev/guides/custom-components
+ */
+export function DatePicker3Dropdown({ caption, children, ...props }: DropdownProps) {
+    const containerElement = React.useRef<HTMLDivElement>(null);
+    const selectElement = React.useRef<HTMLSelectElement>(null);
+
+    // Use a custom hook to adjust the position of the position of the HTMLSelect icon to appear right next to
+    // the month name. N.B. we expect props.caption to be a simple string representing the month name.
+    const displayedMonthText = typeof caption === "string" ? caption : "";
+    const monthSelectRightOffset = useMonthSelectRightOffset(selectElement, containerElement, displayedMonthText);
+    const iconProps = React.useMemo(
+        () => (props.name === "months" ? { style: { right: monthSelectRightOffset } } : {}),
+        [props.name, monthSelectRightOffset],
+    );
+
+    return (
+        <div ref={containerElement}>
+            <HTMLSelect iconProps={iconProps} minimal={true} ref={selectElement} {...props}>
+                {children}
+            </HTMLSelect>
+        </div>
+    );
+}
diff --git a/packages/datetime/src/components/react-day-picker/datePickerNavIcons.tsx b/packages/datetime/src/components/react-day-picker/datePickerNavIcons.tsx
new file mode 100644
index 00000000000..5dec8d6845b
--- /dev/null
+++ b/packages/datetime/src/components/react-day-picker/datePickerNavIcons.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as React from "react";
+import type { StyledComponent } from "react-day-picker-8";
+
+import { ChevronLeft, ChevronRight } from "@blueprintjs/icons";
+
+export function IconLeft({ children, ...props }: StyledComponent) {
+    return <ChevronLeft {...props} />;
+}
+
+export function IconRight({ children, ...props }: StyledComponent) {
+    return <ChevronRight {...props} />;
+}
diff --git a/yarn.lock b/yarn.lock
index 0a3c8137671..cd9041ab392 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -541,6 +541,7 @@ __metadata:
     react-day-picker: "patch:react-day-picker@npm%3A7.4.9#~/.yarn/patches/react-day-picker-npm-7.4.9-8853eff118.patch"
     react-day-picker-8: "npm:react-day-picker@^8.10.0"
     react-dom: "npm:^18.3.1"
+    react-innertext: "npm:^1.1.5"
     react-test-renderer: "npm:^18.3.1"
     tslib: "npm:~2.6.2"
     typescript: "npm:~5.3.3"


Reviews:
user: login: ggdouglas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:27:07Z
user: login: jscheiny
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:31:28Z

Review comments:
diff_hunk: @@ -48,6 +48,7 @@
         "date-fns-tz": "^2.0.0",
         "react-day-picker": "7.4.9",
         "react-day-picker-8": "npm:react-day-picker@^8.10.0",
+        "react-innertext": "^1.1.5",
path: packages/datetime/package.json
user: login: ggdouglas
body: Used by [`DatePicker3Caption`](https://github.com/palantir/blueprint/pull/7349/files#diff-cb5fe0038d28acc640bad9cf1baa75573558b50f9e6116f0b51b9c042e180ddbR20)
created_at: 2025-03-13T17:23:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+/*
path: packages/datetime/src/components/date-picker3/datePicker3Context.tsx
user: login: ggdouglas
body: Need to also port over `DatePicker3Context` and  `DatePicker3State` since they are used by `react-day-picker/datePicker3Caption.tsx`
created_at: 2025-03-13T17:25:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+/*
+ * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import classNames from "classnames";
+import * as React from "react";
+import { CaptionLabel, type CaptionProps, useDayPicker, useNavigation } from "react-day-picker-8";
+import innerText from "react-innertext";
+
+import { Button, DISPLAYNAME_PREFIX, HTMLSelect, type OptionProps } from "@blueprintjs/core";
+import { ChevronLeft, ChevronRight } from "@blueprintjs/icons";
+
+import { DateUtils, Months } from "../../common";
+import { DatePicker3CaptionClasses as CaptionClasses, ReactDayPickerClasses } from "../../common/classes";
+import { useMonthSelectRightOffset } from "../../common/useMonthSelectRightOffset";
+import { DatePicker3Context } from "../date-picker3/datePicker3Context";
path: packages/datetime/src/components/react-day-picker/datePicker3Caption.tsx
user: login: ggdouglas
body: Imports changed when ported over to new package. All other code remains the same.
https://github.com/palantir/blueprint/blob/ca86eebdf9e7ed242d0cfd8abaf73ac376c9a5f1/packages/datetime2/src/components/react-day-picker/datePicker3Caption.tsx#L22-L28
created_at: 2025-03-13T17:26:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: svc-palantir-github
created_at: 2025-03-13T17:26:11Z
body: <h3>Port over datetime2 DateFnsLocalizedComponent</h3>
Build artifact links for this commit: <strong><a href="https://output.circle-artifacts.com/output/job/79a70b7b-1b43-43c7-9fa6-890890121121/artifacts/0/packages/docs-app/dist/index.html">documentation</a> | <a href="https://output.circle-artifacts.com/output/job/79a70b7b-1b43-43c7-9fa6-890890121121/artifacts/0/packages/landing-app/dist/index.html">landing</a> | <a href="https://output.circle-artifacts.com/output/job/79a70b7b-1b43-43c7-9fa6-890890121121/artifacts/0/packages/table-dev-app/dist/index.html">table</a> | <a href="https://output.circle-artifacts.com/output/job/79a70b7b-1b43-43c7-9fa6-890890121121/artifacts/0/packages/demo-app/dist/index.html">demo</a></strong>

<em>This is an automated comment from the deploy-preview CircleCI job.</em>
author_association: NONE