Skip to content

Instantly share code, notes, and snippets.

@acstll
Last active August 20, 2021 09:45
Show Gist options
  • Save acstll/07f3146c61e6f0c72471cf4d7f627fee to your computer and use it in GitHub Desktop.
Save acstll/07f3146c61e6f0c72471cf4d7f627fee to your computer and use it in GitHub Desktop.
React-only Scale Select
/**
* @license
* Scale https://github.com/telekom/scale
*
* Copyright (c) 2021 Egor Kirpichev and contributors, Deutsche Telekom AG
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
.REACT-scale-select {
--font-weight: var(--scl-font-weight-bold);
--height: var(--scl-spacing-48);
--height-small: var(--scl-spacing-40);
--spacing-x: var(--scl-spacing-12);
--spacing-dropdown: var(--scl-spacing-12) var(--scl-spacing-40) 0
calc(var(--spacing-x) - 1px);
--transition: all var(--scl-motion-duration-fast)
var(--scl-motion-easing-standard);
--radius: var(--scl-radius-4);
--border: var(--scl-spacing-1) solid var(--scl-color-text-standard);
--border-error: var(--scl-spacing-2) solid var(--scl-color-background-error);
--border-color-hover: var(--scl-color-primary-hover, #f90984);
--border-color-focus: var(--scl-color-primary-hover, #f90984);
--box-shadow-focus: 0 0 0 var(--scl-spacing-2) var(--scl-color-focus);
--color-disabled: var(--scl-color-background-disabled);
--background-disabled: var(--scl-color-white);
/* input */
--transition-input: var(--transition);
--font-size-input: var(--scl-font-size-16);
/* helper-text */
--transition-helper-text: var(--transition);
--font-size-helper-text: var(--scl-font-size-12);
--line-height-helper-text: var(--scl-font-line-height-133);
--color-helper-text: var(--scl-color-blue-70);
--color-helper-text-error: var(--scl-color-text-error);
/* meta */
--spacing-y-meta: var(--scl-spacing-4);
--color-meta: var(--scl-color-text-standard);
/* icon */
--height-icon: var(--scl-spacing-24);
--color-icon: var(--scl-color-text-standard);
--color-icon-hover: var(--scl-color-primary-hover, #f90984);
--color-icon-active: var(--scl-color-primary-active, #cb0068);
--transition-icon: var(--transition);
/* label */
--color-label: var(--scl-color-grey-60);
--z-index-label: var(--scl-z-index-10);
--transition-label: var(--transition);
--font-size-label: var(--scl-font-size-16);
--font-size-label-small: var(--scl-font-size-16);
--font-weight-label: var(--scl-font-weight-medium);
--font-size-label-focus: var(--scl-font-size-10);
--font-weight-label-focus: var(--scl-font-weight-bold);
}
.REACT-scale-select {
position: relative;
}
.REACT-scale-select__helper-text {
font-weight: var(--font-weight);
}
.REACT-scale-select__control {
width: 100%;
height: var(--height);
margin: 0;
display: flex;
outline: none;
padding: var(--spacing-dropdown);
z-index: 1;
box-sizing: border-box;
transition: var(--transition-input);
font-family: inherit;
font-size: var(--font-size-input);
border-radius: var(--radius);
border: var(--border);
white-space: nowrap;
text-overflow: ellipsis;
appearance: none;
-webkit-appearance: none;
}
@-moz-document url-prefix() {
.REACT-scale-select__control {
text-indent: -2px;
}
}
.REACT-scale-select__wrapper {
position: relative;
}
.REACT-scale-select__helper-text {
transition: var(--transition-helper-text);
padding-left: var(--spacing-x);
font-size: var(--font-size-helper-text);
line-height: var(--line-height-helper-text);
color: var(--color-helper-text);
}
.REACT-scale-select__meta {
display: flex;
justify-content: space-between;
margin-top: var(--spacing-y-meta);
color: var(--color-meta);
}
.REACT-scale-select__icon {
color: var(--color-disabled);
}
.REACT-scale-select:not(.REACT-scale-select--disabled):hover .REACT-scale-select__icon {
color: var(--color-icon-hover);
}
.REACT-scale-select:not(.REACT-scale-select--disabled):active .REACT-scale-select__icon {
color: var(--color-icon-active);
}
.REACT-scale-select__control:hover {
border-color: var(--border-color-hover);
}
.REACT-scale-select:not(.REACT-scale-select--disabled) .REACT-scale-select__control:focus {
border-color: var(--border-color-focus);
}
.REACT-scale-select:not(.REACT-scale-select--disabled) .REACT-scale-select__control:focus {
box-shadow: var(--box-shadow-focus);
}
.REACT-scale-select__icon {
top: 50%;
right: var(--spacing-x);
position: absolute;
transform: translateY(-50%);
pointer-events: none;
height: var(--height-icon);
color: var(--color-icon);
transition: var(--transition-icon);
}
.REACT-scale-select__label {
top: 0;
left: 0;
color: var(--color-label);
display: flex;
z-index: var(--z-index-label);
position: absolute;
transition: var(--transition-label);
pointer-events: none;
font-size: var(--font-size-label);
transform: translate(
var(--spacing-x),
calc((var(--scl-spacing-48) - var(--font-size-label)) / 2)
);
font-weight: var(--font-weight-label);
}
.animated .REACT-scale-select__label {
transform: translate(var(--spacing-x), var(--scl-spacing-8));
font-size: var(--font-size-label-focus);
font-weight: var(--font-weight-label-focus);
line-height: var(--scl-font-variant-label-size);
}
.REACT-scale-select--status-error .REACT-scale-select__control {
border: var(--border-error);
}
.REACT-scale-select--status-error .REACT-scale-select__helper-text {
color: var(--color-helper-text-error);
}
.REACT-scale-select--size-small .REACT-scale-select__control {
height: var(--height-small);
}
.REACT-scale-select--size-small .REACT-scale-select__label {
font-size: var(--font-size-label-small);
transform: translate(
var(--spacing-x),
calc((var(--height-small) - var(--font-size-label-small)) / 2)
);
font-weight: var(--font-weight-label-small);
}
.REACT-scale-select--size-small.animated .REACT-scale-select__label {
transform: translate(var(--spacing-x), var(--scl-spacing-4));
font-size: var(--font-size-label-focus);
font-weight: var(--font-weight-label-focus);
line-height: var(--scl-font-variant-label-size);
}
.REACT-scale-select--transparent .REACT-scale-select__control {
background-color: transparent;
}
.REACT-scale-select--disabled .REACT-scale-select__label,
.REACT-scale-select--disabled .REACT-scale-select__control,
.REACT-scale-select--disabled .REACT-scale-select__helper-text {
cursor: not-allowed;
border-color: var(--color-disabled);
color: var(--color-disabled);
background: var(--background-disabled);
}
/**
* @license
* Scale https://github.com/telekom/scale
*
* Copyright (c) 2021 Egor Kirpichev and contributors, Deutsche Telekom AG
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import React, { useState, useMemo, forwardRef } from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import './ScaleSelect.css'
let i = 0
const classPrefix = (x = '') => `REACT-scale-select${x}`
const Icon = ({ size = 24, ...rest }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" {...rest}>
<g fill="currentColor">
<path d="M20.65 7.4c-.3-.3-.75-.3-1.05 0L12 15 4.4 7.4c-.3-.3-.75-.3-1.05 0s-.3.75 0 1.05L12 17.1l8.65-8.65c.3-.25.3-.75 0-1.05z" fillRule="evenodd" />
</g>
</svg>
)
Icon.propTypes = {
size: PropTypes.number
}
const ScaleSelect = forwardRef((props, ref) => {
const {
label,
defaultValue,
value,
onChange,
inputId,
inputProps,
size,
helperText,
status,
disabled,
required,
transparent,
icon,
children,
...rest
} = props
const [id] = useState(inputId == null ? `scale-select-${i++}` : inputId)
const helperTextId = `scale-select-helper-message-${i}`
const ariaDescribedByAttr = helperText ? { 'aria-describedby': helperTextId } : {}
const ariaInvalidAttr = status === 'error' ? { 'aria-invalid': true } : {}
const animated = useMemo(() => {
if (onChange != null) {
return value != null && value !== ''
}
return true
}, [value, onChange])
const classMap = classNames(
classPrefix(),
disabled && classPrefix('--disabled'),
transparent && classPrefix('--transparent'),
status && classPrefix(`--status-${status}`),
size && classPrefix(`--size-${size}`),
animated && 'animated'
)
return (
<div className={classMap}>
<label htmlFor={id} className={classPrefix('__label')}>{label}</label>
<div className={classPrefix('__wrapper')}>
<select
ref={ref}
id={id}
className={classPrefix('__control')}
value={value}
defaultValue={defaultValue}
onChange={onChange}
disabled={disabled}
required={required}
{...ariaInvalidAttr}
{...ariaDescribedByAttr}
{...inputProps}
{...rest}
>
{children}
</select>
<div className={classPrefix('__icon')}>
{icon || <Icon aria-hidden="true" />}
</div>
</div>
{helperText && (
<div
className={classPrefix('__meta')}
id={helperTextId}
aria-live="polite"
aria-relevant="additions removals"
>
<span className={classPrefix('__helper-text')}>{helperText}</span>
</div>
)}
</div>
)
})
ScaleSelect.propTypes = {
name: PropTypes.string,
label: PropTypes.string.isRequired,
value: PropTypes.any,
defaultValue: PropTypes.any,
onChange: PropTypes.func,
inputId: PropTypes.string,
inputProps: PropTypes.object,
size: PropTypes.string,
helperText: PropTypes.string,
status: PropTypes.oneOf(['error']),
disabled: PropTypes.bool,
required: PropTypes.bool,
transparent: PropTypes.bool,
icon: PropTypes.element,
children: PropTypes.any.isRequired
}
ScaleSelect.displayName = 'ScaleSelect'
export { ScaleSelect }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment