diff --git a/.storybook/__snapshots__/Welcome.story.storyshot b/.storybook/__snapshots__/Welcome.story.storyshot
index 1c7bb991a2..2f57a63291 100644
--- a/.storybook/__snapshots__/Welcome.story.storyshot
+++ b/.storybook/__snapshots__/Welcome.story.storyshot
@@ -2868,6 +2868,18 @@ exports[`Storybook Snapshot tests and console checks Storyshots 0/Getting Starte
IconDropdown
+
diff --git a/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.jsx b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.jsx
new file mode 100644
index 0000000000..fcfa2bddb4
--- /dev/null
+++ b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.jsx
@@ -0,0 +1,256 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+import merge from 'lodash/merge';
+import { NumberInput } from 'carbon-components-react';
+import {
+ TextBold16 as TextBold,
+ TextItalic16 as TextItalic,
+ TextUnderline16 as TextUnderline,
+} from '@carbon/icons-react';
+
+import { settings } from '../../../constants/Settings';
+import IconSwitch from '../../IconSwitch/IconSwitch';
+import ColorDropdown from '../../ColorDropdown/ColorDropdown';
+
+const { iotPrefix } = settings;
+
+const colorPropType = PropTypes.shape({
+ carbonColor: PropTypes.string,
+ name: PropTypes.string,
+});
+
+const propTypes = {
+ light: PropTypes.bool,
+ i18n: PropTypes.shape({
+ boldLabel: PropTypes.string,
+ italicLabel: PropTypes.string,
+ underlineLabel: PropTypes.string,
+ fontLabel: PropTypes.string,
+ fontSize: PropTypes.string,
+ fontSizeInvalid: PropTypes.string,
+ backgroundLabel: PropTypes.string,
+ fillOpacityLabel: PropTypes.string,
+ fillOpacityInvalid: PropTypes.string,
+ borderLabel: PropTypes.string,
+ borderWidth: PropTypes.string,
+ borderWidthInvalid: PropTypes.string,
+ }),
+ /** Callback for when any of the form element's value changes */
+ onChange: PropTypes.func.isRequired,
+ /** The state values of the controlled form elements e.g. { title: 'My hotspot 1', description: 'Lorem ipsum' } */
+ formValues: PropTypes.objectOf(
+ PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.bool,
+ colorPropType,
+ ])
+ ),
+ fontColors: PropTypes.arrayOf(colorPropType),
+ backgroundColors: PropTypes.arrayOf(colorPropType),
+ borderColors: PropTypes.arrayOf(colorPropType),
+};
+
+const defaultProps = {
+ light: true,
+ i18n: {
+ boldLabel: 'Text Bold',
+ italicLabel: 'Text Italic',
+ underlineLabel: 'Text Underline',
+ fontLabel: 'Font',
+ fontSizeLabel: 'Font Size',
+ fontSizeInvalid: 'Font Size is invalid',
+ backgroundLabel: 'Background',
+ fillOpacityLabel: 'Fill Opacity',
+ fillOpacityInvalid: 'Fill Opacity is invalid',
+ borderLabel: 'Border',
+ borderWidthLabel: 'Border Width',
+ borderWidthInvalid: 'Border Width is invalid',
+ },
+ formValues: {
+ bold: false,
+ italic: false,
+ underline: false,
+ font: {
+ color: undefined,
+ size: 12,
+ },
+ background: {
+ color: undefined,
+ opacity: 100,
+ },
+ border: {
+ color: undefined,
+ width: 1,
+ },
+ },
+ fontColors: undefined,
+ backgroundColors: undefined,
+ borderColors: undefined,
+};
+
+/* this component is only used internally where props are defined and set. */
+const HotspotTextStyleTab = ({
+ className,
+ fontColors,
+ backgroundColors,
+ borderColors,
+ formValues,
+ light,
+ i18n,
+ onChange,
+}) => {
+ const {
+ boldLabel,
+ italicLabel,
+ underlineLabel,
+ fontLabel,
+ fontSizeLabel,
+ fontSizeInvalid,
+ backgroundLabel,
+ fillOpacityLabel,
+ fillOpacityInvalid,
+ borderLabel,
+ borderWidthLabel,
+ borderWidthInvalid,
+ } = merge({}, defaultProps.i18n, i18n);
+
+ const fontSizeRef = useRef(null);
+ const fillOpacityRef = useRef(null);
+ const borderWidthRef = useRef(null);
+
+ const { bold, italic, underline, font, background, border } = merge(
+ {},
+ defaultProps.formValues,
+ formValues
+ );
+
+ return (
+
+
+ onChange({ bold: !bold })}
+ data-testid="hotspot-bold"
+ selected={bold}
+ text={boldLabel}
+ renderIcon={TextBold}
+ index={0}
+ light={light}
+ />
+ onChange({ italic: !italic })}
+ data-testid="hotspot-italic"
+ selected={italic}
+ text={italicLabel}
+ renderIcon={TextItalic}
+ index={1}
+ light={light}
+ />
+ onChange({ underline: !underline })}
+ data-testid="hotspot-underline"
+ selected={underline}
+ text={underlineLabel}
+ renderIcon={TextUnderline}
+ index={2}
+ light={light}
+ />
+
+
+
+ {
+ onChange({ font: selected });
+ }}
+ />
+
+ {
+ onChange({ font: { size: fontSizeRef.current.value } });
+ }}
+ />
+
+
+
+ {
+ onChange({
+ background: selected,
+ });
+ }}
+ />
+
+ {
+ onChange({
+ background: { opacity: fillOpacityRef.current.value },
+ });
+ }}
+ />
+
+
+
+ {
+ onChange({ border: selected });
+ }}
+ selectedColor={border.color ?? borderColors?.[0]}
+ />
+
+ {
+ onChange({ border: { width: borderWidthRef.current.value } });
+ }}
+ />
+
+
+ );
+};
+
+HotspotTextStyleTab.propTypes = propTypes;
+HotspotTextStyleTab.defaultProps = defaultProps;
+
+export default HotspotTextStyleTab;
diff --git a/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.story.jsx b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.story.jsx
new file mode 100644
index 0000000000..fd49f8601e
--- /dev/null
+++ b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.story.jsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import merge from 'lodash/merge';
+import { action } from '@storybook/addon-actions';
+import { withKnobs } from '@storybook/addon-knobs';
+import {
+ purple70,
+ cyan50,
+ teal70,
+ magenta70,
+ red50,
+ red90,
+ green60,
+ blue80,
+ magenta50,
+ purple50,
+ teal50,
+ cyan90,
+} from '@carbon/colors';
+
+import HotspotTextStyleTab from './HotspotTextStyleTab';
+
+export default {
+ title: 'Watson IoT Experimental/HotSpotEditorModal/HotspotTextStyleTab',
+ decorators: [withKnobs],
+
+ parameters: {
+ component: HotspotTextStyleTab,
+ },
+};
+
+const colors = [
+ { carbonColor: purple70, name: 'purple70' },
+ { carbonColor: cyan50, name: 'cyan50' },
+ { carbonColor: teal70, name: 'teal70' },
+ { carbonColor: magenta70, name: 'magenta70' },
+ { carbonColor: red50, name: 'red50' },
+ { carbonColor: red90, name: 'red90' },
+ { carbonColor: green60, name: 'green60' },
+ { carbonColor: blue80, name: 'blue80' },
+ { carbonColor: magenta50, name: 'magenta50' },
+ { carbonColor: purple50, name: 'purple50' },
+ { carbonColor: teal50, name: 'teal50' },
+ { carbonColor: cyan90, name: 'cyan90' },
+];
+
+export const Default = () => {
+ const WithState = () => {
+ const [formValues, setFormValues] = useState({});
+
+ return (
+
{
+ setFormValues(merge({}, formValues, change));
+ action('onChange')(change);
+ }}
+ />
+ );
+ };
+
+ return (
+
+
+
+ );
+};
+
+Default.story = {
+ name: 'default',
+
+ parameters: {
+ info: {
+ text: 'TODO',
+ },
+ },
+};
diff --git a/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.test.jsx b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.test.jsx
new file mode 100644
index 0000000000..3b710bac47
--- /dev/null
+++ b/src/components/HotspotEditorModal/HotspotTextStyleTab/HotspotTextStyleTab.test.jsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import merge from 'lodash/merge';
+import { render, fireEvent, screen } from '@testing-library/react';
+import { purple70, cyan50, teal70 } from '@carbon/colors';
+
+import HotspotTextStyleTab from './HotspotTextStyleTab';
+
+const colors = [
+ { carbonColor: purple70, name: 'purple70' },
+ { carbonColor: cyan50, name: 'cyan50' },
+ { carbonColor: teal70, name: 'teal70' },
+];
+
+describe('HotspotTextStyleTab', () => {
+ it('handles text styling', () => {
+ let formValues = {};
+
+ render(
+ {
+ formValues = merge({}, formValues, change);
+ }}
+ />
+ );
+
+ const boldButton = screen.getByTestId('hotspot-bold');
+ const italicButton = screen.getByTestId('hotspot-italic');
+ const underlineButton = screen.getByTestId('hotspot-underline');
+
+ fireEvent.click(boldButton);
+ fireEvent.click(italicButton);
+ fireEvent.click(underlineButton);
+
+ expect(formValues.bold).toBe(true);
+ expect(formValues.italic).toBe(true);
+ expect(formValues.underline).toBe(true);
+ });
+
+ it('handles dropdown updates', () => {
+ let formValues = {};
+
+ render(
+ {
+ formValues = merge({}, formValues, change);
+ }}
+ />
+ );
+
+ const dropdowns = screen.getAllByText(colors[0].name);
+
+ fireEvent.click(dropdowns[0]);
+ fireEvent.click(screen.getAllByText(colors[1].name)[0]);
+
+ fireEvent.click(dropdowns[1]);
+ fireEvent.click(screen.getAllByText(colors[1].name)[0]);
+
+ fireEvent.click(dropdowns[2]);
+ fireEvent.click(screen.getAllByText(colors[1].name)[0]);
+
+ expect(formValues.font.color).toEqual(colors[1]);
+ expect(formValues.background.color).toEqual(colors[1]);
+ expect(formValues.border.color).toEqual(colors[1]);
+ });
+
+ it('handles number input updates', () => {
+ let formValues = {};
+
+ render(
+ {
+ formValues = merge({}, formValues, change);
+ }}
+ />
+ );
+
+ const incrementButtons = screen.getAllByTitle('Increment number');
+ const decrementButtons = screen.getAllByTitle('Decrement number');
+
+ fireEvent.click(incrementButtons[0]);
+ fireEvent.click(incrementButtons[1]);
+ fireEvent.click(incrementButtons[2]);
+
+ expect(formValues.font.size).toEqual('13');
+ expect(formValues.background).toBeUndefined(); // Invalid update, will be undefined
+ expect(formValues.border.width).toEqual('2');
+
+ fireEvent.click(decrementButtons[0]);
+ fireEvent.click(decrementButtons[1]);
+ fireEvent.click(decrementButtons[2]);
+
+ expect(formValues.font.size).toEqual('12');
+ expect(formValues.background.opacity).toEqual('99');
+ expect(formValues.border.width).toEqual('1');
+ });
+});
diff --git a/src/components/HotspotEditorModal/HotspotTextStyleTab/__snapshots__/HotspotTextStyleTab.story.storyshot b/src/components/HotspotEditorModal/HotspotTextStyleTab/__snapshots__/HotspotTextStyleTab.story.storyshot
new file mode 100644
index 0000000000..d1744ec72b
--- /dev/null
+++ b/src/components/HotspotEditorModal/HotspotTextStyleTab/__snapshots__/HotspotTextStyleTab.story.storyshot
@@ -0,0 +1,696 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/HotSpotEditorModal/HotspotTextStyleTab default 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/HotspotEditorModal/HotspotTextStyleTab/_hotspot-text-style-tab.scss b/src/components/HotspotEditorModal/HotspotTextStyleTab/_hotspot-text-style-tab.scss
new file mode 100644
index 0000000000..13a0a97cf2
--- /dev/null
+++ b/src/components/HotspotEditorModal/HotspotTextStyleTab/_hotspot-text-style-tab.scss
@@ -0,0 +1,32 @@
+@import '../../../globals/vars';
+
+.#{$iot-prefix}--hotspot-text-style-tab {
+ &__text-style {
+ :first-child {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+
+ :last-child {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+ }
+
+ &__dropdown {
+ width: 100%;
+ }
+
+ &__row {
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+ gap: $spacing-06;
+
+ margin-top: $spacing-06;
+
+ .#{$prefix}--dropdown__wrapper.#{$prefix}--list-box__wrapper {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/components/IconDropdown/__snapshots__/IconDropdown.story.storyshot b/src/components/IconDropdown/__snapshots__/IconDropdown.story.storyshot
index e3ed95130f..16f7a876bd 100644
--- a/src/components/IconDropdown/__snapshots__/IconDropdown.story.storyshot
+++ b/src/components/IconDropdown/__snapshots__/IconDropdown.story.storyshot
@@ -36,8 +36,8 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
>
@@ -52,10 +52,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
aria-disabled={false}
aria-expanded={false}
aria-haspopup="listbox"
- aria-labelledby="downshift-22-label downshift-22-toggle-button"
+ aria-labelledby="downshift-25-label downshift-25-toggle-button"
className="bx--list-box__field"
disabled={false}
- id="downshift-22-toggle-button"
+ id="downshift-25-toggle-button"
onClick={[Function]}
onKeyDown={[Function]}
type="button"
@@ -90,9 +90,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper