UNPKG

@kiwicom/orbit-components

Version:

Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com's products.

413 lines (412 loc) 20.4 kB
"use strict"; "use client"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; exports.__esModule = true; exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var React = _interopRequireWildcard(require("react")); var _clsx = _interopRequireDefault(require("clsx")); var _FormLabel = _interopRequireDefault(require("../FormLabel")); var _InputField = require("../InputField"); var _ChevronDown = _interopRequireDefault(require("../icons/ChevronDown")); var _AlertCircle = _interopRequireDefault(require("../icons/AlertCircle")); var _InformationCircle = _interopRequireDefault(require("../icons/InformationCircle")); var _getFieldDataState = _interopRequireDefault(require("../common/getFieldDataState")); var _ErrorFormTooltip = _interopRequireDefault(require("../ErrorFormTooltip")); var _useErrorTooltip = _interopRequireDefault(require("../ErrorFormTooltip/hooks/useErrorTooltip")); var _useRandomId = _interopRequireDefault(require("../hooks/useRandomId")); var _tailwind = require("../common/tailwind"); /** * @orbit-doc-start * README * ---------- * # Select * * To implement Select component into your project you'll need to add the import: * * ```jsx * import Select from "@kiwicom/orbit-components/lib/Select"; * ``` * * After adding import into your project you can use it simply like: * * ```jsx * <Select options={Option} /> * ``` * * ## Props * * Table below contains all types of the props available in the Select component. * * | Name | Type | Default | Description | * | :-------------- | :------------------------- | :------ | :--------------------------------------------------------------------------------------- | * | dataAttrs | `Object` | | Optional prop for passing `data-*` attributes to the `input` DOM element. | * | dataTest | `string` | | Optional prop for testing purposes. | * | disabled | `boolean` | `false` | If `true`, the Select will be disabled. | * | error | `React.Node` | | The error message for the Select. [See Functional specs](#functional-specs) | * | help | `React.Node` | | The help message for the Select. | * | id | `string` | | Adds `id` HTML attribute to an element. | * | label | `Translation` | | The label for the Select. | * | inlineLabel | `boolean` | | If true the label renders on the left side of the Select. | * | name | `string` | | The name for the Select. | * | onBlur | `event => void \| Promise` | | Function for handling onBlur event. | * | onChange | `event => void \| Promise` | | Function for handling onChange event. | * | onFocus | `event => void \| Promise` | | Function for handling onFocus event. | * | **options** | [`Option[]`](#option) | | The content of the Select, passed as array of objects. | * | placeholder | `TranslationString` | | The placeholder for the Select. | * | prefix | `React.Node` | | The prefix component for the Select. [See Functional specs](#functional-specs) | * | ref | `func` | | Prop for forwarded ref of the Select. [See Functional specs](#functional-specs) | * | required | `boolean` | `false` | If true, the label is displayed as required. | * | spaceAfter | `enum` | | Additional `margin-bottom` after component. | * | tabIndex | `string \| number` | | Specifies the tab order of an element | * | value | `string` | `""` | The value of the Select. | * | width | `string` | `100%` | Specifies width of the Select | * | customValueText | `string` | | The custom text alternative of current value. [See Functional specs](#functional-specs). | * | ariaLabel | `string` | | Optional prop for `aria-label` value. See Accessibility tab. | * | ariaLabelledby | `string` | | Optional prop for `aria-labelledby` value. See Accessibility tab. | * | ariaDescribedby | `string` | | Optional prop for `aria-describedby` value. See Accessibility tab. | * * ### enum * * | spaceAfter | * | :----------- | * | `"none"` | * | `"smallest"` | * | `"small"` | * | `"normal"` | * | `"medium"` | * | `"large"` | * | `"largest"` | * * ## Option * * Table below contains all types of the props available for object in Option array. * * | Name | Type | Description | * | :-------- | :----------------- | :-------------------------------------- | * | **value** | `string \| number` | The value of the Option. | * | label | `string` | The label for the Option. | * | key | `string` | The key of the Option. | * | disabled | `boolean` | If `true`, the Option will be disabled. | * * ## Functional specs * * - The `error` prop overwrites the `help` prop, due to higher priority. * * - When you have limited space for the `Select`, you can use `customValueText` property to pass an alternative text for the current value. For instance, when the label of the selected option is `Czech Republic (+420)`, you can pass only `+420` to this property and the original label will be visually hidden. * * - The `prefix` prop can accept any element. However, it is not recommended to pass it more than an icon (or flag). * * - `ref` can be used for example auto-focus the elements immediately after render. * * * Accessibility * ------------- * ## Accessibility * * The Select component has been designed with accessibility in mind. * * It supports keyboard navigation and includes the following properties that provide additional information to screen readers: * * | Name | Type | Description | * | :-------------- | :------- | :---------------------------------------------------------------------- | * | ariaLabel | `string` | Allows you to specify an `aria-label` attribute of the component. | * | ariaLabelledby | `string` | Allows you to specify an `aria-labelledby` attribute of the component. | * | ariaDescribedby | `string` | Allows you to specify an `aria-describedby` attribute of the component. | * * While these props are optional, we recommend including them to ensure proper accessibility of the component, especially if the `label` prop is not provided. * * These attributes help users better understand the component's purpose and context, improving the overall experience with assistive technologies. * * ### Examples * * The following code snippets show different ways to use these properties: * * ```jsx * <Select options={options} value={categoryValue} onChange={onChange} label="Category" /> * ``` * * ```jsx * <Select * options={options} * value={categoryValue} * onChange={onChange} * ariaLabel="Select passenger category" * /> * ``` * * ```jsx * <Stack> * <p id="passengers-category-label" style={{ display: "none", visibility: "hidden" }}> * Select passenger category * </p> * * <Select * options={options} * value={categoryValue} * onChange={onChange} * ariaLabelledby="passengers-category-label" * /> * </Stack> * ``` * * Using the `ariaLabel` prop enables screen readers to properly announce the Select component if the `label` prop is not provided. * * Alternatively, you can use the `ariaLabelledby` prop to reference another element that serves as a label for the Select component. The `ariaLabelledby` prop can reference multiple ids, separated by a space. The elements with those ids can be hidden, so that their text is only announced by screen readers. * * Note that if both `ariaLabel` and `ariaLabelledby` props are provided, `ariaLabelledby` takes precedence. * * For better screen reader experience, you can always complement the `label` prop with `ariaLabel` or `ariaLabelledby`: * * ```jsx * <Select * options={options} * value={languageValue} * onChange={onChange} * label="Language" * ariaLabel="Select your language" * /> * ``` * * For enhanced accessibility, it is recommended to have these label strings translated. * * The `ariaDescribedby` prop allows you to associate additional descriptive text with the Select component. This is useful for providing supplementary information that screen readers will announce after reading the component's label. * * ```jsx * <Select * options={countryOptions} * value={countryValue} * onChange={onChange} * label="Country" * ariaDescribedby="country-info" * /> * <p id="country-info" style={{ display: "none", visibility: "hidden" }}> * Select the country where you currently reside. * </p> * ``` * * When using the `help` or `error` props, their content is set as `aria-describedby` on the element. Screen readers will announce this additional information after reading the component's label (whether provided via `label`, `ariaLabel`, or `ariaLabelledby` props). * * ```jsx * <Select * options={options} * value={nationalityValue} * onChange={onChange} * label="Nationality" * error="Required field" * /> * ``` * * It would have the screen reader announce: "Nationality. Required field." * * If you provide both an `ariaDescribedby` prop and use `error` or `help`, the component automatically combines them, ensuring all descriptive content is properly announced: * * ```jsx * <p id="terms-info" style={{ display: "none", visibility: "hidden" }}> * Please review our terms and conditions. * </p> * <Select * options={termsOptions} * value={termsValue} * onChange={onChange} * label="Accept Terms" * error="This field is required" * ariaDescribedby="terms-info" * /> * ``` * * In this example, both the "terms-info" text and the error message will be announced by screen readers. The text from `ariaDescribedby` will be announced first, followed by the text from `error`. * * ### InputGroup Integration * * When using Select within an InputGroup, the `aria-describedby` association follows these rules: * * #### Example 1 * * If the InputGroup has `error`/`help` messages, these will be properly associated with all child components: * * ```jsx * <InputGroup label="Travel preferences" error="Please complete all fields"> * <Select options={countryOptions} label="Departure country" /> * <Select options={countryOptions} label="Destination country" /> * </InputGroup> * ``` * * In this example, all Select components will have the InputGroup's error message "Please complete all fields" announced by screen readers. * * #### Example 2 * * If individual Select components have their own `error`/`help` messages (and the InputGroup doesn't), only those specific components will have `aria-describedby` set: * * ```jsx * <InputGroup label="Travel preferences"> * <Select options={countryOptions} label="Departure country" /> * <Select options={countryOptions} label="Destination country" error="This field is required" /> * </InputGroup> * ``` * * In this example, only the second Select will have its error message "This field is required" announced. * * #### Example 3 * * Avoid setting `ariaDescribedby` directly on Select components when inside an InputGroup, as these values will be overwritten by the InputGroup's internal accessibility logic: * * ```jsx * <InputGroup label="Contact information"> * <Select * options={countryOptions} * label="Country" * ariaDescribedby="country-hint" // This will be overwritten * /> * <InputField label="Phone number" /> * <p id="country-hint" style={{ display: "none", visibility: "hidden" }}> * Select the country prefix of your phone number * </p> * </InputGroup> * ``` * * In this example, the `ariaDescribedby` value "country-hint" will be ignored because the InputGroup manages the accessibility associations internally. Instead, rely on the InputGroup's `error`/`help` props or the component's own `error`/`help` props. * * * @orbit-doc-end */ const Select = props => { const { label, inlineLabel, placeholder, value, disabled = false, error, help, name, onChange, onBlur, onFocus, width = "100%", options, tabIndex, id, required, dataTest, prefix, spaceAfter, customValueText, insideInputGroup, dataAttrs, ariaLabel, ariaLabelledby, ariaDescribedby, ref } = props; const filled = !(value == null || value === ""); const forID = (0, _useRandomId.default)(); const selectId = id || forID; const hasTooltip = Boolean(error || help); const { tooltipShown, tooltipShownHover, setTooltipShownHover, labelRef, iconRef, setTooltipShown, handleFocus } = (0, _useErrorTooltip.default)({ onFocus, hasTooltip }); const inputRef = React.useRef(null); const shown = tooltipShown || tooltipShownHover; const tooltipId = shown ? `${selectId}-feedback` : undefined; const ariaDescribedbyValue = insideInputGroup ? ariaDescribedby : [ariaDescribedby, tooltipId].filter(Boolean).join(" ") || undefined; return /*#__PURE__*/React.createElement("div", { className: (0, _clsx.default)("orbit-select-label-container relative block", spaceAfter && _tailwind.spaceAfterClasses[spaceAfter]), style: { width } }, /*#__PURE__*/React.createElement("label", { ref: inputRef }, label && !inlineLabel && /*#__PURE__*/React.createElement(_FormLabel.default, { error: !!error, help: !!help, labelRef: labelRef, iconRef: iconRef, onMouseEnter: () => hasTooltip ? setTooltipShownHover(true) : undefined, onMouseLeave: () => hasTooltip ? setTooltipShownHover(false) : undefined, required: required }, label), /*#__PURE__*/React.createElement("div", { ref: label ? null : labelRef, className: (0, _clsx.default)("orbit-select-container", "relative flex flex-row items-center justify-between", "h-form-box-normal box-border w-full", disabled ? "text-form-element-disabled-foreground cursor-not-allowed" : "text-form-element-filled-foreground cursor-pointer", !disabled && (error ? "[&_.orbit-input-field-fake-input]:hover:shadow-form-element-error-hover" : "[&_.orbit-input-field-fake-input]:hover:shadow-form-element-hover"), "focus-within:outline-none", "[&_.orbit-input-field-fake-input]:focus-within:outline-blue-normal [&_.orbit-input-field-fake-input]:focus-within:outline [&_.orbit-input-field-fake-input]:focus-within:outline-2") }, label && inlineLabel && /*#__PURE__*/React.createElement("div", { className: (0, _clsx.default)("pointer-events-none z-[3] flex h-full items-center justify-center", error || help ? "ps-100" : "ps-300", "[&_.orbit-form-label]:text-normal [&_.orbit-form-label]:mb-0 [&_.orbit-form-label]:inline-block [&_.orbit-form-label]:max-w-[20ch] [&_.orbit-form-label]:truncate [&_.orbit-form-label]:leading-normal"), ref: labelRef }, help && !error && /*#__PURE__*/React.createElement("span", { className: "flex", ref: iconRef }, /*#__PURE__*/React.createElement(_InformationCircle.default, { color: "info", size: "small", ariaHidden: true })), error && /*#__PURE__*/React.createElement("span", { className: "flex", ref: iconRef }, /*#__PURE__*/React.createElement(_AlertCircle.default, { color: "critical", size: "small", ariaHidden: true })), /*#__PURE__*/React.createElement(_FormLabel.default, { required: required, error: !!error, help: !!help, inlineLabel: inlineLabel }, label)), /*#__PURE__*/React.createElement("div", { className: "relative z-[3] size-full" }, prefix && /*#__PURE__*/React.createElement("div", { className: "px-300 pointer-events-none absolute top-0 z-[3] flex h-full items-center" }, prefix), customValueText && /*#__PURE__*/React.createElement("div", { className: (0, _clsx.default)(disabled && "text-form-element-disabled-foreground" || filled ? "text-form-element-filled-foreground" : "text-form-element-foreground", "text-form-element-large font-base pointer-events-none absolute inset-y-0 z-[3] flex items-center", prefix ? "ps-1200" : "ps-300") }, customValueText), /*#__PURE__*/React.createElement("select", (0, _extends2.default)({ className: (0, _clsx.default)("cursor-pointer appearance-none bg-transparent", insideInputGroup ? "focus:outline-blue-normal focus:rounded-200 focus:outline-2 focus:outline-offset-0" : "outline-none", filled ? "text-form-element-filled-foreground" : "text-form-element-foreground", "font-base text-form-element-large", "pe-1000", prefix ? "ps-1200" : "ps-300", "shrink grow basis-1/5", "size-full", "border-0", Boolean(customValueText) && "!text-transparent", "duration-fast transition-shadow ease-in-out", "rounded-200", "[&>option]:text-form-element-filled-foreground", "disabled:text-form-element-disabled-foreground disabled:cursor-not-allowed"), id: selectId, "data-test": dataTest, "data-state": (0, _getFieldDataState.default)(!!error), disabled: disabled, value: value == null ? "" : value, name: name, onFocus: handleFocus, onBlur: onBlur, onChange: onChange, tabIndex: tabIndex ? Number(tabIndex) : undefined, required: required, ref: ref, "aria-describedby": ariaDescribedbyValue, "aria-invalid": error ? true : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby }, dataAttrs), placeholder && /*#__PURE__*/React.createElement("option", { label: placeholder.toString(), value: "" }, placeholder), options.map(option => /*#__PURE__*/React.createElement("option", { key: `option-${option.key || option.value}`, value: option.value, disabled: option.disabled }, option.label)))), /*#__PURE__*/React.createElement("div", { className: (0, _clsx.default)("end-200 pointer-events-none absolute top-0 z-[3] flex h-full items-center justify-center", disabled ? "text-form-element-disabled-foreground" : "text-form-element-filled-foreground") }, /*#__PURE__*/React.createElement(_ChevronDown.default, { color: "secondary", ariaHidden: true })), /*#__PURE__*/React.createElement(_InputField.FakeInput, { disabled: disabled, error: error }))), !insideInputGroup && hasTooltip && /*#__PURE__*/React.createElement(_ErrorFormTooltip.default, { id: tooltipId, help: help, error: error, shown: shown, onShown: setTooltipShown, inlineLabel: inlineLabel, referenceElement: inlineLabel ? iconRef : inputRef })); }; var _default = exports.default = Select;