
Halo semuanya, hari ini saya akan memberi tahu Anda bagaimana saya mengembangkan tombol untuk proyek XMars UI . Oh ya, itu agak sepele, tetapi ada sesuatu untuk diceritakan. Saya akan menghilangkan detail yang terkait dengan menambahkan komponen baru ke proyek open source. Secara lebih rinci saya akan berbicara tentang proyek dalam artikel terpisah.
Pendahuluan
XMars UI adalah salah satu proyek open source baru saya. Perpustakaan sederhana komponen UI untuk HTML / CSS dan Bereaksi. Di masa depan saya berencana untuk mendukung Vue. Sejauh ini, hanya ada tombol dan ikon :)
Proyek ini lahir sebagai ide dalam rangka Kontes Telegram, yang intinya adalah mengembangkan versi web klien. Bersama dengan seorang kolega, kami memutuskan, mengapa tidak mengambil bagian dalam hal ini. Peran dibagikan sehingga saya memiliki tata letak, dan ketika seorang rekan memahami otorisasi, saya akan terhubung untuk menulis komponen. Semuanya akan baik-baik saja, tetapi masuk ke Telegram tidak sesederhana itu. Akibatnya, kami tidak mengirim apa-apa, tetapi saya mengejar banyak hal dan ternyata - sia-sia. Tetapi seperti yang dikatakan Varlamov, proyek Anda sudah bernilai sesuatu, karena Anda menghabiskan waktu untuk itu. Sulit untuk tidak setuju dengan ini, karena jika Anda mentransfer ke jam dan uang, maka hanya menyiapkan Webpack di awal proyek tidak lagi gratis. Melihat semua aib ini, saya memutuskan untuk membuangnya ke open source. Bootstrap tunggal apa yang digunakan? Saya ingin kerangka UI saya sendiri untuk proyek saya yang lain.
Tombol pada antarmuka mungkin merupakan elemen utama yang digunakan pengguna untuk berinteraksi dengan aplikasi. Oleh karena itu, ini adalah salah satu komponen pertama dari kerangka / pustaka UI.
Dalam desain Telegram, tidak ada banyak variasi tombol:

Saya menyoroti 3 utama (default, aksen, primer), bulat dengan ikon dan hijau. Masih ada semi transparan, tetapi dikecewakan. Untuk sebagian besar, mengembangkan XMars UI Saya mencoba untuk melanjutkan dari kebutuhan, saya belum tahu di mana tombol transparan akan diperlukan.
Pengguna perpustakaan harus merasa nyaman menggunakan kelas CSS. Saya bukan penggemar sistem penamaan seperti BEM. Saya suka cara Bootstrap memberi nama kelas. Tetapi saya akan menyederhanakan sedikit lagi. Alih-alih .btn .btn-primary
, hanya .btn .primary
. .btn .primary
. Dan dalam kasus komponen Bereaksi, akan terlihat seperti ini:
<Button primary>Hey</Button>
Tombol yang sama tetapi efek riak:
<Button primary ripple>Hey</Button>
HTML / CSS
Perpustakaan UI tidak harus diikat ke kerangka UI apa pun. Di masa depan saya berencana untuk meregangkan tata letak pada komponen Vue
. Mari kita mulai dengan HTML / CSS sederhana.
Di bawah kap proyek Tailwindcss , ini adalah kerangka kerja CSS pertama-utilitas, yaitu, kerangka kerja yang menyediakan Anda dengan utilitas, bukan komponen lengkap.

Selain Tailwindcss, PostCSS digunakan untuk mixin , variabel, dan gaya bersarang.
Secara lebih rinci tentang penggunaan kerangka kerja seperti itu dan bagaimana proyek dikonfigurasikan, saya akan menceritakan dalam artikel terpisah. Pada tahap ini, cukup kita memiliki toolkit yang sangat kuat dan menggunakan komponen yang digunakan secara penuh.
<button>
memiliki sejumlah gaya default yang harus kita hapus atau ubah.

Dalam kasus Tailwindcss, tag tombol memiliki gaya ini:

Semua yang tidak perlu secara default dihapus. Anda dapat memahat apa yang Anda inginkan tanpa takut bahwa batas default akan keluar pada beberapa negara. Tapi ini peringatan, outline
standar masih perlu dipaku:

Tombol di XMars UI memiliki kelas .btn
:
<button class="btn">Button</button>
Tambahkan kelas ini ke gaya kami:
.btn { @apply text-black text-base leading-snug; @apply py-3 px-4 border-none rounded-lg; @apply inline-block cursor-pointer no-underline; &:focus { @apply outline-none; } }
Selain fakta bahwa Tailwindcs menyediakan kelas yang dapat Anda gunakan, itu menyediakan semacam mixins
. @apply
bukan SCSS atau semacam plugin untuk PostCSS. Ini adalah sintaks dari Tailwindcs sendiri. Gaya yang diterapkan secara semantik jelas dari namanya. py-3
dan px-4
saja dapat menimbulkan pertanyaan. Yang pertama adalah padding sepanjang y, yaitu, secara vertikal, yaitu padding-top: 0.75rem;
padding-bottom: 0.75rem;
. Akibatnya, px-4
secara horizontal adalah padding-right: 1rem;
, padding-left: 1rem;
.
Desain yang disediakan Telegram untuk membuatnya ringan tidak terdokumentasi dengan baik dan hal-hal seperti tombol border-radius
harus diambil dengan penggaris langsung dari gambar. Pernah bertanya-tanya apa arti sebenarnya dari nilai dalam border-radius
?

Ini secara harfiah jari-jari lingkaran yang dihasilkan di sudut. Jika pertanian kolektif, maka Anda dapat mengubah penggaris seperti yang ditunjukkan pada gambar di atas. Jadi saya menggunakan seleksi persegi panjang di Gimp.

border-radius
tombol dalam desain adalah 10px, sayangnya tidak ada kelas seperti itu dari kotak di Tailwindcss, tetapi saya secara visual memiliki rounded-lg
yang 8px
untuk ukuran font default (rem).
Inilah yang terjadi saat ini, saya melukis tombol abu-abu sehingga ujung-ujungnya terlihat:

Selanjutnya, kita perlu membuat efek pada :hover
. Kemudian para desainer dari Telegram memutuskan untuk memberi sedikit cahaya dan menunjukkan warna 0,08% dari #707579
. Saya melihat dua opsi, ambil saja warna dengan pipet atau lakukan seperti yang didokumentasikan. Opsi pertama lebih sederhana, tetapi di masa depan itu bukan yang terbaik. Faktanya adalah bahwa jika latar belakang berbeda dari putih, maka pada :hover
kita akan mendapatkan warna tertentu, kehilangan "cahaya" dan transparansi tombol. Untuk ini, lebih baik mengikuti dokumentasi dan meletakkan alpha laki-laki salurannya. Ini dapat dilakukan dengan banyak cara, misalnya menggunakan fungsi warna SCSS. Tetapi tidak ada SCSS dalam proyek ini, dan karena warna yang sama, saya tidak ingin menghubungkan plug-in apa pun ke PostCSS, kami akan membuat semuanya sangat sederhana. Di Chrome, ada colopaker yang memungkinkan Anda untuk mengubah warna menjadi sistem yang berbeda, mendorong warna HEX #707579
, menerjemahkannya ke rgba
dan mengatur saluran alfa - 0,08%.

Voila! Sesuatu dengan tajam memotret gambar saya:

Kami rgba(112, 117, 121, 0.08)
- rgba(112, 117, 121, 0.08)
.

(: hover)
Lebih membosankan dan tanpa banyak usaha, saya menambahkan sisa negara:
&:hover { background-color: var(--grey04); } &.accent { color: var(--blue01); } &.primary { @apply text-white; background-color: var(--blue01); &:hover { background-color: var(--blue02); } }
Bereaksi komponen
Awalnya, tombol dibuat untuk kontes Telegram dan tidak mungkin untuk menggunakan kerangka kerja apa pun. Saya harus menerapkan efek riak pada JS murni. Saya benar-benar ingin itu tetap demikian, tetapi saat Anda melakukan proyek sendiri, Anda harus mengorbankan sesuatu.
Komponen yang memerlukan beberapa jenis logika, seperti efek riak, akan diimplementasikan dan hanya tersedia sebagai komponen Bereaksi.
Bungkus tombol dalam komponen Bereaksi tidak sulit:
import React, { FunctionComponent } from 'react'; export interface ButtonProps { } const Button: FunctionComponent<ButtonProps> = (props) => { return ( <button className="btn">props.children</button> ); } export default Button;
Tombol ini akan ditampilkan dalam gaya yang ditentukan, tetapi sebenarnya itu tidak banyak berguna. Kita perlu mengaktifkan pengguna untuk menyesuaikan tombol, menambahkan gaya khusus, menggantung penangan acara, dan sebagainya.
Agar pengguna dapat mentransfer semua yang diperlukan, pertama-tama Anda perlu mengatasi Script, jika tidak, bahkan onClick
tidak akan memungkinkan Anda untuk mentransfer secara normal. ButtonProps
sedikit mengedit antarmuka ButtonProps
, kami memecahkan masalah:
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
setelah itu kita dapat dengan aman melakukan penghancuran props
:
<button className="btn" {...props}>props.children</button>
Penggunaan tombol yang serupa akan berlaku seperti yang diharapkan:
<Button onClick={() => alert()}>Hey</Button>

Selanjutnya, kami menambahkan gaya tombol dan kemampuan untuk mendaftarkan kelas kustom (tiba-tiba seseorang akan membutuhkan). Paket npm classnames sangat cocok untuk tujuan ini.
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, additionalClass?: string, } ... const classNames = classnames( 'btn', { primary }, { accent }, additionalClass ); ... <button className={classNames} {...props}>props.children</button>
Kelas btn
selalu disetel, tetapi primary
dan accent
hanya jika true
. Classnames menambahkan kelas jika memiliki nilai true
logis, menggunakan singkatan ES6 kita mendapatkan entri sederhana { primary }
, bukan { primary: true }
.
additionalClass
adalah sebuah string, dan jika itu kosong atau tidak terdefinisi, itu tidak memainkan peran khusus bagi kami, hanya tidak ada yang akan ditambahkan ke elemen.
Pada awalnya, saya menetapkan props
sebagai berikut:
{...omit(props, ['additionalClass', 'primary'])}
Menghilangkan semua yang tidak berlaku untuk properti dari elemen tombol, tetapi ini tidak perlu karena Bereaksi tidak akan membuat terlalu banyak.
Efek riak

Sebenarnya, ini terlihat seperti ini, tetapi diinginkan bahwa "gelombang" berbeda dari tempat klik.
Ada banyak cara untuk membuat animasi seperti itu, itu seperti lelucon tentang kotak biru .
Tetapi google, melihat contoh-contoh pada codepen, menjadi jelas bahwa dalam kebanyakan kasus, "gelombang" diwujudkan melalui elemen anak, yang mengembang dan menghilang.
Itu diposisikan di dalam tombol sesuai dengan koordinat klik. Di XMars UI , saat ini saya memutuskan untuk tidak menerapkan efek ini pada onPress
seperti UI Material, tetapi saya berencana untuk onPress
di masa mendatang. Sejauh ini, hanya onClick
.

Gambar di atas semuanya ajaib. Klik menciptakan elemen tombol anak, diposisikan secara absolut, di tengah klik dan mengembang. overflow: hidden
properti overflow: hidden
gelombang melampaui tombol. Item harus dihapus di akhir animasi.
Pertama, kami akan menentukan gaya di mana kami bisa, menggunakan Tailwindcss sebanyak mungkin:
.with-ripple { @apply relative overflow-hidden; @keyframes ripple { to { @apply opacity-0; transform: scale(2.5); } } .ripple { @apply absolute; z-index: 1; border-radius: 50%; background-color: var(--grey04); transform: scale(0); animation: ripple 0.6s linear; } &.primary { .ripple { background-color: var(--black02); } } }
Elemen yang bertanggung jawab atas efeknya akan diberi kelas .ripple
. border-radius: 50%;
sama dengan lingkaran (fillet 50% pada sudut * 2), tombol memiliki posisi relatif, tombol .ripple
memiliki posisi absolut. Animasi ini sangat sederhana, peningkatan gelombang menjadi transparan dalam 0,6 detik. Warna latar belakang sama dengan :hover
dan melipat, dua warna dan tombol "gelombang" transparan memberi kita hasil yang diinginkan. Pada tombol .primary
biru, ini tidak begitu penting dan di sana Anda dapat menggunakan warna yang tidak transparan.
Pada klik, Anda perlu membuat elemen "gelombang". Oleh karena itu, kami membuat keadaan untuk bisnis ini dan menambahkan penangan klik yang sesuai ke tombol, tetapi sedemikian rupa sehingga tidak mengganggu kustom onClick.
... const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); ... function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> );
rippleElements
- array elemen JSX, fungsi render di sini mungkin tampak berlebihan, tetapi ini lebih merupakan masalah gaya dan penggabungan untuk masa depan.
function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); }
penangan onRippleClick
yang menciptakan "gelombang". Dengan mengklik tombol, kami mengetahui ukuran tombol yang digunakan untuk memposisikan lingkaran dengan benar, setelah itu semua yang diperlukan diteruskan ke newRippleElement
yang pada gilirannya hanya menciptakan elemen div
dengan kelas ripple
, menciptakan gaya yang diperlukan untuk penentuan posisi.
Dari hal-hal utama yang patut disorot onAnimationEnd
. Kami membutuhkan acara ini untuk menghapus DOM dari elemen yang sudah digunakan.
function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); }
Sangat penting untuk tidak lupa, berikan rippleElements
saat ini ke dalam argumen, jika tidak, Anda bisa mendapatkan array dengan nilai-nilai lama, dan semuanya tidak akan berfungsi sebagaimana dimaksud.
Kode tombol lengkap:
import React, { FunctionComponent, ButtonHTMLAttributes, useState } from 'react'; import uuid from 'uuid/v4'; import classnames from 'classnames'; export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, circle?: boolean, ripple?: boolean, additionalClass?: string, } const Button: FunctionComponent<ButtonProps> = (props) => { const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); const {primary, accent, circle, ripple, additionalClass, children} = props; const classNames = classnames( 'btn', { primary }, { 'with-ripple': ripple }, { circle }, { accent }, additionalClass ); function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); } export default Button;
Hasil akhirnya dapat dilaporkan di sini.
Kesimpulan
Cukup banyak yang dihilangkan, misalnya, bagaimana proyek itu diatur, bagaimana dokumentasi ditulis, tes untuk komponen baru dalam proyek. Saya akan mencoba untuk membahas topik-topik ini dengan publikasi terpisah.
Repositori XMars UI Github