UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

125 lines (124 loc) 6.06 kB
import React from 'react'; import { createElemPropsHook, useLocalRef, dispatchInputEvent, } from '@workday/canvas-kit-react/common'; import { useComboboxModel } from './useComboboxModel'; function onlyDefined(input) { return !!input; } /** * A constrained combobox input can only offer values that are part of the provided list of `items`. * The default is an unconstrained. A constrained input should have both a form input that is hidden * from the user as well as a user input that will be visible to the user. This hook is in charge of * keeping the inputs and the model in sync with each other and working with a browser's * autocomplete, form libraries and the model. */ export const useComboboxInputConstrained = createElemPropsHook(useComboboxModel)((model, ref, { disabled, value: reactValue, onChange, name, } = {}) => { // The user element is what the user sees const { elementRef: userElementRef, localRef: userLocalRef } = useLocalRef(model.state.targetRef); // The form element is what is seen in `FormData` during for submission to the server const { elementRef: formElementRef, localRef: formLocalRef } = useLocalRef(ref); // Create React refs so we can get the current value inside an Effect without using those values // as part of the dependency array. const modelNavigationRef = React.useRef(model.navigation); modelNavigationRef.current = model.navigation; const modelStateRef = React.useRef(model.state); modelStateRef.current = model.state; // Watch the `value` prop passed from React props and update the model accordingly React.useLayoutEffect(() => { if (formLocalRef.current && typeof reactValue === 'string') { if (reactValue !== formLocalRef.current.value) { model.events.setSelectedIds(reactValue ? reactValue.split(', ') : []); } } }, [reactValue, formLocalRef, model.events]); // useImperativeHandle allows us to modify the `ref` before it is sent to the application, // but after it is defined. We can add value watches, and redirect methods here. React.useImperativeHandle(formElementRef, () => { if (formLocalRef.current && userLocalRef.current) { const formElement = formLocalRef.current; const userElement = userLocalRef.current; // Hook into the DOM `value` property of the form input element and update the model // accordingly Object.defineProperty(formElement, 'value', { get() { var _a, _b; const value = (_b = (_a = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(formElement), 'value')) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(formElement); return value; }, set(value) { if (formElement && value !== (modelStateRef.current.selectedIds === 'all' ? [] : modelStateRef.current.selectedIds).join(', ')) { model.events.setSelectedIds(value ? value.split(', ') : []); } }, }); // forward calls to `.focus()` and `.blur()` to the user input // https://github.com/testing-library/user-event/pull/1252 doesn't allow writable, but // allows reconfiguration, so we use `Object.defineProperty`. Object.defineProperty(formElement, 'focus', { configurable: true, writable: true, value: (options) => userElement.focus(options), }); Object.defineProperty(formElement, 'blur', { configurable: true, writable: true, value: () => userElement.blur(), }); return formElement; } return formLocalRef.current; }, [formLocalRef, userLocalRef, model.events]); // sync model selection state with inputs React.useLayoutEffect(() => { if (userLocalRef.current) { const userValue = model.state.items.length === 0 ? '' : (model.state.selectedIds === 'all' ? [] : model.state.selectedIds .map(id => modelNavigationRef.current.getItem(id, { state: modelStateRef.current })) .filter(onlyDefined) .map(item => item.textValue)).join(', '); if (userValue !== userLocalRef.current.value) { dispatchInputEvent(userLocalRef.current, userValue); } } if (formLocalRef.current) { const formValue = (model.state.selectedIds === 'all' ? [] : model.state.selectedIds).join(', '); if (formValue !== formLocalRef.current.value) { dispatchInputEvent(formLocalRef.current, formValue); } } }, [model.state.selectedIds, model.state.items, formLocalRef, userLocalRef]); // The props here will go to the user input. return { ref: userElementRef, form: '', value: null, onChange: (event) => { var _a; (_a = model.onFilterChange) === null || _a === void 0 ? void 0 : _a.call(model, event); return null; // Prevent further `onChange` callbacks from firing }, name: null, disabled, /** * These props should be spread onto the form input. */ formInputProps: { disabled, tabIndex: -1, 'aria-hidden': true, ref: formElementRef, onChange: (event) => { var _a; onChange === null || onChange === void 0 ? void 0 : onChange(event); (_a = model.onChange) === null || _a === void 0 ? void 0 : _a.call(model, event); }, name, }, }; });