UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

240 lines (239 loc) • 11 kB
import { jsx as _jsx } from "react/jsx-runtime"; import React from 'react'; import { createStencil, handleCsProp, wrapProperty } from '@workday/canvas-kit-styling'; import { system } from '@workday/canvas-tokens-web'; import { createContainer, createElemPropsHook, createModelHook, createSubcomponent, dispatchInputEvent, useForkRef, } from '@workday/canvas-kit-react/common'; import { mergeStyles } from '@workday/canvas-kit-react/layout'; import { TertiaryButton } from '@workday/canvas-kit-react/button'; import { xSmallIcon } from '@workday/canvas-system-icons-web'; import { TextInput } from './TextInput'; export const useInputGroupModel = createModelHook({})(() => { const inputRef = React.useRef(null); return { state: { inputRef, }, events: {}, }; }); export const inputGroupInnerStencil = createStencil({ vars: { /** * Offset of the inner item. Set by the `InputGroup` and depends on siblings. Do not change this * on your own. */ insetInlineStart: 'initial', /** * Offset of the inner item. Set by the `InputGroup` and depends on siblings. Do not change this * on your own. */ insetInlineEnd: 'initial', width: system.space.x10, height: system.space.x10, /** * Some inner input group elements are decoration only and should not have pointer events */ pointerEvents: '', }, base: { name: "11ajz0", styles: "--insetInlineStart-input-group-inner-42c96b:initial;--insetInlineEnd-input-group-inner-42c96b:initial;--width-input-group-inner-42c96b:var(--cnvs-sys-space-x10);--height-input-group-inner-42c96b:var(--cnvs-sys-space-x10);box-sizing:border-box;display:flex;position:absolute;align-items:center;justify-content:center;width:var(--width-input-group-inner-42c96b);height:var(--height-input-group-inner-42c96b);inset-inline-start:var(--insetInlineStart-input-group-inner-42c96b);inset-inline-end:var(--insetInlineEnd-input-group-inner-42c96b);" }, modifiers: { pointerEvents: { _: { name: "2qtxnl", styles: "pointer-events:var(--pointerEvents-input-group-inner-42c96b);" } } } }, "input-group-inner-42c96b"); const InputGroupInnerStart = createSubcomponent('div')({ modelHook: useInputGroupModel, })(({ pointerEvents, insetInlineStart, insetInlineEnd, width, height, ...elemProps }, Element) => { return (_jsx(Element, { ...mergeStyles(elemProps, inputGroupInnerStencil({ pointerEvents, insetInlineStart: toPx(insetInlineStart), insetInlineEnd: toPx(insetInlineEnd), width: toPx(width), height: toPx(height), })) })); }); const InputGroupInnerEnd = createSubcomponent('div')({ modelHook: useInputGroupModel, })(({ pointerEvents, insetInlineStart, insetInlineEnd, width, height, ...elemProps }, Element) => { return (_jsx(Element, { ...mergeStyles(elemProps, inputGroupInnerStencil({ pointerEvents, insetInlineStart: insetInlineStart, insetInlineEnd: insetInlineEnd, width: toPx(width), height: toPx(height), })) })); }); export const useInputGroupInput = createElemPropsHook(useInputGroupModel)((model, ref) => { const elementRef = useForkRef(ref, model.state.inputRef); return { ref: elementRef, placeholder: '', // Make sure a placeholder attribute always exists for `:placeholder-shown` }; }); export const inputGroupInputStencil = createStencil({ vars: { paddingInlineStart: '', paddingInlineEnd: '', }, base: { name: "3576l7", styles: "box-sizing:border-box;display:flex;width:100%;" }, modifiers: { paddingInlineStart: { _: { name: "26iyng", styles: "padding-inline-start:var(--paddingInlineStart-input-group-input-9155da);" } }, paddingInlineEnd: { _: { name: "4b7ing", styles: "padding-inline-end:var(--paddingInlineEnd-input-group-input-9155da);" } } } }, "input-group-input-9155da"); const InputGroupInput = createSubcomponent(TextInput)({ modelHook: useInputGroupModel, elemPropsHook: useInputGroupInput, })(({ paddingInlineStart, paddingInlineEnd, ...elemProps }, Element) => { return (_jsx(Element, { as: Element, ...mergeStyles(elemProps, inputGroupInputStencil({ paddingInlineStart: toPx(paddingInlineStart), paddingInlineEnd: toPx(paddingInlineEnd), })) })); }); export const useClearButton = createElemPropsHook(useInputGroupModel)(model => { return { // This element does not need to be accessible via screen reader. The user can already clear // an input role: 'presentation', // A clear input button doesn't need focus. There's already keyboard keys to clear an input tabIndex: -1, icon: xSmallIcon, // "small" is needed to render correctly within a `TextInput` size: 'small', // prevent a focus change to the button. Focus should stay in the input. onMouseDown(event) { event.preventDefault(); }, onClick() { // This will clear the input's value dispatchInputEvent(model.state.inputRef.current, ''); }, }; }); /** * A clear input button. This can be a component later. */ const ClearButton = createSubcomponent(TertiaryButton)({ modelHook: useInputGroupModel, elemPropsHook: useClearButton, })((elemProps, Element) => { return _jsx(Element, { "data-part": "input-group-clear-button", ...handleCsProp(elemProps) }); }); // make sure we always use pixels if the input is a number - this is required for `calc` const toPx = (input) => { return typeof input === 'number' ? `${input}px` : input; }; // wrap an array of widths into something the browser can understand, including `calc` for multiple // values const wrapInCalc = (values) => { if (values.length === 0) { return undefined; } if (values.length === 1) { return values[0]; } return `calc(${values.map(toPx).join(' + ')})`; }; export const inputGroupStencil = createStencil({ base: { name: "3ank37", styles: "box-sizing:border-box;display:flex;position:relative;align-items:center;& :has([data-part=\"input-group-clear-button\"]){transition:opacity 300ms ease;}&:where(:has(input:placeholder-shown)) :has([data-part=\"input-group-clear-button\"]){opacity:0;pointer-events:none;}" } }, "input-group-27e30b"); /** * An `InputGroup` is a container around a {@link TextInput} with optional inner start and end * elements. The inner start and end elements are usually icons or icon buttons visually represented * inside the input. The `InputGroup` will add padding to the input so the icons/buttons display * correctly. This component uses `React.Children.map` and `React.cloneElement` from the * [React.Children](https://react.dev/reference/react/Children) API. This means all children must be * `InputGroup.*` components. Any other direct children will cause issues. You can add different * elements/components inside the {@link InputGroupInnerStart InputGroup.InnerStart} and * {@link InputGroupInnerEnd InputGroup.InnerEnd} subcomponents. * * ```tsx * <InputGroup> * <InputGroup.InnerStart as={SystemIcon} pointerEvents="none" icon={searchIcon} /> * <InputGroup.Input /> * <InputGroup.InnerEnd> * <TertiaryButton tabIndex={-1} icon={xIcon} size="small" /> * </InputGroup.InnerEnd> * </InputGroup> * ``` */ export const InputGroup = createContainer('div')({ displayName: 'InputGroup', modelHook: useInputGroupModel, subComponents: { /** * A component to show inside and at the start of the input. The input's padding will be * adjusted by the `InputGroup` to not overlap with this element. Use `width` to adjust the * width offset. The width defaults to 40px which is the correct width for icons or icon * buttons. */ InnerStart: InputGroupInnerStart, /** * The input to render. By default, this is a {@link TextInput}. Use the `as` prop to change the * component to be rendered. */ Input: InputGroupInput, /** * A component to show inside and at the end of the input. The input's padding will be adjusted * by the `InputGroup` to not overlap with this element. Use `width` to adjust the width offset. * The width defaults to 40px which is the correct width for icons or icon buttons within the * input. */ InnerEnd: InputGroupInnerEnd, /** * A component that can be added to an input group that will clear the input. It will only render * when the input has a value and will fade when a value is entered. */ ClearButton: ClearButton, }, })(({ children, ...elemProps }, Element) => { const offsetsStart = []; const offsetsEnd = []; // Collect the widths of the `InnerStart` and `InnerEnd` components into `offsetStart` and // `offsetEnd` arrays React.Children.forEach(children, child => { if (React.isValidElement(child) && child.type === InputGroupInnerStart) { const width = wrapProperty(child.props.width || system.space.x10); offsetsStart.push(width); } if (React.isValidElement(child) && child.type === InputGroupInnerEnd) { const width = wrapProperty(child.props.width || system.space.x10); offsetsEnd.push(width); } }); // keep track of the index offsets to make sure we calculate the correct position offset let indexStart = 0; let indexEnd = 0; // Loop over all the children and set the correct padding and positions const mappedChildren = React.Children.map(children, child => { if (React.isValidElement(child)) { if (child.type === InputGroupInput) { return React.cloneElement(child, { paddingInlineStart: wrapInCalc(offsetsStart), paddingInlineEnd: wrapInCalc(offsetsEnd), }); } if (child.type === InputGroupInnerStart) { const offset = wrapInCalc(offsetsStart.slice(0, indexStart)) || '0px'; indexStart++; return React.cloneElement(child, { insetInlineStart: offset, }); } if (child.type === InputGroupInnerEnd) { const offset = wrapInCalc(offsetsEnd.slice(indexEnd + 1, offsetsEnd.length)) || '0px'; indexEnd++; return React.cloneElement(child, { insetInlineEnd: offset, }); } } return child; }); return _jsx(Element, { ...mergeStyles(elemProps, inputGroupStencil()), children: mappedChildren }); });