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 ( - - - - + 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 */} + + + + {/* 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