@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
240 lines (239 loc) • 11 kB
JavaScript
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 });
});