diff --git a/package.json b/package.json
index 85798125ac..d3804f4aed 100644
--- a/package.json
+++ b/package.json
@@ -131,5 +131,5 @@
"browserslist": [
"extends @instructure/browserslist-config-instui"
],
- "packageManager": "pnpm@10.23.0"
+ "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
}
diff --git a/packages/__docs__/resolve.mjs b/packages/__docs__/resolve.mjs
index 84b72d9525..7613588f23 100644
--- a/packages/__docs__/resolve.mjs
+++ b/packages/__docs__/resolve.mjs
@@ -92,6 +92,7 @@ const alias = {
),
'@instructure/ui-grid$': path.resolve(import.meta.dirname, '../ui-grid/src/'),
'@instructure/ui-i18n$': path.resolve(import.meta.dirname, '../ui-i18n/src/'),
+ '@instructure/ui-icons-lucide': path.resolve(import.meta.dirname, '../ui-icons-lucide/src/'),
'@instructure/ui-img$': path.resolve(import.meta.dirname, '../ui-img/src/'),
'@instructure/ui-instructure$': path.resolve(import.meta.dirname, '../ui-instructure/src/'),
'@instructure/ui-link$': path.resolve(import.meta.dirname, '../ui-link/src/'),
diff --git a/packages/emotion/src/styleUtils/calcFocusOutlineStyles.ts b/packages/emotion/src/styleUtils/calcFocusOutlineStyles.ts
index f5e258dfc8..234b3bd0bc 100644
--- a/packages/emotion/src/styleUtils/calcFocusOutlineStyles.ts
+++ b/packages/emotion/src/styleUtils/calcFocusOutlineStyles.ts
@@ -78,6 +78,7 @@ const calcFocusOutlineStyles = (
transition: 'outline-color 0.2s, outline-offset 0.25s'
}),
outlineOffset: '-0.8rem',
+ outlineStyle: 'solid',
outlineColor: alpha(outlineStyle.outlineColor, 0),
'&:focus': {
...outlineStyle,
diff --git a/packages/emotion/src/useStyle.ts b/packages/emotion/src/useStyle.ts
index abb572df46..6f44656dbd 100644
--- a/packages/emotion/src/useStyle.ts
+++ b/packages/emotion/src/useStyle.ts
@@ -60,7 +60,8 @@ const isNewThemeObject = (obj: BaseThemeOrOverride): obj is Theme => {
const useStyle =
(useStyleParams: {
generateStyle: P
params?: SecondParameter
- componentId: keyof NewComponentTypes
+ // needs to be a string too because it might be a child component
+ componentId: keyof NewComponentTypes | string
themeOverride: ThemeOverrideValue | undefined
displayName?: string
//in case of a child component needed to use it's parent's tokens, provide parent's name
diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx
index 264d7013ec..a877aede31 100644
--- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx
+++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx
@@ -146,6 +146,7 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon {
return (
)
> Note: `NumberInput` accepts a string or number as its `value`. However, the value returned by the `onChange` callback is always a string and should be converted to a number before attempting to augment it.
-NumberInput comes in 2 sizes. The default size is "medium".
+You can see here most of the visual states of the component.
```js
---
-type: example
+ type: example
---
-
-
-
-
+
+
+
+
+
+
+
+
```
### Guidelines
diff --git a/packages/ui-number-input/src/NumberInput/__tests__/NumberInput.test.tsx b/packages/ui-number-input/src/NumberInput/__tests__/NumberInput.test.tsx
index 23bfa7f867..be080d8bcd 100644
--- a/packages/ui-number-input/src/NumberInput/__tests__/NumberInput.test.tsx
+++ b/packages/ui-number-input/src/NumberInput/__tests__/NumberInput.test.tsx
@@ -29,7 +29,10 @@ import '@testing-library/jest-dom'
import { NumberInput } from '../index'
-import { IconZoomInLine, IconZoomOutLine } from '@instructure/ui-icons'
+import {
+ ChevronUpInstUIIcon,
+ ChevronDownInstUIIcon
+} from '@instructure/ui-icons-lucide'
describe(' ', () => {
let consoleWarningMock: ReturnType
@@ -139,7 +142,7 @@ describe(' ', () => {
it('shows arrow spinbuttons by default', async () => {
const { container } = render( )
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
expect(buttons).toHaveLength(2)
@@ -150,7 +153,7 @@ describe(' ', () => {
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
expect(buttons).toHaveLength(0)
@@ -162,7 +165,7 @@ describe(' ', () => {
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[0])
@@ -182,7 +185,7 @@ describe(' ', () => {
/>
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[0])
@@ -198,7 +201,7 @@ describe(' ', () => {
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[0])
@@ -215,7 +218,7 @@ describe(' ', () => {
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[1])
@@ -235,7 +238,7 @@ describe(' ', () => {
/>
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[1])
@@ -251,7 +254,7 @@ describe(' ', () => {
)
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[1])
@@ -277,19 +280,19 @@ describe(' ', () => {
onIncrement={onIncrement}
onDecrement={onDecrement}
renderIcons={{
- increase: ,
- decrease:
+ increase: ,
+ decrease:
}}
/>
)
- const zoomInIcon = container.querySelector('svg[name="IconZoomIn"]')
- const zoomOutIcon = container.querySelector('svg[name="IconZoomOut"]')
+ const zoomInIcon = container.querySelector('svg[name="ChevronUp"]')
+ const zoomOutIcon = container.querySelector('svg[name="ChevronDown"]')
expect(zoomInIcon).toBeInTheDocument()
expect(zoomOutIcon).toBeInTheDocument()
const buttons = container.querySelectorAll(
- 'button[class$="-numberInput_arrow'
+ 'button[class$="-numberInput_arrow"]'
)
userEvent.click(buttons[0])
diff --git a/packages/ui-number-input/src/NumberInput/index.tsx b/packages/ui-number-input/src/NumberInput/index.tsx
index cb6b3dcb3e..50fb6c7510 100644
--- a/packages/ui-number-input/src/NumberInput/index.tsx
+++ b/packages/ui-number-input/src/NumberInput/index.tsx
@@ -22,34 +22,36 @@
* SOFTWARE.
*/
-import { Fragment, Component } from 'react'
+import {
+ Fragment,
+ useState,
+ useRef,
+ useCallback,
+ useImperativeHandle,
+ forwardRef,
+ useEffect
+} from 'react'
import keycode from 'keycode'
import { FormField } from '@instructure/ui-form-field'
import {
- IconArrowOpenDownLine,
- IconArrowOpenUpLine
-} from '@instructure/ui-icons'
+ ChevronUpInstUIIcon,
+ ChevronDownInstUIIcon
+} from '@instructure/ui-icons-lucide'
import {
- omitProps,
pickProps,
callRenderProp,
getInteraction,
- withDeterministicId
+ useDeterministicId,
+ passthroughProps
} from '@instructure/ui-react-utils'
import { hasVisibleChildren } from '@instructure/ui-a11y-utils'
-import { withStyleRework as withStyle } from '@instructure/emotion'
+import { useStyle } from '@instructure/emotion'
import generateStyle from './styles'
-import generateComponentTheme from './theme'
-import { allowedProps } from './props'
-import type {
- NumberInputProps,
- NumberInputState,
- NumberInputStyleProps
-} from './props'
+import type { NumberInputProps } from './props'
import { Renderable } from '@instructure/shared-types'
/**
@@ -58,207 +60,270 @@ category: components
id: NumberInput
---
**/
-@withDeterministicId()
-@withStyle(generateStyle, generateComponentTheme)
-class NumberInput extends Component {
- static readonly componentId = 'NumberInput'
- static allowedProps = allowedProps
- static defaultProps = {
- // Leave interaction default undefined so that `disabled` and `readOnly` can also be supplied
- interaction: undefined,
- messages: [],
- isRequired: false,
- showArrows: true,
- size: 'medium',
- display: 'block',
- textAlign: 'start',
- inputMode: 'numeric',
- allowStringValue: false
- }
-
- state: NumberInputState = { hasFocus: false }
-
- ref: Element | null = null
-
- private _input: HTMLInputElement | null = null
- private _id?: string
-
- handleRef = (el: Element | null) => {
- this.ref = el
- }
-
- get id() {
- if (this.props.id) {
- return this.props.id
- }
- if (!this._id) {
- this._id = this.props.deterministicId!()
- }
- return this._id
- }
-
- get invalid() {
- return (
- !!this.props.messages &&
- this.props.messages.some(
+const NumberInput = forwardRef(
+ (props, ref) => {
+ const {
+ messages = [],
+ isRequired = false,
+ showArrows = true,
+ size = 'medium',
+ display = 'block',
+ textAlign = 'start',
+ inputMode = 'numeric',
+ allowStringValue = false,
+ renderLabel,
+ placeholder,
+ value,
+ width,
+ renderIcons,
+ margin,
+ inputRef: inputRefProp,
+ onFocus,
+ onBlur,
+ onChange,
+ onKeyDown,
+ onDecrement,
+ onIncrement,
+ id: idProp,
+ themeOverride,
+ ...rest
+ } = props
+ // these are icon tokens
+ type ArrowButtonColors =
+ | 'actionSecondaryBaseColor'
+ | 'actionSecondaryHoverColor'
+ | 'actionSecondaryActiveColor'
+ | 'actionSecondaryDisabledColor'
+ const [upButtonState, setUpButtonState] = useState(
+ 'actionSecondaryBaseColor'
+ )
+ const [downButtonState, setDownButtonState] = useState(
+ 'actionSecondaryBaseColor'
+ )
+ // Refs
+ const containerRef = useRef(null)
+ const inputRef = useRef(null)
+
+ // Deterministic ID generation
+ const [deterministicId, setDeterministicId] = useState()
+ const getId = useDeterministicId('NumberInput')
+ useEffect(() => {
+ setDeterministicId(getId())
+ }, []) // Empty deps array - only run once on mount
+ const id = idProp || deterministicId
+
+ // Computed values
+ const invalid =
+ !!messages &&
+ messages.some(
(message) => message.type === 'error' || message.type === 'newError'
)
- )
- }
-
- get interaction() {
- return getInteraction({ props: this.props })
- }
-
- componentDidMount() {
- this.props.makeStyles?.(this.makeStyleVariables)
- }
-
- componentDidUpdate() {
- this.props.makeStyles?.(this.makeStyleVariables)
- }
-
- get makeStyleVariables(): NumberInputStyleProps {
- return {
- interaction: this.interaction,
- hasFocus: this.state.hasFocus,
- invalid: this.invalid
- }
- }
-
- handleInputRef = (element: HTMLInputElement | null) => {
- this._input = element
-
- if (typeof this.props.inputRef === 'function') {
- this.props.inputRef(element)
- }
- }
-
- handleFocus = (event: React.FocusEvent) => {
- this.setState({ hasFocus: true })
-
- if (typeof this.props.onFocus === 'function') {
- this.props.onFocus(event)
- }
- }
-
- handleBlur = (event: React.FocusEvent) => {
- this.setState({ hasFocus: false })
-
- if (typeof this.props.onBlur === 'function') {
- this.props.onBlur(event)
- }
- }
-
- handleChange = (event: React.ChangeEvent) => {
- if (typeof this.props.onChange === 'function') {
- this.props.onChange(event, event.target.value)
- }
- }
-
- handleKeyDown = (event: React.KeyboardEvent) => {
- const { onKeyDown, onDecrement, onIncrement } = this.props
-
- if (typeof onKeyDown === 'function') {
- onKeyDown(event)
+ const success =
+ !!messages && messages.some((message) => message.type === 'success')
+
+ const interaction = getInteraction({ props })
+ if (
+ interaction === 'disabled' &&
+ upButtonState !== 'actionSecondaryDisabledColor'
+ ) {
+ setUpButtonState('actionSecondaryDisabledColor')
+ setDownButtonState('actionSecondaryDisabledColor')
+ } else if (
+ interaction === 'enabled' &&
+ downButtonState !== 'actionSecondaryBaseColor'
+ ) {
+ setUpButtonState('actionSecondaryBaseColor')
+ setDownButtonState('actionSecondaryBaseColor')
}
+ // Styles - useStyle will pass these to generateStyle(componentTheme, params as props, params as state)
+ // We need to provide all values that generateStyle needs from both props and state
+ const styles = useStyle({
+ generateStyle,
+ themeOverride,
+ params: {
+ size,
+ textAlign,
+ interaction,
+ invalid,
+ success
+ },
+ componentId: 'NumberInput',
+ displayName: 'NumberInput',
+ useTokensFrom: 'TextInput'
+ })
+
+ // Event handlers
+ const handleInputRef = useCallback(
+ (element: HTMLInputElement | null) => {
+ inputRef.current = element
+
+ if (typeof inputRefProp === 'function') {
+ inputRefProp(element)
+ }
+ },
+ [inputRefProp]
+ )
- if (event.keyCode === keycode.codes.down) {
- event.preventDefault()
- if (typeof onDecrement === 'function') {
- onDecrement(event)
- }
- } else if (event.keyCode === keycode.codes.up) {
- event.preventDefault()
- if (typeof onIncrement === 'function') {
- onIncrement(event)
- }
- }
- }
+ const handleRef = useCallback((el: Element | null) => {
+ containerRef.current = el
+ }, [])
+
+ const handleFocus = useCallback(
+ (event: React.FocusEvent) => {
+ if (typeof onFocus === 'function') {
+ onFocus(event)
+ }
+ },
+ [onFocus]
+ )
- handleClickUpArrow = (event: React.MouseEvent) => {
- this.arrowClicked(event, this.props.onIncrement)
- }
+ const handleBlur = useCallback(
+ (event: React.FocusEvent) => {
+ if (typeof onBlur === 'function') {
+ onBlur(event)
+ }
+ },
+ [onBlur]
+ )
- handleClickDownArrow = (event: React.MouseEvent) => {
- this.arrowClicked(event, this.props.onDecrement)
- }
+ const handleChange = useCallback(
+ (event: React.ChangeEvent) => {
+ if (typeof onChange === 'function') {
+ onChange(event, event.target.value)
+ }
+ },
+ [onChange]
+ )
- arrowClicked(
- event: React.MouseEvent,
- callback: NumberInputProps['onIncrement'] | NumberInputProps['onDecrement']
- ) {
- const { interaction } = this
+ const handleKeyDown = useCallback(
+ (event: React.KeyboardEvent) => {
+ if (typeof onKeyDown === 'function') {
+ onKeyDown(event)
+ }
+
+ if (event.keyCode === keycode.codes.down) {
+ event.preventDefault()
+ if (typeof onDecrement === 'function') {
+ onDecrement(event)
+ }
+ } else if (event.keyCode === keycode.codes.up) {
+ event.preventDefault()
+ if (typeof onIncrement === 'function') {
+ onIncrement(event)
+ }
+ }
+ },
+ [onKeyDown, onDecrement, onIncrement]
+ )
- event.preventDefault()
- if (interaction === 'enabled') {
- this._input?.focus()
+ const arrowClicked = useCallback(
+ (
+ event: React.MouseEvent,
+ callback:
+ | NumberInputProps['onIncrement']
+ | NumberInputProps['onDecrement']
+ ) => {
+ event.preventDefault()
+ if (interaction === 'enabled') {
+ inputRef.current?.focus()
+
+ if (typeof callback === 'function') {
+ callback(event)
+ }
+ }
+ },
+ [interaction]
+ )
- if (typeof callback === 'function') {
- callback(event)
- }
- }
- }
+ const handleClickUpArrow = useCallback(
+ (event: React.MouseEvent) => {
+ setUpButtonState('actionSecondaryActiveColor')
+ arrowClicked(event, onIncrement)
+ },
+ [arrowClicked, onIncrement]
+ )
- renderArrows(customIcons?: { increase: Renderable; decrease: Renderable }) {
- return (
-
-
- {customIcons?.increase ? (
- callRenderProp(customIcons.increase)
- ) : (
-
- )}
-
-
- {customIcons?.decrease ? (
- callRenderProp(customIcons.decrease)
- ) : (
-
- )}
-
-
+ const handleClickDownArrow = useCallback(
+ (event: React.MouseEvent) => {
+ setDownButtonState('actionSecondaryActiveColor')
+ arrowClicked(event, onDecrement)
+ },
+ [arrowClicked, onDecrement]
)
- }
- render() {
- const {
- renderLabel,
- display,
- placeholder,
- isRequired,
- showArrows,
- value,
- width,
- styles,
- allowStringValue,
- renderIcons,
- margin
- } = this.props
+ // Expose imperative API via ref
+ useImperativeHandle(
+ ref,
+ () => ({
+ focus: () => {
+ inputRef.current?.focus()
+ },
+ get id() {
+ return id
+ },
+ get invalid() {
+ return invalid
+ },
+ get interaction() {
+ return interaction
+ },
+ get value() {
+ return inputRef.current?.value
+ }
+ }),
+ [id, invalid, interaction]
+ )
- const { interaction } = this
+ // Render methods
+ const renderArrows = (customIcons?: {
+ increase: Renderable
+ decrease: Renderable
+ }) => {
+ return (
+
+ {/* eslint-disable jsx-a11y/mouse-events-have-key-events */}
+ setUpButtonState('actionSecondaryHoverColor')}
+ onMouseOut={() => setUpButtonState('actionSecondaryBaseColor')}
+ tabIndex={-1}
+ type="button"
+ >
+ {customIcons?.increase ? (
+ callRenderProp(customIcons.increase)
+ ) : (
+
+ )}
+
+
+ setDownButtonState('actionSecondaryHoverColor')}
+ onMouseOut={() => setDownButtonState('actionSecondaryBaseColor')}
+ tabIndex={-1}
+ type="button"
+ >
+ {customIcons?.decrease ? (
+ callRenderProp(customIcons.decrease)
+ ) : (
+
+ )}
+
+ {/* eslint-enable jsx-a11y/mouse-events-have-key-events */}
+
+ )
+ }
const rawLabel = callRenderProp(renderLabel)
const label = hasVisibleChildren(rawLabel) ? (
{rawLabel}
{isRequired && (
-
+
{' '}
*
@@ -268,48 +333,61 @@ class NumberInput extends Component {
rawLabel
)
+ const passedProps = passthroughProps(rest)
+
+ // Don't render until we have an ID
+ if (!id) {
+ return null
+ }
+
return (
-
-
+
+
- {showArrows ? this.renderArrows(renderIcons) : null}
+ {showArrows && interaction !== 'readonly'
+ ? renderArrows(renderIcons)
+ : null}
)
}
+)
+
+NumberInput.displayName = 'NumberInput'
+
+export interface NumberInputHandle {
+ focus: () => void
+ readonly id: string | undefined
+ readonly invalid: boolean
+ readonly interaction: ReturnType
+ readonly value: string | undefined
}
export default NumberInput
diff --git a/packages/ui-number-input/src/NumberInput/props.ts b/packages/ui-number-input/src/NumberInput/props.ts
index bf5f4614d4..9f406cc775 100644
--- a/packages/ui-number-input/src/NumberInput/props.ts
+++ b/packages/ui-number-input/src/NumberInput/props.ts
@@ -26,14 +26,13 @@ import React from 'react'
import type { InputHTMLAttributes } from 'react'
import type {
- NumberInputTheme,
OtherHTMLAttributes,
PickPropsWithExceptions
} from '@instructure/shared-types'
import type {
- WithStyleProps,
ComponentStyle,
- Spacing
+ Spacing,
+ ThemeOverrideValue
} from '@instructure/emotion'
import type { FormFieldOwnProps, FormMessage } from '@instructure/ui-form-field'
import type {
@@ -70,8 +69,9 @@ type NumberInputOwnProps = {
messages?: FormMessage[]
/**
- * Html placeholder text to display when the input has no value. This
+ * HTML placeholder text to display when the input has no value. This
* should be hint text, not a label replacement.
+ * Not visible when `disabled` or `readonly`
*/
placeholder?: string
@@ -82,6 +82,7 @@ type NumberInputOwnProps = {
/**
* Whether or not to display the up/down arrow buttons.
+ * They are not visible when `readonly`
*/
showArrows?: boolean
@@ -180,15 +181,6 @@ type NumberInputOwnProps = {
margin?: Spacing
}
-type NumberInputState = {
- hasFocus: boolean
-}
-
-type NumberInputStyleProps = NumberInputState & {
- interaction: InteractionType
- invalid: boolean
-}
-
type PropKeys = keyof NumberInputOwnProps
type AllowedPropKeys = Readonly>
@@ -199,9 +191,9 @@ type NumberInputProps =
FormFieldOwnProps,
'label' | 'inline' | 'id' | 'elementRef'
> &
- NumberInputOwnProps &
- WithStyleProps &
- OtherHTMLAttributes<
+ NumberInputOwnProps & {
+ themeOverride?: ThemeOverrideValue
+ } & OtherHTMLAttributes<
NumberInputOwnProps,
InputHTMLAttributes
> &
@@ -242,10 +234,5 @@ const allowedProps: AllowedPropKeys = [
'margin'
]
-export type {
- NumberInputProps,
- NumberInputState,
- NumberInputStyleProps,
- NumberInputStyle
-}
+export type { NumberInputProps, NumberInputStyle }
export { allowedProps }
diff --git a/packages/ui-number-input/src/NumberInput/styles.ts b/packages/ui-number-input/src/NumberInput/styles.ts
index 40428e9ccd..8cc67c5f05 100644
--- a/packages/ui-number-input/src/NumberInput/styles.ts
+++ b/packages/ui-number-input/src/NumberInput/styles.ts
@@ -22,58 +22,106 @@
* SOFTWARE.
*/
-import type { NumberInputTheme } from '@instructure/shared-types'
-import type {
- NumberInputProps,
- NumberInputStyleProps,
- NumberInputStyle
-} from './props'
+import type { NumberInputProps, NumberInputStyle } from './props'
+import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes'
+import { calcFocusOutlineStyles } from '@instructure/emotion'
+
+type StyleParams = {
+ size: NumberInputProps['size']
+ textAlign: NumberInputProps['textAlign']
+ interaction: NumberInputProps['interaction']
+ invalid: boolean
+ success: boolean
+}
/**
* ---
* 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
+ * @param componentTheme The theme variable object.
+ * @param params Additional parameters to customize the style.
+ * @param sharedTokens Shared token object that stores common values for the theme.
+ * @return The final style object, which will be used in the component
*/
const generateStyle = (
- componentTheme: NumberInputTheme,
- props: NumberInputProps,
- state: NumberInputStyleProps
+ componentTheme: NewComponentTypes['TextInput'],
+ params: StyleParams,
+ sharedTokens: SharedTokens
): NumberInputStyle => {
- const { size, textAlign } = props
- const { interaction, hasFocus, invalid } = state
-
- const disabledStyles =
- interaction === 'disabled'
- ? {
- cursor: 'not-allowed',
- pointerEvents: 'none',
- opacity: 0.5
- }
- : {}
+ const { size, textAlign, interaction, success, invalid } = params
- const focusStyles = hasFocus
- ? {
- opacity: 1,
- transform: 'scale(1)'
+ const containerInteractionStates = {
+ ...(interaction === 'enabled' && {
+ backgroundColor: componentTheme.backgroundColor,
+ borderColor: componentTheme.borderColor,
+ ...(success && {
+ borderColor: componentTheme.successBorderColor
+ }),
+ ...(invalid && {
+ borderColor: componentTheme.errorBorderColor
+ }),
+ '&:hover': {
+ backgroundColor: componentTheme.backgroundHoverColor,
+ borderColor: componentTheme.borderHoverColor,
+ ...(success && {
+ borderColor: componentTheme.successBorderColor
+ }),
+ ...(invalid && {
+ borderColor: componentTheme.errorBorderColor
+ })
}
- : {}
-
- const invalidStyles = invalid
- ? {
- borderColor: componentTheme.errorOutlineColor
+ }),
+ ...(interaction === 'readonly' && {
+ backgroundColor: componentTheme.backgroundReadonlyColor,
+ borderColor: componentTheme.borderReadonlyColor
+ }),
+ ...(interaction === 'disabled' && {
+ cursor: 'not-allowed',
+ pointerEvents: 'none',
+ backgroundColor: componentTheme.backgroundDisabledColor,
+ borderColor: componentTheme.borderDisabledColor
+ })
+ }
+ const arrowInteractionStates = {
+ ...(interaction === 'enabled' && {
+ backgroundColor: componentTheme.arrowsBackgroundColor,
+ borderColor: componentTheme.arrowsBorderColor,
+ '&:hover': {
+ backgroundColor: componentTheme.arrowsBackgroundHoverColor,
+ borderColor: componentTheme.arrowsBorderHoverColor
+ },
+ '&:active': {
+ backgroundColor: componentTheme.arrowsBackgroundActiveColor,
+ borderColor: componentTheme.arrowsBorderActiveColor
}
- : {}
-
- const invalidContainerStyles = invalid
- ? {
- borderColor: componentTheme.errorBorderColor
+ }),
+ ...(interaction === 'disabled' && {
+ cursor: 'not-allowed',
+ pointerEvents: 'none',
+ backgroundColor: componentTheme.arrowsBackgroundDisabledColor,
+ borderColor: componentTheme.arrowsBorderDisabledColor
+ })
+ // arrow buttons are not rendered in the `readOnly` state
+ }
+ const inputInteractionStates = {
+ ...(interaction === 'enabled' && {
+ color: componentTheme.textColor,
+ '&::placeholder': {
+ color: componentTheme.placeholderColor
+ },
+ '&:hover::placeholder': {
+ color: componentTheme.placeholderHoverColor
}
- : {}
+ // placeholder is not rendered in the `readOnly` and `disabled` state
+ }),
+ ...(interaction === 'readonly' && {
+ color: componentTheme.textReadonlyColor
+ }),
+ ...(interaction === 'disabled' && {
+ color: componentTheme.textDisabledColor
+ })
+ }
const inputStyle = {
all: 'initial',
@@ -89,17 +137,24 @@ const generateStyle = (
boxSizing: 'border-box',
fontFamily: 'inherit',
fontSize: 'inherit',
- fontWeight: componentTheme.fontWeight,
- color: componentTheme.color,
- background: componentTheme.background,
- padding: componentTheme.padding,
- textyAlign: textAlign,
- '&::placeholder': { color: componentTheme.placeholderColor }
+ fontWeight: 'inherit',
+ ...(size === 'medium'
+ ? {
+ padding: componentTheme.paddingHorizontalMd
+ }
+ : {
+ padding: componentTheme.paddingHorizontalLg
+ }),
+ ...inputInteractionStates
}
+ const focusOutline = calcFocusOutlineStyles(sharedTokens.focusOutline, {
+ focusWithin: true
+ })
return {
requiredInvalid: {
- color: componentTheme.requiredInvalidColor
+ // color of the small required star
+ //color: componentTheme.requiredInvalidColor TODO handle in FormFieldLayout
},
numberInput: {
label: 'numberInput'
@@ -108,8 +163,7 @@ const generateStyle = (
label: 'numberInput_arrowContainer',
flex: `0 0 ${componentTheme.arrowsContainerWidth}`,
display: 'flex',
- flexDirection: 'column',
- ...disabledStyles
+ flexDirection: 'column'
},
arrow: {
label: 'numberInput_arrow',
@@ -120,59 +174,38 @@ const generateStyle = (
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: componentTheme.arrowsBackgroundColor,
borderTop: 'none',
borderInlineEnd: 'none',
- borderInlineStart: `${componentTheme.borderWidth} ${componentTheme.borderStyle} ${componentTheme.arrowsBorderColor}`,
- borderBottom: `${componentTheme.borderWidth} ${componentTheme.borderStyle} ${componentTheme.arrowsBorderColor}`,
- color: componentTheme.arrowsColor,
+ borderInlineStart: `${componentTheme.borderWidth} solid`,
+ borderBottom: `${componentTheme.borderWidth} solid`,
'&:last-child': { borderBottom: 'none' },
- '&:hover': { backgroundColor: componentTheme.arrowsHoverBackgroundColor },
- '&:active': { boxShadow: componentTheme.arrowsActiveBoxShadow }
+ ...arrowInteractionStates
},
inputWidth: {
label: 'numberInput_inputWidth',
display: 'block',
- position: 'relative',
- '&::before': {
- content: '""',
- pointerEvents: 'none',
- boxSizing: 'border-box',
- display: 'block',
- position: 'absolute',
- top: '-0.25rem',
- bottom: '-0.25rem',
- left: '-0.25rem',
- right: '-0.25rem',
- border: `${componentTheme.focusOutlineWidth} ${componentTheme.focusOutlineStyle} ${componentTheme.focusOutlineColor}`,
- borderRadius: `calc(${componentTheme.borderRadius} * 1.5)`,
- transition: 'all 0.2s',
- opacity: 0,
- transform: 'scale(0.95)',
- ...focusStyles,
- ...invalidStyles
- }
+ position: 'relative'
},
inputContainer: {
label: 'numberInput_inputContainer',
display: 'flex',
margin: '0',
boxSizing: 'border-box',
- transition: 'all 0.2s',
overflow: 'hidden',
fontFamily: componentTheme.fontFamily,
- border: `${componentTheme.borderWidth} ${componentTheme.borderStyle} ${componentTheme.borderColor}`,
+ fontWeight: componentTheme.fontWeight,
+ border: `${componentTheme.borderWidth} solid`,
borderRadius: componentTheme.borderRadius,
- ...disabledStyles,
- ...invalidContainerStyles,
+ ...containerInteractionStates,
+ ...focusOutline,
...(size === 'medium'
? {
- fontSize: componentTheme.mediumFontSize,
- height: componentTheme.mediumHeight
+ fontSize: componentTheme.fontSizeMd,
+ height: componentTheme.heightMd
}
: {
- fontSize: componentTheme.largeFontSize,
- height: componentTheme.largeHeight
+ fontSize: componentTheme.fontSizeLg,
+ height: componentTheme.heightLg
})
},
input: {
diff --git a/packages/ui-number-input/tsconfig.build.json b/packages/ui-number-input/tsconfig.build.json
index c5c246c869..bf36a62346 100644
--- a/packages/ui-number-input/tsconfig.build.json
+++ b/packages/ui-number-input/tsconfig.build.json
@@ -23,7 +23,7 @@
"path": "../ui-form-field/tsconfig.build.json"
},
{
- "path": "../ui-icons/tsconfig.build.json"
+ "path": "../ui-icons-lucide/tsconfig.build.json"
},
{
"path": "../ui-react-utils/tsconfig.build.json"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b56c7c81a8..519e25dc51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2969,9 +2969,9 @@ importers:
'@instructure/ui-form-field':
specifier: workspace:*
version: link:../ui-form-field
- '@instructure/ui-icons':
+ '@instructure/ui-icons-lucide':
specifier: workspace:*
- version: link:../ui-icons
+ version: link:../ui-icons-lucide
'@instructure/ui-react-utils':
specifier: workspace:*
version: link:../ui-react-utils