+ )
+ }
+
+ render()
+```
diff --git a/packages/ui-color-picker/src/ColorContrast/v1/__tests__/ColorContrast.test.tsx b/packages/ui-color-picker/src/ColorContrast/v2/__tests__/ColorContrast.test.tsx
similarity index 100%
rename from packages/ui-color-picker/src/ColorContrast/v1/__tests__/ColorContrast.test.tsx
rename to packages/ui-color-picker/src/ColorContrast/v2/__tests__/ColorContrast.test.tsx
diff --git a/packages/ui-color-picker/src/ColorContrast/v2/index.tsx b/packages/ui-color-picker/src/ColorContrast/v2/index.tsx
new file mode 100644
index 0000000000..6a6120c23f
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorContrast/v2/index.tsx
@@ -0,0 +1,236 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { omitProps } from '@instructure/ui-react-utils'
+import { error } from '@instructure/console'
+import {
+ contrastWithAlpha,
+ validateContrast
+} from '@instructure/ui-color-utils'
+import { withStyle } from '@instructure/emotion'
+
+import { Text } from '@instructure/ui-text/latest'
+import { Heading } from '@instructure/ui-heading/latest'
+import { Pill } from '@instructure/ui-pill/latest'
+
+import ColorIndicator from '../../ColorIndicator/v2'
+
+import { allowedProps } from './props'
+import type { ColorContrastProps, ColorContrastState } from './props'
+
+import generateStyle from './styles'
+
+/**
+---
+category: components
+---
+**/
+@withStyle(generateStyle)
+class ColorContrast extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorContrast'
+
+ static defaultProps = {
+ withoutColorPreview: false,
+ validationLevel: 'AA'
+ }
+
+ constructor(props: ColorContrastProps) {
+ super(props)
+
+ this.state = {
+ contrast: 1,
+ isValidNormalText: false,
+ isValidLargeText: false,
+ isValidGraphicsText: false
+ }
+ }
+
+ ref: HTMLDivElement | null = null
+
+ handleRef = (el: HTMLDivElement | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ componentDidMount() {
+ this.props.makeStyles?.()
+ this.calcState()
+ }
+
+ componentDidUpdate(prevProps: ColorContrastProps) {
+ this.props.makeStyles?.()
+ if (
+ prevProps?.firstColor !== this.props?.firstColor ||
+ prevProps?.secondColor !== this.props?.secondColor ||
+ prevProps?.validationLevel !== this.props?.validationLevel
+ ) {
+ const newState = this.calcState()
+
+ this.props?.onContrastChange?.({
+ contrast: newState.contrast,
+ isValidNormalText: newState.isValidNormalText,
+ isValidLargeText: newState.isValidLargeText,
+ isValidGraphicsText: newState.isValidGraphicsText,
+ firstColor: this.props.firstColor,
+ secondColor: this.props.secondColor
+ })
+ }
+ }
+
+ renderStatus = (pass: boolean, description: string) => {
+ const { successLabel, failureLabel, styles } = this.props
+ return (
+
+ )
+ }
+}
+
+export { ColorContrast }
+export default ColorContrast
diff --git a/packages/ui-color-picker/src/ColorContrast/v2/props.ts b/packages/ui-color-picker/src/ColorContrast/v2/props.ts
new file mode 100644
index 0000000000..c8abd09b60
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorContrast/v2/props.ts
@@ -0,0 +1,181 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+import type {
+ OtherHTMLAttributes,
+ ColorContrastTheme
+} from '@instructure/shared-types'
+
+type HeadingLevel = U
+
+type ColorContrastOwnProps = {
+ /**
+ * Provides a reference to the component's underlying html element.
+ */
+ elementRef?: (element: Element | null) => void
+ /**
+ * Text of the failure indicator (Suggested english text: FAIL)
+ */
+ failureLabel: string
+ /**
+ * The first color to compare (HEX code)
+ */
+ firstColor: string
+ /**
+ * The name of the first color which will be compared
+ */
+ firstColorLabel?: string
+ /**
+ * Text of the third check (Suggested english text: Graphics text)
+ */
+ graphicsTextLabel: string
+ /**
+ * Label of the component
+ */
+ label: string
+ /**
+ * The heading level for the label. If provided, the label will be rendered as a `` instead of ``.
+ */
+ labelLevel?: HeadingLevel<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>
+ /**
+ * Text of the second check (Suggested english text: Large text)
+ */
+ largeTextLabel: string
+ /**
+ * Text of the first check (Suggested english text: Normal text)
+ */
+ normalTextLabel: string
+ /**
+ * The second color to compare (HEX code)
+ */
+ secondColor: string
+ /**
+ * The name of the second color which will be compared
+ */
+ secondColorLabel?: string
+ /**
+ * Text of the success indicator (Suggested english text: PASS)
+ */
+ successLabel: string
+ /**
+ * Toggles the color preview part of the component.
+ *
+ * If true, firstColorLabel and secondColorLabel is not necessary.
+ * Otherwise, it is required.
+ */
+ withoutColorPreview?: boolean
+ /**
+ * Triggers a callback whenever the contrast changes, due to a changing color input.
+ * Communicates the contrast and the success/fail state of the contrast, depending on
+ * the situation:
+ *
+ * isValidNormalText true if at least 4.5:1
+ *
+ * isValidLargeText true if at least 3:1
+ *
+ * isValidGraphicsText true if at least 3:1
+ */
+ onContrastChange?: (conrastData: {
+ contrast: number
+ isValidNormalText: boolean
+ isValidLargeText: boolean
+ isValidGraphicsText: boolean
+ firstColor: string
+ secondColor: string
+ }) => null
+ /**
+ * According to WCAG 2.2
+ *
+ * AA level (https://www.w3.org/TR/WCAG22/#contrast-minimum)
+ *
+ * text: 4.5:1
+ *
+ * large text: 3:1
+ *
+ * non-text: 3:1 (https://www.w3.org/TR/WCAG22/#non-text-contrast)
+ *
+ *
+ * AAA level (https://www.w3.org/TR/WCAG22/#contrast-enhanced)
+ *
+ * text: 7:1
+ *
+ * large text: 4.5:1
+ *
+ * non-text: 3:1 (https://www.w3.org/TR/WCAG22/#non-text-contrast)
+ */
+ validationLevel?: 'AA' | 'AAA'
+}
+
+type PropKeys = keyof ColorContrastOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorContrastProps = ColorContrastOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type ColorContrastStyle = ComponentStyle<
+ | 'colorContrast'
+ | 'successDescription'
+ | 'failureDescription'
+ | 'statusWrapper'
+ | 'colorIndicator'
+ | 'statusIndicatorWrapper'
+ | 'colorIndicatorLabel'
+ | 'pickedColorHex'
+ | 'colorPreview'
+ | 'firstColorPreview'
+ | 'secondColorPreview'
+ | 'label'
+ | 'onContrastChange'
+ | 'validationLevel'
+>
+const allowedProps: AllowedPropKeys = [
+ 'elementRef',
+ 'failureLabel',
+ 'firstColor',
+ 'firstColorLabel',
+ 'graphicsTextLabel',
+ 'withoutColorPreview',
+ 'label',
+ 'labelLevel',
+ 'largeTextLabel',
+ 'normalTextLabel',
+ 'secondColor',
+ 'secondColorLabel',
+ 'successLabel',
+ 'onContrastChange',
+ 'validationLevel'
+]
+
+type ColorContrastState = {
+ contrast: number
+ isValidNormalText: boolean
+ isValidLargeText: boolean
+ isValidGraphicsText: boolean
+}
+export type { ColorContrastProps, ColorContrastStyle, ColorContrastState }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorContrast/v2/styles.ts b/packages/ui-color-picker/src/ColorContrast/v2/styles.ts
new file mode 100644
index 0000000000..fc85bce39c
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorContrast/v2/styles.ts
@@ -0,0 +1,108 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { NewComponentTypes } from '@instructure/ui-themes'
+import type { ColorContrastProps } from './props'
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['ColorContrast'],
+ props: ColorContrastProps
+) => {
+ const statusDescriptionStyle = (pass: boolean) => ({
+ label: pass
+ ? 'colorContrast__successDescription'
+ : 'colorContrast__failureDescription',
+ flex: 1,
+ color: pass ? componentTheme.successColor : componentTheme.failureColor
+ })
+
+ return {
+ colorContrast: {
+ label: 'colorContrast',
+ width: componentTheme.width,
+ fontFamily: componentTheme.fontFamily,
+ fontWeight: componentTheme.fontWeight,
+ lineHeight: componentTheme.lineHeight,
+ fontSize: componentTheme.fontSize
+ },
+ statusWrapper: {
+ label: 'colorContrast__statusWrapper',
+ width: '100%',
+ display: 'flex',
+ marginBottom: componentTheme.statusWrapperBottomMargin
+ },
+ successDescription: statusDescriptionStyle(true),
+ failureDescription: statusDescriptionStyle(false),
+ colorIndicator: {
+ marginInlineEnd: componentTheme.colorIndicatorRightMargin
+ },
+ statusIndicatorWrapper: {
+ label: 'colorContrast__statusIndicatorWrapper',
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'row-reverse'
+ },
+ colorIndicatorLabel: {
+ label: 'colorContrast__colorIndicatorLabel',
+ wordBreak: 'break-all',
+ color: componentTheme.colorIndicatorLabelColor
+ },
+ pickedColorHex: {
+ label: 'colorContrast__pickedColorHex',
+ color: componentTheme.pickedHexColor
+ },
+ colorPreview: {
+ label: 'colorContrast__colorPreview',
+ display: 'flex',
+ width: '100%',
+ marginBottom: componentTheme.colorPreviewBottomMargin,
+ marginTop: componentTheme.colorPreviewTopMargin
+ },
+ firstColorPreview: {
+ label: 'colorContrast__firstColorPreview',
+ display: 'flex',
+ flex: 1
+ },
+ secondColorPreview: {
+ label: 'colorContrast__secondColorPreview',
+ display: 'flex'
+ },
+ label: {
+ label: 'colorContrast__label',
+ marginBottom: componentTheme.labelBottomMargin,
+ ...(props.labelLevel && { fontWeight: 'bold' })
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorIndicator/v2/README.md b/packages/ui-color-picker/src/ColorIndicator/v2/README.md
new file mode 100644
index 0000000000..4e55631036
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorIndicator/v2/README.md
@@ -0,0 +1,76 @@
+---
+describes: ColorIndicator
+---
+
+A component displaying a circle with checkerboard background capable of displaying colors
+
+### Color Indicator
+
+```js
+---
+type: example
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Shapes
+
+`ColorIndicator` can have a `shape` prop. It is either a `circle` which is the default, or a `rectangle`
+
+```js
+---
+type: example
+---
+
+
+
+
+
+
+
+
+```
+
+### Color Button Pattern
+
+```js
+---
+type: example
+---
+
+
+
+```
diff --git a/packages/ui-color-picker/src/ColorIndicator/v1/__tests__/ColorIndicator.test.tsx b/packages/ui-color-picker/src/ColorIndicator/v2/__tests__/ColorIndicator.test.tsx
similarity index 100%
rename from packages/ui-color-picker/src/ColorIndicator/v1/__tests__/ColorIndicator.test.tsx
rename to packages/ui-color-picker/src/ColorIndicator/v2/__tests__/ColorIndicator.test.tsx
diff --git a/packages/ui-color-picker/src/ColorIndicator/v2/index.tsx b/packages/ui-color-picker/src/ColorIndicator/v2/index.tsx
new file mode 100644
index 0000000000..469c1d1dfe
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorIndicator/v2/index.tsx
@@ -0,0 +1,84 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { omitProps } from '@instructure/ui-react-utils'
+import { withStyle } from '@instructure/emotion'
+
+import generateStyle from './styles'
+
+import type { ColorIndicatorProps } from './props'
+import { allowedProps } from './props'
+
+/**
+---
+category: components
+---
+**/
+@withStyle(generateStyle)
+class ColorIndicator extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorIndicator'
+
+ static defaultProps = {
+ shape: 'circle'
+ }
+
+ ref: HTMLDivElement | null = null
+
+ componentDidMount() {
+ this.props.makeStyles?.()
+ }
+
+ componentDidUpdate() {
+ this.props.makeStyles?.()
+ }
+
+ handleRef = (el: HTMLDivElement | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ render() {
+ const { styles } = this.props
+
+ return (
+
+ )
+ }
+}
+
+export default ColorIndicator
+export { ColorIndicator }
diff --git a/packages/ui-color-picker/src/ColorIndicator/v2/props.ts b/packages/ui-color-picker/src/ColorIndicator/v2/props.ts
new file mode 100644
index 0000000000..d404dae017
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorIndicator/v2/props.ts
@@ -0,0 +1,58 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+import type {
+ OtherHTMLAttributes,
+ ColorIndicatorTheme
+} from '@instructure/shared-types'
+
+type ColorIndicatorOwnProps = {
+ /**
+ * Valid CSS color string. E.g.: #555, rgba(55,55,55,1). It can accept empty strings
+ */
+ color?: string
+ /**
+ * Provides a reference to the `ColorIndicator`'s underlying html element.
+ */
+ elementRef?: (element: Element | null) => void
+ /**
+ * Sets the shape of the indicator. Either a circle or a rectangle
+ */
+ shape?: 'circle' | 'rectangle'
+}
+
+type PropKeys = keyof ColorIndicatorOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorIndicatorProps = ColorIndicatorOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type ColorIndicatorStyle = ComponentStyle<'colorIndicator'>
+const allowedProps: AllowedPropKeys = ['color', 'elementRef', 'shape']
+
+export type { ColorIndicatorProps, ColorIndicatorStyle }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorIndicator/v2/styles.ts b/packages/ui-color-picker/src/ColorIndicator/v2/styles.ts
new file mode 100644
index 0000000000..6b6c4eb931
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorIndicator/v2/styles.ts
@@ -0,0 +1,89 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { ColorIndicatorProps, ColorIndicatorStyle } from './props'
+import type { NewComponentTypes } from '@instructure/ui-themes'
+import type { RGBAType } from '@instructure/ui-color-utils'
+import conversions from '@instructure/ui-color-utils'
+import { isValid } from '@instructure/ui-color-utils'
+
+const calcBlendedColor = (c1: RGBAType, c2: RGBAType) => {
+ // 0.4 as decided by design
+ const c2Alpha = c2.a * 0.4
+ const c1Alpha = 1 - c2Alpha
+ const alpha = 1 - c1Alpha * (1 - c1Alpha)
+
+ return `rgba(
+ ${(c2.r * c2Alpha) / alpha + (c1.r * c1Alpha * (1 - c2Alpha)) / alpha},
+ ${(c2.g * c2Alpha) / alpha + (c1.g * c1Alpha * (1 - c2Alpha)) / alpha},
+ ${(c2.b * c2Alpha) / alpha + (c1.b * c1Alpha * (1 - c2Alpha)) / alpha},
+ ${c2.a < 0.6 ? 0.6 : c2.a})`
+}
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['ColorIndicator'],
+ props: ColorIndicatorProps
+): ColorIndicatorStyle => {
+ const { color, shape } = props
+
+ return {
+ colorIndicator: {
+ label: 'colorIndicator',
+ width:
+ shape === 'rectangle'
+ ? componentTheme.rectangleIndicatorSize
+ : componentTheme.circleIndicatorSize,
+ height:
+ shape === 'rectangle'
+ ? componentTheme.rectangleIndicatorSize
+ : componentTheme.circleIndicatorSize,
+ borderRadius:
+ shape === 'rectangle'
+ ? componentTheme.rectangularIndicatorBorderRadius
+ : componentTheme.circleIndicatorSize,
+ boxSizing: 'border-box',
+ borderWidth: componentTheme.borderWidth,
+ boxShadow: color ? `inset 0 0 0 1.5rem ${color}` : 'none',
+ borderStyle: 'solid',
+ backgroundImage: componentTheme.backgroundImage,
+ backgroundSize: componentTheme.backgroundSize,
+ backgroundPosition: componentTheme.backgroundPosition,
+ borderColor: calcBlendedColor(
+ conversions.colorToRGB(componentTheme.colorIndicatorBorderColor),
+ conversions.colorToRGB(isValid(color!) ? color! : '#fff')
+ )
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorMixer/v1/ColorPalette/index.tsx b/packages/ui-color-picker/src/ColorMixer/v1/ColorPalette/index.tsx
index 1a8d7948ee..15f7ed8fb0 100644
--- a/packages/ui-color-picker/src/ColorMixer/v1/ColorPalette/index.tsx
+++ b/packages/ui-color-picker/src/ColorMixer/v1/ColorPalette/index.tsx
@@ -28,8 +28,8 @@ import { withStyleLegacy as withStyle } from '@instructure/emotion'
import { addEventListener } from '@instructure/ui-dom-utils'
import type { HSVType } from '@instructure/ui-color-utils'
-import { View } from '@instructure/ui-view/latest'
-import type { ViewOwnProps } from '@instructure/ui-view/latest'
+import { View } from '@instructure/ui-view/v11_6'
+import type { ViewOwnProps } from '@instructure/ui-view/v11_6'
import { px } from '@instructure/ui-utils'
import { withDeterministicId } from '@instructure/ui-react-utils'
diff --git a/packages/ui-color-picker/src/ColorMixer/v1/RGBAInput/index.tsx b/packages/ui-color-picker/src/ColorMixer/v1/RGBAInput/index.tsx
index d63ce98664..965bf9b53f 100644
--- a/packages/ui-color-picker/src/ColorMixer/v1/RGBAInput/index.tsx
+++ b/packages/ui-color-picker/src/ColorMixer/v1/RGBAInput/index.tsx
@@ -28,7 +28,7 @@ import { withStyleLegacy as withStyle } from '@instructure/emotion'
import shallowCompare from '../utils/shallowCompare'
import { ScreenReaderContent } from '@instructure/ui-a11y-content'
-import { TextInput } from '@instructure/ui-text-input/latest'
+import { TextInput } from '@instructure/ui-text-input/v11_6'
import { allowedProps } from './props'
import type { RGBAInputProps, RGBAInputState } from './props'
diff --git a/packages/ui-color-picker/src/ColorMixer/v1/Slider/index.tsx b/packages/ui-color-picker/src/ColorMixer/v1/Slider/index.tsx
index 2bb627b72f..3f888cd202 100644
--- a/packages/ui-color-picker/src/ColorMixer/v1/Slider/index.tsx
+++ b/packages/ui-color-picker/src/ColorMixer/v1/Slider/index.tsx
@@ -27,8 +27,8 @@ import { Component } from 'react'
import { addEventListener } from '@instructure/ui-dom-utils'
import { withStyleLegacy as withStyle } from '@instructure/emotion'
-import { View } from '@instructure/ui-view/latest'
-import type { ViewOwnProps } from '@instructure/ui-view/latest'
+import { View } from '@instructure/ui-view/v11_6'
+import type { ViewOwnProps } from '@instructure/ui-view/v11_6'
import { allowedProps } from './props'
import type { SliderProps, SliderStyleProps } from './props'
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/index.tsx b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/index.tsx
new file mode 100644
index 0000000000..387522bab6
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/index.tsx
@@ -0,0 +1,263 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyrigfht notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { withStyle } from '@instructure/emotion'
+import { addEventListener } from '@instructure/ui-dom-utils'
+import type { HSVType } from '@instructure/ui-color-utils'
+
+import { View } from '@instructure/ui-view/latest'
+import type { ViewOwnProps } from '@instructure/ui-view/latest'
+import { px } from '@instructure/ui-utils'
+import { withDeterministicId } from '@instructure/ui-react-utils'
+
+import shallowCompare from '../utils/shallowCompare'
+
+import { allowedProps } from './props'
+import type { ColorPaletteProps, ColorPaletteState } from './props'
+
+import generateStyle from './styles'
+
+/**
+---
+private: true
+---
+**/
+@withDeterministicId()
+@withStyle(generateStyle,'Palette')
+class ColorPalette extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorMixer.Palette'
+
+ constructor(props: ColorPaletteProps) {
+ super(props)
+ this.state = {
+ colorPosition: { x: 0, y: 0 }
+ }
+ this._id = props.deterministicId!('ColorMixer__Palette')
+ }
+ private readonly _id: string
+ ref: Element | null = null
+ private _paletteRef: HTMLDivElement | null = null
+ private _mouseMoveListener?: { remove(): void }
+ private _mouseUpListener?: { remove(): void }
+ private _paletteOffset = px(this.props.styles?.paletteOffset as string)
+
+ handleRef = (el: Element | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ componentDidMount() {
+ this.props.makeStyles?.(this.state)
+ this.setState({
+ colorPosition: this.calcPositionFromColor(this.props.color)
+ })
+ }
+
+ componentDidUpdate(prevProps: ColorPaletteProps) {
+ this.props.makeStyles?.(this.state)
+
+ if (shallowCompare(prevProps.color, this.props.color)) {
+ this.setState({
+ colorPosition: this.calcPositionFromColor(this.props.color)
+ })
+ }
+ }
+ componentWillUnmount() {
+ this.removeEventListeners()
+ }
+
+ get paletteWidth() {
+ return this._paletteRef!.getBoundingClientRect().width - this._paletteOffset
+ }
+
+ get paletteHeight() {
+ return (
+ this._paletteRef!.getBoundingClientRect().height - this._paletteOffset
+ )
+ }
+ calcSaturation = (position: number) =>
+ Math.round((position / this.paletteWidth) * 100) / 100
+ calcLuminance = (position: number) =>
+ Math.round(((this.paletteHeight - position) / this.paletteHeight) * 100) /
+ 100
+
+ calcPositionFromColor(hsv: HSVType) {
+ const { s, v } = hsv
+ const x = s * this.paletteWidth
+ const y = (1 - v) * this.paletteHeight
+ return { x, y }
+ }
+
+ handlePaletteMouseDown(e: React.MouseEvent) {
+ // Prevent selection outside palette during dragging the indicator
+ e.preventDefault()
+
+ // Restore focus since preventDefault() blocks automatic focus on mouse event
+ if (e.currentTarget instanceof HTMLElement) {
+ e.currentTarget.focus()
+ }
+
+ this.handleChange(e)
+
+ this._mouseMoveListener = addEventListener(
+ window,
+ 'mousemove',
+ this.handleChange
+ )
+ this._mouseUpListener = addEventListener(
+ window,
+ 'mouseup',
+ this.handleMouseUp
+ )
+ }
+
+ handleMouseUp = () => {
+ this.removeEventListeners()
+ }
+
+ removeEventListeners() {
+ this._mouseMoveListener?.remove()
+ this._mouseUpListener?.remove()
+ }
+
+ calcColorPosition(clientX: number, clientY: number) {
+ const { x, y } = this._paletteRef!.getBoundingClientRect()
+
+ return this.applyBoundaries(clientX - x, clientY - y)
+ }
+
+ applyBoundaries(x: number, y: number) {
+ let newXPosition = x
+ let newYPosition = y
+ if (x > this.paletteWidth) {
+ newXPosition = this.paletteWidth
+ }
+ if (x < 0) {
+ newXPosition = 0
+ }
+ if (y > this.paletteHeight) {
+ newYPosition = this.paletteHeight
+ }
+ if (y < 0) {
+ newYPosition = 0
+ }
+ return { newXPosition, newYPosition }
+ }
+
+ handleChange = (e: React.MouseEvent) => {
+ if (this.props.disabled) return
+ const { clientX, clientY } = e
+ const { newXPosition, newYPosition } = this.calcColorPosition(
+ clientX,
+ clientY
+ )
+ this.setState({
+ colorPosition: { x: newXPosition, y: newYPosition }
+ })
+
+ this.props.onChange({
+ h: this.props.hue,
+ s: this.calcSaturation(newXPosition),
+ v: this.calcLuminance(newYPosition)
+ })
+ }
+
+ handleKeyDown(e: React.KeyboardEvent) {
+ if (this.props.disabled) return
+ const { key } = e
+ if (key === 'Tab') return
+ e.preventDefault()
+ let deltaX = 0
+ let deltaY = 0
+ if (key === 'ArrowLeft' || key === 'a') {
+ deltaX = -2
+ }
+ if (key === 'ArrowRight' || key === 'd') {
+ deltaX = 2
+ }
+ if (key === 'ArrowUp' || key === 'w') {
+ deltaY = -2
+ }
+ if (key === 'ArrowDown' || key === 's') {
+ deltaY = 2
+ }
+
+ const { newXPosition, newYPosition } = this.applyBoundaries(
+ this.state.colorPosition.x + deltaX,
+ this.state.colorPosition.y + deltaY
+ )
+
+ this.setState({
+ colorPosition: { x: newXPosition, y: newYPosition }
+ })
+ this.props.onChange({
+ h: this.props.hue,
+ s: this.calcSaturation(newXPosition),
+ v: this.calcLuminance(newYPosition)
+ })
+ }
+
+ render() {
+ return (
+ this.handleKeyDown(e)}
+ onMouseDown={(e) => this.handlePaletteMouseDown(e)}
+ aria-label={this.props.navigationExplanationScreenReaderLabel}
+ id={this._id}
+ role="button"
+ >
+
+ {this.props.disabled && (
+
+ )}
+
{
+ this._paletteRef = ref
+ }}
+ />
+
+ )
+ }
+}
+
+export default ColorPalette
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/props.ts b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/props.ts
new file mode 100644
index 0000000000..4ad7985376
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/props.ts
@@ -0,0 +1,75 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+
+import type {
+ OtherHTMLAttributes,
+ ColorMixerPaletteTheme
+} from '@instructure/shared-types'
+import type { HSVType } from '@instructure/ui-color-utils'
+import type { WithDeterministicIdProps } from '@instructure/ui-react-utils'
+
+type ColorPaletteOwnProps = {
+ disabled?: boolean
+ hue: number
+ color: HSVType
+ width: number
+ height: number
+ indicatorRadius: number
+ onChange: (rgb: HSVType) => void
+ elementRef?: (element: Element | null) => void
+ navigationExplanationScreenReaderLabel: string
+}
+
+type ColorPaletteState = {
+ colorPosition: { x: number; y: number }
+}
+
+type PropKeys = keyof ColorPaletteOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorPaletteProps = ColorPaletteOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes &
+ WithDeterministicIdProps
+
+type ColorPaletteStyle = ComponentStyle<
+ 'ColorPalette' | 'indicator' | 'palette' | 'disabledOverlay' | 'paletteOffset'
+>
+const allowedProps: AllowedPropKeys = [
+ 'disabled',
+ 'hue',
+ 'color',
+ 'width',
+ 'height',
+ 'indicatorRadius',
+ 'onChange',
+ 'elementRef',
+ 'navigationExplanationScreenReaderLabel'
+]
+
+export type { ColorPaletteProps, ColorPaletteState, ColorPaletteStyle }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/styles.ts b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/styles.ts
new file mode 100644
index 0000000000..2517c1f8f3
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/ColorPalette/styles.ts
@@ -0,0 +1,96 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type {
+ ColorPaletteStyle,
+ ColorPaletteProps,
+ ColorPaletteState
+} from './props'
+import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes'
+import { px } from '@instructure/ui-utils'
+
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['Palette'],
+ props: ColorPaletteProps,
+ _sharedTokens: SharedTokens,
+ state: ColorPaletteState
+): ColorPaletteStyle => {
+ return {
+ ColorPalette: {
+ label: 'ColorPalette'
+ },
+ indicator: {
+ label: 'ColorPalette__indicator',
+ width: `${props.indicatorRadius / 8}rem`,
+ height: `${props.indicatorRadius / 8}rem`,
+ borderRadius: `${props.indicatorRadius / 8}rem`,
+ background: componentTheme.whiteColor,
+ position: 'absolute',
+ borderStyle: 'solid',
+ borderWidth: componentTheme.indicatorBorderWidth,
+ borderColor: componentTheme.indicatorBorderColor,
+ top: `${
+ state?.colorPosition?.y - px(`${props.indicatorRadius / 16}rem`)
+ }px`,
+ left: `${
+ state?.colorPosition?.x - px(`${props.indicatorRadius / 16}rem`)
+ }px`
+ },
+ palette: {
+ label: 'ColorPalette__palette',
+ width: `${props.width / 16}rem`,
+ height: `${props.height / 16}rem`,
+ borderRadius: componentTheme.paletteBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: componentTheme.paletteBorderWidth,
+ boxSizing: 'border-box',
+ borderColor: componentTheme.colorIndicatorBorderColor,
+ background: `linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)),
+ linear-gradient(to right, white, hsl(${props.hue},100%,50%))`
+ },
+ disabledOverlay: {
+ label: 'ColorPalette__disabledOverlay',
+ background: 'rgba(255,255,255,.5)',
+ zIndex: componentTheme.disabledOverlayZIndex,
+ width: `${props.width / 16 + 1}rem`,
+ height: `${props.height / 16 + 1}rem`,
+ position: 'absolute',
+ top: '-.5rem',
+ left: '-.5rem'
+ },
+ paletteOffset: componentTheme.paletteOffset
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/README.md b/packages/ui-color-picker/src/ColorMixer/v2/README.md
new file mode 100644
index 0000000000..bc7c7aca41
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/README.md
@@ -0,0 +1,76 @@
+---
+describes: ColorMixer
+---
+
+A component for selecting a color. It lets pick from a palette manually or type in an RGBA color.
+
+### Color Mixer
+
+```js
+---
+type: example
+---
+ const Example = () => {
+ const [value, setValue] = useState('#328DCFC2')
+
+ return (
+
+
+ setValue(value)}
+ rgbRedInputScreenReaderLabel="Input field for red"
+ rgbGreenInputScreenReaderLabel="Input field for green"
+ rgbBlueInputScreenReaderLabel="Input field for blue"
+ rgbAlphaInputScreenReaderLabel="Input field for alpha"
+ colorSliderNavigationExplanationScreenReaderLabel={`You are on a color slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`}
+ alphaSliderNavigationExplanationScreenReaderLabel={`You are on an alpha slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`}
+ colorPaletteNavigationExplanationScreenReaderLabel={`You are on a color palette. To navigate on the palette up, left, down or right, use the 'W', 'A', 'S' and 'D' buttons respectively`}
+ />
+
+ {value}
+
+
+
+
+
+
+ )
+ }
+
+ render()
+```
+
+### Disabled Color Mixer
+
+```js
+---
+type: example
+---
+ {}}
+ rgbRedInputScreenReaderLabel='Input field for red'
+ rgbGreenInputScreenReaderLabel='Input field for green'
+ rgbBlueInputScreenReaderLabel='Input field for blue'
+ rgbAlphaInputScreenReaderLabel='Input field for alpha'
+ colorSliderNavigationExplanationScreenReaderLabel={`You are on a color slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`}
+ alphaSliderNavigationExplanationScreenReaderLabel={`You are on an alpha slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`}
+ colorPaletteNavigationExplanationScreenReaderLabel={`You are on a color palette. To navigate on the palette up, left, down or right, use the 'W', 'A', 'S' and 'D' buttons respectively`}
+/>
+
+```
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/index.tsx b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/index.tsx
new file mode 100644
index 0000000000..e764e613fd
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/index.tsx
@@ -0,0 +1,169 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { withStyle } from '@instructure/emotion'
+import shallowCompare from '../utils/shallowCompare'
+
+import { ScreenReaderContent } from '@instructure/ui-a11y-content'
+import { TextInput } from '@instructure/ui-text-input/latest'
+
+import { allowedProps } from './props'
+import type { RGBAInputProps, RGBAInputState } from './props'
+
+import generateStyle from './styles'
+
+/**
+---
+private: true
+---
+**/
+@withStyle(generateStyle, 'RgbaInput')
+class RGBAInput extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorMixer.RGBAInput'
+
+ static defaultProps = {
+ withAlpha: false
+ }
+
+ constructor(props: RGBAInputProps) {
+ super(props)
+ this.state = {
+ value: props.value
+ }
+ }
+
+ ref: HTMLDivElement | null = null
+
+ handleRef = (el: HTMLDivElement | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ componentDidMount() {
+ this.props.makeStyles?.()
+ }
+
+ componentDidUpdate(prevProps: RGBAInputProps) {
+ this.props.makeStyles?.()
+ if (
+ shallowCompare(prevProps.value, this.props.value) ||
+ shallowCompare(this.state.value, this.props.value)
+ ) {
+ this.setState({
+ value: this.props.value
+ })
+ }
+ }
+
+ handleChange(type: string, e: React.ChangeEvent) {
+ const upperLimit = type === 'a' ? 100 : 255
+ const newValue =
+ type === 'a' ? Number(e.target.value) / 100 : Number(e.target.value)
+ const newColor = { ...this.props.value, [type]: newValue }
+
+ if (!isNaN(Number(newValue)) && newValue >= 0 && newValue <= upperLimit) {
+ this.setState({ value: newColor })
+ this.props.onChange(newColor)
+ return
+ }
+ }
+
+ render() {
+ const { styles, disabled, label, withAlpha } = this.props
+
+ return (
+
+ )
+ }
+}
+
+export default RGBAInput
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/props.ts b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/props.ts
new file mode 100644
index 0000000000..a815a47fb2
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/props.ts
@@ -0,0 +1,77 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+
+import type {
+ OtherHTMLAttributes,
+ ColorMixerRGBAInputTheme
+} from '@instructure/shared-types'
+import type { RGBAType } from '@instructure/ui-color-utils'
+
+type RGBAInputOwnProps = {
+ disabled?: boolean
+ label?: string
+ width: number
+ value: RGBAType
+ onChange: (rgba: RGBAType) => void
+ withAlpha?: boolean
+ rgbRedInputScreenReaderLabel: string
+ rgbGreenInputScreenReaderLabel: string
+ rgbBlueInputScreenReaderLabel: string
+ rgbAlphaInputScreenReaderLabel: string
+ elementRef?: (element: Element | null) => void
+}
+
+type RGBAInputState = {
+ value: RGBAType
+}
+
+type PropKeys = keyof RGBAInputOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type RGBAInputProps = RGBAInputOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type RGBAInputStyle = ComponentStyle<
+ 'RGBAInput' | 'label' | 'inputContainer' | 'rgbInput' | 'aInput'
+>
+const allowedProps: AllowedPropKeys = [
+ 'disabled',
+ 'label',
+ 'width',
+ 'value',
+ 'onChange',
+ 'withAlpha',
+ 'rgbRedInputScreenReaderLabel',
+ 'rgbGreenInputScreenReaderLabel',
+ 'rgbBlueInputScreenReaderLabel',
+ 'rgbAlphaInputScreenReaderLabel',
+ 'elementRef'
+]
+
+export type { RGBAInputProps, RGBAInputState, RGBAInputStyle }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/styles.ts b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/styles.ts
new file mode 100644
index 0000000000..dc3e55d98e
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/RGBAInput/styles.ts
@@ -0,0 +1,70 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { RGBAInputStyle, RGBAInputProps } from './props'
+import type { NewComponentTypes } from '@instructure/ui-themes'
+
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['RgbaInput'],
+ props: RGBAInputProps
+): RGBAInputStyle => {
+ return {
+ RGBAInput: {
+ label: 'RGBAInput',
+ marginTop: componentTheme.rgbaInputTopMargin,
+ width: `${props.width / 16}rem`
+ },
+ label: {
+ label: 'RGBAInput__label',
+ fontWeight: componentTheme.labelFontWeight,
+ color: componentTheme.rgbaInputLabel
+ },
+ inputContainer: {
+ label: 'RGBAInput__inputContainer',
+ display: 'flex',
+ marginTop: componentTheme.inputContainerTopMargin
+ },
+ rgbInput: {
+ label: 'RGBAInput__rgbInput',
+ marginInlineEnd: componentTheme.tgbInputTopMargin,
+ flex: 7
+ },
+ aInput: {
+ label: 'RGBAInput__aInput',
+ flex: 10
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/Slider/index.tsx b/packages/ui-color-picker/src/ColorMixer/v2/Slider/index.tsx
new file mode 100644
index 0000000000..1d8c98d14b
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/Slider/index.tsx
@@ -0,0 +1,245 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { addEventListener } from '@instructure/ui-dom-utils'
+import { withStyle } from '@instructure/emotion'
+
+import { View } from '@instructure/ui-view/latest'
+import type { ViewOwnProps } from '@instructure/ui-view/latest'
+
+import { allowedProps } from './props'
+import type { SliderProps, SliderStyleProps } from './props'
+
+import generateStyle from './styles'
+
+/**
+---
+private: true
+---
+**/
+@withStyle(generateStyle, 'Slider')
+class Slider extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorMixer.Slider'
+
+ static defaultProps = {
+ isColorSlider: false
+ }
+
+ ref: Element | null = null
+ private _sliderRef: HTMLDivElement | null = null
+ private _mouseMoveListener?: { remove(): void }
+ private _mouseUpListener?: { remove(): void }
+
+ handleRef = (el: Element | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ componentDidMount() {
+ this.props.makeStyles?.(this.makeStylesProps)
+ }
+
+ componentDidUpdate() {
+ this.props.makeStyles?.(this.makeStylesProps)
+ }
+
+ componentWillUnmount() {
+ this.removeEventListeners()
+ }
+
+ removeEventListeners() {
+ this._mouseMoveListener?.remove()
+ this._mouseUpListener?.remove()
+ }
+
+ get sliderPositionFromValue() {
+ return this.calcSliderPositionFromValue(this.props.value)
+ }
+
+ get roundedValue() {
+ const { value, maxValue } = this.props
+
+ if (maxValue <= 1) {
+ return Math.round(value * 100)
+ } else {
+ return Math.round(value)
+ }
+ }
+
+ get makeStylesProps(): SliderStyleProps {
+ return {
+ sliderPositionFromValue: this.sliderPositionFromValue
+ }
+ }
+
+ handleMouseDown(e: React.MouseEvent) {
+ // Prevent selection outside palette during dragging the indicator
+ e.preventDefault()
+
+ // Restore focus since preventDefault() blocks automatic focus on mouse event
+ if (e.currentTarget instanceof HTMLElement) {
+ e.currentTarget.focus()
+ }
+
+ this.handleChange(e)
+
+ this._mouseMoveListener = addEventListener(
+ window,
+ 'mousemove',
+ this.handleChange
+ )
+ this._mouseUpListener = addEventListener(
+ window,
+ 'mouseup',
+ this.handleMouseUp
+ )
+ }
+
+ handleChange = (e: React.MouseEvent) => {
+ if (this.props.disabled) return
+ const { clientX } = e
+ const newPosition = this.calcSliderPositionFromCursorPosition(
+ clientX,
+ this._sliderRef!
+ )
+
+ this.props.onChange(this.calcValueFromSliderPosition(newPosition))
+ }
+
+ handleMouseUp = () => {
+ this.removeEventListeners()
+ }
+
+ applyBoundaries(x: number) {
+ if (x > this.props.width) return this.props.width
+ if (x < 0) return 0
+ return x
+ }
+
+ calcSliderPositionFromCursorPosition = (
+ clientX: number,
+ _sliderRef: HTMLDivElement
+ ) => {
+ if (this.props.isColorSlider) {
+ const { x } = _sliderRef.getBoundingClientRect()
+ const newPosition = clientX - x
+ return newPosition < 0
+ ? 0
+ : newPosition > this.props.width
+ ? this.props.width - 1
+ : newPosition
+ } else {
+ const { x } = _sliderRef.getBoundingClientRect()
+ return clientX - x
+ }
+ }
+
+ calcSliderPositionFromValue = (value: number) => {
+ if (this.props.isColorSlider) {
+ return (value / 360) * this.props.width
+ } else {
+ return this.props.width - (1 - value) * this.props.width
+ }
+ }
+
+ calcValueFromSliderPosition = (position: number) => {
+ if (this.props.isColorSlider) {
+ return (position / this.props.width) * 360
+ } else {
+ const positionWithBoundaries =
+ position < 0
+ ? 0
+ : position > this.props.width
+ ? this.props.width
+ : position
+ return Math.round((positionWithBoundaries * 100) / this.props.width)
+ }
+ }
+
+ handleKeyDown(e: React.KeyboardEvent) {
+ const { key } = e
+ if (key === 'Tab') return
+ e.preventDefault()
+ let deltaX = 0
+ if (key === 'ArrowLeft' || key === 'a') {
+ deltaX = -2
+ }
+ if (key === 'ArrowRight' || key === 'd') {
+ deltaX = 2
+ }
+
+ const newPosition = this.applyBoundaries(
+ this.sliderPositionFromValue + deltaX
+ )
+ this.props.onChange(this.calcValueFromSliderPosition(newPosition))
+ }
+
+ render() {
+ return (
+ this.handleKeyDown(e)}
+ onMouseDown={(e) => this.handleMouseDown(e)}
+ tabIndex={this.props.disabled ? undefined : 0}
+ aria-label={this.props.navigationExplanationScreenReaderLabel}
+ role="slider"
+ aria-valuemin={this.props.minValue}
+ aria-valuemax={this.props.maxValue}
+ aria-valuenow={this.roundedValue}
+ >
+
+ {this.props.disabled && (
+
+ )}
+
+
+ )
+ }
+}
+
+export default Slider
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/Slider/props.ts b/packages/ui-color-picker/src/ColorMixer/v2/Slider/props.ts
new file mode 100644
index 0000000000..0a12bec596
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/Slider/props.ts
@@ -0,0 +1,77 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+import type {
+ OtherHTMLAttributes,
+ ColorMixerSliderTheme
+} from '@instructure/shared-types'
+
+type SliderOwnProps = {
+ isColorSlider?: boolean
+ onChange: (position: number) => void
+ width: number
+ value: number
+ minValue: number
+ maxValue: number
+ indicatorRadius: number
+ height: number
+ elementRef?: (element: Element | null) => void
+ navigationExplanationScreenReaderLabel: string
+}
+
+type PropKeys = keyof SliderOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type SliderProps = SliderOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type SliderStyleProps = {
+ sliderPositionFromValue: number
+}
+
+type SliderStyle = ComponentStyle<
+ | 'colorSlider'
+ | 'indicator'
+ | 'slider'
+ | 'sliderBackground'
+ | 'disabledOverlay'
+>
+const allowedProps: AllowedPropKeys = [
+ 'isColorSlider',
+ 'onChange',
+ 'width',
+ 'value',
+ 'minValue',
+ 'maxValue',
+ 'indicatorRadius',
+ 'height',
+ 'elementRef',
+ 'navigationExplanationScreenReaderLabel'
+]
+
+export type { SliderProps, SliderStyle, SliderStyleProps }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/Slider/styles.ts b/packages/ui-color-picker/src/ColorMixer/v2/Slider/styles.ts
new file mode 100644
index 0000000000..c23cac1ccd
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/Slider/styles.ts
@@ -0,0 +1,115 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes'
+import type { SliderStyle, SliderProps, SliderStyleProps } from './props'
+
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['Slider'],
+ props: SliderProps,
+ _sharedTokens: SharedTokens,
+ state: SliderStyleProps
+): SliderStyle => {
+ const sliderBackground = props.isColorSlider
+ ? {
+ background:
+ 'linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%)'
+ }
+ : {
+ background: componentTheme.checkerboardBackgroundImage,
+ backgroundSize: componentTheme.checkerboardBackgroundSize,
+ backgroundPosition: componentTheme.checkerboardBackgroundPosition
+ }
+
+ return {
+ colorSlider: {
+ label: 'colorMixerSlider'
+ },
+ indicator: {
+ label: 'colorMixerSlider__indicator',
+ width: `${props.indicatorRadius / 8}rem`,
+ height: `${props.indicatorRadius / 8}rem`,
+ borderRadius: `${props.indicatorRadius / 8}rem`,
+ background: 'white',
+ position: 'absolute',
+ borderStyle: 'solid',
+ borderWidth: componentTheme.indicatorBorderWidth,
+ borderColor: componentTheme.indicatorBorderColor,
+ top: `-0.1875rem`,
+ left: `${
+ (state?.sliderPositionFromValue - props.indicatorRadius) / 16
+ }rem`,
+ zIndex: componentTheme.indicatorZIndex
+ },
+
+ sliderBackground: {
+ label: 'colorMixerSlider__sliderBackground',
+ borderRadius: `${props.height / 16}rem`,
+ width: `${props.width / 16}rem`,
+ height: `${props.height / 16}rem`,
+ boxSizing: 'border-box',
+ ...sliderBackground
+ },
+ slider: {
+ label: 'colorMixerSlider__slider',
+ width: `${props.width / 16}rem`,
+ height: `${props.height / 16}rem`,
+ background: props.isColorSlider
+ ? 'transparent'
+ : `linear-gradient(to right, rgba(255,0,0,0), ${props.color?.slice(
+ 0,
+ -2
+ )})`,
+ borderRadius: `${props.height / 16}rem`,
+
+ boxSizing: 'border-box',
+
+ borderStyle: 'solid',
+ borderColor: componentTheme.colorIndicatorBorderColor,
+ borderWidth: componentTheme.sliderBorderWidth
+ },
+ disabledOverlay: {
+ label: 'colorMixerSlider__disabledOverlay',
+ background: 'rgba(255,255,255,.5)',
+ zIndex: componentTheme.disabledOverlayZIndex,
+ width: `${props.width / 16 + 1}rem`,
+ height: `${props.height / 16 + 1}rem`,
+ position: 'absolute',
+ top: '-.5rem',
+ left: '-.5rem'
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorMixer/v1/__tests__/ColorMixer.test.tsx b/packages/ui-color-picker/src/ColorMixer/v2/__tests__/ColorMixer.test.tsx
similarity index 100%
rename from packages/ui-color-picker/src/ColorMixer/v1/__tests__/ColorMixer.test.tsx
rename to packages/ui-color-picker/src/ColorMixer/v2/__tests__/ColorMixer.test.tsx
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/index.tsx b/packages/ui-color-picker/src/ColorMixer/v2/index.tsx
new file mode 100644
index 0000000000..8e57ba6da8
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/index.tsx
@@ -0,0 +1,218 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Component } from 'react'
+
+import { withStyle } from '@instructure/emotion'
+import { omitProps } from '@instructure/ui-react-utils'
+import { isValid } from '@instructure/ui-color-utils'
+import conversions from '@instructure/ui-color-utils'
+import { logWarn as warn } from '@instructure/console'
+import type { HSVType } from '@instructure/ui-color-utils'
+import ColorPalette from './ColorPalette'
+import Slider from './Slider'
+import RGBAInput from './RGBAInput'
+
+import type { ColorMixerProps, ColorMixerState } from './props'
+import { allowedProps } from './props'
+import generateStyle from './styles'
+
+/**
+---
+category: components
+---
+**/
+@withStyle(generateStyle)
+class ColorMixer extends Component {
+ static allowedProps = allowedProps
+ static readonly componentId = 'ColorMixer'
+
+ static defaultProps = {
+ value: '#000',
+ withAlpha: false,
+ disabled: false
+ }
+
+ constructor(props: ColorMixerProps) {
+ super(props)
+ this.state = {
+ h: 0,
+ s: 0,
+ v: 0,
+ a: 1
+ }
+ }
+
+ ref: HTMLDivElement | null = null
+
+ private _width = 272
+ private _paletteHeight = 160
+ private _sliderHeight = 8
+ private _sliderIndicatorRadius = 6
+ private _paletteIndicatorRadius = 6
+
+ handleRef = (el: HTMLDivElement | null) => {
+ const { elementRef } = this.props
+
+ this.ref = el
+
+ if (typeof elementRef === 'function') {
+ elementRef(el)
+ }
+ }
+
+ componentDidMount() {
+ this.props.makeStyles?.()
+ warn(
+ isValid(this.props.value!),
+ `[ColorMixer] The passed color value is not valid.`
+ )
+ this.setState({
+ ...conversions.colorToHsva(this.props.value!)
+ })
+ }
+
+ componentDidUpdate(prevProps: ColorMixerProps, prevState: ColorMixerState) {
+ this.props.makeStyles?.()
+ const { h, s, v, a } = this.state
+ if (
+ prevState.h !== h ||
+ prevState.s !== s ||
+ prevState.v !== v ||
+ prevState.a !== a
+ ) {
+ this.props.onChange(conversions.colorToHex8({ h, s, v, a }))
+ }
+ if (
+ prevProps.value !== this.props.value &&
+ conversions.colorToHex8({ h, s, v, a }) !== this.props.value
+ ) {
+ this.setState({
+ ...conversions.colorToHsva(this.props.value!)
+ })
+ }
+ }
+
+ render() {
+ const {
+ disabled,
+ styles,
+ withAlpha,
+ rgbRedInputScreenReaderLabel,
+ rgbGreenInputScreenReaderLabel,
+ rgbBlueInputScreenReaderLabel,
+ rgbAlphaInputScreenReaderLabel,
+ colorSliderNavigationExplanationScreenReaderLabel,
+ alphaSliderNavigationExplanationScreenReaderLabel,
+ colorPaletteNavigationExplanationScreenReaderLabel
+ } = this.props
+
+ const { h, s, v, a } = this.state
+
+ return (
+
+ )
+ }
+}
+
+export { ColorMixer }
+export default ColorMixer
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/props.ts b/packages/ui-color-picker/src/ColorMixer/v2/props.ts
new file mode 100644
index 0000000000..6bd8bcdb2e
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/props.ts
@@ -0,0 +1,116 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+import type { OtherHTMLAttributes } from '@instructure/shared-types'
+
+type ColorMixerOwnProps = {
+ /**
+ * Makes the component uninteractable
+ */
+ disabled?: boolean
+ /**
+ * Provides a reference to the component's underlying html element.
+ */
+ elementRef?: (element: Element | null) => void
+ /**
+ * Gets called each time the color changes
+ */
+ onChange: (hex: string) => void
+ /**
+ * Sets the value of the component. If changes, the color changes inside the component as well
+ */
+ value?: string
+ /**
+ * Toggles alpha. If true, alpha slider will appear
+ */
+ withAlpha?: boolean
+ /**
+ * screenReaderLabel for the RGBA input's red input field
+ */
+ rgbRedInputScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the RGBA input's green input field
+ */
+ rgbGreenInputScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the RGBA input's blue input field
+ */
+ rgbBlueInputScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the RGBA input's alpha input field
+ */
+ rgbAlphaInputScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the color slider. It should explain how to navigate the slider
+ * with the keyboard ('A' for left, 'D' for right)
+ */
+ colorSliderNavigationExplanationScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the alpha slider. It should explain how to navigate the slider
+ * with the keyboard ('A' for left, 'D' for right)
+ */
+ alphaSliderNavigationExplanationScreenReaderLabel: string
+ /**
+ * screenReaderLabel for the color palette. It should explain how to navigate the palette
+ * with the keyboard ('W' for up, 'A' for left, 'S' for down and 'D' for right)
+ */
+ colorPaletteNavigationExplanationScreenReaderLabel: string
+}
+
+type ColorMixerState = {
+ h: number
+ s: number
+ v: number
+ a: number
+}
+
+type PropKeys = keyof ColorMixerOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorMixerProps = ColorMixerOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type ColorMixerStyle = ComponentStyle<
+ 'colorMixer' | 'sliderAndPaletteContainer'
+>
+const allowedProps: AllowedPropKeys = [
+ 'disabled',
+ 'elementRef',
+ 'value',
+ 'onChange',
+ 'withAlpha',
+ 'rgbRedInputScreenReaderLabel',
+ 'rgbGreenInputScreenReaderLabel',
+ 'rgbBlueInputScreenReaderLabel',
+ 'rgbAlphaInputScreenReaderLabel',
+ 'colorSliderNavigationExplanationScreenReaderLabel',
+ 'alphaSliderNavigationExplanationScreenReaderLabel',
+ 'colorPaletteNavigationExplanationScreenReaderLabel'
+]
+
+export type { ColorMixerProps, ColorMixerStyle, ColorMixerState }
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/styles.ts b/packages/ui-color-picker/src/ColorMixer/v2/styles.ts
new file mode 100644
index 0000000000..1b51ae52f4
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/styles.ts
@@ -0,0 +1,51 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { ColorMixerStyle } from './props'
+
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (): ColorMixerStyle => {
+ return {
+ colorMixer: {
+ label: 'colorMixer',
+ width: '17rem'
+ },
+ sliderAndPaletteContainer: {
+ label: 'colorMixer__sliderAndPaletteContainer',
+ display: 'flex',
+ flexDirection: 'column'
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorMixer/v2/utils/shallowCompare.ts b/packages/ui-color-picker/src/ColorMixer/v2/utils/shallowCompare.ts
new file mode 100644
index 0000000000..0b560a1c38
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorMixer/v2/utils/shallowCompare.ts
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+const shallowCompare = (
+ newObj: Record,
+ prevObj: Record
+) => {
+ for (const key in newObj) {
+ if (newObj[key] !== prevObj[key]) return true
+ }
+ return false
+}
+
+export default shallowCompare
diff --git a/packages/ui-color-picker/src/ColorPicker/v1/index.tsx b/packages/ui-color-picker/src/ColorPicker/v1/index.tsx
index b2744d4579..de613cef66 100644
--- a/packages/ui-color-picker/src/ColorPicker/v1/index.tsx
+++ b/packages/ui-color-picker/src/ColorPicker/v1/index.tsx
@@ -32,17 +32,17 @@ import { warn, error } from '@instructure/console'
import { omitProps } from '@instructure/ui-react-utils'
import { isValid, contrast as getContrast } from '@instructure/ui-color-utils'
import conversions from '@instructure/ui-color-utils'
-import { TextInput } from '@instructure/ui-text-input/latest'
-import { Tooltip } from '@instructure/ui-tooltip'
-import { Button, IconButton } from '@instructure/ui-buttons/latest'
-import { Popover } from '@instructure/ui-popover/latest'
+import { TextInput } from '@instructure/ui-text-input/v11_6'
+import { Tooltip } from '@instructure/ui-tooltip/v11_6'
+import { Button, IconButton } from '@instructure/ui-buttons/v11_6'
+import { Popover } from '@instructure/ui-popover/v11_6'
import {
IconCheckDarkLine,
IconWarningLine,
IconTroubleLine,
IconInfoLine
} from '@instructure/ui-icons'
-import type { FormMessage } from '@instructure/ui-form-field/latest'
+import type { FormMessage } from '@instructure/ui-form-field/v11_6'
import ColorIndicator from '../../ColorIndicator/v1'
import ColorMixer from '../../ColorMixer/v1'
diff --git a/packages/ui-color-picker/src/ColorPicker/v2/ColorPicker.test.tsx b/packages/ui-color-picker/src/ColorPicker/v2/ColorPicker.test.tsx
new file mode 100644
index 0000000000..d0ef618600
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPicker/v2/ColorPicker.test.tsx
@@ -0,0 +1,618 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
+import { userEvent } from '@testing-library/user-event'
+import { vi } from 'vitest'
+import '@testing-library/jest-dom'
+
+import { runAxeCheck } from '@instructure/ui-axe-check'
+import conversions from '@instructure/ui-color-utils'
+
+import type { ColorPickerProps } from '../v2/props'
+import { ContrastStrength } from '../v2/props'
+import { ColorPicker } from '../v2'
+
+const SimpleExample = (props: Partial) => {
+ return (
+
+ )
+}
+
+describe('', () => {
+ let consoleErrorMock: ReturnType
+ let consoleWarningMock: ReturnType
+
+ beforeEach(() => {
+ // Mocking console to prevent test output pollution and expect for messages
+ consoleErrorMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {}) as any
+ consoleWarningMock = vi
+ .spyOn(console, 'warn')
+ .mockImplementation(() => {}) as any
+ })
+
+ afterEach(() => {
+ consoleErrorMock.mockRestore()
+ consoleWarningMock.mockRestore()
+ })
+
+ describe('simple input mode', () => {
+ it('should render correctly', async () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('should work controlled', async () => {
+ const color = '#FFF'
+ const onChange = vi.fn()
+
+ const { rerender } = render(
+
+ )
+
+ const input = screen.getByRole('textbox')
+ expect(input).toHaveValue('FFF')
+
+ // set new value
+ rerender()
+
+ const inputUpdated = screen.getByRole('textbox')
+ expect(inputUpdated).toHaveValue('FFF555')
+ })
+
+ it('should accept 3 digit hex code', async () => {
+ const color = '0CB'
+ render()
+
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, color)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue(color)
+ })
+ })
+
+ it('should accept 6 digit hex code', async () => {
+ const color = '0CBF2D'
+ render()
+
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, color)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue(color)
+ })
+ })
+
+ it('should not accept not valid hex code', async () => {
+ const color = 'WWWZZZ'
+ render()
+
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, color)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).not.toHaveValue(color)
+ })
+ })
+
+ it('should not allow more than 6 characters', async () => {
+ const color = '0CBF2D1234567'
+ render()
+
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, color)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue('0CBF2D')
+ })
+ })
+
+ it('should not allow input when disabled', async () => {
+ render()
+
+ const input = screen.getByRole('textbox')
+ expect(input).toHaveAttribute('disabled')
+ })
+
+ for (const contrastStrength of [
+ 'min',
+ 'mid',
+ 'max'
+ ] as ContrastStrength[]) {
+ it(`should check contrast correctly when color has enough contrast [contrastStrength=${contrastStrength}]`, async () => {
+ //oxford in canvas color palette, should be valid with all contrast strenght checkers
+ const colorToCheck = '394B58'
+ const { container } = render(
+
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue(colorToCheck)
+
+ const successIconWrapper = container.querySelector(
+ 'div[class$="-colorPicker__successIcon"]'
+ )
+ const successIcon = container.querySelector(
+ 'svg[name="Check"]'
+ )
+
+ expect(successIconWrapper).toBeInTheDocument()
+ expect(successIcon).toBeInTheDocument()
+ })
+ })
+
+ it(`should check contrast correctly when color does not have enough contrast [contrastStrength=${contrastStrength}, isStrict=false]`, async () => {
+ //porcelain in canvas color palette, it should be failing even the min check
+ const colorToCheck = 'F5F5F5'
+ const { container } = render(
+
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue(colorToCheck)
+
+ const warningIconWrapper = container.querySelector(
+ 'div[class$="-colorPicker__errorIcons"]'
+ )
+ const warningIcon = container.querySelector('svg[name="CircleAlert"]')
+
+ expect(warningIconWrapper).toBeInTheDocument()
+ expect(warningIcon).toBeInTheDocument()
+ })
+ })
+
+ it(`should check contrast correctly when color does not have enough contrast [contrastStrength=${contrastStrength}, isStrict=true]`, async () => {
+ //porcelain in canvas color palette, it should be failing even the min check
+ const colorToCheck = 'F5F5F5'
+ const { container } = render(
+
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(input).toHaveValue(colorToCheck)
+
+ const errorIconWrapper = container.querySelector(
+ 'div[class$="-colorPicker__errorIcons"]'
+ )
+ const errorIcon = container.querySelector('svg[name="CircleX"]')
+
+ expect(errorIconWrapper).toBeInTheDocument()
+ expect(errorIcon).toBeInTheDocument()
+ })
+ })
+
+ it(`should display success message when contrast is met [contrastStrength=${contrastStrength}]`, async () => {
+ const colorToCheck = '394B58'
+ render(
+ [
+ { type: 'success', text: 'I am a contrast success message' }
+ ]
+ }}
+ />
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ const successMessage = screen.getByText(
+ 'I am a contrast success message'
+ )
+
+ expect(input).toHaveValue(colorToCheck)
+ expect(successMessage).toBeInTheDocument()
+ })
+ })
+
+ it(`should display error message when contrast is not met [contrastStrength=${contrastStrength}, isStrict=false]`, async () => {
+ const colorToCheck = 'F5F5F5'
+ render(
+ [
+ { type: 'error', text: 'I am a contrast warning message' }
+ ]
+ }}
+ />
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ const warningMessage = screen.getByText(
+ 'I am a contrast warning message'
+ )
+
+ expect(input).toHaveValue(colorToCheck)
+ expect(warningMessage).toBeInTheDocument()
+ })
+ })
+
+ it(`should display error message when contrast is not met [contrastStrength=${contrastStrength}, isStrict=true]`, async () => {
+ const colorToCheck = 'F5F5F5'
+ render(
+ [
+ { type: 'error', text: 'I am a contrast error message' }
+ ]
+ }}
+ />
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, colorToCheck)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ const errorMessage = screen.getByText('I am a contrast error message')
+
+ expect(input).toHaveValue(colorToCheck)
+ expect(errorMessage).toBeInTheDocument()
+ })
+ })
+ }
+
+ it('should call onChange', async () => {
+ const onChange = vi.fn()
+ render()
+
+ const input = screen.getByRole('textbox')
+
+ fireEvent.change(input, { target: { value: 'FFF' } })
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(onChange).toHaveBeenLastCalledWith('#FFF')
+ })
+ })
+
+ it('should display message when ColorPicker is a required field', async () => {
+ render(
+ [
+ { type: 'error', text: 'I am an invalid color message' }
+ ]}
+ renderIsRequiredMessage={() => [
+ { type: 'error', text: 'I am a required message' }
+ ]}
+ />
+ )
+ const input = screen.getByRole('textbox')
+
+ fireEvent.focus(input)
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ const requiredMessage = screen.getByText('I am a required message')
+
+ expect(requiredMessage).toBeInTheDocument()
+ })
+ })
+
+ it('should display message when color is invalid', async () => {
+ render(
+ [
+ { type: 'error', text: 'I am an invalid color message' }
+ ]}
+ />
+ )
+ const input = screen.getByRole('textbox')
+
+ await userEvent.type(input, 'F')
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ const errorMessage = screen.getByText('I am an invalid color message')
+
+ expect(errorMessage).toBeInTheDocument()
+ })
+ })
+
+ it('should provide an inputRef prop', async () => {
+ const inputRef = vi.fn()
+ render()
+ const input = screen.getByRole('textbox')
+
+ expect(inputRef).toHaveBeenCalledWith(input)
+ })
+ })
+
+ describe('complex mode', () => {
+ it('should display trigger button', async () => {
+ const { container } = render(
+
+ )
+ const buttonWrapper = container.querySelector(
+ 'div[class$="-colorPicker__colorMixerButtonWrapper"]'
+ )
+ const button = screen.getByRole('button')
+
+ expect(buttonWrapper).toBeInTheDocument()
+ expect(button).toBeInTheDocument()
+ })
+
+ it('should open popover when trigger is clicked', async () => {
+ render(
+
+ )
+ const trigger = screen.getByRole('button')
+
+ expect(trigger).toBeInTheDocument()
+ expect(trigger).toHaveAttribute('aria-expanded', 'false')
+
+ fireEvent.click(trigger)
+
+ await waitFor(() => {
+ const buttons = screen.getAllByRole('button')
+ const popoverContent = document.querySelector(
+ 'div[class$="-colorPicker__popoverContent"]'
+ )
+
+ expect(trigger).toHaveAttribute('aria-expanded', 'true')
+ expect(popoverContent).toBeInTheDocument()
+
+ expect(buttons.length).toBe(2)
+ expect(buttons[0]).toHaveTextContent('close')
+ expect(buttons[1]).toHaveTextContent('add')
+ })
+ })
+
+ it('should display the color mixer', async () => {
+ render(
+
+ )
+ const trigger = screen.getByRole('button')
+
+ fireEvent.click(trigger)
+
+ await waitFor(() => {
+ const redInput = screen.getByLabelText('Red input')
+ const blueInput = screen.getByLabelText('Blue input')
+ const greenInput = screen.getByLabelText('Green input')
+
+ expect(redInput).toBeInTheDocument()
+ expect(blueInput).toBeInTheDocument()
+ expect(greenInput).toBeInTheDocument()
+ })
+ })
+
+ it('should display the correct color in the colormixer when the input is prefilled', async () => {
+ const color = '0374B5'
+ render(
+
+ )
+ const input = screen.getByRole('textbox')
+ const trigger = screen.getByRole('button')
+
+ await userEvent.type(input, color)
+ fireEvent.blur(input)
+ fireEvent.click(trigger)
+
+ await waitFor(() => {
+ const redInput = screen.getByLabelText('Red input') as HTMLInputElement
+ const blueInput = screen.getByLabelText(
+ 'Blue input'
+ ) as HTMLInputElement
+ const greenInput = screen.getByLabelText(
+ 'Green input'
+ ) as HTMLInputElement
+ const convertedColor = conversions.colorToRGB(`#${color}`)
+
+ const actualColor = {
+ r: parseInt(redInput.value),
+ g: parseInt(greenInput.value),
+ b: parseInt(blueInput.value),
+ a: 1
+ }
+
+ expect(convertedColor).toStrictEqual(actualColor)
+ })
+ })
+
+ it('should trigger onChange when selected color is added from colorMixer', async () => {
+ const onChange = vi.fn()
+ const rgb = { r: 131, g: 6, b: 25, a: 1 }
+ render(
+
+ )
+
+ const trigger = screen.getByRole('button')
+
+ fireEvent.click(trigger)
+
+ await waitFor(() => {
+ const addBtn = screen.getByRole('button', { name: 'add' })
+ const redInput = screen.getByLabelText('Red input') as HTMLInputElement
+ const greenInput = screen.getByLabelText(
+ 'Green input'
+ ) as HTMLInputElement
+ const blueInput = screen.getByLabelText(
+ 'Blue input'
+ ) as HTMLInputElement
+
+ fireEvent.change(redInput, { target: { value: `${rgb.r}` } })
+ fireEvent.change(greenInput, { target: { value: `${rgb.g}` } })
+ fireEvent.change(blueInput, { target: { value: `${rgb.b}` } })
+
+ fireEvent.click(addBtn)
+
+ expect(onChange).toHaveBeenCalledWith(conversions.color2hex(rgb))
+ })
+ })
+ })
+
+ describe('custom popover mode', () => {
+ it('should throw warning if children and settings object are passed too', async () => {
+ render(
+
+ {() => }
+
+ )
+
+ await waitFor(() => {
+ expect(consoleWarningMock.mock.calls[0][0]).toEqual(
+ expect.stringContaining(
+ 'Warning: You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.'
+ )
+ )
+ })
+ })
+
+ it('should display trigger button', async () => {
+ render({() => })
+
+ const trigger = screen.getByRole('button')
+ expect(trigger).toBeInTheDocument()
+ expect(trigger).toHaveAttribute('data-popover-trigger', 'true')
+ })
+ })
+
+ describe('should be accessible', () => {
+ it('a11y', async () => {
+ const { container } = render()
+ const axeCheck = await runAxeCheck(container)
+
+ expect(axeCheck).toBe(true)
+ })
+ })
+})
diff --git a/packages/ui-color-picker/src/ColorPicker/v2/README.md b/packages/ui-color-picker/src/ColorPicker/v2/README.md
new file mode 100644
index 0000000000..fe2e7f64c8
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPicker/v2/README.md
@@ -0,0 +1,410 @@
+---
+describes: ColorPicker
+---
+
+The `ColorPicker` is a versatile component that can be used to select colors and check their contrast ratios. It has 2 modes of operation:
+
+- In the simple, color input mode, it lets the user enter hex codes. It will display the color, validate the hex and check the contrast.
+- In the more complex, color picker mode, the same functionality is available as for the simpler mode, but there is the option to pick a color from a visual color mixer component or from any other method added in a popover.
+
+The component can be either `uncontrolled` or `controlled`. If the `onChange` and `value` props are used, it will behave in a `controlled` manner, otherwise `uncontrolled`.
+
+### ColorPicker with default popover content
+
+```js
+---
+type: example
+---
+
+```
+
+### ColorPicker with custom popover content
+
+```js
+---
+type: example
+---
+ const Example = () => {
+ const [value, setValue] = useState('')
+
+ const renderPopoverContent = (value, onChange, handleAdd, handleClose) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+
+ return (
+
+ {
+ setValue(val)
+ }}
+ label="Color Input"
+ tooltip="This is an example"
+ placeholderText="Enter HEX"
+ popoverButtonScreenReaderLabel="Open color mixer popover"
+ >
+ {renderPopoverContent}
+
+
+ )
+ }
+}
+
+export { ColorPicker }
+export default ColorPicker
diff --git a/packages/ui-color-picker/src/ColorPicker/v2/props.ts b/packages/ui-color-picker/src/ColorPicker/v2/props.ts
new file mode 100644
index 0000000000..acd94ad886
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPicker/v2/props.ts
@@ -0,0 +1,304 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import type { FormMessage } from '@instructure/ui-form-field'
+import type {
+ WithStyleProps,
+ ComponentStyle,
+ Spacing
+} from '@instructure/emotion'
+import type {
+ ColorPickerTheme,
+ OtherHTMLAttributes
+} from '@instructure/shared-types'
+type ContrastStrength = 'min' | 'mid' | 'max'
+
+type ColorPickerOwnProps = {
+ /**
+ * Configures the contrast checker. If not provided, there will be no checking.
+ *
+ *
+ * isStrict: if it's true, it will display an error if false, a warning
+ *
+ * contrastStrength: can be one of ('min','mid','max'), which translates to 3:1, 4.5:1, 7:1 contrast, defalts to 'mid'
+ *
+ * contrastAgainst: is the color which the component checks the contrast against. Accepts hex, defaults to #ffffff
+ *
+ * renderContrastSuccessMessage: if set and the contrast is high enough, it will display the message
+ *
+ * renderContrastErrorMessage: if set and the contrast is not high enough, it will display the message
+ *
+ * FormMessage[]: Array of objects with shape: `{
+ * text: ReactNode,
+ * type: One of: ['newError', 'error', 'hint', 'success', 'screenreader-only']
+ * }`
+ */
+ checkContrast?: {
+ isStrict?: boolean
+ contrastStrength?: ContrastStrength
+ contrastAgainst?: string
+ renderContrastSuccessMessage?: (
+ contrast: number,
+ minContrast: number
+ ) => FormMessage[]
+ renderContrastErrorMessage?: (
+ contrast: number,
+ minContrast: number
+ ) => FormMessage[]
+ }
+
+ /**
+ * If set, the default popover will appear for the picker. Those components whose corresponding keys aren't provided (e.g. `colorMixer`, `colorPreset` or `colorContrast`)
+ * will not be rendered.
+ */
+ colorMixerSettings?: {
+ popoverAddButtonLabel: string
+ popoverCloseButtonLabel: string
+ colorMixer?: {
+ withAlpha?: boolean
+ rgbRedInputScreenReaderLabel: string
+ rgbGreenInputScreenReaderLabel: string
+ rgbBlueInputScreenReaderLabel: string
+ rgbAlphaInputScreenReaderLabel: string
+ colorSliderNavigationExplanationScreenReaderLabel: string
+ alphaSliderNavigationExplanationScreenReaderLabel: string
+ colorPaletteNavigationExplanationScreenReaderLabel: string
+ }
+ colorPreset?: {
+ colors: Array
+ label: string
+ }
+ colorContrast?: {
+ firstColor: string
+ label: string
+ successLabel: string
+ failureLabel: string
+ normalTextLabel: string
+ largeTextLabel: string
+ graphicsTextLabel: string
+ firstColorLabel: string
+ secondColorLabel: string
+ onContrastChange?: (conrastData: {
+ contrast: number
+ isValidNormalText: boolean
+ isValidLargeText: boolean
+ isValidGraphicsText: boolean
+ firstColor: string
+ secondColor: string
+ }) => null
+ }
+ }
+
+ /**
+ * If a child function is provided, the component will render it to the popover.
+ */
+ children?: (
+ value: string,
+ onChange: (hex: string) => void,
+ handleAdd: () => void,
+ handleClose: () => void
+ ) => React.ReactNode
+
+ /**
+ * Sets the input to disabled state
+ */
+ disabled?: boolean
+
+ /**
+ * provides a reference to the underlying html root element
+ */
+ elementRef?: (element: Element | null) => void
+
+ /**
+ * If true, it will display a red error ring or a message after a blur event and remove it after a change event
+ */
+ isRequired?: boolean
+
+ /**
+ * The label of the component
+ */
+ label: string
+
+ /**
+ * If 'value' is set, this must be set. It'll be called on every change
+ */
+ onChange?: (value: string) => void
+
+ /**
+ * Placeholder for the input field
+ */
+ placeholderText: string
+
+ /**
+ * If set, it will set the popover's max height.
+ * Useful when the popover is too big
+ */
+ popoverMaxHeight?: string
+
+ /**
+ * Sets the ScreenReaderLabel for the popover
+ */
+ popoverScreenReaderLabel?: string
+
+ /**
+ * Sets the ScreenReaderLabel for the popover Button
+ */
+ popoverButtonScreenReaderLabel?: string
+
+ /**
+ * If set and the hex is invalid, it will display the message after a blur event and remove it after a change event
+ *
+ * FormMessage[]: Array of objects with shape: `{
+ * text: ReactNode,
+ * type: One of: ['newError', 'error', 'hint', 'success', 'screenreader-only']
+ * }`
+ */
+ renderInvalidColorMessage?: (hexCode: string) => FormMessage[]
+
+ /**
+ * If set, isRequired is true and the input is empty, it will display the message after a blur event and remove it after a change event
+ *
+ * FormMessage[]: Array of objects with shape: `{
+ * text: ReactNode,
+ * type: One of: ['newError', 'error', 'hint', 'success', 'screenreader-only']
+ * }`
+ */
+ renderIsRequiredMessage?: () => FormMessage[]
+
+ /**
+ * If set, it will display the message it returns
+ *
+ * FormMessage[]: Array of objects with shape: `{
+ * text: ReactNode,
+ * type: One of: ['newError', 'error', 'hint', 'success', 'screenreader-only']
+ * }`
+ */
+ renderMessages?: (
+ hexCode: string,
+ isValidHex: boolean,
+ minContrast: number,
+ contrast?: number
+ ) => FormMessage[]
+
+ /**
+ * If set, an info icon with a tooltip will be displayed
+ */
+ tooltip?: React.ReactNode
+
+ /**
+ * The id of the text input. One is generated if not supplied.
+ */
+ id?: string
+
+ /**
+ * If set, the component will behave as controlled
+ */
+ value?: string
+
+ /**
+ * The width of the input.
+ */
+ width?: string
+
+ /**
+ * If true, alpha slider will be rendered. Defaults to false
+ */
+ withAlpha?: boolean
+
+ /**
+ * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](https://instructure.design/#layout-spacing).
+ */
+ margin?: Spacing
+
+ /**
+ * A function that provides a reference to the input element
+ */
+ inputRef?: (inputElement: HTMLInputElement | null) => void
+}
+
+type ColorPickerState = {
+ hexCode: string
+ showHelperErrorMessages: boolean
+ openColorPicker: boolean
+ mixedColor: string
+ labelHeight: number
+ calculatedPopoverMaxHeight: string | undefined
+ isHeightCalculated: boolean
+}
+
+type PropKeys = keyof ColorPickerOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorPickerProps = ColorPickerOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type ColorPickerStyle = ComponentStyle<
+ | 'colorPicker'
+ | 'simpleColorContainer'
+ | 'hashMarkContainer'
+ | 'errorIcons'
+ | 'successIcon'
+ | 'label'
+ | 'popoverContent'
+ | 'popoverContentBlock'
+ | 'popoverFooter'
+ | 'colorMixerButtonContainer'
+ | 'popoverContentContainer'
+ | 'colorMixerButtonWrapper'
+>
+const allowedProps: AllowedPropKeys = [
+ 'id',
+ 'checkContrast',
+ 'colorMixerSettings',
+ 'children',
+ 'disabled',
+ 'elementRef',
+ 'isRequired',
+ 'label',
+ 'onChange',
+ 'placeholderText',
+ 'popoverScreenReaderLabel',
+ 'popoverButtonScreenReaderLabel',
+ 'popoverMaxHeight',
+ 'renderInvalidColorMessage',
+ 'renderIsRequiredMessage',
+ 'renderMessages',
+ 'tooltip',
+ 'value',
+ 'width',
+ 'withAlpha',
+ 'margin',
+ 'inputRef'
+]
+
+export type {
+ ColorPickerProps,
+ ColorPickerStyle,
+ ColorPickerState,
+ ContrastStrength
+}
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorPicker/v2/styles.ts b/packages/ui-color-picker/src/ColorPicker/v2/styles.ts
new file mode 100644
index 0000000000..6989239227
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPicker/v2/styles.ts
@@ -0,0 +1,141 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { calcSpacingFromShorthand } from '@instructure/emotion'
+import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes'
+
+import type {
+ ColorPickerProps,
+ ColorPickerState,
+ ColorPickerStyle
+} from './props'
+
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['ColorPicker'],
+ props: ColorPickerProps,
+ sharedTokens: SharedTokens,
+ state: ColorPickerState & { isSimple: boolean }
+): ColorPickerStyle => {
+ const { hashMarkColor } = componentTheme
+ const { popoverMaxHeight, margin } = props
+ const { isSimple, calculatedPopoverMaxHeight } = state
+
+ const cssMargin = calcSpacingFromShorthand(margin, {
+ ...sharedTokens.spacing,
+ ...sharedTokens.legacy.spacing
+ })
+ return {
+ colorPicker: {
+ label: 'colorPicker',
+ display: 'flex',
+ margin: cssMargin
+ },
+ simpleColorContainer: {
+ label: 'colorPicker__simpleColorContainer',
+ display: 'flex',
+ paddingLeft: componentTheme.simpleColorContainerLeftPadding,
+ alignItems: 'center'
+ },
+ hashMarkContainer: {
+ label: 'colorPicker__hashMarkContainer',
+ color: hashMarkColor,
+ display: 'inline-block',
+ fontSize: '1rem',
+ lineHeight: componentTheme.hashMarkContainerLineHeight,
+ ...(isSimple
+ ? {
+ paddingInlineStart: componentTheme.hashMarkContainerLeftPadding,
+ paddingInlineEnd: componentTheme.hashMarkContainerRightPadding
+ }
+ : {})
+ },
+ errorIcons: {
+ label: 'colorPicker__errorIcons',
+ display: 'flex',
+ paddingInlineEnd: componentTheme.errorIconsRightPadding
+ },
+ successIcon: {
+ label: 'colorPicker__successIcon',
+ display: 'flex',
+ paddingInlineEnd: componentTheme.successIconRightPadding
+ },
+ label: {
+ label: 'colorPicker__label',
+ marginInlineEnd: componentTheme.labelRightMargin
+ },
+ popoverContent: {
+ label: 'colorPicker__popoverContent',
+ padding: componentTheme.popoverContentPadding
+ },
+ popoverContentBlock: {
+ label: 'colorPicker__popoverContentBlock',
+ borderTop: 'solid',
+ borderWidth: componentTheme.popoverContentBlockBorderWidth,
+ borderColor: componentTheme.popoverSeparatorColor,
+ marginTop: componentTheme.popoverContentBlockTopMargin,
+ marginBottom: componentTheme.popoverContentBlockBottomMargin,
+ paddingTop: componentTheme.popoverContentBlockTopPadding
+ },
+ popoverFooter: {
+ label: 'colorPicker__popoverFooter',
+ backgroundColor: componentTheme.popoverFooterColor,
+ display: 'flex',
+ justifyContent: 'flex-end',
+ padding: componentTheme.popoverFooterPadding,
+ borderTop: `solid ${componentTheme.popoverFooterTopBorderWidth}`,
+ borderColor: componentTheme.popoverSeparatorColor
+ },
+ colorMixerButtonContainer: {
+ label: 'colorPicker__colorMixerButtonContainer',
+ marginInlineStart: componentTheme.colorMixerButtonContainerLeftMargin
+ },
+ popoverContentContainer: {
+ label: 'colorPicker__popoverContentContainer',
+ maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ scrollbarGutter: 'stable',
+ display: 'flex',
+ flexDirection: 'column',
+ opacity: state.isHeightCalculated ? 1 : 0,
+ transition: 'opacity 150ms ease-in'
+ },
+ colorMixerButtonWrapper: {
+ label: 'colorPicker__colorMixerButtonWrapper',
+ position: 'static'
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/ColorPreset/v1/index.tsx b/packages/ui-color-picker/src/ColorPreset/v1/index.tsx
index 5b30752fd2..7fb56407c3 100644
--- a/packages/ui-color-picker/src/ColorPreset/v1/index.tsx
+++ b/packages/ui-color-picker/src/ColorPreset/v1/index.tsx
@@ -28,13 +28,13 @@ import { withStyleLegacy as withStyle } from '@instructure/emotion'
import { omitProps } from '@instructure/ui-react-utils'
import conversions from '@instructure/ui-color-utils'
-import { IconButton, Button } from '@instructure/ui-buttons/latest'
-import { View } from '@instructure/ui-view/latest'
-import { Tooltip } from '@instructure/ui-tooltip/latest'
-import { Popover } from '@instructure/ui-popover/latest'
-import { Text } from '@instructure/ui-text/latest'
-import { Drilldown } from '@instructure/ui-drilldown/latest'
-import type { DrilldownProps } from '@instructure/ui-drilldown/latest'
+import { IconButton, Button } from '@instructure/ui-buttons/v11_6'
+import { View } from '@instructure/ui-view/v11_6'
+import { Tooltip } from '@instructure/ui-tooltip/v11_6'
+import { Popover } from '@instructure/ui-popover/v11_6'
+import { Text } from '@instructure/ui-text/v11_6'
+import { Drilldown } from '@instructure/ui-drilldown/v11_6'
+import type { DrilldownProps } from '@instructure/ui-drilldown/v11_6'
import { IconAddLine, IconCheckDarkSolid } from '@instructure/ui-icons'
import { ColorIndicator } from '../../ColorIndicator/v1'
diff --git a/packages/ui-color-picker/src/ColorPreset/v2/README.md b/packages/ui-color-picker/src/ColorPreset/v2/README.md
new file mode 100644
index 0000000000..129c250508
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPreset/v2/README.md
@@ -0,0 +1,112 @@
+---
+describes: ColorPreset
+---
+
+A component for picking a color from a list of colors. Supports adding new colors either programmatically through the `colors` prop, or manually with the built in color picker.
+
+### Color Preset
+
+```js
+---
+type: example
+---
+ const Example = () => {
+ const [selected, setSelected] = useState('')
+ const [colors, setColors] = useState([
+ '#ffffff',
+ '#0CBF94',
+ '#0C89BF00',
+ '#BF0C6D',
+ '#BF8D0C',
+ '#ff0000',
+ '#576A66',
+ '#35423A',
+ '#35423F'
+ ])
+
+ return (
+
+ )
+ }
+}
+
+export { ColorPreset }
+export default ColorPreset
diff --git a/packages/ui-color-picker/src/ColorPreset/v2/props.ts b/packages/ui-color-picker/src/ColorPreset/v2/props.ts
new file mode 100644
index 0000000000..31e132d0ab
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPreset/v2/props.ts
@@ -0,0 +1,160 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
+import type {
+ OtherHTMLAttributes,
+ ColorPresetTheme
+} from '@instructure/shared-types'
+import type { RGBAType } from '@instructure/ui-color-utils'
+
+type ContrastStrength = 'min' | 'mid' | 'max'
+
+type ColorPresetOwnProps = {
+ /**
+ * Array of HEX strings which are the preset colors. Supports 8 character HEX (with alpha)
+ */
+ colors: Array
+ /**
+ * Makes the component uninteractable
+ */
+ disabled?: boolean
+ /**
+ * Provides a reference to the component's underlying html element.
+ */
+ elementRef?: (element: Element | null) => void
+ /**
+ * Label text of the component
+ */
+ label?: string
+
+ /**
+ * If set, a `plus` button will appear for the preset. Those components whose corresponding keys aren't provided (`colorMixer` or `colorContrast`)
+ * will not be rendered.
+ * The `onPresetChange` function gets called when a color gets added or removed from the preset list.
+ * It will be called with the new list of colors
+ */
+ colorMixerSettings?: {
+ /**
+ * screenReaderLabel for the add new preset button
+ */
+ addNewPresetButtonScreenReaderLabel: string
+ selectColorLabel: string
+ removeColorLabel: string
+ onPresetChange: (colors: ColorPresetOwnProps['colors']) => void
+ popoverAddButtonLabel: string
+ popoverCloseButtonLabel: string
+ maxHeight?: string
+ colorMixer: {
+ withAlpha?: boolean
+ rgbRedInputScreenReaderLabel: string
+ rgbGreenInputScreenReaderLabel: string
+ rgbBlueInputScreenReaderLabel: string
+ rgbAlphaInputScreenReaderLabel: string
+ colorSliderNavigationExplanationScreenReaderLabel: string
+ alphaSliderNavigationExplanationScreenReaderLabel: string
+ colorPaletteNavigationExplanationScreenReaderLabel: string
+ }
+ colorContrast?: {
+ firstColor: string
+ label: string
+ successLabel: string
+ failureLabel: string
+ normalTextLabel: string
+ largeTextLabel: string
+ graphicsTextLabel: string
+ firstColorLabel: string
+ secondColorLabel: string
+ }
+ }
+ /**
+ * The function gets called when a color gets selected
+ */
+ onSelect: (selected: string) => void
+ /**
+ * Sets the ScreenReaderLabel for the popover
+ */
+ popoverScreenReaderLabel?: string
+ /**
+ * The currently selected HEX string
+ */
+ selected?: string
+ /**
+ * A function for formatting the text provided to screen readers about the color.
+ *
+ * @param {string} hexCode - The hexadecimal color code (e.g., "#FFFFFF") of the current color option. Provided by the component - treat as read-only.
+ *
+ * @param {boolean} isSelected - Indicates whether this color is currently selected. Provided by the component - treat as read-only.
+ *
+ * Sets the aria-label attribute of the color.
+ *
+ * If not set, aria-label defaults to the hex code of the color.
+ */
+ colorScreenReaderLabel?: (hexCode: string, isSelected: boolean) => string
+}
+
+type ColorPresetState = {
+ openEditor: boolean | string
+ openAddNew: boolean
+ newColor: RGBAType
+}
+
+type PropKeys = keyof ColorPresetOwnProps
+
+type AllowedPropKeys = Readonly>
+
+type ColorPresetProps = ColorPresetOwnProps &
+ WithStyleProps &
+ OtherHTMLAttributes
+
+type ColorPresetStyle = ComponentStyle<
+ | 'colorPreset'
+ | 'addNewPresetButton'
+ | 'selectedIndicator'
+ | 'popoverContent'
+ | 'popoverDivider'
+ | 'popoverFooter'
+ | 'label'
+ | 'popoverContrastBlock'
+>
+
+const allowedProps: AllowedPropKeys = [
+ 'colors',
+ 'disabled',
+ 'elementRef',
+ 'label',
+ 'colorMixerSettings',
+ 'onSelect',
+ 'popoverScreenReaderLabel',
+ 'selected',
+ 'colorScreenReaderLabel'
+]
+
+export type {
+ ColorPresetProps,
+ ColorPresetStyle,
+ ColorPresetState,
+ ContrastStrength
+}
+export { allowedProps }
diff --git a/packages/ui-color-picker/src/ColorPreset/v2/styles.ts b/packages/ui-color-picker/src/ColorPreset/v2/styles.ts
new file mode 100644
index 0000000000..11cc12c0bb
--- /dev/null
+++ b/packages/ui-color-picker/src/ColorPreset/v2/styles.ts
@@ -0,0 +1,112 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import type { NewComponentTypes } from '@instructure/ui-themes'
+import type { ColorPresetProps } from './props'
+/**
+ * ---
+ * private: true
+ * ---
+ * Generates the style object from the theme and provided additional information
+ * @param {Object} componentTheme The theme variable object.
+ * @param {Object} props the props of the component, the style is applied to
+ * @param {Object} state the state of the component, the style is applied to
+ * @return {Object} The final style object, which will be used in the component
+ */
+const generateStyle = (
+ componentTheme: NewComponentTypes['ColorPreset'],
+ props: ColorPresetProps
+) => {
+ const { colorMixerSettings, disabled } = props
+ return {
+ colorPreset: {
+ label: 'colorPreset',
+ display: 'flex',
+ flexWrap: 'wrap',
+ width: '17rem',
+ ...(disabled && {
+ opacity: 0.5
+ })
+ },
+ addNewPresetButton: {
+ label: 'colorPreset__addNewPresetButton',
+ width: '2.375rem',
+ height: '2.375rem',
+ margin: componentTheme.xxSmallSpacing
+ },
+ selectedIndicator: {
+ label: 'colorPreset__selectedIndicator',
+ width: '1.25rem',
+ height: '1.25rem',
+ borderStyle: 'solid',
+ borderColor: componentTheme.selectedIndicatorBorderColor,
+ borderWidth: componentTheme.smallBorder,
+ borderRadius: '1.25rem',
+ boxSizing: 'border-box',
+ position: 'relative',
+ insetInlineStart: '1.5rem',
+ bottom: '2.75rem',
+ backgroundColor: componentTheme.selectedIndicatorBackgroundColor,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ popoverContent: {
+ label: 'colorPreset__popoverContent',
+ padding: componentTheme.smallSpacing,
+ maxHeight: colorMixerSettings?.maxHeight || '100vh',
+ overflow: 'auto'
+ },
+ popoverDivider: {
+ label: 'colorPreset__popoverDivider',
+ borderTop: 'solid',
+ borderWidth: componentTheme.smallBorder,
+ borderColor: componentTheme.popoverDividerColor,
+ margin: `${componentTheme.smallSpacing} 0 ${componentTheme.smallSpacing} 0`
+ },
+ popoverContrastBlock: {
+ label: 'colorPreset__popoverContrastBlock',
+ borderTop: 'solid',
+ borderWidth: componentTheme.smallBorder,
+ borderColor: componentTheme.popoverDividerColor,
+ marginTop: componentTheme.popoverContentBlockTopMargin,
+ marginBottom: componentTheme.popoverContentBlockBottomMargin,
+ paddingTop: componentTheme.popoverContentBlockTopPadding
+ },
+ popoverFooter: {
+ label: 'colorPreset__popoverFooter',
+ backgroundColor: componentTheme.popoverFooterColor,
+ display: 'flex',
+ flexDirection: 'row-reverse',
+ padding: componentTheme.smallSpacing
+ },
+ label: {
+ label: 'colorPreset__label',
+ width: '100%',
+ margin: componentTheme.xxSmallSpacing
+ }
+ }
+}
+
+export default generateStyle
diff --git a/packages/ui-color-picker/src/exports/b.ts b/packages/ui-color-picker/src/exports/b.ts
new file mode 100644
index 0000000000..e4a7411699
--- /dev/null
+++ b/packages/ui-color-picker/src/exports/b.ts
@@ -0,0 +1,34 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+export { ColorPicker } from '../ColorPicker/v2'
+export { ColorMixer } from '../ColorMixer/v2'
+export { ColorPreset } from '../ColorPreset/v2'
+export { ColorContrast } from '../ColorContrast/v2'
+export { ColorIndicator } from '../ColorIndicator/v2'
+
+export type { ColorPickerProps } from '../ColorPicker/v2/props'
+export type { ColorMixerProps } from '../ColorMixer/v2/props'
+export type { ColorPresetProps } from '../ColorPreset/v2/props'
+export type { ColorContrastProps } from '../ColorContrast/v2/props'
+export type { ColorIndicatorProps } from '../ColorIndicator/v2/props'